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
merchant / assets / js / admin / admin.js

admin.js in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.2, at assets/js/admin/admin.js

2,967 lines 124.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
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); }
10 ;
11 (function ($, window, document, undefined) {
12 'use strict';
13
14 var merchant = merchant || {};
15 var params = new URLSearchParams(window.location.search);
16 var currentModule = params.get('module');
17 $(document).ready(function () {
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;
40 }
41 }
42 GroubField.initFlag();
43 });
44
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();
49 }
50 });
51 $ajaxForm.ajaxForm({
52 beforeSubmit: function beforeSubmit(arr) {
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 });
168 },
169 success: function success() {
170 $ajaxHeader.removeClass('merchant-show');
171 merchant.show_save = false;
172
173 // Module Alert after Ajax Save
174 if (!$('.merchant-module-action').hasClass('merchant-enabled')) {
175 var $moduleAlert = $('.merchant-module-alert');
176 $moduleAlert.addClass('merchant-show');
177 $(document).off('click.merchant-alert-close');
178 $(document).on('click.merchant-alert-close', function (e) {
179 if (!$(e.target).closest('.merchant-module-alert-wrapper').length) {
180 $moduleAlert.removeClass('merchant-show');
181 $(document).off('click.merchant-alert-close');
182 }
183 });
184 }
185 $(document).trigger('save.merchant', [currentModule]);
186 }
187 });
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) {
194 e.preventDefault();
195 var message = $disableModuleTextField.val();
196 if (!message.trim()) {
197 alert('Please provide the required information.');
198 return;
199 }
200 var $button = $(this);
201 $('.merchant-module-question-answer-dropdown').removeClass('merchant-show');
202 $('.merchant-module-question-thank-you-dropdown').addClass('merchant-show');
203 window.wp.ajax.post('merchant_module_feedback', {
204 subject: $disableModuleTextField.attr('data-subject'),
205 message: message,
206 module: $button.closest('.merchant-module-action').find('.merchant-module-page-button-action-activate').data('module'),
207 nonce: window.merchant.nonce
208 });
209 });
210 $('.merchant-module-page-button-action-activate').on('click', function (e) {
211 e.preventDefault();
212 if ($(this).hasClass('merchant-module-deactivated-by-bp')) {
213 return false;
214 }
215 $('.merchant-module-question-list-dropdown').removeClass('merchant-show');
216 $('.merchant-module-question-answer-dropdown').removeClass('merchant-show');
217 $('.merchant-module-question-answer-form').removeClass('merchant-show');
218 $('.merchant-module-question-answer-title').removeClass('merchant-show');
219 $('.merchant-module-question-thank-you-dropdown').removeClass('merchant-show');
220 $('.merchant-module-question-answer-textarea').val('');
221 window.wp.ajax.post('merchant_module_activate', {
222 module: $(this).data('module'),
223 nonce: window.merchant.nonce
224 }).done(function () {
225 $('body').removeClass('merchant-module-disabled').addClass('merchant-module-enabled');
226 $('.merchant-module-action').addClass('merchant-enabled');
227 });
228 });
229 $('.merchant-module-page-button-action-deactivate').on('click', function (e) {
230 e.preventDefault();
231 window.wp.ajax.post('merchant_module_deactivate', {
232 module: $(this).data('module'),
233 nonce: window.merchant.nonce
234 }).done(function () {
235 $('body').removeClass('merchant-module-enabled').addClass('merchant-module-disabled');
236 $('.merchant-module-action').removeClass('merchant-enabled');
237 $('.merchant-module-question-list-dropdown').addClass('merchant-show');
238 });
239 });
240 $('.merchant-module-question-list-dropdown li').on('click', function (e) {
241 $disableModuleSubmitBtn.prop('disabled', $disableModuleTextField.val().trim() === '');
242 var $question = $(this);
243 var target = $question.data('answer-target');
244 var $answer = $('[data-answer-title="' + target + '"]');
245 if ($answer.length) {
246 $answer.addClass('merchant-show').siblings().removeClass('merchant-show');
247 $('.merchant-module-question-answer-dropdown').addClass('merchant-show');
248 $('.merchant-module-question-answer-textarea').attr('data-subject', $question.text().trim());
249 } else {
250 $('.merchant-module-question-thank-you-dropdown').addClass('merchant-show');
251 $('.merchant-module-question-answer-dropdown').removeClass('merchant-show');
252 }
253 $('.merchant-module-question-answer-textarea').val('');
254 $('.merchant-module-question-list-dropdown').removeClass('merchant-show');
255 });
256 $('.merchant-module-dropdown-close').on('click', function (e) {
257 e.preventDefault();
258 $(this).closest('.merchant-module-dropdown').removeClass('merchant-show');
259 });
260 $('.merchant-module-page-button-deactivate').on('click', function (e) {
261 e.preventDefault();
262 var $button = $(this);
263 var $dropdown = $('.merchant-module-deactivate-dropdown');
264 $dropdown.toggleClass('merchant-show');
265 $(document).off('click.merchant-close');
266 $(document).on('click.merchant-close', function (e) {
267 if (!$(e.target).closest('.merchant-module-deactivate').length) {
268 $dropdown.removeClass('merchant-show');
269 $(document).off('click.merchant-close');
270 }
271 });
272 });
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 () {
281 var $range = $(this);
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 }
306 });
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');
319 });
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);
439 }
440 },
441 error: function error(xhr, status, _error2) {
442 self.displayError(_error2);
443 }
444 });
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 }));
550 }
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]);
556 });
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();
566
567 // Sortable.
568 var SortableField = {
569 init: function init(field) {
570 this.events();
571 },
572 events: function events() {
573 var self = this;
574 $('.merchant-sortable').each(function () {
575 var field = $(this),
576 input = field.find('.merchant-sortable-input');
577
578 // Init sortable.
579 $(field.find('ul.merchant-sortable-list').first()).sortable({
580 // Update value when we stop sorting.
581 update: function update() {
582 input.val(self.sortableGetNewVal(field)).trigger('change.merchant');
583 }
584 }).disableSelection().find('li').each(function () {
585 // Enable/disable options when we click on the eye of Thundera.
586 $(this).find('i.visibility').click(function () {
587 $(this).toggleClass('dashicons-visibility-faint').parents('li:eq(0)').toggleClass('invisible');
588 });
589 }).click(function () {
590 // Update value when click in the eye.
591 if ($(event.target).hasClass('dashicons-visibility')) {
592 input.val(self.sortableGetNewVal(field)).trigger('change.merchant');
593 }
594 });
595 });
596 },
597 sortableGetNewVal: function sortableGetNewVal(field) {
598 var items = $(field.find('li'));
599 var newVal = [];
600 _.each(items, function (item) {
601 if (!$(item).hasClass('invisible')) {
602 newVal.push($(item).data('value'));
603 }
604 });
605 return JSON.stringify(newVal);
606 }
607 };
608
609 // Initialize Sortable.
610 SortableField.init();
611 flexibleToggleField.init();
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
620 // Sortable Repeater.
621 var SortableRepeaterField = {
622 init: function init(field) {
623 var self = this;
624
625 // Update the values for all our input fields and initialise the sortable repeater.
626 $('.merchant-sortable-repeater-control').each(function () {
627 // If there is an existing customizer value, populate our rows
628 var defaultValuesArray = JSON.parse($(this).find('.merchant-sortable-repeater-input').val());
629 var numRepeaterItems = defaultValuesArray.length;
630 if (numRepeaterItems > 0) {
631 // Add the first item to our existing input field
632 $(this).find('.repeater-input').val(defaultValuesArray[0]);
633
634 // Create a new row for each new value
635 if (numRepeaterItems > 1) {
636 // var i;
637 for (var i = 1; i < numRepeaterItems; ++i) {
638 self.appendRow($(this), defaultValuesArray[i]);
639 }
640 }
641 }
642
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 // }
652 });
653
654 // Events.
655 this.events();
656 },
657 events: function events() {
658 var self = this;
659
660 // Remove item starting from its parent element
661 $(document).on('click', '.merchant-sortable-repeater.sortable .customize-control-sortable-repeater-delete', function (event) {
662 event.preventDefault();
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');
669 });
670
671 // Add new item
672 $(document).on('click', '.customize-control-sortable-repeater-add', function (event) {
673 event.preventDefault();
674 self.appendRow($(this).parent());
675 self.getAllInputs($(this).parent());
676 });
677
678 // Refresh our hidden field if any fields change
679 $(document).on('change', '.merchant-sortable-repeater.sortable', function () {
680 self.getAllInputs($(this).parent());
681 });
682 $(document).on('focusout', '.merchant-sortable-repeater.sortable .repeater-input', function () {
683 self.getAllInputs($(this).parent());
684 });
685 },
686 /**
687 * Append a new row to our list of elements.
688 *
689 */
690 appendRow: function appendRow($element) {
691 var defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
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>';
693 $element.find('.sortable').append(newRow);
694 var $newItem = $element.find('.sortable').find('.repeater:last');
695 $newItem.slideDown('slow', function () {
696 $(this).find('input').focus();
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')]);
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 },
712 /**
713 * Get the values from the repeater input fields and add to our hidden field.
714 *
715 */
716 getAllInputs: function getAllInputs($element) {
717 var inputValues = $element.find('.repeater-input').map(function () {
718 return $(this).val();
719 }).toArray();
720
721 // Keep one empty item if all deleted.
722 if (!inputValues.length) {
723 inputValues.push('');
724 }
725
726 // Add all the values from our repeater fields to the hidden field (which is the one that actually gets saved)
727 $element.find('.merchant-sortable-repeater-input').val(JSON.stringify(inputValues));
728 // Important! Make sure to trigger change event so Customizer knows it has to save the field
729 $element.find('.merchant-sortable-repeater-input').trigger('change');
730 $element.find('.merchant-sortable-repeater-input').trigger('sortable.repeater.change');
731 }
732 };
733
734 // Initialize Sortable Repeater.
735 SortableRepeaterField.init();
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
1319 // Flexible Content.
1320 var FlexibleContentField = {
1321 init: function init(field) {
1322 var self = this;
1323
1324 // Update the values for all our input fields and initialise the sortable repeater.
1325 $('.merchant-flexible-content-control').each(function () {
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);
1447 }
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();
1520 });
1521
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 });
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 },
1583 events: function events() {
1584 var self = this;
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) {
1600 event.preventDefault();
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 }
1607 $(this).parent().find('.customize-control-flexible-content-add-list').toggleClass('active');
1608 });
1609
1610 // Add new item
1611 $(document).on('click', '.customize-control-flexible-content-add', function (event) {
1612 event.preventDefault();
1613 event.stopImmediatePropagation();
1614 var $field = $('.merchant-flexible-content-control[data-id=' + $(this).data('id') + ']');
1615 var $layouts = $field.find('.layouts');
1616 var $selected = $(this).data('layout');
1617 var $layout = $layouts.find('.layout[data-type=' + $selected + ']').clone(true);
1618 var $content = $field.find('.merchant-flexible-content');
1619 var $items = $content.find('.layout');
1620 var uuid = self.generateUUID();
1621 $layout.find('input, select, textarea').each(function () {
1622 if ($(this).data('name')) {
1623 $(this).attr('name', $(this).data('name').replace('0', $items.length));
1624 }
1625 if ($(this).is(':checkbox, :radio') && $(this).attr('checked')) {
1626 $(this).prop('checked', true);
1627 }
1628 });
1629 $layout.attr('data-layout-id', uuid);
1630 $layout.find('.layout-count').text($items.length + 1);
1631 $layout.find('.flexible-id').val(uuid);
1632 $content.append($layout);
1633 $content.removeClass('empty');
1634 $(this).parent().removeClass('active');
1635 if ($layout.find('.merchant-module-page-setting-field-upload').length) {
1636 initUploadField($layout.find('.merchant-module-page-setting-field-upload'));
1637 }
1638 if ($layout.find('.merchant-module-page-setting-field-select_ajax').length) {
1639 initSelectAjax($layout.find('.merchant-module-page-setting-field-select_ajax'));
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');
1655 });
1656
1657 // Duplicate item
1658 $(document).on('click', '.customize-control-flexible-content-duplicate', function (event) {
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();
1734 var $item = $(this).closest('.layout');
1735 var $content = $item.parent();
1736 $item.remove();
1737 if ($content.find('.layout').length === 0) {
1738 $content.addClass('empty');
1739 }
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');
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 });
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('�
2027 '.repeat(parseInt(review.rating) || 0)));
2028 $reviewEl.append($('<span>').addClass('review-product').text(review.product_name));
2029 $reviewEl.append($('<span>').addClass('product-review-delete').html('&times;'));
2030 $reviewEl.append($('<span>').addClass('product-review-move').html('⋮⋮'));
2031 $selectedList.append($reviewEl);
2032 });
2033 $container.find('.merchant-selected-reviews').val(ids.join(','));
2034 });
2035
2036 // Handle fields_group and compound (e.g. hook_select) nested values.
2037 // Recursively walks the value tree to find and set inputs
2038 // at any nesting depth (e.g. fields_group → hook_select → hook_name).
2039 var _setNestedValues = function setNestedValues(obj, namePrefix) {
2040 $.each(obj, function (key, val) {
2041 if (_typeof(val) === 'object' && val !== null && !Array.isArray(val)) {
2042 // Recurse into nested objects.
2043 _setNestedValues(val, namePrefix + '[' + key + ']');
2044 } else if (Array.isArray(val)) {
2045 // checkbox_multiple inside a nested object: check boxes whose value is in the array.
2046 $body.find('[name="' + namePrefix + '[' + key + '][]"]').each(function () {
2047 if ($(this).is(':checkbox')) {
2048 $(this).prop('checked', val.indexOf($(this).val()) !== -1);
2049 }
2050 });
2051 } else {
2052 var $field = $body.find('[name="' + namePrefix + '[' + key + ']"]');
2053 if (!$field.length) {
2054 return;
2055 }
2056 if ($field.is(':checkbox') && !$field.is(':radio')) {
2057 $field.prop('checked', !!parseInt(val, 10));
2058 } else if ($field.is(':radio')) {
2059 $field.prop('checked', $field.val() === String(val));
2060 } else {
2061 $field.val(val);
2062 }
2063 }
2064 });
2065 };
2066 $.each(values, function (key, val) {
2067 if (_typeof(val) === 'object' && val !== null && !Array.isArray(val)) {
2068 _setNestedValues(val, 'merchant[' + fieldId + '][' + rowIndex + '][' + key + ']');
2069 }
2070 });
2071 },
2072 refreshNumbers: function refreshNumbers($content) {
2073 $content.find('.layout').each(function (index) {
2074 var $count = $(this).find('.layout-count').text();
2075 var $inputIndex = parseInt($count) - 1;
2076 $(this).find('.layout-count').text(index + 1);
2077 $(this).find('input, select, textarea').each(function () {
2078 if ($(this).attr('name')) {
2079 $(this).attr('name', $(this).attr('name').replace('[' + $inputIndex + ']', '[*refreshed*' + index + ']'));
2080 }
2081 });
2082 });
2083 $content.find('.layout').each(function (index) {
2084 $(this).find('input, select, textarea').each(function () {
2085 // We've added *refreshed* to the attribute name in the prior loop as refreshing the numbers in the attribute can cause
2086 // checked boxes to be unchecked due to similar attribute names during the change while sorting, within this loop we remove them
2087 var nameAttr = $(this).attr('name');
2088 if (nameAttr) {
2089 // Check if name attribute exists
2090 $(this).attr('name', nameAttr.replace('*refreshed*', ''));
2091 }
2092 });
2093 });
2094 $content.parent().find('input').trigger('change.merchant');
2095 }
2096 };
2097
2098 // Initialize Flexible Content.
2099 FlexibleContentField.init();
2100 GroubField.init();
2101 ReviewsSelector.init();
2102
2103 // Products selector.
2104 // Handle keyup event for the search input
2105 var debounceTimer;
2106 $(document).on('keyup', '.merchant-module-page-setting-field-products_selector .merchant-search-field', function () {
2107 clearTimeout(debounceTimer);
2108 var categories = [];
2109 var $excluded = $(this).closest('[data-id="excluded_products"]');
2110 if ($excluded.length) {
2111 var $layout = $(this).closest('.layout');
2112 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();
2113 if (rules === 'categories' || rules === 'by_category') {
2114 categories = $layout.find('.merchant-field-category_slugs select').val() || $layout.find('.merchant-field-product_cats select').val();
2115 }
2116 }
2117 var parent = $(this).closest('.merchant-products-search-container');
2118 if ($(this).val() !== '') {
2119 parent.find('.merchant-searching').addClass('active');
2120 var data = {
2121 action: 'merchant_admin_products_search',
2122 nonce: merchant_admin_options.ajaxnonce,
2123 keyword: $(this).val(),
2124 product_types: $(this).data('allowed-types'),
2125 ids: parent.find('.merchant-selected-products').val(),
2126 categories: categories
2127 };
2128 debounceTimer = setTimeout(function () {
2129 $.post(merchant_admin_options.ajaxurl, data, function (response) {
2130 var results = parent.find('.merchant-selections-products-preview');
2131 results.show();
2132 results.html(response);
2133 parent.find('.merchant-searching').removeClass('active');
2134 });
2135 }, 250);
2136 } else {
2137 parent.find('.merchant-selections-products-preview').html('').hide();
2138 }
2139 });
2140
2141 // Products selector: dismiss search results on click outside or Escape.
2142 $(document).on('click touch', function (e) {
2143 if (!$(e.target).closest('.merchant-products-search-container').length) {
2144 $('.merchant-selections-products-preview').html('').hide();
2145 $('.merchant-search-field').val('');
2146 }
2147 });
2148 $(document).on('keydown', '.merchant-search-field', function (e) {
2149 if (e.key === 'Escape') {
2150 var parent = $(this).closest('.merchant-products-search-container');
2151 parent.find('.merchant-selections-products-preview').html('').hide();
2152 $(this).val('').blur();
2153 }
2154 });
2155
2156 // Products selector.
2157 // Handle click/touch event for the search results
2158 $(document).on('click touch', '.merchant-module-page-setting-field-products_selector .merchant-selections-products-preview li', function () {
2159 var parent = $(this).closest('.merchant-products-search-container'),
2160 valueField = parent.find('.merchant-selected-products'),
2161 oldValue = valueField.val(),
2162 multiple = parent.data('multiple') === 'multiple';
2163 if (parent.find('.merchant-selected-products-preview ul li').length > 0 && !multiple) {
2164 // replace the first item
2165 parent.find('.merchant-selected-products-preview ul li').remove();
2166 valueField.val('').change();
2167 }
2168 $(this).children('.remove').attr('aria-label', 'Remove').html('×');
2169 parent.find('.merchant-selected-products-preview ul').append($(this));
2170 parent.find('.merchant-selections-products-preview').html('').hide();
2171 parent.find('.merchant-search-field').val('').change();
2172 if (oldValue === '') {
2173 valueField.val($(this).data('id')).change();
2174 } else {
2175 if (!multiple) {
2176 valueField.val($(this).data('id')).change();
2177 } else {
2178 var newValue = oldValue.split(',');
2179 newValue.push($(this).data('id'));
2180 valueField.val(newValue.join(',')).change();
2181 }
2182 }
2183 });
2184
2185 // Products selector.
2186 // Handle click/touch event for the remove button.
2187 $(document).on('click touch', '.merchant-selected-products-preview .remove', function () {
2188 var removeButton = $(this);
2189 var parent = removeButton.closest('.merchant-products-search-container'),
2190 valueField = parent.find('.merchant-selected-products'),
2191 id = removeButton.parent().data('id');
2192 removeButton.parent().remove();
2193 var currentValue = valueField.val().split(',');
2194 if (currentValue.length > 0) {
2195 for (var key in currentValue) {
2196 if (parseInt(currentValue[key]) === parseInt(id)) {
2197 currentValue.splice(key, 1);
2198 }
2199 }
2200 }
2201 valueField.val(currentValue.join(',')).change();
2202 valueField.trigger('change.merchant');
2203 });
2204 $(document).on('merchant-admin-check-fields merchant-flexible-content-added', function () {
2205 $('.merchant-module-page-setting-field').each(function () {
2206 var $field = $(this);
2207 if ($field.data('condition') && $field.data('condition').length) {
2208 var condition = $field.data('condition');
2209 var $target = $(this).closest('.layout-body').find('input[name*="' + condition[0] + '"],select[name*="' + condition[0] + '"]');
2210 if (!$target.length) {
2211 $target = $('input[name="merchant[' + condition[0] + ']"],select[name="merchant[' + condition[0] + ']"]');
2212 }
2213 if ($target.length) {
2214 var passed = false;
2215 switch (condition[1]) {
2216 case '==':
2217 if ($target.attr('type') === 'radio' || $target.attr('type') === 'checkbox') {
2218 var checked = $target.parent().find('input:checked');
2219 if (checked.length && checked.val() === condition[2]) {
2220 passed = true;
2221 }
2222 }
2223 if ($target.is('select') && $target.val() == condition[2]) {
2224 passed = true;
2225 }
2226 break;
2227 case 'any':
2228 if ($target.attr('type') === 'radio' || $target.attr('type') === 'checkbox') {
2229 var _checked = $target.parent().find('input:checked');
2230 if (_checked.length && condition[2].split('|').includes(_checked.val())) {
2231 passed = true;
2232 }
2233 }
2234 if ($target.is('select') && condition[2].split('|').includes($target.val())) {
2235 passed = true;
2236 }
2237 break;
2238 }
2239 if (passed) {
2240 $field.removeClass('merchant-hide').addClass('merchant-show');
2241 } else {
2242 $field.removeClass('merchant-show').addClass('merchant-hide');
2243 }
2244 }
2245 }
2246 });
2247 }).trigger('merchant.change');
2248 $(document).on('merchant-admin-check-fields merchant-flexible-content-added change keyup', function () {
2249 $(document).find('.merchant-module-page-setting-field').each(function () {
2250 var $field = $(this);
2251 if ($field.data('conditions')) {
2252 var conditions = $field.data('conditions'),
2253 passed = evaluateConditions(conditions, $field);
2254 if (passed) {
2255 $field.removeClass('merchant-hide').addClass('merchant-show');
2256 } else {
2257 $field.removeClass('merchant-show').addClass('merchant-hide');
2258 }
2259 }
2260 });
2261 }).trigger('merchant.change');
2262 $(document).on('change', '.merchant-module-page-setting-field', function () {
2263 $(document).trigger('merchant-admin-check-fields');
2264 }).trigger('merchant.change');
2265 $(document).trigger('merchant-admin-check-fields');
2266 $(document).on('merchant-admin-check-color-fields merchant-flexible-content-added', function () {
2267 $('.merchant-color').each(function () {
2268 var $color = $(this);
2269 var $picker = $color.find('.merchant-color-picker');
2270 var $input = $color.find('.merchant-color-input');
2271 var inited = false;
2272 var pickr;
2273 $picker.off('click').on('click', function (e) {
2274 e.preventDefault();
2275 e.stopPropagation();
2276 var $bodyHTML = $('body,html');
2277 $bodyHTML.addClass('merchant-height-auto');
2278 if (!inited) {
2279 pickr = new Pickr({
2280 el: $picker.get(0),
2281 container: 'body',
2282 theme: 'merchant',
2283 appClass: 'merchant-pcr-app',
2284 default: $input.val() || $picker.data('default-color') || '#212121',
2285 swatches: ['#000000', '#F44336', '#E91E63', '#673AB7', '#03A9F4', '#8BC34A', '#FFEB3B', '#FFC107', '#FFFFFF'],
2286 sliders: 'h',
2287 useAsButton: true,
2288 components: {
2289 hue: true,
2290 preview: true,
2291 opacity: true,
2292 interaction: {
2293 input: true,
2294 clear: true
2295 }
2296 },
2297 i18n: {
2298 'btn:clear': 'Default'
2299 }
2300 });
2301 pickr.on('change', function (color) {
2302 var colorCode;
2303 if (color.a === 1) {
2304 pickr.setColorRepresentation('HEX');
2305 colorCode = color.toHEXA().toString(0);
2306 } else {
2307 pickr.setColorRepresentation('RGBA');
2308 colorCode = color.toRGBA().toString(0);
2309 }
2310 $picker.css({
2311 'background-color': colorCode
2312 });
2313 if ($input.val() !== colorCode) {
2314 $input.val(colorCode).trigger('change.merchant');
2315 }
2316 $(document).trigger('merchant-color-picker-updated', [colorCode, $input]);
2317 });
2318 pickr.on('clear', function () {
2319 var defaultColor = $picker.data('default-color');
2320 if (defaultColor) {
2321 pickr.setColor(defaultColor);
2322 } else {
2323 $picker.css({
2324 'background-color': 'white'
2325 });
2326 $input.val('');
2327 }
2328 });
2329 pickr.on('hide', function () {
2330 $bodyHTML.removeClass('merchant-height-auto');
2331 });
2332 $picker.data('pickr', pickr);
2333 setTimeout(function () {
2334 pickr.show();
2335 }, 200);
2336 inited = true;
2337 } else {
2338 pickr.setColor($input.val());
2339 }
2340 });
2341 $input.on('change keyup', function () {
2342 var colorCode = $(this).val();
2343 $picker.css({
2344 'background-color': colorCode
2345 });
2346 });
2347 });
2348 });
2349 $(document).trigger('merchant-admin-check-color-fields');
2350
2351 // Create Page Control.
2352 var CreatePageControl = {
2353 init: function init() {
2354 this.events();
2355 },
2356 events: function events() {
2357 var self = this;
2358 $(document).on('click', '.merchant-create-page-control-button', function (e) {
2359 e.preventDefault();
2360 var $this = $(this),
2361 $create_message = $this.parent().find('.merchant-create-page-control-create-message'),
2362 $success_message = $this.parent().find('.merchant-create-page-control-success-message'),
2363 initial_text = $this.text(),
2364 creating_text = $this.data('creating-text'),
2365 created_text = $this.data('created-text'),
2366 page_title = $this.data('page-title'),
2367 page_meta_key = $this.data('page-meta-key'),
2368 page_meta_value = $this.data('page-meta-value'),
2369 option_name = $this.data('option-name'),
2370 nonce = $this.data('nonce');
2371 if (!page_title) {
2372 return false;
2373 }
2374 $(this).text(creating_text);
2375 $(this).attr('disabled', true);
2376 $.ajax({
2377 type: 'post',
2378 url: ajaxurl,
2379 data: {
2380 action: 'merchant_create_page_control',
2381 page_title: page_title,
2382 page_meta_key: page_meta_key,
2383 page_meta_value: page_meta_value,
2384 option_name: option_name,
2385 nonce: nonce
2386 },
2387 success: function success(response) {
2388 self.ajaxResponseHandler(response, $this, $success_message, $create_message);
2389 }
2390 });
2391 });
2392 },
2393 ajaxResponseHandler: function ajaxResponseHandler(response, $this, $success_message, $create_message) {
2394 if ('success' === response.status) {
2395 var $editLink = $success_message.find('a').first(),
2396 href = $editLink.attr('href');
2397 if (href) {
2398 $editLink.attr('href', href.replace('?post=&', '?post=' + response.page_id + '&'));
2399 }
2400 $success_message.css('display', 'block');
2401 $create_message.remove();
2402 $this.remove();
2403 }
2404 }
2405 };
2406
2407 // Initialize Create Page Control.
2408 CreatePageControl.init();
2409 $('.merchant-module-page-setting-field-gallery').each(function () {
2410 var $this = $(this);
2411 var $button = $this.find('.merchant-gallery-button');
2412 var $input = $this.find('.merchant-gallery-input');
2413 var $images = $this.find('.merchant-gallery-images');
2414 var $remove = $this.find('.merchant-gallery-remove');
2415 var wpMediaFrame;
2416 var sortable = $images.sortable({
2417 helper: 'original',
2418 update: function update(event, ui) {
2419 var selectedIds = [];
2420 $images.find('.merchant-gallery-image').each(function () {
2421 selectedIds.push($(this).data('item-id'));
2422 });
2423 $input.val(selectedIds.join(',')).trigger('change');
2424 }
2425 });
2426 $remove.on('click', function (e) {
2427 e.preventDefault();
2428 $(this).parent().remove();
2429 var selectedIds = [];
2430 $images.find('.merchant-gallery-image').each(function () {
2431 selectedIds.push($(this).data('item-id'));
2432 });
2433 $input.val(selectedIds.join(',')).trigger('change');
2434 });
2435 $button.on('click', function (e) {
2436 var $btn = $(this);
2437 var ids = $input.val();
2438 var mode = ids ? 'edit' : 'add';
2439 e.preventDefault();
2440 if (typeof window.wp === 'undefined' || !window.wp.media || !window.wp.media.gallery) {
2441 return;
2442 }
2443 if (mode === 'add') {
2444 wpMediaFrame = window.wp.media({
2445 library: {
2446 type: 'image'
2447 },
2448 frame: 'post',
2449 state: 'gallery',
2450 multiple: true
2451 });
2452 wpMediaFrame.open();
2453 } else {
2454 wpMediaFrame = window.wp.media.gallery.edit('[gallery ids="' + ids + '"]');
2455 }
2456 wpMediaFrame.on('update', function (selection) {
2457 $images.empty();
2458 var selectedIds = selection.models.map(function (attachment) {
2459 var item = attachment.toJSON();
2460 var thumb = item.sizes && item.sizes.thumbnail && item.sizes.thumbnail.url ? item.sizes.thumbnail.url : item.url;
2461 $images.append('<div class="merchant-gallery-image" data-item-id="' + item.id + '"><i class="merchant-gallery-remove dashicons dashicons-no-alt"></i><img src="' + thumb + '" /></div>');
2462 return item.id;
2463 });
2464 $input.val(selectedIds.join(',')).trigger('change');
2465 $this.find('.merchant-gallery-remove').on('click', function (e) {
2466 e.preventDefault();
2467 $(this).parent().remove();
2468 var selectedIds = [];
2469 $images.find('.merchant-gallery-image').each(function () {
2470 selectedIds.push($(this).data('item-id'));
2471 });
2472 $input.val(selectedIds.join(',')).trigger('change');
2473 });
2474 });
2475 });
2476 });
2477 var initUploadField = function initUploadField(element) {
2478 var $this = element;
2479 var $button = $this.find('.merchant-upload-button');
2480 var $input = $this.find('.merchant-upload-input');
2481 var $wrapper = $this.find('.merchant-upload-wrapper');
2482 var $remove = $this.find('.merchant-upload-remove');
2483 var wpMediaFrame;
2484 $remove.on('click', function (e) {
2485 e.preventDefault();
2486 $(this).parent().remove();
2487 $input.val('').trigger('change');
2488 });
2489 $button.on('click', function (e) {
2490 e.preventDefault();
2491 if (typeof window.wp === 'undefined' || !window.wp.media) {
2492 return;
2493 }
2494 if (!wpMediaFrame) {
2495 wpMediaFrame = window.wp.media({
2496 library: {
2497 type: 'image'
2498 }
2499 });
2500 }
2501 wpMediaFrame.open();
2502 wpMediaFrame.on('select', function () {
2503 $wrapper.empty();
2504 var item = wpMediaFrame.state().get('selection').first().attributes;
2505 var thumb = item.sizes && item.sizes.thumbnail && item.sizes.thumbnail.url ? item.sizes.thumbnail.url : item.url;
2506 var sizes = item.sizes ? JSON.stringify(item.sizes) : '';
2507 $wrapper.append('<div class="merchant-upload-image" data-sizes=\'' + sizes + '\'><i class="merchant-upload-remove dashicons dashicons-no-alt"></i><img src="' + thumb + '" /></div>');
2508 $input.val(item.id).trigger('change');
2509 $this.find('.merchant-upload-button-drag-drop').hide();
2510 $this.find('.merchant-upload-remove').on('click', function (e) {
2511 e.preventDefault();
2512 $(this).parent().remove();
2513 $input.val('').trigger('change');
2514 $this.find('.merchant-upload-button-drag-drop').show();
2515 });
2516 });
2517 });
2518 };
2519 $('.merchant-module-page-setting-field-upload:not(.template)').each(function () {
2520 initUploadField($(this));
2521 });
2522
2523 // Drag & Drag
2524 var events = ['dragenter', 'dragover', 'dragleave', 'drop'];
2525 jQuery.each(events, function (index, eventName) {
2526 $(document).on(eventName, '.merchant-upload-button-drag-drop', function (e) {
2527 e.preventDefault();
2528 e.stopPropagation();
2529 });
2530 });
2531 $(document).on('dragenter', '.merchant-upload-button-drag-drop', function (e) {
2532 $(this).closest('.merchant-module-page-setting-field-upload').find('.merchant-upload-button').click();
2533 });
2534
2535 // Callers pass a collection, and reading the source off a collection would give
2536 // every select the first field's source, so each field is set up on its own.
2537 var initSelectAjax = function initSelectAjax($selectAjaxFields) {
2538 $selectAjaxFields.each(function () {
2539 initSelectAjaxField($(this));
2540 });
2541 };
2542 var initSelectAjaxField = function initSelectAjaxField($selectAjax) {
2543 var $select = $selectAjax.find('select');
2544 var $source = $select.data('source');
2545 var $config = window.merchant_admin_options;
2546 var $object = {
2547 width: '100%',
2548 templateSelection: function templateSelection(category) {
2549 return category.text.replace(/&nbsp;-*\s*/g, '').trim();
2550 }
2551 };
2552 if ($source === 'post' || $source === 'product' || $source === 'user') {
2553 $object.minimumInputLength = 1;
2554 $object.ajax = {
2555 url: $config.ajaxurl,
2556 dataType: 'json',
2557 delay: 250,
2558 cache: true,
2559 data: function data(params) {
2560 return {
2561 action: 'merchant_admin_options_select_ajax',
2562 nonce: $config.ajaxnonce,
2563 term: params.term,
2564 source: $source
2565 };
2566 },
2567 processResults: function processResults(response, params) {
2568 if (response.success) {
2569 return {
2570 results: response.data
2571 };
2572 }
2573 return {};
2574 }
2575 };
2576 }
2577 $select.select2($object);
2578 $selectAjax.find('.select2-selection--multiple').append('<span class="merchant-select2-clear"></span>');
2579 };
2580 $('.merchant-module-page-setting-field-select_ajax:not(.template)').each(function () {
2581 initSelectAjax($(this));
2582 });
2583 $('.merchant-module-page-settings-responsive').each(function () {
2584 var $this = $(this);
2585 var $button = $this.find('.merchant-module-page-settings-devices button');
2586 var $container = $this.find('.merchant-module-page-settings-device-container');
2587 $button.on('click', function (e) {
2588 e.preventDefault();
2589 var $device = $(this).data('device');
2590 $button.removeClass('active');
2591 $container.removeClass('active');
2592 $(this).addClass('active');
2593 $container.each(function () {
2594 if ($(this).data('device') === $device) {
2595 $(this).addClass('active');
2596 }
2597 });
2598 });
2599 });
2600 $('.merchant-animated-buttons').each(function () {
2601 var $button = $(this).find('label');
2602 var $demo = $('.merchant-animation-demo');
2603 var animation;
2604 var animationHover;
2605 $button.on('click', function () {
2606 $demo.removeClass('merchant-animation-' + animation);
2607 $demo.removeClass('merchant-animation-' + animationHover);
2608 animation = $(this).find('input').attr('value');
2609 setTimeout(function () {
2610 $demo.addClass('merchant-animation-' + animation);
2611 }, 100);
2612 setTimeout(function () {
2613 $demo.removeClass('merchant-animation-' + animation);
2614 }, 1000);
2615 });
2616 $button.mouseover(function () {
2617 $demo.removeClass('merchant-animation-' + animation);
2618 animationHover = $(this).find('input').attr('value');
2619 $demo.addClass('merchant-animation-' + animationHover);
2620 }).mouseout(function () {
2621 $demo.removeClass('merchant-animation-' + animationHover);
2622 });
2623 });
2624
2625 // Notifications Sidebar
2626 var $notificationsSidebar = $('.merchant-notifications-sidebar');
2627 if ($notificationsSidebar.length) {
2628 var $notifications = $('.merchant-notifications');
2629 $notifications.on('click', function (e) {
2630 e.preventDefault();
2631 var $notification = $(this);
2632 var latestNotificationDate = $notificationsSidebar.find('.merchant-notification:first-child .merchant-notification-date').data('raw-date');
2633 $notificationsSidebar.toggleClass('opened');
2634 if (!$notification.hasClass('read')) {
2635 $.post(window.merchant.ajax_url, {
2636 action: 'merchant_notifications_read',
2637 nonce: window.merchant.nonce,
2638 latest_notification_date: latestNotificationDate
2639 }, function (response) {
2640 if (response.success) {
2641 setTimeout(function () {
2642 $notification.addClass('read');
2643 }, 2000);
2644 }
2645 });
2646 }
2647 });
2648
2649 // Hide fixed changelog items
2650 var merchantNotificationsContent = $('.merchant-notification-content');
2651 merchantNotificationsContent.each(function () {
2652 var notificationContentItem = $(this);
2653 // search for all spans that contains class "changelog-fixed" and then go to the parent li of that span and add class hidden
2654 var fixedItems = notificationContentItem.find('span.changelog-fixed');
2655 fixedItems.each(function () {
2656 var fixedItem = $(this);
2657 fixedItem.closest('li').remove();
2658 });
2659 // now check if all li inside this notificationContentItem are hidden, so we need to look for the parent .merchant-notification and remove it
2660 var allItems = notificationContentItem.find('li');
2661 var hiddenItems = notificationContentItem.find('li.hidden');
2662 if (allItems.length === hiddenItems.length) {
2663 notificationContentItem.closest('.merchant-notification').remove();
2664 }
2665 });
2666 $(window).on('scroll', function () {
2667 if (window.pageYOffset > 60) {
2668 $notificationsSidebar.addClass('closing');
2669 setTimeout(function () {
2670 $notificationsSidebar.removeClass('opened');
2671 $notificationsSidebar.removeClass('closing');
2672 }, 300);
2673 }
2674 });
2675
2676 // Close Sidebar
2677 $('.merchant-notifications-sidebar-close').on('click', function (e) {
2678 e.preventDefault();
2679 $notificationsSidebar.addClass('closing');
2680 setTimeout(function () {
2681 $notificationsSidebar.removeClass('opened');
2682 $notificationsSidebar.removeClass('closing');
2683 }, 300);
2684 });
2685 }
2686
2687 // Tabs Navigation.
2688 var tabs = $('.merchant-tabs-nav');
2689 if (tabs.length) {
2690 tabs.each(function () {
2691 var tabWrapperId = $(this).data('tab-wrapper-id');
2692 $(this).find('.merchant-tabs-nav-link').on('click', function (e) {
2693 e.preventDefault();
2694 var tabsNavLink = $(this).closest('.merchant-tabs-nav').find('.merchant-tabs-nav-link'),
2695 to = $(this).data('tab-to');
2696
2697 // Tab Nav Item
2698 tabsNavLink.each(function () {
2699 $(this).closest('.merchant-tabs-nav-item').removeClass('active');
2700 });
2701 $(this).closest('.merchant-tabs-nav-item').addClass('active');
2702
2703 // Tab Content
2704 var tabContentWrapper = $('.merchant-tab-content-wrapper[data-tab-wrapper-id="' + tabWrapperId + '"]');
2705 tabContentWrapper.find('> .merchant-tab-content').removeClass('active');
2706 tabContentWrapper.find('> .merchant-tab-content[data-tab-content-id="' + to + '"]').addClass('active');
2707 });
2708 });
2709 }
2710
2711 // Module Alert
2712 var $moduleAlert = $('.merchant-module-alert');
2713 if ($moduleAlert.length) {
2714 $moduleAlert.find('.merchant-module-alert-close').on('click', function (e) {
2715 e.preventDefault();
2716 $moduleAlert.removeClass('merchant-show');
2717 $(document).off('click.merchant-alert-close');
2718 });
2719 }
2720
2721 // AI Prompt Examples Modal
2722 var $aiPromptsModal = $('#merchant-ai-prompts-modal');
2723 if ($aiPromptsModal.length) {
2724 var $aiPromptsList = $aiPromptsModal.find('.merchant-ai-prompts-modal-list');
2725 var copyHintText = window.merchant.ai_prompt_copy_hint || 'Click to copy';
2726 var copiedLabelText = window.merchant.ai_prompt_copied_label || 'Copied!';
2727 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>';
2728 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>';
2729 var copyTextToClipboard = function copyTextToClipboard(text, onCopied) {
2730 if (navigator.clipboard && navigator.clipboard.writeText) {
2731 navigator.clipboard.writeText(text).then(onCopied);
2732 return;
2733 }
2734 var $temp = $('<textarea readonly></textarea>').val(text).css({
2735 position: 'fixed',
2736 top: '-1000px'
2737 });
2738 $('body').append($temp);
2739 $temp[0].select();
2740 document.execCommand('copy');
2741 $temp.remove();
2742 onCopied();
2743 };
2744 $(document).on('click', '.merchant-ai-badge', function (e) {
2745 e.preventDefault();
2746 e.stopPropagation();
2747 var prompts = $(this).data('aiPrompts');
2748 $aiPromptsModal.find('.merchant-ai-prompts-modal-title-text').text($(this).data('aiTitle') || '');
2749 $aiPromptsModal.find('.merchant-ai-prompts-modal-blurb').text($(this).data('aiBlurb') || '');
2750 $aiPromptsList.empty();
2751 if (Array.isArray(prompts)) {
2752 prompts.forEach(function (prompt) {
2753 var $row = $('<button type="button" class="merchant-ai-prompt-row"></button>').attr('data-prompt', prompt);
2754 $row.append($('<span class="merchant-ai-prompt-text"></span>').text(prompt));
2755 $row.append($('<span class="merchant-ai-prompt-icon"></span>').html(copyIconSvg));
2756 $row.attr('aria-label', copyHintText);
2757 $aiPromptsList.append($row);
2758 });
2759 }
2760 $aiPromptsModal.addClass('merchant-show');
2761 var $badgeIcon = $aiPromptsModal.find('.merchant-ai-prompts-modal-badge svg');
2762 $badgeIcon.removeClass('merchant-ai-badge-spin');
2763 void $badgeIcon[0].offsetWidth;
2764 $badgeIcon.addClass('merchant-ai-badge-spin');
2765 });
2766 $(document).on('keydown', '.merchant-ai-badge', function (e) {
2767 if (e.key === 'Enter' || e.key === ' ') {
2768 e.preventDefault();
2769 $(this).trigger('click');
2770 }
2771 });
2772 $aiPromptsList.on('click', '.merchant-ai-prompt-row', function () {
2773 var $row = $(this);
2774 if ($row.hasClass('is-copied')) {
2775 return;
2776 }
2777 copyTextToClipboard($row.attr('data-prompt'), function () {
2778 $row.addClass('is-copied').attr('aria-label', copiedLabelText);
2779 $row.find('.merchant-ai-prompt-icon').html(checkIconSvg);
2780 setTimeout(function () {
2781 $row.removeClass('is-copied').attr('aria-label', copyHintText);
2782 $row.find('.merchant-ai-prompt-icon').html(copyIconSvg);
2783 }, 1600);
2784 });
2785 });
2786 var closeAiPromptsModal = function closeAiPromptsModal() {
2787 $aiPromptsModal.removeClass('merchant-show');
2788 $aiPromptsModal.find('.merchant-ai-prompts-modal-badge svg').removeClass('merchant-ai-badge-spin');
2789 };
2790 $(document).on('click', '.merchant-ai-prompts-modal-close', function () {
2791 closeAiPromptsModal();
2792 });
2793 $aiPromptsModal.on('click', function (e) {
2794 if ($(e.target).is($aiPromptsModal)) {
2795 closeAiPromptsModal();
2796 }
2797 });
2798 $(document).on('keydown', function (e) {
2799 if (e.key === 'Escape' && $aiPromptsModal.hasClass('merchant-show')) {
2800 closeAiPromptsModal();
2801 }
2802 });
2803 }
2804 });
2805
2806 /**
2807 * Check if a string is numeric.
2808 *
2809 * @param str the string to check
2810 *
2811 * @returns {boolean} true if numeric, false otherwise
2812 */
2813 function isNumeric(str) {
2814 if (typeof str != "string") return false; // we only process strings!
2815 return !isNaN(str) &&
2816 // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
2817 !isNaN(parseFloat(str)); // ...and ensure strings of whitespace fail
2818 }
2819
2820 /**
2821 * Evaluate conditional fields.
2822 *
2823 * @param conditions the array of conditions
2824 * @param field the field that is being evaluated
2825 *
2826 * @returns {boolean}
2827 */
2828 function evaluateConditions(conditions) {
2829 var field = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
2830 var passed = false;
2831 if ('relation' in conditions) {
2832 //loop through terms
2833 var relation = conditions.relation.toUpperCase();
2834 // lowercase relation
2835 if (relation === 'OR') {
2836 for (var i = 0; i < conditions.terms.length; i++) {
2837 var term = conditions.terms[i];
2838 passed = evaluateConditions(term, field);
2839 if (passed) {
2840 return true;
2841 }
2842 }
2843 } else if (relation === 'AND') {
2844 var n = 0;
2845 for (var _i = 0; _i < conditions.terms.length; _i++) {
2846 // check if inner terms are passed
2847 var _term = conditions.terms[_i];
2848 if (evaluateConditions(_term, field)) {
2849 n++;
2850 }
2851 }
2852 if (n === conditions.terms.length) {
2853 passed = true;
2854 }
2855 }
2856 } else {
2857 var condition = '';
2858 if ('terms' in conditions) {
2859 condition = conditions.terms[0];
2860 } else {
2861 condition = conditions;
2862 }
2863 var $target = $('input[name="merchant[' + condition.field + ']"],select[name="merchant[' + condition.field + ']"]');
2864 if (!$target.length) {
2865 // check if inside flexible content
2866 var flexibleContentParent = field.closest('.layout-body');
2867 if (flexibleContentParent.length > 0) {
2868 $target = flexibleContentParent.find('.merchant-field-' + condition.field).find('input, select');
2869 }
2870 }
2871 if (!$target.length) {
2872 // Maybe the field is a multiple field
2873 $target = $('input[name="merchant[' + condition.field + '][]"],select[name="merchant[' + condition.field + '][]"]');
2874 }
2875 if (!$target.length) {
2876 // Maybe the field is inside fields group
2877 $target = $('.merchant-group-fields-container').find('.merchant-field-' + condition.field + ' input[name*="' + condition.field + '"],.merchant-field-' + condition.field + ' select[name*="' + condition.field + '"]');
2878 }
2879 var value = $target.val();
2880 if ($target.attr('type') === 'checkbox') {
2881 value = $target.is(':checked');
2882 }
2883 if ($target.attr('type') === 'radio') {
2884 value = $target.filter(':checked').val();
2885 }
2886
2887 // check if the field is multiple checkbox
2888 if ($target.attr('type') === 'checkbox' && $target.length > 1) {
2889 value = [];
2890 $target.each(function () {
2891 if ($(this).is(':checked')) {
2892 value.push($(this).val());
2893 }
2894 });
2895 }
2896
2897 // cast value as string if numeric
2898 if (isNumeric(value)) {
2899 value = Number(value);
2900 }
2901
2902 // check if is array condition.value
2903 if (Array.isArray(condition.value)) {
2904 condition.value = condition.value.map(function (item) {
2905 if (isNumeric(item)) {
2906 return Number(item);
2907 }
2908 return item;
2909 });
2910 }
2911 if (condition.operator === '===' && value === condition.value) {
2912 passed = true;
2913 } else if (condition.operator === '!==' && value !== condition.value) {
2914 passed = true;
2915 } else if (condition.operator === '>' && value > condition.value) {
2916 passed = true;
2917 } else if (condition.operator === '<' && value < condition.value) {
2918 passed = true;
2919 } else if (condition.operator === '>=' && value >= condition.value) {
2920 passed = true;
2921 } else if (condition.operator === '<=' && value <= condition.value) {
2922 passed = true;
2923 } else if (condition.operator === 'in' && condition.value.includes(value)) {
2924 passed = true;
2925 } else if (condition.operator === '!in' && !condition.value.includes(value)) {
2926 passed = true;
2927 } else if (condition.operator === 'contains' && Array.isArray(value) && value.includes(condition.value)) {
2928 passed = true;
2929 } else if (condition.operator === '!contains' && Array.isArray(value) && !value.includes(condition.value)) {
2930 passed = true;
2931 }
2932 }
2933 return passed;
2934 }
2935 })(jQuery, window, document);
2936
2937 // Extend jQuery to add getPath func to get accurate dynamic selector to an element.
2938 jQuery.fn.extend({
2939 getPath: function getPath() {
2940 var pathes = [];
2941 this.each(function (index, element) {
2942 var path,
2943 $node = jQuery(element);
2944 while ($node.length) {
2945 var realNode = $node.get(0),
2946 name = realNode.localName;
2947 if (!name) {
2948 break;
2949 }
2950 name = name.toLowerCase();
2951 var parent = $node.parent();
2952 var sameTagSiblings = parent.children(name);
2953 if (sameTagSiblings.length > 1) {
2954 var allSiblings = parent.children();
2955 var index = allSiblings.index(realNode) + 1;
2956 if (index > 0) {
2957 name += ':nth-child(' + index + ')';
2958 }
2959 }
2960 path = name + (path ? ' > ' + path : '');
2961 $node = parent;
2962 }
2963 pathes.push(path);
2964 });
2965 return pathes.join(',');
2966 }
2967 });