I need to add third compound filter to this shuffle.js code anyone can help?
// ES7 will have Array.prototype.includes.
function arrayIncludes(array, value) {
return array.indexOf(value) !== -1;
}
// Convert an array-like object to a real array.
function toArray(thing) {
return Array.prototype.slice.call(thing);
}
var Demo = function (element) {
this.shapes = toArray(document.querySelectorAll('.js-shapes input'));
this.colors = toArray(document.querySelectorAll('.js-colors button'));
this.shuffle = new Shuffle(element, {
easing: 'cubic-bezier(0.165, 0.840, 0.440, 1.000)', // easeOutQuart
sizer: '.the-sizer',
});
this.filters = {
shapes: [],
colors: [],
};
this._bindEventListeners();
};
/**
* Bind event listeners for when the filters change.
*/
Demo.prototype._bindEventListeners = function () {
this._onShapeChange = this._handleShapeChange.bind(this);
this._onColorChange = this._handleColorChange.bind(this);
this.shapes.forEach(function (input) {
input.addEventListener('change', this._onShapeChange);
}, this);
this.colors.forEach(function (butt) {
butt.addEventListener('click', this._onColorChange);
}, this);
};
/**
* Get the values of each checked input.
* #return {Array.<string>}
*/
Demo.prototype._getCurrentShapeFilters = function () {
return this.shapes.filter(function (input) {
return input.checked;
}).map(function (input) {
return input.value;
});
};
/**
* Get the values of each `active` button.
* #return {Array.<string>}
*/
Demo.prototype._getCurrentColorFilters = function () {
return this.colors.filter(function (butt) {
return butt.classList.contains('active');
}).map(function (butt) {
return butt.getAttribute('data-value');
});
};
/**
* A shape input check state changed, update the current filters and filte.r
*/
Demo.prototype._handleShapeChange = function () {
this.filters.shapes = this._getCurrentShapeFilters();
this.filter();
};
/**
* A color button was clicked. Update filters and display.
* #param {Event} evt Click event object.
*/
Demo.prototype._handleColorChange = function (evt) {
var butt = evt.currentTarget;
// Treat these buttons like radio buttons where only 1 can be selected.
if (butt.classList.contains('active')) {
butt.classList.remove('active');
} else {
this.colors.forEach(function (bt) {
bt.classList.remove('active');
});
butt.classList.add('active');
}
this.filters.colors = this._getCurrentColorFilters();
this.filter();
};
/**
* Filter shuffle based on the current state of filters.
*/
Demo.prototype.filter = function () {
if (this.hasActiveFilters()) {
this.shuffle.filter(this.itemPassesFilters.bind(this));
} else {
this.shuffle.filter(Shuffle.ALL_ITEMS);
}
};
/**
* If any of the arrays in the `filters` property have a length of more than zero,
* that means there is an active filter.
* #return {boolean}
*/
Demo.prototype.hasActiveFilters = function () {
return Object.keys(this.filters).some(function (key) {
return this.filters[key].length > 0;
}, this);
};
/**
* Determine whether an element passes the current filters.
* #param {Element} element Element to test.
* #return {boolean} Whether it satisfies all current filters.
*/
Demo.prototype.itemPassesFilters = function (element) {
var shapes = this.filters.shapes;
var colors = this.filters.colors;
var shape = element.getAttribute('data-shape');
var color = element.getAttribute('data-color');
// If there are active shape filters and this shape is not in that array.
if (shapes.length > 0 && !arrayIncludes(shapes, shape)) {
return false;
}
// If there are active color filters and this color is not in that array.
if (colors.length > 0 && !arrayIncludes(colors, color)) {
return false;
}
return true;
This code already filter items by shape and color I need also to filter this by group.
<div class= "filter-group col-xs-12 filter-options" style="padding:0">
<div class="col-sm-4 col-xs-12" data-group="full">
<a>
<img src="images/product/one-piece.png" alt=""/>
</a>
</div>
<div class="col-sm-4 col-xs-12" data-group="top">
<a>
<img src="images/product/top.png" alt=""/>
</a>
</div>
<div class="col-sm-4 col-xs-12 bottom" data-group="bottom">
<a>
<img src="images/product/bottom.png" alt=""/>
</a>
</div>
</div>
</div>
If some one clicked to div inside .filter-options, can we make another filter option using data-group="" attribute?
It took some time but I was able to do this. Here is the code.
// ES7 will have Array.prototype.includes.
function arrayIncludes(array, value) {
return array.indexOf(value) !== -1;
}
// Convert an array-like object to a real array.
function toArray(thing) {
return Array.prototype.slice.call(thing);
}
var Demo = function (element) {
this.shapes = toArray(document.querySelectorAll('.js-shapes input'));
this.colors = toArray(document.querySelectorAll('.js-colors button'));
this.styles = toArray(document.querySelectorAll('.filter-group div'));
this.shuffle = new Shuffle(element, {
easing: 'cubic-bezier(0.165, 0.840, 0.440, 1.000)', // easeOutQuart
sizer: '.the-sizer',
});
this.filters = {
shapes: [],
colors: [],
styles: [],
};
this._bindEventListeners();
};
/**
* Bind event listeners for when the filters change.
*/
Demo.prototype._bindEventListeners = function () {
this._onShapeChange = this._handleShapeChange.bind(this);
this._onColorChange = this._handleColorChange.bind(this);
this._onStyleChange = this._handleStyleChange.bind(this);
this.shapes.forEach(function (input) {
input.addEventListener('change', this._onShapeChange);
}, this);
this.colors.forEach(function (button) {
button.addEventListener('click', this._onColorChange);
}, this);
this.styles.forEach(function (div) {
div.addEventListener('click', this._onStyleChange);
}, this);
};
/**
* Get the values of each checked input.
* #return {Array.<string>}
*/
Demo.prototype._getCurrentShapeFilters = function () {
return this.shapes.filter(function (input) {
return input.checked;
}).map(function (input) {
return input.value;
});
};
/**
* Get the values of each `active` button.
* #return {Array.<string>}
*/
Demo.prototype._getCurrentColorFilters = function () {
return this.colors.filter(function (button) {
return button.classList.contains('active');
}).map(function (button) {
return button.getAttribute('data-value');
});
};
Demo.prototype._getCurrentStyleFilters = function () {
return this.styles.filter(function (div) {
return div.classList.contains('active');
}).map(function (div) {
return div.getAttribute('data-group');
});
};
/**
* A shape input check state changed, update the current filters and filte.r
*/
Demo.prototype._handleShapeChange = function () {
this.filters.shapes = this._getCurrentShapeFilters();
this.filter();
};
/**
* A color button was clicked. Update filters and display.
* #param {Event} evt Click event object.
*/
Demo.prototype._handleColorChange = function (evt) {
var button = evt.currentTarget;
// Treat these buttons like radio buttons where only 1 can be selected.
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
this.colors.forEach(function (btn) {
btn.classList.remove('active');
});
button.classList.add('active');
}
this.filters.colors = this._getCurrentColorFilters();
this.filter();
};
Demo.prototype._handleStyleChange = function (ev) {
var div = ev.currentTarget;
// Treat these buttons like radio buttons where only 1 can be selected.
if (div.classList.contains('active')) {
div.classList.remove('active');
} else {
this.colors.forEach(function (dv) {
//dv.classList.remove('active');
});
div.classList.add('active');
}
this.filters.styles = this._getCurrentStyleFilters();
this.filter();
};
/**
* Filter shuffle based on the current state of filters.
*/
Demo.prototype.filter = function () {
if (this.hasActiveFilters()) {
this.shuffle.filter(this.itemPassesFilters.bind(this));
} else {
this.shuffle.filter(Shuffle.ALL_ITEMS);
}
};
/**
* If any of the arrays in the `filters` property have a length of more than zero,
* that means there is an active filter.
* #return {boolean}
*/
Demo.prototype.hasActiveFilters = function () {
return Object.keys(this.filters).some(function (key) {
return this.filters[key].length > 0;
}, this);
};
/**
* Determine whether an element passes the current filters.
* #param {Element} element Element to test.
* #return {boolean} Whether it satisfies all current filters.
*/
Demo.prototype.itemPassesFilters = function (element) {
var shapes = this.filters.shapes;
var colors = this.filters.colors;
var styles = this.filters.styles;
var shape = element.getAttribute('data-shape');
var color = element.getAttribute('data-color');
var style = element.getAttribute('data-groups');
// If there are active shape filters and this shape is not in that array.
if (shapes.length > 0 && !arrayIncludes(shapes, shape)) {
return false;
}
// If there are active color filters and this color is not in that array.
if (colors.length > 0 && !arrayIncludes(colors, color)) {
return false;
}
// If there are active color filters and this color is not in that array.
if (styles.length > 0 && !arrayIncludes(styles, style)) {
return false;
}
return true;
};
Related
I have built a custom product variation panel for woocommerce that is working all good except for the removal button which is supposed to remove the selected product from the cart list and update the total price. Price is not getting updated until a new selection is made. Attached below is the Jquery code that is supposed to do all this work.
link to product: https://katie.designingdevelopment.com/product/lean-to-style-wood-storage-shed/
/**
* Single Product Page
*
*/
jQuery(document).ready(function () {
if (jQuery('body.single-product').length < 1) {
return;
}
jQuery('.product-options .option > .icon').click(function () {
jQuery(this).closest('.option').toggleClass('expanded');
});
jQuery('.product-options .option .option-info').popover({
trigger: 'hover',
placement: 'bottom',
});
jQuery('.product-options .option.upgrade .link a').click(function (evt) {
evt.preventDefault();
jQuery(this).closest('.option').toggleClass('active');
});
jQuery('.variations select').each(function (selectIndex, selectElement) {
var select = jQuery(selectElement);
buildSelectReplacements(selectElement);
select.parent().on('click', '.radioControl', function () {
var selectedValue,
currentlyChecked = jQuery(this).hasClass('checked');
jQuery(this).
parent().
parent().
find('.radioControl').
removeClass('checked');
if (!currentlyChecked) {
jQuery(this).addClass('checked');
selectedValue = jQuery(this).data('value');
} else {
selectedValue = '';
}
select.val(selectedValue);
select.find('option').each(function () {
jQuery(this).prop('checked', (jQuery(this).val() === selectedValue));
});
select.trigger('change');
});
jQuery('.reset_variations').on('mouseup', function () {
jQuery('.radioControl.checked').removeClass('checked');
});
});
// Fired when the user selects all the required dropdowns / attributes
// and a final variation is selected / shown
jQuery('.single_variation_wrap').
on('show_variation', function (event, variation) {
const price = variation.display_regular_price;
setPercentOptionsPrices(price);
});
jQuery('.variations_form').on('change', (function (e) {
e.preventDefault();
const rawFormValues = jQuery(this).serializeArray();
let description = jQuery('.woocommerce-variation-description');
description.find('ul.chosenOptionsList').remove(); //remove old list
let formValues = jQuery(`<ul class="chosenOptionsList"/>`);
let totalPrice = Number(getProductVariationPrice());
jQuery.each(rawFormValues, function (index, item) {
if ((/^options\[.+\]$/g).test(item.name) && item.value !== '') {
const parsedFormObject = parseFormValue(item);
// console.log(parsedFormObject);
const input = jQuery(
`input[name="${item.name}"][value=${JSON.stringify(item.value)}]`
);
input.val(JSON.stringify(parsedFormObject.value));
const value = parsedFormObject.value;
const optionType = parsedFormObject.optionType;
if(value.qty !== 0) {
totalPrice += value.price * value.qty;
buildChosenOptionsListMarkup(value, formValues, item, optionType);
}
}
}.bind(this));
formValues.appendTo(description);
let subTotalPrice = jQuery('.woocommerce-variation-price');
subTotalPrice.find('span.base-price, span.total').remove();
subTotalPrice.prepend(`
<span class="base-price">Base Price: </span>
`).append(`
<span class="total">Total: <span class="price">$${numberWithCommas(
totalPrice.toFixed(2))}</span></span>
`);
}));
jQuery('.variations_form').
on('woocommerce_update_variation_values', function () {
let selectValues = {};
jQuery('.variations_form select').
each(function (selectIndex, selectElement) {
var id = jQuery(this).attr('id');
selectValues[id] = jQuery(this).val();
jQuery(this).parent().find('label').remove();
jQuery(this).parent().find('div.paintSwatchOptionWrapper').remove();
//Rebuild Select Replacement Spans
buildSelectReplacements(selectElement);
//Reactivate Selected Values
jQuery(this).parent().find('span').each(function () {
if (selectValues[id] === jQuery(this).data('value')) {
jQuery(this).addClass('checked');
}
});
});
});
jQuery('.page-thumbnail').slick({
infinite: true,
slidesToShow: 1,
slidesToScroll: 1,
autoplay: false,
autoplaySpeed: 5000,
// pauseOnHover: true,
arrows: false,
asNavFor: '.img-pagination-wrapper .thumbnails',
});
jQuery('.img-pagination-wrapper .thumbnails').slick({
infinite: true,
slidesToShow: 6,
vertical: true,
slidesToScroll: 1,
// centerMode: true,
// pauseOnHover: true,
focusOnSelect: true,
asNavFor: '.page-thumbnail',
nextArrow: '.page-thumbnail-wrapper .thumbnails .right',
prevArrow: '.page-thumbnail-wrapper .thumbnails .left',
});
// handle form submit
// jQuery('.variations_form').on('submit', function (evt) {
// evt.preventDefault();
// console.log(jQuery(this).serializeArray());
// return false;
// });
});
//small utility function for capitalizing words
String.prototype.capitalize = function () {
return this.replace(/(?:^|\s)\S/g, function (a) { return a.toUpperCase(); });
};
String.prototype.replaceAll = function (search, replace) {
//if replace is not sent, return original string otherwise it will
//replace search string with 'undefined'.
if (replace === undefined) {
return this.toString();
}
return this.replace(new RegExp('[' + search + ']', 'g'), replace);
};
String.prototype.kebab = function () {
return (
this.substring('options['.length).
replace(']', '')
);
}
String.prototype.prettify = function () {
return (
this.substring('options['.length).
replace(']', '').
replaceAll('-', ' ').
capitalize()
);
};
function numberWithCommas (x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/**
*
* #param basePrice
*/
function setPercentOptionsPrices (basePrice) {
// percent prices
jQuery('.option.percent, .single-file-wrapper').each(function (index, ele) {
ele = jQuery(ele);
const amount = ele.data('amount');
if (amount) {
ele.find('.price').text('$' + parseInt(basePrice * (amount / 100)));
}
});
// unit prices
jQuery('.option.unit').each(function (index, ele) {
ele = jQuery(ele);
const amount = ele.data('amount');
if (amount) {
// ele.find('.price').text('$' + parseInt(basePrice * (amount/100)));
}
});
}
/**
* getPriceHtml
* #param optionElement
* #returns {string}
*/
function getPriceHtml (optionElement) {
let priceHtml = '';
const price = jQuery(optionElement).data('price');
let currency = jQuery(optionElement).data('currency');
currency = typeof currency !== 'undefined' && currency.length > 0
? currency
: '$';
if (typeof price !== 'undefined') {
let priceText;
if (price === 0) {
priceText = 'included';
} else {
priceText = `${currency}${price}`;
}
priceHtml = `<small><strong class="text-success price">${priceText}</strong></small>`;
}
return priceHtml;
}
/**
*
* #param container
* #param control
* #param select
*/
function handleCheckedControl (container, control, select) {
let selectedValue,
currentlyChecked = control.hasClass('checked');
container.find('.radioControl').removeClass('checked');
if (!currentlyChecked) {
control.addClass('checked');
selectedValue = control.data('value');
} else {
selectedValue = '';
}
if (typeof selectedValue === 'object') {
selectedValue = JSON.stringify(selectedValue).replace(/\\/g, '');
}
select.val(selectedValue);
select.find('option').each((_, option) => {
jQuery(option).prop('checked', (jQuery(option).val() === selectedValue));
});
select.trigger('change');
}
/**
*
* #param container
* #param control
* #param select
*/
function handleBulkCheckedControl (container, control, select) {
let selectedValue,
controlInput = control.find('input[type="number"]');
const currentInput = parseInt(controlInput.val());
//if it is checked then update the selected value
//else update the selected value
container.find('.radioControl').removeClass('checked');
container.find('input[type="number"]').each(function (_, input) {
jQuery(input).val(0);
});
select.find('option').each(function (_, option) {
if (typeof jQuery(option).val() !== 'undefined' && jQuery(option).val() !==
'') {
let value = JSON.parse(jQuery(option).val());
value.qty = 1; //default value
jQuery(option).val(JSON.stringify(value).replace(/\\/g, ''));
}
});
if (currentInput >= 1) {
control.addClass('checked');
controlInput.val(currentInput);
}
selectedValue = control.data('value');
if (typeof selectedValue === 'object') {
// control.val(selectedValue);
selectedValue = JSON.stringify(selectedValue).replace(/\\/g, '');
}
if (typeof selectedValue !== 'undefined' && selectedValue !== '') {
select.val(selectedValue);
selectedValue = JSON.parse(selectedValue);
const selectedValueName = selectedValue.name;
select.find('option').each((_, option) => {
if (jQuery(option).val() !== '') {
value = JSON.parse(jQuery(option).val());
if (value.name === selectedValueName) {
jQuery(option).prop('checked', true);
value.qty = currentInput;
jQuery(option).val(JSON.stringify(value).replace(/\\/g, ''));
}
}
});
select.trigger('change');
}
}
/**
*
* #param container
* #param optionElement
* #param select
* #param color
*/
function attachPaintSwatchControls (container, optionElement, select) {
let color = jQuery(optionElement).data('sub-options');
if (typeof color !== 'undefined' && color.length > 0) {
color = color[0];
}
let div = jQuery('<div class=\'paintSwatchOptionWrapper\' />');
container.append(div);
let control = jQuery(`
<div class='radioControl paintSwatchOption' data-value='${jQuery(
optionElement).val()}'>
<span class="circle color"
style="background-color: ${color}; border-radius: 50px; width: 50px; height: 50px; border: dashed 1px #666; display: inline-block; margin-right: .85rem; margin-bottom: .65rem;"></span>
</div>
`);
control.appendTo(div);
jQuery(control).find('.color').click(function (ele) {
handleCheckedControl(container, control, select);
});
}
/**
*
* #param container
* #param optionElement
* #param select
*/
function attachFileControls (container, optionElement, select) {
let html = `
<div class='fileOptionWrapper'>
<div class='radioControl fileOption' data-value='${jQuery(
optionElement).val()}'>
<img class="image-fluid" src='${jQuery(optionElement).val()}' />
${getPriceHtml(optionElement)}
</div>
</div>
`;
container.append(jQuery(html));
jQuery(container).find('img').on('click', () => {
handleCheckedControl(container, jQuery(container).find('radioControl'),
select);
});
}
/**
*
* #param container
* #param optionElement
* #param select
*/
function attachControls (container, optionElement, select) {
let selectOptions = jQuery(select).data('options');
if (selectOptions) {
// selectOptions = JSON.parse(selectOptions);
}
let label = jQuery('<label />');
container.append(label);
let qtyHtml = '';
let qtyFlag = false;
if (typeof selectOptions !== 'undefined') {
if (
typeof selectOptions['multiple_quantity'] !== 'undefined' &&
(selectOptions['multiple_quantity'] === 'on' ||
selectOptions['multiple_quantity'])
) {
qtyHtml = `
<span class="option-qty-wrapper">
<input type="number" placeholder="0" min="0" max="10" />
</span>
`;
qtyFlag = true;
}
}
let control = jQuery(`
<span class='shower radioControl' data-value='${jQuery(optionElement).val().replace(/\\/g, '')}'>
${jQuery(optionElement).text()}
${getPriceHtml(optionElement)}
${qtyHtml}
</span>
`);
control.appendTo(label);
if (qtyFlag) {
control.click(() => {
handleBulkCheckedControl(container, control, select);
});
} else {
control.click(() => {
handleCheckedControl(container, control, select);
});
}
}
/**
*
* #param select
* #param container
*/
function buildRadioSelectContainer (select, container) {
select.find('option').each(function (_, optionElement) {
if (jQuery(optionElement).val() === '') return;
attachControls(container, optionElement, select);
});
}
/**
*
* #param select
* #param container
*/
function buildPaintSwatchContainer (select, container) {
select.find('option').each(function (_, optionElement) {
if (jQuery(optionElement).val() === '') return;
attachPaintSwatchControls(container, optionElement, select);
});
}
/**
*
* #param select
* #param container
*/
function buildFileContainer (select, container) {
if (jQuery(select).hasClass('built')) {
return;
}
select.find('option').each(function (_, optionElement) {
if (jQuery(optionElement).val() === '') return;
attachFileControls(container, optionElement, select);
});
jQuery(select).toggleClass('built');
}
/**
*
* #param selectElement
*/
function buildSelectReplacements (selectElement) {
let select = jQuery(selectElement);
let container = select.parent().hasClass('radioSelectContainer')
? select.parent()
: jQuery('<div class=\'radioSelectContainer\' />');
select.after(
select.parent().hasClass('radioSelectContainer') ? '' : container);
container.addClass(select.attr('id'));
container.append(select);
switch (select.data('type')) {
case 'paint':
case 'color':
case 'colorpicker':
buildPaintSwatchContainer(select, container);
break;
case 'file':
case'image':
case 'icon':
buildFileContainer(select, container);
break;
default:
buildRadioSelectContainer(select, container);
break;
}
}
//given an option value in the form (item)
//the item can be either
//JSON object(name: String, price: Number)
//JSON object(name: String, price: Array<Number>)
//returns an object with name, price properties regardless
function parseFormValue (item) {
//JSON object(name: String, price: Number)
let value = jQuery.parseJSON(item.value);
let optionType = jQuery(`select[name="${item.name}"]`).length ? 'select' : 'input';
if (value.price instanceof Array) {
//JSON object(name: String, price: Array<Number>)
let price = getProductVariationPrice();
const select = jQuery(`select[name="${item.name}"]`);
//we need max specificity for inputs because they can be grouped (resulting in multiple input elements returned by checking just the name prop)
const input = jQuery(
`input[name="${item.name}"][value=${JSON.stringify(item.value)}]`
);
let amount = 0;
if (select.length) {
optionType = 'select';
amount = select.
parent().
parent().
parent().
parent().
data('amount');
} else if (input.length) {
//if the input is a checkbox the data-amount prop lives in the grandparent
let grandParent = input.parent().parent();
if (!grandParent.hasClass('checkbox')) {
//else the data-amount prop lives in the grandparent's grandparent(because all of the options are grouped)
grandParent = grandParent.parent().parent();
}
amount = grandParent.data('amount');
}
value.price = parseFloat(Number(price) * (Number(amount) / 100));
}
return {
value: value,
optionType: optionType
};
}
//simple dom search and formatting
function getProductVariationPrice () {
let price = jQuery(
'.single_variation_wrap > .single_variation > .woocommerce-variation-price > .price > .amount').
text();
price = price.replace(price.charAt(0), '').replaceAll(',', '');
return price;
}
function getProductCurrencySymbol () {
return jQuery(
'.single_variation_wrap > .single_variation > .woocommerce-variation-price > .price > .amount').
text().
charAt(0);
}
//create markup for the list
//value: object(name: String, price: String|Number)
//formValues: jQuery(`<ul />`)
function buildRemoveButton (optionType, item) {
return (jQuery(
`<button type="button" style="box-shadow: none;" class="btn btn-outline-secondary btn-sm">x</button>`
).click(function () {
if (optionType === 'input') {
jQuery(`input[name="${item.name}"][value=${JSON.stringify(item.value)}]`).prop('checked', false);
} else {
jQuery(`select[name="${item.name}"]`).
find('option').
each(function (_, option) {
jQuery(option).prop('selected', false);
jQuery(option).prop('checked', false);
let value = jQuery(option).val();
if (value !== '' && typeof value !== 'undefined') {
value = JSON.parse((jQuery(option).val()).replaceAll('/', ''));
value.qty = 0;
let valueCopy = value;
valueCopy.qty = 1;
let span = jQuery(`span[data-value='${JSON.stringify(valueCopy)}']`);
span.removeClass('checked');
span.find('input').val(0);
jQuery(option).val(JSON.stringify(value));
}
});
}
jQuery(`li[data-type="${item.name.kebab()}"]`).remove();
//remove from the preview list
//get options list markup, delete by name
//remove from form data
//set checked or selected to false for the option
})
);
}
//item: raw form value
function buildChosenOptionsListMarkup (value, formValues, item, optionType) {
let currencySymbol = getProductCurrencySymbol();
let priceMarkup = jQuery(`<li class="pb-3" data-type="${item.name.kebab()}"></li>`);
let removeButton = buildRemoveButton(optionType, item);
priceMarkup.append(removeButton);
//value.name === '' for checkboxes
// if the price is 0: display Included instead of 0
if(value.price === 0) {
if (value.name !== '') {
priceMarkup.append(jQuery(
`<span class="pl-2">${item.name.prettify()}: ${value.name} - <span class="text-success">Included</span></span>`,
));
} else {
priceMarkup.append(jQuery(
`<span class="pl-2">${item.name.prettify()}: <span class="text-success">Included</span></span>`,
));
}
} else {
if (value.name !== '') {
priceMarkup.append(jQuery(
`<span class="pl-2">${item.name.prettify()}: ${value.name} - <span class="text-success">${currencySymbol +
value.price.toFixed(2)}</span><span> x ${value.qty} (${currencySymbol}${(value.qty *
value.price).toFixed(2)})</span></span>`,
));
} else if(value.qty === 1) {
priceMarkup.append(jQuery(
`<span class="pl-2">${item.name.prettify()}: <span class="text-success">${currencySymbol +
value.price.toFixed(2)}</span></span>`,
));
} else {
priceMarkup.append(jQuery(
`<span class="pl-2">${item.name.prettify()}: <span class="text-success">${currencySymbol +
value.price.toFixed(2)}</span><span> x ${value.qty} (${currencySymbol}${(value.qty *
value.price).toFixed(2)})</span></span>`,
));
}
}
formValues.append(priceMarkup);
}
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
after upgrade Chrome browser to v. 91.0.4472.106, console show this error:
Uncaught TypeError: Cannot read property 'documentElement' of null
at Iframe.initializeIframe (Iframe.js?bust=f74493421b3bb4c9f2ea18198ca25746b5ef8a20:202)
this is content of Iframe.js:
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Module: TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe
* The editor iframe
*/
define(['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Walker',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/TYPO3',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/KeyMap'],
function (UserAgent, Walker, Typo3, Util, Dom, Event, KeyMap) {
/**
* Editor iframe constructor
*
* #param {Object} config
* #constructor
* #exports TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe
*/
var Iframe = function (config) {
Util.apply(this, config);
};
Iframe.prototype = {
/**
* Render the iframe (called by framework rendering)
*
* #param object container: the container into which to insert the iframe (that is the framework)
* #return void
*/
render: function (container) {
this.config = this.getEditor().config;
this.createIframe(container);
if (!this.config.showStatusBar) {
Dom.addClass(this.getEl(), 'noStatusBar');
}
this.initStyleChangeEventListener();
if (UserAgent.isOpera) {
var self = this;
Event.one(this.getEl(), 'load', function (event) { self.initializeIframe(); return true; })
} else {
this.initializeIframe();
}
},
/**
* Get the element to which the iframe is rendered
*/
getEl: function () {
return this.el;
},
/**
* The editor iframe may become hidden with style.display = "none" on some parent div
* This breaks the editor in Firefox: the designMode attribute needs to be reset after the style.display of the container div is reset to "block"
* In all browsers, it breaks the evaluation of the framework dimensions
*/
initStyleChangeEventListener: function () {
if (this.isNested) {
if (typeof MutationObserver === 'function') {
var self = this;
this.mutationObserver = new MutationObserver( function (mutations) { self.onNestedShowMutation(mutations); });
var options = {
attributes: true,
attributeFilter: ['class', 'style']
};
for (var i = this.nestedParentElements.sorted.length; --i >= 0;) {
var nestedElement = document.getElementById(this.nestedParentElements.sorted[i]);
this.mutationObserver.observe(nestedElement, options);
this.mutationObserver.observe(nestedElement.parentNode, options);
}
} else {
this.initMutationEventsListeners();
}
}
},
/**
* When Mutation Observer is not available, listen to DOMAttrModified events
*/
initMutationEventsListeners: function () {
var self = this;
var options = {
delay: 50
};
for (var i = this.nestedParentElements.sorted.length; --i >= 0;) {
var nestedElement = document.getElementById(this.nestedParentElements.sorted[i]);
Event.on(
nestedElement,
'DOMAttrModified',
function (event) { return self.onNestedShow(event); },
options
);
Event.on(
nestedElement.parentNode,
'DOMAttrModified',
function (event) { return self.onNestedShow(event); },
options
);
}
},
/**
* editorId should be set in config
*/
editorId: null,
/**
* Get a reference to the editor
*/
getEditor: function () {
return RTEarea[this.editorId].editor;
},
/**
* Get a reference to the toolbar
*/
getToolbar: function () {
return this.framework.toolbar;
},
/**
* Get a reference to the statusBar
*/
getStatusBar: function () {
return this.framework.statusBar;
},
/**
* Get a reference to a button
*/
getButton: function (buttonId) {
return this.getToolbar().getButton(buttonId);
},
/**
* Flag set to true when the iframe becomes usable for editing
*/
ready: false,
/**
* Create the iframe element at rendering time
*
* #param object container: the container into which to insert the iframe (that is the framework)
* #return void
*/
createIframe: function (container) {
if (this.autoEl && this.autoEl.tag) {
this.el = document.createElement(this.autoEl.tag);
if (this.autoEl.id) {
this.el.setAttribute('id', this.autoEl.id);
}
if (this.autoEl.cls) {
this.el.setAttribute('class', this.autoEl.cls);
}
if (this.autoEl.src) {
this.el.setAttribute('src', this.autoEl.src);
}
this.el = container.appendChild(this.el);
}
},
/**
* Get the content window of the iframe
*/
getIframeWindow: function () {
return this.el.contentWindow ? this.el.contentWindow : this.el.contentDocument;
},
/**
* Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
*/
initializeIframe: function () {
var self = this;
var iframe = this.getEl();
// All browsers
if (!iframe || (!iframe.contentWindow && !iframe.contentDocument)) {
window.setTimeout(function () {
self.initializeIframe();
}, 50);
// All except WebKit
} else if (iframe.contentWindow && !UserAgent.isWebKit && (!iframe.contentWindow.document || !iframe.contentWindow.document.documentElement)) {
window.setTimeout(function () {
self.initializeIframe();
}, 50);
// WebKit
} else if (UserAgent.isWebKit && (!iframe.contentDocument.documentElement || !iframe.contentDocument.body)) {
window.setTimeout(function () {
self.initializeIframe();
}, 50);
} else {
this.document = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
this.getEditor().document = this.document;
this.createHead();
// Style the document body
Dom.addClass(this.document.body, 'htmlarea-content-body');
// Start listening to things happening in the iframe
// For some unknown reason, this is too early for Opera
if (!UserAgent.isOpera) {
this.startListening();
}
// Hide the iframe
this.hide();
// Set iframe ready
this.ready = true;
/**
* #event HTMLAreaEventIframeReady
* Fires when the iframe style sheets become accessible
*/
Event.trigger(this, 'HTMLAreaEventIframeReady');
}
},
/**
* Show the iframe
*/
show: function () {
this.getEl().style.display = '';
Event.trigger(this, 'HTMLAreaEventIframeShow');
},
/**
* Hide the iframe
*/
hide: function () {
this.getEl().style.display = 'none';
},
/**
* Build the iframe document head
*/
createHead: function () {
var head = this.document.getElementsByTagName('head')[0];
if (!head) {
head = this.document.createElement('head');
this.document.documentElement.appendChild(head);
}
if (this.config.baseURL) {
var base = this.document.getElementsByTagName('base')[0];
if (!base) {
base = this.document.createElement('base');
base.href = this.config.baseURL;
head.appendChild(base);
}
this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Iframe baseURL set to: ' + base.href, 'info');
}
var link0 = this.document.getElementsByTagName('link')[0];
if (!link0) {
link0 = this.document.createElement('link');
link0.rel = 'stylesheet';
link0.type = 'text/css';
link0.href = this.config.editedContentStyle;
head.appendChild(link0);
this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Skin CSS set to: ' + link0.href, 'info');
}
var pageStyle;
for (var i = 0, n = this.config.pageStyle.length; i < n; i++) {
pageStyle = this.config.pageStyle[i];
var link = this.document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = pageStyle;
head.appendChild(link);
this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link.href, 'info');
}
},
/**
* Focus on the iframe
*/
focus: function () {
try {
if (UserAgent.isWebKit) {
this.getEl().focus();
}
this.getEl().contentWindow.focus();
} catch(e) { }
},
/**
* Flag indicating whether the framework is inside a tab or inline element that may be hidden
* Should be set in config
*/
isNested: false,
/**
* All nested tabs and inline levels in the sorting order they were applied
* Should be set in config
*/
nestedParentElements: {},
/**
* Set designMode
*
* #param boolean on: if true set designMode to on, otherwise set to off
*
* #rturn void
*/
setDesignMode: function (on) {
if (on) {
if (!UserAgent.isIE) {
if (UserAgent.isGecko) {
// In Firefox, we can't set designMode when we are in a hidden TYPO3 tab or inline element
if (!this.isNested || Typo3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
this.document.designMode = 'on';
this.setOptions();
}
} else {
this.document.designMode = 'on';
this.setOptions();
}
}
if (UserAgent.isIE || UserAgent.isWebKit) {
this.document.body.contentEditable = true;
}
} else {
if (!UserAgent.isIE) {
this.document.designMode = 'off';
}
if (UserAgent.isIE || UserAgent.isWebKit) {
this.document.body.contentEditable = false;
}
}
},
/**
* Set editing mode options (if we can... raises exception in Firefox 3)
*
* #return void
*/
setOptions: function () {
if (!UserAgent.isIE) {
try {
if (this.document.queryCommandEnabled('insertBrOnReturn')) {
this.document.execCommand('insertBrOnReturn', false, this.config.disableEnterParagraphs);
}
if (this.document.queryCommandEnabled('styleWithCSS')) {
this.document.execCommand('styleWithCSS', false, this.config.useCSS);
} else if (UserAgent.isGecko && this.document.queryCommandEnabled('useCSS')) {
this.document.execCommand('useCSS', false, !this.config.useCSS);
}
if (UserAgent.isGecko) {
if (this.document.queryCommandEnabled('enableObjectResizing')) {
this.document.execCommand('enableObjectResizing', false, !this.config.disableObjectResizing);
}
if (this.document.queryCommandEnabled('enableInlineTableEditing')) {
this.document.execCommand('enableInlineTableEditing', false, (this.config.buttons.table && this.config.buttons.table.enableHandles) ? true : false);
}
}
} catch(e) {}
}
},
/**
* Mutations handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
*/
onNestedShowMutation: function (mutations) {
for (var i = mutations.length; --i >= 0;) {
var targetId = mutations[i].target.id;
if (this.nestedParentElements.sorted.indexOf(targetId) !== -1 || this.nestedParentElements.sorted.indexOf(targetId.replace('_div', '_fields')) !== -1) {
this.onNestedShowAction();
}
}
},
/**
* Handler invoked when an hidden TYPO3 hidden nested tab or inline element is shown
*/
onNestedShow: function (event) {
Event.stopEvent(event);
var target = event.target;
var delay = event.data.delay;
var self = this;
window.setTimeout(function () {
var styleEvent = true;
// In older versions of Gecko attrName is not set and referring to it causes a non-catchable crash
if ((UserAgent.isGecko && navigator.productSub > 2007112700) || UserAgent.isOpera || UserAgent.isIE) {
styleEvent = (event.originalEvent.attrName === 'style') || (event.originalEvent.attrName === 'className') || (event.originalEvent.attrName === 'class');
}
if (styleEvent && (self.nestedParentElements.sorted.indexOf(target.id) != -1 || self.nestedParentElements.sorted.indexOf(target.id.replace('_div', '_fields')) != -1)) {
self.onNestedShowAction();
}
}, delay);
return false;
},
/**
* Take action when nested tab or inline element is shown
*/
onNestedShowAction: function () {
// Check if all container nested elements are displayed
if (Typo3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
if (this.getEditor().getMode() === 'wysiwyg') {
if (UserAgent.isGecko) {
this.setDesignMode(true);
}
Event.trigger(this, 'HTMLAreaEventIframeShow');
} else {
Event.trigger(this.framework.getTextAreaContainer(), 'HTMLAreaEventTextAreaContainerShow');
}
this.getToolbar().update();
}
},
/**
* Instance of DOM walker
*/
htmlRenderer: null,
/**
* Getter for the instance of DOM walker
*/
getHtmlRenderer: function () {
if (!this.htmlRenderer) {
this.htmlRenderer = new Walker({
keepComments: !this.config.htmlRemoveComments,
removeTags: this.config.htmlRemoveTags,
removeTagsAndContents: this.config.htmlRemoveTagsAndContents,
baseUrl: this.config.baseURL
});
}
return this.htmlRenderer;
},
/**
* Get the HTML content of the iframe
*/
getHTML: function () {
return this.getHtmlRenderer().render(this.document.body, false);
},
/**
* Start listening to things happening in the iframe
*/
startListening: function () {
var self = this;
// Create keyMap so that plugins may bind key handlers
this.keyMap = new KeyMap(this.document.documentElement, (UserAgent.isIE || UserAgent.isWebKit) ? 'keydown' : 'keypress');
// Special keys map
this.keyMap.addBinding(
{
key: [Event.DOWN, Event.UP, Event.LEFT, Event.RIGHT],
alt: false,
handler: function (event) { return self.onArrow(event); }
}
);
this.keyMap.addBinding(
{
key: Event.TAB,
ctrl: false,
alt: false,
handler: function (event) { return self.onTab(event); }
}
);
this.keyMap.addBinding(
{
key: Event.SPACE,
ctrl: true,
shift: false,
alt: false,
handler: function (event) { return self.onCtrlSpace(event); }
}
);
if (UserAgent.isGecko || UserAgent.isIE || UserAgent.isWebKit) {
this.keyMap.addBinding(
{
key: [Event.BACKSPACE, Event.DELETE],
alt: false,
handler: function (event) { return self.onBackSpace(event); }
});
}
if (!UserAgent.isIE && !this.config.disableEnterParagraphs) {
this.keyMap.addBinding(
{
key: Event.ENTER,
shift: false,
handler: function (event) { return self.onEnter(event); }
});
}
if (UserAgent.isWebKit) {
this.keyMap.addBinding(
{
key: Event.ENTER,
alt: false,
handler: function (event) { return self.onWebKitEnter(event); }
});
}
// Hot key map (on keydown for all browsers)
var hotKeys = [];
for (var key in this.config.hotKeyList) {
if (key.length === 1) {
hotKeys.push(key);
}
}
// Make hot key map available, even if empty, so that plugins may add bindings
this.hotKeyMap = new KeyMap(this.document.documentElement, 'keydown');
if (hotKeys.length > 0) {
this.hotKeyMap.addBinding({
key: hotKeys,
ctrl: true,
shift: false,
alt: false,
handler: function (event) { return self.onHotKey(event); }
});
}
Event.on(
this.document.documentElement,
(UserAgent.isIE || UserAgent.isWebKit) ? 'keydown' : 'keypress',
function (event) { return self.onAnyKey(event); }
);
Event.on(
this.document.documentElement,
'mouseup',
function (event) { return self.onMouse(event); }
);
Event.on(
this.document.documentElement,
'click',
function (event) { return self.onMouse(event); }
);
if (UserAgent.isGecko) {
Event.on(
this.document.documentElement,
'paste',
function (event) { return self.onPaste(event); }
);
}
Event.on(
this.document.documentElement,
'drop',
function (event) { return self.onDrop(event); }
);
if (UserAgent.isWebKit) {
Event.on(
this.document.body,
'dragend',
function (event) { return self.onDrop(event); }
);
}
},
/**
* Handler for other key events
*/
onAnyKey: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
/**
* #event HTMLAreaEventWordCountChange
* Fires when the word count may have changed
*/
Event.trigger(this, 'HTMLAreaEventWordCountChange', [100]);
if (!event.altKey && !(event.ctrlKey || event.metaKey)) {
var key = Event.getKey(event);
// Detect URL in non-IE browsers
if (!UserAgent.isIE && (key !== Event.ENTER || (event.shiftKey && !UserAgent.isWebKit))) {
this.getEditor().getSelection().detectURL(event);
}
// Handle option+SPACE for Mac users
if (UserAgent.isMac && key === Event.NON_BREAKING_SPACE) {
return this.onOptionSpace(key, event);
}
}
return true;
},
/**
* On any key input event, check if input is currently inhibited
*/
inhibitKeyboardInput: function (event) {
// Inhibit key events while server-based cleaning is being processed
if (this.getEditor().inhibitKeyboardInput) {
Event.stopEvent(event);
return true;
} else {
return false;
}
},
/**
* Handler for mouse events
*/
onMouse: function (event) {
// In WebKit, select the image when it is clicked
if (UserAgent.isWebKit && /^(img)$/i.test(event.target.nodeName) && event.type === 'click') {
this.getEditor().getSelection().selectNode(event.target);
}
this.getToolbar().updateLater(100);
return true;
},
/**
* Handler for paste operations in Gecko
*/
onPaste: function (event) {
// Make src and href urls absolute
if (UserAgent.isGecko) {
var self = this;
window.setTimeout(function () {
Dom.makeUrlsAbsolute(self.getEditor().document.body, self.config.baseURL, self.getHtmlRenderer());
}, 50);
}
return true;
},
/**
* Handler for drag and drop operations
*/
onDrop: function (event) {
var self = this;
// Clean up span elements added by WebKit
if (UserAgent.isWebKit) {
window.setTimeout(function () {
self.getEditor().getDomNode().cleanAppleStyleSpans(self.getEditor().document.body);
}, 50);
}
// Make src url absolute in Firefox
if (UserAgent.isGecko) {
window.setTimeout(function () {
Dom.makeUrlsAbsolute(event.target, self.config.baseURL, self.getHtmlRenderer());
}, 50);
}
this.getToolbar().updateLater(100);
return true;
},
/**
* Handler for UP, DOWN, LEFT and RIGHT arrow keys
*/
onArrow: function (event) {
this.getToolbar().updateLater(100);
return true;
},
/**
* Handler for TAB and SHIFT-TAB keys
*
* If available, BlockElements plugin will handle the TAB key
*/
onTab: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
var keyName = (event.shiftKey ? 'SHIFT-' : '') + 'TAB';
if (this.config.hotKeyList[keyName] && this.config.hotKeyList[keyName].cmd) {
var button = this.getButton(this.config.hotKeyList[keyName].cmd);
if (button) {
Event.stopEvent(event);
/**
* #event HTMLAreaEventHotkey
* Fires when the button hotkey is pressed
*/
Event.trigger(button, 'HTMLAreaEventHotkey', [keyName, event]);
return false;
}
}
return true;
},
/**
* Handler for BACKSPACE and DELETE keys
*/
onBackSpace: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
if ((!UserAgent.isIE && !event.shiftKey) || UserAgent.isIE) {
if (this.getEditor().getSelection().handleBackSpace()) {
Event.stopEvent(event);
return false;
}
}
// Update the toolbar state after some time
this.getToolbar().updateLater(200);
return true;
},
/**
* Handler for ENTER key in non-IE browsers
*/
onEnter: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
this.getEditor().getSelection().detectURL(event);
if (this.getEditor().getSelection().checkInsertParagraph()) {
Event.stopEvent(event);
// Update the toolbar state after some time
this.getToolbar().updateLater(200);
return false;
}
// Update the toolbar state after some time
this.getToolbar().updateLater(200);
return true;
},
/**
* Handler for ENTER key in WebKit browsers
*/
onWebKitEnter: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
if (event.shiftKey || this.config.disableEnterParagraphs) {
var editor = this.getEditor();
editor.getSelection().detectURL(event);
if (UserAgent.isSafari) {
var brNode = editor.document.createElement('br');
editor.getSelection().insertNode(brNode);
brNode.parentNode.normalize();
// Selection issue when an URL was detected
if (editor._unlinkOnUndo) {
brNode = brNode.parentNode.parentNode.insertBefore(brNode, brNode.parentNode.nextSibling);
}
if (!brNode.nextSibling || !/\S+/i.test(brNode.nextSibling.textContent)) {
var secondBrNode = editor.document.createElement('br');
secondBrNode = brNode.parentNode.appendChild(secondBrNode);
}
editor.getSelection().selectNode(brNode, false);
Event.stopEvent(event);
// Update the toolbar state after some time
this.getToolbar().updateLater(200);
return false;
}
}
// Update the toolbar state after some time
this.getToolbar().updateLater(200);
return true;
},
/**
* Handler for CTRL-SPACE keys
*/
onCtrlSpace: function (event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
this.getEditor().getSelection().insertHtml(' ');
Event.stopEvent(event);
return false;
},
/**
* Handler for OPTION-SPACE keys on Mac
*/
onOptionSpace: function (key, event) {
if (this.inhibitKeyboardInput(event)) {
return false;
}
this.getEditor().getSelection().insertHtml(' ');
Event.stopEvent(event);
return false;
},
/**
* Handler for configured hotkeys
*/
onHotKey: function (event) {
var key = Event.getKey(event);
if (this.inhibitKeyboardInput(event)) {
return false;
}
var hotKey = String.fromCharCode(key).toLowerCase();
/**
* #event HTMLAreaEventHotkey
* Fires when the button hotkey is pressed
*/
Event.trigger(this.getButton(this.config.hotKeyList[hotKey].cmd), 'HTMLAreaEventHotkey', [hotKey, event]);
return false;
},
/**
* Cleanup (called by framework)
*/
onBeforeDestroy: function () {
// Remove listeners on nested elements
if (this.isNested) {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
} else {
for (var i = this.nestedParentElements.sorted.length; --i >= 0;) {
var nestedElement = document.getElementById(this.nestedParentElements.sorted[i]);
Event.off(nestedElement);
Event.off(nestedElement.parentNode);
}
}
}
Event.off(this);
Event.off(this.getEl());
Event.off(this.document.body);
Event.off(this.document.documentElement);
// Cleaning references to DOM in order to avoid IE memory leaks
this.document = null;
this.el = null;
}
};
return Iframe;
});
error on line 202 by documentElement || !iframe.contentDocument.body)) {:
} else if (UserAgent.isWebKit && (!iframe.contentDocument.documentElement || !iframe.contentDocument.body)) {
How i can fix it ?
I had the same problem and found the bugfix. In old versions, the iframe source was initialized with about:blank in the rtehtmlarea extension, this is the root cause for the problem you are facing. There was an official bugfix 2 years ago - link to Github: https://github.com/FriendsOfTYPO3/rtehtmlarea/commit/a20e23445ca760ba94ed06dca05266b6e22a25fb
You can backport the fix or update the extension, then it is working as expected in the new Chrome version.
Apparently you're using a TYPO3 version prior to v8.7 with rtehtmlarea which is outdated and unsupported since quite a while. With TYPO3 v8.7 ckeditor was introduced.
Maybe this StackOverflow answer helps you a little bit. Otherwise just use Firefox.
But you're strongly advised to upgrade your TYPO3 installation to the latest version v10.4.
I installed Shuffle.js from a codepen demo with some customizations to style and figure cards. I've added in recommended js code but the shuffle function doesn't seem work.
Getting several errors:
console errors
I've tried updating script based on some past answers to this problem. I've also downloaded and added the actual js file on my site and referenced it in the script. Here's what my script looks like right now:
<script src="/v/vspfiles/assets/js/shuffle.js"></script>
<script>
'use strict';
var Shuffle = window.shuffle;
var Demo = function (element) {
this.element = element;
// Log out events.
this.addShuffleEventListeners();
this.shuffle = new Shuffle(element, {
itemSelector: '.picture-item',
sizer: element.querySelector('.my-sizer-element'),
});
this._activeFilters = [];
this.addFilterButtons();
this.addSorting();
this.addSearchFilter();
this.mode = 'exclusive';
};
Demo.prototype.toArray = function (arrayLike) {
return Array.prototype.slice.call(arrayLike);
};
Demo.prototype.toggleMode = function () {
if (this.mode === 'additive') {
this.mode = 'exclusive';
} else {
this.mode = 'additive';
}
};
/**
* Shuffle uses the CustomEvent constructor to dispatch events. You can listen
* for them like you normally would (with jQuery for example). The extra event
* data is in the `detail` property.
*/
Demo.prototype.addShuffleEventListeners = function () {
var handler = function (event) {
console.log('type: %s', event.type, 'detail:', event.detail);
};
this.element.addEventListener(Shuffle.EventType.LAYOUT, handler, false);
this.element.addEventListener(Shuffle.EventType.REMOVED, handler, false);
};
Demo.prototype.addFilterButtons = function () {
var options = document.querySelector('.filter-options');
if (!options) {
return;
}
var filterButtons = this.toArray(
options.children
);
filterButtons.forEach(function (button) {
button.addEventListener('click', this._handleFilterClick.bind(this), false);
}, this);
};
Demo.prototype._handleFilterClick = function (evt) {
var btn = evt.currentTarget;
var isActive = btn.classList.contains('active');
var btnGroup = btn.getAttribute('data-group');
// You don't need _both_ of these modes. This is only for the demo.
// For this custom 'additive' mode in the demo, clicking on filter buttons
// doesn't remove any other filters.
if (this.mode === 'additive') {
// If this button is already active, remove it from the list of filters.
if (isActive) {
this._activeFilters.splice(this._activeFilters.indexOf(btnGroup));
} else {
this._activeFilters.push(btnGroup);
}
btn.classList.toggle('active');
// Filter elements
this.shuffle.filter(this._activeFilters);
// 'exclusive' mode lets only one filter button be active at a time.
} else {
this._removeActiveClassFromChildren(btn.parentNode);
var filterGroup;
if (isActive) {
btn.classList.remove('active');
filterGroup = Shuffle.ALL_ITEMS;
} else {
btn.classList.add('active');
filterGroup = btnGroup;
}
this.shuffle.filter(filterGroup);
}
};
Demo.prototype._removeActiveClassFromChildren = function (parent) {
var children = parent.children;
for (var i = children.length - 1; i >= 0; i--) {
children[i].classList.remove('active');
}
};
Demo.prototype.addSorting = function () {
var menu = document.querySelector('.sort-options');
if (!menu) {
return;
}
menu.addEventListener('change', this._handleSortChange.bind(this));
};
Demo.prototype._handleSortChange = function (evt) {
var value = evt.target.value;
var options = {};
function sortByDate(element) {
return element.getAttribute('data-created');
}
function sortByTitle(element) {
return element.getAttribute('data-title').toLowerCase();
}
if (value === 'date-created') {
options = {
reverse: true,
by: sortByDate,
};
} else if (value === 'title') {
options = {
by: sortByTitle,
};
}
this.shuffle.sort(options);
};
// Advanced filtering
Demo.prototype.addSearchFilter = function () {
var searchInput = document.querySelector('.js-shuffle-search');
if (!searchInput) {
return;
}
searchInput.addEventListener('keyup', this._handleSearchKeyup.bind(this));
};
/**
* Filter the shuffle instance by items with a title that matches the search input.
* #param {Event} evt Event object.
*/
Demo.prototype._handleSearchKeyup = function (evt) {
var searchText = evt.target.value.toLowerCase();
this.shuffle.filter(function (element, shuffle) {
// If there is a current filter applied, ignore elements that don't match it.
if (shuffle.group !== Shuffle.ALL_ITEMS) {
// Get the item's groups.
var groups = JSON.parse(element.getAttribute('data-groups'));
var isElementInCurrentGroup = groups.indexOf(shuffle.group) !== -1;
// Only search elements in the current group
if (!isElementInCurrentGroup) {
return false;
}
}
var titleElement = element.querySelector('.picture-item__title');
var titleText = titleElement.textContent.toLowerCase().trim();
return titleText.indexOf(searchText) !== -1;
});
};
document.addEventListener('DOMContentLoaded', function () {
window.demo = new Demo(document.getElementById('grid'));
});
</script>
Any insight into what I need to remedy to get this working properly would be great. Thanks!
I'm working on my own dropdown plugin built with jquery (slim). The dropdown element itself is a div with tabindex="0".
I'd like the dropdown to work with the browser's focus state: open the dropdown when the element is focused and close it when the element loses focus. Currently I'm getting the following error:
jquery.slim.min.js:2 Uncaught RangeError: Maximum call stack size
exceeded
The code looks like this (removed parts for readability, marked problems):
var plugin = 'dropdown',
defaults = {
onOpened : function() {},
onClosed : function() {}
};
// Constructor
function Dropdown(element, options) {
this.element = element;
this.settings = $.extend({}, defaults, options);
this.init();
}
// Instance
$.extend(Dropdown.prototype, {
init: function() {
var instance = this,
$element = $(instance.element);
// Bind listeners
$element.focus(function(e) {
instance.open();
e.preventDefault();
}).focusout(function() {
instance.close();
}).mousedown(function() {
instance.toggle();
});
},
/**
* Check the state of the dropdown.
*
* #param state
* #returns {*}
*/
is: function(state) {
var $element = $(this.element);
return {
open: function() {
return $element.hasClass('dropdown--open');
},
focused: function() {
return document.activeElement === $element[0];
}
}[state].apply();
},
/**
* Open the dropdown.
*/
open: function() {
var instance = this,
$element = $(instance.element);
if (instance.is('open')) {
return;
}
$element.addClass('dropdown--open');
this.callback(this.settings.onOpened, $element);
},
/**
* Close the dropdown.
*/
close: function() {
var instance = this,
$element = $(this.element);
if ( ! instance.is('open')) {
return;
}
$element.removeClass('dropdown--open');
this.callback(this.settings.onClosed, $element);
},
/**
* Make a callback.
*
* #param callback
* #param $element
*/
callback: function(callback, $element) {
if (callback && typeof callback === 'function') {
callback($element);
}
}
});
I know I'm triggering a (endless) recursive function, but I'm unsure how to tackle this problem.
All help is appreciated!
Edit:
Fixed
;(function($, window, document) {
'use strict';
var plugin = 'dropdown',
defaults = {
onOpened : function() {},
onClosed : function() {}
};
// Constructor
function Dropdown(element, options) {
this.element = element;
this.settings = $.extend({}, defaults, options);
this.init();
}
// Instance
$.extend(Dropdown.prototype, {
init: function() {
var instance = this,
$element = $(instance.element);
// Bind listeners
$element.focus(function(e) {
console.log('opening');
instance.open();
e.preventDefault();
}).focusout(function() {
console.log('closing');
instance.close();
}).mousedown(function() {
console.log('toggling');
instance.toggle();
});
},
/**
* Check the state of the dropdown.
*
* #param state
* #returns {*}
*/
is: function(state) {
var $element = $(this.element);
return {
open: function() {
return $element.hasClass('dropdown--open');
},
empty: function() {
return $element.hasClass('dropdown--empty');
},
focused: function() {
return document.activeElement === $element[0];
}
}[state].apply();
},
/**
* Toggles the dropdown.
*/
toggle: function() {
if (this.is('open')) this.close();
else this.open();
},
/**
* Open the dropdown.
*/
open: function() {
var instance = this,
$element = $(instance.element);
if (instance.is('open')) {
return;
}
$element.addClass('dropdown--open');
this.callback(this.settings.onOpened, $element);
},
/**
* Close the dropdown.
*/
close: function() {
var instance = this,
$element = $(this.element);
if ( ! instance.is('open')) {
return;
}
$element.removeClass('dropdown--open');
this.callback(this.settings.onClosed, $element);
},
/**
* Make a callback.
*
* #param callback
* #param $element
*/
callback: function(callback, $element) {
if (callback && typeof callback === 'function') {
callback($element);
}
}
});
// Plugin definition
$.fn.dropdown = function(options, args) {
return this.each(function() {
if ( ! $ .data(this, plugin)) {
$.data(this, plugin, new Dropdown(this, options));
}
});
};
})(jQuery, window, document);
$('.dropdown').dropdown();
.dropdown {
position: relative;
display: block;
padding: .625rem .8125rem;
padding-right: 2rem;
font-size: 16px;
color: #333;
line-height: 1.125;
outline: 0;
cursor: pointer;
border: 1px solid #d9d9d9;
background-color: #fff;
}
.dropdown.dropdown--open .dropdown__menu {
display: block;
}
.dropdown__menu {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="dropdown" tabindex="0">
<span>Favorite animal</span>
<ul class="dropdown__menu" tabindex="-1">
<li class="dropdown__item">Cats</li>
<li class="dropdown__item">Dogs</li>
<li class="dropdown__item">Monkeys</li>
<li class="dropdown__item">Elephants</li>
</ul>
</div>
So. The Problems:
1) You triggered again and again focus()and focusout() if Dropdown open/close. (You already done this)
2) Use your toggle() function to close/open your dropdown
Your Problem was you have click event which checks is dropdown open, then close. But you have todo this in focusOut().
I edited your fiddle
// Bind listeners
$element.on('click', function(e) {
instance.toggle();
});
3) Update from your comment
Fiddle with changing the values
I have a JavaScript variable below which I need to pass some non-breaking spaces to, before the word.
var str = " Test";
How do I achieve this?
You can do something like this,
Creating a temporary div using $("<div/>")
Adding html content as the string using html(str)
Now to get the decoded string we can use text()
var str = " Test"
console.log($("<div/>").html(str).text());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
In pure javascript you can use a textarea,
You can create a textarea using document.createElement('textarea')
Now you can use innerHTML to set the html content
For retrieving the decoded data you can use ele.value
var str = " Test",
ele = document.createElement('textarea');
ele.innerHTML = str;
console.log(ele.value);
I have added slight variation in your plugin code
/**
* jquery.typist — animated text typing
* #author Alexander Burtsev, http://burtsev.me, 2014—2015
* #license MIT
*/
(function(factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else {
factory(jQuery);
}
}(function($) {
'use strict';
$.fn.typistInit = function() {
return this.each(function() {
if (!$(this).data('typist')) {
new Typist(this);
}
});
};
$.fn.typist = function(opts) {
return this.each(function() {
new Typist(this, opts);
});
};
$.fn.typistAdd = function(text, callback) {
return this
.typistInit()
.each(function() {
var self = $(this).data('typist');
self.queue.push({
text: text,
callback: callback
});
self.type();
});
};
$.fn.typistRemove = function(length, callback) {
length = parseInt(length) || 0;
return this
.typistInit()
.each(function() {
var self = $(this).data('typist');
self.queue.push({
remove: length,
callback: callback
});
self.type();
});
};
$.fn.typistPause = function(delay, callback) {
delay = parseInt(delay) || 0;
return this
.typistInit()
.each(function() {
var self = $(this).data('typist');
self.queue.push({
delay: delay,
callback: callback
});
self.type();
});
};
$.fn.typistStop = function() {
return this
.typistInit()
.each(function() {
var self = $(this).data('typist');
self.queue.push({
stop: true
});
self.type();
});
};
/**
* #class
* #param {HTMLElement} element
* #param {Object} [opts]
* #param {String} [opts.text=''] Text for typing
* #param {Number} [opts.speed=10] Typing speed (characters per second)
* #param {Boolean} [opts.cursor=true] Shows blinking cursor
* #param {Number} [opts.blinkSpeed=2] Blinking per second
* #param {String} [opts.typeFrom='end'] Typing from start/end of element
* #param {Object} [opts.cursorStyles] CSS properties for cursor element
*/
function Typist(element, opts) {
$.extend(this, {
speed: 10, // characters per second
text: '',
cursor: true,
blinkSpeed: 2, // blink per second
typeFrom: 'end', // 'start', 'end'
cursorStyles: {
display: 'inline-block',
fontStyle: 'normal',
margin: '-2px 2px 0 2px'
}
}, opts || {});
this._cursor = null;
this._element = $(element);
this._element.data('typist', this);
this._container = null;
this.queue = [];
this.timer = null;
this.delay = 1000 / this.speed;
this.blinkTimer = null;
this.blinkDelay = 1000 / this.blinkSpeed;
if (this.text) {
this.queue.push({
text: this.text
});
this.type();
}
}
Typist.prototype =
/** #lends Typist */
{
/**
* Adds blinking cursor into element
*/
addCursor: function() {
if (this._cursor) {
clearInterval(this.blinkTimer);
this._cursor
.stop()
.remove();
}
this._cursor = $('<span>|</span>')
.css(this.cursorStyles)
.insertAfter(this._container);
this.cursorVisible = true;
this.blinkTimer = setInterval($.proxy(function() {
this.cursorVisible = !this.cursorVisible;
this._cursor.animate({
opacity: this.cursorVisible ? 1 : 0
}, 100);
}, this), this.blinkDelay);
},
/**
* Triggers event
* #param {String} event
* #return {Typist}
*/
fire: function(event) {
this._element.trigger(event, this);
return this;
},
/**
* New line to <br> tag
* #param {String} text
* #return {String}
*/
nl2br: function(text) {
return text.replace(/\n/g, '<br>');
},
/**
* <br> tag to new line
* #param {String} html
* #return {String}
*/
br2nl: function(html) {
return html.replace(/<br.*?>/g, '\n');
},
/**
* Removes given number of characters
* #param {Number} length
* #param {Function} [callback]
*/
remove: function(length, callback) {
if (length <= 0) {
callback();
this.timer = null;
return this
.fire('end_remove.typist')
.type();
}
var text = this._container.html();
length--;
text = this.br2nl(text);
text = text.substr(0, text.length - 1);
text = this.nl2br(text);
this.timer = setTimeout($.proxy(function() {
this._container.html(text);
this.remove(length, callback);
}, this), this.delay);
},
/**
* Adds given text character by character
* #param {String|Array} text
*/
step: function(text, callback) {
if (typeof text === 'string') {
text = text.split('').map(function(v){return v.replace(' ',' ');});
}
if (!text.length) {
callback();
this.timer = null;
return this
.fire('end_type.typist')
.type();
}
var character = text.shift();
//character = $('<div>').text(character).html();
character = this.nl2br(character);
this.timer = setTimeout($.proxy(function() {
this._container.html(this._container.html() + character);
this.step(text, callback);
}, this), this.delay);
},
/**
* Stops all animations and removes cursor
* #return {[type]} [description]
*/
stop: function() {
clearInterval(this.blinkTimer);
this.blinkTimer = null;
if (this._cursor) {
this._cursor.remove();
this._cursor = null;
}
clearTimeout(this.timer);
this.timer = null;
},
/**
* Gets and invokes tasks from queue
*/
type: function() {
if (this.timer) {
return;
}
if (!this._container) {
this._container = $('<span>');
if (this.typeFrom === 'start') {
this._element.prepend(this._container);
} else {
this._element.append(this._container);
}
}
if (this.cursor) {
this.addCursor();
}
var item = this.queue.shift() || {},
callback = $.proxy(item.callback || $.noop, this);
if (item.delay) {
this
.fire('start_pause.typist')
.timer = setTimeout($.proxy(function() {
callback();
this.timer = null;
this
.fire('end_pause.typist')
.type();
}, this), item.delay);
return;
} else if (item.remove) {
this
.fire('start_remove.typist')
.remove(item.remove, callback);
return;
} else if (item.stop) {
this.stop();
return;
}
if (!item.text) {
return;
}
this
.fire('start_type.typist')
.step(item.text, callback);
}
};
}));
$('.typist')
.typist({
speed: 12,
text: 'Hello!\n'
})
.typistAdd('It working!');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p class="typist"></p>