PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
← All changes | assets/js/admin/admin.js +2347 -198 1.62.3.2 View file →
@@ -1,67 +1,171 @@
1 1 "use strict";
2 2
3 +function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
4 +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."); }
5 +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; } }
6 +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; }
7 +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; } }
8 +function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
9 +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); }
3 10 ;
4 11 (function ($, window, document, undefined) {
5 12 'use strict';
6 13
7 14 var merchant = merchant || {};
15 + var params = new URLSearchParams(window.location.search);
16 + var currentModule = params.get('module');
8 17 $(document).ready(function () {
9 - $('.merchant-module-page-setting-field').each(function () {
10 - var $field = $(this);
11 - if ($field.data('condition') && $field.data('condition').length) {
12 - var condition = $field.data('condition');
13 - var $target = $('input[name="merchant[' + condition[0] + ']"],select[name="merchant[' + condition[0] + ']"]');
14 - if ($target.length) {
15 - $target.on('change merchant.change', function (e) {
16 - var $element = $(this);
17 - var passed = false;
18 - switch (condition[1]) {
19 - case '==':
20 - if ($element.attr('type') === 'radio' || $element.attr('type') === 'checkbox') {
21 - if ($element.is(':checked') && $element.val() == condition[2]) {
22 - passed = true;
23 - }
24 - }
25 - if ($element.is('select') && $element.val() == condition[2]) {
26 - passed = true;
27 - }
28 - break;
29 - case 'any':
30 - if ($element.attr('type') === 'radio' || $element.attr('type') === 'checkbox') {
31 - if ($element.is(':checked') && condition[2].split('|').includes($element.val())) {
32 - passed = true;
33 - }
34 - }
35 - if ($element.is('select') && condition[2].split('|').includes($element.val())) {
36 - passed = true;
37 - }
38 - break;
39 - }
40 - if (passed) {
41 - $field.removeClass('merchant-hide').addClass('merchant-show');
42 - } else {
43 - $field.removeClass('merchant-show').addClass('merchant-hide');
44 - }
45 - }).trigger('merchant.change');
18 + // AjaxSave
19 + var $ajaxForm = $('.merchant-module-page-ajax-form');
20 + var $ajaxHeader = $('.merchant-module-page-ajax-header');
21 + var $ajaxSaveBtn = $('.merchant-module-save-button');
22 + $('.merchant-module-page-content').on('change keypress change.merchant', function (e) {
23 + if ($(e.target).hasClass('merchant-backup-file') || $(e.target).hasClass('merchant-search-field')) {
24 + return;
25 + }
26 +
27 + // Nothing in the preview is a setting. Its controls are there to be played
28 + // with, so a module whose mock-up has working fields cannot dirty the form.
29 + if ($(e.target).closest('.merchant-module-page-preview').length) {
30 + return;
31 + }
32 +
33 + // The licence key and the module feedback textarea save through their
34 + // own handlers, so editing them leaves the save bar alone.
35 + if (!$(e.target).is('.merchant-module-question-answer-textarea, .merchant-license-code-input')) {
36 + if (!merchant.show_save) {
37 + $ajaxHeader.addClass('merchant-show');
38 + $ajaxHeader.removeClass('merchant-saving');
39 + merchant.show_save = true;
46 40 }
47 41 }
42 + GroubField.initFlag();
48 43 });
49 44
50 - // AjaxSave
51 - var $ajaxForm = $('.merchant-module-page-ajax-form');
52 - var $ajaxHeader = $('.merchant-module-page-ajax-header');
53 - var $ajaxSaveBtn = $('.merchant-module-save-button');
54 - $('.merchant-module-page-content').on('change keypress change.merchant', ':input:not(.merchant-module-question-answer-textarea):not(.merchant-license-code-input)', function () {
55 - if (!merchant.show_save) {
56 - $ajaxHeader.addClass('merchant-show');
57 - $ajaxHeader.removeClass('merchant-saving');
58 - merchant.show_save = true;
45 + // Warn the user before leaving the page with unsaved changes.
46 + window.addEventListener('beforeunload', function (e) {
47 + if (merchant.show_save) {
48 + e.preventDefault();
59 49 }
60 50 });
61 51 $ajaxForm.ajaxForm({
62 - beforeSubmit: function beforeSubmit() {
52 + beforeSubmit: function beforeSubmit(arr) {
63 53 $ajaxHeader.addClass('merchant-saving');
54 +
55 + // Serialize merchant[] fields into a single JSON payload
56 + // to bypass PHP's max_input_vars limit (Issue #608).
57 + var merchantData = {};
58 + var keepIndexes = [];
59 + arr.forEach(function (field, index) {
60 + if (field.name === 'merchant-search-field') {
61 + keepIndexes.push(index);
62 + return;
63 + }
64 + if (field.name && field.name.indexOf('merchant[') === 0) {
65 + var keys = field.name.match(/\[([^\]]*)\]/g);
66 + if (!keys) {
67 + return;
68 + }
69 + var obj = merchantData;
70 + for (var i = 0; i < keys.length - 1; i++) {
71 + var key = keys[i].slice(1, -1);
72 + if (obj[key] === undefined) {
73 + var nextKey = keys[i + 1] ? keys[i + 1].slice(1, -1) : '';
74 + obj[key] = /^\d+$/.test(nextKey) ? [] : {};
75 + }
76 + obj = obj[key];
77 + }
78 + var lastKey = keys[keys.length - 1].slice(1, -1);
79 + if (lastKey === '') {
80 + // Array notation (e.g. name="...[show_pages][]") — push into parent array.
81 + var parentKey = keys[keys.length - 2].slice(1, -1);
82 + var parentObj = merchantData;
83 + for (var j = 0; j < keys.length - 2; j++) {
84 + parentObj = parentObj[keys[j].slice(1, -1)];
85 + }
86 + if (!Array.isArray(parentObj[parentKey])) {
87 + parentObj[parentKey] = [];
88 + }
89 + parentObj[parentKey].push(field.value);
90 + } else {
91 + obj[lastKey] = field.value;
92 + }
93 + } else {
94 + keepIndexes.push(index);
95 + }
96 + });
97 +
98 + // Ensure unchecked checkboxes inside hydrated FC rows are saved as 0.
99 + // serializeArray() omits unchecked checkboxes entirely, which would
100 + // silently drop the field from the JSON payload and lose the "off" state.
101 + $('.merchant-flexible-content .layout:not([data-deferred])').each(function () {
102 + $(this).find('.layout-body input[type="checkbox"]:not(:checked)').each(function () {
103 + var name = $(this).attr('name') || '';
104 + if (name.indexOf('merchant[') !== 0) {
105 + return;
106 + }
107 +
108 + // Skip checkbox_multiple (name ends with []) — absence = empty array.
109 + if (/\[\]$/.test(name)) {
110 + return;
111 + }
112 + var keys = name.match(/\[([^\]]*)\]/g);
113 + if (!keys) {
114 + return;
115 + }
116 + var obj = merchantData;
117 + for (var i = 0; i < keys.length - 1; i++) {
118 + var key = keys[i].slice(1, -1);
119 + if (obj[key] === undefined) {
120 + obj[key] = {};
121 + }
122 + obj = obj[key];
123 + }
124 + var lastKey = keys[keys.length - 1].slice(1, -1);
125 + if (obj[lastKey] === undefined) {
126 + obj[lastKey] = 0;
127 + }
128 + });
129 + });
130 +
131 + // Merge deferred (non-hydrated) layout row data into merchantData.
132 + // These rows have no form inputs, only a data-fields-json attribute.
133 + $('.merchant-flexible-content .layout[data-deferred="1"]').each(function () {
134 + var jsonStr = $(this).attr('data-fields-json');
135 + if (!jsonStr) {
136 + return;
137 + }
138 + var deferredData;
139 + try {
140 + deferredData = JSON.parse(jsonStr);
141 + } catch (e) {
142 + return;
143 + }
144 + var $control = $(this).closest('.merchant-flexible-content-control');
145 + var fcFieldId = $control.attr('data-id');
146 + var rowIndex = parseInt($(this).find('.layout-count').text(), 10) - 1;
147 + if (!merchantData[fcFieldId]) {
148 + merchantData[fcFieldId] = {};
149 + }
150 +
151 + // Build the row data from deferred values + layout/flexible_id from hidden inputs.
152 + var rowData = $.extend({}, deferredData.values || {});
153 + rowData.layout = $(this).attr('data-type') || '';
154 + rowData.flexible_id = $(this).find('.flexible-id').val() || '';
155 + merchantData[fcFieldId][rowIndex] = rowData;
156 + });
157 + var kept = keepIndexes.map(function (i) {
158 + return arr[i];
159 + });
160 + kept.push({
161 + name: 'merchant_json_payload',
162 + value: JSON.stringify(merchantData)
163 + });
164 + arr.length = 0;
165 + kept.forEach(function (item) {
166 + arr.push(item);
167 + });
64 168 },
65 169 success: function success() {
66 170 $ajaxHeader.removeClass('merchant-show');
67 171 merchant.show_save = false;
@@ -77,19 +181,29 @@
77 181 $(document).off('click.merchant-alert-close');
78 182 }
79 183 });
80 184 }
185 + $(document).trigger('save.merchant', [currentModule]);
81 186 }
82 187 });
83 - $('.merchant-module-question-answer-button').on('click', function (e) {
188 + var $disableModuleSubmitBtn = $('.merchant-module-question-answer-button');
189 + var $disableModuleTextField = $('.merchant-module-question-answer-textarea');
190 + $disableModuleTextField.on('input', function () {
191 + $disableModuleSubmitBtn.prop('disabled', $(this).val().trim() === '');
192 + });
193 + $disableModuleSubmitBtn.on('click', function (e) {
84 194 e.preventDefault();
195 + var message = $disableModuleTextField.val();
196 + if (!message.trim()) {
197 + alert('Please provide the required information.');
198 + return;
199 + }
85 200 var $button = $(this);
86 - var $textarea = $('.merchant-module-question-answer-textarea');
87 201 $('.merchant-module-question-answer-dropdown').removeClass('merchant-show');
88 202 $('.merchant-module-question-thank-you-dropdown').addClass('merchant-show');
89 203 window.wp.ajax.post('merchant_module_feedback', {
90 - subject: $textarea.data('subject'),
91 - message: $textarea.val(),
204 + subject: $disableModuleTextField.attr('data-subject'),
205 + message: message,
92 206 module: $button.closest('.merchant-module-action').find('.merchant-module-page-button-action-activate').data('module'),
93 207 nonce: window.merchant.nonce
94 208 });
95 209 });
@@ -94,8 +208,11 @@
94 208 });
95 209 });
96 210 $('.merchant-module-page-button-action-activate').on('click', function (e) {
97 211 e.preventDefault();
212 + if ($(this).hasClass('merchant-module-deactivated-by-bp')) {
213 + return false;
214 + }
98 215 $('.merchant-module-question-list-dropdown').removeClass('merchant-show');
99 216 $('.merchant-module-question-answer-dropdown').removeClass('merchant-show');
100 217 $('.merchant-module-question-answer-form').removeClass('merchant-show');
101 218 $('.merchant-module-question-answer-title').removeClass('merchant-show');
@@ -120,8 +237,9 @@
120 237 $('.merchant-module-question-list-dropdown').addClass('merchant-show');
121 238 });
122 239 });
123 240 $('.merchant-module-question-list-dropdown li').on('click', function (e) {
241 + $disableModuleSubmitBtn.prop('disabled', $disableModuleTextField.val().trim() === '');
124 242 var $question = $(this);
125 243 var target = $question.data('answer-target');
126 244 var $answer = $('[data-answer-title="' + target + '"]');
127 245 if ($answer.length) {
@@ -151,108 +269,301 @@
151 269 $(document).off('click.merchant-close');
152 270 }
153 271 });
154 272 });
155 - $('.merchant-range').each(function () {
156 - var $range = $(this);
157 - var $rangeInput = $range.find('.merchant-range-input');
158 - var $numberInput = $range.find('.merchant-range-number-input');
159 - $rangeInput.on('change input merchant.range merchant-init.range', function (e) {
273 +
274 + // Create a function that initializes a single range or all ranges in a context for range field
275 + function initMerchantRange() {
276 + var rangeFields = $(document).find('.merchant-range');
277 + if (rangeFields.length === 0) {
278 + return;
279 + }
280 + rangeFields.each(function () {
160 281 var $range = $(this);
161 - var value = (e.type === 'merchant' ? $numberInput.val() : $range.val()) || 0;
162 - var min = $range.attr('min') || 0;
163 - var max = $range.attr('max') || 1;
164 - var percentage = (value - min) / (max - min) * 100;
165 - $range.css({
166 - 'background': 'linear-gradient(to right, #3858E9 0%, #3858E9 ' + percentage + '%, #ddd ' + percentage + '%, #ddd 100%)'
282 + var $rangeInput = $range.find('.merchant-range-input');
283 + var $numberInput = $range.find('.merchant-range-number-input');
284 + $rangeInput.on('change input merchant.range merchant-init.range', function (e) {
285 + var $range = $(this);
286 + var value = (e.type === 'merchant' ? $numberInput.val() : $range.val()) || 0;
287 + var min = $range.attr('min') || 0;
288 + var max = $range.attr('max') || 1;
289 + var percentage = (value - min) / (max - min) * 100;
290 + if ($('body').hasClass('rtl')) {
291 + $range.css({
292 + 'background': 'linear-gradient(to left, #3858E9 0%, #3858E9 ' + percentage + '%, #ddd ' + percentage + '%, #ddd 100%)'
293 + });
294 + } else {
295 + $range.css({
296 + 'background': 'linear-gradient(to right, #3858E9 0%, #3858E9 ' + percentage + '%, #ddd ' + percentage + '%, #ddd 100%)'
297 + });
298 + }
299 + $rangeInput.val(value);
300 + $numberInput.val(value);
301 + }).trigger('merchant-init.range');
302 + $numberInput.on('change input blur', function () {
303 + if ($rangeInput.hasClass('merchant-range-input')) {
304 + $rangeInput.val($(this).val()).trigger('merchant.range');
305 + }
167 306 });
168 - $rangeInput.val(value);
169 - $numberInput.val(value);
170 - }).trigger('merchant-init.range');
171 - $numberInput.on('change input blur', function () {
172 - if ($rangeInput.hasClass('merchant-range-input')) {
173 - $rangeInput.val($(this).val()).trigger('merchant.range');
174 - }
175 307 });
308 + }
309 +
310 + // 1. Initialize on DOM ready (existing fields)
311 + initMerchantRange();
312 + $(document).on('click', '.merchant-module-page-setting-field-hidden-desc-trigger', function () {
313 + var $trigger = $(this);
314 + $trigger.toggleClass('expanded');
315 + var showText = $trigger.attr('data-show-text');
316 + var hiddenText = $trigger.attr('data-hidden-text');
317 + $(this).find('span:first').text($trigger.text() === showText ? hiddenText : showText);
318 + $(this).closest('.merchant-module-page-setting-field').find('.merchant-module-page-setting-field-hidden-desc').stop(true, true).slideToggle('fast');
176 319 });
177 - $('.merchant-color').each(function () {
178 - var $color = $(this);
179 - var $picker = $color.find('.merchant-color-picker');
180 - var $input = $color.find('.merchant-color-input');
181 - var inited;
182 - var pickr;
183 - $picker.on('click', function () {
184 - var $bodyHTML = $('body,html');
185 - $bodyHTML.addClass('merchant-height-auto');
186 - if (!inited) {
187 - pickr = new Pickr({
188 - el: $picker.get(0),
189 - container: 'body',
190 - theme: 'merchant',
191 - appClass: 'merchant-pcr-app',
192 - default: $input.val() || '#212121',
193 - swatches: ['#000000', '#F44336', '#E91E63', '#673AB7', '#03A9F4', '#8BC34A', '#FFEB3B', '#FFC107', '#FFFFFF'],
194 - sliders: 'h',
195 - useAsButton: true,
196 - components: {
197 - hue: true,
198 - preview: true,
199 - opacity: true,
200 - interaction: {
201 - input: true,
202 - clear: true
320 + var moduleBackup = {
321 + init: function init() {
322 + this.events();
323 + },
324 + events: function events() {
325 + var self = this;
326 + $(document).on('click', '#download-backup-button', this.download.bind(this));
327 +
328 + // Browse button opens file picker.
329 + $(document).on('click', '.merchant-dropzone__browse', function (e) {
330 + e.preventDefault();
331 + e.stopPropagation();
332 + $(this).closest('.merchant-dropzone').find('.merchant-backup-file').trigger('click');
333 + });
334 +
335 + // Auto-restore when a file is selected (via browse or drop).
336 + $(document).on('change', '#merchant-backup-file', function () {
337 + var file = this.files[0];
338 + if (file) {
339 + self.restoreFile(file);
340 + }
341 + });
342 +
343 + // Drag-and-drop visual feedback.
344 + var $dropzone = $('#merchant-restore-dropzone');
345 + $dropzone.on('dragover dragenter', function (e) {
346 + e.preventDefault();
347 + e.stopPropagation();
348 + $(this).addClass('drag-over');
349 + });
350 + $dropzone.on('dragleave drop', function (e) {
351 + e.preventDefault();
352 + e.stopPropagation();
353 + $(this).removeClass('drag-over');
354 + });
355 + $dropzone.on('drop', function (e) {
356 + var file = e.originalEvent.dataTransfer.files[0];
357 + if (file) {
358 + self.restoreFile(file);
359 + }
360 + });
361 + },
362 + download: function download(e) {
363 + var self = this;
364 + e.preventDefault();
365 + var container = $('.merchant-module-page-setting-fields .backup-section');
366 + var $status = container.find('.merchant-backup-status');
367 + $status.removeAttr('class').addClass('merchant-backup-status').text('');
368 + var module_id = $(e.target).attr('data-module-id');
369 + $.ajax({
370 + url: merchant_admin_options.ajaxurl,
371 + type: 'GET',
372 + data: {
373 + action: 'merchant_get_module_settings',
374 + nonce: merchant_admin_options.ajaxnonce,
375 + module_id: module_id
376 + },
377 + beforeSend: function beforeSend() {
378 + self.showLoadingIndicator(container);
379 + },
380 + success: function success(response) {
381 + self.hideLoadingIndicator(container);
382 + if (response.success) {
383 + var moduleSettings = response.data;
384 + var fileName = 'merchant-' + module_id + '-backup-' + new Date().toISOString().slice(0, 10) + '-' + new Date().getHours() + '-' + new Date().getMinutes() + '-' + new Date().getSeconds() + '.json';
385 + self.downloadJson(moduleSettings, fileName);
386 + $status.addClass('is-success').text(merchant_admin_options.backup_success || 'Downloaded!');
387 + } else {
388 + $status.addClass('is-error').text(response.data.message);
389 + }
390 + setTimeout(function () {
391 + $status.removeAttr('class').addClass('merchant-backup-status').text('');
392 + }, 3000);
393 + },
394 + error: function error(xhr, status, _error) {
395 + self.hideLoadingIndicator(container);
396 + $status.addClass('is-error').text(_error);
397 + setTimeout(function () {
398 + $status.removeAttr('class').addClass('merchant-backup-status').text('');
399 + }, 3000);
400 + }
401 + });
402 + },
403 + restoreFile: function restoreFile(file) {
404 + var self = this;
405 + var container = $('.merchant-module-page-setting-fields .restore-section');
406 + var module_id = $('#merchant-restore-dropzone').data('module-id');
407 + self.hideError();
408 + if (!file || file.type !== 'application/json') {
409 + self.displayError(merchant_admin_options.invalid_file);
410 + return;
411 + }
412 + var reader = new FileReader();
413 + reader.onload = function (e) {
414 + var moduleSettings = e.target.result;
415 + $.ajax({
416 + url: merchant_admin_options.ajaxurl,
417 + type: 'POST',
418 + data: {
419 + action: 'merchant_restore_module_settings',
420 + nonce: merchant_admin_options.ajaxnonce,
421 + module_id: module_id,
422 + module_settings: moduleSettings
423 + },
424 + beforeSend: function beforeSend() {
425 + $('#merchant-restore-dropzone').addClass('is-loading');
426 + self.showLoadingIndicator(container);
427 + },
428 + success: function success(response) {
429 + if (response.success) {
430 + merchant.show_save = false;
431 + var $dz = $('#merchant-restore-dropzone');
432 + $dz.removeClass('is-loading').addClass('is-success');
433 + self.hideLoadingIndicator(container);
434 + setTimeout(function () {
435 + window.location.href = response.data.redirect_url;
436 + }, 1500);
437 + } else {
438 + self.displayError(response.data.message);
203 439 }
204 440 },
205 - i18n: {
206 - 'btn:clear': 'Default'
441 + error: function error(xhr, status, _error2) {
442 + self.displayError(_error2);
207 443 }
208 444 });
209 - pickr.on('change', function (color) {
210 - var colorCode;
211 - if (color.a === 1) {
212 - pickr.setColorRepresentation('HEX');
213 - colorCode = color.toHEXA().toString(0);
214 - } else {
215 - pickr.setColorRepresentation('RGBA');
216 - colorCode = color.toRGBA().toString(0);
445 + };
446 + reader.readAsText(file);
447 + },
448 + downloadJson: function downloadJson(data, filename) {
449 + if (_typeof(data) === 'object') {
450 + data = JSON.stringify(data);
451 + }
452 + var blob = new Blob([data], {
453 + type: "application/json"
454 + });
455 + var url = URL.createObjectURL(blob);
456 + var a = document.createElement('a');
457 + a.href = url;
458 + a.download = filename;
459 + a.click();
460 + URL.revokeObjectURL(url);
461 + },
462 + displayError: function displayError(errorMsg) {
463 + var $dz = $('#merchant-restore-dropzone');
464 + $dz.find('.merchant-dropzone__error p').text(errorMsg);
465 + $dz.removeClass('is-loading').addClass('is-error');
466 + this.hideLoadingIndicator($('.merchant-module-page-setting-fields .restore-section'));
467 + setTimeout(function () {
468 + $dz.removeClass('is-error');
469 + // Reset file input so same file can be re-selected.
470 + $dz.find('.merchant-backup-file').val('');
471 + }, 3000);
472 + },
473 + hideError: function hideError() {
474 + $('#merchant-restore-dropzone').removeClass('is-error');
475 + },
476 + showLoadingIndicator: function showLoadingIndicator(container) {
477 + container.find('.merchant-loading-spinner').addClass('show');
478 + },
479 + hideLoadingIndicator: function hideLoadingIndicator(container) {
480 + container.find('.merchant-loading-spinner').removeClass('show');
481 + }
482 + };
483 + moduleBackup.init();
484 +
485 + // Add support for toggle field inside flexible content.
486 + var flexibleToggleField = {
487 + init: function init(field) {
488 + this.events();
489 + },
490 + events: function events() {
491 + $(document).on('click', '.merchant-flexible-content .merchant-toggle-switch .toggle-switch-label span', function () {
492 + var checkBox = $(this).closest('.merchant-toggle-switch').find('.toggle-switch-checkbox');
493 + checkBox.prop('checked', !checkBox.prop('checked'));
494 + }).trigger('merchant.change');
495 + }
496 + };
497 + var dateTimePickerField = {
498 + init: function init() {
499 + this.initiate_datepicker();
500 + this.events();
501 + },
502 + // Date() cannot parse the bare 'H:i' a time-only picker stores, so pin it to today.
503 + parseInitialDate: function parseInitialDate(value, fieldOptions) {
504 + if (!value) {
505 + return [];
506 + }
507 + if (fieldOptions.onlyTimepicker && /^\d{1,2}:\d{2}$/.test(value)) {
508 + var _value$split = value.split(':'),
509 + _value$split2 = _slicedToArray(_value$split, 2),
510 + hours = _value$split2[0],
511 + minutes = _value$split2[1];
512 + return [new Date(new Date().setHours(Number(hours), Number(minutes), 0, 0))];
513 + }
514 + var date = new Date(value);
515 + return isNaN(date) ? [] : [date];
516 + },
517 + initiate_datepicker: function initiate_datepicker() {
518 + var self = this;
519 + var elements = $('.merchant-module-page-setting-field .merchant-datetime-field');
520 + if (elements.length === 0) {
521 + return;
522 + }
523 + elements.each(function (index) {
524 + var input = $(this).find('input'),
525 + fieldOptions = $(this).data('options') || {},
526 + options = {
527 + locale: JSON.parse(merchant_datepicker_locale),
528 + selectedDates: self.parseInitialDate(input.val(), fieldOptions),
529 + onSelect: function onSelect(_ref) {
530 + var date = _ref.date,
531 + formattedDate = _ref.formattedDate,
532 + datepicker = _ref.datepicker;
533 + if (typeof formattedDate === "undefined") {
534 + // allow removing date
535 + // input.val('');
536 + datepicker.$el.value = '';
537 + }
538 + input.trigger('change.merchant');
539 + input.trigger('change.merchant-datepicker', [formattedDate, input, options, index]);
540 + }
541 + };
542 + // add buttons to fieldOptions
543 + fieldOptions.buttons = ['clear'];
544 + if (fieldOptions.minDate !== undefined && fieldOptions.minDate === 'today') {
545 + fieldOptions.minDate = new Date();
546 + if (fieldOptions.timeZone !== undefined && fieldOptions.timeZone !== '') {
547 + fieldOptions.minDate = new Date(fieldOptions.minDate.toLocaleString('en-US', {
548 + timeZone: fieldOptions.timeZone
549 + }));
217 550 }
218 - $picker.css({
219 - 'background-color': colorCode
220 - });
221 - if ($input.val() !== colorCode) {
222 - $input.val(colorCode).trigger('change.merchant');
223 - }
224 - });
225 - pickr.on('clear', function () {
226 - var defaultColor = $picker.data('default-color');
227 - if (defaultColor) {
228 - pickr.setColor(defaultColor);
229 - } else {
230 - $picker.css({
231 - 'background-color': 'white'
232 - });
233 - $input.val('');
234 - }
235 - });
236 - pickr.on('hide', function () {
237 - $bodyHTML.removeClass('merchant-height-auto');
238 - });
239 - $picker.data('pickr', pickr);
240 - setTimeout(function () {
241 - pickr.show();
242 - });
243 - inited = true;
244 - } else {
245 - pickr.setColor($input.val());
246 - }
247 - });
248 - $input.on('change keyup', function () {
249 - var colorCode = $(this).val();
250 - $picker.css({
251 - 'background-color': colorCode
551 + }
552 + options = Object.assign(options, fieldOptions);
553 + var datepickerObj = new AirDatepicker(input.getPath(), options);
554 + input.attr('readonly', true);
555 + $(document).trigger('initiated.merchant-datepicker', [datepickerObj, input, options, index]);
252 556 });
253 - });
254 - });
557 + },
558 + events: function events() {
559 + var self = this;
560 + $(document).on('merchant-flexible-content-added', function () {
561 + self.initiate_datepicker();
562 + });
563 + }
564 + };
565 + dateTimePickerField.init();
255 566
256 567 // Sortable.
257 568 var SortableField = {
258 569 init: function init(field) {
@@ -294,11 +605,19 @@
294 605 return JSON.stringify(newVal);
295 606 }
296 607 };
297 608
298 - // Intialize Sortable.
609 + // Initialize Sortable.
299 610 SortableField.init();
611 + flexibleToggleField.init();
300 612
613 + // When adding/duplicating new item, refresh sorting
614 + $(document).on('merchant-flexible-content-added', function (e, $layout) {
615 + var $sortableWrapper = $layout.find('.merchant-sortable-repeater-control');
616 + var $sortableElement = $sortableWrapper.find('.merchant-sortable-repeater.sortable');
617 + SortableRepeaterField.makeFieldsSortable($sortableElement);
618 + });
619 +
301 620 // Sortable Repeater.
302 621 var SortableRepeaterField = {
303 622 init: function init(field) {
304 623 var self = this;
@@ -313,23 +632,24 @@
313 632 $(this).find('.repeater-input').val(defaultValuesArray[0]);
314 633
315 634 // Create a new row for each new value
316 635 if (numRepeaterItems > 1) {
317 - var i;
318 - for (i = 1; i < numRepeaterItems; ++i) {
636 + // var i;
637 + for (var i = 1; i < numRepeaterItems; ++i) {
319 638 self.appendRow($(this), defaultValuesArray[i]);
320 639 }
321 640 }
322 641 }
323 642
324 - // Make our Repeater fields sortable.
325 - if (!$(this).hasClass('disable-sorting')) {
326 - $(this).find('.merchant-sortable-repeater.sortable').sortable({
327 - update: function update(event, ui) {
328 - self.getAllInputs($(this).parent());
329 - }
330 - });
331 - }
643 + // Todo: remove
644 + // Make our Repeater fields sortable. Doesn't work with flexible content
645 + // if (!$(this).hasClass('disable-sorting')) {
646 + // $(this).find('.merchant-sortable-repeater.sortable').sortable({
647 + // update: function (event, ui) {
648 + // self.getAllInputs($(this).parent());
649 + // }
650 + // });
651 + // }
332 652 });
333 653
334 654 // Events.
335 655 this.events();
@@ -336,26 +656,21 @@
336 656 },
337 657 events: function events() {
338 658 var self = this;
339 659
340 - // Remove item starting from it's parent element
341 - $('.merchant-sortable-repeater.sortable').on('click', '.customize-control-sortable-repeater-delete', function (event) {
660 + // Remove item starting from its parent element
661 + $(document).on('click', '.merchant-sortable-repeater.sortable .customize-control-sortable-repeater-delete', function (event) {
342 662 event.preventDefault();
343 - var numItems = $(this).parent().parent().find('.repeater').length;
344 - if (numItems > 1) {
345 - $(this).parent().slideUp('fast', function () {
346 - var parentContainer = $(this).parent().parent();
347 - $(this).remove();
348 - self.getAllInputs(parentContainer);
349 - });
350 - } else {
351 - // $(this).parent().find('.repeater-input').val('');
352 - self.getAllInputs($(this).parent().parent().parent());
353 - }
663 + $(this).parent().slideUp('fast', function () {
664 + var parentContainer = $(this).parent().parent();
665 + $(this).remove();
666 + self.getAllInputs(parentContainer);
667 + });
668 + $(document).trigger('merchant-sortable-repeater-item-deleted');
354 669 });
355 670
356 671 // Add new item
357 - $('.customize-control-sortable-repeater-add').click(function (event) {
672 + $(document).on('click', '.customize-control-sortable-repeater-add', function (event) {
358 673 event.preventDefault();
359 674 self.appendRow($(this).parent());
360 675 self.getAllInputs($(this).parent());
361 676 });
@@ -360,12 +675,12 @@
360 675 self.getAllInputs($(this).parent());
361 676 });
362 677
363 678 // Refresh our hidden field if any fields change
364 - $('.merchant-sortable-repeater.sortable').change(function () {
679 + $(document).on('change', '.merchant-sortable-repeater.sortable', function () {
365 680 self.getAllInputs($(this).parent());
366 681 });
367 - $('.merchant-sortable-repeater.sortable').on('focusout', '.repeater-input', function () {
682 + $(document).on('focusout', '.merchant-sortable-repeater.sortable .repeater-input', function () {
368 683 self.getAllInputs($(this).parent());
369 684 });
370 685 },
371 686 /**
@@ -375,12 +690,26 @@
375 690 appendRow: function appendRow($element) {
376 691 var defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
377 692 var newRow = '<div class="repeater" style="display:none"><input type="text" value="' + defaultValue + '" class="repeater-input" /><span class="dashicons dashicons-menu"></span><a class="customize-control-sortable-repeater-delete" href="#"><span class="dashicons dashicons-no-alt"></span></a></div>';
378 693 $element.find('.sortable').append(newRow);
379 - $element.find('.sortable').find('.repeater:last').slideDown('slow', function () {
694 + var $newItem = $element.find('.sortable').find('.repeater:last');
695 + $newItem.slideDown('slow', function () {
380 696 $(this).find('input').focus();
381 697 });
698 +
699 + // Make Repeater fields sortable; Putting here works better with flexible content
700 + this.makeFieldsSortable($element.find('.sortable'));
701 + $(document).trigger('merchant-sortable-repeater-item-added', [$newItem, $element.find('.sortable')]);
382 702 },
703 + makeFieldsSortable: function makeFieldsSortable($sortableElement) {
704 + if (!$sortableElement.hasClass('disable-sorting')) {
705 + $sortableElement.sortable({
706 + update: function update(event, ui) {
707 + SortableRepeaterField.getAllInputs($sortableElement.parent());
708 + }
709 + });
710 + }
711 + },
383 712 /**
384 713 * Get the values from the repeater input fields and add to our hidden field.
385 714 *
386 715 */
@@ -388,12 +717,18 @@
388 717 var inputValues = $element.find('.repeater-input').map(function () {
389 718 return $(this).val();
390 719 }).toArray();
391 720
721 + // Keep one empty item if all deleted.
722 + if (!inputValues.length) {
723 + inputValues.push('');
724 + }
725 +
392 726 // Add all the values from our repeater fields to the hidden field (which is the one that actually gets saved)
393 727 $element.find('.merchant-sortable-repeater-input').val(JSON.stringify(inputValues));
394 728 // Important! Make sure to trigger change event so Customizer knows it has to save the field
395 729 $element.find('.merchant-sortable-repeater-input').trigger('change');
730 + $element.find('.merchant-sortable-repeater-input').trigger('sortable.repeater.change');
396 731 }
397 732 };
398 733
399 734 // Initialize Sortable Repeater.
@@ -398,8 +733,590 @@
398 733
399 734 // Initialize Sortable Repeater.
400 735 SortableRepeaterField.init();
401 736
737 + // Sortable Repeater Icons.
738 + var SortableRepeaterIconsField = {
739 + CONTROL: '.merchant-sortable-repeater-icons-control',
740 + init: function init() {
741 + var self = this;
742 + $(self.CONTROL).each(function () {
743 + self.populate($(this));
744 + });
745 + this.events();
746 + },
747 + /**
748 + * Populate rows from the hidden JSON input.
749 + */
750 + populate: function populate($control) {
751 + var self = this;
752 + var items = [];
753 + try {
754 + items = JSON.parse($control.find('.merchant-sortable-repeater-input').val());
755 + } catch (e) {
756 + items = [];
757 + }
758 + if (!Array.isArray(items) || items.length === 0) {
759 + return;
760 + }
761 + var $firstRow = $control.find('.repeater').first();
762 +
763 + // First item populates the existing row.
764 + var firstItem = self.normalizeItem(items[0]);
765 + $firstRow.find('.repeater-input').val(firstItem.text);
766 + self.setRowIcon($firstRow, firstItem.icon);
767 +
768 + // Remaining items get new rows (silent = true to avoid triggering save bar).
769 + for (var i = 1; i < items.length; i++) {
770 + var item = self.normalizeItem(items[i]);
771 + self.appendRow($control, item.text, item.icon, true);
772 + }
773 + },
774 + events: function events() {
775 + var self = this;
776 +
777 + // Toggle icon picker dropdown.
778 + $(document).on('click', self.CONTROL + ' .merchant-icon-picker-toggle', function (e) {
779 + e.preventDefault();
780 + e.stopPropagation();
781 + var $dropdown = $(this).siblings('.merchant-icon-picker-dropdown');
782 +
783 + // Close all other dropdowns first.
784 + $(self.CONTROL + ' .merchant-icon-picker-dropdown').not($dropdown).hide();
785 + $dropdown.toggle();
786 + });
787 +
788 + // Select icon from dropdown.
789 + $(document).on('click', self.CONTROL + ' .merchant-icon-option', function (e) {
790 + e.preventDefault();
791 + e.stopPropagation();
792 + var $repeater = $(this).closest('.repeater');
793 + var iconKey = $(this).data('icon') || '';
794 + self.setRowIcon($repeater, iconKey);
795 + $(this).closest('.merchant-icon-picker-dropdown').hide();
796 + self.syncHiddenInput($(this).closest(self.CONTROL));
797 + });
798 +
799 + // Close dropdown on outside click.
800 + $(document).on('click', function () {
801 + $(SortableRepeaterIconsField.CONTROL + ' .merchant-icon-picker-dropdown').hide();
802 + });
803 +
804 + // Prevent dropdown clicks from closing.
805 + $(document).on('click', self.CONTROL + ' .merchant-icon-picker-dropdown', function (e) {
806 + e.stopPropagation();
807 + });
808 +
809 + // Delete row.
810 + $(document).on('click', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable .customize-control-sortable-repeater-delete', function (e) {
811 + e.preventDefault();
812 + var $control = $(this).closest(self.CONTROL);
813 + $(this).parent().slideUp('fast', function () {
814 + $(this).remove();
815 + self.syncHiddenInput($control);
816 + });
817 + $(document).trigger('merchant-sortable-repeater-item-deleted');
818 + });
819 +
820 + // Add new row.
821 + $(document).on('click', self.CONTROL + ' .customize-control-sortable-repeater-icons-add', function (e) {
822 + e.preventDefault();
823 + var $control = $(this).closest(self.CONTROL);
824 + self.appendRow($control, '', '');
825 + self.syncHiddenInput($control);
826 + });
827 +
828 + // Text change → sync.
829 + $(document).on('focusout', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable .repeater-input', function () {
830 + self.syncHiddenInput($(this).closest(self.CONTROL));
831 + });
832 +
833 + // Sort change → sync.
834 + $(document).on('change', self.CONTROL + ' .merchant-sortable-repeater-icons.sortable', function () {
835 + self.syncHiddenInput($(this).closest(self.CONTROL));
836 + });
837 +
838 + // Re-init on flexible content add.
839 + $(document).on('merchant-flexible-content-added', function (e, $layout) {
840 + var $control = $layout.find(self.CONTROL);
841 + if ($control.length) {
842 + self.populate($control);
843 + self.makeFieldsSortable($control.find('.sortable'));
844 + }
845 + });
846 + },
847 + /**
848 + * Append a new repeater row.
849 + */
850 + appendRow: function appendRow($control, text, icon, silent) {
851 + var self = this;
852 + var iconLibrary = self.getIconLibrary($control);
853 + var dropdownHtml = '<div class="merchant-icon-picker-dropdown" style="display:none;">';
854 + dropdownHtml += '<button type="button" class="merchant-icon-option" data-icon="" title="Use campaign icon"><span class="dashicons dashicons-no-alt"></span></button>';
855 + Object.keys(iconLibrary).forEach(function (key) {
856 + dropdownHtml += '<button type="button" class="merchant-icon-option" data-icon="' + key + '" title="' + key + '">' + iconLibrary[key] + '</button>';
857 + });
858 + dropdownHtml += '</div>';
859 + var escapedText = $('<div>').text(text).html();
860 + var newRow = $('<div class="repeater" style="display:none">' + '<button type="button" class="merchant-icon-picker-toggle" data-icon="" title="Select icon"><span class="dashicons dashicons-plus-alt2"></span></button>' + dropdownHtml + '<input type="text" value="' + escapedText + '" class="repeater-input" />' + '<span class="dashicons dashicons-menu"></span>' + '<a class="customize-control-sortable-repeater-delete" href="#"><span class="dashicons dashicons-no-alt"></span></a>' + '</div>');
861 + $control.find('.sortable').append(newRow);
862 + if (silent) {
863 + newRow.show();
864 + } else {
865 + newRow.slideDown('slow', function () {
866 + $(this).find('input').focus();
867 + });
868 + }
869 + if (icon) {
870 + self.setRowIcon(newRow, icon);
871 + }
872 + self.makeFieldsSortable($control.find('.sortable'));
873 + $(document).trigger('merchant-sortable-repeater-item-added', [newRow, $control.find('.sortable')]);
874 + },
875 + /**
876 + * Set the icon on a repeater row.
877 + */
878 + setRowIcon: function setRowIcon($row, iconKey) {
879 + var $toggle = $row.find('.merchant-icon-picker-toggle');
880 + $toggle.attr('data-icon', iconKey);
881 + if (iconKey) {
882 + var $control = $row.closest(SortableRepeaterIconsField.CONTROL);
883 + var iconLibrary = this.getIconLibrary($control);
884 + var svg = iconLibrary[iconKey] || '';
885 + if (svg) {
886 + $toggle.html(svg).addClass('has-icon');
887 + } else {
888 + $toggle.html('<span class="dashicons dashicons-plus-alt2"></span>').removeClass('has-icon');
889 + }
890 + } else {
891 + $toggle.html('<span class="dashicons dashicons-plus-alt2"></span>').removeClass('has-icon');
892 + }
893 +
894 + // Mark selected option in dropdown.
895 + $row.find('.merchant-icon-option').removeClass('selected');
896 + $row.find('.merchant-icon-option[data-icon="' + iconKey + '"]').addClass('selected');
897 + },
898 + /**
899 + * Sync all rows back to the hidden JSON input.
900 + */
901 + syncHiddenInput: function syncHiddenInput($control) {
902 + var items = [];
903 + $control.find('.merchant-sortable-repeater-icons .repeater').each(function () {
904 + var text = $(this).find('.repeater-input').val();
905 + var icon = $(this).find('.merchant-icon-picker-toggle').attr('data-icon') || '';
906 + items.push({
907 + text: text,
908 + icon: icon
909 + });
910 + });
911 + if (!items.length) {
912 + items.push({
913 + text: '',
914 + icon: ''
915 + });
916 + }
917 + $control.find('.merchant-sortable-repeater-input').val(JSON.stringify(items));
918 + $control.find('.merchant-sortable-repeater-input').trigger('change');
919 + $control.find('.merchant-sortable-repeater-input').trigger('sortable.repeater.change');
920 + },
921 + makeFieldsSortable: function makeFieldsSortable($sortable) {
922 + var self = this;
923 + if (!$sortable.hasClass('disable-sorting')) {
924 + $sortable.sortable({
925 + handle: '.dashicons-menu',
926 + update: function update() {
927 + self.syncHiddenInput($sortable.closest(self.CONTROL));
928 + }
929 + });
930 + }
931 + },
932 + getIconLibrary: function getIconLibrary($control) {
933 + if (!$control.data('_iconLibrary')) {
934 + try {
935 + $control.data('_iconLibrary', JSON.parse($control.attr('data-icons')) || {});
936 + } catch (e) {
937 + $control.data('_iconLibrary', {});
938 + }
939 + }
940 + return $control.data('_iconLibrary');
941 + },
942 + normalizeItem: function normalizeItem(item) {
943 + if (typeof item === 'string') {
944 + return {
945 + text: item,
946 + icon: ''
947 + };
948 + }
949 + return {
950 + text: item.text || '',
951 + icon: item.icon || ''
952 + };
953 + }
954 + };
955 +
956 + // Initialize Sortable Repeater Icons.
957 + SortableRepeaterIconsField.init();
958 + var GroubField = {
959 + init: function init() {
960 + var self = this;
961 + self.initAccordion();
962 + self.initFlag();
963 + },
964 + initAccordion: function initAccordion() {
965 + var self = this;
966 + $('.merchant-group-field.has-accordion').each(function () {
967 + var element = $(this);
968 + element.accordion({
969 + collapsible: true,
970 + header: "> .title-area",
971 + heightStyle: "content",
972 + active: element.hasClass('open') ? 0 : false
973 + });
974 + });
975 + },
976 + initFlag: function initFlag() {
977 + $('.merchant-group-field.has-flag').each(function () {
978 + var element = $(this);
979 + var field_id = element.data('id');
980 + var status_field = element.find(".merchant-field-".concat(field_id, "_status select"));
981 + var selected_value = status_field.val();
982 + var selected_label = status_field.find('option:selected').text();
983 + var status_element = element.find('.field-status');
984 + status_element.removeClass('hidden active inactive').text(selected_label).addClass(selected_value);
985 + });
986 + }
987 + };
988 + var ReviewsSelector = {
989 + accordion: null,
990 + activePopupContainer: null,
991 + // Initialize the Reviews Selector
992 + init: function init() {
993 + var self = this;
994 + self.initAccordion();
995 + self.events();
996 + // Skip the flexible content hidden elements that will be cloned when initialize the sortable reviews functionality
997 + self.makeSelectedReviewsSortable($('.merchant-reviews-selector').not('.layouts .merchant-reviews-selector'));
998 + },
999 + // Set up event listeners
1000 + events: function events() {
1001 + var self = this;
1002 + $(document).on('input', '.merchant-reviews-selector .products-search', self.ajaxSearch.bind(self));
1003 + $(document).on('change', '.merchant-reviews-selector .product-review input[type="checkbox"]', self.toggleReview.bind(self));
1004 + $(document).on('click', '.merchant-reviews-selector .review-photo img', self.requestReviewImages.bind(self));
1005 + $(document).on('click', '.review-photos-popup .overlay', self.dismissImagesGallery.bind(self));
1006 + $(document).on('click', '.merchant-reviews-selector .product-reviews-load-more button', self.loadMoreReviews.bind(self));
1007 + $(document).on('click', '.merchant-reviews-selector .popup-trigger', self.initPopUp.bind(self));
1008 + $(document).on('click', '.merchant-reviews-selector .popup-header .close, .merchant-reviews-selector > .overlay', self.dismissPopUp.bind(self));
1009 + $(document).on('click', '.merchant-reviews-selector .product-review-delete', self.deleteSelectedReview.bind(self));
1010 + // Listen to escape key
1011 + $(document).on('keyup', function (e) {
1012 + if (e.key === "Escape") {
1013 + var isGallery = self.dismissImagesGallery();
1014 + if (isGallery) {
1015 + return;
1016 + }
1017 + self.dismissPopUp(self);
1018 + }
1019 + });
1020 + $(document).on('merchant-flexible-content-added', function (e, layout) {
1021 + self.makeSelectedReviewsSortable();
1022 + self.initAccordion();
1023 + });
1024 + },
1025 + // Make selected reviews sortable
1026 + makeSelectedReviewsSortable: function makeSelectedReviewsSortable() {
1027 + var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $(document).find('.merchant-reviews-selector').not('.layouts .merchant-reviews-selector');
1028 + var self = this;
1029 + var selectedReviews = container.find('.selected-reviews .product-reviews');
1030 + selectedReviews.sortable({
1031 + axis: 'y',
1032 + cursor: 'move',
1033 + helper: 'original',
1034 + handle: '.product-review-move',
1035 + cancel: '',
1036 + placeholder: 'placeholder',
1037 + // Add a class for styling
1038 + update: function update(event, ui) {
1039 + var container = ui.item.closest('.merchant-reviews-selector');
1040 + self.saveModifications(container);
1041 + },
1042 + start: function start(event, ui) {
1043 + ui.placeholder.height(ui.item.height()); // Match height
1044 + }
1045 + });
1046 + // $('.product-reviews').sortable('refresh');
1047 + // console.log('selectedReviews',selectedReviews.sortable('instance'))
1048 + },
1049 + // Delete selected review
1050 + deleteSelectedReview: function deleteSelectedReview(e) {
1051 + var self = this;
1052 + var review = $(e.target).closest('.product-review');
1053 + var container = review.closest('.merchant-reviews-selector');
1054 + review.remove();
1055 + self.saveModifications(container);
1056 + self.updateSelectedReviewsCheckboxState(container);
1057 + self.countSelectedReviewsInProduct(container);
1058 + },
1059 + // Handle AJAX search for reviews
1060 + ajaxSearch: function ajaxSearch(e) {
1061 + var self = this;
1062 + var searchField = $(e.target);
1063 + var container = searchField.closest('.merchant-reviews-selector');
1064 + var header = container.find('.popup-header');
1065 +
1066 + // check value length > 3
1067 + if (searchField.val().length === 0 || searchField.val().length >= 3) {
1068 + $.ajax({
1069 + url: merchant_admin_options.ajaxurl,
1070 + type: 'POST',
1071 + data: {
1072 + action: 'merchant_search_reviews',
1073 + search: searchField.val(),
1074 + nonce: merchant_admin_options.ajaxnonce
1075 + },
1076 + beforeSend: function beforeSend() {
1077 + header.addClass('loading');
1078 + self.destroyAccordion(container);
1079 + },
1080 + success: function success(response) {
1081 + if (response.success) {
1082 + header.removeClass('popup-error');
1083 + container.find('.products-search-results').html(response.data);
1084 + self.initAccordion(container);
1085 + } else {
1086 + header.addClass('popup-error');
1087 + }
1088 + },
1089 + complete: function complete() {
1090 + header.removeClass('loading');
1091 + },
1092 + error: function error() {
1093 + header.addClass('popup-error');
1094 + }
1095 + });
1096 + }
1097 + },
1098 + // Load more reviews
1099 + loadMoreReviews: function loadMoreReviews(e) {
1100 + var self = this;
1101 + var product = $(e.target).closest('.product-item');
1102 + var offset = product.find('.product-review').length;
1103 + $.ajax({
1104 + url: merchant_admin_options.ajaxurl,
1105 + type: 'POST',
1106 + data: {
1107 + action: 'merchant_load_more_reviews',
1108 + product_id: product.attr('data-id'),
1109 + offset: offset,
1110 + nonce: merchant_admin_options.ajaxnonce
1111 + },
1112 + beforeSend: function beforeSend() {
1113 + product.addClass('loading');
1114 + },
1115 + success: function success(response) {
1116 + if (response.success) {
1117 + product.find('.product-reviews .reviews-wrapper').append(response.data.reviews);
1118 + if (response.data.load_more === '') {
1119 + self.hideLoadMoreButton(product);
1120 + }
1121 + self.updateSelectedReviewsCheckboxState(product.closest('.merchant-reviews-selector'));
1122 + self.countSelectedReviewsInProduct(product.closest('.merchant-reviews-selector'));
1123 + }
1124 + },
1125 + complete: function complete() {
1126 + product.removeClass('loading');
1127 + }
1128 + });
1129 + },
1130 + // Hide load more button
1131 + hideLoadMoreButton: function hideLoadMoreButton(product) {
1132 + product.find('.product-reviews-load-more').remove();
1133 + },
1134 + // Initialize the popup
1135 + initPopUp: function initPopUp(e) {
1136 + var self = this;
1137 + var container = $(e.target).closest('.merchant-reviews-selector');
1138 + var popup = container.find('.selector-popup');
1139 + var overlay = container.find('.overlay');
1140 + popup.addClass('active');
1141 + overlay.addClass('active');
1142 + self.activePopupContainer = container;
1143 + },
1144 + // Dismiss the popup
1145 + dismissPopUp: function dismissPopUp() {
1146 + var self = this;
1147 + var container = self.activePopupContainer;
1148 + if (container === null) {
1149 + return;
1150 + }
1151 + var popup = container.find('.selector-popup');
1152 + var overlay = container.find('.overlay');
1153 + popup.removeClass('active');
1154 + overlay.removeClass('active');
1155 + self.activePopupContainer = null;
1156 + },
1157 + // Initialize the accordion
1158 + initAccordion: function initAccordion() {
1159 + var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $(document).find('.merchant-reviews-selector');
1160 + var self = this;
1161 + container.each(function () {
1162 + var elements = $(this).find('.popup-content .product-item.product-item-has-reviews');
1163 + elements.each(function () {
1164 + elements.accordion({
1165 + collapsible: true,
1166 + heightStyle: "content",
1167 + icons: false,
1168 + // Disable icons
1169 + active: elements.hasClass('opened') ? 0 : false,
1170 + activate: function activate(event, ui) {
1171 + // Check if a new panel is being activated
1172 + if (ui.newPanel.length) {
1173 + // Add 'opened' class to the parent product item
1174 + ui.newPanel.parent().addClass('opened');
1175 + } else {
1176 + // Remove 'opened' class from all product items
1177 + elements.removeClass('opened');
1178 + }
1179 + }
1180 + });
1181 + });
1182 + elements.find('a').on('click', function (e) {
1183 + e.stopPropagation(); // Prevent event from bubbling up to the accordion header
1184 + });
1185 + self.updateSelectedReviewsCheckboxState($(this));
1186 + self.countSelectedReviewsInProduct($(this));
1187 + });
1188 + },
1189 + // Destroy the accordion
1190 + destroyAccordion: function destroyAccordion(container) {
1191 + var elements = container.find('.selector-popup .product-item.product-item-has-reviews');
1192 + elements.each(function () {
1193 + if ($(this).hasClass('ui-accordion')) {
1194 + // Check if it has the accordion class
1195 + $(this).accordion('destroy');
1196 + }
1197 + });
1198 + },
1199 + // Toggle review selection
1200 + toggleReview: function toggleReview(e) {
1201 + var self = this;
1202 + var checked = $(e.target).prop('checked');
1203 + var reviewsFieldContainer = $(e.target).closest('.merchant-reviews-selector');
1204 + var review = $(e.target).closest('.product-review');
1205 + var reviewId = review.attr('data-id');
1206 + var selectedReviews = reviewsFieldContainer.find('.selected-reviews .product-reviews');
1207 + var selectedReview = selectedReviews.find(".product-review[data-id=\"".concat(reviewId, "\"]"));
1208 + if (checked) {
1209 + if (selectedReview.length === 0) {
1210 + var newReview = review.clone();
1211 + selectedReviews.append(newReview);
1212 + self.makeSelectedReviewsSortable();
1213 + }
1214 + } else {
1215 + selectedReview.remove();
1216 + }
1217 + setTimeout(function () {
1218 + self.saveModifications(reviewsFieldContainer);
1219 + }, 150);
1220 + },
1221 + // Save modifications to the selected reviews
1222 + saveModifications: function saveModifications(container) {
1223 + var self = this;
1224 + var reviewsSavedIdsField = container.find('.review-saved-ids');
1225 + var selectedReviews = container.find('.selected-reviews .product-reviews');
1226 + var reviews = selectedReviews.find('.product-review');
1227 + var reviewsIds = [];
1228 + reviews.each(function () {
1229 + var id = $(this).attr('data-id');
1230 + reviewsIds.push(id);
1231 + });
1232 + reviewsSavedIdsField.val(reviewsIds.join(','));
1233 + reviewsSavedIdsField.trigger('change');
1234 + // reviewsSavedIds;
1235 + self.updateSelectedReviewsCheckboxState(container);
1236 + self.countSelectedReviewsInProduct(container);
1237 + },
1238 + // Mark selected reviews checkbox
1239 + updateSelectedReviewsCheckboxState: function updateSelectedReviewsCheckboxState(container) {
1240 + var popupContent = container.find('.selector-popup');
1241 + var reviewsSavedIds = container.find('.review-saved-ids');
1242 + var reviews = popupContent.find('.product-review');
1243 + var reviewsIds = reviewsSavedIds.val().split(',');
1244 + reviews.each(function () {
1245 + var reviewId = $(this).attr('data-id');
1246 + var checkbox = container.find(".product-review[data-id=\"".concat(reviewId, "\"] input[type=\"checkbox\"]"));
1247 + if (reviewsIds.includes(reviewId)) {
1248 + checkbox.prop('checked', true);
1249 + } else {
1250 + checkbox.prop('checked', false);
1251 + }
1252 + });
1253 + },
1254 + // Count selected reviews in product
1255 + countSelectedReviewsInProduct: function countSelectedReviewsInProduct(container) {
1256 + var self = this;
1257 + var products = container.find('.selector-popup .product-item');
1258 + var reviewsSavedIds = container.find('.review-saved-ids');
1259 + var reviewsIds = reviewsSavedIds.val().split(',');
1260 + products.each(function () {
1261 + var selectedReviews = 0;
1262 + var product = $(this);
1263 + var productReviews = product.find('.product-review');
1264 + productReviews.each(function () {
1265 + var review = $(this);
1266 + var reviewId = review.attr('data-id');
1267 + if (reviewsIds.includes(reviewId)) {
1268 + selectedReviews++;
1269 + }
1270 + });
1271 + product.find('.selected-reviews-count .counter').text(selectedReviews);
1272 + });
1273 + },
1274 + // Request review images
1275 + requestReviewImages: function requestReviewImages(e) {
1276 + var reviewContainer = $(e.target).closest('.product-review');
1277 + var image = $(e.target);
1278 + var reviewId = reviewContainer.attr('data-id');
1279 + $.ajax({
1280 + url: merchant_admin_options.ajaxurl,
1281 + type: 'POST',
1282 + data: {
1283 + action: 'merchant_get_review_images',
1284 + review_id: reviewId,
1285 + nonce: merchant_admin_options.ajaxnonce
1286 + },
1287 + beforeSend: function beforeSend() {
1288 + // Show loading spinner
1289 + image.addClass('loading');
1290 + },
1291 + success: function success(response) {
1292 + if (response.success) {
1293 + $('body').append(response.data);
1294 + setTimeout(function () {
1295 + $('body').find('.review-photos-popup').addClass('active');
1296 + }, 300);
1297 + }
1298 + },
1299 + complete: function complete() {
1300 + // Hide loading spinner
1301 + image.removeClass('loading');
1302 + }
1303 + });
1304 + },
1305 + // Dismiss the images gallery
1306 + dismissImagesGallery: function dismissImagesGallery() {
1307 + var gallery = $('body').find('.review-photos-popup');
1308 + if (gallery.length) {
1309 + gallery.removeClass('active');
1310 + setTimeout(function () {
1311 + gallery.remove();
1312 + }, 300);
1313 + return true;
1314 + }
1315 + return false;
1316 + }
1317 + };
1318 +
402 1319 // Flexible Content.
403 1320 var FlexibleContentField = {
404 1321 init: function init(field) {
405 1322 var self = this;
@@ -405,32 +1322,294 @@
405 1322 var self = this;
406 1323
407 1324 // Update the values for all our input fields and initialise the sortable repeater.
408 1325 $('.merchant-flexible-content-control').each(function () {
409 - var $content = $(this).find('.merchant-flexible-content');
410 - $content.sortable({
411 - axis: 'y',
412 - cursor: 'move',
413 - helper: 'original',
414 - handle: '.customize-control-flexible-content-move',
415 - stop: function stop(event, ui) {
416 - $content.trigger('merchant.sorted');
417 - self.refreshNumbers($content);
1326 + var hasAccordion = $(this).hasClass('has-accordion'),
1327 + $content = $(this).find('.merchant-flexible-content');
1328 + var campaignId = params.get('campaign_id');
1329 + var campaignIndex = 0;
1330 +
1331 + // Find the campaignIndex based on campaignId
1332 + if (campaignId) {
1333 + $content.find('input.flexible-id').each(function (index) {
1334 + if ($(this).val() === campaignId) {
1335 + campaignIndex = index;
1336 + return false;
1337 + }
1338 + });
1339 + }
1340 + if (hasAccordion) {
1341 + $content.accordion({
1342 + active: campaignIndex,
1343 + // Open the campaign with campaign index
1344 + collapsible: true,
1345 + //header: "> div > .layout-header",
1346 + header: function header(elem) {
1347 + return elem.find('.layout__inner > .layout-header');
1348 + },
1349 + heightStyle: "content",
1350 + beforeActivate: function beforeActivate(event, ui) {
1351 + // Prevent accordion toggle when clicking the status badge.
1352 + if (event.originalEvent && $(event.originalEvent.target).hasClass('layout-status')) {
1353 + event.preventDefault();
1354 + return;
1355 + }
1356 +
1357 + // Hydrate deferred layout before the accordion animation
1358 + // so the panel opens with content already rendered.
1359 + if (ui.newPanel.length) {
1360 + var $layout = ui.newPanel.closest('.layout');
1361 + if ($layout.attr('data-deferred') === '1') {
1362 + self.hydrateLayout($layout);
1363 + }
1364 + }
1365 + }
1366 + }).sortable({
1367 + axis: 'y',
1368 + cursor: 'move',
1369 + helper: 'original',
1370 + handle: '.customize-control-flexible-content-move',
1371 + stop: function stop(event, ui) {
1372 + $content.trigger('merchant.sorted');
1373 + self.refreshNumbers($content);
1374 + $content.accordion("refresh");
1375 + }
1376 + });
1377 + } else {
1378 + $content.sortable({
1379 + axis: 'y',
1380 + cursor: 'move',
1381 + helper: 'original',
1382 + handle: '.customize-control-flexible-content-move',
1383 + stop: function stop(event, ui) {
1384 + $content.trigger('merchant.sorted');
1385 + self.refreshNumbers($content);
1386 + $content.accordion("refresh");
1387 + }
1388 + });
1389 + }
1390 + });
1391 + this.updateLayoutTitle();
1392 + this.updateLayoutStatus();
1393 + this.updateDiscountPercentMaxVal();
1394 + // Events.
1395 + this.events();
1396 + },
1397 + updateLayoutTitle: function updateLayoutTitle() {
1398 + // Update the title for all layout header.
1399 + $('.merchant-flexible-content .layout').each(function () {
1400 + var title = $(this).find('.layout-title[data-title-field]');
1401 + if (title.length) {
1402 + var input = $(this).find('.layout-body .merchant-field-' + title.data('title-field') + ' input');
1403 + input.on('change keyup', function () {
1404 + title.text($(this).val());
1405 + });
1406 + title.text(input.val());
1407 + }
1408 + });
1409 + },
1410 + /**
1411 + * Resolve a status field inside a layout, returning a unified adapter
1412 + * that works with select, radio, and switcher field types.
1413 + *
1414 + * @param {jQuery} $layout The .layout element.
1415 + * @param {string} fieldId The field identifier (e.g. 'campaign_status').
1416 + * @returns {object|null} Adapter with getValue, getLabel, getOptions, setNext, onChange — or null.
1417 + */
1418 + resolveStatusField: function resolveStatusField($layout, fieldId) {
1419 + var $wrapper = $layout.find('.layout-body .merchant-field-' + fieldId);
1420 + if (!$wrapper.length) return null;
1421 + var $select = $wrapper.find('select');
1422 + var $radios = $wrapper.find('input[type="radio"]');
1423 + var $checkbox = $wrapper.find('input[type="checkbox"]');
1424 +
1425 + // Select field adapter.
1426 + if ($select.length) {
1427 + return {
1428 + getValue: function getValue() {
1429 + return $select.val() || 'active';
1430 + },
1431 + getLabel: function getLabel() {
1432 + return $select.find('option:selected').text() || this.getValue();
1433 + },
1434 + getOptions: function getOptions() {
1435 + return $select.find('option').map(function () {
1436 + return $(this).val();
1437 + }).get();
1438 + },
1439 + setNext: function setNext() {
1440 + var opts = $select.find('option');
1441 + var idx = opts.index(opts.filter(':selected'));
1442 + var next = (idx + 1) % opts.length;
1443 + $select.val(opts.eq(next).val()).trigger('change');
1444 + },
1445 + onChange: function onChange(fn) {
1446 + $select.off('change.statusBadge').on('change.statusBadge', fn);
418 1447 }
419 - });
1448 + };
1449 + }
1450 +
1451 + // Radio field adapter.
1452 + if ($radios.length) {
1453 + return {
1454 + getValue: function getValue() {
1455 + return $radios.filter(':checked').val() || 'active';
1456 + },
1457 + getLabel: function getLabel() {
1458 + var $checked = $radios.filter(':checked');
1459 + var $label = $wrapper.find('label[for="' + $checked.attr('id') + '"]');
1460 + return $label.length ? $label.text() : this.getValue();
1461 + },
1462 + getOptions: function getOptions() {
1463 + return $radios.map(function () {
1464 + return $(this).val();
1465 + }).get();
1466 + },
1467 + setNext: function setNext() {
1468 + var vals = this.getOptions();
1469 + var idx = vals.indexOf(this.getValue());
1470 + var next = (idx + 1) % vals.length;
1471 + $radios.filter('[value="' + vals[next] + '"]').prop('checked', true).trigger('change');
1472 + },
1473 + onChange: function onChange(fn) {
1474 + $radios.off('change.statusBadge').on('change.statusBadge', fn);
1475 + }
1476 + };
1477 + }
1478 +
1479 + // Switcher (checkbox) field adapter — on/off.
1480 + if ($checkbox.length) {
1481 + return {
1482 + getValue: function getValue() {
1483 + return $checkbox.is(':checked') ? 'active' : 'inactive';
1484 + },
1485 + getLabel: function getLabel() {
1486 + return $checkbox.is(':checked') ? 'Active' : 'Inactive';
1487 + },
1488 + getOptions: function getOptions() {
1489 + return ['active', 'inactive'];
1490 + },
1491 + setNext: function setNext() {
1492 + $checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change');
1493 + },
1494 + onChange: function onChange(fn) {
1495 + $checkbox.off('change.statusBadge').on('change.statusBadge', fn);
1496 + }
1497 + };
1498 + }
1499 + return null;
1500 + },
1501 + updateLayoutStatus: function updateLayoutStatus() {
1502 + var self = this;
1503 +
1504 + // Sync status badge on each layout header with the field value.
1505 + $('.merchant-flexible-content .layout').each(function () {
1506 + var $badge = $(this).find('.layout-header .layout-status[data-status-field]');
1507 + if (!$badge.length) return;
1508 + var fieldId = $badge.data('status-field');
1509 + var adapter = self.resolveStatusField($(this), fieldId);
1510 + if (!adapter) return;
1511 + function syncBadge() {
1512 + var val = adapter.getValue();
1513 + var label = adapter.getLabel();
1514 + $badge.removeClass(function (i, cls) {
1515 + return (cls.match(/layout-status--\S+/g) || []).join(' ');
1516 + }).addClass('layout-status--' + val).text(label.trim());
1517 + }
1518 + adapter.onChange(syncBadge);
1519 + syncBadge();
420 1520 });
421 1521
422 - // Events.
423 - this.events();
1522 + // Delegated click handler for badge toggle — works on all rows including deferred.
1523 + $(document).off('click.statusToggle').on('click.statusToggle', '.layout-status[data-status-field]', function (e) {
1524 + e.stopPropagation();
1525 + e.preventDefault();
1526 + var $badge = $(this);
1527 + var fieldId = $badge.data('status-field');
1528 + var $layout = $badge.closest('.layout');
1529 + var adapter = self.resolveStatusField($layout, fieldId);
1530 + if (adapter) {
1531 + // Hydrated row: toggle via the field adapter.
1532 + adapter.setNext();
1533 + } else {
1534 + // Deferred row: cycle through options stored in data-status-options,
1535 + // falling back to active/inactive.
1536 + var optionsAttr = $badge.attr('data-status-options');
1537 + var options = optionsAttr ? optionsAttr.split(',') : ['active', 'inactive'];
1538 + var currentVal = $badge.attr('class').match(/layout-status--(\S+)/);
1539 + currentVal = currentVal ? currentVal[1] : options[0];
1540 + var idx = options.indexOf(currentVal);
1541 + var nextIdx = (idx + 1) % options.length;
1542 + var newVal = options[nextIdx];
1543 + var newLabel = newVal.charAt(0).toUpperCase() + newVal.slice(1);
1544 + $badge.removeClass('layout-status--' + currentVal).addClass('layout-status--' + newVal).text(newLabel);
1545 +
1546 + // Update data-fields-json so the value persists when hydrated.
1547 + var jsonAttr = $layout.attr('data-fields-json');
1548 + if (jsonAttr) {
1549 + try {
1550 + var data = JSON.parse(jsonAttr);
1551 + if (!data.values) data.values = {};
1552 + data.values[fieldId] = newVal;
1553 + $layout.attr('data-fields-json', JSON.stringify(data));
1554 + } catch (ex) {/* ignore parse errors */}
1555 + }
1556 +
1557 + // Mark the form as changed.
1558 + $('.merchant-module-page-content').trigger('change.merchant');
1559 + }
1560 + });
424 1561 },
1562 + updateDiscountPercentMaxVal: function updateDiscountPercentMaxVal() {
1563 + $('.merchant-flexible-content .layout').each(function () {
1564 + var $layout = $(this);
1565 + var $discountType = $layout.find('.merchant-module-page-setting-field[data-id="discount_type"]');
1566 + var $discountVal = $layout.find('.merchant-module-page-setting-field[data-id="discount_value"]');
1567 + $discountVal = $discountVal.length ? $discountVal : $layout.find('.merchant-module-page-setting-field[data-id="discount"]');
1568 + $discountVal = $discountVal.length ? $discountVal : $layout.find('.merchant-module-page-setting-field[data-id="discount_amount"]');
1569 + var checkedValue = $discountType.find('input:checked').val();
1570 +
1571 + // Set/Remove max value based on the discount type.
1572 + checkedValue === 'percentage_discount' || checkedValue === 'percentage' ? $discountVal.find('input').attr('max', 100) : $discountVal.find('input').removeAttr('max');
1573 + });
1574 + $('.merchant-module-page-setting-fields');
1575 + },
1576 + generateUUID: function generateUUID() {
1577 + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
1578 + var r = Math.random() * 16 | 0;
1579 + var v = c === 'x' ? r : r & 0x3 | 0x8;
1580 + return v.toString(16);
1581 + });
1582 + },
425 1583 events: function events() {
426 1584 var self = this;
427 - $('.customize-control-flexible-content-add-button').click(function (event) {
1585 + $(document).on('change', '.merchant-module-page-setting-field[data-id="discount_type"] input', function () {
1586 + self.updateDiscountPercentMaxVal();
1587 + });
1588 +
1589 + // Update "selected" attribute on select. It's Required for duplicating layouts.
1590 + $(document).on('change', 'select', function () {
1591 + var selectedValue = $(this).val();
1592 + var isMultiple = $(this).prop('multiple');
1593 + $(this).find('option').each(function () {
1594 + var currentValue = $(this).val();
1595 + var isSelected = isMultiple ? selectedValue.includes(currentValue) : currentValue === selectedValue;
1596 + $(this).attr('selected', isSelected);
1597 + });
1598 + });
1599 + $('.customize-control-flexible-content-add-button').on('click', function (event) {
428 1600 event.preventDefault();
429 1601 event.stopImmediatePropagation();
1602 + if ($(this).parent().find('.customize-control-flexible-content-add-list a').length === 1) {
1603 + // If there is only one layout, trigger click on it.
1604 + $(this).parent().find('.customize-control-flexible-content-add-list a').trigger('click');
1605 + return;
1606 + }
430 1607 $(this).parent().find('.customize-control-flexible-content-add-list').toggleClass('active');
431 1608 });
432 - $('.customize-control-flexible-content-add').click(function (event) {
1609 +
1610 + // Add new item
1611 + $(document).on('click', '.customize-control-flexible-content-add', function (event) {
433 1612 event.preventDefault();
434 1613 event.stopImmediatePropagation();
435 1614 var $field = $('.merchant-flexible-content-control[data-id=' + $(this).data('id') + ']');
436 1615 var $layouts = $field.find('.layouts');
@@ -437,14 +1616,20 @@
437 1616 var $selected = $(this).data('layout');
438 1617 var $layout = $layouts.find('.layout[data-type=' + $selected + ']').clone(true);
439 1618 var $content = $field.find('.merchant-flexible-content');
440 1619 var $items = $content.find('.layout');
441 - $layout.find('input, select').each(function () {
1620 + var uuid = self.generateUUID();
1621 + $layout.find('input, select, textarea').each(function () {
442 1622 if ($(this).data('name')) {
443 1623 $(this).attr('name', $(this).data('name').replace('0', $items.length));
444 1624 }
1625 + if ($(this).is(':checkbox, :radio') && $(this).attr('checked')) {
1626 + $(this).prop('checked', true);
1627 + }
445 1628 });
1629 + $layout.attr('data-layout-id', uuid);
446 1630 $layout.find('.layout-count').text($items.length + 1);
1631 + $layout.find('.flexible-id').val(uuid);
447 1632 $content.append($layout);
448 1633 $content.removeClass('empty');
449 1634 $(this).parent().removeClass('active');
450 1635 if ($layout.find('.merchant-module-page-setting-field-upload').length) {
@@ -452,11 +1637,101 @@
452 1637 }
453 1638 if ($layout.find('.merchant-module-page-setting-field-select_ajax').length) {
454 1639 initSelectAjax($layout.find('.merchant-module-page-setting-field-select_ajax'));
455 1640 }
1641 + var parentDiv = $(this).closest('.merchant-flexible-content-control'),
1642 + hasAccordion = parentDiv.hasClass('has-accordion');
1643 + if (hasAccordion) {
1644 + parentDiv.find('.merchant-flexible-content').accordion("refresh");
1645 + // Expand the accordion last added
1646 + parentDiv.find('.merchant-flexible-content').accordion("option", "active", -1);
1647 + }
1648 + GroubField.init();
1649 + $(document).trigger('merchant-flexible-content-added', [$layout]);
1650 + initMerchantRange();
1651 + self.updateLayoutTitle();
1652 + self.updateLayoutStatus();
1653 + self.updateDiscountPercentMaxVal();
1654 + $('.merchant-module-page-content').trigger('change.merchant');
456 1655 });
457 - $('.customize-control-flexible-content-delete').click(function (event) {
1656 +
1657 + // Duplicate item
1658 + $(document).on('click', '.customize-control-flexible-content-duplicate', function (event) {
458 1659 event.preventDefault();
1660 + event.stopImmediatePropagation();
1661 + var $duplicateBtn = $(this);
1662 + var $flexibleContentWrapper = $duplicateBtn.closest('.merchant-flexible-content-control[data-id=' + $duplicateBtn.data('id') + ']');
1663 + var $flexibleContent = $flexibleContentWrapper === null || $flexibleContentWrapper === void 0 ? void 0 : $flexibleContentWrapper.find('.merchant-flexible-content');
1664 + if (!$flexibleContentWrapper.length || !$flexibleContent.length) {
1665 + return;
1666 + }
1667 + var $sourceLayout = $duplicateBtn.closest('.layout');
1668 + if (!$sourceLayout.length) {
1669 + return;
1670 + }
1671 + $sourceLayout.find('.layout-actions__inner').hide();
1672 +
1673 + // Clone the layout without data & events.
1674 + var $clonedLayout = $sourceLayout.clone();
1675 + var $items = $flexibleContent.find('.layout');
1676 + var index = $sourceLayout.find('.layout-count').text();
1677 + var uuid = self.generateUUID();
1678 + $clonedLayout.attr('data-layout-id', uuid);
1679 + $clonedLayout.find('.flexible-id').val(uuid);
1680 + $clonedLayout.find('input, select, textarea').each(function () {
1681 + var $input = $(this);
1682 + var inputName = $input.attr('name');
1683 + if (inputName) {
1684 + var prefix = inputName.split('[')[0];
1685 + var indexPart = inputName.match(/\[(.*?)\]/g);
1686 + if (indexPart && indexPart.length > 1) {
1687 + indexPart[1] = '[' + index + ']';
1688 + var newName = "".concat(prefix).concat(indexPart.join(''));
1689 + $input.attr('name', newName);
1690 + }
1691 + }
1692 + });
1693 +
1694 + // Find select2, Remove and Re-init again. select.select2('destroy') doesn't work.
1695 + $clonedLayout.find('select').each(function () {
1696 + if ($(this).hasClass('select2-hidden-accessible')) {
1697 + $(this).removeClass('select2-hidden-accessible').removeAttr('data-live-search').removeAttr('data-select2-id').removeAttr('aria-hidden').removeAttr('tabindex');
1698 +
1699 + // Remove the existing dropdown
1700 + $(this).nextAll('.select2-container').remove();
1701 +
1702 + // Re-init
1703 + $(this).select2();
1704 + }
1705 + });
1706 +
1707 + // Removing copied style to make the accordion work properly.
1708 + $clonedLayout.find('.layout-body').removeAttr('style');
1709 +
1710 + // Append the cloned layout right after the source one with fadeIn effect.
1711 + $clonedLayout.hide();
1712 + $clonedLayout.insertAfter($sourceLayout);
1713 + $clonedLayout.fadeIn();
1714 + if ($clonedLayout.find('.merchant-module-page-setting-field-upload').length) {
1715 + initUploadField($clonedLayout.find('.merchant-module-page-setting-field-upload'));
1716 + }
1717 + if ($clonedLayout.find('.merchant-module-page-setting-field-select_ajax').length) {
1718 + initSelectAjax($clonedLayout.find('.merchant-module-page-setting-field-select_ajax'));
1719 + }
1720 + self.refreshNumbers($flexibleContent);
1721 + $(document).trigger('merchant-flexible-content-added', [$clonedLayout]);
1722 + if ($flexibleContentWrapper.hasClass('has-accordion')) {
1723 + $flexibleContent.accordion('refresh');
1724 + }
1725 + self.updateLayoutTitle();
1726 + self.updateLayoutStatus();
1727 + GroubField.init();
1728 + $('.merchant-module-page-content').trigger('change.merchant');
1729 + });
1730 +
1731 + // Delete item
1732 + $(document).on('click', '.customize-control-flexible-content-delete', function (event) {
1733 + event.preventDefault();
459 1734 var $item = $(this).closest('.layout');
460 1735 var $content = $item.parent();
461 1736 $item.remove();
462 1737 if ($content.find('.layout').length === 0) {
@@ -462,16 +1737,344 @@
462 1737 if ($content.find('.layout').length === 0) {
463 1738 $content.addClass('empty');
464 1739 }
465 1740 self.refreshNumbers($content);
1741 + $(document).trigger('merchant-flexible-content-deleted', [$item]);
1742 + var parentDiv = $(this).closest('.merchant-flexible-content-control'),
1743 + hasAccordion = parentDiv.hasClass('has-accordion');
1744 + if (hasAccordion) {
1745 + parentDiv.find('.merchant-flexible-content').accordion("refresh");
1746 + }
1747 + $('.merchant-module-page-content').trigger('change.merchant');
466 1748 });
1749 +
1750 + // Toggle Actions(delete/duplicate)
1751 + $(document).on('click', '.layout-actions__toggle', function (e) {
1752 + e.preventDefault();
1753 +
1754 + // Hide other opened elements
1755 + hideOtherActions($(this).closest('.layout'));
1756 +
1757 + // Toggle the current element
1758 + $(this).closest('.layout-actions').find('.layout-actions__inner').stop().slideToggle(300);
1759 + });
1760 +
1761 + // Hide Actions when collapse/open
1762 + $(document).on('click', '.layout-header', function () {
1763 + hideOtherActions($(this).closest('.layout'));
1764 + });
1765 + $(document).on('merchant-flexible-content-added', function (e, $layout) {
1766 + hideOtherActions($layout);
1767 + });
1768 + function hideOtherActions($layout) {
1769 + if ($layout && $layout.length) {
1770 + $layout.siblings().find('.layout-actions__inner').slideUp(300);
1771 + }
1772 + }
1773 +
1774 + // Dismiss actions menu on click outside or Escape.
1775 + $(document).on('click', function (e) {
1776 + if (!$(e.target).closest('.layout-actions').length) {
1777 + $('.layout-actions__inner').slideUp(300);
1778 + }
1779 + });
1780 + $(document).on('keydown', function (e) {
1781 + if (e.key === 'Escape') {
1782 + $('.layout-actions__inner').slideUp(300);
1783 + }
1784 + });
467 1785 },
1786 + /**
1787 + * Hydrate a deferred layout row.
1788 + *
1789 + * Clones the hidden template for the layout type, populates field values
1790 + * from the data-fields-json attribute, initialises all widgets, and
1791 + * replaces the empty layout-body content with the fully rendered fields.
1792 + *
1793 + * @param {jQuery} $layout The .layout element with data-deferred="1".
1794 + */
1795 + hydrateLayout: function hydrateLayout($layout) {
1796 + var jsonStr = $layout.attr('data-fields-json');
1797 + if (!jsonStr) {
1798 + return;
1799 + }
1800 + var data;
1801 + try {
1802 + data = JSON.parse(jsonStr);
1803 + } catch (e) {
1804 + return;
1805 + }
1806 + var layoutType = $layout.attr('data-type');
1807 + var $control = $layout.closest('.merchant-flexible-content-control');
1808 + var $template = $control.find('.layouts .layout[data-type="' + layoutType + '"]');
1809 + if (!$template.length) {
1810 + return;
1811 + }
1812 +
1813 + // Clone the template's layout-body content (the rendered fields).
1814 + var $clonedBody = $template.find('.layout-body').clone();
1815 +
1816 + // Determine the correct row index from layout-count.
1817 + var rowIndex = parseInt($layout.find('.layout-count').text(), 10) - 1;
1818 + var fieldId = $control.attr('data-id');
1819 +
1820 + // Fix name attributes: replace data-name with name, and index [0] with [rowIndex].
1821 + $clonedBody.find('input, select, textarea').each(function () {
1822 + var dataName = $(this).attr('data-name');
1823 + if (dataName) {
1824 + $(this).attr('name', dataName.replace('[0]', '[' + rowIndex + ']'));
1825 + $(this).removeAttr('data-name');
1826 + }
1827 + });
1828 +
1829 + // Populate field values from the resolved data.
1830 + this.populateFieldValues($clonedBody, data, fieldId, rowIndex);
1831 +
1832 + // Sync color picker preview boxes to reflect populated values.
1833 + $clonedBody.find('.merchant-color').each(function () {
1834 + var colorVal = $(this).find('.merchant-color-input').val();
1835 + if (colorVal) {
1836 + $(this).find('.merchant-color-picker').css('background-color', colorVal);
1837 + }
1838 + });
1839 +
1840 + // Sync range slider values from their sibling number inputs.
1841 + // populateFieldValues only sets the number input (it has the name attribute);
1842 + // the range slider (no name) still holds the template default.
1843 + $clonedBody.find('.merchant-range').each(function () {
1844 + var numVal = $(this).find('.merchant-range-number-input').val();
1845 + if (numVal !== undefined && numVal !== '') {
1846 + $(this).find('.merchant-range-input').val(numVal);
1847 + }
1848 + });
1849 +
1850 + // Swap the content of the layout-body (not the element itself)
1851 + // to preserve jQuery UI accordion's internal panel reference.
1852 + var $existingBody = $layout.find('.layout-body');
1853 + $existingBody.empty().append($clonedBody.children());
1854 +
1855 + // Initialise widgets within the hydrated layout.
1856 + if ($existingBody.find('.merchant-module-page-setting-field-upload').length) {
1857 + initUploadField($existingBody.find('.merchant-module-page-setting-field-upload'));
1858 + }
1859 + if ($existingBody.find('.merchant-module-page-setting-field-select_ajax').length) {
1860 + initSelectAjax($existingBody.find('.merchant-module-page-setting-field-select_ajax'));
1861 + }
1862 +
1863 + // Trigger events to re-initialise fields_group, color pickers, conditions, etc.
1864 + $(document).trigger('merchant-flexible-content-added', [$layout]);
1865 + GroubField.init();
1866 + initMerchantRange();
1867 +
1868 + // Init layout title binding.
1869 + var $title = $layout.find('.layout-title[data-title-field]');
1870 + if ($title.length) {
1871 + var titleFieldId = $title.attr('data-title-field');
1872 + var $titleInput = $layout.find('.layout-body .merchant-field-' + titleFieldId + ' input');
1873 + $titleInput.on('change keyup', function () {
1874 + $title.text($(this).val());
1875 + });
1876 + }
1877 +
1878 + // Init status badge binding for hydrated row.
1879 + var $badge = $layout.find('.layout-header .layout-status[data-status-field]');
1880 + if ($badge.length) {
1881 + var statusFieldId = $badge.data('status-field');
1882 + var statusAdapter = this.resolveStatusField($layout, statusFieldId);
1883 + if (statusAdapter) {
1884 + var syncHydratedBadge = function syncHydratedBadge() {
1885 + var val = statusAdapter.getValue();
1886 + var label = statusAdapter.getLabel();
1887 + $badge.removeClass(function (i, cls) {
1888 + return (cls.match(/layout-status--\S+/g) || []).join(' ');
1889 + }).addClass('layout-status--' + val).text(label.trim());
1890 + };
1891 + statusAdapter.onChange(syncHydratedBadge);
1892 + syncHydratedBadge();
1893 + }
1894 + }
1895 +
1896 + // Init discount max val.
1897 + this.updateDiscountPercentMaxVal();
1898 +
1899 + // Remove the deferred flag and clean up.
1900 + $layout.removeAttr('data-deferred');
1901 + $layout.removeAttr('data-fields-json');
1902 +
1903 + // Refresh accordion to re-sync panel references after DOM change.
1904 + var $content = $control.find('.merchant-flexible-content');
1905 + if ($content.data('ui-accordion')) {
1906 + $content.accordion('refresh');
1907 + }
1908 +
1909 + // Trigger condition checks for the newly hydrated panel.
1910 + $(document).trigger('merchant-admin-check-fields');
1911 + $(document).trigger('merchant-admin-check-color-fields');
1912 + },
1913 + /**
1914 + * Populate field values from deferred data into a cloned template body.
1915 + *
1916 + * @param {jQuery} $body The cloned .layout-body element.
1917 + * @param {Object} data The parsed data-fields-json object.
1918 + * @param {string} fieldId The flexible content field ID.
1919 + * @param {number} rowIndex The row index for name attribute construction.
1920 + */
1921 + populateFieldValues: function populateFieldValues($body, data, fieldId, rowIndex) {
1922 + var values = data.values || {};
1923 + var selectOptions = data.select_options || {};
1924 + var productOptions = data.product_options || {};
1925 + var reviewOptions = data.review_options || {};
1926 +
1927 + // Populate simple field values.
1928 + $body.find('input, select, textarea').each(function () {
1929 + var $el = $(this);
1930 + var name = $el.attr('name') || '';
1931 +
1932 + // Handle checkbox_multiple: name ends with [] (e.g. merchant[fc][0][show_pages][]).
1933 + var arrayMatch = name.match(/\[([^\]]+)\]\[\]$/);
1934 + if (arrayMatch) {
1935 + var arrayKey = arrayMatch[1];
1936 + var arrVal = values[arrayKey];
1937 + if (Array.isArray(arrVal) && $el.is(':checkbox')) {
1938 + $el.prop('checked', arrVal.indexOf($el.val()) !== -1);
1939 + }
1940 + return;
1941 + }
1942 +
1943 + // Extract the field key from name="merchant[fieldId][rowIndex][fieldKey]".
1944 + var match = name.match(/\[([^\]]+)\]$/);
1945 + if (!match) {
1946 + return;
1947 + }
1948 + var fieldKey = match[1];
1949 + var val = values[fieldKey];
1950 + if (val === undefined || val === null) {
1951 + return;
1952 + }
1953 + if ($el.is(':checkbox') && !$el.is(':radio')) {
1954 + $el.prop('checked', !!parseInt(val, 10));
1955 + } else if ($el.is(':radio')) {
1956 + $el.prop('checked', $el.val() === String(val));
1957 + } else if ($el.is('select')) {
1958 + $el.val(val);
1959 + } else {
1960 + $el.val(_typeof(val) === 'object' && val !== null ? JSON.stringify(val) : val);
1961 + }
1962 + });
1963 +
1964 + // Populate select_ajax pre-selected options.
1965 + $.each(selectOptions, function (selectFieldId, options) {
1966 + var $select = $body.find('[data-id="' + selectFieldId + '"] select');
1967 + if (!$select.length) {
1968 + return;
1969 + }
1970 +
1971 + // Clear any existing options and add resolved ones.
1972 + $select.empty();
1973 + $.each(options, function (_, opt) {
1974 + $select.append($('<option>').val(opt.id).text(opt.text).prop('selected', true));
1975 + });
1976 + });
1977 +
1978 + // Populate products_selector pre-selected products.
1979 + $.each(productOptions, function (prodFieldId, products) {
1980 + var $container = $body.find('[data-id="' + prodFieldId + '"] .merchant-products-search-container');
1981 + if (!$container.length) {
1982 + return;
1983 + }
1984 + var $preview = $container.find('.merchant-selected-products-preview ul');
1985 + var ids = [];
1986 + $preview.empty();
1987 + $.each(products, function (_, product) {
1988 + ids.push(product.id);
1989 +
1990 + // Build <li> matching the structure from Merchant_Admin_Ajax::product_data_li().
1991 + var $li = $('<li>').addClass('product-item').attr('data-id', product.id).attr('data-name', product.title);
1992 + if (product.image) {
1993 + $li.append($('<span>').addClass('img').append($('<img>').attr('src', product.image).attr('alt', product.title).attr('width', 30).attr('height', 30)));
1994 + }
1995 + $li.append($('<span>').addClass('data').append($('<span>').addClass('name').text(product.title), $('<span>').addClass('info').html(product.price)));
1996 + if (product.type || product.id) {
1997 + var typeHtml = (product.type || '') + '<br>#' + product.id;
1998 + var $typeSpan = $('<span>').addClass('type');
1999 + if (product.edit_url) {
2000 + $typeSpan.append($('<a>').attr('href', product.edit_url).attr('target', '_blank').html(typeHtml));
2001 + } else {
2002 + $typeSpan.html(typeHtml);
2003 + }
2004 + $li.append($typeSpan);
2005 + }
2006 + $li.append($('<span>').addClass('remove hint--left').attr('aria-label', 'Remove').html('&times;'));
2007 + $preview.append($li);
2008 + });
2009 + $container.find('.merchant-selected-products').val(ids.join(','));
2010 + });
2011 +
2012 + // Populate reviews_selector pre-selected reviews.
2013 + $.each(reviewOptions, function (reviewFieldId, reviews) {
2014 + var $container = $body.find('[data-id="' + reviewFieldId + '"] .merchant-reviews-selector');
2015 + if (!$container.length) {
2016 + return;
2017 + }
2018 + var $selectedList = $container.find('.selected-reviews .product-reviews');
2019 + var ids = [];
2020 + $selectedList.empty();
2021 + $.each(reviews, function (_, review) {
2022 + ids.push(review.id);
2023 + var $reviewEl = $('<div>').addClass('product-review').attr('data-review-id', review.id);
2024 + $reviewEl.append($('<span>').addClass('review-author').text(review.author));
2025 + $reviewEl.append($('<span>').addClass('review-content').text(review.content));
2026 + $reviewEl.append($('<span>').addClass('review-rating').text('★'.repeat(parseInt(review.rating) || 0)));
2027 + $reviewEl.append($('<span>').addClass('review-product').text(review.product_name));
2028 + $reviewEl.append($('<span>').addClass('product-review-delete').html('&times;'));
2029 + $reviewEl.append($('<span>').addClass('product-review-move').html('⋮⋮'));
2030 + $selectedList.append($reviewEl);
2031 + });
2032 + $container.find('.merchant-selected-reviews').val(ids.join(','));
2033 + });
2034 +
2035 + // Handle fields_group and compound (e.g. hook_select) nested values.
2036 + // Recursively walks the value tree to find and set inputs
2037 + // at any nesting depth (e.g. fields_group → hook_select → hook_name).
2038 + var _setNestedValues = function setNestedValues(obj, namePrefix) {
2039 + $.each(obj, function (key, val) {
2040 + if (_typeof(val) === 'object' && val !== null && !Array.isArray(val)) {
2041 + // Recurse into nested objects.
2042 + _setNestedValues(val, namePrefix + '[' + key + ']');
2043 + } else if (Array.isArray(val)) {
2044 + // checkbox_multiple inside a nested object: check boxes whose value is in the array.
2045 + $body.find('[name="' + namePrefix + '[' + key + '][]"]').each(function () {
2046 + if ($(this).is(':checkbox')) {
2047 + $(this).prop('checked', val.indexOf($(this).val()) !== -1);
2048 + }
2049 + });
2050 + } else {
2051 + var $field = $body.find('[name="' + namePrefix + '[' + key + ']"]');
2052 + if (!$field.length) {
2053 + return;
2054 + }
2055 + if ($field.is(':checkbox') && !$field.is(':radio')) {
2056 + $field.prop('checked', !!parseInt(val, 10));
2057 + } else if ($field.is(':radio')) {
2058 + $field.prop('checked', $field.val() === String(val));
2059 + } else {
2060 + $field.val(val);
2061 + }
2062 + }
2063 + });
2064 + };
2065 + $.each(values, function (key, val) {
2066 + if (_typeof(val) === 'object' && val !== null && !Array.isArray(val)) {
2067 + _setNestedValues(val, 'merchant[' + fieldId + '][' + rowIndex + '][' + key + ']');
2068 + }
2069 + });
2070 + },
468 2071 refreshNumbers: function refreshNumbers($content) {
469 2072 $content.find('.layout').each(function (index) {
470 2073 var $count = $(this).find('.layout-count').text();
471 2074 var $inputIndex = parseInt($count) - 1;
472 2075 $(this).find('.layout-count').text(index + 1);
473 - $(this).find('input, select').each(function () {
2076 + $(this).find('input, select, textarea').each(function () {
474 2077 if ($(this).attr('name')) {
475 2078 $(this).attr('name', $(this).attr('name').replace('[' + $inputIndex + ']', '[*refreshed*' + index + ']'));
476 2079 }
477 2080 });
@@ -476,12 +2079,16 @@
476 2079 }
477 2080 });
478 2081 });
479 2082 $content.find('.layout').each(function (index) {
480 - // We've added *refreshed* tp the attribute name in the prior loop as refreshing the numbers in the attribute can cause
481 - // checked boxes to be unchecked due to similar attribute names during the change while sorting, within this loop we remove them
482 - $(this).find('input, select').each(function () {
483 - $(this).attr('name', $(this).attr('name').replace('*refreshed*', ''));
2083 + $(this).find('input, select, textarea').each(function () {
2084 + // We've added *refreshed* to the attribute name in the prior loop as refreshing the numbers in the attribute can cause
2085 + // checked boxes to be unchecked due to similar attribute names during the change while sorting, within this loop we remove them
2086 + var nameAttr = $(this).attr('name');
2087 + if (nameAttr) {
2088 + // Check if name attribute exists
2089 + $(this).attr('name', nameAttr.replace('*refreshed*', ''));
2090 + }
484 2091 });
485 2092 });
486 2093 $content.parent().find('input').trigger('change.merchant');
487 2094 }
@@ -488,9 +2095,259 @@
488 2095 };
489 2096
490 2097 // Initialize Flexible Content.
491 2098 FlexibleContentField.init();
2099 + GroubField.init();
2100 + ReviewsSelector.init();
492 2101
2102 + // Products selector.
2103 + // Handle keyup event for the search input
2104 + var debounceTimer;
2105 + $(document).on('keyup', '.merchant-module-page-setting-field-products_selector .merchant-search-field', function () {
2106 + clearTimeout(debounceTimer);
2107 + var categories = [];
2108 + var $excluded = $(this).closest('[data-id="excluded_products"]');
2109 + if ($excluded.length) {
2110 + var $layout = $(this).closest('.layout');
2111 + var rules = $layout.find('.merchant-field-rules_to_apply select').val() || $layout.find('.merchant-field-rules_to_display select').val() || $layout.find('.merchant-field-display_rules select').val();
2112 + if (rules === 'categories' || rules === 'by_category') {
2113 + categories = $layout.find('.merchant-field-category_slugs select').val() || $layout.find('.merchant-field-product_cats select').val();
2114 + }
2115 + }
2116 + var parent = $(this).closest('.merchant-products-search-container');
2117 + if ($(this).val() !== '') {
2118 + parent.find('.merchant-searching').addClass('active');
2119 + var data = {
2120 + action: 'merchant_admin_products_search',
2121 + nonce: merchant_admin_options.ajaxnonce,
2122 + keyword: $(this).val(),
2123 + product_types: $(this).data('allowed-types'),
2124 + ids: parent.find('.merchant-selected-products').val(),
2125 + categories: categories
2126 + };
2127 + debounceTimer = setTimeout(function () {
2128 + $.post(merchant_admin_options.ajaxurl, data, function (response) {
2129 + var results = parent.find('.merchant-selections-products-preview');
2130 + results.show();
2131 + results.html(response);
2132 + parent.find('.merchant-searching').removeClass('active');
2133 + });
2134 + }, 250);
2135 + } else {
2136 + parent.find('.merchant-selections-products-preview').html('').hide();
2137 + }
2138 + });
2139 +
2140 + // Products selector: dismiss search results on click outside or Escape.
2141 + $(document).on('click touch', function (e) {
2142 + if (!$(e.target).closest('.merchant-products-search-container').length) {
2143 + $('.merchant-selections-products-preview').html('').hide();
2144 + $('.merchant-search-field').val('');
2145 + }
2146 + });
2147 + $(document).on('keydown', '.merchant-search-field', function (e) {
2148 + if (e.key === 'Escape') {
2149 + var parent = $(this).closest('.merchant-products-search-container');
2150 + parent.find('.merchant-selections-products-preview').html('').hide();
2151 + $(this).val('').blur();
2152 + }
2153 + });
2154 +
2155 + // Products selector.
2156 + // Handle click/touch event for the search results
2157 + $(document).on('click touch', '.merchant-module-page-setting-field-products_selector .merchant-selections-products-preview li', function () {
2158 + var parent = $(this).closest('.merchant-products-search-container'),
2159 + valueField = parent.find('.merchant-selected-products'),
2160 + oldValue = valueField.val(),
2161 + multiple = parent.data('multiple') === 'multiple';
2162 + if (parent.find('.merchant-selected-products-preview ul li').length > 0 && !multiple) {
2163 + // replace the first item
2164 + parent.find('.merchant-selected-products-preview ul li').remove();
2165 + valueField.val('').change();
2166 + }
2167 + $(this).children('.remove').attr('aria-label', 'Remove').html('×');
2168 + parent.find('.merchant-selected-products-preview ul').append($(this));
2169 + parent.find('.merchant-selections-products-preview').html('').hide();
2170 + parent.find('.merchant-search-field').val('').change();
2171 + if (oldValue === '') {
2172 + valueField.val($(this).data('id')).change();
2173 + } else {
2174 + if (!multiple) {
2175 + valueField.val($(this).data('id')).change();
2176 + } else {
2177 + var newValue = oldValue.split(',');
2178 + newValue.push($(this).data('id'));
2179 + valueField.val(newValue.join(',')).change();
2180 + }
2181 + }
2182 + });
2183 +
2184 + // Products selector.
2185 + // Handle click/touch event for the remove button.
2186 + $(document).on('click touch', '.merchant-selected-products-preview .remove', function () {
2187 + var removeButton = $(this);
2188 + var parent = removeButton.closest('.merchant-products-search-container'),
2189 + valueField = parent.find('.merchant-selected-products'),
2190 + id = removeButton.parent().data('id');
2191 + removeButton.parent().remove();
2192 + var currentValue = valueField.val().split(',');
2193 + if (currentValue.length > 0) {
2194 + for (var key in currentValue) {
2195 + if (parseInt(currentValue[key]) === parseInt(id)) {
2196 + currentValue.splice(key, 1);
2197 + }
2198 + }
2199 + }
2200 + valueField.val(currentValue.join(',')).change();
2201 + valueField.trigger('change.merchant');
2202 + });
2203 + $(document).on('merchant-admin-check-fields merchant-flexible-content-added', function () {
2204 + $('.merchant-module-page-setting-field').each(function () {
2205 + var $field = $(this);
2206 + if ($field.data('condition') && $field.data('condition').length) {
2207 + var condition = $field.data('condition');
2208 + var $target = $(this).closest('.layout-body').find('input[name*="' + condition[0] + '"],select[name*="' + condition[0] + '"]');
2209 + if (!$target.length) {
2210 + $target = $('input[name="merchant[' + condition[0] + ']"],select[name="merchant[' + condition[0] + ']"]');
2211 + }
2212 + if ($target.length) {
2213 + var passed = false;
2214 + switch (condition[1]) {
2215 + case '==':
2216 + if ($target.attr('type') === 'radio' || $target.attr('type') === 'checkbox') {
2217 + var checked = $target.parent().find('input:checked');
2218 + if (checked.length && checked.val() === condition[2]) {
2219 + passed = true;
2220 + }
2221 + }
2222 + if ($target.is('select') && $target.val() == condition[2]) {
2223 + passed = true;
2224 + }
2225 + break;
2226 + case 'any':
2227 + if ($target.attr('type') === 'radio' || $target.attr('type') === 'checkbox') {
2228 + var _checked = $target.parent().find('input:checked');
2229 + if (_checked.length && condition[2].split('|').includes(_checked.val())) {
2230 + passed = true;
2231 + }
2232 + }
2233 + if ($target.is('select') && condition[2].split('|').includes($target.val())) {
2234 + passed = true;
2235 + }
2236 + break;
2237 + }
2238 + if (passed) {
2239 + $field.removeClass('merchant-hide').addClass('merchant-show');
2240 + } else {
2241 + $field.removeClass('merchant-show').addClass('merchant-hide');
2242 + }
2243 + }
2244 + }
2245 + });
2246 + }).trigger('merchant.change');
2247 + $(document).on('merchant-admin-check-fields merchant-flexible-content-added change keyup', function () {
2248 + $(document).find('.merchant-module-page-setting-field').each(function () {
2249 + var $field = $(this);
2250 + if ($field.data('conditions')) {
2251 + var conditions = $field.data('conditions'),
2252 + passed = evaluateConditions(conditions, $field);
2253 + if (passed) {
2254 + $field.removeClass('merchant-hide').addClass('merchant-show');
2255 + } else {
2256 + $field.removeClass('merchant-show').addClass('merchant-hide');
2257 + }
2258 + }
2259 + });
2260 + }).trigger('merchant.change');
2261 + $(document).on('change', '.merchant-module-page-setting-field', function () {
2262 + $(document).trigger('merchant-admin-check-fields');
2263 + }).trigger('merchant.change');
2264 + $(document).trigger('merchant-admin-check-fields');
2265 + $(document).on('merchant-admin-check-color-fields merchant-flexible-content-added', function () {
2266 + $('.merchant-color').each(function () {
2267 + var $color = $(this);
2268 + var $picker = $color.find('.merchant-color-picker');
2269 + var $input = $color.find('.merchant-color-input');
2270 + var inited = false;
2271 + var pickr;
2272 + $picker.off('click').on('click', function (e) {
2273 + e.preventDefault();
2274 + e.stopPropagation();
2275 + var $bodyHTML = $('body,html');
2276 + $bodyHTML.addClass('merchant-height-auto');
2277 + if (!inited) {
2278 + pickr = new Pickr({
2279 + el: $picker.get(0),
2280 + container: 'body',
2281 + theme: 'merchant',
2282 + appClass: 'merchant-pcr-app',
2283 + default: $input.val() || $picker.data('default-color') || '#212121',
2284 + swatches: ['#000000', '#F44336', '#E91E63', '#673AB7', '#03A9F4', '#8BC34A', '#FFEB3B', '#FFC107', '#FFFFFF'],
2285 + sliders: 'h',
2286 + useAsButton: true,
2287 + components: {
2288 + hue: true,
2289 + preview: true,
2290 + opacity: true,
2291 + interaction: {
2292 + input: true,
2293 + clear: true
2294 + }
2295 + },
2296 + i18n: {
2297 + 'btn:clear': 'Default'
2298 + }
2299 + });
2300 + pickr.on('change', function (color) {
2301 + var colorCode;
2302 + if (color.a === 1) {
2303 + pickr.setColorRepresentation('HEX');
2304 + colorCode = color.toHEXA().toString(0);
2305 + } else {
2306 + pickr.setColorRepresentation('RGBA');
2307 + colorCode = color.toRGBA().toString(0);
2308 + }
2309 + $picker.css({
2310 + 'background-color': colorCode
2311 + });
2312 + if ($input.val() !== colorCode) {
2313 + $input.val(colorCode).trigger('change.merchant');
2314 + }
2315 + $(document).trigger('merchant-color-picker-updated', [colorCode, $input]);
2316 + });
2317 + pickr.on('clear', function () {
2318 + var defaultColor = $picker.data('default-color');
2319 + if (defaultColor) {
2320 + pickr.setColor(defaultColor);
2321 + } else {
2322 + $picker.css({
2323 + 'background-color': 'white'
2324 + });
2325 + $input.val('');
2326 + }
2327 + });
2328 + pickr.on('hide', function () {
2329 + $bodyHTML.removeClass('merchant-height-auto');
2330 + });
2331 + $picker.data('pickr', pickr);
2332 + setTimeout(function () {
2333 + pickr.show();
2334 + }, 200);
2335 + inited = true;
2336 + } else {
2337 + pickr.setColor($input.val());
2338 + }
2339 + });
2340 + $input.on('change keyup', function () {
2341 + var colorCode = $(this).val();
2342 + $picker.css({
2343 + 'background-color': colorCode
2344 + });
2345 + });
2346 + });
2347 + });
2348 + $(document).trigger('merchant-admin-check-color-fields');
2349 +
493 2350 // Create Page Control.
494 2351 var CreatePageControl = {
495 2352 init: function init() {
496 2353 this.events();
@@ -533,11 +2390,13 @@
533 2390 });
534 2391 },
535 2392 ajaxResponseHandler: function ajaxResponseHandler(response, $this, $success_message, $create_message) {
536 2393 if ('success' === response.status) {
537 - var href = $success_message.find('a').attr('href'),
538 - newhref = href.replace('?post=&', '?post=' + response.page_id + '&');
539 - $success_message.find('a').attr('href', newhref);
2394 + var $editLink = $success_message.find('a').first(),
2395 + href = $editLink.attr('href');
2396 + if (href) {
2397 + $editLink.attr('href', href.replace('?post=&', '?post=' + response.page_id + '&'));
2398 + }
540 2399 $success_message.css('display', 'block');
541 2400 $create_message.remove();
542 2401 $this.remove();
543 2402 }
@@ -642,14 +2501,17 @@
642 2501 wpMediaFrame.on('select', function () {
643 2502 $wrapper.empty();
644 2503 var item = wpMediaFrame.state().get('selection').first().attributes;
645 2504 var thumb = item.sizes && item.sizes.thumbnail && item.sizes.thumbnail.url ? item.sizes.thumbnail.url : item.url;
646 - $wrapper.append('<div class="merchant-upload-image"><i class="merchant-upload-remove dashicons dashicons-no-alt"></i><img src="' + thumb + '" /></div>');
2505 + var sizes = item.sizes ? JSON.stringify(item.sizes) : '';
2506 + $wrapper.append('<div class="merchant-upload-image" data-sizes=\'' + sizes + '\'><i class="merchant-upload-remove dashicons dashicons-no-alt"></i><img src="' + thumb + '" /></div>');
647 2507 $input.val(item.id).trigger('change');
2508 + $this.find('.merchant-upload-button-drag-drop').hide();
648 2509 $this.find('.merchant-upload-remove').on('click', function (e) {
649 2510 e.preventDefault();
650 2511 $(this).parent().remove();
651 2512 $input.val('').trigger('change');
2513 + $this.find('.merchant-upload-button-drag-drop').show();
652 2514 });
653 2515 });
654 2516 });
655 2517 };
@@ -655,16 +2517,39 @@
655 2517 };
656 2518 $('.merchant-module-page-setting-field-upload:not(.template)').each(function () {
657 2519 initUploadField($(this));
658 2520 });
659 - var initSelectAjax = function initSelectAjax($selectAjax) {
2521 +
2522 + // Drag & Drag
2523 + var events = ['dragenter', 'dragover', 'dragleave', 'drop'];
2524 + jQuery.each(events, function (index, eventName) {
2525 + $(document).on(eventName, '.merchant-upload-button-drag-drop', function (e) {
2526 + e.preventDefault();
2527 + e.stopPropagation();
2528 + });
2529 + });
2530 + $(document).on('dragenter', '.merchant-upload-button-drag-drop', function (e) {
2531 + $(this).closest('.merchant-module-page-setting-field-upload').find('.merchant-upload-button').click();
2532 + });
2533 +
2534 + // Callers pass a collection, and reading the source off a collection would give
2535 + // every select the first field's source, so each field is set up on its own.
2536 + var initSelectAjax = function initSelectAjax($selectAjaxFields) {
2537 + $selectAjaxFields.each(function () {
2538 + initSelectAjaxField($(this));
2539 + });
2540 + };
2541 + var initSelectAjaxField = function initSelectAjaxField($selectAjax) {
660 2542 var $select = $selectAjax.find('select');
661 2543 var $source = $select.data('source');
662 2544 var $config = window.merchant_admin_options;
663 2545 var $object = {
664 - width: '100%'
2546 + width: '100%',
2547 + templateSelection: function templateSelection(category) {
2548 + return category.text.replace(/&nbsp;-*\s*/g, '').trim();
2549 + }
665 2550 };
666 - if ($source === 'post' || $source === 'product') {
2551 + if ($source === 'post' || $source === 'product' || $source === 'user') {
667 2552 $object.minimumInputLength = 1;
668 2553 $object.ajax = {
669 2554 url: $config.ajaxurl,
670 2555 dataType: 'json',
@@ -758,8 +2643,26 @@
758 2643 }
759 2644 });
760 2645 }
761 2646 });
2647 +
2648 + // Hide fixed changelog items
2649 + var merchantNotificationsContent = $('.merchant-notification-content');
2650 + merchantNotificationsContent.each(function () {
2651 + var notificationContentItem = $(this);
2652 + // search for all spans that contains class "changelog-fixed" and then go to the parent li of that span and add class hidden
2653 + var fixedItems = notificationContentItem.find('span.changelog-fixed');
2654 + fixedItems.each(function () {
2655 + var fixedItem = $(this);
2656 + fixedItem.closest('li').remove();
2657 + });
2658 + // now check if all li inside this notificationContentItem are hidden, so we need to look for the parent .merchant-notification and remove it
2659 + var allItems = notificationContentItem.find('li');
2660 + var hiddenItems = notificationContentItem.find('li.hidden');
2661 + if (allItems.length === hiddenItems.length) {
2662 + notificationContentItem.closest('.merchant-notification').remove();
2663 + }
2664 + });
762 2665 $(window).on('scroll', function () {
763 2666 if (window.pageYOffset > 60) {
764 2667 $notificationsSidebar.addClass('closing');
765 2668 setTimeout(function () {
@@ -812,6 +2715,252 @@
812 2715 $moduleAlert.removeClass('merchant-show');
813 2716 $(document).off('click.merchant-alert-close');
814 2717 });
815 2718 }
2719 +
2720 + // AI Prompt Examples Modal
2721 + var $aiPromptsModal = $('#merchant-ai-prompts-modal');
2722 + if ($aiPromptsModal.length) {
2723 + var $aiPromptsList = $aiPromptsModal.find('.merchant-ai-prompts-modal-list');
2724 + var copyHintText = window.merchant.ai_prompt_copy_hint || 'Click to copy';
2725 + var copiedLabelText = window.merchant.ai_prompt_copied_label || 'Copied!';
2726 + var copyIconSvg = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M3 10V3.5C3 2.67157 3.67157 2 4.5 2H10.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>';
2727 + var checkIconSvg = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 4.5L6 12L2.5 8.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
2728 + var copyTextToClipboard = function copyTextToClipboard(text, onCopied) {
2729 + if (navigator.clipboard && navigator.clipboard.writeText) {
2730 + navigator.clipboard.writeText(text).then(onCopied);
2731 + return;
2732 + }
2733 + var $temp = $('<textarea readonly></textarea>').val(text).css({
2734 + position: 'fixed',
2735 + top: '-1000px'
2736 + });
2737 + $('body').append($temp);
2738 + $temp[0].select();
2739 + document.execCommand('copy');
2740 + $temp.remove();
2741 + onCopied();
2742 + };
2743 + $(document).on('click', '.merchant-ai-badge', function (e) {
2744 + e.preventDefault();
2745 + e.stopPropagation();
2746 + var prompts = $(this).data('aiPrompts');
2747 + $aiPromptsModal.find('.merchant-ai-prompts-modal-title-text').text($(this).data('aiTitle') || '');
2748 + $aiPromptsModal.find('.merchant-ai-prompts-modal-blurb').text($(this).data('aiBlurb') || '');
2749 + $aiPromptsList.empty();
2750 + if (Array.isArray(prompts)) {
2751 + prompts.forEach(function (prompt) {
2752 + var $row = $('<button type="button" class="merchant-ai-prompt-row"></button>').attr('data-prompt', prompt);
2753 + $row.append($('<span class="merchant-ai-prompt-text"></span>').text(prompt));
2754 + $row.append($('<span class="merchant-ai-prompt-icon"></span>').html(copyIconSvg));
2755 + $row.attr('aria-label', copyHintText);
2756 + $aiPromptsList.append($row);
2757 + });
2758 + }
2759 + $aiPromptsModal.addClass('merchant-show');
2760 + var $badgeIcon = $aiPromptsModal.find('.merchant-ai-prompts-modal-badge svg');
2761 + $badgeIcon.removeClass('merchant-ai-badge-spin');
2762 + void $badgeIcon[0].offsetWidth;
2763 + $badgeIcon.addClass('merchant-ai-badge-spin');
2764 + });
2765 + $(document).on('keydown', '.merchant-ai-badge', function (e) {
2766 + if (e.key === 'Enter' || e.key === ' ') {
2767 + e.preventDefault();
2768 + $(this).trigger('click');
2769 + }
2770 + });
2771 + $aiPromptsList.on('click', '.merchant-ai-prompt-row', function () {
2772 + var $row = $(this);
2773 + if ($row.hasClass('is-copied')) {
2774 + return;
2775 + }
2776 + copyTextToClipboard($row.attr('data-prompt'), function () {
2777 + $row.addClass('is-copied').attr('aria-label', copiedLabelText);
2778 + $row.find('.merchant-ai-prompt-icon').html(checkIconSvg);
2779 + setTimeout(function () {
2780 + $row.removeClass('is-copied').attr('aria-label', copyHintText);
2781 + $row.find('.merchant-ai-prompt-icon').html(copyIconSvg);
2782 + }, 1600);
2783 + });
2784 + });
2785 + var closeAiPromptsModal = function closeAiPromptsModal() {
2786 + $aiPromptsModal.removeClass('merchant-show');
2787 + $aiPromptsModal.find('.merchant-ai-prompts-modal-badge svg').removeClass('merchant-ai-badge-spin');
2788 + };
2789 + $(document).on('click', '.merchant-ai-prompts-modal-close', function () {
2790 + closeAiPromptsModal();
2791 + });
2792 + $aiPromptsModal.on('click', function (e) {
2793 + if ($(e.target).is($aiPromptsModal)) {
2794 + closeAiPromptsModal();
2795 + }
2796 + });
2797 + $(document).on('keydown', function (e) {
2798 + if (e.key === 'Escape' && $aiPromptsModal.hasClass('merchant-show')) {
2799 + closeAiPromptsModal();
2800 + }
2801 + });
2802 + }
816 2803 });
817 -})(jQuery, window, document);
2804 +
2805 + /**
2806 + * Check if a string is numeric.
2807 + *
2808 + * @param str the string to check
2809 + *
2810 + * @returns {boolean} true if numeric, false otherwise
2811 + */
2812 + function isNumeric(str) {
2813 + if (typeof str != "string") return false; // we only process strings!
2814 + return !isNaN(str) &&
2815 + // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
2816 + !isNaN(parseFloat(str)); // ...and ensure strings of whitespace fail
2817 + }
2818 +
2819 + /**
2820 + * Evaluate conditional fields.
2821 + *
2822 + * @param conditions the array of conditions
2823 + * @param field the field that is being evaluated
2824 + *
2825 + * @returns {boolean}
2826 + */
2827 + function evaluateConditions(conditions) {
2828 + var field = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
2829 + var passed = false;
2830 + if ('relation' in conditions) {
2831 + //loop through terms
2832 + var relation = conditions.relation.toUpperCase();
2833 + // lowercase relation
2834 + if (relation === 'OR') {
2835 + for (var i = 0; i < conditions.terms.length; i++) {
2836 + var term = conditions.terms[i];
2837 + passed = evaluateConditions(term, field);
2838 + if (passed) {
2839 + return true;
2840 + }
2841 + }
2842 + } else if (relation === 'AND') {
2843 + var n = 0;
2844 + for (var _i = 0; _i < conditions.terms.length; _i++) {
2845 + // check if inner terms are passed
2846 + var _term = conditions.terms[_i];
2847 + if (evaluateConditions(_term, field)) {
2848 + n++;
2849 + }
2850 + }
2851 + if (n === conditions.terms.length) {
2852 + passed = true;
2853 + }
2854 + }
2855 + } else {
2856 + var condition = '';
2857 + if ('terms' in conditions) {
2858 + condition = conditions.terms[0];
2859 + } else {
2860 + condition = conditions;
2861 + }
2862 + var $target = $('input[name="merchant[' + condition.field + ']"],select[name="merchant[' + condition.field + ']"]');
2863 + if (!$target.length) {
2864 + // check if inside flexible content
2865 + var flexibleContentParent = field.closest('.layout-body');
2866 + if (flexibleContentParent.length > 0) {
2867 + $target = flexibleContentParent.find('.merchant-field-' + condition.field).find('input, select');
2868 + }
2869 + }
2870 + if (!$target.length) {
2871 + // Maybe the field is a multiple field
2872 + $target = $('input[name="merchant[' + condition.field + '][]"],select[name="merchant[' + condition.field + '][]"]');
2873 + }
2874 + if (!$target.length) {
2875 + // Maybe the field is inside fields group
2876 + $target = $('.merchant-group-fields-container').find('.merchant-field-' + condition.field + ' input[name*="' + condition.field + '"],.merchant-field-' + condition.field + ' select[name*="' + condition.field + '"]');
2877 + }
2878 + var value = $target.val();
2879 + if ($target.attr('type') === 'checkbox') {
2880 + value = $target.is(':checked');
2881 + }
2882 + if ($target.attr('type') === 'radio') {
2883 + value = $target.filter(':checked').val();
2884 + }
2885 +
2886 + // check if the field is multiple checkbox
2887 + if ($target.attr('type') === 'checkbox' && $target.length > 1) {
2888 + value = [];
2889 + $target.each(function () {
2890 + if ($(this).is(':checked')) {
2891 + value.push($(this).val());
2892 + }
2893 + });
2894 + }
2895 +
2896 + // cast value as string if numeric
2897 + if (isNumeric(value)) {
2898 + value = Number(value);
2899 + }
2900 +
2901 + // check if is array condition.value
2902 + if (Array.isArray(condition.value)) {
2903 + condition.value = condition.value.map(function (item) {
2904 + if (isNumeric(item)) {
2905 + return Number(item);
2906 + }
2907 + return item;
2908 + });
2909 + }
2910 + if (condition.operator === '===' && value === condition.value) {
2911 + passed = true;
2912 + } else if (condition.operator === '!==' && value !== condition.value) {
2913 + passed = true;
2914 + } else if (condition.operator === '>' && value > condition.value) {
2915 + passed = true;
2916 + } else if (condition.operator === '<' && value < condition.value) {
2917 + passed = true;
2918 + } else if (condition.operator === '>=' && value >= condition.value) {
2919 + passed = true;
2920 + } else if (condition.operator === '<=' && value <= condition.value) {
2921 + passed = true;
2922 + } else if (condition.operator === 'in' && condition.value.includes(value)) {
2923 + passed = true;
2924 + } else if (condition.operator === '!in' && !condition.value.includes(value)) {
2925 + passed = true;
2926 + } else if (condition.operator === 'contains' && Array.isArray(value) && value.includes(condition.value)) {
2927 + passed = true;
2928 + } else if (condition.operator === '!contains' && Array.isArray(value) && !value.includes(condition.value)) {
2929 + passed = true;
2930 + }
2931 + }
2932 + return passed;
2933 + }
2934 +})(jQuery, window, document);
2935 +
2936 +// Extend jQuery to add getPath func to get accurate dynamic selector to an element.
2937 +jQuery.fn.extend({
2938 + getPath: function getPath() {
2939 + var pathes = [];
2940 + this.each(function (index, element) {
2941 + var path,
2942 + $node = jQuery(element);
2943 + while ($node.length) {
2944 + var realNode = $node.get(0),
2945 + name = realNode.localName;
2946 + if (!name) {
2947 + break;
2948 + }
2949 + name = name.toLowerCase();
2950 + var parent = $node.parent();
2951 + var sameTagSiblings = parent.children(name);
2952 + if (sameTagSiblings.length > 1) {
2953 + var allSiblings = parent.children();
2954 + var index = allSiblings.index(realNode) + 1;
2955 + if (index > 0) {
2956 + name += ':nth-child(' + index + ')';
2957 + }
2958 + }
2959 + path = name + (path ? ' > ' + path : '');
2960 + $node = parent;
2961 + }
2962 + pathes.push(path);
2963 + });
2964 + return pathes.join(',');
2965 + }
2966 +});