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-frontend.js

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

670 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * The following jsdoc blocks are for declaring the types of the injected variables from php.
3 */
4
5 /**
6 * @property {Object} PostNLDisplaySettings
7 * @property {String} PostNLDisplaySettings.isUsingSplitAddressFields
8 * @property {String[]} PostNLDisplaySettings.splitAddressFieldsCountries
9 *
10 * @see \wcpn_checkout::inject_delivery_options_variables
11 */
12
13 /**
14 * @property {Object} wcpn
15 * @property {String} wcpn.ajax_url
16 *
17 * @see \wcpn_checkout::inject_delivery_options_variables
18 */
19
20 /**
21 * @property {Object} MyParcelDeliveryOptions
22 * @property {String} MyParcelDeliveryOptions.allowedShippingMethods
23 * @property {String} MyParcelDeliveryOptions.disallowedShippingMethods
24 * @property {String} MyParcelDeliveryOptions.hiddenInputName
25 * @see \wcpn_checkout::inject_delivery_options_variables
26 */
27 /* eslint-disable-next-line max-lines-per-function */
28 jQuery(function($) {
29 var PostNLFrontend = {
30 /**
31 * Whether the delivery options are currently shown or not. Defaults to true and can be set to false depending on
32 * shipping methods.
33 *
34 * @type {Boolean}
35 */
36 hasDeliveryOptions: true,
37
38 /**
39 * @type {RegExp}
40 */
41 splitStreetRegex: /(.*?)\s?(\d{1,4})[/\s-]{0,2}([A-z]\d{1,3}|-\d{1,4}|\d{2}\w{1,2}|[A-z][A-z\s]{0,3})?$/,
42
43 /**
44 * @type {Boolean}
45 */
46 isUsingSplitAddressFields: Boolean(Number(PostNLDisplaySettings.isUsingSplitAddressFields)),
47
48 /**
49 * @type {String[]}
50 */
51 splitAddressFieldsCountries: PostNLDisplaySettings.splitAddressFieldsCountries,
52
53 /**
54 * @type {Array}
55 */
56 allowedShippingMethods: JSON.parse(MyParcelDeliveryOptions.allowedShippingMethods),
57
58 /**
59 * @type {Array}
60 */
61 disallowedShippingMethods: JSON.parse(MyParcelDeliveryOptions.disallowedShippingMethods),
62
63 /**
64 * @type {Boolean}
65 */
66 alwaysShow: Boolean(parseInt(MyParcelDeliveryOptions.alwaysShow)),
67
68 /**
69 * @type {Object<String, String>}
70 */
71 previousCountry: {},
72
73 /**
74 * @type {String}
75 */
76 selectedShippingMethod: null,
77
78 /**
79 * @type {Element}
80 */
81 hiddenDataInput: null,
82
83 /**
84 * @type {String}
85 */
86 addressType: null,
87
88 /**
89 * Ship to different address checkbox.
90 *
91 * @type {String}
92 */
93 shipToDifferentAddressField: '#ship-to-different-address-checkbox',
94
95 /**
96 * Shipping method radio buttons.
97 *
98 * @type {String}
99 */
100 shippingMethodField: '[name="shipping_method[0]"]',
101
102 /**
103 * Highest shipping class field.
104 *
105 * @type {String}
106 */
107 highestShippingClassField: '[name="postnl_highest_shipping_class"]',
108
109 addressField: 'address_1',
110 cityField: 'city',
111 countryField: 'country',
112 countryRow: 'country_field',
113 houseNumberField: 'house_number',
114 houseNumberSuffixField: 'house_number_suffix',
115 postcodeField: 'postcode',
116 streetNameField: 'street_name',
117
118 /**
119 * Delivery options events.
120 */
121 updateDeliveryOptionsEvent: 'myparcel_update_delivery_options',
122 updatedDeliveryOptionsEvent: 'myparcel_updated_delivery_options',
123 updatedAddressEvent: 'myparcel_updated_address',
124
125 showDeliveryOptionsEvent: 'myparcel_show_delivery_options',
126 hideDeliveryOptionsEvent: 'myparcel_hide_delivery_options',
127
128 /**
129 * WooCommerce checkout events.
130 */
131 countryToStateChangedEvent: 'country_to_state_changed',
132 updateWooCommerceCheckoutEvent: 'update_checkout',
133 updatedWooCommerceCheckoutEvent: 'updated_checkout',
134
135 /**
136 * Initialize the script.
137 */
138 init: function() {
139 PostNLFrontend.addListeners();
140 PostNLFrontend.injectHiddenInput();
141 },
142
143 /**
144 * When the delivery options are updated, fill the hidden input with the new data and trigger the WooCommerce
145 * update_checkout event.
146 *
147 * @param {CustomEvent} event - The update event.
148 */
149 onDeliveryOptionsUpdate: function(event) {
150 PostNLFrontend.hiddenDataInput.value = JSON.stringify(event.detail);
151
152 /**
153 * Remove this event before triggering and re-add it after because it will cause an infinite loop otherwise.
154 */
155 $(document.body).off(PostNLFrontend.updatedWooCommerceCheckoutEvent, PostNLFrontend.updateShippingMethod);
156 PostNLFrontend.triggerEvent(PostNLFrontend.updateWooCommerceCheckoutEvent);
157
158 $(document.body).on(PostNLFrontend.updatedWooCommerceCheckoutEvent, restoreEventListener);
159
160 /**
161 * After the "updated_checkout" event the shipping methods will be rendered, restore the event listener and delete
162 * this one in the process.
163 */
164 function restoreEventListener() {
165 $(document.body).on(PostNLFrontend.updatedWooCommerceCheckoutEvent, PostNLFrontend.updateShippingMethod);
166 $(document.body).off(PostNLFrontend.updatedWooCommerceCheckoutEvent, restoreEventListener);
167 }
168 },
169
170 /**
171 * If split fields are used add house number to the fields. Otherwise use address line 1.
172 *
173 * @returns {String}
174 */
175 getSplitField: function() {
176 return PostNLFrontend.hasSplitAddressFields()
177 ? PostNLFrontend.houseNumberField
178 : PostNLFrontend.addressField;
179 },
180
181 /**
182 * Add all event listeners.
183 */
184 addListeners: function() {
185 PostNLFrontend.addAddressListeners();
186 PostNLFrontend.updateShippingMethod();
187
188 document.querySelector(PostNLFrontend.shipToDifferentAddressField)
189 .addEventListener('change', PostNLFrontend.addAddressListeners);
190
191 document.addEventListener(PostNLFrontend.updatedAddressEvent, PostNLFrontend.onDeliveryOptionsAddressUpdate);
192 document.addEventListener(PostNLFrontend.updatedDeliveryOptionsEvent, PostNLFrontend.onDeliveryOptionsUpdate);
193
194 /*
195 * jQuery events.
196 */
197 $(document.body).on(PostNLFrontend.countryToStateChangedEvent, PostNLFrontend.synchronizeAddress);
198 $(document.body).on(PostNLFrontend.countryToStateChangedEvent, PostNLFrontend.updateAddress);
199 $(document.body).on(PostNLFrontend.updatedWooCommerceCheckoutEvent, PostNLFrontend.updateShippingMethod);
200 },
201
202 /**
203 * Get field by name. Will return element with PostNLFrontend selector: "#<billing|shipping>_<name>".
204 *
205 * @param {String} name - The part after `shipping/billing` in the id of an element in WooCommerce.
206 * @param {?String} addressType - "shipping" or "billing".
207 *
208 * @returns {Element}
209 */
210 getField: function(name, addressType) {
211 if (!addressType) {
212 if (!PostNLFrontend.addressType) {
213 PostNLFrontend.getAddressType();
214 }
215
216 addressType = PostNLFrontend.addressType;
217 }
218
219 var selector = '#' + addressType + '_' + name;
220 var field = document.querySelector(selector);
221
222 if (!field) {
223 // eslint-disable-next-line no-console
224 console.warn('Field ' + selector + ' not found.');
225 }
226
227 return field;
228 },
229
230 /**
231 * Update address type.
232 *
233 * @returns {String}
234 */
235 getAddressType: function() {
236 var useShipping = document.querySelector(PostNLFrontend.shipToDifferentAddressField).checked;
237
238 PostNLFrontend.addressType = useShipping ? 'shipping' : 'billing';
239
240 return PostNLFrontend.addressType;
241 },
242
243 /**
244 * Get the house number from either the house_number field or the address_1 field. If it's the address field use
245 * the split street regex to extract the house number.
246 *
247 * @returns {String}
248 */
249 getHouseNumber: function() {
250 var hasBillingNumber = $('#billing_' + PostNLFrontend.houseNumberField).val() !== '';
251 var hasShippingNumber = $('#shipping_' + PostNLFrontend.houseNumberField).val() !== '';
252 var hasNumber = hasBillingNumber || hasShippingNumber;
253
254 if (PostNLFrontend.hasSplitAddressFields() && hasNumber) {
255 return PostNLFrontend.getField(PostNLFrontend.houseNumberField).value;
256 }
257
258 return PostNLFrontend.getAddressParts().house_number;
259 },
260
261 /**
262 * @returns {{house_number_suffix: (String | null), house_number: (String | null), street_name: (String | null)}}
263 */
264 getAddressParts: function() {
265 var address = PostNLFrontend.getField(PostNLFrontend.addressField).value;
266 var result = PostNLFrontend.splitStreetRegex.exec(address);
267
268 var parts = {};
269
270 parts[PostNLFrontend.streetNameField] = result ? result[1] : null;
271 parts[PostNLFrontend.houseNumberField] = result ? result[2] : null;
272 parts[PostNLFrontend.houseNumberSuffixField] = result ? result[3] : null;
273
274 return parts;
275 },
276
277 /**
278 * Trigger an event on a given element. Defaults to body.
279 *
280 * @param {String} identifier - Name of the event.
281 * @param {String|HTMLElement|Document} [element] - Element to trigger from. Defaults to 'body'.
282 */
283 triggerEvent: function(identifier, element) {
284 var event = document.createEvent('HTMLEvents');
285 event.initEvent(identifier, true, false);
286 element = !element || typeof element === 'string' ? document.querySelector(element || 'body') : element;
287 element.dispatchEvent(event);
288 },
289
290 /**
291 * Check if the country changed by comparing the old value with the new value before overwriting the PostNLConfig
292 * with the new value. Returns true if none was set yet.
293 *
294 * @returns {Boolean}
295 */
296 countryHasChanged: function() {
297 if (window.MyParcelConfig.address && window.MyParcelConfig.address.hasOwnProperty('cc')) {
298 return window.MyParcelConfig.address.cc !== PostNLFrontend.getField(PostNLFrontend.countryField).value;
299 }
300
301 return true;
302 },
303
304 /**
305 * Get data from form fields, put it in the global PostNLConfig, then trigger updating the delivery options.
306 */
307 updateAddress: function() {
308 if (!window.hasOwnProperty('MyParcelConfig')) {
309 throw 'window.MyParcelConfig not found!';
310 }
311
312 if (typeof window.MyParcelConfig === 'string') {
313 window.MyParcelConfig = JSON.parse(window.MyParcelConfig);
314 }
315
316 window.MyParcelConfig.address = {
317 cc: PostNLFrontend.getField(PostNLFrontend.countryField).value,
318 postalCode: PostNLFrontend.getField(PostNLFrontend.postcodeField).value,
319 number: PostNLFrontend.getHouseNumber(),
320 city: PostNLFrontend.getField(PostNLFrontend.cityField).value,
321 };
322
323 if (PostNLFrontend.hasDeliveryOptions) {
324 PostNLFrontend.triggerEvent(PostNLFrontend.updateDeliveryOptionsEvent);
325 }
326 },
327
328 /**
329 * Set the values of the WooCommerce fields from delivery options data.
330 *
331 * @param {Object|null} address - The new address.
332 */
333 setAddressFromDeliveryOptions: function(address) {
334 if (!address) {
335 return;
336 }
337
338 if (address.postalCode) {
339 PostNLFrontend.getField(PostNLFrontend.postcodeField).value = address.postalCode;
340 }
341
342 if (address.city) {
343 PostNLFrontend.getField(PostNLFrontend.cityField).value = address.city;
344 }
345
346 if (address.number) {
347 PostNLFrontend.setHouseNumber(address.number);
348 }
349 },
350
351 /**
352 * Set the values of the WooCommerce fields. Ignores empty values.
353 *
354 * @param {Object|null} address - The new address.
355 */
356 fillCheckoutFields: function(address) {
357 if (!address) {
358 return;
359 }
360
361 Object
362 .keys(address)
363 .forEach(function(fieldName) {
364 var field = PostNLFrontend.getField(fieldName);
365 var value = address[fieldName];
366
367 if (!field || !value) {
368 return;
369 }
370
371 field.value = value;
372 });
373 },
374
375 /**
376 * Set the house number.
377 *
378 * @param {String|Number} number - New house number to set.
379 */
380 setHouseNumber: function(number) {
381 var address = PostNLFrontend.getField(PostNLFrontend.addressField).value;
382 var oldHouseNumber = PostNLFrontend.getHouseNumber();
383
384 if (PostNLFrontend.hasSplitAddressFields()) {
385 if (oldHouseNumber) {
386 PostNLFrontend.getField(PostNLFrontend.addressField).value = address.replace(oldHouseNumber, number);
387 } else {
388 PostNLFrontend.getField(PostNLFrontend.addressField).value = address + number;
389 }
390 } else {
391 PostNLFrontend.getField(PostNLFrontend.houseNumberField).value = number;
392 }
393 },
394
395 /**
396 * Create an input field in the checkout form to be able to pass the checkout data to the $_POST variable when
397 * placing the order.
398 *
399 * @see includes/class-wcpn-checkout.php::save_delivery_options();
400 */
401 injectHiddenInput: function() {
402 PostNLFrontend.hiddenDataInput = document.createElement('input');
403 PostNLFrontend.hiddenDataInput.setAttribute('hidden', 'hidden');
404 PostNLFrontend.hiddenDataInput.setAttribute('name', MyParcelDeliveryOptions.hiddenInputName);
405
406 document.querySelector('form[name="checkout"]').appendChild(PostNLFrontend.hiddenDataInput);
407 },
408
409 /**
410 * When the delivery options module has updated the address, using the "retry" option.
411 *
412 * @param {CustomEvent} event - The event containing the new address.
413 */
414 onDeliveryOptionsAddressUpdate: function(event) {
415 PostNLFrontend.setAddressFromDeliveryOptions(event.detail);
416 },
417
418 /**
419 * Update the shipping method to the new selections. Triggers hiding/showing of the delivery options.
420 */
421 updateShippingMethod: function() {
422 var shippingMethod;
423 var shippingMethodField = document.querySelectorAll(PostNLFrontend.shippingMethodField);
424 var selectedShippingMethodField = document.querySelector(PostNLFrontend.shippingMethodField + ':checked');
425
426 /**
427 * Check if shipping method field exists. It doesn't exist if there are no shipping methods available for the
428 * current address/product combination or in general.
429 *
430 * If there is no shipping method the delivery options will always be hidden.
431 */
432 if (shippingMethodField.length) {
433 shippingMethod = selectedShippingMethodField ? selectedShippingMethodField.value : shippingMethodField[0].value;
434
435 /**
436 * This shipping method will have a suffix in the checkout, but this is not present in the array of
437 * selected shipping methods from the SETTING_DELIVERY_OPTIONS_DISPLAY setting.
438 *
439 * All variants of flat_rate (including shipping classes) do already have their suffix set properly.
440 */
441 if (shippingMethod.indexOf('flat_rate') === 0) {
442 var shippingClass = PostNLFrontend.getHighestShippingClass();
443
444 if (shippingClass) {
445 shippingMethod = 'flat_rate:' + shippingClass;
446 }
447 }
448
449 PostNLFrontend.selectedShippingMethod = shippingMethod;
450 } else {
451 PostNLFrontend.selectedShippingMethod = null;
452 }
453
454 PostNLFrontend.toggleDeliveryOptions();
455 },
456
457 /**
458 * Hides/shows the delivery options based on the current shipping method. Makes sure to not update the checkout
459 * unless necessary by checking if hasDeliveryOptions is true or false.
460 */
461 toggleDeliveryOptions: function() {
462 if (PostNLFrontend.currentShippingMethodHasDeliveryOptions()) {
463 PostNLFrontend.hasDeliveryOptions = true;
464 PostNLFrontend.triggerEvent(PostNLFrontend.showDeliveryOptionsEvent, document);
465 PostNLFrontend.updateAddress();
466 } else {
467 PostNLFrontend.hasDeliveryOptions = false;
468 PostNLFrontend.triggerEvent(PostNLFrontend.hideDeliveryOptionsEvent, document);
469 }
470 },
471
472 /**
473 * Check if the currently selected shipping method is allowed to have delivery options by checking if the name
474 * starts with any value in a list of shipping methods.
475 *
476 * Most of the values in this list will be full shipping method names, with an instance id, but some can't have one.
477 * That's the reason we're checking if it starts with this value instead of whether it's equal.
478 *
479 * @returns {Boolean}
480 */
481 currentShippingMethodHasDeliveryOptions: function() {
482 var display = false;
483 var invert = false;
484 var list = PostNLFrontend.allowedShippingMethods;
485 var shippingMethod = PostNLFrontend.getSelectedShippingMethod();
486
487 if (!shippingMethod) {
488 return false;
489 }
490
491 if (shippingMethod.indexOf('free_shipping') === 0) {
492 shippingMethod = 'free_shipping';
493 }
494
495 /**
496 * If "all" is selected for allowed shipping methods check if the current method is NOT in the
497 * disallowedShippingMethods array.
498 */
499 if (PostNLFrontend.alwaysShow) {
500 list = PostNLFrontend.disallowedShippingMethods;
501 invert = true;
502 }
503
504 list.forEach(function(method) {
505 var currentMethodIsAllowed = shippingMethod.indexOf(method) > -1;
506
507 if (currentMethodIsAllowed) {
508 display = true;
509 }
510 });
511
512 if (invert) {
513 display = !display;
514 }
515
516 return display;
517 },
518
519 /**
520 * Add listeners to the address fields remove them before adding new ones if they already exist, then update
521 * shipping method and delivery options if needed.
522 *
523 * Uses the country field's parent row because there is no better way to catch the select2 (or selectWoo) events as
524 * we never know when the select is loaded and can't add a normal change event. The delivery options has a debounce
525 * function on the update event so it doesn't matter if we send 5 updates at once.
526 */
527 addAddressListeners: function() {
528 var fields = [PostNLFrontend.countryField, PostNLFrontend.postcodeField, PostNLFrontend.getSplitField()];
529
530 /* If address type is already set, remove the existing listeners before adding new ones. */
531 if (PostNLFrontend.addressType) {
532 fields.forEach(function(field) {
533 PostNLFrontend.getField(field).removeEventListener('change', PostNLFrontend.updateAddress);
534 });
535 }
536
537 PostNLFrontend.getAddressType();
538
539 fields.forEach(function(field) {
540 PostNLFrontend.getField(field).addEventListener('change', PostNLFrontend.updateAddress);
541 });
542
543 PostNLFrontend.updateAddress();
544 },
545
546 /**
547 * Get the current shipping method without the shipping class.
548 *
549 * @returns {String}
550 */
551 getShippingMethodWithoutClass: function() {
552 var shippingMethod = PostNLFrontend.getSelectedShippingMethod();
553 var indexOfSemicolon = shippingMethod.indexOf(':');
554
555 shippingMethod = shippingMethod.substring(0, indexOfSemicolon === -1 ? shippingMethod.length : indexOfSemicolon);
556
557 return shippingMethod;
558 },
559
560 /**
561 * Get the highest shipping class by doing a call to WordPress. We're getting it this way and not from the
562 * highest_shipping_class input because that causes some kind of timing issue which makes the delivery options not
563 * show up.
564 *
565 * @returns {String|null}
566 */
567 getHighestShippingClass: function() {
568 var shippingClass = null;
569
570 $.ajax({
571 type: 'POST',
572 url: wcpn.ajax_url,
573 async: false,
574 data: {
575 action: 'get_highest_shipping_class',
576 },
577 success: function(data) {
578 shippingClass = data;
579 },
580 });
581
582 return shippingClass;
583 },
584
585 /**
586 * @returns {String}
587 */
588 getSelectedShippingMethod: function() {
589 var shippingMethod = PostNLFrontend.selectedShippingMethod;
590
591 if (shippingMethod === 'flat_rate') {
592 shippingMethod += ':' + document.querySelectorAll(PostNLFrontend.highestShippingClassField).length;
593 }
594
595 return shippingMethod;
596 },
597
598 /**
599 * Sync addresses between split and non-split address fields.
600 *
601 * @param {Event} event
602 * @param {String} newCountry
603 */
604 synchronizeAddress: function(event, newCountry) {
605 if (!PostNLFrontend.isUsingSplitAddressFields) {
606 return;
607 }
608
609 var data = $('form').serializeArray();
610
611 ['shipping', 'billing'].forEach(function(addressType) {
612 var typeCountry = data.find(function(item) {
613 return item.name === addressType + '_country';
614 });
615 var hasAddressTypeCountry = PostNLFrontend.previousCountry.hasOwnProperty(addressType);
616 var countryChanged = PostNLFrontend.previousCountry[addressType] !== newCountry;
617
618 var addressField = PostNLFrontend.getField(PostNLFrontend.addressField, addressType);
619 var houseNumberField = PostNLFrontend.getField(PostNLFrontend.houseNumberField, addressType);
620 var houseNumberSuffixField = PostNLFrontend.getField(PostNLFrontend.houseNumberSuffixField, addressType);
621 var streetNameField = PostNLFrontend.getField(PostNLFrontend.streetNameField, addressType);
622
623 if (!hasAddressTypeCountry || countryChanged) {
624 PostNLFrontend.previousCountry[addressType] = typeCountry.value;
625 }
626
627 if (!countryChanged) {
628 return;
629 }
630
631 if (PostNLFrontend.hasSplitAddressFields(newCountry)) {
632 var parts = PostNLFrontend.getAddressParts();
633
634 PostNLFrontend.fillCheckoutFields(parts);
635 } else {
636 var number = houseNumberField.value || '';
637 var street = streetNameField.value || '';
638 var suffix = houseNumberSuffixField.value || '';
639
640 PostNLFrontend.fillCheckoutFields({
641 address_1: (street + ' ' + number + suffix).trim(),
642 });
643 }
644
645 PostNLFrontend.updateAddress();
646 });
647 },
648
649 /**
650 * @param {?String} country
651 *
652 * @returns {Boolean}
653 */
654 hasSplitAddressFields: function(country) {
655 if (!country) {
656 country = PostNLFrontend.getField(PostNLFrontend.countryField).value;
657 }
658
659 if (!PostNLFrontend.isUsingSplitAddressFields) {
660 return false;
661 }
662
663 return PostNLFrontend.splitAddressFieldsCountries.includes(country.toUpperCase());
664 },
665 };
666
667 window.PostNLFrontend = PostNLFrontend;
668 PostNLFrontend.init();
669 });
670