관리-도구
편집 파일: control.js
import "./control.scss"; /* global kirkiControlLoader */ /* eslint max-depth: 0 */ /* eslint no-useless-escape: 0 */ var RepeaterRow = function (rowIndex, container, label, control) { 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 (rowNum) { this.rowIndex = rowNum; this.container.attr("data-row", rowNum); this.container.data("row", rowNum); 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, rowLabelSelector; if ("field" === this.label.type) { rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' ); if (_.isFunction(rowLabelField.val)) { rowLabel = rowLabelField.val(); if ("" !== rowLabel) { if (!_.isUndefined(control.params.fields[this.label.field])) { if (!_.isUndefined(control.params.fields[this.label.field].type)) { if ("select" === control.params.fields[this.label.field].type) { if ( !_.isUndefined( control.params.fields[this.label.field].choices ) && !_.isUndefined( control.params.fields[this.label.field].choices[ rowLabelField.val() ] ) ) { rowLabel = control.params.fields[this.label.field].choices[ rowLabelField.val() ]; } } else if ( "radio" === control.params.fields[this.label.field].type || "radio-image" === control.params.fields[this.label.field].type ) { rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked'; rowLabel = jQuery(rowLabelSelector).val(); } } } 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({ // When we're finished loading continue processing ready: function () { var control = this; // Init the control. if ( !_.isUndefined(window.kirkiControlLoader) && _.isFunction(kirkiControlLoader) ) { kirkiControlLoader(control); } else { control.initKirkiControl(); } }, initKirkiControl: function (control) { var limit, theNewRow, settingValue; control = control || this; // The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function) settingValue = control.params.value; // The hidden field that keeps the data saved (though we never update it) control.settingField = control.container .find("[data-customize-setting-link]") .first(); // Set the field value for the first time, we'll fill it up later control.setValue([], false); // The DIV that holds all the rows control.repeaterFieldsContainer = control.container .find(".repeater-fields") .first(); // Set number of rows to 0 control.currentIndex = 0; // Save the rows objects control.rows = []; // Default limit choice limit = false; if (!_.isUndefined(control.params.choices.limit)) { limit = 0 >= control.params.choices.limit ? false : parseInt(control.params.choices.limit, 10); } control.container.on("click", "button.repeater-add", function (e) { e.preventDefault(); if (!limit || control.currentIndex < limit) { theNewRow = control.addRow(); theNewRow.toggleMinimize(); control.initColorPicker(); control.initSelect(theNewRow); } else { jQuery(control.selector + " .limit").addClass("highlight"); } }); control.container.on("click", ".repeater-row-remove", function () { control.currentIndex--; if (!limit || control.currentIndex < limit) { jQuery(control.selector + " .limit").removeClass("highlight"); } }); control.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); } ); control.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); } ); control.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 */ control.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); control.initColorPicker(); control.initSelect(theNewRow, subValue); }); } control.repeaterFieldsContainer.sortable({ handle: ".repeater-row-header", update: function () { control.sort(); }, }); }, /** * Open the media modal. * * @param {Object} event - The JS event. * @returns {void} */ openFrame: function (event) { 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 () { 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 * * @returns {void} */ initCropperFrame: function () { // 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 (_.isString(currentFieldId) && "" !== currentFieldId) { // Make fields is defined and only do the hack for cropped_image if ( _.isObject(this.params.fields[currentFieldId]) && "cropped_image" === this.params.fields[currentFieldId].type ) { //Iterate over the list of attributes attrs.forEach( function (el) { // If the attribute exists in the field if (!_.isUndefined(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 () { 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 () { 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. * @returns {void} */ onCropped: function (croppedImage) { 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 - The attachment from the WP API. * @param {wp.media.controller.Cropper} controller - Media controller. * @returns {Object} - Options. */ calculateImageSelectOptions: function (attachment, controller) { 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 - The flex-width. * @param {bool} flexH - The flex-height. * @param {int} dstW - Initial point distance in the X axis. * @param {int} dstH - Initial point distance in the Y axis. * @param {int} imgW - Width. * @param {int} imgH - Height. * @returns {bool} - Whether the image must be cropped or not based on required dimensions. */ mustBeCropped: function (flexW, flexH, dstW, dstH, imgW, imgH) { return !( (true === flexW && true === flexH) || (true === flexW && dstH === imgH) || (true === flexH && dstW === imgW) || (dstW === imgW && dstH === imgH) || imgW <= dstW ); }, /** * If cropping was skipped, apply the image data directly to the setting. * * @returns {void} */ onSkippedCrop: function () { var attachment = this.frame.state().get("selection").first().toJSON(); this.setImageInRepeaterField(attachment); }, /** * Updates the setting and re-renders the control UI. * * @param {object} attachment - The attachment object. * @returns {void} */ setImageInRepeaterField: function (attachment) { 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 - The attachment object. * @returns {void} */ setFileInRepeaterField: function (attachment) { 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 () { // We get the field id from which this was called var currentFieldId = this.$thisButton .siblings("input.hidden-field") .attr("data-field"); // Make sure we got it if (_.isString(currentFieldId) && "" !== currentFieldId) { // Make fields is defined and only do the hack for cropped_image if ( _.isObject(this.params.fields[currentFieldId]) && "upload" === this.params.fields[currentFieldId].type ) { // If the attribute exists in the field if (!_.isUndefined(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) { 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) { 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 * * @returns {Object} - Returns the value. */ getValue: function () { // The setting is saved in JSON return JSON.parse(decodeURI(this.setting.get())); }, /** * Set a new value for the setting * * @param {Object} newValue - The new value. * @param {bool} refresh - If we want to refresh the previewer or not * @param {bool} filtering - If we want to filter or not. * @returns {void} */ setValue: function (newValue, refresh, filtering) { // 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 var filteredValue = newValue, filter = []; if (filtering) { 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 (!_.isUndefined(value[field]) && !_.isUndefined(value[field].id)) { filteredValue[index][field] = value[field].id; } }); }); } this.setting.set(encodeURI(JSON.stringify(filteredValue))); if (refresh) { // Trigger the change event on the hidden field so // previewer refresh the website on Customizer this.settingField.trigger("change"); } }, /** * Add a new row to repeater settings based on the structure. * * @param {Object} data - (Optional) Object of field => value pairs (undefined if you want to get the default values) * @returns {Object} - Returns the new row. */ addRow: function (data) { 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, control ); 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); // eslint-disable-line no-useless-call 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; } } settingValue[this.currentIndex] = newRowSetting; this.setValue(settingValue, true); this.currentIndex++; return newRow; } }, sort: function () { 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 {int} index - Position of the row in the complete Setting Array * @returns {void} */ deleteRow: function (index) { var currentSettings = this.getValue(), row, prop; if (currentSettings[index]) { // Find the row row = this.rows[index]; if (row) { // Remove the row settings delete currentSettings[index]; // Remove the row from the rows collection delete this.rows[index]; // Update the new setting values this.setValue(currentSettings, true); } } // Remap the row numbers for (prop in this.rows) { if (this.rows.hasOwnProperty(prop) && this.rows[prop]) { this.rows[prop].updateLabel(); } } }, /** * Update a single field inside a row. * Triggered when a field has changed * * @param {Object} e - Event Object * @param {int} rowIndex - The row's index as an integer. * @param {string} fieldId - The field ID. * @param {string|Object} element - The element's identifier, or jQuery Object of the element. * @returns {void} */ updateField: function (e, rowIndex, fieldId, element) { 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 (_.isUndefined(currentSettings[row.rowIndex][fieldId])) { return; } if ("checkbox" === type) { currentSettings[row.rowIndex][fieldId] = element.is(":checked"); } else { // Update the settings currentSettings[row.rowIndex][fieldId] = element.val(); } this.setValue(currentSettings, true); }, /** * Init the color picker on color fields * Called after AddRow * * @returns {void} */ initColorPicker: function () { var control = this; var colorPicker = control.container.find(".kirki-classic-color-picker"); var fieldId = colorPicker.data("field"); var options = {}; // We check if the color palette parameter is defined. if ( !_.isUndefined(fieldId) && !_.isUndefined(control.params.fields[fieldId]) && !_.isUndefined(control.params.fields[fieldId].palettes) && _.isObject(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); var row = currentPicker.closest(".repeater-row"); var rowIndex = row.data("row"); var currentSettings = control.getValue(); var value = ui.color._alpha < 1 ? ui.color.to_s() : ui.color.toString(); currentSettings[rowIndex][currentPicker.data("field")] = value; control.setValue(currentSettings, true); // By default if the alpha is 1, the input will be rgb. // We setTimeout to 50ms to prevent race value set. setTimeout(function() { event.target.value = value; }, 50); }; // Init the color picker if (colorPicker.length && 0 !== colorPicker.length) { colorPicker.wpColorPicker(options); } }, /** * Init the dropdown-pages field. * 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 * @returns {void} */ initSelect: function (theNewRow, data) { var control = this, dropdown = theNewRow.container.find(".repeater-field select"), dataField; if (0 === dropdown.length) { return; } dataField = dropdown.data("field"); multiple = jQuery(dropdown).data("multiple"); data = data || {}; data[dataField] = data[dataField] || ""; jQuery(dropdown).val(data[dataField] || jQuery(dropdown).val()); this.container.on("change", ".repeater-field 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); }); }, });