PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / assets / js / utils.js
vikappointments / site / assets / js Last commit date
charts-framework 2 days ago colorpicker 2 days ago select2 2 days ago tel 2 days ago contextmenu.js 2 days ago currency.js 2 days ago index.html 2 days ago jquery-ui.draggable.min.js 2 days ago jquery-ui.min.js 2 days ago jquery-ui.sortable.min.js 2 days ago jquery.fancybox.js 2 days ago jquery.min.js 2 days ago sortablelist.js 2 days ago statuscodes.js 2 days ago toast.js 2 days ago utils.js 2 days ago vap-emparea.js 2 days ago vikappointments.js 2 days ago
utils.js
1928 lines
1 /*
2 * TIME
3 */
4
5 function getDateFromFormat(value, format, object) {
6 if (!value) {
7 return null;
8 }
9
10 // second char of format can be only [/.-]
11 let separator = format.charAt(1);
12
13 let formatChunks = format.split(separator);
14 let dateChunks = value.split(separator);
15
16 if (formatChunks.length != dateChunks.length || formatChunks.length != 3)
17 {
18 // invalid date
19 return null;
20 }
21
22 // create lookup to easily access the date components
23 let lookup = {};
24
25 for (let i = 0; i < formatChunks.length; i++)
26 {
27 let k = formatChunks[i].toLowerCase();
28
29 lookup[k] = dateChunks[i];
30 }
31
32 // rebuild date by using the military format
33 let date = lookup.y + '-' + lookup.m + '-' + lookup.d;
34
35 if (object === false) {
36 // return only the date string
37 return date;
38 }
39
40 // instantiate date
41 return new Date(date);
42 }
43
44 function getFormattedTime(hour, min, format, tz) {
45 if (typeof format !== 'string') {
46 format = 'H:i';
47 }
48
49 // use by default HH:ii format (24H)
50 const options = {
51 hour: '2-digit',
52 minute: '2-digit',
53 hour12: false,
54 };
55
56 if (format.match(/^[Gg]/)) {
57 // display hours as a number
58 options.hour = 'numeric';
59 }
60
61 if (format.match(/A$/)) {
62 // use AM/PM notation
63 options.hour12 = true;
64 }
65
66 if (tz && typeof tz === 'string') {
67 // display time according to the specified timezone
68 options.timeZone = tz;
69 }
70
71 // create date time
72 let dt = new Date();
73 dt.setHours(hour);
74 dt.setMinutes(min);
75
76 // format time
77 return dt.toLocaleTimeString([], options);
78 }
79
80 /*
81 * EMAIL
82 */
83
84 function isEmailCompliant(email) {
85 if (typeof email !== 'string') {
86 // the input field was passed, get specified e-mail
87 var tmp = jQuery(email).val();
88 // trim the e-mail address
89 tmp = tmp.trim();
90 // Update the input field.
91 // Use the attr method in order force the update,
92 // because val won't do anything as the passed value
93 // will result equals to the previous one.
94 jQuery(email).attr('value', tmp);
95
96 // keep only the e-mail
97 email = tmp;
98 }
99
100 // define regex for e-mail validation
101 var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
102
103 // make sure the e-mail is compliant
104 return re.test(email);
105 }
106
107 /*
108 * FORM VALIDATION
109 */
110
111 function VikFormValidator(form, clazz) {
112 this.form = form;
113
114 if (typeof clazz === 'undefined') {
115 clazz = 'invalid';
116 }
117
118 this.clazz = clazz;
119 this.labels = {};
120 this.callbacks = [];
121
122 // prevent the form submission on enter keydown
123
124 jQuery(this.form).on('keyup', function(e) {
125 var keyCode = e.keyCode || e.which;
126
127 if (keyCode === 13) {
128 e.preventDefault();
129 return false;
130 }
131 });
132
133 this.registerFields('.required');
134 }
135
136 VikFormValidator.prototype.isValid = function(input) {
137 if (jQuery(input).is(':checkbox')) {
138 // make sure the checkbox if selected
139 return jQuery(input).is(':checked') ? true : false;
140 }
141
142 var val = jQuery(input).val();
143
144 if (val === null || val.length == 0) {
145 return false;
146 }
147
148 // if we have an e-mail field, make sure the address is valid
149 if (jQuery(input).attr('type') === 'email' && !isEmailCompliant(input)) {
150 return false;
151 }
152
153 return true;
154 }
155
156 VikFormValidator.prototype.registerFields = function(selector) {
157
158 var _this = this;
159
160 jQuery(this.form).find(selector).each(function() {
161 if (!jQuery(this).hasClass('required')) {
162 jQuery(this).addClass('required');
163 }
164
165 jQuery(this).on('blur', function() {
166 if (_this.isValid(this)) {
167 _this.unsetInvalid(this);
168 } else {
169 _this.setInvalid(this);
170 }
171 });
172 });
173
174 return this;
175 }
176
177 VikFormValidator.prototype.unregisterFields = function(selector) {
178 var _this = this;
179
180 jQuery(this.form).find(selector).each(function() {
181 if (jQuery(this).hasClass('required')) {
182 jQuery(this).removeClass('required');
183 }
184
185 // unset invalid class from deregistered fields
186 _this.unsetInvalid(this);
187
188 jQuery(this).off('blur');
189 });
190
191 return this;
192 }
193
194 VikFormValidator.prototype.validate = function(callback) {
195 var ok = true;
196
197 var _this = this;
198
199 this.clearInvalidTabPane();
200
201 jQuery(this.form).find('.required').filter('input,select,textarea,checkbox').each(function() {
202 if (_this.isValid(this)) {
203 _this.unsetInvalid(this);
204 } else {
205 _this.setInvalid(this);
206 ok = false;
207
208 if (!jQuery(this).is(':visible')) {
209 // the input is probably hidden behind
210 // an unactive tab pane
211 _this.setInvalidTabPane(this);
212 }
213 }
214 });
215
216 // execute registered callbacks
217 for (var i = 0; i < this.callbacks.length; i++) {
218 ok = this.callbacks[i](this) && ok;
219 }
220
221 // execute specified validation
222 if (typeof callback !== 'undefined') {
223 ok = callback(this) && ok;
224 }
225
226 return ok;
227 }
228
229 VikFormValidator.prototype.setLabel = function(input, label) {
230 this.labels[jQuery(input).attr('name')] = label;
231
232 return this;
233 }
234
235 VikFormValidator.prototype.getLabel = function(input) {
236 var name = jQuery(input).attr('name');
237
238 if (this.labels.hasOwnProperty(name)) {
239 return jQuery(this.labels[name]);
240 }
241
242 return jQuery(input).closest('.controls').prev().find('b,label');
243 }
244
245 VikFormValidator.prototype.setInvalid = function(input) {
246 if (jQuery(input).is('input,textarea')) {
247 // do not add "invalid" class to select
248 jQuery(input).addClass(this.clazz);
249 }
250
251 this.getLabel(input).addClass(this.clazz);
252
253 return this;
254 }
255
256 VikFormValidator.prototype.unsetInvalid = function(input) {
257 jQuery(input).removeClass(this.clazz);
258 this.getLabel(input).removeClass(this.clazz);
259
260 return this;
261 }
262
263 VikFormValidator.prototype.isInvalid = function(input) {
264 return jQuery(input).hasClass(this.clazz);
265 }
266
267 VikFormValidator.prototype.clearInvalidTabPane = function() {
268 jQuery('ul.nav-tabs li a').removeClass(this.clazz);
269
270 return this;
271 }
272
273 VikFormValidator.prototype.setInvalidTabPane = function(input) {
274 var pane = jQuery(input).closest('.tab-pane');
275
276 if (pane.length) {
277 var id = jQuery(pane).attr('id');
278 var link = jQuery('ul.nav-tabs li a[href="#' + id + '"]');
279
280 if (link.length) {
281 link.addClass(this.clazz);
282 }
283 }
284
285 return this;
286 }
287
288 VikFormValidator.prototype.addCallback = function(callback) {
289 if (typeof callback === 'function') {
290 this.callbacks.push(callback);
291 }
292
293 return this;
294 }
295
296 VikFormValidator.prototype.removeCallback = function(callback) {
297 for (var i = 0; i < this.callbacks.length; i++) {
298 if (this.callbacks[i] === callback) {
299 this.callbacks.splice(i, 1);
300
301 return true;
302 }
303 }
304
305 return false;
306 }
307
308 /*
309 * FORM OBSERVER
310 */
311
312 function VikFormObserver(form, types, skip) {
313
314 if (typeof types === 'undefined') {
315 types = ['hidden', 'input', 'textarea', 'select', 'file'];
316 }
317
318 if (typeof skip === 'undefined') {
319 skip = [];
320 }
321
322 // always exclude select2 search input
323 skip.push('[id^="s2id_autogen"]');
324
325 this.form = form;
326 this.types = types;
327 this.skipList = skip;
328 this.custom = {};
329 this.cache = {};
330 this.force = 0;
331 this.debug = false;
332 }
333
334 VikFormObserver.prototype.freeze = function() {
335 this.cache = this.map();
336
337 this.force = 0;
338
339 return this;
340 }
341
342 VikFormObserver.prototype.isChanged = function() {
343 if (this.force == 1) {
344 return true;
345 } else if (this.force == -1) {
346 return false
347 }
348
349 var map = this.map();
350
351 var keys1 = Object.keys(this.cache);
352 var keys2 = Object.keys(map);
353
354 if (keys1.length != keys2.length) {
355 return true;
356 }
357
358 for (var i = 0; i < keys1.length; i++) {
359 if (!map.hasOwnProperty(keys1[i])) {
360 if (this.debug) {
361 console.warn('missing property', keys1[i]);
362 }
363 return true;
364 }
365
366 let v1 = this.cache[keys1[i]];
367 let v2 = map[keys1[i]];
368
369 if (Array.isArray(v1) || Array.isArray(v2)) {
370 v1 = JSON.stringify(v1);
371 v2 = JSON.stringify(v2);
372 }
373
374 if (v1 != v2) {
375 if (this.debug) {
376 console.warn(keys1[i], v1, v2);
377 }
378 return true;
379 }
380 }
381
382 return false;
383 }
384
385 VikFormObserver.prototype.changed = function() {
386 this.force = 1;
387
388 return this;
389 }
390
391 VikFormObserver.prototype.unchanged = function() {
392 this.force = -1;
393
394 return this;
395 }
396
397 VikFormObserver.prototype.map = function() {
398
399 var map = {};
400
401 var _this = this;
402
403 jQuery(this.form)
404 .find(this.types.join(', '))
405 .not(this.skipList.join(', '))
406 .each(function() {
407
408 var key = jQuery(this).attr('name') || jQuery(this).attr('id');
409
410 // stop observing the fields without a name/ID
411 if (key) {
412 if (_this.custom.hasOwnProperty(key))
413 {
414 map[key] = _this.custom[key]();
415 }
416 else if (jQuery(this).is(':checkbox'))
417 {
418 map[key] = jQuery(this).is(':checked');
419 }
420 else if (Joomla.editors && Joomla.editors.instances[key])
421 {
422 map[key] = Joomla.editors.instances[key].getValue();
423 }
424 else
425 {
426 map[key] = jQuery(this).val();
427 }
428 }
429 });
430
431 return map;
432 }
433
434 VikFormObserver.prototype.exclude = function(selector) {
435 this.skipList.push(selector);
436
437 return this;
438 }
439
440 VikFormObserver.prototype.push = function(selector) {
441 this.types.push(selector);
442
443 return this;
444 }
445
446 VikFormObserver.prototype.setCustom = function(selector, handler) {
447 this.custom[jQuery(selector).attr('name')] = handler;
448
449 return this;
450 }
451
452 /*
453 * RENDERER
454 */
455
456 function VikRenderer() {
457 return this;
458 }
459
460 /**
461 * Renders the specified select by using the Chosen jQuery plugin.
462 *
463 * @param string selector Either the container of the selects or the select tag itself.
464 * @param string width An optional width to be applied to the select.
465 * @param mixed options An object of options to be passed when initializing CHZN.
466 *
467 * @return void
468 */
469 VikRenderer.chosen = function(selector, width, options) {
470 var chzn;
471
472 if (typeof options !== 'object') {
473 // use empty options
474 options = {};
475 }
476
477 // check if the specified selector is a select itself
478 if (jQuery(selector).is('select')) {
479 // render select with chosen plugin
480 jQuery(selector).chosen(options);
481
482 // find chzn next to select
483 chzn = jQuery(selector).next('div.chzn-container');
484
485 if (chzn.length == 0) {
486 // No chosen, we are probably under WordPress...
487 // So, lets try retrieving previous select2 container.
488 chzn = jQuery(selector).prev('.select2-container');
489 }
490 } else {
491 // render all select under the specified selector with chosen plugin
492 jQuery(selector).find('select').chosen(options);
493
494 // find chzn under selector
495 chzn = jQuery(selector).find('div.chzn-container');
496
497 if (chzn.length == 0) {
498 // No chosen, we are probably under WordPress...
499 // So, lets try retrieving all select2 containers.
500 chzn = jQuery(selector).find('.select2-container');
501 }
502 }
503
504 if (!width) {
505 width = '200px';
506 }
507
508 // copy select classes into the chosen wrapper
509 chzn.each(function() {
510 // auto set default width
511 jQuery(this).css('width', width);
512
513 if (width == 'auto' && jQuery(this).hasClass('select2-container')) {
514 // add a minimum width in order to avoid a small dropdown when
515 // the multiple attribute is set (WP only)
516 jQuery(this).css('min-width', '200px');
517 }
518
519 var select = jQuery(this).prev();
520
521 jQuery(this).addClass(select.attr('class'));
522 });
523 }
524
525 /**
526 * Checks if the given box is currently visible within the monitor.
527 *
528 * @param object box The element to check.
529 * @param integer margin An additional margin to use in order to ignore fixed elements.
530 * @param integer height An optional box height to use. If not specified, the default
531 * height of the box will be retrieved.
532 *
533 * @return integer The pixels to scroll if the box is not visible, otherwise false.
534 */
535 function isBoxOutOfMonitor(box, margin, height) {
536 var box_y = box.offset().top;
537 var scroll = jQuery(window).scrollTop();
538 var screen_height = jQuery(window).height();
539 var box_height = height ? height : box.height();
540
541 // check whether the height of the box exceeds
542 // the total height of the window
543 if (box_height > screen_height) {
544 // use a third of the screen height as reference
545 box_height = screen_height / 3;
546 }
547
548 if (margin === undefined) {
549 margin = 0;
550 }
551
552 // check if we should scroll down
553 if (box_y - scroll + box_height + margin > screen_height) {
554 return box_y - scroll + margin + box.height() - screen_height;
555 }
556
557 // check if we should scroll up
558 if (scroll - margin > box_y + box_height) {
559 return box_y - scroll - margin;
560 }
561
562 // the box is visible
563 return false;
564 }
565
566 /**
567 * Custom confirmation dialog.
568 *
569 * @since 1.6
570 */
571 class VikConfirmDialog {
572
573 /**
574 * Class constructor.
575 *
576 * @param string message The dialog body text/HTML.
577 * @param string id The dialog unique ID (optional).
578 * @param string class A dialog additional class (optional).
579 */
580 constructor(message, id, clazz) {
581 if (id === undefined) {
582 // use a default ID if not specified
583 id = 'vik-confirm-dialog';
584
585 var cont = 1;
586 var tmp = id;
587 // add suffix and repeat as long as the ID already exists
588 while (jQuery('#' + id).length) {
589 id = tmp + '-' + cont;
590 cont++;
591 }
592 }
593
594 if (clazz === undefined) {
595 clazz = '';
596 }
597
598 // check if the message is a DOM element
599 if (typeof message === 'string' && message.match(/^[#.]/)) {
600 try {
601 if (jQuery(message).length) {
602 // extract HTML from specified DOM element
603 var tmp = jQuery(message).html();
604 // unset HTML from original element
605 jQuery(message).remove();
606 // update message parameter
607 message = tmp;
608 }
609 } catch (err) {
610 // invalid selector, suppress error
611 }
612 }
613
614 this.message = message;
615 this.buttons = [];
616 this.id = id;
617 this.clazz = 'vik-confirm-dialog' + (clazz.length ? ' ' + clazz : '');
618 this.built = false;
619 this.args = null;
620 }
621
622 /**
623 * Updates the dialog message.
624 *
625 * @param string message The dialog text/HTML.
626 *
627 * @return self
628 */
629 setMessage(message) {
630 this.message = message;
631
632 // check if the dialog was already built
633 if (this.built) {
634 // destroy the dialog and re-create it in order
635 // to use the new HTML message
636 this.refresh();
637 }
638
639 return this;
640 }
641
642 /**
643 * Adds a button to the dialog.
644 *
645 * @param string text The button text.
646 * @param function callback The function to invoke when the button is clicked.
647 * @param boolean dispose False to avoid closing the dialog after clicking the
648 * button (Optional). If not specified, true by default.
649 * @param boolean queue True to push the button at the beginning of the
650 * list (optional). If not specified, false by default.
651 *
652 * @return self
653 */
654 addButton(text, callback, dispose, head) {
655 if (dispose === undefined) {
656 dispose = true;
657 }
658
659 var btn = {
660 text: text,
661 callback: callback,
662 dispose: dispose,
663 };
664
665 // Push a button in the list.
666 // The first button will be considered as primary.
667 if (head) {
668 // push as first
669 this.buttons.unshift(btn);
670 } else {
671 // push as last
672 this.buttons.push(btn);
673 }
674
675 // check if the dialog was already built
676 if (this.built) {
677 // destroy the dialog and re-create it in order
678 // to support the new button
679 this.refresh();
680 }
681
682 return this;
683 }
684
685 /**
686 * Gets the requested button.
687 *
688 * @param mixed Either the button text or its position
689 * in the list.
690 *
691 * @return mixed The button object if exists, false otherwise.
692 * Returns the button position in case the
693 * button instance is passed.
694 */
695 getButton(id) {
696 // check if the we have a button matching the specified text
697 for (var i = 0; i < this.buttons.length; i++) {
698 // check if the argument matches the button object
699 if (this.buttons[i] === id) {
700 // return button position
701 return i;
702 }
703 // check if the argument matches the button text
704 else if (this.buttons[i].text === id) {
705 return this.buttons[i];
706 }
707 }
708
709 if (typeof id === 'number' || id.match(/^[\d]+$/)) {
710 // a number was used, try to return the
711 // button at the specified position
712 if (this.buttons[id]) {
713 return this.buttons[id];
714 }
715 }
716
717 return false;
718 }
719
720 /**
721 * Make specified button as default.
722 *
723 * @param object btn The button to make primary.
724 *
725 * @return boolean True on success, false otherwise.
726 */
727 makeDefault(btn) {
728 if (typeof btn !== 'object') {
729 return false;
730 }
731
732 // get button position
733 var index = this.getButton(btn);
734
735 if (typeof index !== 'number' || index === 0) {
736 // we got a non-numeric value or the button
737 // is already at the initial position
738 return false;
739 }
740
741 // remove button
742 this.buttons.splice(index, 1);
743
744 // re-push button as first (4th argument)
745 this.addButton(btn.text, btn.callback, btn.dispose, true);
746
747 return true;
748 }
749
750 /**
751 * Build the HTML of the dialog only if it hasn't been created yet.
752 *
753 * @return self
754 */
755 build() {
756 // build HTML only once
757 if (!this.built) {
758 var html = '';
759
760 // open dialog body
761 html += '<div id="' + this.id + '" class="' + this.clazz + '">\n';
762
763 // set dialog message
764 html += '<div class="vik-confirm-message">' + this.message + '</div>\n';
765
766 // open buttons block
767 html += '<div class="vik-confirm-buttons">\n';
768
769 if (this.buttons.length == 0) {
770 // no specified buttons, create default one
771 this.addButton('Ok');
772 }
773
774 // create buttons
775 for (var i = 0; i < this.buttons.length; i++) {
776 html += '<a data-index="' + i + '">' + this.buttons[i].text + '</a>\n';
777 }
778
779 // close buttons block
780 html += '</div>\n';
781
782 // close dialog body
783 html += '</div>\n';
784
785 // append overlay and dialog to the document
786 jQuery('body').append('<div class="vik-confirm-overlay">' + html + '</div>');
787
788 var _this = this;
789
790 // assign an event to the dialog buttons
791 jQuery('#' + this.id)
792 .find('.vik-confirm-buttons a')
793 .on('click', function(event) {
794 // execute event when clicked
795 _this.triggerEvent(this, event);
796 });
797
798 this.built = true;
799 }
800
801 return this;
802 }
803
804 /**
805 * Refreshes the HTML of the dialog.
806 * Useful to support any changes that are made when
807 * the dialog was already built.
808 *
809 * NOTE: do not act if the dialog is visible.
810 *
811 * @return self
812 */
813 refresh() {
814 // make sure the dialog is not open
815 if (!this.isOpen()) {
816 // unset build flag
817 this.built = false;
818 // remove dialog from document
819 jQuery('#' + this.id).parent().remove();
820
821 // re-building will be made before showing the dialog
822 }
823
824 return this;
825 }
826
827 /**
828 * Triggers the callback assigned to the button that
829 * has been clicked. If specified, the dialog will be
830 * closed after the callback execution.
831 *
832 * @param mixed btn The clicked button.
833 * @param Event event The unleashed event.
834 *
835 * @return boolean True on success, false otherwise.
836 */
837 triggerEvent(btn, event) {
838 // find clicked button
839 var button = this.buttons[parseInt(jQuery(btn).data('index'))];
840
841 if (!button) {
842 // button not found
843 return false;
844 }
845
846 // trigger event if owns a callback
847 if (button.callback) {
848 // pass the arguments that was set when showing the dialog
849 button.callback(this.args, event);
850 }
851
852 // dispose dialog if needed
853 if (button.dispose) {
854 this.dispose();
855 }
856
857 return true;
858 }
859
860 /**
861 * Shows the dialog.
862 * Builds the HTML in case it wasn't yet rendered.
863 * Registers the shortcuts to handle ENTER and ESC keys.
864 *
865 * @param mixed args The arguments to pass to the callback
866 * when a button is clicked.
867 * @param object options An object of options.
868 *
869 * @return self
870 */
871 show(args, options) {
872 // build dialog if missing
873 this.build();
874
875 // register specified arguments for later use (trigger event)
876 this.args = args;
877
878 // create default options object
879 options = jQuery.extend({
880 /**
881 * Flag used to allow the user to dismiss the confirmation
882 * dialog by pressing ESC from the keyboard.
883 *
884 * @param boolean True by default.
885 */
886 esc: true,
887 /**
888 * Flag used to allow the user to auto-submit the confirmation
889 * dialog by pressing ENTER from the keyboard.
890 *
891 * @param boolean True by default.
892 */
893 submit: true,
894 }, options);
895
896 // register KEY shortcuts before showing the dialog (inject dialog instance within event)
897 jQuery(window).on('keydown', {dialog: this, options: options}, VikConfirmDialog.handleShortcut);
898
899 // lock page scroll as long as the dialog is open
900 jQuery('body').css('overflow', 'hidden');
901
902 // trigger event before displaying the dialog
903 jQuery('#' + this.id).trigger('beforeshow');
904
905 // display dialog
906 jQuery('#' + this.id).parent().show();
907
908 // trigger event after displaying the dialog
909 jQuery('#' + this.id).trigger('aftershow');
910
911 return this;
912 }
913
914 /**
915 * Closes the dialog.
916 * Turns off the shortcuts that was used to handle keyboard events.
917 *
918 * @return self
919 */
920 dispose() {
921 // deregister KEY shortcuts before hiding the dialog
922 jQuery(window).off('keydown', VikConfirmDialog.handleShortcut);
923
924 // re-enable page scroll
925 jQuery('body').css('overflow', 'auto');
926
927 // hide dialog
928 jQuery('#' + this.id).parent().hide();
929
930 // trigger event after displaying the dialog
931 jQuery('#' + this.id).trigger('dismiss');
932
933 return this;
934 }
935
936 /**
937 * Checks whether the dialog is currently visible.
938 *
939 * @return boolean True if visible, false otherwise.
940 */
941 isOpen() {
942 // check if the dialog is visible (open)
943 return jQuery('#' + this.id).parent().is(':visible');
944 }
945
946 /**
947 * Initialises a keyboard event to catch ENTER and ESC
948 * when pressed. The ENTER key simulates a click to the
949 * primary button (the first added). The ESC key disposes
950 * the dialog without executing any events.
951 *
952 * @param Event event The keyboard event.
953 *
954 * @return false
955 */
956 static handleShortcut(event) {
957 // retrieve dialog instance from event data
958 var dialog = event.data.dialog;
959 // retrieve options from event data
960 var options = event.data.options;
961
962 // check for ENTER
963 if (event.keyCode == 13) {
964 // make sure auto-submit is allowed
965 if (options.submit) {
966 // find first button in dialog and simulate a 'click'
967 jQuery('#' + dialog.id)
968 .find('.vik-confirm-buttons a')
969 .first()
970 .trigger('click');
971 }
972 }
973 // check for ESC
974 else if (event.keyCode == 27) {
975 // make sure ESC is allowed
976 if (options.esc) {
977 // hide dialog
978 dialog.dispose();
979 }
980 }
981 }
982 }
983
984 /*
985 * Geolocation helper class.
986 *
987 * @since 1.7
988 */
989 class VikGeo {
990
991 /**
992 * Promise used to obtain the user coordinates.
993 *
994 * When it resolves, an object containing
995 * the latitude ("lat") and longitude ("lng")
996 * is returned.
997 *
998 * In case of rejection, an error code is returned.
999 *
1000 * @param string name The cookie name. If not specified,
1001 * the default one will be used.
1002 *
1003 * @return Promise
1004 */
1005 static getCurrentPosition(name) {
1006 if (!name) {
1007 name = VikGeo.cookieName;
1008 }
1009
1010 // create promise
1011 return new Promise((resolve, reject) => {
1012 // create base regex
1013 var regex = "(?:^|;)\\s*%s\\s*=\\s*([^;]*)(?:;|$)";
1014 // insert cookie name within regex (escape dots)
1015 regex = regex.replace(/%s/, name.replace(/\./g, '\\.'));
1016 // create pattern
1017 regex = new RegExp(regex, 'i');
1018
1019 // check whether the cookie string contains our cookie
1020 var match = document.cookie.match(regex);
1021
1022 if (match && match.length) {
1023 // split coordinates
1024 var coord = match[1].split(/,\s*/);
1025
1026 coord = {
1027 lat: parseFloat(coord[0]),
1028 lng: parseFloat(coord[1]),
1029 };
1030
1031 resolve(coord);
1032 return true;
1033 }
1034
1035 // missing coordinates, we need to obtain them
1036 if (!navigator.geolocation) {
1037 // browser doesn't support geolocation
1038 reject({code: 0, message: 'Geolocation not supported'});
1039 return false;
1040 }
1041
1042 // ask the user to retrieve the position
1043 navigator.geolocation.getCurrentPosition(function(position) {
1044 // create coordinates
1045 var coord = {
1046 lat: position.coords.latitude,
1047 lng: position.coords.longitude,
1048 };
1049
1050 // store coordinates in a cookie for 1 week
1051 var date = new Date();
1052 date.setDate(date.getDate() + 7);
1053
1054 document.cookie = name + '=' + coord.lat + ',' + coord.lng + '; expires=' + date.toUTCString() + '; path=/';
1055
1056 resolve(coord);
1057 }, function(err) {
1058 // create config to make a GET request
1059 var url = {
1060 url: 'https://ipinfo.io/geo',
1061 type: 'get',
1062 };
1063
1064 // retrieve position by using IPINFO service as fallback
1065 UIAjax.do(url, null,
1066 function(resp) {
1067 if (!resp.loc) {
1068 // missing coordinates, reject with navigator error
1069 reject(err);
1070 return false;
1071 }
1072
1073 // split coordinates
1074 var coord = resp.loc.split(/,\s*/);
1075
1076 coord = {
1077 lat: parseFloat(coord[0]),
1078 lng: parseFloat(coord[1]),
1079 };
1080
1081 // store coordinates in a cookie for 1 week
1082 var date = new Date();
1083 date.setDate(date.getDate() + 7);
1084
1085 document.cookie = name + '=' + coord.lat + ',' + coord.lng + '; expires=' + date.toUTCString() + '; path=/';
1086
1087 // complete process
1088 resolve(coord);
1089 }, function(failure) {
1090 // unable to retrieve the position
1091 reject(err);
1092 }
1093 );
1094 });
1095 });
1096 }
1097
1098 /**
1099 * Elaborates the data contained within the place instance
1100 * and returns an object with details following the customer
1101 * standards.
1102 *
1103 * @param object place An object returned by Autocomplete.getPlace().
1104 *
1105 * @return object An object containing a customer address.
1106 */
1107 static extractDataFromPlace(place) {
1108 if (!place || !place.address_components) {
1109 // nothing to fetch
1110 return false;
1111 }
1112
1113 var lookup = {};
1114 var data = {};
1115
1116 for (var i = 0; i < place.address_components.length; i++) {
1117 var comp = place.address_components[i];
1118
1119 switch (comp.types[0]) {
1120 // cities
1121 case 'sublocality_level_1':
1122 case 'administrative_area_level_3':
1123 case 'locality':
1124 // state
1125 case 'administrative_area_level_2':
1126 case 'administrative_area_level_1':
1127 // country
1128 case 'country':
1129 // post code
1130 case 'postal_code':
1131 // street name
1132 case 'route':
1133 // street number
1134 case 'street_number':
1135 case 'premise':
1136 lookup[comp.types[0]] = comp.short_name;
1137 break;
1138 }
1139 }
1140
1141 // fetch country
1142 if (lookup.country) {
1143 data.country = lookup.country;
1144 }
1145
1146 // fetch components depending on country
1147 if (data.country == 'US') {
1148 // extract state from "administrative_area_level_1" for US
1149 data.state = lookup.administrative_area_level_1;
1150 // extract city from "locality" for US
1151 data.city = lookup.locality;
1152 } else {
1153 // otherwise extract state from "administrative_area_level_2"
1154 data.state = lookup.administrative_area_level_2;
1155 // otherwise extract city from "administrative_area_level_3"
1156 data.city = lookup.administrative_area_level_3 || lookup.locality;
1157 }
1158
1159 // set post code
1160 data.zip = lookup.postal_code;
1161
1162 // clean address field by leaving only the street name and number
1163 data.address = place.name;
1164
1165 // fetch street data
1166 data.street = {
1167 name: lookup.route,
1168 number: lookup.street_number || lookup.premise,
1169 };
1170
1171 // fill latitude and longitude
1172 if (place.geometry) {
1173 data.lat = place.geometry.location.lat();
1174 data.lng = place.geometry.location.lng();
1175 }
1176
1177 return data;
1178 }
1179 }
1180
1181 VikGeo.cookieName = 'vik.position.coord';
1182
1183 /**
1184 * Google Maps events handler class.
1185 * Useful to handle errors triggered by GM.
1186 */
1187 class VikMapsFailure {
1188
1189 /**
1190 * Turns off the Places/Autocomplete input field.
1191 *
1192 * @param mixed input The input selector to which the autocomplete is attached.
1193 * @param mixed autocomplete The autocomplete instance returned by Google.
1194 *
1195 * @return void
1196 */
1197 static disableAutocomplete(input, autocomplete) {
1198 // remove all attached events from input
1199 autocomplete.unbindAll();
1200 google.maps.event.clearInstanceListeners(input);
1201 jQuery('.pac-container').remove();
1202
1203 /**
1204 * Manually turn off all the changes applied by google:
1205 * - disabled
1206 * - class
1207 * - style (background)
1208 * - placeholder
1209 *
1210 * We cannot replace the input because of unexpected behaviors
1211 * that could be caused by any other attached events.
1212 */
1213 jQuery(input)
1214 .css('background', 'none')
1215 .attr('placeholder', '')
1216 .prop('disabled', false)
1217 .removeClass('gm-err-autocomplete');
1218 }
1219
1220 /**
1221 * Listens the errors printed within the console to catch all the following
1222 * errors triggered by Google API:
1223 * - ApiNotActivatedMapError
1224 *
1225 * In case of detected errors, an event will be triggered.
1226 *
1227 * @return void
1228 */
1229 static listenConsole() {
1230 // keep original "error" method
1231 var _error = console.error;
1232
1233 // override "error" console method
1234 console.error = function() {
1235 // make sure the message is set
1236 if (arguments[0] && typeof arguments[0] === 'string') {
1237 // look for "ApiNotActivatedMapError" error and extract the related API Lib
1238 var match = arguments[0].match(/([a-zA-Z]+) API error: ApiNotActivatedMapError/);
1239
1240 if (match) {
1241 // trigger error event
1242 jQuery(window).trigger('google.apidisabled.' + match[1].toLowerCase());
1243 }
1244 }
1245
1246 if (_error.apply) {
1247 // do this for normal browsers
1248 _error.apply(console, arguments);
1249 } else {
1250 // IE backward compatibility
1251 var message = Array.prototype.slice.apply(arguments).join(' ')
1252 _error(message);
1253 }
1254 }
1255 }
1256
1257 /**
1258 * Returns true in case something went wrong with
1259 * the authentication of the API Key.
1260 *
1261 * @return boolean True in case the user is not authorised to use Google API services.
1262 */
1263 static hasError() {
1264 return VikMapsFailure.error === true;
1265 }
1266 }
1267
1268 /**
1269 * Google Maps Authentication Error.
1270 */
1271 function gm_authFailure() {
1272 // register failure error
1273 VikMapsFailure.error = true;
1274
1275 // inform all subscribers that the authentication with
1276 // Google Maps APIs failed
1277 jQuery(window).trigger('google.autherror');
1278
1279 return false;
1280 };
1281
1282 /**
1283 * Helper class used to play sounds.
1284 */
1285 class SoundTry {
1286 /**
1287 * Tries to play a sound.
1288 * In case of success, it will be played
1289 * only once within the specified milliseconds.
1290 *
1291 * @param string src The path of the audio to play.
1292 * @param integer threshold The milliseconds in which the
1293 * audio cannot be played again,
1294 * since the last time is was played.
1295 *
1296 * @return mixed The audio element on success, false otherwise.
1297 */
1298 static playOnce(src, threshold) {
1299 let play = true;
1300
1301 if (threshold) {
1302 // create pool of played sounds if undefined
1303 if (typeof SoundTry.pool === 'undefined') {
1304 SoundTry.pool = {};
1305 }
1306
1307 // check if the audio is still in the pool
1308 if (SoundTry.pool.hasOwnProperty(src)) {
1309 // audio already played, don't play it again
1310 play = false;
1311
1312 // reset current timer
1313 clearTimeout(SoundTry.pool[src]);
1314 }
1315
1316 // mark sound as played
1317 SoundTry.pool[src] = setTimeout(function() {
1318 // auto-delete sound from pool on time expiration
1319 delete SoundTry.pool[src];
1320 }, Math.abs(threshold));
1321 }
1322
1323 if (play) {
1324 // create audio element and auto-play
1325 return new Audio(src).play();
1326 }
1327
1328 return null;
1329 }
1330
1331 /**
1332 * Tries to play a sound.
1333 * In case the browser denies the action, a popup
1334 * is displayed in order to inform the user that
1335 * audio auto-play must be enabled from the
1336 * configuration of the browser.
1337 *
1338 * @param string src The path of the audio to play.
1339 *
1340 * @return Audio The audio element.
1341 *
1342 * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement
1343 */
1344 static play(src) {
1345 // create audio element
1346 var audio = new Audio(src);
1347
1348 // try to play the audio
1349 audio.play().catch((error) => {
1350 // make sure local storage is enabled in order
1351 // to avoid spamming the users every time an
1352 // error is faced
1353 if (typeof localStorage === 'undefined') {
1354 // Unable to cache whether the user was
1355 // already informed. Better to fail silently...
1356 return;
1357 }
1358
1359 // check if the user was already informed
1360 if (localStorage.getItem('tryPlaySound.warned')) {
1361 // user already informed, do not proceed again
1362 return;
1363 }
1364
1365 // Unable to play the audio.
1366 // Alert the user with the returned error.
1367 alert(error);
1368
1369 // keep track that the user was already informed
1370 localStorage.setItem('tryPlaySound.warned', true);
1371 });
1372
1373 // return audio element to let the caller use the sound
1374 return audio;
1375 }
1376 }
1377
1378 /**
1379 * Helper class used to temporarily cache some data
1380 * for the whole page life.
1381 */
1382 class VAPTempCache {
1383 /**
1384 * Checks whether there's a cache for the specified signature.
1385 *
1386 * @param mixed key The signature.
1387 *
1388 * @return mixed The cached data or null.
1389 */
1390 static get(key) {
1391 // empty cache
1392 if (typeof VAPTempCache.pool === 'undefined') {
1393 return null;
1394 }
1395
1396 // create cache signature
1397 const sign = VAPTempCache.createSignature(key);
1398
1399 if (!VAPTempCache.hasOwnProperty(sign)) {
1400 // cache not found
1401 return null;
1402 }
1403
1404 // return cached data
1405 return VAPTempCache[sign];
1406 }
1407
1408 /**
1409 * Registers the specified data within the cache.
1410 *
1411 * @param mixed key The signature.
1412 * @param mixed data The data to cache.
1413 *
1414 * @return void
1415 */
1416 static set(key, data) {
1417 // init cache if undefined
1418 if (typeof VAPTempCache.pool === 'undefined') {
1419 VAPTempCache.pool = {};
1420 }
1421
1422 // create cache signature
1423 const sign = VAPTempCache.createSignature(key);
1424
1425 // register data
1426 VAPTempCache[sign] = data;
1427 }
1428
1429 /**
1430 * Creates a normalized signature for the cache.
1431 *
1432 * @param mixed key The signature.
1433 *
1434 * @return string
1435 */
1436 static createSignature(key) {
1437 if (Array.isArray(key)) {
1438 return key.join(':');
1439 }
1440
1441 if (typeof key === 'object') {
1442 return JSON.stringify(key);
1443 }
1444
1445 return key;
1446 }
1447 }
1448
1449 /**
1450 * Returns a promise that resolves when the height of the document
1451 * seems to be stabilized.
1452 *
1453 * @return Promise
1454 */
1455 function onDocumentReady() {
1456 return new Promise((resolve) => {
1457 // register initial height
1458 var height = parseInt(jQuery('body').height());
1459
1460 // prepare safe counter
1461 var count = 0;
1462
1463 var callback = function() {
1464 // get new height
1465 var tmp = parseInt(jQuery('body').height());
1466
1467 count++;
1468
1469 if (tmp == height || count > 10) {
1470 // document ready
1471 resolve();
1472 } else {
1473 // register new height
1474 height = tmp;
1475 // check again
1476 setTimeout(callback, 32 + Math.floor(Math.random() * 128));
1477 }
1478 };
1479
1480 // check
1481 setTimeout(callback, 32 + Math.floor(Math.random() * 128));
1482 });
1483 }
1484
1485 /**
1486 * Returns a promise that resolves when the specified instance
1487 * gets defined.
1488 *
1489 * @param function check The callback to invoke to check whether the instance is ready.
1490 * @param mixed threshold An optional threshold to establish the max number of attempts.
1491 *
1492 * @return Promise
1493 */
1494 function onInstanceReady(check, threshold) {
1495 return new Promise((resolve, reject) => {
1496 // prepare safe counter
1497 var count = 0;
1498
1499 var callback = function() {
1500 // increase counter
1501 count++;
1502
1503 try {
1504 // check whether the instance is ready
1505 var instance = check();
1506 } catch (exception) {
1507 // reject because of an exception thrown by the condition callback
1508 reject(exception);
1509 return;
1510 }
1511
1512 if (instance) {
1513 // object is now ready
1514 resolve(instance);
1515 } else {
1516 if (!threshold || count < Math.abs(threshold)) {
1517 // check again
1518 setTimeout(callback, 32 + Math.floor(Math.random() * 128));
1519 } else {
1520 // instance not ready
1521 reject();
1522 }
1523 }
1524 };
1525
1526 // check
1527 callback();
1528 });
1529 }
1530
1531 /**
1532 * Proxy used to invoke a function asynchronously.
1533 *
1534 * @return Promise
1535 */
1536 function instantCallbackAsync() {
1537 // recover specified arguments
1538 var context = this;
1539 var args = arguments;
1540
1541 // create promise
1542 return new Promise((resolve) => {
1543 // instantly resolve the callback by passing the
1544 // arguments that were specified
1545 resolve.apply(context, args);
1546 });
1547 }
1548
1549 /**
1550 * Helper function used to copy the text of an
1551 * input element within the clipboard.
1552 *
1553 * Clipboard copy will take effect only in case the
1554 * function is handled by a DOM event explicitly
1555 * triggered by the user, such as a "click".
1556 *
1557 * @param mixed input The input containing the text to copy.
1558 *
1559 * @return Promise
1560 */
1561 function copyToClipboard(input) {
1562 // register and return promise
1563 return new Promise((resolve, reject) => {
1564 // define a fallback function
1565 var fallback = function(input) {
1566 // focus the input
1567 input.focus();
1568 // select the text inside the input
1569 input.select();
1570
1571 try {
1572 // try to copy with shell command
1573 var copy = document.execCommand('copy');
1574
1575 if (copy) {
1576 // copied successfully
1577 resolve(copy);
1578 } else {
1579 // unable to copy
1580 reject(copy);
1581 }
1582 } catch (error) {
1583 // unable to exec the command
1584 reject(error);
1585 }
1586 };
1587
1588 // look for navigator clipboard
1589 if (!navigator.clipboard) {
1590 // navigator clipboard not supported, use fallback
1591 fallback(input);
1592 return;
1593 }
1594
1595 // try to copy within the clipboard by using the navigator
1596 navigator.clipboard.writeText(input.value).then(() => {
1597 // copied successfully
1598 resolve(true);
1599 }).catch((error) => {
1600 // lets try with the fallback
1601 fallback(input);
1602 });
1603 });
1604 }
1605
1606 /**
1607 * Checks if the current platform is Mac.
1608 *
1609 * @return boolean True if Mac, otherwise false.
1610 */
1611 Navigator.prototype.isMac = function() {
1612 return this.platform.toUpperCase().indexOf('MAC') === 0;
1613 }
1614
1615 /**
1616 * Checks if the current platform is Windows.
1617 *
1618 * @return boolean True if Windows, otherwise false.
1619 */
1620 Navigator.prototype.isWin = function() {
1621 return this.platform.toUpperCase().indexOf('WIN') === 0;
1622 }
1623
1624 /**
1625 * Checks if the current platform is iPhone.
1626 *
1627 * @return boolean True if Android, otherwise false.
1628 */
1629 Navigator.prototype.isiPhone = function() {
1630 return this.userAgent.toUpperCase().indexOf('iPhone') > -1;
1631 }
1632
1633 /**
1634 * Checks if the current platform is iPhone.
1635 *
1636 * @return boolean True if Android, otherwise false.
1637 */
1638 Navigator.prototype.isiOS = function() {
1639 return [
1640 'iPad Simulator',
1641 'iPhone Simulator',
1642 'iPod Simulator',
1643 'iPad',
1644 'iPhone',
1645 'iPod'
1646 ].includes(this.platform)
1647 // iPad on iOS 13 detection
1648 || (this.userAgent.includes("Mac") && "ontouchend" in document)
1649 }
1650
1651 /**
1652 * Checks if the current platform is Android.
1653 *
1654 * @return boolean True if Android, otherwise false.
1655 */
1656 Navigator.prototype.isAndroid = function() {
1657 return this.userAgent.toUpperCase().indexOf('ANDROID') > -1;
1658 };
1659
1660 /**
1661 * @deprecated 1.8 Use UIAjax.do() instead.
1662 */
1663 function doAjaxWithRetries(action, data, success, failure, attempt) {
1664 UIAjax.do(action, data, success, failure, attempt);
1665 }
1666
1667 /**
1668 * @deprecated 1.8 Use UIAjax.isConnectionLost() instead.
1669 */
1670 function isConnectionLostError(err) {
1671 return UIAjax.isConnectionLost(err);
1672 }
1673
1674 /**
1675 * AJAX
1676 */
1677
1678 /**
1679 * UIAjax class.
1680 * Handles asynch server-side connections.
1681 */
1682 class UIAjax {
1683
1684 /**
1685 * Normalizes the given argument to be sent via AJAX.
1686 *
1687 * @param mixed data An object, an associative array or a serialized string.
1688 *
1689 * @return object The normalized object.
1690 */
1691 static normalizePostData(data) {
1692
1693 if (data === undefined) {
1694 data = {};
1695 } else if (Array.isArray(data)) {
1696 // the form data is serialized @see jQuery.serializeArray()
1697 var form = data;
1698
1699 data = {};
1700
1701 for (var i = 0; i < form.length; i++) {
1702 // if the field ends with [] it should be an array
1703 if (form[i].name.endsWith("[]")) {
1704 // if the field doesn't exist yet, create a new list
1705 if (!data.hasOwnProperty(form[i].name)) {
1706 data[form[i].name] = new Array();
1707 }
1708
1709 // append the value to the array
1710 data[form[i].name].push(form[i].value);
1711 } else {
1712 // otherwise overwrite the value (if any)
1713 data[form[i].name] = form[i].value;
1714 }
1715 }
1716 }
1717
1718 return data;
1719 }
1720
1721 /**
1722 * Makes the connection.
1723 *
1724 * @param string url The URL to reach.
1725 * @param mixed data The data to post.
1726 * @param function success The callback to invoke on success.
1727 * @param function failure The callback to invoke on failure.
1728 * @param integer attempt The current attempt (optional).
1729 *
1730 * @return void
1731 */
1732 static do(url, data, success, failure, attempt) {
1733
1734 if (!UIAjax.concurrent && UIAjax.isDoing()) {
1735 return false;
1736 }
1737
1738 if (attempt === undefined) {
1739 attempt = 1;
1740 }
1741
1742 // return same object if data has been already normalized
1743 data = UIAjax.normalizePostData(data);
1744
1745 var config = {};
1746
1747 if (typeof url === 'object') {
1748 // we have a configuration object, use it
1749 Object.assign(config, url);
1750 } else {
1751 // use the default configuration
1752 config.type = 'post';
1753 config.url = url;
1754 }
1755
1756 // inject data within config
1757 config.data = data;
1758
1759 var xhr = jQuery.ajax(
1760 // use fetched config
1761 config
1762 ).done(function(resp) {
1763
1764 UIAjax.pop(xhr);
1765
1766 if (success !== undefined) {
1767 success(resp);
1768 }
1769
1770 }).fail(function(err) {
1771 // If the error has been raised by a connection failure,
1772 // retry automatically the same request. Do not retry if the
1773 // number of attempts is higher than the maximum number allowed.
1774 if (attempt < UIAjax.maxAttempts && UIAjax.isConnectionLost(err)) {
1775
1776 // wait 256 milliseconds before launching the request
1777 setTimeout(function() {
1778 // relaunch same action and increase number of attempts by 1
1779 UIAjax.do(url, data, success, failure, attempt + 1);
1780 }, 256);
1781
1782 } else {
1783
1784 // otherwise raise the failure method
1785 if (failure !== undefined) {
1786 failure(err);
1787 }
1788
1789 }
1790
1791 UIAjax.pop(xhr);
1792
1793 if (err.statusText != 'abort') {
1794 // display only in case the request hasn't been aborted by the user
1795 console.error(err);
1796 }
1797
1798 if (err.status == 500) {
1799 console.error(err.responseText);
1800 }
1801
1802 });
1803
1804 UIAjax.push(xhr);
1805
1806 return xhr;
1807 }
1808
1809 /**
1810 * Makes the connection with the server and start uploading the given data.
1811 *
1812 * @param string url The URL to reach.
1813 * @param mixed data The data to upload.
1814 * @param function done The callback to invoke on success.
1815 * @param function failure The callback to invoke on failure.
1816 * @param function upload The callback to invoke to track the uploading progress.
1817 *
1818 * @return void
1819 */
1820 static upload(url, data, done, failure, upload) {
1821 // define upload config
1822 var config = {
1823 url: url,
1824 type: 'post',
1825 contentType: false,
1826 processData: false,
1827 cache: false,
1828 };
1829
1830 // define upload callback to keep track of progress
1831 if (typeof upload === 'function') {
1832 config.xhr = function() {
1833 var xhrobj = jQuery.ajaxSettings.xhr();
1834
1835 if (xhrobj.upload) {
1836 // attach progress event
1837 xhrobj.upload.addEventListener('progress', function(event) {
1838 // calculate percentage
1839 var percent = 0;
1840 var position = event.loaded || event.position;
1841 var total = event.total;
1842 if (event.lengthComputable) {
1843 percent = Math.ceil(position / total * 100);
1844 }
1845
1846 // trigger callback
1847 upload(percent);
1848 }, false);
1849 }
1850
1851 return xhrobj;
1852 };
1853 }
1854
1855 // invoke default do() method by using custom config
1856 return UIAjax.do(config, data, done, failure);
1857 }
1858
1859 /**
1860 * Checks if we own at least an active connection.
1861 *
1862 * @return boolean
1863 */
1864 static isDoing() {
1865 return UIAjax.stack.length > 0 && UIAjax.count > 0;
1866 }
1867
1868 /**
1869 * Pushes the opened connection within the stack.
1870 *
1871 * @param mixed xhr The connection resource.
1872 *
1873 * @return void
1874 */
1875 static push(xhr) {
1876 UIAjax.stack.push(xhr);
1877 UIAjax.count++;
1878 }
1879
1880 /**
1881 * Removes the specified connection from the stack.
1882 *
1883 * @param mixed xhr The connection resource.
1884 *
1885 * @return void
1886 */
1887 static pop(xhr) {
1888 var index = UIAjax.stack.indexOf(xhr);
1889
1890 if (index !== -1) {
1891 UIAjax.stack.splice(index, 1);
1892 UIAjax.count--;
1893 }
1894 }
1895
1896 /**
1897 * Checks if the given error can be intended as a loss of connection:
1898 * generic error, no status and no response text.
1899 *
1900 * @param object err The error object.
1901 *
1902 * @return boolean
1903 */
1904 static isConnectionLost(err) {
1905 return (
1906 err.statusText == 'error'
1907 && err.status == 0
1908 && err.readyState == 0
1909 && err.responseText == ''
1910 );
1911 }
1912 }
1913
1914 UIAjax.stack = [];
1915 UIAjax.count = 0;
1916 UIAjax.concurrent = true;
1917 UIAjax.maxAttempts = 3;
1918
1919 jQuery.parseJSON = function(data) {
1920 try {
1921 return JSON.parse(data);
1922 } catch (err) {
1923 console.log(err);
1924 console.log(data);
1925 }
1926
1927 return null;
1928 }