"use strict"; function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } ; (function ($, window, document, undefined) { 'use strict'; var merchant = merchant || {}; var params = new URLSearchParams(window.location.search); var currentModule = params.get('module'); $(document).ready(function () { // AjaxSave var $ajaxForm = $('.merchant-module-page-ajax-form'); var $ajaxHeader = $('.merchant-module-page-ajax-header'); var $ajaxSaveBtn = $('.merchant-module-save-button'); $('.merchant-module-page-content').on('change keypress change.merchant', function (e) { if ($(e.target).hasClass('merchant-backup-file') || $(e.target).hasClass('merchant-search-field')) { return; } // Nothing in the preview is a setting. Its controls are there to be played // with, so a module whose mock-up has working fields cannot dirty the form. if ($(e.target).closest('.merchant-module-page-preview').length) { return; } // The licence key and the module feedback textarea save through their // own handlers, so editing them leaves the save bar alone. if (!$(e.target).is('.merchant-module-question-answer-textarea, .merchant-license-code-input')) { if (!merchant.show_save) { $ajaxHeader.addClass('merchant-show'); $ajaxHeader.removeClass('merchant-saving'); merchant.show_save = true; } } GroubField.initFlag(); }); // Warn the user before leaving the page with unsaved changes. window.addEventListener('beforeunload', function (e) { if (merchant.show_save) { e.preventDefault(); } }); $ajaxForm.ajaxForm({ beforeSubmit: function beforeSubmit(arr) { $ajaxHeader.addClass('merchant-saving'); // Serialize merchant[] fields into a single JSON payload // to bypass PHP's max_input_vars limit (Issue #608). var merchantData = {}; var keepIndexes = []; arr.forEach(function (field, index) { if (field.name === 'merchant-search-field') { keepIndexes.push(index); return; } if (field.name && field.name.indexOf('merchant[') === 0) { var keys = field.name.match(/\[([^\]]*)\]/g); if (!keys) { return; } var obj = merchantData; for (var i = 0; i < keys.length - 1; i++) { var key = keys[i].slice(1, -1); if (obj[key] === undefined) { var nextKey = keys[i + 1] ? keys[i + 1].slice(1, -1) : ''; obj[key] = /^\d+$/.test(nextKey) ? [] : {}; } obj = obj[key]; } var lastKey = keys[keys.length - 1].slice(1, -1); if (lastKey === '') { // Array notation (e.g. name="...[show_pages][]") — push into parent array. var parentKey = keys[keys.length - 2].slice(1, -1); var parentObj = merchantData; for (var j = 0; j < keys.length - 2; j++) { parentObj = parentObj[keys[j].slice(1, -1)]; } if (!Array.isArray(parentObj[parentKey])) { parentObj[parentKey] = []; } parentObj[parentKey].push(field.value); } else { obj[lastKey] = field.value; } } else { keepIndexes.push(index); } }); // Ensure unchecked checkboxes inside hydrated FC rows are saved as 0. // serializeArray() omits unchecked checkboxes entirely, which would // silently drop the field from the JSON payload and lose the "off" state. $('.merchant-flexible-content .layout:not([data-deferred])').each(function () { $(this).find('.layout-body input[type="checkbox"]:not(:checked)').each(function () { var name = $(this).attr('name') || ''; if (name.indexOf('merchant[') !== 0) { return; } // Skip checkbox_multiple (name ends with []) — absence = empty array. if (/\[\]$/.test(name)) { return; } var keys = name.match(/\[([^\]]*)\]/g); if (!keys) { return; } var obj = merchantData; for (var i = 0; i < keys.length - 1; i++) { var key = keys[i].slice(1, -1); if (obj[key] === undefined) { obj[key] = {}; } obj = obj[key]; } var lastKey = keys[keys.length - 1].slice(1, -1); if (obj[lastKey] === undefined) { obj[lastKey] = 0; } }); }); // Merge deferred (non-hydrated) layout row data into merchantData. // These rows have no form inputs, only a data-fields-json attribute. $('.merchant-flexible-content .layout[data-deferred="1"]').each(function () { var jsonStr = $(this).attr('data-fields-json'); if (!jsonStr) { return; } var deferredData; try { deferredData = JSON.parse(jsonStr); } catch (e) { return; } var $control = $(this).closest('.merchant-flexible-content-control'); var fcFieldId = $control.attr('data-id'); var rowIndex = parseInt($(this).find('.layout-count').text(), 10) - 1; if (!merchantData[fcFieldId]) { merchantData[fcFieldId] = {}; } // Build the row data from deferred values + layout/flexible_id from hidden inputs. var rowData = $.extend({}, deferredData.values || {}); rowData.layout = $(this).attr('data-type') || ''; rowData.flexible_id = $(this).find('.flexible-id').val() || ''; merchantData[fcFieldId][rowIndex] = rowData; }); var kept = keepIndexes.map(function (i) { return arr[i]; }); kept.push({ name: 'merchant_json_payload', value: JSON.stringify(merchantData) }); arr.length = 0; kept.forEach(function (item) { arr.push(item); }); }, success: function success() { $ajaxHeader.removeClass('merchant-show'); merchant.show_save = false; // Module Alert after Ajax Save if (!$('.merchant-module-action').hasClass('merchant-enabled')) { var $moduleAlert = $('.merchant-module-alert'); $moduleAlert.addClass('merchant-show'); $(document).off('click.merchant-alert-close'); $(document).on('click.merchant-alert-close', function (e) { if (!$(e.target).closest('.merchant-module-alert-wrapper').length) { $moduleAlert.removeClass('merchant-show'); $(document).off('click.merchant-alert-close'); } }); } $(document).trigger('save.merchant', [currentModule]); } }); var $disableModuleSubmitBtn = $('.merchant-module-question-answer-button'); var $disableModuleTextField = $('.merchant-module-question-answer-textarea'); $disableModuleTextField.on('input', function () { $disableModuleSubmitBtn.prop('disabled', $(this).val().trim() === ''); }); $disableModuleSubmitBtn.on('click', function (e) { e.preventDefault(); var message = $disableModuleTextField.val(); if (!message.trim()) { alert('Please provide the required information.'); return; } var $button = $(this); $('.merchant-module-question-answer-dropdown').removeClass('merchant-show'); $('.merchant-module-question-thank-you-dropdown').addClass('merchant-show'); window.wp.ajax.post('merchant_module_feedback', { subject: $disableModuleTextField.attr('data-subject'), message: message, module: $button.closest('.merchant-module-action').find('.merchant-module-page-button-action-activate').data('module'), nonce: window.merchant.nonce }); }); $('.merchant-module-page-button-action-activate').on('click', function (e) { e.preventDefault(); if ($(this).hasClass('merchant-module-deactivated-by-bp')) { return false; } $('.merchant-module-question-list-dropdown').removeClass('merchant-show'); $('.merchant-module-question-answer-dropdown').removeClass('merchant-show'); $('.merchant-module-question-answer-form').removeClass('merchant-show'); $('.merchant-module-question-answer-title').removeClass('merchant-show'); $('.merchant-module-question-thank-you-dropdown').removeClass('merchant-show'); $('.merchant-module-question-answer-textarea').val(''); window.wp.ajax.post('merchant_module_activate', { module: $(this).data('module'), nonce: window.merchant.nonce }).done(function () { $('body').removeClass('merchant-module-disabled').addClass('merchant-module-enabled'); $('.merchant-module-action').addClass('merchant-enabled'); }); }); $('.merchant-module-page-button-action-deactivate').on('click', function (e) { e.preventDefault(); window.wp.ajax.post('merchant_module_deactivate', { module: $(this).data('module'), nonce: window.merchant.nonce }).done(function () { $('body').removeClass('merchant-module-enabled').addClass('merchant-module-disabled'); $('.merchant-module-action').removeClass('merchant-enabled'); $('.merchant-module-question-list-dropdown').addClass('merchant-show'); }); }); $('.merchant-module-question-list-dropdown li').on('click', function (e) { $disableModuleSubmitBtn.prop('disabled', $disableModuleTextField.val().trim() === ''); var $question = $(this); var target = $question.data('answer-target'); var $answer = $('[data-answer-title="' + target + '"]'); if ($answer.length) { $answer.addClass('merchant-show').siblings().removeClass('merchant-show'); $('.merchant-module-question-answer-dropdown').addClass('merchant-show'); $('.merchant-module-question-answer-textarea').attr('data-subject', $question.text().trim()); } else { $('.merchant-module-question-thank-you-dropdown').addClass('merchant-show'); $('.merchant-module-question-answer-dropdown').removeClass('merchant-show'); } $('.merchant-module-question-answer-textarea').val(''); $('.merchant-module-question-list-dropdown').removeClass('merchant-show'); }); $('.merchant-module-dropdown-close').on('click', function (e) { e.preventDefault(); $(this).closest('.merchant-module-dropdown').removeClass('merchant-show'); }); $('.merchant-module-page-button-deactivate').on('click', function (e) { e.preventDefault(); var $button = $(this); var $dropdown = $('.merchant-module-deactivate-dropdown'); $dropdown.toggleClass('merchant-show'); $(document).off('click.merchant-close'); $(document).on('click.merchant-close', function (e) { if (!$(e.target).closest('.merchant-module-deactivate').length) { $dropdown.removeClass('merchant-show'); $(document).off('click.merchant-close'); } }); }); // Create a function that initializes a single range or all ranges in a context for range field function initMerchantRange() { var rangeFields = $(document).find('.merchant-range'); if (rangeFields.length === 0) { return; } rangeFields.each(function () { var $range = $(this); var $rangeInput = $range.find('.merchant-range-input'); var $numberInput = $range.find('.merchant-range-number-input'); $rangeInput.on('change input merchant.range merchant-init.range', function (e) { var $range = $(this); var value = (e.type === 'merchant' ? $numberInput.val() : $range.val()) || 0; var min = $range.attr('min') || 0; var max = $range.attr('max') || 1; var percentage = (value - min) / (max - min) * 100; if ($('body').hasClass('rtl')) { $range.css({ 'background': 'linear-gradient(to left, #3858E9 0%, #3858E9 ' + percentage + '%, #ddd ' + percentage + '%, #ddd 100%)' }); } else { $range.css({ 'background': 'linear-gradient(to right, #3858E9 0%, #3858E9 ' + percentage + '%, #ddd ' + percentage + '%, #ddd 100%)' }); } $rangeInput.val(value); $numberInput.val(value); }).trigger('merchant-init.range'); $numberInput.on('change input blur', function () { if ($rangeInput.hasClass('merchant-range-input')) { $rangeInput.val($(this).val()).trigger('merchant.range'); } }); }); } // 1. Initialize on DOM ready (existing fields) initMerchantRange(); $(document).on('click', '.merchant-module-page-setting-field-hidden-desc-trigger', function () { var $trigger = $(this); $trigger.toggleClass('expanded'); var showText = $trigger.attr('data-show-text'); var hiddenText = $trigger.attr('data-hidden-text'); $(this).find('span:first').text($trigger.text() === showText ? hiddenText : showText); $(this).closest('.merchant-module-page-setting-field').find('.merchant-module-page-setting-field-hidden-desc').stop(true, true).slideToggle('fast'); }); var moduleBackup = { init: function init() { this.events(); }, events: function events() { var self = this; $(document).on('click', '#download-backup-button', this.download.bind(this)); // Browse button opens file picker. $(document).on('click', '.merchant-dropzone__browse', function (e) { e.preventDefault(); e.stopPropagation(); $(this).closest('.merchant-dropzone').find('.merchant-backup-file').trigger('click'); }); // Auto-restore when a file is selected (via browse or drop). $(document).on('change', '#merchant-backup-file', function () { var file = this.files[0]; if (file) { self.restoreFile(file); } }); // Drag-and-drop visual feedback. var $dropzone = $('#merchant-restore-dropzone'); $dropzone.on('dragover dragenter', function (e) { e.preventDefault(); e.stopPropagation(); $(this).addClass('drag-over'); }); $dropzone.on('dragleave drop', function (e) { e.preventDefault(); e.stopPropagation(); $(this).removeClass('drag-over'); }); $dropzone.on('drop', function (e) { var file = e.originalEvent.dataTransfer.files[0]; if (file) { self.restoreFile(file); } }); }, download: function download(e) { var self = this; e.preventDefault(); var container = $('.merchant-module-page-setting-fields .backup-section'); var $status = container.find('.merchant-backup-status'); $status.removeAttr('class').addClass('merchant-backup-status').text(''); var module_id = $(e.target).attr('data-module-id'); $.ajax({ url: merchant_admin_options.ajaxurl, type: 'GET', data: { action: 'merchant_get_module_settings', nonce: merchant_admin_options.ajaxnonce, module_id: module_id }, beforeSend: function beforeSend() { self.showLoadingIndicator(container); }, success: function success(response) { self.hideLoadingIndicator(container); if (response.success) { var moduleSettings = response.data; var fileName = 'merchant-' + module_id + '-backup-' + new Date().toISOString().slice(0, 10) + '-' + new Date().getHours() + '-' + new Date().getMinutes() + '-' + new Date().getSeconds() + '.json'; self.downloadJson(moduleSettings, fileName); $status.addClass('is-success').text(merchant_admin_options.backup_success || 'Downloaded!'); } else { $status.addClass('is-error').text(response.data.message); } setTimeout(function () { $status.removeAttr('class').addClass('merchant-backup-status').text(''); }, 3000); }, error: function error(xhr, status, _error) { self.hideLoadingIndicator(container); $status.addClass('is-error').text(_error); setTimeout(function () { $status.removeAttr('class').addClass('merchant-backup-status').text(''); }, 3000); } }); }, restoreFile: function restoreFile(file) { var self = this; var container = $('.merchant-module-page-setting-fields .restore-section'); var module_id = $('#merchant-restore-dropzone').data('module-id'); self.hideError(); if (!file || file.type !== 'application/json') { self.displayError(merchant_admin_options.invalid_file); return; } var reader = new FileReader(); reader.onload = function (e) { var moduleSettings = e.target.result; $.ajax({ url: merchant_admin_options.ajaxurl, type: 'POST', data: { action: 'merchant_restore_module_settings', nonce: merchant_admin_options.ajaxnonce, module_id: module_id, module_settings: moduleSettings }, beforeSend: function beforeSend() { $('#merchant-restore-dropzone').addClass('is-loading'); self.showLoadingIndicator(container); }, success: function success(response) { if (response.success) { merchant.show_save = false; var $dz = $('#merchant-restore-dropzone'); $dz.removeClass('is-loading').addClass('is-success'); self.hideLoadingIndicator(container); setTimeout(function () { window.location.href = response.data.redirect_url; }, 1500); } else { self.displayError(response.data.message); } }, error: function error(xhr, status, _error2) { self.displayError(_error2); } }); }; reader.readAsText(file); }, downloadJson: function downloadJson(data, filename) { if (_typeof(data) === 'object') { data = JSON.stringify(data); } var blob = new Blob([data], { type: "application/json" }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }, displayError: function displayError(errorMsg) { var $dz = $('#merchant-restore-dropzone'); $dz.find('.merchant-dropzone__error p').text(errorMsg); $dz.removeClass('is-loading').addClass('is-error'); this.hideLoadingIndicator($('.merchant-module-page-setting-fields .restore-section')); setTimeout(function () { $dz.removeClass('is-error'); // Reset file input so same file can be re-selected. $dz.find('.merchant-backup-file').val(''); }, 3000); }, hideError: function hideError() { $('#merchant-restore-dropzone').removeClass('is-error'); }, showLoadingIndicator: function showLoadingIndicator(container) { container.find('.merchant-loading-spinner').addClass('show'); }, hideLoadingIndicator: function hideLoadingIndicator(container) { container.find('.merchant-loading-spinner').removeClass('show'); } }; moduleBackup.init(); // Add support for toggle field inside flexible content. var flexibleToggleField = { init: function init(field) { this.events(); }, events: function events() { $(document).on('click', '.merchant-flexible-content .merchant-toggle-switch .toggle-switch-label span', function () { var checkBox = $(this).closest('.merchant-toggle-switch').find('.toggle-switch-checkbox'); checkBox.prop('checked', !checkBox.prop('checked')); }).trigger('merchant.change'); } }; var dateTimePickerField = { init: function init() { this.initiate_datepicker(); this.events(); }, // Date() cannot parse the bare 'H:i' a time-only picker stores, so pin it to today. parseInitialDate: function parseInitialDate(value, fieldOptions) { if (!value) { return []; } if (fieldOptions.onlyTimepicker && /^\d{1,2}:\d{2}$/.test(value)) { var _value$split = value.split(':'), _value$split2 = _slicedToArray(_value$split, 2), hours = _value$split2[0], minutes = _value$split2[1]; return [new Date(new Date().setHours(Number(hours), Number(minutes), 0, 0))]; } var date = new Date(value); return isNaN(date) ? [] : [date]; }, initiate_datepicker: function initiate_datepicker() { var self = this; var elements = $('.merchant-module-page-setting-field .merchant-datetime-field'); if (elements.length === 0) { return; } elements.each(function (index) { var input = $(this).find('input'), fieldOptions = $(this).data('options') || {}, options = { locale: JSON.parse(merchant_datepicker_locale), selectedDates: self.parseInitialDate(input.val(), fieldOptions), onSelect: function onSelect(_ref) { var date = _ref.date, formattedDate = _ref.formattedDate, datepicker = _ref.datepicker; if (typeof formattedDate === "undefined") { // allow removing date // input.val(''); datepicker.$el.value = ''; } input.trigger('change.merchant'); input.trigger('change.merchant-datepicker', [formattedDate, input, options, index]); } }; // add buttons to fieldOptions fieldOptions.buttons = ['clear']; if (fieldOptions.minDate !== undefined && fieldOptions.minDate === 'today') { fieldOptions.minDate = new Date(); if (fieldOptions.timeZone !== undefined && fieldOptions.timeZone !== '') { fieldOptions.minDate = new Date(fieldOptions.minDate.toLocaleString('en-US', { timeZone: fieldOptions.timeZone })); } } options = Object.assign(options, fieldOptions); var datepickerObj = new AirDatepicker(input.getPath(), options); input.attr('readonly', true); $(document).trigger('initiated.merchant-datepicker', [datepickerObj, input, options, index]); }); }, events: function events() { var self = this; $(document).on('merchant-flexible-content-added', function () { self.initiate_datepicker(); }); } }; dateTimePickerField.init(); // Sortable. var SortableField = { init: function init(field) { this.events(); }, events: function events() { var self = this; $('.merchant-sortable').each(function () { var field = $(this), input = field.find('.merchant-sortable-input'); // Init sortable. $(field.find('ul.merchant-sortable-list').first()).sortable({ // Update value when we stop sorting. update: function update() { input.val(self.sortableGetNewVal(field)).trigger('change.merchant'); } }).disableSelection().find('li').each(function () { // Enable/disable options when we click on the eye of Thundera. $(this).find('i.visibility').click(function () { $(this).toggleClass('dashicons-visibility-faint').parents('li:eq(0)').toggleClass('invisible'); }); }).click(function () { // Update value when click in the eye. if ($(event.target).hasClass('dashicons-visibility')) { input.val(self.sortableGetNewVal(field)).trigger('change.merchant'); } }); }); }, sortableGetNewVal: function sortableGetNewVal(field) { var items = $(field.find('li')); var newVal = []; _.each(items, function (item) { if (!$(item).hasClass('invisible')) { newVal.push($(item).data('value')); } }); return JSON.stringify(newVal); } }; // Initialize Sortable. SortableField.init(); flexibleToggleField.init(); // When adding/duplicating new item, refresh sorting $(document).on('merchant-flexible-content-added', function (e, $layout) { var $sortableWrapper = $layout.find('.merchant-sortable-repeater-control'); var $sortableElement = $sortableWrapper.find('.merchant-sortable-repeater.sortable'); SortableRepeaterField.makeFieldsSortable($sortableElement); }); // Sortable Repeater. var SortableRepeaterField = { init: function init(field) { var self = this; // Update the values for all our input fields and initialise the sortable repeater. $('.merchant-sortable-repeater-control').each(function () { // If there is an existing customizer value, populate our rows var defaultValuesArray = JSON.parse($(this).find('.merchant-sortable-repeater-input').val()); var numRepeaterItems = defaultValuesArray.length; if (numRepeaterItems > 0) { // Add the first item to our existing input field $(this).find('.repeater-input').val(defaultValuesArray[0]); // Create a new row for each new value if (numRepeaterItems > 1) { // var i; for (var i = 1; i < numRepeaterItems; ++i) { self.appendRow($(this), defaultValuesArray[i]); } } } // Todo: remove // Make our Repeater fields sortable. Doesn't work with flexible content // if (!$(this).hasClass('disable-sorting')) { // $(this).find('.merchant-sortable-repeater.sortable').sortable({ // update: function (event, ui) { // self.getAllInputs($(this).parent()); // } // }); // } }); // Events. this.events(); }, events: function events() { var self = this; // Remove item starting from its parent element $(document).on('click', '.merchant-sortable-repeater.sortable .customize-control-sortable-repeater-delete', function (event) { event.preventDefault(); $(this).parent().slideUp('fast', function () { var parentContainer = $(this).parent().parent(); $(this).remove(); self.getAllInputs(parentContainer); }); $(document).trigger('merchant-sortable-repeater-item-deleted'); }); // Add new item $(document).on('click', '.customize-control-sortable-repeater-add', function (event) { event.preventDefault(); self.appendRow($(this).parent()); self.getAllInputs($(this).parent()); }); // Refresh our hidden field if any fields change $(document).on('change', '.merchant-sortable-repeater.sortable', function () { self.getAllInputs($(this).parent()); }); $(document).on('focusout', '.merchant-sortable-repeater.sortable .repeater-input', function () { self.getAllInputs($(this).parent()); }); }, /** * Append a new row to our list of elements. * */ appendRow: function appendRow($element) { var defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var newRow = ''; $element.find('.sortable').append(newRow); var $newItem = $element.find('.sortable').find('.repeater:last'); $newItem.slideDown('slow', function () { $(this).find('input').focus(); }); // Make Repeater fields sortable; Putting here works better with flexible content this.makeFieldsSortable($element.find('.sortable')); $(document).trigger('merchant-sortable-repeater-item-added', [$newItem, $element.find('.sortable')]); }, makeFieldsSortable: function makeFieldsSortable($sortableElement) { if (!$sortableElement.hasClass('disable-sorting')) { $sortableElement.sortable({ update: function update(event, ui) { SortableRepeaterField.getAllInputs($sortableElement.parent()); } }); } }, /** * Get the values from the repeater input fields and add to our hidden field. * */ getAllInputs: function getAllInputs($element) { var inputValues = $element.find('.repeater-input').map(function () { return $(this).val(); }).toArray(); // Keep one empty item if all deleted. if (!inputValues.length) { inputValues.push(''); } // Add all the values from our repeater fields to the hidden field (which is the one that actually gets saved) $element.find('.merchant-sortable-repeater-input').val(JSON.stringify(inputValues)); // Important! Make sure to trigger change event so Customizer knows it has to save the field $element.find('.merchant-sortable-repeater-input').trigger('change'); $element.find('.merchant-sortable-repeater-input').trigger('sortable.repeater.change'); } }; // Initialize Sortable Repeater. SortableRepeaterField.init(); // Sortable Repeater Icons. var SortableRepeaterIconsField = { CONTROL: '.merchant-sortable-repeater-icons-control', init: function init() { var self = this; $(self.CONTROL).each(function () { self.populate($(this)); }); this.events(); }, /** * Populate rows from the hidden JSON input. */ populate: function populate($control) { var self = this; var items = []; try { items = JSON.parse($control.find('.merchant-sortable-repeater-input').val()); } catch (e) { items = []; } if (!Array.isArray(items) || items.length === 0) { return; } var $firstRow = $control.find('.repeater').first(); // First item populates the existing row. var firstItem = self.normalizeItem(items[0]); $firstRow.find('.repeater-input').val(firstItem.text); self.setRowIcon($firstRow, firstItem.icon); // Remaining items get new rows (silent = true to avoid triggering save bar). for (var i = 1; i < items.length; i++) { var item = self.normalizeItem(items[i]); self.appendRow($control, item.text, item.icon, true); } }, events: function events() { var self = this; // Toggle icon picker dropdown. $(document).on('click', self.CONTROL + ' .merchant-icon-picker-toggle', function (e) { e.preventDefault(); e.stopPropagation(); var $dropdown = $(this).siblings('.merchant-icon-picker-dropdown'); // Close all other dropdowns first. $(self.CONTROL + ' .merchant-icon-picker-dropdown').not($dropdown).hide(); $dropdown.toggle(); }); // Select icon from dropdown. $(document).on('click', self.CONTROL + ' .merchant-icon-option', function (e) { e.preventDefault(); e.stopPropagation(); var $repeater = $(this).closest('.repeater'); var iconKey = $(this).data('icon') || ''; self.setRowIcon($repeater, iconKey); $(this).closest('.merchant-icon-picker-dropdown').hide(); self.syncHiddenInput($(this).closest(self.CONTROL)); }); // Close dropdown on outside click. $(document).on('click', function () { $(SortableRepeaterIconsField.CONTROL + ' .merchant-icon-picker-dropdown').hide(); }); // Prevent dropdown clicks from closing. $(document).on('click', self.CONTROL + ' .merchant-icon-picker-dropdown', function (e) { e.stopPropagation(); }); // Delete row. $(document).on('click', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable .customize-control-sortable-repeater-delete', function (e) { e.preventDefault(); var $control = $(this).closest(self.CONTROL); $(this).parent().slideUp('fast', function () { $(this).remove(); self.syncHiddenInput($control); }); $(document).trigger('merchant-sortable-repeater-item-deleted'); }); // Add new row. $(document).on('click', self.CONTROL + ' .customize-control-sortable-repeater-icons-add', function (e) { e.preventDefault(); var $control = $(this).closest(self.CONTROL); self.appendRow($control, '', ''); self.syncHiddenInput($control); }); // Text change → sync. $(document).on('focusout', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable .repeater-input', function () { self.syncHiddenInput($(this).closest(self.CONTROL)); }); // Sort change → sync. $(document).on('change', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable', function () { self.syncHiddenInput($(this).closest(self.CONTROL)); }); // Re-init on flexible content add. $(document).on('merchant-flexible-content-added', function (e, $layout) { var $control = $layout.find(self.CONTROL); if ($control.length) { self.populate($control); self.makeFieldsSortable($control.find('.sortable')); } }); }, /** * Append a new repeater row. */ appendRow: function appendRow($control, text, icon, silent) { var self = this; var iconLibrary = self.getIconLibrary($control); var dropdownHtml = ''; var escapedText = $('
').text(text).html(); var newRow = $(''); $control.find('.sortable').append(newRow); if (silent) { newRow.show(); } else { newRow.slideDown('slow', function () { $(this).find('input').focus(); }); } if (icon) { self.setRowIcon(newRow, icon); } self.makeFieldsSortable($control.find('.sortable')); $(document).trigger('merchant-sortable-repeater-item-added', [newRow, $control.find('.sortable')]); }, /** * Set the icon on a repeater row. */ setRowIcon: function setRowIcon($row, iconKey) { var $toggle = $row.find('.merchant-icon-picker-toggle'); $toggle.attr('data-icon', iconKey); if (iconKey) { var $control = $row.closest(SortableRepeaterIconsField.CONTROL); var iconLibrary = this.getIconLibrary($control); var svg = iconLibrary[iconKey] || ''; if (svg) { $toggle.html(svg).addClass('has-icon'); } else { $toggle.html('').removeClass('has-icon'); } } else { $toggle.html('').removeClass('has-icon'); } // Mark selected option in dropdown. $row.find('.merchant-icon-option').removeClass('selected'); $row.find('.merchant-icon-option[data-icon="' + iconKey + '"]').addClass('selected'); }, /** * Sync all rows back to the hidden JSON input. */ syncHiddenInput: function syncHiddenInput($control) { var items = []; $control.find('.merchant-sortable-repeater-icons .repeater').each(function () { var text = $(this).find('.repeater-input').val(); var icon = $(this).find('.merchant-icon-picker-toggle').attr('data-icon') || ''; items.push({ text: text, icon: icon }); }); if (!items.length) { items.push({ text: '', icon: '' }); } $control.find('.merchant-sortable-repeater-input').val(JSON.stringify(items)); $control.find('.merchant-sortable-repeater-input').trigger('change'); $control.find('.merchant-sortable-repeater-input').trigger('sortable.repeater.change'); }, makeFieldsSortable: function makeFieldsSortable($sortable) { var self = this; if (!$sortable.hasClass('disable-sorting')) { $sortable.sortable({ handle: '.dashicons-menu', update: function update() { self.syncHiddenInput($sortable.closest(self.CONTROL)); } }); } }, getIconLibrary: function getIconLibrary($control) { if (!$control.data('_iconLibrary')) { try { $control.data('_iconLibrary', JSON.parse($control.attr('data-icons')) || {}); } catch (e) { $control.data('_iconLibrary', {}); } } return $control.data('_iconLibrary'); }, normalizeItem: function normalizeItem(item) { if (typeof item === 'string') { return { text: item, icon: '' }; } return { text: item.text || '', icon: item.icon || '' }; } }; // Initialize Sortable Repeater Icons. SortableRepeaterIconsField.init(); var GroubField = { init: function init() { var self = this; self.initAccordion(); self.initFlag(); }, initAccordion: function initAccordion() { var self = this; $('.merchant-group-field.has-accordion').each(function () { var element = $(this); element.accordion({ collapsible: true, header: "> .title-area", heightStyle: "content", active: element.hasClass('open') ? 0 : false }); }); }, initFlag: function initFlag() { $('.merchant-group-field.has-flag').each(function () { var element = $(this); var field_id = element.data('id'); var status_field = element.find(".merchant-field-".concat(field_id, "_status select")); var selected_value = status_field.val(); var selected_label = status_field.find('option:selected').text(); var status_element = element.find('.field-status'); status_element.removeClass('hidden active inactive').text(selected_label).addClass(selected_value); }); } }; var ReviewsSelector = { accordion: null, activePopupContainer: null, // Initialize the Reviews Selector init: function init() { var self = this; self.initAccordion(); self.events(); // Skip the flexible content hidden elements that will be cloned when initialize the sortable reviews functionality self.makeSelectedReviewsSortable($('.merchant-reviews-selector').not('.layouts .merchant-reviews-selector')); }, // Set up event listeners events: function events() { var self = this; $(document).on('input', '.merchant-reviews-selector .products-search', self.ajaxSearch.bind(self)); $(document).on('change', '.merchant-reviews-selector .product-review input[type="checkbox"]', self.toggleReview.bind(self)); $(document).on('click', '.merchant-reviews-selector .review-photo img', self.requestReviewImages.bind(self)); $(document).on('click', '.review-photos-popup .overlay', self.dismissImagesGallery.bind(self)); $(document).on('click', '.merchant-reviews-selector .product-reviews-load-more button', self.loadMoreReviews.bind(self)); $(document).on('click', '.merchant-reviews-selector .popup-trigger', self.initPopUp.bind(self)); $(document).on('click', '.merchant-reviews-selector .popup-header .close, .merchant-reviews-selector > .overlay', self.dismissPopUp.bind(self)); $(document).on('click', '.merchant-reviews-selector .product-review-delete', self.deleteSelectedReview.bind(self)); // Listen to escape key $(document).on('keyup', function (e) { if (e.key === "Escape") { var isGallery = self.dismissImagesGallery(); if (isGallery) { return; } self.dismissPopUp(self); } }); $(document).on('merchant-flexible-content-added', function (e, layout) { self.makeSelectedReviewsSortable(); self.initAccordion(); }); }, // Make selected reviews sortable makeSelectedReviewsSortable: function makeSelectedReviewsSortable() { var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $(document).find('.merchant-reviews-selector').not('.layouts .merchant-reviews-selector'); var self = this; var selectedReviews = container.find('.selected-reviews .product-reviews'); selectedReviews.sortable({ axis: 'y', cursor: 'move', helper: 'original', handle: '.product-review-move', cancel: '', placeholder: 'placeholder', // Add a class for styling update: function update(event, ui) { var container = ui.item.closest('.merchant-reviews-selector'); self.saveModifications(container); }, start: function start(event, ui) { ui.placeholder.height(ui.item.height()); // Match height } }); // $('.product-reviews').sortable('refresh'); // console.log('selectedReviews',selectedReviews.sortable('instance')) }, // Delete selected review deleteSelectedReview: function deleteSelectedReview(e) { var self = this; var review = $(e.target).closest('.product-review'); var container = review.closest('.merchant-reviews-selector'); review.remove(); self.saveModifications(container); self.updateSelectedReviewsCheckboxState(container); self.countSelectedReviewsInProduct(container); }, // Handle AJAX search for reviews ajaxSearch: function ajaxSearch(e) { var self = this; var searchField = $(e.target); var container = searchField.closest('.merchant-reviews-selector'); var header = container.find('.popup-header'); // check value length > 3 if (searchField.val().length === 0 || searchField.val().length >= 3) { $.ajax({ url: merchant_admin_options.ajaxurl, type: 'POST', data: { action: 'merchant_search_reviews', search: searchField.val(), nonce: merchant_admin_options.ajaxnonce }, beforeSend: function beforeSend() { header.addClass('loading'); self.destroyAccordion(container); }, success: function success(response) { if (response.success) { header.removeClass('popup-error'); container.find('.products-search-results').html(response.data); self.initAccordion(container); } else { header.addClass('popup-error'); } }, complete: function complete() { header.removeClass('loading'); }, error: function error() { header.addClass('popup-error'); } }); } }, // Load more reviews loadMoreReviews: function loadMoreReviews(e) { var self = this; var product = $(e.target).closest('.product-item'); var offset = product.find('.product-review').length; $.ajax({ url: merchant_admin_options.ajaxurl, type: 'POST', data: { action: 'merchant_load_more_reviews', product_id: product.attr('data-id'), offset: offset, nonce: merchant_admin_options.ajaxnonce }, beforeSend: function beforeSend() { product.addClass('loading'); }, success: function success(response) { if (response.success) { product.find('.product-reviews .reviews-wrapper').append(response.data.reviews); if (response.data.load_more === '') { self.hideLoadMoreButton(product); } self.updateSelectedReviewsCheckboxState(product.closest('.merchant-reviews-selector')); self.countSelectedReviewsInProduct(product.closest('.merchant-reviews-selector')); } }, complete: function complete() { product.removeClass('loading'); } }); }, // Hide load more button hideLoadMoreButton: function hideLoadMoreButton(product) { product.find('.product-reviews-load-more').remove(); }, // Initialize the popup initPopUp: function initPopUp(e) { var self = this; var container = $(e.target).closest('.merchant-reviews-selector'); var popup = container.find('.selector-popup'); var overlay = container.find('.overlay'); popup.addClass('active'); overlay.addClass('active'); self.activePopupContainer = container; }, // Dismiss the popup dismissPopUp: function dismissPopUp() { var self = this; var container = self.activePopupContainer; if (container === null) { return; } var popup = container.find('.selector-popup'); var overlay = container.find('.overlay'); popup.removeClass('active'); overlay.removeClass('active'); self.activePopupContainer = null; }, // Initialize the accordion initAccordion: function initAccordion() { var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $(document).find('.merchant-reviews-selector'); var self = this; container.each(function () { var elements = $(this).find('.popup-content .product-item.product-item-has-reviews'); elements.each(function () { elements.accordion({ collapsible: true, heightStyle: "content", icons: false, // Disable icons active: elements.hasClass('opened') ? 0 : false, activate: function activate(event, ui) { // Check if a new panel is being activated if (ui.newPanel.length) { // Add 'opened' class to the parent product item ui.newPanel.parent().addClass('opened'); } else { // Remove 'opened' class from all product items elements.removeClass('opened'); } } }); }); elements.find('a').on('click', function (e) { e.stopPropagation(); // Prevent event from bubbling up to the accordion header }); self.updateSelectedReviewsCheckboxState($(this)); self.countSelectedReviewsInProduct($(this)); }); }, // Destroy the accordion destroyAccordion: function destroyAccordion(container) { var elements = container.find('.selector-popup .product-item.product-item-has-reviews'); elements.each(function () { if ($(this).hasClass('ui-accordion')) { // Check if it has the accordion class $(this).accordion('destroy'); } }); }, // Toggle review selection toggleReview: function toggleReview(e) { var self = this; var checked = $(e.target).prop('checked'); var reviewsFieldContainer = $(e.target).closest('.merchant-reviews-selector'); var review = $(e.target).closest('.product-review'); var reviewId = review.attr('data-id'); var selectedReviews = reviewsFieldContainer.find('.selected-reviews .product-reviews'); var selectedReview = selectedReviews.find(".product-review[data-id=\"".concat(reviewId, "\"]")); if (checked) { if (selectedReview.length === 0) { var newReview = review.clone(); selectedReviews.append(newReview); self.makeSelectedReviewsSortable(); } } else { selectedReview.remove(); } setTimeout(function () { self.saveModifications(reviewsFieldContainer); }, 150); }, // Save modifications to the selected reviews saveModifications: function saveModifications(container) { var self = this; var reviewsSavedIdsField = container.find('.review-saved-ids'); var selectedReviews = container.find('.selected-reviews .product-reviews'); var reviews = selectedReviews.find('.product-review'); var reviewsIds = []; reviews.each(function () { var id = $(this).attr('data-id'); reviewsIds.push(id); }); reviewsSavedIdsField.val(reviewsIds.join(',')); reviewsSavedIdsField.trigger('change'); // reviewsSavedIds; self.updateSelectedReviewsCheckboxState(container); self.countSelectedReviewsInProduct(container); }, // Mark selected reviews checkbox updateSelectedReviewsCheckboxState: function updateSelectedReviewsCheckboxState(container) { var popupContent = container.find('.selector-popup'); var reviewsSavedIds = container.find('.review-saved-ids'); var reviews = popupContent.find('.product-review'); var reviewsIds = reviewsSavedIds.val().split(','); reviews.each(function () { var reviewId = $(this).attr('data-id'); var checkbox = container.find(".product-review[data-id=\"".concat(reviewId, "\"] input[type=\"checkbox\"]")); if (reviewsIds.includes(reviewId)) { checkbox.prop('checked', true); } else { checkbox.prop('checked', false); } }); }, // Count selected reviews in product countSelectedReviewsInProduct: function countSelectedReviewsInProduct(container) { var self = this; var products = container.find('.selector-popup .product-item'); var reviewsSavedIds = container.find('.review-saved-ids'); var reviewsIds = reviewsSavedIds.val().split(','); products.each(function () { var selectedReviews = 0; var product = $(this); var productReviews = product.find('.product-review'); productReviews.each(function () { var review = $(this); var reviewId = review.attr('data-id'); if (reviewsIds.includes(reviewId)) { selectedReviews++; } }); product.find('.selected-reviews-count .counter').text(selectedReviews); }); }, // Request review images requestReviewImages: function requestReviewImages(e) { var reviewContainer = $(e.target).closest('.product-review'); var image = $(e.target); var reviewId = reviewContainer.attr('data-id'); $.ajax({ url: merchant_admin_options.ajaxurl, type: 'POST', data: { action: 'merchant_get_review_images', review_id: reviewId, nonce: merchant_admin_options.ajaxnonce }, beforeSend: function beforeSend() { // Show loading spinner image.addClass('loading'); }, success: function success(response) { if (response.success) { $('body').append(response.data); setTimeout(function () { $('body').find('.review-photos-popup').addClass('active'); }, 300); } }, complete: function complete() { // Hide loading spinner image.removeClass('loading'); } }); }, // Dismiss the images gallery dismissImagesGallery: function dismissImagesGallery() { var gallery = $('body').find('.review-photos-popup'); if (gallery.length) { gallery.removeClass('active'); setTimeout(function () { gallery.remove(); }, 300); return true; } return false; } }; // Flexible Content. var FlexibleContentField = { init: function init(field) { var self = this; // Update the values for all our input fields and initialise the sortable repeater. $('.merchant-flexible-content-control').each(function () { var hasAccordion = $(this).hasClass('has-accordion'), $content = $(this).find('.merchant-flexible-content'); var campaignId = params.get('campaign_id'); var campaignIndex = 0; // Find the campaignIndex based on campaignId if (campaignId) { $content.find('input.flexible-id').each(function (index) { if ($(this).val() === campaignId) { campaignIndex = index; return false; } }); } if (hasAccordion) { $content.accordion({ active: campaignIndex, // Open the campaign with campaign index collapsible: true, //header: "> div > .layout-header", header: function header(elem) { return elem.find('.layout__inner > .layout-header'); }, heightStyle: "content", beforeActivate: function beforeActivate(event, ui) { // Prevent accordion toggle when clicking the status badge. if (event.originalEvent && $(event.originalEvent.target).hasClass('layout-status')) { event.preventDefault(); return; } // Hydrate deferred layout before the accordion animation // so the panel opens with content already rendered. if (ui.newPanel.length) { var $layout = ui.newPanel.closest('.layout'); if ($layout.attr('data-deferred') === '1') { self.hydrateLayout($layout); } } } }).sortable({ axis: 'y', cursor: 'move', helper: 'original', handle: '.customize-control-flexible-content-move', stop: function stop(event, ui) { $content.trigger('merchant.sorted'); self.refreshNumbers($content); $content.accordion("refresh"); } }); } else { $content.sortable({ axis: 'y', cursor: 'move', helper: 'original', handle: '.customize-control-flexible-content-move', stop: function stop(event, ui) { $content.trigger('merchant.sorted'); self.refreshNumbers($content); $content.accordion("refresh"); } }); } }); this.updateLayoutTitle(); this.updateLayoutStatus(); this.updateDiscountPercentMaxVal(); // Events. this.events(); }, updateLayoutTitle: function updateLayoutTitle() { // Update the title for all layout header. $('.merchant-flexible-content .layout').each(function () { var title = $(this).find('.layout-title[data-title-field]'); if (title.length) { var input = $(this).find('.layout-body .merchant-field-' + title.data('title-field') + ' input'); input.on('change keyup', function () { title.text($(this).val()); }); title.text(input.val()); } }); }, /** * Resolve a status field inside a layout, returning a unified adapter * that works with select, radio, and switcher field types. * * @param {jQuery} $layout The .layout element. * @param {string} fieldId The field identifier (e.g. 'campaign_status'). * @returns {object|null} Adapter with getValue, getLabel, getOptions, setNext, onChange — or null. */ resolveStatusField: function resolveStatusField($layout, fieldId) { var $wrapper = $layout.find('.layout-body .merchant-field-' + fieldId); if (!$wrapper.length) return null; var $select = $wrapper.find('select'); var $radios = $wrapper.find('input[type="radio"]'); var $checkbox = $wrapper.find('input[type="checkbox"]'); // Select field adapter. if ($select.length) { return { getValue: function getValue() { return $select.val() || 'active'; }, getLabel: function getLabel() { return $select.find('option:selected').text() || this.getValue(); }, getOptions: function getOptions() { return $select.find('option').map(function () { return $(this).val(); }).get(); }, setNext: function setNext() { var opts = $select.find('option'); var idx = opts.index(opts.filter(':selected')); var next = (idx + 1) % opts.length; $select.val(opts.eq(next).val()).trigger('change'); }, onChange: function onChange(fn) { $select.off('change.statusBadge').on('change.statusBadge', fn); } }; } // Radio field adapter. if ($radios.length) { return { getValue: function getValue() { return $radios.filter(':checked').val() || 'active'; }, getLabel: function getLabel() { var $checked = $radios.filter(':checked'); var $label = $wrapper.find('label[for="' + $checked.attr('id') + '"]'); return $label.length ? $label.text() : this.getValue(); }, getOptions: function getOptions() { return $radios.map(function () { return $(this).val(); }).get(); }, setNext: function setNext() { var vals = this.getOptions(); var idx = vals.indexOf(this.getValue()); var next = (idx + 1) % vals.length; $radios.filter('[value="' + vals[next] + '"]').prop('checked', true).trigger('change'); }, onChange: function onChange(fn) { $radios.off('change.statusBadge').on('change.statusBadge', fn); } }; } // Switcher (checkbox) field adapter — on/off. if ($checkbox.length) { return { getValue: function getValue() { return $checkbox.is(':checked') ? 'active' : 'inactive'; }, getLabel: function getLabel() { return $checkbox.is(':checked') ? 'Active' : 'Inactive'; }, getOptions: function getOptions() { return ['active', 'inactive']; }, setNext: function setNext() { $checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change'); }, onChange: function onChange(fn) { $checkbox.off('change.statusBadge').on('change.statusBadge', fn); } }; } return null; }, updateLayoutStatus: function updateLayoutStatus() { var self = this; // Sync status badge on each layout header with the field value. $('.merchant-flexible-content .layout').each(function () { var $badge = $(this).find('.layout-header .layout-status[data-status-field]'); if (!$badge.length) return; var fieldId = $badge.data('status-field'); var adapter = self.resolveStatusField($(this), fieldId); if (!adapter) return; function syncBadge() { var val = adapter.getValue(); var label = adapter.getLabel(); $badge.removeClass(function (i, cls) { return (cls.match(/layout-status--\S+/g) || []).join(' '); }).addClass('layout-status--' + val).text(label.trim()); } adapter.onChange(syncBadge); syncBadge(); }); // Delegated click handler for badge toggle — works on all rows including deferred. $(document).off('click.statusToggle').on('click.statusToggle', '.layout-status[data-status-field]', function (e) { e.stopPropagation(); e.preventDefault(); var $badge = $(this); var fieldId = $badge.data('status-field'); var $layout = $badge.closest('.layout'); var adapter = self.resolveStatusField($layout, fieldId); if (adapter) { // Hydrated row: toggle via the field adapter. adapter.setNext(); } else { // Deferred row: cycle through options stored in data-status-options, // falling back to active/inactive. var optionsAttr = $badge.attr('data-status-options'); var options = optionsAttr ? optionsAttr.split(',') : ['active', 'inactive']; var currentVal = $badge.attr('class').match(/layout-status--(\S+)/); currentVal = currentVal ? currentVal[1] : options[0]; var idx = options.indexOf(currentVal); var nextIdx = (idx + 1) % options.length; var newVal = options[nextIdx]; var newLabel = newVal.charAt(0).toUpperCase() + newVal.slice(1); $badge.removeClass('layout-status--' + currentVal).addClass('layout-status--' + newVal).text(newLabel); // Update data-fields-json so the value persists when hydrated. var jsonAttr = $layout.attr('data-fields-json'); if (jsonAttr) { try { var data = JSON.parse(jsonAttr); if (!data.values) data.values = {}; data.values[fieldId] = newVal; $layout.attr('data-fields-json', JSON.stringify(data)); } catch (ex) {/* ignore parse errors */} } // Mark the form as changed. $('.merchant-module-page-content').trigger('change.merchant'); } }); }, updateDiscountPercentMaxVal: function updateDiscountPercentMaxVal() { $('.merchant-flexible-content .layout').each(function () { var $layout = $(this); var $discountType = $layout.find('.merchant-module-page-setting-field[data-id="discount_type"]'); var $discountVal = $layout.find('.merchant-module-page-setting-field[data-id="discount_value"]'); $discountVal = $discountVal.length ? $discountVal : $layout.find('.merchant-module-page-setting-field[data-id="discount"]'); $discountVal = $discountVal.length ? $discountVal : $layout.find('.merchant-module-page-setting-field[data-id="discount_amount"]'); var checkedValue = $discountType.find('input:checked').val(); // Set/Remove max value based on the discount type. checkedValue === 'percentage_discount' || checkedValue === 'percentage' ? $discountVal.find('input').attr('max', 100) : $discountVal.find('input').removeAttr('max'); }); $('.merchant-module-page-setting-fields'); }, generateUUID: function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0; var v = c === 'x' ? r : r & 0x3 | 0x8; return v.toString(16); }); }, events: function events() { var self = this; $(document).on('change', '.merchant-module-page-setting-field[data-id="discount_type"] input', function () { self.updateDiscountPercentMaxVal(); }); // Update "selected" attribute on select. It's Required for duplicating layouts. $(document).on('change', 'select', function () { var selectedValue = $(this).val(); var isMultiple = $(this).prop('multiple'); $(this).find('option').each(function () { var currentValue = $(this).val(); var isSelected = isMultiple ? selectedValue.includes(currentValue) : currentValue === selectedValue; $(this).attr('selected', isSelected); }); }); $('.customize-control-flexible-content-add-button').on('click', function (event) { event.preventDefault(); event.stopImmediatePropagation(); if ($(this).parent().find('.customize-control-flexible-content-add-list a').length === 1) { // If there is only one layout, trigger click on it. $(this).parent().find('.customize-control-flexible-content-add-list a').trigger('click'); return; } $(this).parent().find('.customize-control-flexible-content-add-list').toggleClass('active'); }); // Add new item $(document).on('click', '.customize-control-flexible-content-add', function (event) { event.preventDefault(); event.stopImmediatePropagation(); var $field = $('.merchant-flexible-content-control[data-id=' + $(this).data('id') + ']'); var $layouts = $field.find('.layouts'); var $selected = $(this).data('layout'); var $layout = $layouts.find('.layout[data-type=' + $selected + ']').clone(true); var $content = $field.find('.merchant-flexible-content'); var $items = $content.find('.layout'); var uuid = self.generateUUID(); $layout.find('input, select, textarea').each(function () { if ($(this).data('name')) { $(this).attr('name', $(this).data('name').replace('0', $items.length)); } if ($(this).is(':checkbox, :radio') && $(this).attr('checked')) { $(this).prop('checked', true); } }); $layout.attr('data-layout-id', uuid); $layout.find('.layout-count').text($items.length + 1); $layout.find('.flexible-id').val(uuid); $content.append($layout); $content.removeClass('empty'); $(this).parent().removeClass('active'); if ($layout.find('.merchant-module-page-setting-field-upload').length) { initUploadField($layout.find('.merchant-module-page-setting-field-upload')); } if ($layout.find('.merchant-module-page-setting-field-select_ajax').length) { initSelectAjax($layout.find('.merchant-module-page-setting-field-select_ajax')); } var parentDiv = $(this).closest('.merchant-flexible-content-control'), hasAccordion = parentDiv.hasClass('has-accordion'); if (hasAccordion) { parentDiv.find('.merchant-flexible-content').accordion("refresh"); // Expand the accordion last added parentDiv.find('.merchant-flexible-content').accordion("option", "active", -1); } GroubField.init(); $(document).trigger('merchant-flexible-content-added', [$layout]); initMerchantRange(); self.updateLayoutTitle(); self.updateLayoutStatus(); self.updateDiscountPercentMaxVal(); $('.merchant-module-page-content').trigger('change.merchant'); }); // Duplicate item $(document).on('click', '.customize-control-flexible-content-duplicate', function (event) { event.preventDefault(); event.stopImmediatePropagation(); var $duplicateBtn = $(this); var $flexibleContentWrapper = $duplicateBtn.closest('.merchant-flexible-content-control[data-id=' + $duplicateBtn.data('id') + ']'); var $flexibleContent = $flexibleContentWrapper === null || $flexibleContentWrapper === void 0 ? void 0 : $flexibleContentWrapper.find('.merchant-flexible-content'); if (!$flexibleContentWrapper.length || !$flexibleContent.length) { return; } var $sourceLayout = $duplicateBtn.closest('.layout'); if (!$sourceLayout.length) { return; } $sourceLayout.find('.layout-actions__inner').hide(); // Clone the layout without data & events. var $clonedLayout = $sourceLayout.clone(); var $items = $flexibleContent.find('.layout'); var index = $sourceLayout.find('.layout-count').text(); var uuid = self.generateUUID(); $clonedLayout.attr('data-layout-id', uuid); $clonedLayout.find('.flexible-id').val(uuid); $clonedLayout.find('input, select, textarea').each(function () { var $input = $(this); var inputName = $input.attr('name'); if (inputName) { var prefix = inputName.split('[')[0]; var indexPart = inputName.match(/\[(.*?)\]/g); if (indexPart && indexPart.length > 1) { indexPart[1] = '[' + index + ']'; var newName = "".concat(prefix).concat(indexPart.join('')); $input.attr('name', newName); } } }); // Find select2, Remove and Re-init again. select.select2('destroy') doesn't work. $clonedLayout.find('select').each(function () { if ($(this).hasClass('select2-hidden-accessible')) { $(this).removeClass('select2-hidden-accessible').removeAttr('data-live-search').removeAttr('data-select2-id').removeAttr('aria-hidden').removeAttr('tabindex'); // Remove the existing dropdown $(this).nextAll('.select2-container').remove(); // Re-init $(this).select2(); } }); // Removing copied style to make the accordion work properly. $clonedLayout.find('.layout-body').removeAttr('style'); // Append the cloned layout right after the source one with fadeIn effect. $clonedLayout.hide(); $clonedLayout.insertAfter($sourceLayout); $clonedLayout.fadeIn(); if ($clonedLayout.find('.merchant-module-page-setting-field-upload').length) { initUploadField($clonedLayout.find('.merchant-module-page-setting-field-upload')); } if ($clonedLayout.find('.merchant-module-page-setting-field-select_ajax').length) { initSelectAjax($clonedLayout.find('.merchant-module-page-setting-field-select_ajax')); } self.refreshNumbers($flexibleContent); $(document).trigger('merchant-flexible-content-added', [$clonedLayout]); if ($flexibleContentWrapper.hasClass('has-accordion')) { $flexibleContent.accordion('refresh'); } self.updateLayoutTitle(); self.updateLayoutStatus(); GroubField.init(); $('.merchant-module-page-content').trigger('change.merchant'); }); // Delete item $(document).on('click', '.customize-control-flexible-content-delete', function (event) { event.preventDefault(); var $item = $(this).closest('.layout'); var $content = $item.parent(); $item.remove(); if ($content.find('.layout').length === 0) { $content.addClass('empty'); } self.refreshNumbers($content); $(document).trigger('merchant-flexible-content-deleted', [$item]); var parentDiv = $(this).closest('.merchant-flexible-content-control'), hasAccordion = parentDiv.hasClass('has-accordion'); if (hasAccordion) { parentDiv.find('.merchant-flexible-content').accordion("refresh"); } $('.merchant-module-page-content').trigger('change.merchant'); }); // Toggle Actions(delete/duplicate) $(document).on('click', '.layout-actions__toggle', function (e) { e.preventDefault(); // Hide other opened elements hideOtherActions($(this).closest('.layout')); // Toggle the current element $(this).closest('.layout-actions').find('.layout-actions__inner').stop().slideToggle(300); }); // Hide Actions when collapse/open $(document).on('click', '.layout-header', function () { hideOtherActions($(this).closest('.layout')); }); $(document).on('merchant-flexible-content-added', function (e, $layout) { hideOtherActions($layout); }); function hideOtherActions($layout) { if ($layout && $layout.length) { $layout.siblings().find('.layout-actions__inner').slideUp(300); } } // Dismiss actions menu on click outside or Escape. $(document).on('click', function (e) { if (!$(e.target).closest('.layout-actions').length) { $('.layout-actions__inner').slideUp(300); } }); $(document).on('keydown', function (e) { if (e.key === 'Escape') { $('.layout-actions__inner').slideUp(300); } }); }, /** * Hydrate a deferred layout row. * * Clones the hidden template for the layout type, populates field values * from the data-fields-json attribute, initialises all widgets, and * replaces the empty layout-body content with the fully rendered fields. * * @param {jQuery} $layout The .layout element with data-deferred="1". */ hydrateLayout: function hydrateLayout($layout) { var jsonStr = $layout.attr('data-fields-json'); if (!jsonStr) { return; } var data; try { data = JSON.parse(jsonStr); } catch (e) { return; } var layoutType = $layout.attr('data-type'); var $control = $layout.closest('.merchant-flexible-content-control'); var $template = $control.find('.layouts .layout[data-type="' + layoutType + '"]'); if (!$template.length) { return; } // Clone the template's layout-body content (the rendered fields). var $clonedBody = $template.find('.layout-body').clone(); // Determine the correct row index from layout-count. var rowIndex = parseInt($layout.find('.layout-count').text(), 10) - 1; var fieldId = $control.attr('data-id'); // Fix name attributes: replace data-name with name, and index [0] with [rowIndex]. $clonedBody.find('input, select, textarea').each(function () { var dataName = $(this).attr('data-name'); if (dataName) { $(this).attr('name', dataName.replace('[0]', '[' + rowIndex + ']')); $(this).removeAttr('data-name'); } }); // Populate field values from the resolved data. this.populateFieldValues($clonedBody, data, fieldId, rowIndex); // Sync color picker preview boxes to reflect populated values. $clonedBody.find('.merchant-color').each(function () { var colorVal = $(this).find('.merchant-color-input').val(); if (colorVal) { $(this).find('.merchant-color-picker').css('background-color', colorVal); } }); // Sync range slider values from their sibling number inputs. // populateFieldValues only sets the number input (it has the name attribute); // the range slider (no name) still holds the template default. $clonedBody.find('.merchant-range').each(function () { var numVal = $(this).find('.merchant-range-number-input').val(); if (numVal !== undefined && numVal !== '') { $(this).find('.merchant-range-input').val(numVal); } }); // Swap the content of the layout-body (not the element itself) // to preserve jQuery UI accordion's internal panel reference. var $existingBody = $layout.find('.layout-body'); $existingBody.empty().append($clonedBody.children()); // Initialise widgets within the hydrated layout. if ($existingBody.find('.merchant-module-page-setting-field-upload').length) { initUploadField($existingBody.find('.merchant-module-page-setting-field-upload')); } if ($existingBody.find('.merchant-module-page-setting-field-select_ajax').length) { initSelectAjax($existingBody.find('.merchant-module-page-setting-field-select_ajax')); } // Trigger events to re-initialise fields_group, color pickers, conditions, etc. $(document).trigger('merchant-flexible-content-added', [$layout]); GroubField.init(); initMerchantRange(); // Init layout title binding. var $title = $layout.find('.layout-title[data-title-field]'); if ($title.length) { var titleFieldId = $title.attr('data-title-field'); var $titleInput = $layout.find('.layout-body .merchant-field-' + titleFieldId + ' input'); $titleInput.on('change keyup', function () { $title.text($(this).val()); }); } // Init status badge binding for hydrated row. var $badge = $layout.find('.layout-header .layout-status[data-status-field]'); if ($badge.length) { var statusFieldId = $badge.data('status-field'); var statusAdapter = this.resolveStatusField($layout, statusFieldId); if (statusAdapter) { var syncHydratedBadge = function syncHydratedBadge() { var val = statusAdapter.getValue(); var label = statusAdapter.getLabel(); $badge.removeClass(function (i, cls) { return (cls.match(/layout-status--\S+/g) || []).join(' '); }).addClass('layout-status--' + val).text(label.trim()); }; statusAdapter.onChange(syncHydratedBadge); syncHydratedBadge(); } } // Init discount max val. this.updateDiscountPercentMaxVal(); // Remove the deferred flag and clean up. $layout.removeAttr('data-deferred'); $layout.removeAttr('data-fields-json'); // Refresh accordion to re-sync panel references after DOM change. var $content = $control.find('.merchant-flexible-content'); if ($content.data('ui-accordion')) { $content.accordion('refresh'); } // Trigger condition checks for the newly hydrated panel. $(document).trigger('merchant-admin-check-fields'); $(document).trigger('merchant-admin-check-color-fields'); }, /** * Populate field values from deferred data into a cloned template body. * * @param {jQuery} $body The cloned .layout-body element. * @param {Object} data The parsed data-fields-json object. * @param {string} fieldId The flexible content field ID. * @param {number} rowIndex The row index for name attribute construction. */ populateFieldValues: function populateFieldValues($body, data, fieldId, rowIndex) { var values = data.values || {}; var selectOptions = data.select_options || {}; var productOptions = data.product_options || {}; var reviewOptions = data.review_options || {}; // Populate simple field values. $body.find('input, select, textarea').each(function () { var $el = $(this); var name = $el.attr('name') || ''; // Handle checkbox_multiple: name ends with [] (e.g. merchant[fc][0][show_pages][]). var arrayMatch = name.match(/\[([^\]]+)\]\[\]$/); if (arrayMatch) { var arrayKey = arrayMatch[1]; var arrVal = values[arrayKey]; if (Array.isArray(arrVal) && $el.is(':checkbox')) { $el.prop('checked', arrVal.indexOf($el.val()) !== -1); } return; } // Extract the field key from name="merchant[fieldId][rowIndex][fieldKey]". var match = name.match(/\[([^\]]+)\]$/); if (!match) { return; } var fieldKey = match[1]; var val = values[fieldKey]; if (val === undefined || val === null) { return; } if ($el.is(':checkbox') && !$el.is(':radio')) { $el.prop('checked', !!parseInt(val, 10)); } else if ($el.is(':radio')) { $el.prop('checked', $el.val() === String(val)); } else if ($el.is('select')) { $el.val(val); } else { $el.val(_typeof(val) === 'object' && val !== null ? JSON.stringify(val) : val); } }); // Populate select_ajax pre-selected options. $.each(selectOptions, function (selectFieldId, options) { var $select = $body.find('[data-id="' + selectFieldId + '"] select'); if (!$select.length) { return; } // Clear any existing options and add resolved ones. $select.empty(); $.each(options, function (_, opt) { $select.append($('