PluginProbe
PostNL for WooCommerce / 4.0.0
PostNL for WooCommerce v4.0.0
5.9.12 5.9.11 5.9.10 5.9.9 5.9.8 5.9.7 5.9.6 trunk 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.3.2 4.3.3 4.4.0 4.4.1 All 72 releases
woo-postnl / assets / js / wcpn-admin.js

wcpn-admin.js in PostNL for WooCommerce 4.0.0, at assets/js/wcpn-admin.js

1,278 lines 34.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* eslint-disable prefer-object-spread */
2 /**
3 * @member {Object} wcpn
4 * @property {Object} wcpn.actions
5 * @property {{export: String, add_shipments: String, add_return: String, get_labels: String, modal_dialog: String}}
6 * wcpn.actions
7 * @property {String} wcpn.api_url - The API Url we use in PostNL requests.
8 * @property {String} wcpn.ajax_url
9 * @property {String} wcpn.ask_for_print_position
10 * @property {Object} wcpn.bulk_actions
11 * @property {String} wcpn.download_display
12 * @property {String} wcpn.nonce
13 * @property {Object.<String, String>} wcpn.strings
14 */
15
16 /**
17 * @typedef {Object} Dependency
18 * @property {String} name
19 * @property {Condition} condition
20 * @property {HTMLInputElement} node
21 */
22
23 /**
24 * @typedef {Object} Condition
25 * @property {Object<String,*>} parents
26 * @property {String|Number} set_value
27 */
28
29 /**
30 * Object.assign() polyfill.
31 *
32 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
33 */
34 if (typeof Object.assign !== 'function') {
35 /* Must be writable: true, enumerable: false, configurable: true */
36 Object.defineProperty(Object, 'assign', {
37 value: function assign(target) {
38 if (target === null || target === undefined) {
39 throw new TypeError('Cannot convert undefined or null to object');
40 }
41
42 var to = Object(target);
43
44 for (var index = 1; index < arguments.length; index++) {
45 var nextSource = arguments[index];
46
47 if (nextSource !== null && nextSource !== undefined) {
48 for (var nextKey in nextSource) {
49 /* Avoid bugs when hasOwnProperty is shadowed */
50 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
51 to[nextKey] = nextSource[nextKey];
52 }
53 }
54 }
55 }
56 return to;
57 },
58 writable: true,
59 configurable: true,
60 });
61 }
62
63 /**
64 * Array.find() polyfill.
65 *
66 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill
67 */
68 if (!Array.prototype.find) {
69 Object.defineProperty(Array.prototype, 'find', {
70 value: function(predicate) {
71 // 1. Let O be ? ToObject(this value).
72 if (this == null) {
73 throw TypeError('"this" is null or not defined');
74 }
75
76 var o = Object(this);
77
78 // 2. Let len be ? ToLength(? Get(O, "length")).
79 var len = o.length >>> 0;
80
81 // 3. If IsCallable(predicate) is false, throw a TypeError exception.
82 if (typeof predicate !== 'function') {
83 throw TypeError('predicate must be a function');
84 }
85
86 // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
87 var thisArg = arguments[1];
88
89 // 5. Let k be 0.
90 var k = 0;
91
92 // 6. Repeat, while k < len
93 while (k < len) {
94 /*
95 * a. Let Pk be ! ToString(k).
96 * b. Let kValue be ? Get(O, Pk).
97 * c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
98 * d. If testResult is true, return kValue.
99 */
100 var kValue = o[k];
101 if (predicate.call(thisArg, kValue, k, o)) {
102 return kValue;
103 }
104 // e. Increase k by 1.
105 k++;
106 }
107
108 // 7. Return undefined.
109 return undefined;
110 },
111 configurable: true,
112 writable: true,
113 });
114 }
115
116 /**
117 * Object.values() polyfill.
118 *
119 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values#Polyfill
120 */
121 if (!Object.values) {
122 Object.values = function(obj) {
123 var values = [];
124
125 for (var i in obj) {
126 if (obj.hasOwnProperty(i)) {
127 values.push(obj[i]);
128 }
129 }
130
131 return values;
132 };
133 }
134
135 /* eslint-disable-next-line max-lines-per-function */
136 jQuery(function($) {
137 /**
138 * @type {Boolean}
139 */
140 var askForPrintPosition = Boolean(parseInt(wcpn.ask_for_print_position));
141
142 var skeletonHtml
143 = '<table class="wcpn__skeleton-loader">'
144 + '<tr><td><div></div></td><td><div></div></td></tr>'.repeat(5)
145 + '</table>';
146
147 var selectors = {
148 bulkSpinner: '.wcpn__bulk-spinner',
149 notice: '.wcpn__notice',
150 offsetDialog: '.wcpn__offset-dialog',
151 offsetDialogButton: '.wcpn__offset-dialog__button',
152 offsetDialogClose: '.wcpn__offset-dialog__close',
153 offsetDialogInputOffset: '.wcpn__offset-dialog__offset',
154 orderAction: '.wcpn__action',
155 orderActionImage: '.wcpn__action__img',
156 printQueue: '.wcpn__print-queue',
157 printQueueOffset: '.wcpn__print-queue__offset',
158 shipmentOptions: '.wcpn__shipment-options',
159 shipmentOptionsDialog: '.wcpn__shipment-options-dialog',
160 shipmentOptionsSaveButton: '.wcpn__shipment-options__save',
161 shipmentOptionsShowButton: '.wcpn__shipment-options__show',
162 shipmentSettingsWrapper: '.wcpn__shipment-settings-wrapper',
163 shipmentSummaryList: '.wcpn__shipment-summary__list',
164 showShipmentSummaryList: '.wcpn__shipment-summary__show',
165 spinner: '.wcpn__spinner',
166 toggle: '.wcpn__toggle',
167 tipTipHolder: '#tiptip_holder',
168 tipTipContent: '#tiptip_content',
169 };
170
171 var spinner = {
172 loading: 'loading',
173 success: 'success',
174 failed: 'failed',
175 };
176
177 addListeners();
178 runTriggers();
179 addDependencies();
180 printQueuedLabels();
181
182 var timeoutAfterRequest = 200;
183 var baseEasing = 300;
184
185 /**
186 * Add event listeners.
187 */
188 function addListeners() {
189 /**
190 * Click offset dialog button (single export).
191 */
192 $(selectors.offsetDialog + ' button').click(printOrder);
193
194 $(selectors.offsetDialogClose).click(hideOffsetDialog);
195
196 /**
197 * Show and enable options when clicked.
198 */
199 $(selectors.shipmentOptionsShowButton).click(showShipmentOptionsForm);
200
201 /**
202 * Show summary when clicked.
203 */
204 $(selectors.showShipmentSummaryList).click(showShipmentSummaryList);
205
206 /**
207 * Bulk actions.
208 */
209 $('#doaction, #doaction2').click(doBulkAction);
210
211 /**
212 * Add offset dialog when address labels option is selected.
213 */
214 $('select[name=\'action\'], select[name=\'action2\']').change(showBulkOffsetDialog);
215
216 /**
217 * Single actions click. The .wc_actions .single_wc_actions for support wc > 3.3.0.
218 */
219 $(selectors.orderAction).click(onActionClick);
220
221 $(window).bind('tb_unload', onThickBoxUnload);
222
223 addToggleListeners();
224 }
225
226 /**
227 * Run the things that need to be done on load.
228 */
229 function runTriggers() {
230 /* init options on settings page and in bulk form */
231 $('#wcpn_settings :input, .wcpn__bulk-options :input').change();
232
233 /**
234 * Move the shipment options form and the shipment summary from the actions column to the shipping address column.
235 *
236 * @see includes/admin/class-wcpn-admin.php:49
237 */
238 $(selectors.shipmentSettingsWrapper).each(function() {
239 var shippingAddressColumn = $(this)
240 .closest('tr')
241 .find('td.shipping_address');
242
243 $(this).appendTo(shippingAddressColumn);
244 $(this).show();
245 });
246 }
247
248 /**
249 * Add dependencies for form elements with conditions.
250 */
251 function addDependencies() {
252 /**
253 * Get all nodes with a data-conditions attribute.
254 */
255 var nodesWithConditions = document.querySelectorAll('[data-conditions]');
256
257 /**
258 * Dependency object.
259 *
260 * @type {Object.<String, Dependency[]>}
261 */
262 var dependencies = {};
263
264 /**
265 * Loop through the classes to create a dependency like this: { [parent]: [{condition: Condition, node: Node}] }.
266 */
267 nodesWithConditions.forEach(function(node) {
268 var conditions = node.getAttribute('data-conditions');
269 conditions = JSON.parse(conditions);
270
271 conditions
272 .forEach(function(condition) {
273 Object
274 .keys(condition.parents)
275 .forEach(function(parent) {
276 /**
277 * @type {Dependency}
278 */
279 var data = {
280 condition: condition,
281 node: node,
282 };
283
284 if (dependencies.hasOwnProperty(parent)) {
285 dependencies[parent].push(data);
286 } else {
287 // Or create the list with the node inside it
288 dependencies[parent] = [data];
289 }
290 });
291 });
292 });
293
294 createDependencies(dependencies);
295 }
296
297 /**
298 * Loops through dependants and collects changes that need to be done in queue.
299 *
300 * @param {Object<String, Dependency[]>} dependencies
301 * @param {HTMLInputElement|Node} input
302 * @param {?Number} level
303 * @param {?Object[]} queue
304 *
305 * @returns {Object[]} - Queue.
306 */
307 function checkDependenciesRecursively(dependencies, input, level, queue) {
308 if (level >= 20) {
309 throw new Error('Depth limit of ' + level + ' exceeded (probably an infinite loop)');
310 }
311
312 if (!dependencies.hasOwnProperty(input.name)) {
313 return queue;
314 }
315
316 dependencies[input.name]
317 .forEach(function(dependency) {
318 var data = handleDependency(dependency, level);
319
320 queue.push({
321 name: dependency.node.name.replace(/postnl_options\[\d+\]/, ''),
322 parent: input,
323 node: dependency.node,
324 type: dependency.condition.type,
325 setValue: data.setValue,
326 toggle: data.toggle,
327 });
328
329 if (dependencies.hasOwnProperty(dependency.node.name)) {
330 var dependantInput = document.querySelector('[name="' + dependency.node.name + '"]');
331
332 queue = checkDependenciesRecursively(dependencies, dependantInput, level + 1, queue);
333 }
334 });
335
336 return queue;
337 }
338
339 /**
340 * Executes a set of changes on an element and its parent.
341 *
342 * @param {Object} data
343 * @param {HTMLInputElement} data.node
344 * @param {HTMLInputElement} data.parent
345 * @param {*} data.setValue
346 * @param {Boolean} data.toggle
347 * @param {String} data.type
348 * @param {Number} easing
349 */
350 function toggleElement(data, easing) {
351 var node = data.node;
352 var setValue = data.setValue;
353 var toggle = data.toggle;
354 var elementContainer = $(node).closest('tr');
355
356 switch (data.type) {
357 case 'show':
358 elementContainer[toggle ? 'hide' : 'show'](easing);
359 break;
360 case 'readonly':
361 $(elementContainer).attr('data-readonly', toggle);
362 $(node).prop('readonly', toggle);
363 break;
364 case 'disable':
365 $(elementContainer).attr('data-disabled', toggle);
366 $(node).prop('disabled', toggle);
367 break;
368 }
369
370 if (toggle && setValue) {
371 node.value = setValue;
372 node.dispatchEvent(new Event('change'));
373 // Sync toggles here as well as in the createDependencies because not all inputs listen to the change event.
374 syncToggle(node);
375 }
376
377 data.parent.setAttribute('data-toggled', toggle.toString());
378 node.setAttribute('data-toggled', toggle.toString());
379 }
380
381 function toggleElement2(data, easing) {
382 var node = data.node;
383 var setValue = data.setValue;
384 // var toggle = data.toggle;
385 var elementContainer = $(node).closest('tr');
386
387 data.changes.forEach(function(change) {
388 var toggle = change.toggle;
389 var type = change.type;
390
391 switch (type) {
392 case 'show':
393 elementContainer[toggle ? 'hide' : 'show'](easing);
394 data.parent.setAttribute('data-toggled', toggle.toString());
395 node.setAttribute('data-toggled', toggle.toString());
396 break;
397 case 'readonly':
398 $(elementContainer).attr('data-readonly', toggle);
399 $(node).prop('readonly', toggle);
400 break;
401 case 'disable':
402 $(elementContainer).attr('data-disabled', toggle);
403 $(node).prop('disabled', toggle);
404 break;
405 }
406 })
407
408 // Hacky use of vars here
409 if (toggle && setValue) {
410 node.value = setValue;
411 node.dispatchEvent(new Event('change'));
412 // Sync toggles here as well as in the createDependencies because not all inputs listen to the change event.
413 syncToggle(node);
414 }
415 }
416
417
418 /**
419 * Sync the appearance of toggle elements with the value their hidden input.
420 *
421 * @param {EventTarget} target
422 */
423 function syncToggle(target) {
424 var element = $(target);
425 var toggle = element.siblings('.woocommerce-input-toggle');
426
427 if (element.attr('data-type') !== 'toggle') {
428 return;
429 }
430
431 var mismatch0 = element.val() === '0' && toggle.hasClass('woocommerce-input-toggle--enabled');
432 var mismatch1 = element.val() === '1' && toggle.hasClass('woocommerce-input-toggle--disabled');
433
434 if (mismatch0 || mismatch1) {
435 toggle.toggleClass('woocommerce-input-toggle--disabled');
436 toggle.toggleClass('woocommerce-input-toggle--enabled');
437 }
438 }
439
440 /**
441 * Handle showing and hiding of settings.
442 *
443 * @param {Object<String, Dependency[]>} dependencies - Dependency names and all the nodes that depend on them.
444 */
445 function createDependencies(dependencies) {
446 Object
447 .keys(dependencies)
448 .forEach(function(name) {
449 var inputSelector = '[name="' + name + '"]';
450 var input = document.querySelector(inputSelector);
451
452 if (!input) {
453 // eslint-disable-next-line no-console
454 console.error('Element ' + inputSelector + ' not found.');
455 return;
456 }
457
458 /**
459 * Loop through all the dependencies.
460 *
461 * @param {Event|null} event - Event.
462 * @param {Number} easing - Amount of easing.
463 */
464 function handle(event, easing) {
465 if (easing === undefined) {
466 easing = baseEasing;
467 }
468
469 if (event) {
470 syncToggle(event.target);
471 }
472
473 var updateQueue = checkDependenciesRecursively(dependencies, input, 1, []);
474
475 // Executes all needed updates gathered by checkDependenciesRecursively.
476 updateQueue.forEach(function(dependency) {
477 toggleElement(dependency, easing);
478 });
479 }
480
481 input.addEventListener('change', handle);
482
483 // Do this on load too.
484 handle(null, 0);
485 });
486 }
487
488 /**
489 * Determines if an element should be toggled and if its value should change by checking all parent elements' values.
490 *
491 * @param {Dependency} dependency
492 * @param {Number} level
493 *
494 * @returns {Object}
495 */
496 function handleDependency(dependency, level) {
497 var parents = dependency.condition.parents;
498 var setValue = dependency.condition.set_value || null;
499 var toggle = false;
500
501 Object
502 .keys(parents)
503 .forEach(function(parent) {
504 var parentInput = document.getElementsByName(parent)[0];
505 var localToggle;
506 var wantedValue = parents[parent] || '1';
507
508 var parentToggled = parentInput.getAttribute('data-toggled') === 'true';
509 var dependantToggled = dependency.node.getAttribute('data-toggled') === 'true';
510
511 if (parentToggled && !dependantToggled && level > 1) {
512 localToggle = true;
513 } else if (typeof wantedValue === 'string') {
514 localToggle = parentInput.value !== wantedValue;
515 } else {
516 localToggle = wantedValue.indexOf(parentInput.value) === -1;
517 }
518
519 if (localToggle === true) {
520 toggle = true;
521 }
522 });
523
524 return {
525 toggle: toggle,
526 setValue: setValue,
527 };
528 }
529
530 /**
531 * Add event listeners to all toggle elements.
532 */
533 function addToggleListeners() {
534 $(selectors.toggle).each(function() {
535 $(this).on('click', handleToggle);
536 });
537 }
538
539 /**
540 * Print queued labels.
541 */
542 function printQueuedLabels() {
543 var printData = $(selectors.printQueue).val();
544
545 if (printData) {
546 printLabel(JSON.parse(printData));
547 }
548 }
549
550 /**
551 * Show the shipment options form on the Woo Orders page.
552 *
553 * @param {Event} event - Click event.
554 */
555 function showShipmentOptionsForm(event) {
556 event.preventDefault();
557 var button = $(this);
558 var orderId = button.data('order-id');
559
560 var form = $(selectors.shipmentOptionsDialog);
561 var isSameAsLast = form.data('order-id') === orderId;
562 var isVisible = form.is(':visible');
563
564 if (isVisible) {
565 document.removeEventListener('click', hideShipmentOptionsForm);
566
567 // Close form on second "details" click
568 if (isSameAsLast) {
569 form.slideUp(100);
570 return;
571 }
572
573 // Hide other opened form before opening new one
574 form.hide(0);
575 }
576
577 // Set the position for the dialog to be under the clicked "Details" link.
578 var position = button.offset();
579 position.top -= button.height();
580 form.css(position);
581
582 // Set the data-order-id attribute on the dialog to keep track of which dialog was last opened.
583 form.data('order-id', orderId);
584
585 doRequest.bind(this)({
586 url: wcpn.ajax_url,
587 data: {
588 action: 'wcpn_get_shipment_options',
589 orderId: orderId,
590 security: wcpn.nonce,
591 },
592 onStart: function() {
593 form.html(skeletonHtml);
594 form.slideDown(100);
595 },
596
597 /**
598 * Show the correct data in the form and add event listeners for handling saving and clicking outside the form.
599 *
600 * @param {String} response - Html to put in the form.
601 */
602 afterDone: function(response) {
603 form.html(response);
604
605 addDependencies();
606 addToggleListeners();
607
608 $(selectors.shipmentOptionsSaveButton).on('click', saveShipmentOptions);
609 document.addEventListener('click', hideShipmentOptionsForm);
610 // Trigger WooCommerce's event to init any tipTips.
611 document.body.dispatchEvent(new Event('init_tooltips'));
612 },
613 afterFail: function() {
614 form.slideUp(100);
615 },
616 });
617 }
618
619 /**
620 * @param {Node} element
621 * @param {String} state
622 */
623 function setSpinner(element, state) {
624 var baseSelector = selectors.spinner.replace('.', '');
625 var spinner = $(element).find(selectors.spinner);
626
627 if (state) {
628 spinner
629 .removeClass()
630 .addClass(baseSelector)
631 .addClass(baseSelector + '--' + state)
632 .show();
633 } else {
634 spinner
635 .removeClass()
636 .addClass(baseSelector)
637 .hide();
638 }
639 }
640
641 /**
642 * Save the shipment options in the bulk form.
643 */
644 function saveShipmentOptions() {
645 var form = $(selectors.shipmentOptionsDialog);
646
647 doRequest.bind(this)({
648 url: wcpn.ajax_url,
649 data: {
650 action: 'wcpn_save_shipment_options',
651 form_data: form.find(':input').serialize(),
652 security: wcpn.nonce,
653 },
654 afterDone: function() {
655 setTimeout(function() {
656 form.slideUp();
657 }, timeoutAfterRequest);
658 },
659 });
660 }
661
662 /**
663 * @param {Event} event - Click event.
664 */
665 function doBulkAction(event) {
666 var action = document.querySelector('[name="action"]').value;
667 var spinnerWrapper = $(this).parent('.bulkactions');
668
669 /**
670 * Check the selected action is ours.
671 */
672 if (!Object.values(wcpn.bulk_actions).includes(action)) {
673 return;
674 }
675
676 event.preventDefault();
677
678 /*
679 * Remove notices
680 */
681 $(selectors.notice).remove();
682 var order_ids = [];
683 var rows = [];
684
685 /*
686 * Get array of selected order_ids
687 */
688 $('tbody th.check-column input[type="checkbox"]:checked').each(
689 function() {
690 order_ids.push($(this).val());
691 rows.push('.post-' + $(this).val());
692 }
693 );
694
695 $(rows.join(', ')).addClass('wcpn__loading');
696
697 if (!order_ids.length) {
698 alert(wcpn.strings.no_orders_selected);
699 return;
700 }
701
702 switch (action) {
703
704 /**
705 * Export orders.
706 */
707 case wcpn.bulk_actions.export:
708 exportToPostNL.bind(spinnerWrapper)(order_ids);
709 break;
710
711 /**
712 * Print labels.
713 */
714 case wcpn.bulk_actions.print:
715 printLabel.bind(spinnerWrapper)({
716 order_ids: order_ids,
717 });
718 break;
719
720 /**
721 * Export and print.
722 */
723 case wcpn.bulk_actions.export_print:
724 exportToPostNL.bind(spinnerWrapper)(order_ids, 'after_reload');
725 break;
726 }
727 }
728
729 /**
730 * Do an ajax request.
731 *
732 * @param {Object} request - Request object.
733 */
734 function doRequest(request) {
735 var button = this;
736
737 $(button).prop('disabled', true);
738 setSpinner(button, spinner.loading);
739
740 if (!request.url) {
741 request.url = wcpn.ajax_url;
742 }
743
744 if (request.hasOwnProperty('onStart') && typeof request.onStart === 'function') {
745 request.onStart();
746 }
747
748 $.ajax({
749 url: request.url,
750 method: request.method || 'POST',
751 data: request.data || {},
752 })
753 .done(function(res) {
754 setSpinner(button, spinner.success);
755
756 if (request.hasOwnProperty('afterDone') && typeof request.afterDone === 'function') {
757 request.afterDone(res);
758 }
759 })
760
761 .fail(function(res) {
762 setSpinner(button, spinner.failed);
763
764 if (request.hasOwnProperty('afterFail') && typeof request.afterFail === 'function') {
765 request.afterFail(res);
766 }
767 })
768
769 .always(function(res) {
770 $(button).prop('disabled', false);
771
772 if (request.hasOwnProperty('afterAlways') && typeof request.afterAlways === 'function') {
773 request.afterAlways(res);
774 }
775 });
776 }
777
778 /**
779 * @param name
780 * @param url
781 */
782 function getParameterByName(name, url) {
783 if (!url) {
784 url = window.location.href;
785 }
786 name = name.replace(/[\[\]]/g, '\\$&');
787
788 var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
789 var results = regex.exec(url);
790
791 if (!results) {
792 return null;
793 }
794
795 if (!results[2]) {
796 return '';
797 }
798
799 return decodeURIComponent(results[2].replace(/\+/g, ' '));
800 }
801
802 /**
803 * On clicking the actions in a single order.
804 *
805 * @param {Event} event - Click event.
806 */
807 function onActionClick(event) {
808 var button = this;
809
810 var request = getParameterByName('request', button.href);
811 var order_ids = getParameterByName('order_ids', button.href);
812
813 if (!wcpn.actions.hasOwnProperty(request)) {
814 return;
815 }
816
817 event.preventDefault();
818
819 switch (request) {
820 case wcpn.actions.add_shipments:
821 exportToPostNL.bind(button)();
822 break;
823 case wcpn.actions.get_labels:
824 if (askForPrintPosition && !$(button).hasClass('wcpn__offset-dialog__button')) {
825 showOffsetDialog.bind(button)();
826 } else {
827 printLabel.bind(button)();
828 }
829 break;
830 case wcpn.actions.add_return:
831 postnl_modal_dialog(order_ids, 'return');
832 break;
833 }
834 }
835
836 /**
837 * Show the offset dialog before printing.
838 *
839 * @param {String?} position - To position the dialog `left` or `right` relative to the bound element.
840 * @param {String?} context - Context in which the dialog was created. Ex. 'bulk'.
841 */
842 function showOffsetDialog(position, context) {
843 position = position || 'left';
844
845 var parent = this;
846 var offsetDialog = $(selectors.offsetDialog);
847 var dialogButton = $(selectors.offsetDialogButton);
848 var parentOffset = $(parent).offset();
849
850 /**
851 * Position it to the bottom left or right of the clicked button.
852 */
853 if (position === 'left') {
854 offsetDialog.css({
855 left: parentOffset.left - offsetDialog.width(),
856 top: parentOffset.top,
857 });
858 } else {
859 offsetDialog.css(parentOffset);
860 }
861
862 dialogButton.attr('href', parent.href);
863
864 /**
865 * Reset input(s).
866 */
867 offsetDialog.find('input').val(0);
868
869 /**
870 * Make sure button is not shown and there is no input listener to update it if context is bulk.
871 */
872 if (context === 'bulk') {
873 dialogButton.hide();
874 $(selectors.offsetDialogInputOffset).off('blur update change', onUpdateOffset);
875 } else {
876 dialogButton.show();
877 $(selectors.offsetDialogInputOffset).on('blur update change', onUpdateOffset);
878 }
879
880 /**
881 * Finally, show the dialog.
882 */
883 offsetDialog.slideDown();
884 }
885
886 /**
887 * Hide the offset dialog and remove the input listener.
888 *
889 * @param {Event?} event - Click event if called from a button.
890 */
891 function hideOffsetDialog(event) {
892 if (event) {
893 event.preventDefault();
894 }
895
896 $(selectors.offsetDialogInputOffset).off('blur update change', onUpdateOffset);
897 $(selectors.offsetDialog).slideUp();
898 }
899
900 /**
901 * On changing the offset value in the dialog, update the offset parameter in the dialog button's href attribute.
902 */
903 function onUpdateOffset() {
904 var dialogButton = $(selectors.offsetDialogButton);
905 var hasOffset = dialogButton.attr('href').indexOf('offset=') > -1;
906 var newOffset = this.value;
907
908 if (hasOffset) {
909 dialogButton.attr('href', dialogButton.attr('href').replace(/([?&]offset=)\d*/, '$1' + newOffset));
910 } else {
911 dialogButton.attr('href', dialogButton.attr('href') + '&offset=' + newOffset);
912 }
913 }
914
915 /**
916 * Show the offset dialog for bulk options that allow it.
917 */
918 function showBulkOffsetDialog() {
919 if ([wcpn.bulk_actions.print, wcpn.bulk_actions.export_print].indexOf(this.value) === -1) {
920 hideOffsetDialog();
921 return;
922 }
923
924 showOffsetDialog.bind(this)('right', 'bulk');
925 }
926
927 /**
928 *
929 */
930 function printOrder() {
931 var dialog = $(this).parent();
932
933 /* set print variables */
934 var order_ids = [dialog.find('input.order_id').val()];
935 var offset = dialog.find(selectors.offsetDialogInputOffset).val();
936
937 /* hide dialog */
938 dialog.hide();
939
940 /* print labels */
941 printLabel({
942 order_ids: order_ids,
943 offset: offset,
944 });
945 }
946
947 /* export orders to PostNL via AJAX */
948 /**
949 * @param order_ids
950 * @param print
951 */
952 function exportToPostNL(order_ids, print) {
953 var url;
954 var data;
955
956 if (typeof print === 'undefined') {
957 print = 'no';
958 }
959
960 if (this.href) {
961 url = this.href;
962 } else {
963 data = {
964 action: wcpn.actions.export,
965 request: wcpn.actions.add_shipments,
966 offset: getPrintOffset(),
967 order_ids: order_ids,
968 print: print,
969 _wpnonce: wcpn.nonce,
970 };
971 }
972
973 doRequest.bind(this)({
974 url: url,
975 data: data || {},
976 afterDone: function(response) {
977 var redirect_url = updateUrlParameter(window.location.href, 'postnl_done', 'true');
978
979 if (print === 'no' || print === 'after_reload') {
980 /* refresh page, admin notices are stored in options and will be displayed automatically */
981 window.location.href = redirect_url;
982 } else {
983 /* when printing, output notices directly so that we can init print in the same run */
984 if (response !== null && typeof response === 'object' && 'error' in response) {
985 postnl_admin_notice(response.error, 'error');
986 }
987
988 if (response !== null && typeof response === 'object' && 'success' in response) {
989 postnl_admin_notice(response.success, 'success');
990 }
991
992 /* load PDF */
993 printLabel({
994 order_ids: order_ids,
995 });
996 }
997 },
998 });
999 }
1000
1001 /**
1002 * @param order_ids
1003 * @param dialog
1004 */
1005 function postnl_modal_dialog(order_ids, dialog) {
1006 var data = {
1007 action: wcpn.actions.export,
1008 request: wcpn.actions.modal_dialog,
1009 height: 380,
1010 width: 720,
1011 order_ids: order_ids,
1012 dialog: dialog,
1013 _wpnonce: wcpn.nonce,
1014 // LEAVE THIS AT THE BOTTOM! The awful code behind the thickbox splits the url on "TB_" for some reason.
1015 TB_iframe: true,
1016 };
1017
1018 var url = wcpn.ajax_url + '?' + $.param(data);
1019
1020 /* disable background scrolling */
1021 $('body').css({overflow: 'hidden'});
1022
1023 tb_show('', url);
1024 }
1025
1026 /**
1027 * Re-enable scrolling after closing thickbox.
1028 */
1029 function onThickBoxUnload() {
1030 $('body').css({overflow: 'inherit'});
1031 }
1032
1033 /**
1034 * Open given pdf link. Depending on the link it will be either downloaded or viewed. Refreshes the original window.
1035 *
1036 * @param {String} pdfUrl - The url of the created pdf.
1037 * @param {Boolean?} waitForOnload - Wait for onload to refresh the original window. Refreshes immediately if false.
1038 *
1039 */
1040 function openPdf(pdfUrl, waitForOnload) {
1041 var pdfWindow = window.open(pdfUrl, '_blank');
1042
1043 if (waitForOnload) {
1044 /*
1045 * When the pdf window is loaded reload the main window. If we reload earlier the track & trace code won't be
1046 * ready yet and can't be shown.
1047 */
1048 pdfWindow.onload = function() {
1049 window.location.reload();
1050 };
1051 } else {
1052 /* For when there is no onload event or there is no need to wait. */
1053 window.location.reload();
1054 }
1055 }
1056
1057 /**
1058 * Get the offset from the offset dialog if it's present. Otherwise return 0.
1059 *
1060 * @returns {Number}
1061 */
1062 function getPrintOffset() {
1063 return parseInt(askForPrintPosition ? $(selectors.offsetDialogInputOffset).val() : 0);
1064 }
1065
1066 /* Request PostNL labels */
1067 /**
1068 * @param data
1069 */
1070 function printLabel(data) {
1071 var button = this;
1072 var request;
1073
1074 if (button.href) {
1075 request = {
1076 url: button.href,
1077 };
1078 } else {
1079 request = {
1080 data: Object.assign({
1081 action: wcpn.actions.export,
1082 request: wcpn.actions.get_labels,
1083 offset: getPrintOffset(),
1084 _wpnonce: wcpn.nonce,
1085 }, data),
1086 };
1087 }
1088
1089 request.afterDone = function(response) {
1090 var isDisplay = wcpn.download_display === 'display';
1091 var isDownload = wcpn.download_display === 'download';
1092 var isPdf = response.includes('PDF');
1093 var isApi = response.includes('api.myparcel.nl');
1094
1095 if (isDisplay && isPdf) {
1096 handlePDF(request);
1097 }
1098
1099 if (isDownload && isApi) {
1100 openPdf(response);
1101 }
1102
1103 window.location.reload();
1104 };
1105
1106 doRequest.bind(button)(request);
1107 }
1108
1109 /**
1110 * @param request
1111 */
1112 function handlePDF(request) {
1113 var url;
1114
1115 if (request.hasOwnProperty('data')) {
1116 url = wcpn.ajax_url + '?' + $.param(request.data);
1117 } else {
1118 url = request.url;
1119 }
1120
1121 openPdf(url, true);
1122 }
1123
1124 /**
1125 * @param message
1126 * @param type
1127 */
1128 function postnl_admin_notice(message, type) {
1129 var mainHeader = $('#wpbody-content > .wrap > h1:first');
1130 var notice = '<div class="' + selectors.notice + ' notice notice-' + type + '"><p>' + message + '</p></div>';
1131 mainHeader.after(notice);
1132 $('html, body').animate({scrollTop: 0}, 'slow');
1133 }
1134
1135 /* Add / Update a key-value pair in the URL query parameters */
1136
1137 /* https://gist.github.com/niyazpk/f8ac616f181f6042d1e0 */
1138 /**
1139 * @param uri
1140 * @param key
1141 * @param value
1142 */
1143 function updateUrlParameter(uri, key, value) {
1144 /* remove the hash part before operating on the uri */
1145 var i = uri.indexOf('#');
1146 var hash = i === -1 ? '' : uri.substr(i);
1147 uri = i === -1 ? uri : uri.substr(0, i);
1148
1149 var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i');
1150 var separator = uri.indexOf('?') !== -1 ? '&' : '?';
1151 if (uri.match(re)) {
1152 uri = uri.replace(re, '$1' + key + '=' + value + '$2');
1153 } else {
1154 uri = uri + separator + key + '=' + value;
1155 }
1156 return uri + hash; /* finally append the hash as well */
1157 }
1158
1159 /**
1160 *
1161 */
1162 function showShipmentSummaryList() {
1163 var summaryList = $(this).next(selectors.shipmentSummaryList);
1164
1165 if (summaryList.is(':hidden')) {
1166 summaryList.slideDown();
1167 document.addEventListener('click', hideShipmentSummaryList);
1168 } else {
1169 }
1170
1171 if (summaryList.data('loaded') === '') {
1172 summaryList.addClass('ajax-waiting');
1173 summaryList.find(selectors.spinner).show();
1174
1175 var data = {
1176 security: wcpn.nonce,
1177 action: 'wcpn_get_shipment_summary_status',
1178 order_id: summaryList.data('order_id'),
1179 shipment_id: summaryList.data('shipment_id'),
1180 };
1181
1182 $.ajax({
1183 type: 'POST',
1184 url: wcpn.ajax_url,
1185 data: data,
1186 context: summaryList,
1187 success: function(response) {
1188 this.removeClass('ajax-waiting');
1189 this.html(response);
1190 this.data('loaded', true);
1191 },
1192 });
1193 }
1194 }
1195
1196 /**
1197 * @param {MouseEvent} event - The click event.
1198 * @param {Element} event.target - Click target.
1199 */
1200 function hideShipmentOptionsForm(event) {
1201 handleClickOutside.bind(hideShipmentOptionsForm)(event, {
1202 main: selectors.shipmentOptionsDialog,
1203 wrappers: [
1204 selectors.shipmentOptions,
1205 selectors.shipmentOptionsShowButton,
1206 // Add the tipTip ids as well so clicking a tipTip inside shipment options won't close the form.
1207 selectors.tipTipHolder,
1208 selectors.tipTipContent,
1209 ],
1210 });
1211 }
1212
1213 /**
1214 * Main: The element that will be hidden.
1215 * Wrappers: Elements which don't count as "outside" when clicked.
1216 *
1217 * @param {MouseEvent} event - Click event.
1218 * @property {Element} event.target
1219 */
1220 function hideShipmentSummaryList(event) {
1221 handleClickOutside.bind(hideShipmentSummaryList)(event, {
1222 main: selectors.shipmentSummaryList,
1223 wrappers: [selectors.shipmentSummaryList, selectors.showShipmentSummaryList],
1224 });
1225 }
1226
1227 /**
1228 * Hide any element by checking if the element clicked is not in the list of wrapper elements and not inside the
1229 * element itself.
1230 *
1231 * @param {MouseEvent} event - The click event.
1232 * @param {Object} elements - The elements to show/hide and check inside.
1233 * @property {Node[]} elements.wrappers
1234 * @property {Node} elements.main
1235 */
1236 function handleClickOutside(event, elements) {
1237 event.preventDefault();
1238 var listener = this;
1239 var clickedOutside = true;
1240
1241 elements.wrappers.forEach(function(cls) {
1242 if (clickedOutside && event.target.matches(cls) || event.target.closest(elements.main)) {
1243 clickedOutside = false;
1244 }
1245 });
1246
1247 if (clickedOutside) {
1248 $(elements.main).slideUp();
1249 document.removeEventListener('click', listener);
1250 }
1251 }
1252
1253 /**
1254 * On clicking a toggle. Doesn't do anything if the parent row has data-readonly or data-disabled set to true.
1255 */
1256 function handleToggle() {
1257 var disabledClass = 'woocommerce-input-toggle--disabled';
1258 var enabledClass = 'woocommerce-input-toggle--enabled';
1259 var row = $(this).closest('tr');
1260 var input = $(this).find('input')[0];
1261 var toggle = $(this).find('.woocommerce-input-toggle');
1262
1263 var rowReadOnly = row.attr('data-readonly') === 'true';
1264 var rowDisabled = row.attr('data-disabled') === 'true';
1265
1266 if (rowReadOnly || rowDisabled) {
1267 return;
1268 }
1269
1270 input.value = toggle.hasClass(disabledClass) ? '1' : '0';
1271 toggle.toggleClass(disabledClass);
1272 toggle.toggleClass(enabledClass);
1273
1274 // To trigger event listeners
1275 input.dispatchEvent(new Event('change'));
1276 }
1277 });
1278