PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.0
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.0
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.0, at assets/js/admin/admin.js

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