File: /home/lucendi/www/wp-content/themes/mesmerize/customizer/kirki/assets/js/controls/repeater.js
/*jshint -W065 */
var RepeaterRow = function (rowIndex, container, label) {
'use strict';
var self = this;
this.rowIndex = rowIndex;
this.container = container;
this.label = label;
this.header = this.container.find('.repeater-row-header'),
this.header.on('click', function () {
self.toggleMinimize();
});
this.container.on('click', '.repeater-row-remove', function () {
self.remove();
});
this.header.on('mousedown', function () {
self.container.trigger('row:start-dragging');
});
this.container.on('keyup change', 'input, select, textarea', function (e) {
self.container.trigger('row:update', [self.rowIndex, jQuery(e.target).data('field'), e.target]);
});
this.setRowIndex = function (rowIndex) {
this.rowIndex = rowIndex;
this.container.attr('data-row', rowIndex);
this.container.data('row', rowIndex);
this.updateLabel();
};
this.toggleMinimize = function () {
// Store the previous state.
this.container.toggleClass('minimized');
this.header.find('.dashicons').toggleClass('dashicons-arrow-up').toggleClass('dashicons-arrow-down');
};
this.remove = function () {
this.container.slideUp(300, function () {
jQuery(this).detach();
});
this.container.trigger('row:remove', [this.rowIndex]);
};
this.updateLabel = function () {
var rowLabelField,
rowLabel;
if ('field' === this.label.type) {
rowLabelField = this.container.find('.repeater-field [data-field="' + this.label.field + '"]');
if ('function' === typeof rowLabelField.val) {
rowLabel = rowLabelField.val();
if ('' !== rowLabel) {
this.header.find('.repeater-row-label').text(rowLabel);
return;
}
}
}
this.header.find('.repeater-row-label').text(this.label.value + ' ' + (this.rowIndex + 1));
};
this.updateLabel();
};
wp.customize.controlConstructor.repeater = wp.customize.Control.extend({
__valueToSet: [],
ready: function () {
'use strict';
var control = this,
limit,
theNewRow;
// The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
var settingValue = this.params.value;
// The hidden field that keeps the data saved (though we never update it)
this.settingField = this.container.find('[data-customize-setting-link]').first();
// The DIV that holds all the rows
this.repeaterFieldsContainer = this.container.find('.repeater-fields').first();
// Set number of rows to 0
this.currentIndex = 0;
// Save the rows objects
this.rows = [];
// Default limit choice
limit = false;
if (undefined !== this.params.choices.limit) {
limit = (0 >= this.params.choices.limit) ? false : parseInt(this.params.choices.limit);
}
this.container.on('click', 'button.repeater-add', function (e) {
e.preventDefault();
if (!limit || control.currentIndex < limit) {
theNewRow = control.addRow();
theNewRow.toggleMinimize();
control.initColorPicker();
control.initDropdownPages(theNewRow);
}
if (limit && control.currentIndex >= limit) {
jQuery(control.selector + ' .limit').addClass('highlight');
jQuery(control.selector + ' .repeater-add').hide();
}
});
if (limit && limit == settingValue.length) {
jQuery(control.selector + ' .repeater-add').hide();
}
this.container.on('click', '.repeater-row-remove', function (e) {
control.currentIndex--;
if (!limit || control.currentIndex < limit) {
jQuery(control.selector + ' .limit').removeClass('highlight');
jQuery(control.selector + ' .repeater-add').show();
}
});
this.container.on('click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function (e) {
e.preventDefault();
control.$thisButton = jQuery(this);
control.openFrame(e);
});
this.container.on('click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function (e) {
e.preventDefault();
control.$thisButton = jQuery(this);
control.removeImage(e);
});
this.container.on('click keypress', '.repeater-field-upload .remove-button', function (e) {
e.preventDefault();
control.$thisButton = jQuery(this);
control.removeFile(e);
});
/**
* Function that loads the Mustache template
*/
this.repeaterTemplate = _.memoize(function () {
var compiled,
/*
* Underscore's default ERB-style templates are incompatible with PHP
* when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
*
* @see trac ticket #22344.
*/
options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
escape: /\{\{([^\}]+?)\}\}(?!\})/g,
variable: 'data'
};
return function (data) {
compiled = _.template(control.container.find('.customize-control-repeater-content').first().html(), null, options);
return compiled(data);
};
});
// When we load the control, the fields have not been filled up
// This is the first time that we create all the rows
if (settingValue.length) {
_.each(settingValue, function (subValue) {
theNewRow = control.addRow(subValue, true);
control.initColorPicker();
control.initDropdownPages(theNewRow, subValue);
});
}
// Once we have displayed the rows, we cleanup the values
this.__valueToSet = settingValue;
this.setValue(settingValue, true);
this.repeaterFieldsContainer.sortable({
handle: '.repeater-row-header',
axis: "y",
update: function (e, ui) {
control.sort();
}
});
},
/**
* Open the media modal.
*/
openFrame: function (event) {
'use strict';
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return;
}
if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-cropped_image')) {
this.initCropperFrame();
} else {
this.initFrame();
}
this.frame.open();
},
initFrame: function () {
'use strict';
var libMediaType = this.getMimeType();
this.frame = wp.media({
states: [
new wp.media.controller.Library({
library: wp.media.query({type: libMediaType}),
multiple: false,
date: false
})
]
});
// When a file is selected, run a callback.
this.frame.on('select', this.onSelect, this);
},
/**
* Create a media modal select frame, and store it so the instance can be reused when needed.
* This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
*/
initCropperFrame: function () {
'use strict';
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
attrs = ['width', 'height', 'flex_width', 'flex_height'], // A list of attributes to look for
libMediaType = this.getMimeType();
// Make sure we got it
if ('string' === typeof currentFieldId && '' !== currentFieldId) {
// Make fields is defined and only do the hack for cropped_image
if ('object' === typeof this.params.fields[currentFieldId] && 'cropped_image' === this.params.fields[currentFieldId].type) {
//Iterate over the list of attributes
attrs.forEach(function (el, index) {
// If the attribute exists in the field
if ('undefined' !== typeof this.params.fields[currentFieldId][el]) {
// Set the attribute in the main object
this.params[el] = this.params.fields[currentFieldId][el];
}
}.bind(this));
}
}
this.frame = wp.media({
button: {
text: 'Select and Crop',
close: false
},
states: [
new wp.media.controller.Library({
library: wp.media.query({type: libMediaType}),
multiple: false,
date: false,
suggestedWidth: this.params.width,
suggestedHeight: this.params.height
}),
new wp.media.controller.CustomizeImageCropper({
imgSelectOptions: this.calculateImageSelectOptions,
control: this
})
]
});
this.frame.on('select', this.onSelectForCrop, this);
this.frame.on('cropped', this.onCropped, this);
this.frame.on('skippedcrop', this.onSkippedCrop, this);
},
onSelect: function () {
'use strict';
var attachment = this.frame.state().get('selection').first().toJSON();
if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-upload')) {
this.setFileInRepeaterField(attachment);
} else {
this.setImageInRepeaterField(attachment);
}
},
/**
* After an image is selected in the media modal, switch to the cropper
* state if the image isn't the right size.
*/
onSelectForCrop: function () {
'use strict';
var attachment = this.frame.state().get('selection').first().toJSON();
if (this.params.width === attachment.width && this.params.height === attachment.height && !this.params.flex_width && !this.params.flex_height) {
this.setImageInRepeaterField(attachment);
} else {
this.frame.setState('cropper');
}
},
/**
* After the image has been cropped, apply the cropped image data to the setting.
*
* @param {object} croppedImage Cropped attachment data.
*/
onCropped: function (croppedImage) {
'use strict';
this.setImageInRepeaterField(croppedImage);
},
/**
* Returns a set of options, computed from the attached image data and
* control-specific data, to be fed to the imgAreaSelect plugin in
* wp.media.view.Cropper.
*
* @param {wp.media.model.Attachment} attachment
* @param {wp.media.controller.Cropper} controller
* @returns {Object} Options
*/
calculateImageSelectOptions: function (attachment, controller) {
'use strict';
var control = controller.get('control'),
flexWidth = !!parseInt(control.params.flex_width, 10),
flexHeight = !!parseInt(control.params.flex_height, 10),
realWidth = attachment.get('width'),
realHeight = attachment.get('height'),
xInit = parseInt(control.params.width, 10),
yInit = parseInt(control.params.height, 10),
ratio = xInit / yInit,
xImg = realWidth,
yImg = realHeight,
x1,
y1,
imgSelectOptions;
controller.set('canSkipCrop', !control.mustBeCropped(flexWidth, flexHeight, xInit, yInit, realWidth, realHeight));
if (xImg / yImg > ratio) {
yInit = yImg;
xInit = yInit * ratio;
} else {
xInit = xImg;
yInit = xInit / ratio;
}
x1 = (xImg - xInit) / 2;
y1 = (yImg - yInit) / 2;
imgSelectOptions = {
handles: true,
keys: true,
instance: true,
persistent: true,
imageWidth: realWidth,
imageHeight: realHeight,
x1: x1,
y1: y1,
x2: xInit + x1,
y2: yInit + y1
};
if (false === flexHeight && false === flexWidth) {
imgSelectOptions.aspectRatio = xInit + ':' + yInit;
}
if (false === flexHeight) {
imgSelectOptions.maxHeight = yInit;
}
if (false === flexWidth) {
imgSelectOptions.maxWidth = xInit;
}
return imgSelectOptions;
},
/**
* Return whether the image must be cropped, based on required dimensions.
*
* @param {bool} flexW
* @param {bool} flexH
* @param {int} dstW
* @param {int} dstH
* @param {int} imgW
* @param {int} imgH
* @return {bool}
*/
mustBeCropped: function (flexW, flexH, dstW, dstH, imgW, imgH) {
'use strict';
if (true === flexW && true === flexH) {
return false;
}
if (true === flexW && dstH === imgH) {
return false;
}
if (true === flexH && dstW === imgW) {
return false;
}
if (dstW === imgW && dstH === imgH) {
return false;
}
if (imgW <= dstW) {
return false;
}
return true;
},
/**
* If cropping was skipped, apply the image data directly to the setting.
*/
onSkippedCrop: function () {
'use strict';
var attachment = this.frame.state().get('selection').first().toJSON();
this.setImageInRepeaterField(attachment);
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment
*/
setImageInRepeaterField: function (attachment) {
'use strict';
var $targetDiv = this.$thisButton.closest('.repeater-field-image,.repeater-field-cropped_image');
$targetDiv.find('.kirki-image-attachment').html('<img src="' + attachment.url + '">').hide().slideDown('slow');
$targetDiv.find('.hidden-field').val(attachment.id);
this.$thisButton.text(this.$thisButton.data('alt-label'));
$targetDiv.find('.remove-button').show();
//This will activate the save button
$targetDiv.find('input, textarea, select').trigger('change');
this.frame.close();
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment
*/
setFileInRepeaterField: function (attachment) {
'use strict';
var $targetDiv = this.$thisButton.closest('.repeater-field-upload');
$targetDiv.find('.kirki-file-attachment').html('<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>').hide().slideDown('slow');
$targetDiv.find('.hidden-field').val(attachment.id);
this.$thisButton.text(this.$thisButton.data('alt-label'));
$targetDiv.find('.upload-button').show();
$targetDiv.find('.remove-button').show();
//This will activate the save button
$targetDiv.find('input, textarea, select').trigger('change');
this.frame.close();
},
getMimeType: function () {
'use strict';
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
attrs = ['mime_type']; // A list of attributes to look for
// Make sure we got it
if ('string' === typeof currentFieldId && '' !== currentFieldId) {
// Make fields is defined and only do the hack for cropped_image
if ('object' === typeof this.params.fields[currentFieldId] && 'upload' === this.params.fields[currentFieldId].type) {
// If the attribute exists in the field
if ('undefined' !== typeof this.params.fields[currentFieldId].mime_type) {
// Set the attribute in the main object
return this.params.fields[currentFieldId].mime_type;
}
}
}
return 'image';
},
removeImage: function (event) {
'use strict';
var $targetDiv,
$uploadButton;
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return;
}
$targetDiv = this.$thisButton.closest('.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload');
$uploadButton = $targetDiv.find('.upload-button');
$targetDiv.find('.kirki-image-attachment').slideUp('fast', function () {
jQuery(this).show().html(jQuery(this).data('placeholder'));
});
$targetDiv.find('.hidden-field').val('');
$uploadButton.text($uploadButton.data('label'));
this.$thisButton.hide();
$targetDiv.find('input, textarea, select').trigger('change');
},
removeFile: function (event) {
'use strict';
var $targetDiv,
$uploadButton;
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return;
}
$targetDiv = this.$thisButton.closest('.repeater-field-upload');
$uploadButton = $targetDiv.find('.upload-button');
$targetDiv.find('.kirki-file-attachment').slideUp('fast', function () {
jQuery(this).show().html(jQuery(this).data('placeholder'));
});
$targetDiv.find('.hidden-field').val('');
$uploadButton.text($uploadButton.data('label'));
this.$thisButton.hide();
$targetDiv.find('input, textarea, select').trigger('change');
},
/**
* Get the current value of the setting
*
* @return Object
*/
getValue: function () {
'use strict';
// The setting is saved in JSON
var value = [];
if (_.isString(this.setting.get())) {
value = JSON.parse(decodeURI(this.setting.get()));
} else {
value = this.setting.get();
}
return JSON.parse(JSON.stringify(_.toArray(value)));
},
/**
* Set a new value for the setting
*
* @param newValue Object
* @param refresh If we want to refresh the previewer or not
*/
putValueInSetting: _.debounce(function () {
var newValue = JSON.parse(JSON.stringify(this.__valueToSet));
this.setting.set(newValue);
var self = this;
newValue.forEach(function (item, index) {
if (!self.rows[index]) {
return;
}
var container = self.rows[index].container;
for (var field in item) {
var oldValue = container.find('[data-field="' + field + '"]').val();
if (item.hasOwnProperty(field) && oldValue !== item[field]) {
container.find('[data-field="' + field + '"]').val(item[field]).trigger('change');
}
}
});
}, 300),
filterValue: function (newValue) {
var filteredValue = newValue,
filter = [];
jQuery.each(this.params.fields, function (index, value) {
if ('image' === value.type || 'cropped_image' === value.type || 'upload' === value.type) {
filter.push(index);
}
});
jQuery.each(newValue, function (index, value) {
jQuery.each(filter, function (ind, field) {
if ('undefined' !== typeof value[field] && 'undefined' !== typeof value[field].id) {
filteredValue[index][field] = value[field].id;
}
});
});
return filteredValue;
},
normalizeValue: function (value, convertToArray) {
if (_.isString(value)) {
try {
value = decodeURI(value);
} catch (e) {
}
try {
value = JSON.parse(value);
} catch (e) {
}
}
if (_.isObject(value) && convertToArray) {
var hasOnlyNumberKeys = _.keys(value).map(function (k) {
return _.isNumber(parseInt(k))
}).reduce(function (a, b) {
return (a && b);
}, true);
if (hasOnlyNumberKeys) {
var newValue = [];
_.keys(value).forEach(function (k) {
if (_.isUndefined(value[k])) {
return;
}
newValue.push(value[k]);
});
value = newValue;
}
}
return value;
},
setValue: function (newValue, filtering) {
'use strict';
this.__valueToSet = this.getValue();
// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
if (filtering) {
newValue = this.filterValue(newValue);
}
if (this.params.choices.beforeValueSet && window[this.params.choices.beforeValueSet]) {
newValue = window[this.params.choices.beforeValueSet].call(this, newValue);
}
newValue = this.normalizeValue(newValue, true);
this.__valueToSet = newValue;
this.putValueInSetting();
},
/**
* Add a new row to repeater settings based on the structure.
*
* @param data (Optional) Object of field => value pairs (undefined if you want to get the default values)
*/
addRow: function (data, silent) {
'use strict';
var control = this,
template = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
settingValue = this.getValue(), // Get the current setting value.
newRowSetting = {}, // Saves the new setting data.
templateData, // Data to pass to the template
newRow,
i;
if (template) {
// The control structure is going to define the new fields
// We need to clone control.params.fields. Assigning it
// ould result in a reference assignment.
templateData = jQuery.extend(true, {}, control.params.fields);
// But if we have passed data, we'll use the data values instead
if (data) {
for (i in data) {
if (data.hasOwnProperty(i) && templateData.hasOwnProperty(i)) {
templateData[i]['default'] = data[i];
}
}
}
templateData.index = this.currentIndex;
// Append the template content
template = template(templateData);
// Create a new row object and append the element
newRow = new RepeaterRow(
control.currentIndex,
jQuery(template).appendTo(control.repeaterFieldsContainer),
control.params.row_label
);
newRow.container.on('row:remove', function (e, rowIndex) {
control.deleteRow(rowIndex);
});
newRow.container.on('row:update', function (e, rowIndex, fieldName, element) {
control.updateField.call(control, e, rowIndex, fieldName, element);
newRow.updateLabel();
});
// Add the row to rows collection
this.rows[this.currentIndex] = newRow;
for (i in templateData) {
if (templateData.hasOwnProperty(i)) {
newRowSetting[i] = templateData[i]['default'];
}
}
if (!silent) {
settingValue[this.currentIndex] = newRowSetting;
this.setValue(settingValue);
}
this.currentIndex++;
return newRow;
}
},
sort: function () {
'use strict';
var control = this,
$rows = this.repeaterFieldsContainer.find('.repeater-row'),
newOrder = [],
settings = control.getValue(),
newRows = [],
newSettings = [];
$rows.each(function (i, element) {
newOrder.push(jQuery(element).data('row'));
});
jQuery.each(newOrder, function (newPosition, oldPosition) {
newRows[newPosition] = control.rows[oldPosition];
newRows[newPosition].setRowIndex(newPosition);
newSettings[newPosition] = settings[oldPosition];
});
control.rows = newRows;
control.setValue(newSettings);
},
/**
* Delete a row in the repeater setting
*
* @param index Position of the row in the complete Setting Array
*/
deleteRow: function (index) {
'use strict';
var currentSettings = this.getValue(),
row,
i,
prop;
if (currentSettings[index]) {
// Find the row
row = this.rows[index];
if (row) {
// The row exists, let's delete it
// Remove the row settings
if (_.isArray(currentSettings)) {
currentSettings.splice(index, 1)
} else {
delete currentSettings[index];
}
// Remove the row from the rows collection
if (_.isArray(this.rows)) {
this.rows.splice(index, 1)
} else {
delete this.rows[index];
}
// clean null
currentSettings = _.omit(currentSettings, _.isNull);
currentSettings = _.omit(currentSettings, _.isUndefined);
// Update the new setting values
this.setValue(currentSettings);
}
}
// Remap the row numbers
if (_.isArray(this.rows)) {
this.rows.forEach(function (row, index) {
row.setRowIndex(index);
row.updateLabel();
});
} else {
var i = 1;
for (prop in this.rows) {
if (this.rows.hasOwnProperty(prop) && this.rows[prop]) {
this.rows[prop].updateLabel();
i++;
}
}
}
},
/**
* Update a single field inside a row.
* Triggered when a field has changed
*
* @param e Event Object
*/
updateField: function (e, rowIndex, fieldId, element) {
'use strict';
var type,
row,
currentSettings;
if (!this.rows[rowIndex]) {
return;
}
if (!this.params.fields[fieldId]) {
return;
}
type = this.params.fields[fieldId].type;
row = this.rows[rowIndex];
currentSettings = this.getValue();
element = jQuery(element);
if (undefined === typeof currentSettings[row.rowIndex][fieldId]) {
return;
}
var value = currentSettings[row.rowIndex][fieldId];
if ('checkbox' === type) {
value = element.is(':checked');
} else {
// Update the settings
value = element.val();
}
if (value !== currentSettings[row.rowIndex][fieldId]) {
currentSettings[row.rowIndex][fieldId] = value;
this.setValue(currentSettings);
}
},
/**
* Init the color picker on color fields
* Called after AddRow
*
*/
initColorPicker: function () {
'use strict';
var control = this,
colorPicker = control.container.find('.color-picker-hex'),
options = {},
fieldId = colorPicker.data('field');
// We check if the color palette parameter is defined.
if ('undefined' !== typeof fieldId && 'undefined' !== typeof control.params.fields[fieldId] && 'undefined' !== typeof control.params.fields[fieldId].palettes && 'object' === typeof control.params.fields[fieldId].palettes) {
options.palettes = control.params.fields[fieldId].palettes;
}
// When the color picker value is changed we update the value of the field
options.change = function (event, ui) {
var currentPicker = jQuery(event.target),
row = currentPicker.closest('.repeater-row'),
rowIndex = row.data('row'),
currentSettings = control.getValue();
currentSettings[rowIndex][currentPicker.data('field')] = ui.color.toString();
control.setValue(currentSettings);
};
// Init the color picker
if (0 !== colorPicker.length) {
colorPicker.wpColorPicker(options);
}
},
/**
* Init the dropdown-pages field with selectize
* Called after AddRow
*
* @param {object} theNewRow the row that was added to the repeater
* @param {object} data the data for the row if we're initializing a pre-existing row
*
*/
initDropdownPages: function (theNewRow, data) {
'use strict';
var control = this,
dropdown = theNewRow.container.find('.repeater-dropdown-pages select'),
$select,
selectize,
dataField;
if (0 === dropdown.length) {
return;
}
$select = jQuery(dropdown).selectize();
selectize = $select[0].selectize;
dataField = dropdown.data('field');
if (data) {
selectize.setValue(data[dataField]);
}
this.container.on('change', '.repeater-dropdown-pages select', function (event) {
var currentDropdown = jQuery(event.target),
row = currentDropdown.closest('.repeater-row'),
rowIndex = row.data('row'),
currentSettings = control.getValue();
currentSettings[rowIndex][currentDropdown.data('field')] = jQuery(this).val();
control.setValue(currentSettings);
});
}
});