I wanna simplify my life and display language flags inline next to input fields in admin panel.
Example:
Turn this:
into this:
I tried override
abstract class ModuleCore { public function displayFlags() }
but no effect.
Then I modify admin\themes\default\template\helpers\options\options.tpl to:
<div class="displayed_flag">
{foreach $languages as $language}
<img src="../img/l/{$language.id_lang}.jpg"
class="pointer"
alt="{$language.name}"
title="{$language.name}"
onclick="changeLanguage('{$key}', '{if isset($custom_key)}{$custom_key}{else}{$key}{/if}', {$language.id_lang}, '{$language.iso_code}');" />
{/foreach}
</div>
But still nothing.
Of course I deleted class_index.php, clear cache etc...
I am using Prestashop 1.5.5 and default theme.
You're searching for the displayFlags function inside /js/admin.js file.
Here it works on my installation with this changes:
function displayFlags(languages, defaultLanguageID, employee_cookie)
{
if ($('.translatable'))
{
$('.translatable').each(function() {
if (!$(this).find('.displayed_flag').length > 0) {
$.each(languages, function(key, language) {
if (language['id_lang'] == defaultLanguageID)
{
defaultLanguage = language;
return false;
}
});
var displayFlags = $('<div></div>')
.addClass('displayed_flag');
$.each(languages, function(key, language) {
var img = $('<img>')
.addClass('pointer')
.css('margin', '0 2px')
.attr('src', '../img/l/' + language['id_lang'] + '.jpg')
.attr('alt', language['name'])
.click(function() {
changeFormLanguage(language['id_lang'], language['iso_code'], employee_cookie);
});
displayFlags.append(img);
});
if ($(this).find('p:last-child').hasClass('clear'))
$(this).find('p:last-child').before(displayFlags);
else
$(this).append(displayFlags);
}
});
}
}
Related
I'm building a theme with Slate and I have been researching how to prevent the default function of going to the cart page after you click add to cart on a product page.
All the answers I have gotten thus far have lead to dead ends. I also tried to load Cart.js onto the theme and it didn't let me because there's some liquid code mixed in with the initialize script.
Really looking for help to prevent a theme built with Slate from automatically going to the cart page once you click add to cart. Thanks!
The redirect is probably based on a form submission, so you just need to use jQuery's preventDefault method when the form is submitted.
$('form[action^="/cart/add"]').on('submit', function(evt) {
evt.preventDefault();
//add custom cart code here
});
Found a solution using Ajaxify cart (https://help.shopify.com/themes/customization/products/add-to-cart/stay-on-product-page-when-items-added-to-cart)
To get this to work with Slate, you need to follow the instructions and make a few changes once they are all followed. Here's what I did.
I had to take all the jQuery in the script tags and place it in a new file under scripts > vendor > vendor.js
/**
* Module to ajaxify all add to cart forms on the page.
*
* Copyright (c) 2015 Caroline Schnapp (11heavens.com)
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
Shopify.AjaxifyCart = (function($) {
// Some configuration options.
// I have separated what you will never need to change from what
// you might change.
var _config = {
// What you might want to change
addToCartBtnLabel: 'Add to cart',
addedToCartBtnLabel: 'Thank you!',
addingToCartBtnLabel: 'Adding...',
soldOutBtnLabel: 'Sold Out',
howLongTillBtnReturnsToNormal: 1000, // in milliseconds.
cartCountSelector: '.cart-count, #cart-count a:first, #gocart p a, #cart .checkout em, .item-count',
cartTotalSelector: '#cart-price',
// 'aboveForm' for top of add to cart form,
// 'belowForm' for below the add to cart form, and
// 'nextButton' for next to add to cart button.
feedbackPosition: 'nextButton',
// What you will never need to change
addToCartBtnSelector: '[type="submit"]',
addToCartFormSelector: 'form[action="/cart/add"]',
shopifyAjaxAddURL: '/cart/add.js',
shopifyAjaxCartURL: '/cart.js'
};
// We need some feedback when adding an item to the cart.
// Here it is.
var _showFeedback = function(success, html, $addToCartForm) {
$('.ajaxified-cart-feedback').remove();
var feedback = '<p class="ajaxified-cart-feedback ' + success + '">' + html + '</p>';
switch (_config.feedbackPosition) {
case 'aboveForm':
$addToCartForm.before(feedback);
break;
case 'belowForm':
$addToCartForm.after(feedback);
break;
case 'nextButton':
default:
$addToCartForm.find(_config.addToCartBtnSelector).after(feedback);
break;
}
// If you use animate.css
// $('.ajaxified-cart-feedback').addClass('animated bounceInDown');
$('.ajaxified-cart-feedback').slideDown();
};
var _setText = function($button, label) {
if ($button.children().length) {
$button.children().each(function() {
if ($.trim($(this).text()) !== '') {
$(this).text(label);
}
});
}
else {
$button.val(label).text(label);
}
};
var _init = function() {
$(document).ready(function() {
$(_config.addToCartFormSelector).submit(function(e) {
e.preventDefault();
var $addToCartForm = $(this);
var $addToCartBtn = $addToCartForm.find(_config.addToCartBtnSelector);
_setText($addToCartBtn, _config.addingToCartBtnLabel);
$addToCartBtn.addClass('disabled').prop('disabled', true);
// Add to cart.
$.ajax({
url: _config.shopifyAjaxAddURL,
dataType: 'json',
type: 'post',
data: $addToCartForm.serialize(),
success: function(itemData) {
// Re-enable add to cart button.
$addToCartBtn.addClass('inverted');
_setText($addToCartBtn, _config.addedToCartBtnLabel);
_showFeedback('success','<i class="fa fa-check"></i> Added to cart! View cart or continue shopping.',$addToCartForm);
window.setTimeout(function(){
$addToCartBtn.prop('disabled', false).removeClass('disabled').removeClass('inverted');
_setText($addToCartBtn,_config.addToCartBtnLabel);
}, _config.howLongTillBtnReturnsToNormal);
// Update cart count and show cart link.
$.getJSON(_config.shopifyAjaxCartURL, function(cart) {
if (_config.cartCountSelector && $(_config.cartCountSelector).size()) {
var value = $(_config.cartCountSelector).html() || '0';
$(_config.cartCountSelector).html(value.replace(/[0-9]+/,cart.item_count)).removeClass('hidden-count');
}
if (_config.cartTotalSelector && $(_config.cartTotalSelector).size()) {
if (typeof Currency !== 'undefined' && typeof Currency.moneyFormats !== 'undefined') {
var newCurrency = '';
if ($('[name="currencies"]').size()) {
newCurrency = $('[name="currencies"]').val();
}
else if ($('#currencies span.selected').size()) {
newCurrency = $('#currencies span.selected').attr('data-currency');
}
if (newCurrency) {
$(_config.cartTotalSelector).html('<span class=money>' + Shopify.formatMoney(Currency.convert(cart.total_price, "{{ shop.currency }}", newCurrency), Currency.money_format[newCurrency]) + '</span>');
}
else {
$(_config.cartTotalSelector).html(Shopify.formatMoney(cart.total_price, "{{ shop.money_format | remove: "'" | remove: '"' }}"));
}
}
else {
$(_config.cartTotalSelector).html(Shopify.formatMoney(cart.total_price, "{{ shop.money_format | remove: "'" | remove: '"' }}"));
}
};
});
},
error: function(XMLHttpRequest) {
var response = eval('(' + XMLHttpRequest.responseText + ')');
response = response.description;
if (response.slice(0,4) === 'All ') {
_showFeedback('error', response.replace('All 1 ', 'All '), $addToCartForm);
$addToCartBtn.prop('disabled', false);
_setText($addToCartBtn, _config.soldOutBtnLabel);
$addToCartBtn.prop('disabled',true);
}
else {
_showFeedback('error', '<i class="fa fa-warning"></i> ' + response, $addToCartForm);
$addToCartBtn.prop('disabled', false).removeClass('disabled');
_setText($addToCartBtn, _config.addToCartBtnLabel);
}
}
});
return false;
});
});
};
return {
init: function(params) {
// Configuration
params = params || {};
// Merging with defaults.
$.extend(_config, params);
// Action
$(function() {
_init();
});
},
getConfig: function() {
return _config;
}
}
})(jQuery);
Shopify.AjaxifyCart.init();
Next I made sure that this file was being called to the main vendor.js file using this code in scripts > vendor.js
/*!
* ajaxify-cart.js
*/
// =require vendor/ajaxify-cart.js
Last thing I had to do was edit out the liquid markup that was in the ajaxify-cart.js file. Since it is a .js file the liquid markup was making it malfunction. Here's the lines where I replaced liquid markup:
if (newCurrency) {
$(_config.cartTotalSelector).html('<span class=money>' + Shopify.formatMoney(Currency.convert(cart.total_price, "CAD", newCurrency), Currency.money_format[newCurrency]) + '</span>');
}
else {
$(_config.cartTotalSelector).html(Shopify.formatMoney(cart.total_price));
}
}
else {
$(_config.cartTotalSelector).html(Shopify.formatMoney(cart.total_price));
}
It's a bit hacky but so far it is working with my slate theme.
I'm open to suggestions for improvement. Thanks.
I created a custom select2 input element for Vue 2.
My question is: why is
<select2 v-model="vacancy.staff_member_id" #input="update(vacancy)"></select2>
working, but
<select2 v-model="vacancy.staff_member_id" #change="update(vacancy)"></select2>
not?
Since normal <input> elements in Vue have a #change handler, it would be nice if my custom select2 input has the same.
Some information on my custom element:
The purpose of this element is to not render all <option> elements but only those needed, because we have many select2 inputs on one page and many options inside a select2 input, causing page load to become slow.
This solution makes it much faster.
Vue.component('select2', {
props: ['options', 'value', 'placeholder', 'config', 'disabled'],
template: '<select><slot></slot></select>',
data: function() {
return {
newValue: null
}
},
mounted: function () {
var vm = this;
$.fn.select2.amd.require([
'select2/data/array',
'select2/utils'
], function (ArrayData, Utils) {
function CustomData ($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
if (params.term && params.term !== '') {
// search for term
var results;
var termLC = params.term.toLowerCase();
var length = termLC.length;
if (length < 3) {
// if only one or two characters, search for words in string that start with it
// the string starts with the term, or the term is used directly after a space
results = _.filter(vm.options, function(option){
return option.text.substr(0,length).toLowerCase() === termLC ||
_.includes(option.text.toLowerCase(), ' '+termLC.substr(0,2));
});
}
if (length > 2 || results.length < 2) {
// if more than two characters, or the previous search give less then 2 results
// look anywhere in the texts
results = _.filter(vm.options, function(option){
return _.includes(option.text.toLowerCase(), termLC);
});
}
callback({results: results});
} else {
callback({results: vm.options}); // no search input -> return all options to scroll through
}
};
var config = {
// dataAdapter for displaying all options when opening the input
// and for filtering when the user starts typing
dataAdapter: CustomData,
// only the selected value, needed for un-opened display
// we are not using all options because that might become slow if we have many select2 inputs
data:_.filter(vm.options, function(option){return option.id === parseInt(vm.value);}),
placeholder:vm.placeholder
};
for (var attr in vm.config) {
config[attr] = vm.config[attr];
}
if (vm.disabled) {
config.disabled = vm.disabled;
}
if (vm.placeholder && vm.placeholder !== '') {
$(vm.$el).append('<option></option>');
}
$(vm.$el)
// init select2
.select2(config)
.val(vm.value)
.trigger('change')
// prevent dropdown to open when clicking the unselect-cross
.on("select2:unselecting", function (e) {
$(this).val('').trigger('change');
e.preventDefault();
})
// emit event on change.
.on('change', function () {
var newValue = $(this).val();
if (newValue !== null) {
Vue.nextTick(function(){
vm.$emit('input', newValue);
});
}
})
});
},
watch: {
value: function (value, value2) {
if (value === null) return;
var isChanged = false;
if (_.isArray(value)) {
if (value.length !== value2.length) {
isChanged = true;
} else {
for (var i=0; i<value.length; i++) {
if (value[i] !== value2[i]) {
isChanged = true;
}
}
}
} else {
if (value !== value2) {
isChanged = true;
}
}
if (isChanged) {
var selectOptions = $(this.$el).find('option');
var selectOptionsIds = _.map(selectOptions, 'value');
if (! _.includes(selectOptionsIds, value)) {
var missingOption = _.find(this.options, {id: value});
var missingText = _.find(this.options, function(opt){
return opt.id === parseInt(value);
}).text;
$(this.$el).append('<option value='+value+'>'+missingText+'</option>');
}
// update value only if there is a real change
// (without checking isSame, we enter a loop)
$(this.$el).val(value).trigger('change');
}
}
},
destroyed: function () {
$(this.$el).off().select2('destroy')
}
The reason is because you are listening to events on a component <select2> and not an actual DOM node. Events on components will refer to the custom events emitted from within, unless you use the .native modifier.
Custom events are different from native DOM events: they do not bubble up the DOM tree, and cannot be captured unless you use the .native modifier. From the docs:
Note that Vue’s event system is separate from the browser’s EventTarget API. Though they work similarly, $on and $emit are not aliases for addEventListener and dispatchEvent.
If you look into the code you posted, you will see this at the end of it:
Vue.nextTick(function(){
vm.$emit('input', newValue);
});
This code emits a custom event input in the VueJS event namespace, and is not a native DOM event. This event will be captured by v-on:input or #input on your <select2> VueJS component. Conversely, since no change event is emitted using vm.$emit, the binding v-on:change will never be fired and hence the non-action you have observed.
Terry pointed out the reason, but actually you can simply pass your update event to the child component as a prop. Check demo below.
Vue.component('select2', {
template: '<select #change="change"><option value="value1">Value 1</option><option value="value2">Value 2</option></select>',
props: [ 'change' ]
})
new Vue({
el: '#app',
methods: {
onChange() {
console.log('on change');
}
}
});
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
<div id="app">
<div>
<p>custom select</p>
<select2 :change="onChange"></select2>
</div>
<div>
<p>default select</p>
<select #change="onChange">
<option value="value1">Value 1</option>
<option value="value2">Value 2</option>
</select>
</div>
</div>
fiddle
I have created a Vue component call imageUpload and pass property as v-model
<image-upload v-model="form.image"></image-upload>
and within imgeUpload component
I have this code
<input type="file" accept="images/*" class="file-input" #change="upload">
upload:(e)=>{
const files = e.target.files;
if(files && files.length > 0){
console.log(files[0])
this.$emit('input',files[0])
}
}
and I received
Uncaught TypeError: _this.$emit is not a function
Thanks
Do not define your method with a fat arrow. Use:
upload: function(e){
const files = e.target.files;
if(files && files.length > 0){
console.log(files[0])
this.$emit('input',files[0])
}
}
When you define your method with a fat arrow, you capture the lexical scope, which means this will be pointing to the containing scope (often window, or undefined), and not Vue.
This error surfaces if $emit is not on the current context/reference of this, perhaps when you're in the then or catch methods of a promise. In that case, capture a reference to this outside of the promise to then use so the call to $emit is successful.
<script type="text/javascript">
var Actions = Vue.component('action-history-component', {
template: '#action-history-component',
props: ['accrual'],
methods: {
deleteAction: function(accrualActionId) {
var self = this;
axios.post('/graphql',
{
query:
"mutation($accrualId: ID!, $accrualActionId: String!) { deleteAccrualAction(accrualId: $accrualId, accrualActionId: $accrualActionId) { accrualId accrualRate name startingDate lastModified hourlyRate isHeart isArchived minHours maxHours rows { rowId currentAccrual accrualDate hoursUsed actions { actionDate amount note dateCreated } } actions {accrualActionId accrualAction actionDate amount note dateCreated }} }",
variables: {
accrualId: this.accrual.accrualId,
accrualActionId: accrualActionId
}
}).then(function(res) {
if (res.data.errors) {
console.log(res);
alert('errors');
} else {
self.$emit('accrualUpdated', res.data.data.deleteAccrualAction);
}
}).catch(function(err) {
console.log(err);
});
}
}
});
You can write the method in short using upload(e) { instead of upload:(e)=>{ to make this point to the component.
Here is the full example
watch: {
upload(e) {
const files = e.target.files;
if(files && files.length > 0) {
console.log(files[0]);
this.$emit('input',files[0]);
}
}
}
I need to find a reliable solution to making the two frameworks play nicely.
Using materialize-css, their select element uses jquery to apply the value change. However that then does not trigger aurelia in seeing the change. Using the technique of...
$("select")
.change((eventObject: JQueryEventObject) => {
fireEvent(eventObject.target, "change");
});
I can fire an event aurelia sees, however, aurelia then cause the event to be triggered again while it's updating it's bindings and I end up in an infinite loop.... Stack Overflow :D
Whats the most reliable way of getting the two to play together in this respect?
I have worked with materialize-css + aurelia for a while and I can confirm that the select element from materialize is quite problematic.
I just wanted to share one of my solutions here in case anyone wants some additional examples. Ashley's is probably cleaner in this case. Mine uses a bindable for the options instead of a slot.
Other than that the basic idea is the same (using a guard variable and a micro task).
One lesson I learned in dealing with 3rd party plugins and two-way data binding is that it helps to make a more clear, distinct separation between handling changes that originate from the binding target (the select element on the DOM) and changes that originate from the binding source (e.g. the ViewModel of the page containing the element).
I tend to use change handlers with names like onValueChangedByBindingSource and onValueChangedByBindingTarget to deal with the different ways of syncing the ViewModel with the DOM in a way that results in less confusing code.
Example: https://gist.run?id=6ee17e333cd89dc17ac62355a4b31ea9
src/material-select.html
<template>
<div class="input-field">
<select value.two-way="value" id="material-select">
<option repeat.for="option of options" model.bind="option">
${option.displayName}
</option>
</select>
</div>
</template>
src/material-select.ts
import {
customElement,
bindable,
bindingMode,
TaskQueue,
Disposable,
BindingEngine,
inject,
DOM
} from "aurelia-framework";
#customElement("material-select")
#inject(DOM.Element, TaskQueue, BindingEngine)
export class MaterialSelect {
public element: HTMLElement;
public selectElement: HTMLSelectElement;
#bindable({ defaultBindingMode: bindingMode.twoWay })
public value: { name: string, value: number };
#bindable({ defaultBindingMode: bindingMode.oneWay })
public options: { displayName: string }[];
constructor(
element: Element,
private tq: TaskQueue,
private bindingEngine: BindingEngine
) {
this.element = element;
}
private subscription: Disposable;
public isAttached: boolean = false;
public attached(): void {
this.selectElement = <HTMLSelectElement>this.element.querySelector("select");
this.isAttached = true;
$(this.selectElement).material_select();
$(this.selectElement).on("change", this.handleChangeFromNativeSelect);
this.subscription = this.bindingEngine.collectionObserver(this.options).subscribe(() => {
$(this.selectElement).material_select();
});
}
public detached(): void {
this.isAttached = false;
$(this.selectElement).off("change", this.handleChangeFromNativeSelect);
$(this.selectElement).material_select("destroy");
this.subscription.dispose();
}
private valueChanged(newValue, oldValue): void {
this.tq.queueMicroTask(() => {
this.handleChangeFromViewModel(newValue);
});
}
private _suspendUpdate = false;
private handleChangeFromNativeSelect = () => {
if (!this._suspendUpdate) {
this._suspendUpdate = true;
let event = new CustomEvent("change", {
bubbles: true
});
this.selectElement.dispatchEvent(event)
this._suspendUpdate = false;
}
}
private handleChangeFromViewModel = (newValue) => {
if (!this._suspendUpdate) {
$(this.selectElement).material_select();
}
}
}
EDIT
How about a custom attribute?
Gist: https://gist.run?id=b895966489502cc4927570c0beed3123
src/app.html
<template>
<div class="container">
<div class="row"></div>
<div class="row">
<div class="col s12">
<div class="input-element" style="position: relative;">
<select md-select value.two-way="currentOption">
<option repeat.for="option of options" model.bind="option">${option.displayName}</option>
</select>
<label>Selected: ${currentOption.displayName}</label>
</div>
</div>
</div>
</div>
</template>
src/app.ts
export class App {
public value: string;
public options: {displayName: string}[];
constructor() {
this.options = new Array<any>();
this.options.push({ displayName: "Option 1" });
this.options.push({ displayName: "Option 2" });
this.options.push({ displayName: "Option 3" });
this.options.push({ displayName: "Option 4" });
}
public attached(): void {
this.value = this.options[1];
}
}
src/md-select.ts
import {
customAttribute,
bindable,
bindingMode,
TaskQueue,
Disposable,
BindingEngine,
DOM,
inject
} from "aurelia-framework";
#inject(DOM.Element, TaskQueue, BindingEngine)
#customAttribute("md-select")
export class MdSelect {
public selectElement: HTMLSelectElement;
#bindable({ defaultBindingMode: bindingMode.twoWay })
public value;
constructor(element: Element, private tq: TaskQueue) {
this.selectElement = element;
}
public attached(): void {
$(this.selectElement).material_select();
$(this.selectElement).on("change", this.handleChangeFromNativeSelect);
}
public detached(): void {
$(this.selectElement).off("change", this.handleChangeFromNativeSelect);
$(this.selectElement).material_select("destroy");
}
private valueChanged(newValue, oldValue): void {
this.tq.queueMicroTask(() => {
this.handleChangeFromViewModel(newValue);
});
}
private _suspendUpdate = false;
private handleChangeFromNativeSelect = () => {
if (!this._suspendUpdate) {
this._suspendUpdate = true;
const event = new CustomEvent("change", { bubbles: true });
this.selectElement.dispatchEvent(event)
this.tq.queueMicroTask(() => this._suspendUpdate = false);
}
}
private handleChangeFromViewModel = (newValue) => {
if (!this._suspendUpdate) {
$(this.selectElement).material_select();
}
}
}
Ok, I spent entirely too long getting this one answered the way I wanted, but more on that later. The actual answer to stop the infinite loop is fairly simple, so let's look at it first. You need to have a guard property, and you'll need to use Aurelia's TaskQueue to help unset the guard property.
Your code will look a little something like this:
$(this.selectElement).change(evt => {
if(!this.guard) {
this.guard = true;
const changeEvent = new Event('change');
this.selectElement.dispatchEvent(changeEvent);
this.taskQueue.queueMicroTask(() => this.guard = false);
}
});
Notice that I'm using queueing up a microtask to unset the guard. This makes sure that everything will work the way you want.
Now that we've got that out of the way, let's look at a gist I created here. In this gist I created a custom element to wrap the Materialize select functionality. While creating this, I learned that select elements and content projection via slot elements don't go together. So you'll see in the code that I have to do some coding gymnastics to move the option elements over from a dummy div element into the select element. I'm going to file an issue so we can look in to this and see if this is a bug in the framework or simply a limitation of the browser.
Normally, I would highly recommend creating a custom element to wrap this functionality. Given the code I had to write to shuffle nodes around, I can't say that I highly recommend creating a custom element. I just really recommend it in this case.
But anyways, there you go!
Have been using simpleCart for a while and it works great and i know it has a search function in it but it only seems to search certain elements my question is this, i would like to know if it can be set up to search within the contents of html files? as all the items are stored in Html pages for simple cataloging.
try this: JS
function filter(e){
search = e.value.toLowerCase();
console.log(e.value)
document.querySelectorAll('.item_name').forEach(function(row){
text = row.innerText.toLowerCase();
if(text.match(search)){
row.style.display="block"
} else {
row.style.display="none"
}
// need to count hidden items and if all instances of .kb-items are hidden, then hide .kb-item
var countHidden = document.querySelectorAll(".item_name[style='display: none;']").length;
console.log(countHidden);
})
}
function detectParent()
{
var collectionref=document.querySelectorAll(".simpleCart_shelfItem");
collectionref.forEach(group=>{
var itemcollection=group.getElementsByClassName("item_name");
var hidecounter=0;
for(var j=0;j<itemcollection.length;j++)
{
if(itemcollection[j].style.display==='none')
{
hidecounter++;
}
}
if(hidecounter===itemcollection.length)
{
group.style.display="none";
}else{
group.style.display="block";
}
});
}
And HTML:
<input type="text" onkeyup="filter(this);detectParent();"/>