PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / trunk
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant vtrunk
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 1.9.12 All 59 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 trunk, at assets/js/admin/admin.js

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