PageSourceSearch

https://www.myschoolbucks.com/scripts/utils.js?version=26.7.35

js myschoolbucks.com collected 2026-09-24 08:56:19 UTC 125,193 bytes, 3,656 lines download raw bytes

1function isTrue(value) {
2	if (value == null || value === undefined) {
3		return false;
4	}
5
6	switch (typeof(value)) {
7		case "boolean": return value;
8		case "number": return (value > 0);
9		case "string":
10			switch(value.trim().toLowerCase()) {
11				case "true":
12				case "1":
13				case "on":
14				case "yes":
15					return true;
16				default:
17					return false;
18			}
19		default: return false;
20	}
21}
22function isEmpty(value) {
23	if (value == null || value === undefined) {
24		return true;
25	}
26
27	switch(typeof value) {
28		case "number":
29		case "boolean":
30		case "bigint":
31		case "function":
32			return false;
33		case "string":
34			return value.trim().length === 0;
35		case "object":
36			if (Array.isArray(value)) {
37				return (value.length == 0);
38			}
39			return Object.keys(value).length === 0;
40		default:
41			return true;
42	}
43	/* Old code using jQuery - deprecated with 14.7
44	if (!jQuery || (typeof(value) === "number") || (typeof(value) === "boolean")) return false;
45	return (
46		($.isPlainObject(value) && $.isEmptyObject(value)) ||
47		($.isArray(value) && value.length == 0) ||
48		(typeof(value) == "string" && ($.trim(value) === "")) ||
49		(!value)
50	);
51	*/
52}
53
54function isNumber(value) {
55	return (!isEmpty(value) && !isNaN(value));
56}
57function isPositiveNumber(value) {
58	return isNumber(value)?(parseInt(value) > 0):false;
59}
60function isPositiveFloat(value) {
61	return isNumber(value)?(parseFloat(value) > 0):false;
62}
63function isNonZeroNumber(value) {
64	return isNumber(value)?(parseFloat(value) != 0):false;
65}
66function containsNumber(value) {
67	if (value == null)
68		return false;
69	else if (isNumber(value))
70		return true;
71	else if (typeof(value) === "string")
72		return (/\d/.test(value));
73	else
74		return false;
75}
76function replaceEmpty(object, replace) {
77	return isEmpty(object)?replace:object;
78}
79
80/* Methods for currency amounts and quantities */
81function stripCurrency(val) {
82	if (!isEmpty(val) && (typeof(val) === "string")) {
83		val = val.replace(/[^0-9-\.]+/g,"");
84	}
85	return val;
86}
87function formatAmount(val, precision) {
88	if (!isEmpty(val)) {
89		if (!precision) precision = 2;
90		val = accounting.unformat(val);
91		return accounting.toFixed(val,precision);
92	}
93	return val;
94}
95function isValidAmount(val, allowCommas) {
96	if (typeof allowCommas === "undefined" || allowCommas === null) allowCommas = false;
97	var regEx = (!allowCommas) ? /^[0-9]+(?:\.[0-9]{1,2})?$/ : /^[+-]?[0-9]{1,3}(?:,?[0-9]{3})*(?:\.[0-9]{1,2})?$/;
98	return regEx.test(val);
99}
100function isValidQuantity(val,allowUnlimited) {
101	if (isEmpty(val)) return false;
102	if (typeof allowUnlimited === "undefined" || allowUnlimited === null) allowUnlimited = true;
103	var regEx = (!allowUnlimited) ? /^[+-]?[0-9]*$/ : /^[+-]?[0-9]*$|^(UNL)$/;
104	return regEx.test(val.toUpperCase());
105}
106
107function isValidDate(value) {
108	if (value == null) {
109		return false;
110	} else if (moment.isDate(value)) {
111		return !isNaN(value.getTime());
112	} else if (moment.isMoment(value)) {
113		return !isNaN(value.toDate().getTime());
114	} else if (typeof value === 'string' || typeof value === 'number') {
115		return !isNaN(moment(value).toDate().getTime());
116	} else {
117		return false;
118	}
119}
120
121function isUrlValid(url) {
122	return /^(https?):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(url);
123}
124
125// Note: `enforceNumberField` and `enforceMoneyField` moved here from dynforms.js
126function enforceNumberField(field) {
127	field.on('keydown', function (e) {
128		// Allow: backspace, delete, tab, escape, enter and .
129		if (
130			$.inArray(e.keyCode, [46, 8, 9, 27, 13]) !== -1 ||
131			// Allow: Ctrl+A
132			(e.keyCode == 65 && e.ctrlKey === true) ||
133			// Allow: home, end, left, right
134			(e.keyCode >= 35 && e.keyCode <= 39)
135		) {
136			// let it happen, don't do anything
137			return;
138		}
139		// If ctrl or alt keys, just return so those keys are not blocked (ctrl+r or ctrl+F5 reloads);
140		if (e.altKey || e.ctrlKey) return true;
141		// Ensure that it is a number and stop the keypress
142		if (
143			(e.shiftKey || e.keyCode < 48 || e.keyCode > 57) &&
144			(e.keyCode < 96 || e.keyCode > 105)
145		) {
146			e.preventDefault();
147		}
148	});
149}
150
151function enforceMoneyField(field) {
152	field.on('keydown', function (e) {
153		// Allow: backspace, delete, tab, escape, enter and .
154		if (
155			$.inArray(e.keyCode, [46, 8, 9, 27, 13, 110, 190]) !== -1 ||
156			// Allow: Ctrl+A
157			(e.keyCode == 65 && e.ctrlKey === true) ||
158			// Allow: home, end, left, right
159			(e.keyCode >= 35 && e.keyCode <= 39)
160		) {
161			// let it happen, don't do anything
162			return;
163		}
164		// If ctrl or alt keys, just return so those keys are not blocked (ctrl+r or ctrl+F5 reloads);
165		if (e.altKey || e.ctrlKey) return true;
166		// Ensure that it is a number and stop the keypress
167		if (
168			(e.shiftKey || e.keyCode < 48 || e.keyCode > 57) &&
169			(e.keyCode < 96 || e.keyCode > 105)
170		) {
171			e.preventDefault();
172		}
173	});
174}
175
176function convertToDate(value, options) {
177	let format = (options && options.format) ? options.format : 'MM/DD/YYYY';
178	if (value == null) {
179		return null;
180	} else if (moment.isDate(value)) {
181		return value;
182	} else if (moment.isMoment(value)) {
183		return value.toDate();
184	} else if (typeof value === 'string' || typeof value === 'number') {
185		return moment(value, format).toDate();
186	}
187	return null;
188}
189
190/* HTML5 Local Storage functions
191 * Local Storage are NOT cookies and never sent to the server.
192 * window.localStorage - stores data with no expiration date
193 */
194function getValueFromLocalStorage(name) {
195	try {
196		if (typeof Storage !== 'undefined' && window.localStorage) {
197			return window.localStorage.getItem(name);
198		}
199	} catch (e) {
200		console.warn('Local storage is not available:', e);
201	}
202	return null;
203}
204function saveValueInLocalStorage(name,value) {
205	try {
206		if (typeof Storage !== 'undefined' && window.localStorage) {
207			window.localStorage.setItem(name, value);
208		}
209	} catch (e) {
210		console.warn('Local storage is not available:', e);
211	}
212}
213function getObjectFromLocalStorage(name) {
214	try {
215		if (typeof Storage !== 'undefined' && window.localStorage) {
216			return JSON.parse(window.localStorage.getItem(name));
217		}
218	} catch (e) {
219		console.warn('Local storage is not available:', e);
220	}
221	return {};
222}
223function saveObjectInLocalStorage(name,object) {
224	try {
225		if (typeof Storage !== 'undefined' && window.localStorage) {
226			window.localStorage.setItem(name, JSON.stringify(object));
227		}
228	} catch (e) {
229		console.warn('Local storage is not available:', e);
230	}
231}
232function removeObjectFromLocalStorage(name) {
233	try {
234		if (typeof Storage !== 'undefined' && window.localStorage) {
235			window.localStorage.removeItem(name);
236		}
237	} catch (e) {
238		console.warn('Local storage is not available:', e);
239	}
240}
241
242/* HTML5 Local Storage functions
243 * Local Storage are NOT cookies and never sent to the server.
244 * window.sessionStorage -  stores data for a SINGLE SESSION ONLY
245 */
246function getValueFromSessionStorage(name) {
247	try {
248		if (typeof Storage !== 'undefined' && window.sessionStorage) {
249			return window.sessionStorage.getItem(name);
250		}
251	} catch (e) {
252		console.warn('Session storage is not available:', e);
253	}
254	return null;
255}
256function saveValueInSessionStorage(name,value) {
257	try {
258		if (typeof Storage !== 'undefined' && window.sessionStorage) {
259			window.sessionStorage.setItem(name, value);
260		}
261	} catch (e) {
262		console.warn('Session storage is not available:', e);
263	}
264}
265function getObjectFromSessionStorage(name) {
266	try {
267		if (typeof Storage !== 'undefined' && window.sessionStorage) {
268			return JSON.parse(window.sessionStorage.getItem(name));
269		}
270	} catch (e) {
271		console.warn('Session storage is not available:', e);
272	}
273	return {};
274}
275function saveObjectInSessionStorage(name,object) {
276	try {
277		if (typeof Storage !== 'undefined' && window.sessionStorage) {
278			window.sessionStorage.setItem(name, JSON.stringify(object));
279		}
280	} catch (e) {
281		console.warn('Session storage is not available:', e);
282	}
283}
284function removeObjectFromSessionStorage(name) {
285	try {
286		if (typeof Storage !== 'undefined' && window.sessionStorage) {
287			window.sessionStorage.removeItem(name);
288		}
289	} catch (e) {
290		console.warn('Session storage is not available:', e);
291	}
292}
293
294function getUniqueId(seed,max) {
295	max = (isNumber(max))?parseInt(max):60;
296		return (((!seed?'':seed) + Math.ceil(new Date().getTime() + (Math.random() * 100))).substring(0,max));
297}
298
299function updateSiteMgrSetting(key,value) {
300	$.ajax({
301		traditional : true,
302		url : '/ver2/updateSiteMgrSetting',
303		data: { key: key, value: value },
304		type : 'POST'
305	}).done(function(data) {
306		console.log('updateSiteMgrSetting: success!');
307	}).fail(function(xhr, textStatus, errorThrown) {
308		console.log('updateSiteMgrSetting - error:', errorThrown);
309	});
310}
311function updateUserOption(key,value) {
312	$.ajax({
313		traditional : true,
314		url : '/ver2/updateUserOption',
315		data: { key: key, value: value },
316		type : 'POST'
317	}).done(function(data) {
318		console.log('updateUserOption: success!');
319	}).fail(function(xhr, textStatus, errorThrown) {
320		console.log('updateUserOption - error:', errorThrown);
321	});
322}
323/*
324 * update multiple UserOptions.
325 * example call: updateUserOptions(['UserOptionName:UserOptionsValue',...]);
326 */
327function updateUserOptions(kvList) {
328	console.log('updateUserOptions - kvList:',kvList);
329	$.ajax({
330		traditional : true,
331		url : '/ver2/updateUserOptions',
332		data: { 'keyValueList': kvList},
333		type : 'POST'
334	}).done(function(data) {
335		console.log('updateUserOptions: success!');
336	}).fail(function(xhr, textStatus, errorThrown) {
337		console.log('updateUserOptions - error:', errorThrown);
338	});
339}
340
341function enableSubmitOnEnterKey($form) {
342	if ($form == null) {
343		console.error('utils.js:enableSubmitOnEnterKey - form is null');
344		return;
345	}
346	if (!($form instanceof jQuery))
347		$form = $("#" + $form);
348
349	if ($form.length) {
350		$form.keydown(function(e) {
351			if (((e.keyCode ? e.keyCode : e.which) == 13) && (e.target.nodeName === "INPUT") && (e.target.type !== "textarea")) {
352				e.preventDefault();
353				$form.submit();
354			}
355		});
356	}
357}
358
359function disableSubmitOnEnterKey() {
360	//Intercept enter key to prevent inadvertent form submission.
361	window.addEventListener("keydown", function(e) {
362		if ((e.keyCode ? e.keyCode : e.which) == 13) {
363			//target only input areas except for textarea and the search box
364			if(e.target.nodeName === "INPUT" && e.target.type !== "textarea" && e.target.id !== "header-search-text") {
365				e.preventDefault();
366				return false;
367			}
368		}
369	}, true);
370}
371
372function initSelect2($sel, options) {
373	if ($sel == null) {
374		console.error('utils.js:initSelect2 - sel is null')
375		return;
376	}
377	if (!($sel instanceof jQuery))
378		$sel = $("#" + $sel);
379
380	if ($sel.length > 0) {
381
382		if (!options.hasOwnProperty('language')) {
383			if ((typeof msbDefaultMessages !== 'undefined') && msbDefaultMessages.hasOwnProperty('select2')) {
384				if (msbDefaultMessages.select2.hasOwnProperty('inputTooShort')) {
385					var funcOverrideInputTooShort = function() {
386						if (options.hasOwnProperty('minimumInputLength')) {
387							var message = msbDefaultMessages.select2.inputTooShort;
388							return message.replace("{0}",options.minimumInputLength);
389						} else {
390							return msbDefaultMessages.select2.inputTooShort;
391						}
392					};
393				}
394				if (msbDefaultMessages.select2.hasOwnProperty('noResults')) {
395					var funcOverrideNoResults = function() {
396						return msbDefaultMessages.select2.noResults;
397					};
398				}
399
400				if ((typeof funcOverrideInputTooShort !== 'undefined') && (typeof funcOverrideNoResults !== 'undefined')) {
401					options.language = {inputTooShort: funcOverrideInputTooShort, noResults: funcOverrideNoResults};
402				} else if ((typeof funcOverrideInputTooShort !== 'undefined')) {
403					options.language = {inputTooShort: funcOverrideInputTooShort};
404				} else if ((typeof funcOverrideNoResults !== 'undefined')) {
405					options.language = {noResults: funcOverrideNoResults};
406				}
407			}
408		}
409
410		options = $.extend({
411			allowClear: true,
412			delay: 250,
413			minimumInputLength: 0,
414			minimumResultsForSearch: 10,
415			multiple: false,
416			placeholder: '-- Select --',
417			theme: null,
418			width: null,
419			widthFull: false, // Kaleida/Vega Setting
420			minWidth: '',
421			maxWidth: '',
422			language: {},
423			dropdownParent: null
424		}, options);
425
426		if (isBootstrap5()) {
427			if (!options.theme)
428				options.theme = "bootstrap-5";
429		} else {
430			if (!options.theme)
431				options.theme = "bootstrap";
432			options.width = '100%';
432 // Do not set for bootstrap-5!
433		}
434
435		if (isTrue(options.widthFull)) {
436			if ($sel.parent().hasClass('input-group')) {
437				$sel.parent().css('width', '100%');
438			} else {
439				$sel.css('width', '100%');
440			}
441		}
442
443		if (!isEmpty(options.width))
444			$sel.css('width', options.width);
445		if (!isEmpty(options.minWidth))
446			$sel.css('min-width', options.minWidth);
447		if (!isEmpty(options.maxWidth))
448			$sel.css('max-width', options.maxWidth);
449
450		var opts = {
451			theme: options.theme,
452			language: options.language,
453			minimumInputLength: options.minimumInputLength,
454			minimumResultsForSearch: options.minimumResultsForSearch,
455			multiple: options.multiple,
456			placeholder: options.placeholder,
457			allowClear: options.allowClear,
458			dropdownParent: options.dropdownParent,
459			escapeMarkup: function (markup) { return htmlEncode(markup); } // let our custom formatter work
460		};
461		if (options.hasOwnProperty('templateSelection')) {
462			opts.templateSelection = options.templateSelection;
463		}
464		if (options.hasOwnProperty('templateResult')) {
465			opts.templateResult = options.templateResult;
466		}
467
468		$sel.select2(opts);
469	}
470}
471
472function initMultipleSelect2($sel, placeholder) {
473	if (!($sel instanceof jQuery))
474		$sel = $("#" + $sel);
475
476	initSelect2($sel, {placeholder: placeholder, multiple: true, allowClear: false});
477}
478
479function initAjaxSelect2($sel, options) {
480	if($sel == null) {
481		console.error('utils.js:initAjaxSelect2 - select is null');
482		return;
483	}
484	if (!options.hasOwnProperty("ajaxUrl") || isEmpty(options.ajaxUrl)) {
485		console.error('utils.js:initAjaxSelect2 - ajaxUrl not provided!');
486		return;
487	}
488
489	if (!($sel instanceof jQuery))
490		$sel = $("#" + $sel);
491
492	/* setup select2 plugin for Product Group selection */
493	if ($sel.length > 0) {
494
495		if ($sel.hasClass("select2-hidden-accessible")) {
496			$sel.select2('destroy');
497		}
498
499		options = $.extend(true, {
500			allowClear: true,
501			delay:250,
502			loadingText: 'Loading',
503			minimumInputLength: 0,
504			placeholder: '',
505			theme: null,
506			multiple: false,
507			width: '100%',
508			minWidth: '',
509			maxWidth: '',
510			request: {}
511		}, options);
512
513		if (isBootstrap5()) {
514			if (!options.theme)
515				options.theme = "bootstrap-5";
516		} else {
517			if (!options.theme)
518				options.theme = "bootstrap";
519		}
520
521		if (!isEmpty(options.width))
522			$sel.css('width', options.width);
523		if (!isEmpty(options.minWidth))
524			$sel.css('min-width', options.minWidth);
525		if (!isEmpty(options.maxWidth))
526			$sel.css('max-width', options.maxWidth);
527
528		$sel.select2({
529			language: getSelect2Language(),
530			theme: options.theme,
531			minimumInputLength: options.minimumInputLength,
532			//quietMillis: 100,
533			multiple: options.multiple,
534			placeholder: options.placeholder,
535			allowClear: options.allowClear,
536			ajax: {
537				dataType: 'json',
538				type: 'POST',
539				url: options.ajaxUrl,
540				cache: false,
541				delay: options.delay, //milliseconds to wait for the user to stop typing before making ajax call
542				data : function(params) {
543					var reqData = {
544						'searchText': params.term || '',
545						'currentPage': params.page || 1
546					};
547					if ($.isFunction(options.request)) {
548						reqData = $.extend(true, reqData, options.request(reqData));
549					} else {
550						reqData = $.extend(true, reqData, options.request);
551					}
552					//console.log('initAjaxSelect2 ajaxUrl:',options.ajaxUrl,' reqData:',reqData);
553					return reqData;
554				},
555				processResults: function (data, params) {
556					//console.log('initAjaxSelect2 - data:',data,' - params:',params);
557					if (data && isTrue(data.loginRequired)) {
558						window.location.reload(true);
559					}
560					var ajaxResults = { results:[], pagination: { more: false } }
561					if (data && data.listItems && data.listItems.length > 0) {
562						var arResults = [];
563						$.each(data.listItems, function(idx,item) {
564							arResults.push({'id':item.id || item.value, 'text':item.text});
565						});
566						ajaxResults.results = arResults;
567
568						if ('morePages' in data)
569							ajaxResults.pagination.more = isTrue(data.morePages)
570						else if ('pagesComplete' in data)
571							ajaxResults.pagination.more = !isTrue(data.pagesComplete)
572						else
573							ajaxResults.pagination.more = true;
574					}
575					return ajaxResults;
576				}
577			},
578			escapeMarkup: function (markup) {
579				if (!isEmpty(markup)) {
580					markup = markup.replaceAll('&amp;','&');
581					markup = htmlEncode(markup);
582				}
583				return markup;
584			}
585		});
586	}
587}
588
589
590function getSelect2Language() {
591	var select2Language = {};
592	if ((typeof msbDefaultMessages !== 'undefined') && msbDefaultMessages.hasOwnProperty('select2')) {
593		if (msbDefaultMessages.select2.hasOwnProperty('inputTooShort')) {
594			var funcOverrideInputTooShort = function() {
595				var message = msbDefaultMessages.select2.inputTooShort;
596				return message.replace("{0}","2");
597			};
598		}
599		if (msbDefaultMessages.select2.hasOwnProperty('noResults')) {
600			var funcOverrideNoResults = function() {
601				return msbDefaultMessages.select2.noResults;
602			};
603		}
604
605		if ((typeof funcOverrideInputTooShort !== 'undefined') && (typeof funcOverrideNoResults !== 'undefined')) {
606			select2Language = {inputTooShort: funcOverrideInputTooShort, noResults: funcOverrideNoResults};
607		} else if ((typeof funcOverrideInputTooShort !== 'undefined')) {
608			select2Language = {inputTooShort: funcOverrideInputTooShort};
609		} else if ((typeof funcOverrideNoResults !== 'undefined')) {
610			select2Language = {noResults: funcOverrideNoResults};
611		}
612	}
613
614	select2Language = $.extend(true,select2Language,{loadingMore: function () { return null; }});
615	return select2Language;
616}
617
618function clearSelectToggleProps($sel, options){
619	if ($sel == null) {
620		console.error('utils.js:clearSelectToggleProps - sel is null')
621		return;
622	}
623	if (!($sel instanceof jQuery)) $sel = $("#" + $sel);
624	options = $.extend(true, {}, {text:'', value:'', enable:true, cursorClass:'cursor-not-allowed', select2:true, chosen:false}, options);
625	if (isTrue(options.chosen)) options.select2 = false;
626
627	$sel.empty().append($('<option></option>', {value:options.value, text:options.text}));
628	$sel.prop('disabled',(options.enable?false:true));
629
630	if (options.enable) {
631		if (options.select2) $sel.parent().find('.select2-container').removeClass(options.cursorClass);
632		else if (options.chosen) $sel.parent().find('.chosen-single').removeClass(options.cursorClass);
633		else $sel.removeClass(options.cursorClass);
634	} else {
635		if (options.select2)  $sel.parent().find('.select2-container').addClass(options.cursorClass);
636		else if (options.chosen) $sel.parent().find('.chosen-single').addClass(options.cursorClass);
637		else $sel.addClass(options.cursorClass);
638	}
639}
640
641
642function initDatatable($sel, settings) {
643	if ($sel == null) {
644		console.error('utils.js:datatable is null')
645		return;
646	}
647	if (!($sel instanceof jQuery))
648		$sel = $("#" + $sel);
649
650	var defaultSettings = {
651		dom: 'Blftipr',
652		"buttons": [
653				{
654								 text: '<i class="fas fa-sync"></i>',
655								 action: function ( e, dt, node, config ) {
656										 dt.ajax.reload();
657								 }
658						 },
659							 {
660									extend: 'colvis',
661									text: '<i class="fas fa-list fa-fw"></i>',
662									postfixButtons: ['colvisRestore'],
663									columns: ':not(.noVis)'
664							 },
665				],
666		"language": {
667				"lengthMenu": "_MENU_",
668				"processing": '<i class="fa fa-spinner fa-spin fa-3x fa-fw"></i><span class="processing">Loading...</span> ',
669		 	"search": '<i class="fas fa-search"></i>',
670		 	"searchPlaceholder": "Search",
671		 	"info": "Showing _START_ to _END_ of _TOTAL_",
672		 	"infoEmpty": ""
673		},
674		lengthMenu: [[50, 100, 500, -1], [50, 100, 500, "All"]],
675		order: [[ 1, "desc" ]],
676		stateSave: true,
677		serverSide: true,
678		processing: true,
679		ajax : {
680					url: 'placeholder',
681					contentType: 'application/json',
682					"type": "POST",
683						"data": function(data, settings) {
684							if (typeof getFormObjs  !== 'undefined' && $.isFunction(getFormObjs)) {
685								data = $.extend( true, {}, data, getFormObjs() );
686								if (typeof saveFilters  !== 'undefined' && $.isFunction(saveFilters)) {
687									saveFilters();
688								}
689							}
690				//console.log('setupChildTable.ajax request ',JSON.stringify(data));
691				return JSON.stringify(data);
692						},
693						cache: false,
694			"dataSrc": function ( json ) {
695				//console.log('setupChildTable.dataSrc json=',json);
696				if (json.hasOwnProperty('loginRequired') && isTrue(json.loginRequired)) {
697					window.location.reload(true);
698				}
699				if (json.hasOwnProperty('errors') && Array.isArray(json.errors) && json.errors.length) {
700					var out = (Array.isArray(json.errors)) ? json.errors.join('<br>') : json.errors;
701				 	$pageObjects.alerts.error.text.html(out);
702					$pageObjects.alerts.error.area.show();
703				}
704				if (json.hasOwnProperty('messages') && Array.isArray(json.messages) && json.messages.length) {
705					var out = (Array.isArray(json.messages)) ? json.messages.join('<br>') : json.messages;
706				 	$pageObjects.alerts.info.text.html(out);
707					$pageObjects.alerts.info.area.show();
708				}
709				/* Tell DataTables where the row data array is located.
710				 * We use an object so we can return info other than row data (such as errors, etc...)
711				 */
712				console.log(json);
713				return json.data;
714			}
715		},
716				fixedHeader: true,
717				columnDefs: [
718					{
719							defaultContent: '',
720							targets: '_all'
721					}
722			],
723				select: {
724						style: 'multi',
725						selector: '.select-checkbox',
726						items: 'row',
727				},
728				responsive: {
729					details: {
730						type: 'column',
731						target: 0
732					}
733				},
734
735				drawCallback: function(settings) {
736						var pagination = $(this).closest('.dataTables_wrapper').find('.dataTables_paginate');
737						pagination.toggle(this.api().page.info().pages > 1);
738				}
739	};
740
741	settings = $.extend(true, defaultSettings, settings);
742	$selDT = $sel.DataTable(settings);
743
744	$selDT.on('draw',function(){
745		$selDT.columns.adjust();
746	});
747
748	return $selDT;
749}
750
751function initDateTimePicker($dateInput, options) {
752	if ($dateInput == null) {
753		console.error('utils.js:initDateTimePicker - $dateInput is null');
754		return;
755	}
756	if (!($dateInput instanceof jQuery))
757		$dateInput = $("#" + $dateInput);
758
759	if ($dateInput.length) {
760		$dateInput.addClass("cursor-pointer");
761
762		/* onChangeCallback - can be a function to call or a callback object as follows:
763		 * {
764		 *   'callback': 'a function to call',
765		 *   'argument': 'argument for callback function'
766		 * }
767		 */
768		options = $.extend(true, {
769			format: 'MM/DD/YYYY',
770			viewMode: 'days', // 'days', 'months', 'years', 'decades'
771			showTodayButton: true,
772			showClear: true,
773			showClose: true,
774			useCurrent: false,
775			startDate: null,
776			defaultStartDate: null,
777			minDate: moment().subtract(2, 'years').startOf('year').toDate(),
778			maxDate: moment().endOf('day').toDate(),
779			widgetPositioning: { horizontal: 'auto', vertical: 'auto' },
780			onChangeCallback: {
781				'callback': null,
782				'argument': null
783			},
784			reset: {
785				element: '',
786				action: ''
787			}
788		}, options);
789
790		// Ensure that viewMode is valid
791		if (isEmpty(options.viewMode) || (options.viewMode != 'days' && options.viewMode != 'months' && options.viewMode != 'years' && options.viewMode != 'decades'))
792			options.viewMode = 'days';
793
794		// Convert all of the provided date values to instances of Date
795		options.startDate = convertToDate(options.startDate, options);
796		options.defaultStartDate = convertToDate(options.defaultStartDate, options);
797		options.minDate = convertToDate(options.minDate, options);
798		options.maxDate = convertToDate(options.maxDate, options);
799
800		// Replace any invalid dates to prevent errors when initializing datetimepicker
801		if (!isValidDate(options.startDate)) {
802			$dateInput.val('');
803			if (isValidDate(options.defaultStartDate)) {
804				options.startDate = options.defaultStartDate;
805			} else {
806				options.startDate = null;
807			}
808		}
809		if (!isValidDate(options.minDate))
810			options.minDate = moment().subtract(99, 'years').startOf('year').toDate();
811		if (!isValidDate(options.maxDate))
812			options.maxDate = moment().add(99, 'years').endOf('year').toDate();
813
814		$dateInput.datetimepicker({
815			format: options.format,
816			viewMode: options.viewMode,
817			showTodayButton: options.showTodayButton,
818			showClear: options.showClear,
819			showClose: options.showClose,
820			useCurrent: options.useCurrent,
821			defaultDate: options.startDate,
822			minDate: options.minDate,
823			maxDate: options.maxDate,
824			widgetPositioning: options.widgetPositioning,
825			icons: {
826				time: "far fa-clock",
827				date: "fas fa-calendar-alt"
828			},
829			tooltips: (typeof msbApp !== 'undefined') ? msbApp.getDateTimePickerTooltips() : {},
830			locale: (typeof msbApp !== 'undefined') ? msbApp.getMomentSupportedLocale() : 'en'
831		});
832		$dateInput.on("dp.change", function(e) {
833			if (options.onChangeCallback) {
834				if (typeof options.onChangeCallback == "function") {
835					return options.onChangeCallback(e);
836				} else if (options.onChangeCallback.hasOwnProperty("callback") && (typeof options.onChangeCallback.callback == "function")) {
837					return options.onChangeCallback.callback(options.onChangeCallback.argument || e);
838				}
839			}
840		});
841	}
842}
843
844function initStartEndDateTimePicker($startDate, $endDate, options) {
845	if ($startDate == null) {
846		console.error('admin/common.js:initStartEndDateTimePicker - $startDate is null');
847		return;
848	}
849	if ($endDate == null) {
850		console.error('admin/common.js:initStartEndDateTimePicker - $endDate is null');
851		return;
852	}
853
854	if (!($startDate instanceof jQuery))
855		$startDate = $("#" + $startDate);
856	if (!($endDate instanceof jQuery))
857		$endDate = $("#" + $endDate);
858
859	if ($startDate.length && $endDate.length) {
860		$startDate.addClass("cursor-pointer");
861		$endDate.addClass("cursor-pointer");
862
863		/* onChangeCallback - can be a function to call or a callback object as follows:
864		 * {
865		 *   'callback': 'a function to call',
866		 *   'argument': 'argument for callback function'
867		 * }
868		 */
869		options = $.extend(true, {
870			format:$("#pageValuesDatePickerFormat").val() ,
871			viewMode: 'days', // 'days', 'months', 'years', 'decades'
872			showTodayButton: true,
873			showClear: true,
874			showClose: true,
875			useCurrent: false,
876			startDate: null,
877			endDate: null,
878			defaultStartDate: null,
879			defaultEndDate: null,
880			minDate: moment().subtract(2, 'years').startOf('year').toDate(),
881			maxDate: moment().endOf('day').toDate(),
882			widgetPositioning: { horizontal: 'auto', vertical: 'auto' },
883			onChangeCallback: {
884				'callback': null,
885				'argument': null
886			},
887			reset: {
888				element: '',
889				action: ''
890			}
891		}, options);
892
893		//console.log('initStartEndDateTimePicker - options:',options)
894
895
896		// Ensure that viewMode is valid
897		if (isEmpty(options.viewMode) || (options.viewMode != 'days' && options.viewMode != 'months' && options.viewMode != 'years' && options.viewMode != 'decades'))
898			options.viewMode = 'days';
899
900		// Convert all of the provided date values to instances of Date
901		options.startDate = convertToDate(options.startDate, options);
902		options.endDate = convertToDate(options.endDate, options);
903		options.defaultStartDate = convertToDate(options.defaultStartDate, options);
904		options.defaultEndDate = convertToDate(options.defaultEndDate, options);
905		options.minDate = convertToDate(options.minDate, options);
906		options.maxDate = convertToDate(options.maxDate, options);
907
908		// Replace any invalid dates to prevent errors when initializing datetimepicker
909		if (!isValidDate(options.startDate)) {
910			$startDate.val('');
911			if (isValidDate(options.defaultStartDate)) {
912				options.startDate = options.defaultStartDate;
913			} else {
914				options.startDate = null;
915			}
916		}
917		if (!isValidDate(options.endDate)) {
918			$endDate.val('');
919			if (isValidDate(options.defaultEndDate)) {
920				options.endDate = options.defaultEndDate;
921			} else {
922				options.endDate = null;
923			}
924		}
925		if (!isValidDate(options.minDate))
926			options.minDate = moment().subtract(99, 'years').startOf('year').toDate();
927		if (!isValidDate(options.maxDate))
928			options.maxDate = moment().add(99, 'years').endOf('year').toDate();
929
930		$startDate.datetimepicker({
931			format: options.format,
932			viewMode: options.viewMode,
933			showTodayButton: options.showTodayButton,
934			showClear: options.showClear,
935			showClose: options.showClose,
936			useCurrent: options.useCurrent,
937			defaultDate: options.startDate,
938			minDate: options.minDate,
939			maxDate: (options.endDate) ? options.endDate : options.maxDate,
940			widgetPositioning: options.widgetPositioning,
941			icons: {
942				time: "far fa-clock",
943				date: "fas fa-calendar-alt"
944			},
945			tooltips: (typeof msbApp !== 'undefined') ? msbApp.getDateTimePickerTooltips() : {},
946			locale: (typeof msbApp !== 'undefined') ? msbApp.getMomentSupportedLocale() : 'en'
947		});
948		$endDate.datetimepicker({
949			format: options.format,
950			viewMode: options.viewMode,
951			showTodayButton: options.showTodayButton,
952			showClear: options.showClear,
953			showClose: options.showClose,
954			useCurrent: options.useCurrent,
955			defaultDate: options.endDate,
956			minDate: (options.startDate) ? options.startDate : options.minDate,
957			maxDate: options.maxDate,
958			widgetPositioning: options.widgetPositioning,
959			icons: {
960				time: "far fa-clock",
961				date: "fas fa-calendar-alt"
962			},
963			tooltips: (typeof msbApp !== 'undefined') ? msbApp.getDateTimePickerTooltips() : {},
964			locale: (typeof msbApp !== 'undefined') ? msbApp.getMomentSupportedLocale() : 'en'
965		});
966		$startDate.on("dp.change", function(e) {
967			if (e.date)
968				$endDate.data("DateTimePicker").minDate(e.date);
969			else if (options.minDate)
970				$endDate.data("DateTimePicker").minDate(options.minDate);
971
972			if (options.onChangeCallback) {
973				if (typeof options.onChangeCallback == "function") {
974					return options.onChangeCallback(e);
975				} else if (options.onChangeCallback.hasOwnProperty("callback") && (typeof options.onChangeCallback.callback == "function")) {
976					return options.onChangeCallback.callback(options.onChangeCallback.argument || e);
977				}
978			}
979		});
980		$endDate.on("dp.change", function(e) {
981			if (e.date)
982				$startDate.data("DateTimePicker").maxDate(e.date);
983			else if (options.maxDate)
984				$startDate.data("DateTimePicker").maxDate(options.maxDate);
985
986			if (options.onChangeCallback) {
987				if (typeof options.onChangeCallback == "function") {
988					return options.onChangeCallback(e);
989				} else if (options.onChangeCallback.hasOwnProperty("callback") && (typeof options.onChangeCallback.callback == "function")) {
990					return options.onChangeCallback.callback(options.onChangeCallback.argument || e);
991				}
992			}
993		});
994
995		if (options.reset.element != '' && options.reset.action != '') {
996			$reset = $("#" + options.reset.element);
997			if ($reset.length) {
998				$reset.on(options.reset.action, function() {
999					resetStartEndDateTimePicker($startDate, $endDate, options);
1000				});
1001			}
1002		}
1003	}
1004}
1005
1006function resetStartEndDateTimePicker($startDate, $endDate, options) {
1007	if ($startDate == null) {
1008		console.error('admin/common.js:resetStartEndDateTimePicker - $startDate is null');
1009		return;
1010	}
1011	if ($endDate == null) {
1012		console.error('admin/common.js:resetStartEndDateTimePicker - $endDate is null');
1013		return;
1014	}
1015
1016	if (!($startDate instanceof jQuery))
1017		$startDate = $("#" + $startDate);
1018	if (!($endDate instanceof jQuery))
1019		$endDate = $("#" + $endDate);
1020
1021	if ($startDate.length && $endDate.length) {
1022		options = $.extend(true, {
1023			defaultStartDate: null,
1024			defaultEndDate: null,
1025			minDate: moment().subtract(2, 'years').startOf('year').toDate(),
1026			maxDate: moment().endOf('day').toDate()
1027		}, options);
1028
1029		// Convert all of the provided date values to instances of Date
1030		options.defaultStartDate = convertToDate(options.defaultStartDate, options);
1031		options.defaultEndDate = convertToDate(options.defaultEndDate, options);
1032		options.minDate = convertToDate(options.minDate, options);
1033		options.maxDate = convertToDate(options.maxDate, options);
1034
1035		// Replace any invalid dates to prevent errors when initializing datetimepicker
1036		if (!isValidDate(options.defaultStartDate))
1037			options.defaultStartDate = null;
1038		if (!isValidDate(options.defaultEndDate))
1039			options.defaultEndDate = null;
1040		if (!isValidDate(options.minDate))
1041			options.minDate = moment().subtract(99, 'years').startOf('year').toDate();
1042		if (!isValidDate(options.maxDate))
1043			options.maxDate = moment().add(99, 'years').endOf('year').toDate();
1044
1045		// Reset the min and max dates before attempting to reset the dates
1046		$startDate.data("DateTimePicker").minDate(options.minDate);
1047		$startDate.data("DateTimePicker").maxDate((options.defaultEndDate) ? options.defaultEndDate : options.maxDate);
1048		$endDate.data("DateTimePicker").minDate((options.defaultStartDate) ? options.defaultStartDate : options.minDate);
1049		$endDate.data("DateTimePicker").maxDate(options.maxDate);
1050
1051		if (options.defaultStartDate) {
1052			$startDate.data("DateTimePicker").date(options.defaultStartDate);
1053		} else {
1054			$startDate.data("DateTimePicker").clear();
1055		}
1056
1057		if (options.defaultEndDate) {
1058			$endDate.data("DateTimePicker").date(options.defaultEndDate);
1059		} else {
1060			$endDate.data("DateTimePicker").clear();
1061		}
1062	}
1063}
1064
1065function showDateTimePicker(id) {
1066	$("#"+id).data("DateTimePicker").show();
1067}
1068
1069/* Update a URL string to Append or replace a parameter */
1070function appendUrlParam(url, param, value) {
1071	if (!url) throw new Error('Missing required parameter: url');
1072	var hash = {},
1073	parser = document.createElement('a');
1074	parser.href = url;
1075	var parameters = parser.search.split(/\?|&/);
1076
1077	for (var i=0; i < parameters.length; i++) {
1078			if (!parameters[i]) continue;
1079			var ary = parameters[i].split('=');
1080			hash[ary[0]] = ary[1];
1081	}
1082	hash[param] = value;
1083
1084	var list = [];
1085	Object.keys(hash).forEach(function (key) {
1086			list.push(key + '=' + hash[key]);
1087	});
1088
1089	parser.search = '?' + list.join('&');
1090	//console.log('appendUrlParam: url:'+parser.href);
1091	return msbApp.sanitizeHtml(parser.href);
1092}
1093/* Retrieve a URL parameter */
1094function getUrlParam(k){
1095	var p={};
1096	location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){p[k]=v})
1097	return k?p[k]:null;
1098}
1099
1100/* Bootstrap */
1101function getBootstrapVersion() {
1102	let result = 3;
1103	try {
1104		result = (window.bootstrap ? bootstrap.Tooltip.VERSION.split('.')[0] : result);
1105	} catch (exception) {
1106		result = 3;
1107	}
1108	return result;
1109}
1110function isBootstrap5() {
1111	return (getBootstrapVersion() == 5);
1112}
1113
1114function handleBoostrap5Validation(event) {
1115	if (this.checkValidity() === false) {
1116		event.preventDefault();
1117		event.stopPropagation();
1118	}
1119	this.classList.add('was-validated');
1120}
1121function initBootstrap5Validation() {
1122	if (!shouldDisableBootstrapValidator()) {
1123		// Fetch all the forms we want to apply custom Bootstrap validation styles to
1124		let forms = document.getElementsByClassName('needs-validation');
1125		// Loop over them and prevent submission
1126		Array.prototype.filter.call(forms, function(form) {
1127			form.addEventListener('submit', handleBoostrap5Validation, false);
1128		});
1129	}
1130}
1131function disableBootstrap5Validation() {
1132	let forms = document.getElementsByClassName('needs-validation');
1133	Array.prototype.filter.call(forms, function(form) {
1134		form.removeEventListener('submit', handleBoostrap5Validation, false);
1135	});
1136}
1137
1138function initBootstrapValidator($form, options) {
1139
1140	var datePickerFormat = "MM/DD/YYYY";
1141	if($("#pageValuesDatePickerFormat") && $("#pageValuesDatePickerFormat").val()) {
1142		datePickerFormat  = $("#pageValuesDatePickerFormat").val();
1143	}
1144	if ($form == null) {
1145		console.error('utils.js:initBootstrapValidator - form is null')
1146		return;
1147	}
1148	if (!($form instanceof jQuery))
1149		$form = $("#" + $form);
1150
1151	if ($form.length) {
1152		if (!shouldDisableBootstrapValidator()) {
1153			$form.validator('destroy');
1154
1155			const expDateEmptyOptionIndex = "-1";
1156
1157			var newOptions = $.extend(true, {
1158				feedback: {
1159					success: 'fa-check-circle',
1160					error: ''
1161				},
1162				custom: {
1163					match: function($el) {
1164						if ($el.is(":hidden"))
1165							return;
1166						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1167							return;
1168
1169						var matchFieldID = $el.attr('data-match');
1170						var $matchField = $(matchFieldID);
1171						if (!($matchField instanceof jQuery))
1172							$matchField = $("#"+matchFieldID);
1173						if (!$matchField.length)
1174							return;
1175
1176						return $el.val() !== $matchField.val();
1177					},
1178					nomatch: function($el) {
1179						if ($el.is(":hidden"))
1180							return;
1181						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1182							return;
1183
1184						var noMatchFieldID = $el.attr('data-nomatch');
1185						var $noMatchField = $(noMatchFieldID);
1186						if (!($noMatchField instanceof jQuery))
1187							$noMatchField = $("#"+noMatchFieldID);
1188						if (!$noMatchField.length)
1189							return;
1190
1191						return $el.val() === $noMatchField.val();
1192					},
1193					minlength: function($el) {
1194						if ($el.is(":hidden"))
1195							return;
1196						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1197							return;
1198
1199						var val = $el.val();
1200						if (isTrue($el.attr('data-minlength-squish')))
1201							val = squishString(val);
1202						var minlength = $el.attr('data-minlength');
1203						return val.length < minlength;
1204					},
1205					passwordpattern: function($el) {
1206						if ($el.is(":hidden"))
1207							return;
1208						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1209							return;
1210
1211						var val = $el.val();
1212						const re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()+=-\?;,./{}|\":<>\[\]\\\' ~_])/;
1213        				return !re.test(val)
1214					},
1215					passwordwhitespace: function($el) {
1216						if ($el.is(":hidden"))
1217							return;
1218						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1219							return;
1220
1221						var val = $el.val();
1222						if (val == null) {
1223							return;
1224						}
1225						return val.trim().length !== val.length;
1226					},
1227					maxlength: function($el) {
1228						if ($el.is(":hidden"))
1229							return;
1230						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1231							return;
1232
1233						var val = $el.val();
1234						if (isTrue($el.attr('data-maxlength-squish')))
1235							val = squishString(val);
1236						var maxlength = $el.attr('data-maxlength');
1237						return val.length > maxlength;
1238					},
1239					greaterthan: function($el) {
1240						if ($el.is(":hidden"))
1241							return;
1242						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1243							return;
1244
1245						var valueToBeGreaterThan = $el.attr('data-greaterthan');
1246						if (!isNumber(valueToBeGreaterThan) || !isNumber($el.val()))
1247							return;
1248
1249						return parseFloat($el.val()) <= parseFloat(valueToBeGreaterThan);
1250					},
1251					greaterthanorequalto: function($el) {
1252						if ($el.is(":hidden"))
1253							return;
1254						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1255							return;
1256
1257						var valueToBeGreaterThanOrEqualTo = $el.attr('data-greaterthanorequalto');
1258						if (!isNumber(valueToBeGreaterThanOrEqualTo) || !isNumber($el.val()))
1259							return;
1260
1261						return parseFloat($el.val()) < parseFloat(valueToBeGreaterThanOrEqualTo);
1262					},
1263					lessthan: function($el) {
1264						if ($el.is(":hidden"))
1265							return;
1266						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1267							return;
1268
1269						var valueToBeLessThan = $el.attr('data-lessthan');
1270						if (!isNumber(valueToBeLessThan) || !isNumber($el.val()))
1271							return;
1272
1273						return parseFloat($el.val()) >= parseFloat(valueToBeLessThan);
1274					},
1275					lessthanorequalto: function($el) {
1276						if ($el.is(":hidden"))
1277							return;
1278						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1279							return;
1280
1281						var valueToBeLessThanOrEqualTo = $el.attr('data-lessthanorequalto');
1282						if (!isNumber(valueToBeLessThanOrEqualTo) || !isNumber($el.val()))
1283							return;
1284
1285						return parseFloat($el.val()) > parseFloat(valueToBeLessThanOrEqualTo);
1286					},
1287					between: function($el) {
1288						if ($el.is(":hidden"))
1289							return;
1290						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1291							return;
1292
1293						var value = parseFloat($el.val());
1294						var range = $el.attr('data-between');
1295						var min = parseFloat(range.split('-')[0]);
1296						var max = parseFloat(range.split('-')[1]);
1297
1298						if (!isNumber(value) || !isNumber(min) || !isNumber(max))
1299							return;
1300
1301						//May have a minimum, but no maximum
1302						if (max > min && (value < min || value > max))
1303						{
1304							$el.attr('data-between-error', $el.attr('data-range-error'));
1305							return true;
1306						}
1307						else if (value < min)
1308						{
1309							$el.attr('data-between-error', $el.attr('data-min-error'));
1310							return true;
1311						}
1312
1313						return false;
1314					},
1315					numeric: function($el) {
1316						if ($el.is(":hidden"))
1317							return;
1318						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1319							return;
1320
1321						return !isNumber($el.val());
1322					},
1323					containsnumber: function($el) {
1324						if ($el.is(":hidden"))
1325							return;
1326						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1327							return;
1328
1329						return !containsNumber($el.val());
1330					},
1331					date: function($el) {
1332						if ($el.is(":hidden"))
1333							return;
1334						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1335							return;
1336
1337						return !moment($el.val(), datePickerFormat, true).isValid();
1338					},
1339					dateafter: function($el) {
1340						if ($el.is(":hidden"))
1341							return;
1342						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1343							return;
1344						if (($el.attr('data-date') || $el.attr('data-date') !== undefined) && !moment($el.val(), datePickerFormat, true).isValid())
1345							return;
1346
1347						var startDateFieldID = $el.attr('data-dateafter');
1348						var $startDateField = $(startDateFieldID);
1349						if (!($startDateField instanceof jQuery))
1350							$startDateField = $("#"+startDateFieldID);
1351						if (!$startDateField.length || $startDateField.val().length == 0 || !moment($startDateField.val(), datePickerFormat, true).isValid())
1352							return;
1353
1354						return !isValidDateRange($startDateField.val(), $el.val());
1355					},
1356					datebefore: function($el) {
1357						if ($el.is(":hidden"))
1358							return;
1359						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1360							return;
1361						if (($el.attr('data-date') || $el.attr('data-date') !== undefined) && !moment($el.val(), datePickerFormat, true).isValid())
1362							return;
1363
1364						var endDateFieldID = $el.attr('data-datebefore');
1365						var $endDateField = $(endDateFieldID);
1366						if (!($endDateField instanceof jQuery))
1367							$endDateField = $("#"+endDateFieldID);
1368						if (!$endDateField.length || $endDateField.val().length == 0 || !moment($endDateField.val(), datePickerFormat, true).isValid())
1369							return;
1370
1371						return !isValidDateRange($el.val(), $endDateField.val());
1372					},
1373					dualrequired: function($el) {
1374						if ($el.is(":hidden"))
1375							return;
1376						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1377							return;
1378
1379						var dualRequiredFieldID = $el.attr('data-dualrequired');
1380						var $dualRequiredField = $(dualRequiredFieldID);
1381						if (!($dualRequiredField instanceof jQuery))
1382							$dualRequiredField = $("#" + dualRequiredFieldID);
1383						if (!$dualRequiredField.length)
1384							return;
1385
1386						var notEmptyOptionRequired = !!$el.attr('notEmptyOptionRequired');
1387						var dualFieldNotEmptyOptionRequired = !!$dualRequiredField.attr('notEmptyOptionRequired');
1388
1389						return $el.val().length == 0 || $dualRequiredField.val().length == 0
1390							|| (notEmptyOptionRequired && $el.val() == expDateEmptyOptionIndex)
1391							|| (dualFieldNotEmptyOptionRequired && $dualRequiredField.val() == expDateEmptyOptionIndex);
1392					},
1393					routingnumber: function($el) {
1394						if ($el.is(":hidden"))
1395							return;
1396						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1397							return;
1398
1399						var value = $el.val();
1400						var previousValue = $el.attr('data-routingnumber');
1401
1402						if (value != previousValue) {
1403							if (!isValidRoutingNumber(value))
1404								return true;
1405						}
1406
1407						return;
1408					},
1409					checkingacctnumber: function($el) {
1410						if ($el.is(":hidden"))
1411							return;
1412						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1413							return;
1414
1415						var value = $el.val();
1416						var previousValue = $el.attr('data-checkingacctnumber');
1417
1418						if (value != previousValue) {
1419							var formattedValue = "";
1420							for (var i = 0; i < value.length; i++) {
1421								if (isNumber(value.charAt(i)))
1422									formattedValue = formattedValue + value.charAt(i);
1423							}
1424							return (formattedValue.length < 1 || formattedValue.length > 17);
1425						}
1426
1427						return;
1428					},
1429					creditcardlength: function($el) {
1430						if ($el.is(":hidden"))
1431							return;
1432						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1433							return;
1434
1435						var data = $el.attr('data-creditcardnumber').split(';');
1436						if (data.length < 2)
1437							return;
1438
1439						var cardTypeFieldID = data[0];
1440						var $cardTypeField = $(cardTypeFieldID);
1441						if (!($cardTypeField instanceof jQuery))
1442							$cardTypeField = $("#"+cardTypeFieldID);
1443						if (!$cardTypeField.length)
1444							return;
1445						var previousValue = data[1];
1446
1447						if ($el.val() != previousValue) {
1448							var cardType = getCardType($el, $cardTypeField);
1449							if (!isValidCreditCardLength($el, cardType))
1450								return true;
1451						}
1452						return;
1453					},
1454					creditcardcvvlength: function($el) {
1455						if ($el.is(":hidden"))
1456							return;
1457						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1458							return;
1459
1460						var cardType = 'invalid';
1461
1462						if (!isEmpty($el.attr('data-cardtypefield-id'))) {
1463							var cardTypeFieldID = $el.attr('data-cardtypefield-id');
1464							var $cardTypeField = $(cardTypeFieldID);
1465							if (!($cardTypeField instanceof jQuery))
1466								$cardTypeField = $("#"+cardTypeFieldID);
1467							if (!$cardTypeField.length)
1468								return;
1469
1470							cardType = $cardTypeField.val();
1471						} else {
1472							var cardSelectID = $el.attr('data-cardselect-id');
1473							var selectedOption = $(cardSelectID + ' option:selected').text().toLowerCase();
1474							if (selectedOption.indexOf('visa') >= 0)
1475								cardType = 'visa';
1476							else if (selectedOption.indexOf('mastercard') >= 0)
1477								cardType = 'mastercard';
1478							else if (selectedOption.indexOf('discover') >= 0)
1479								cardType = 'discover';
1480							else if (selectedOption.indexOf('american express') >= 0)
1481								cardType = 'amex';
1482						}
1483
1484
1485						if (!isValidCvvLength($el, cardType))
1486							return true;
1487
1488						return;
1489					},
1490					creditcardnumber: function($el) {
1491						if ($el.is(":hidden"))
1492							return;
1493						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1494							return;
1495
1496						var data = $el.attr('data-creditcardnumber').split(';');
1497						if (data.length < 2)
1498							return;
1499
1500						var cardTypeFieldID = data[0];
1501						var $cardTypeField = $(cardTypeFieldID);
1502						if (!($cardTypeField instanceof jQuery))
1503							$cardTypeField = $("#"+cardTypeFieldID);
1504						if (!$cardTypeField.length)
1505							return;
1506
1507						var cardType = getCardType($el, $cardTypeField);
1508						var cardTypeMap = JSON.parse($el.attr('data-cardtypemap'));
1509						var acceptedCardTypes = $el.attr('data-acceptedcardtypes');
1510
1511						// Don't throw an error here, because it will be a different error message - will handle in another validation
1512						if (isEmpty(cardType) || (!isEmpty(acceptedCardTypes) && JSON.stringify(acceptedCardTypes).indexOf(cardType) < 0) || !isValidCreditCardLength($el, cardType))
1513							return;
1514
1515						var value = $el.val();
1516						var previousValue = data[1];
1517
1518						if (value != previousValue) {
1519							// Code translated from BaseActionSupport.isValidCreditCardNumber()
1520							if (!isValidCreditCardNumber(value))
1521								return true;
1522
1523							if (cardType == "visa" && value.indexOf("4") != 0)
1524								return true;
1525							else if (cardType == "mastercard" && (value.indexOf("5") != 0 && value.indexOf("2") != 0))
1526								return true;
1527							else if (cardType == "amex" && value.indexOf("3") != 0)
1528								return true;
1529							else if (cardType == "discover" && value.indexOf("6") != 0)
1530								return true;
1531						}
1532
1533						return;
1534					},
1535					acceptedcardtypes: function($el) {
1536						if ($el.is(":hidden"))
1537							return;
1538						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1539							return;
1540
1541						var data = $el.attr('data-creditcardnumber').split(';');
1542						if (data.length < 2)
1543							return;
1544
1545						var cardTypeFieldID = data[0];
1546						var $cardTypeField = $(cardTypeFieldID);
1547						if (!($cardTypeField instanceof jQuery))
1548							$cardTypeField = $("#"+cardTypeFieldID);
1549						if (!$cardTypeField.length)
1550							return;
1551
1552						var acceptedCardTypes = $el.attr('data-acceptedcardtypes');
1553						if (!acceptedCardTypes.length)
1554							return;
1555
1556						var cardType = getCardType($el, $cardTypeField);
1557						if (isEmpty(cardType))
1558							return;
1559
1560						if (acceptedCardTypes.indexOf(cardType) < 0)
1561						{
1562							var cardTypeMap = JSON.parse($el.attr('data-cardtypemap'));
1563							var cardTypeMapError = $el.attr('data-acceptedcardtypeserror-text');
1564
1565							if (!isEmpty(cardTypeMap) && !isEmpty(cardTypeMapError) && !isEmpty(cardTypeMap[cardType]))
1566								$el.attr('data-acceptedcardtypes-error', cardTypeMapError.replace('{0}', cardTypeMap[cardType]));
1567							else
1568								$el.attr('data-acceptedcardtypes-error', $el.attr('data-creditcardnumber-error'));
1569
1570							return true;
1571						}
1572
1573						return;
1574					},
1575					paymentmethods_expirationdatemonth: function($el) {
1576						if ($el.is(":hidden"))
1577							return;
1578
1579						var expYearFieldID = $el.attr('data-paymentmethods_expirationdatemonth');
1580						var $expYearField = $(expYearFieldID);
1581						if (!($expYearField instanceof jQuery))
1582							$expYearField = $("#" + expYearFieldID);
1583						if (!$expYearField.length)
1584							return;
1585
1586						var notEmptyOptionRequired = $el.attr('notEmptyOptionRequired');
1587						var yearFieldNotEmptyOptionRequired = $expYearField.attr('notEmptyOptionRequired');
1588						var selectedMonth = $el.val();
1589						var selectedYear = $expYearField.val();
1590						if (selectedMonth.length == 0 || selectedYear.length == 0
1591							|| (yearFieldNotEmptyOptionRequired && $expYearField.val() == expDateEmptyOptionIndex)
1592							|| (notEmptyOptionRequired && $el.val() == expDateEmptyOptionIndex)) {
1593							return;
1594						}
1595
1596						if (isNumber(selectedMonth) && isNumber(selectedYear)) {
1597							var date = new Date();
1598							return selectedYear < date.getFullYear() || (selectedYear == date.getFullYear() && selectedMonth < date.getMonth());
1599						}
1600
1601						return;
1602					},
1603					paymentmethods_expirationdateyear: function($el) {
1604						if ($el.is(":hidden"))
1605							return;
1606
1607						var expMonthFieldID = $el.attr('data-paymentmethods_expirationdateyear');
1608						var $expMonthField = $(expMonthFieldID);
1609						if (!($expMonthField instanceof jQuery))
1610							$expMonthfield = $("#" + expMonthFieldID);
1611
1612						if (!$expMonthField.length)
1613							return;
1614
1615						var notEmptyOptionRequired = $el.attr('notEmptyOptionRequired');
1616						var monthFieldNotEmptyOptionRequired = $expMonthField.attr('notEmptyOptionRequired');
1617						var selectedYear = $el.val();
1618						var selectedMonth = $expMonthField.val();
1619						if (selectedYear.length == 0 || selectedMonth.length == 0
1620							|| (monthFieldNotEmptyOptionRequired && $expMonthField.val() == expDateEmptyOptionIndex)
1621							|| (notEmptyOptionRequired && selectedYear == expDateEmptyOptionIndex)) {
1622							return;
1623						}
1624
1625						if (isNumber(selectedYear) && isNumber(selectedMonth)) {
1626							var date = new Date();
1627							return selectedYear < date.getFullYear() || (selectedYear == date.getFullYear() && selectedMonth < date.getMonth());
1628						}
1629
1630						return;
1631					},
1632					editautopaysettings_lowbalance: function($el) {
1633						if ($el.is(":hidden"))
1634							return;
1635
1636						var name = $el.attr('data-editautopaysettings_lowbalance');
1637						var $fieldToCheck = $("input[name='"+name+"']:checked");
1638						if (!$fieldToCheck.length || $fieldToCheck.is(":hidden"))
1639							return;
1640
1641						var lowBalanceThreshold = $fieldToCheck.val();
1642						if (lowBalanceThreshold == "other") {
1643							$fieldToCheck = $("input[name='OTHER_"+name+"']");
1644							if ($fieldToCheck.val().length == 0 || !isNumber($fieldToCheck.val()))
1645								return;
1646							lowBalanceThreshold = $fieldToCheck.val();
1647
1648							var maxLowBalanceThreshold = $fieldToCheck.attr("data-lessthanorequalto");
1649							if (maxLowBalanceThreshold !== undefined && maxLowBalanceThreshold != null && isNumber(maxLowBalanceThreshold) && isNumber(lowBalanceThreshold) && (lowBalanceThreshold * 1 > maxLowBalanceThreshold * 1))
1650								return;
1651						}
1652
1653						if ($el.is("[type='radio']") && !$el.is(":checked"))
1654							$el = $("input[name='"+$el.attr("name")+"']:checked");
1655						if ($el.val() == 0)
1656							return;
1657
1658						var paymentAmount = $el.val();
1659						if (paymentAmount == "other") {
1660							$el = $("input[name='OTHER_"+$el.attr("name")+"']")
1661							if ($el.val().length == 0 || !isNumber($el.val()))
1662								return;
1663							paymentAmount = $el.val();
1664						}
1665
1666						if (!isNumber(lowBalanceThreshold) || !isNumber(paymentAmount))
1667							return;
1668
1669						lowBalanceThreshold = lowBalanceThreshold * 2;
1670						if (paymentAmount < lowBalanceThreshold) {
1671							var fieldName = $el.attr("name");
1672							if (fieldName.indexOf("OTHER_") == 0)
1673								fieldName = fieldName.substring(6);
1674
1675							$("input[name*='"+fieldName+"']").each(function() {
1676								var $this = $(this);
1677								var dataValidate = $this.attr("data-validate");
1678								if (!dataValidate || dataValidate === undefined || dataValidate != "false")
1679									$this.attr('data-editautopaysettings_lowbalance-error',
1680											pageValues.messages.invalidLowBalancePayment + " " + pageValues.currencySymbol + lowBalanceThreshold.toFixed(2));
1681							});
1682
1683							return true;
1684						}
1685
1686						return;
1687					},
1688					phonenumber: function($el) {
1689						if ($el.is(":hidden"))
1690							return;
1691
1692						var val = $el.val().replace(/\D/g,'')
1693						var isRequired = ($el.attr('required') !== undefined);
1694						return ((isRequired || val.length > 0) && val.length != 10);
1695					},
1696					editlowbalancesettings_lowbalance: function($el) {
1697						if ($el.is(":hidden"))
1698							return;
1699
1700						var paymentAmount = $el.val();
1701						if (paymentAmount.length == 0 || !isNumber(paymentAmount))
1702							return;
1703
1704						var lowBalanceFieldID = $el.attr('data-editlowbalancesettings_lowbalance');
1705						var $lowBalanceField = $(lowBalanceFieldID);
1706						if (!($lowBalanceField instanceof jQuery))
1707							$lowBalanceField = $("#"+lowBalanceFieldID);
1708						if (!$lowBalanceField.length || $lowBalanceField.is(":hidden"))
1709							return;
1710
1711						var lowBalanceThreshold = $lowBalanceField.val();
1712						if (lowBalanceThreshold.length == 0 || !isNumber(lowBalanceThreshold))
1713							return;
1714
1715						lowBalanceThreshold = lowBalanceThreshold * 2;
1716						if (paymentAmount < lowBalanceThreshold) {
1717							$el.attr('data-editlowbalancesettings_lowbalance-error',
1718									pageValues.messages.invalidLowBalancePayment + " " + pageValues.currencySymbol + lowBalanceThreshold.toFixed(2));
1719							return true;
1720						}
1721
1722						return;
1723					},
1724					emailaddress: function($el) {
1725						if ($el.is(":hidden"))
1726							return;
1727						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1728							return;
1729
1730						return !performEmailRegex($el.val());
1731					},
1732					singleemailaddress: function($el) {
1733						if ($el.is(":hidden"))
1734						return;
1735						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1736						return;
1737
1738						return !isValidEmail($el.val());
1739					},
1740					username: function($el) {
1741						if ($el.is(":hidden"))
1742							return;
1743						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1744							return;
1745
1746						return $el.val().indexOf(';') >= 0;
1747					},
1748					unlimitedornumeric: function($el) {
1749						if ($el.is(":hidden"))
1750							return;
1751						if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0)
1752							return;
1753
1754						return (!isNumber($el.val()) && $el.val().toUpperCase() != "UNL");
1755					},
1756					multicheckbox_range: ValidatorClass.create(function (genSuperFn, createClass) {
1757							var _super = genSuperFn(MultiCheckboxRange);
1758
1759							function MultiCheckboxRange(el) {
1760								var _this = _super.call(this, el);
1761								_this.canShowError = false;
1762								var rangeArr = el.getAttribute('data-multicheckbox_range').split(':');
1763								_this.min = normalizeMinMax(rangeArr[0]);
1764								_this.max = normalizeMinMax(rangeArr[1]);
1765								return _this;
1766							}
1767
1768							function normalizeMinMax(input) {
1769								var value = Number(input) || 0;
1770								return value >= 0 ? value : 0;
1771							}
1772
1773							return createClass(MultiCheckboxRange, {
1774								validate: function (el, stateless) {
1775									if ($(el).is(':hidden')) return;
1776									if (stateless) this.canShowError = true;
1777
1778									var numChecked = 0;
1779									$('input[type="checkbox"]', el).each(function () {
1780										if ($(this).is(':checked')) numChecked++;
1781									});
1782
1783									var passes =
1784										(this.min <= 0 || numChecked >= this.min) &&
1785										(this.max <= 0 || numChecked <= this.max);
1786									this.canShowError = this.canShowError || passes;
1787									return this.canShowError ? !passes : false;
1788								},
1789							});
1790						})
1791				}
1792			}, options);
1793
1794			$form.validator(newOptions);
1795
1796			// Initialize required checkbox groups
1797			$("input.required-check-group").click(function() {
1798				var name = $(this).attr('name');
1799				if (!name || name === undefined || isEmpty(name))
1800					return;
1801
1802				var checked = false;
1803				$('[name="' + name + '"]').each(function(){
1804					checked = $(this).is(':checked');
1805					if (checked)
1806						return false; // Break the loop
1807				});
1808
1809				$('[name="' + name + '"]').each(function(){
1810					$(this).attr('data-validate', !checked);
1811				});
1812
1813				updateBootstrapValidator($form);
1814			});
1815
1816			$("input.required-check-group").each(function() {
1817				var name = $(this).attr('name');
1818				if (!name || name === undefined || isEmpty(name))
1819					return;
1820
1821				var checked = false;
1822				$('[name="' + name + '"]').each(function(){
1823					checked = $(this).is(':checked');
1824					if (checked)
1825						return false; // Break the loop
1826				});
1827
1828				$('[name="' + name + '"]').each(function(){
1829					$(this).attr('data-validate', !checked);
1830				});
1831			});
1832			updateBootstrapValidator($form);
1833
1834			function normalizeMinMax(input) {
1835				var value = Number(input) || 0;
1836				return value >= 0 ? value : 0;
1837			}
1838
1839			$('[data-multicheckbox_range]').on('change', 'input[type="checkbox"]', function(e) {
1840				$(this).closest('[data-multicheckbox_range]').trigger('change.bs.validator');
1841			});
1842		}
1843		else {
1844			$form.attr('novalidate', true) // Disable automatic native validation
1845		}
1846	}
1847}
1848
1849function updateBootstrapValidator($form) {
1850	if ($form == null) {
1851		console.error('utils.js:updateBootstrapValidator - form is null')
1852		return;
1853	}
1854	if (!($form instanceof jQuery))
1855		$form = $("#" + $form);
1856
1857	if ($form.length) {
1858		if (!shouldDisableBootstrapValidator()) {
1859			$form.validator('update');
1860		}
1861		else {
1862			$form.attr('novalidate', true) // Disable automatic native validation
1863		}
1864	}
1865}
1866
1867function validateBootstrapValidator($form, $el, focusError) {
1868	if ($form == null) {
1869		console.error('utils.js:validateBootstrapValidator - form is null')
1870		return;
1871	}
1872	if (!($form instanceof jQuery))
1873		$form = $("#" + $form);
1874
1875	if ($form.length) {
1876		if (!shouldDisableBootstrapValidator()) {
1877			if ($el != null && ($el || $el !== undefined)) {
1878				if (!($el instanceof jQuery))
1879					$el = $("#" + $el);
1880
1881				if ($el.length) {
1882					$form.validator('validate', $el, focusError);
1883				} else {
1884					$form.validator('validate', null, focusError);
1885				}
1886			} else {
1887				$form.validator('validate', null, focusError);
1888			}
1889		}
1890		else {
1891			$form.attr('novalidate', true) // Disable automatic native validation
1892		}
1893	}
1894}
1895
1896function destroyBootstrapValidator($form) {
1897	if ($form == null) {
1898		console.error('utils.js:destroyBootstrapValidator - form is null')
1899		return;
1900	}
1901	if (!($form instanceof jQuery))
1902		$form = $("#" + $form);
1903
1904	if ($form.length) {
1905		if (!shouldDisableBootstrapValidator()) {
1906			$form.validator('destroy');
1907		}
1908		else {
1909			$form.attr('novalidate', true) // Disable automatic native validation
1910		}
1911	}
1912}
1913
1914function shouldDisableBootstrapValidator() {
1915	if ($("#sessionNoValidate").length || window.location.href.indexOf("novalidate") != -1)
1916		return true;
1917
1918	try {
1919		if (typeof developerOptions !== 'undefined') {
1920			if ('enableValidator' in developerOptions) {
1921				return !developerOptions.enableValidator;
1922			}
1923		}
1924
1925		if (typeof getDeveloperOptions !== 'undefined') {
1926			if (!getDeveloperOptions().enableValidator)
1927				return true;
1928		}
1929
1930		if (window.msbDevOptions) {
1931			return !msbDevOptions.validatorIsEnabled();
1932		}
1933	} catch (exception) {}
1934
1935	return false;
1936}
1937
1938function performEmailRegex(email) {
1939	//Regex translated from BaseActionSupport.validateEmailAddress
1940	var emailValidator = /\S+@\S+\.\S+/;
1941	return emailValidator.test(email);
1942}
1943
1944/**
1945 * Tests if a given string is a valid email. This function is not intended to
1946 * validate a string which may contain one or more emails. Use a different
1947 * function for that.
1948 *
1949 * @param {string} email
1950 * @returns true is given string is an email, else false
1951 */
1952var isValidEmail;
1953isValidEmail = (function() {
1954	const singleEmailRegex =/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/;
1955	return function (email) {
1956		if (typeof email === 'string') {
1957			return singleEmailRegex.test(email.toLowerCase());
1958		}
1959		return false;
1960	}
1961})();
1962
1963function isValidRoutingNumber(routingNumber) {
1964	// Code translated from CustomValidator.isBankRoutingNumber()
1965	if (routingNumber.length != 9)
1966		return false;
1967
1968	var multiples = [3, 7, 1];
1969	var sum = 0;
1970	var counter = 0;
1971
1972	for (var i = 0; i < routingNumber.length; i++) {
1973		if (!isNumber(routingNumber.charAt(i)))
1974			return false;
1975
1976		var numericDigit = parseInt(routingNumber.charAt(i), 10);
1977		sum = sum + (numericDigit * multiples[counter]);
1978		counter = (counter + 1) % 3;
1979	}
1980
1981	if (sum == 0 || sum % 10 != 0)
1982		return false;
1983
1984	return true;
1985}
1986
1987function isValidCreditCardNumber(cardNumber) {
1988	// Code translated from org.apache.commons.validator.routines.CreditCardValidator.isValid()
1989	var formattedCardNumber = "";
1990	for (var i = 0; i < cardNumber.length; i++) {
1991		if (isNumber(cardNumber.charAt(i)))
1992			formattedCardNumber = formattedCardNumber + cardNumber.charAt(i);
1993	}
1994
1995	if (isEmpty(formattedCardNumber))
1996		return false;
1997
1998	if (!performRegexCheck(formattedCardNumber))
1999		return false;
2000
2001	if (!performLuhnCheck(formattedCardNumber))
2002		return false;
2003
2004	return true;
2005}
2006
2007function getCardType($el, $cardTypeField) {
2008	// if the card type field already has a value, default to that first
2009	var cardType = $cardTypeField.val();
2010	var value = $el.val();
2011
2012	// If the page does not allow card type to change, then return the value we found
2013	if (typeof pageValues !== 'undefined' && typeof pageValues.canSetCardType !== 'undefined' && !isTrue(pageValues.canSetCardType))
2014		return cardType;
2015
2016	if (value.length > 0)
2017	{
2018		// if we're changing the card number set a dummy value
2019		if (value.indexOf("*") < 0)
2020			cardType = 'invalid';
2021
2022		if (value.indexOf("4") == 0)
2023			cardType = "visa";
2024		else if (value.indexOf("5") == 0 || value.indexOf("2") == 0)
2025			cardType = "mastercard";
2026		else if (value.indexOf("3") == 0)
2027			cardType = "amex";
2028		else if (value.indexOf("6") == 0)
2029			cardType = "discover";
2030	}
2031	else
2032		cardType = 'invalid';
2033
2034	return cardType;
2035}
2036
2037function isValidCreditCardLength($el, cardType) {
2038	var value = $el.val();
2039	if (cardType == 'visa' && value.length != 13 && value.length < 16)
2040		return false;
2041	else if ((cardType == 'mastercard' || cardType == 'discover') && value.length < 16)
2042		return false;
2043	else if (cardType == 'amex' && value.length < 15)
2044		return false;
2045	return true;
2046}
2047
2048function isValidCvvLength($el, cardType) {
2049	var value = $el.val();
2050	var cvvError = $el.attr('data-creditcardcvvlength-text');
2051	if ('mastercard|visa|discover'.indexOf(cardType) >= 0 && value.length != 3) {
2052		$el.attr('data-creditcardcvvlength-error', cvvError.replace('{0}', '3'));
2053		return false;
2054	}
2055	else if (cardType == 'amex' && value.length != 4) {
2056		$el.attr('data-creditcardcvvlength-error', cvvError.replace('{0}', '4'));
2057		return false;
2058	}
2059	return true;
2060}
2061
2062function setCardIcon($el, cardType) {
2063	if (typeof pageValues !== 'undefined' && typeof pageValues.canSetCardType !== 'undefined' && !isTrue(pageValues.canSetCardType))
2064		return;
2065
2066	var cardIconID = $el.attr('data-creditcardiconid');
2067	if (isEmpty(cardIconID))
2068		return;
2069
2070	var $cardIcon = $(cardIconID);
2071	if (!($cardIcon instanceof jQuery))
2072		$cardIcon = $("#"+cardIconID);
2073	if (!$cardIcon.length)
2074		return;
2075
2076	var iconClass = 'far fa-credit-card';
2077
2078	if (!isEmpty(cardType) && cardType != 'invalid')
2079		iconClass = 'fab fa-cc-' + cardType;
2080
2081	$cardIcon.attr('class', iconClass);
2082}
2083
2084function setCardType($el)
2085{
2086	if (typeof pageValues !== 'undefined' && typeof pageValues.canSetCardType !== 'undefined' && !isTrue(pageValues.canSetCardType))
2087		return;
2088
2089	var data = $el.attr('data-creditcardnumber').split(';');
2090	if (data.length < 2)
2091		return;
2092
2093	var cardTypeFieldID = data[0];
2094	var $cardTypeField = $(cardTypeFieldID);
2095	if (!($cardTypeField instanceof jQuery))
2096		$cardTypeField = $("#"+cardTypeFieldID);
2097	if (!$cardTypeField.length)
2098		return;
2099
2100	// Since we are no longer using a select, set the hidden value manually
2101	var cardType = getCardType($el, $cardTypeField);
2102
2103	setCardIcon($el, cardType);
2104	$cardTypeField.val(cardType);
2105}
2106
2107function performRegexCheck(cardNumber) {
2108	// Code translated from org.apache.commons.validator.routines.CreditCardValidator
2109	if (/^(3[47]\d{13})$/.test(cardNumber)) // American Express
2110		return true;
2111	else if (/^(6011\d{12})$/.test(cardNumber) || /^(64[4-9]\d{13})$/.test(cardNumber) || /^(65\d{14})$/.test(cardNumber)) // Discover
2112		return true;
2113	else if (/^(5[1-5]\d{14})$/.test(cardNumber) || /^(2221\d{12})$/.test(cardNumber) || /^(222[2-9]\d{12})$/.test(cardNumber)
2114			|| /^(22[3-9]\d{13})$/.test(cardNumber) || /^(2[3-6]\d{14})$/.test(cardNumber) || /^(27[01]\d{13})$/.test(cardNumber)
2115			|| /^(2720\d{12})$/.test(cardNumber)) // Mastercard
2116		return true;
2117	else if (/^(4)(\d{12}|\d{15})$/.test(cardNumber)) // Visa
2118		return true;
2119	else
2120		return false;
2121}
2122
2123function performLuhnCheck(cardNumber) {
2124	// Code translated from org.apache.commons.validator.routines.checkdigit.LuhnCheckDigit.isValid()
2125	if (isEmpty(cardNumber))
2126		return false;
2127
2128	var positionWeight = [2, 1];
2129	var total = 0;
2130
2131	for (var i = 0; i < cardNumber.length; i++) {
2132		if (!isNumber(cardNumber.charAt(i)))
2133			return false;
2134
2135		var numericDigit = parseInt(cardNumber.charAt(i), 10);
2136		var weightedValue = numericDigit * positionWeight[(cardNumber.length - i) % 2];
2137		if (weightedValue > 9)
2138			weightedValue = weightedValue - 9;
2139		total = total + weightedValue;
2140	}
2141
2142	if (total == 0 || total % 10 != 0)
2143		return false;
2144
2145	return true;
2146}
2147
2148
2149function isValidDateRange(startDate, endDate, formatMask){
2150
2151	if(!formatMask || formatMask === undefined)
2152		formatMask = $("#pageValuesDatePickerFormat").val();
2153
2154	var start = moment(startDate, formatMask, true)
2155	var end = moment(endDate, formatMask, true)
2156
2157	if (start.isValid() && end.isValid())
2158		return end.diff(start) >= 0;
2159	else
2160		return false;
2161}
2162
2163function initRetypeFieldValidator($form, $field, $retypeField, showFeedback) {
2164	if (!shouldDisableBootstrapValidator()) {
2165		if (!($form instanceof jQuery))
2166			$form = $("#" + $form);
2167		if (!($field instanceof jQuery))
2168			$field = $("#" + $field);
2169		if (!($retypeField instanceof jQuery))
2170			$retypeField = $("#" + $retypeField);
2171
2172		$field.on('change keyup blur', function(e) {
2173			if (!isEmpty($retypeField.val())) {
2174				$retypeField.change();
2175			}
2176		});
2177
2178		$retypeField.on('change keyup blur', function(e) {
2179			validateBootstrapValidator($form, $retypeField, false);
2180
2181			if ($retypeField.is(":visible")) {
2182				if (isTrue(showFeedback)) {
2183					if (!isEmpty($retypeField.val()) && $retypeField.val() === $field.val()) {
2184						$field.closest('.form-group').addClass('has-feedback');
2185						$field.siblings('.form-control-feedback').show();
2186					} else {
2187						$field.closest('.form-group').removeClass('has-feedback has-success');
2188						$field.siblings('.form-control-feedback').hide();
2189					}
2190
2191					updateBootstrapValidator($form);
2192					validateBootstrapValidator($form, $field, false);
2193				}
2194			}
2195		});
2196	}
2197}
2198function initNoMatchFieldValidator($form, $field, $noMatchField) {
2199	if (!shouldDisableBootstrapValidator()) {
2200		if (!($form instanceof jQuery))
2201			$form = $("#" + $form);
2202		if (!($field instanceof jQuery))
2203			$field = $("#" + $field);
2204		if (!($noMatchField instanceof jQuery))
2205			$noMatchField = $("#" + $noMatchField);
2206
2207		$field.on('change keyup blur', function(e) {
2208			if (!isEmpty($noMatchField.val())) {
2209				$noMatchField.change();
2210			}
2211		});
2212
2213		$noMatchField.on('change keyup blur', function(e) {
2214			validateBootstrapValidator($form, $noMatchField, false);
2215		});
2216	}
2217}
2218function toggleFieldError(selector, error) {
2219	if (isEmpty(selector)) return false;
2220	if (typeof error === 'undefined') error = '';
2221
2222	if ($.isArray(selector)) {
2223		$.each(selector, function(idx, val) {
2224			toggleFieldError(val); // Recursively call toggleFieldError
2225		});
2226	} else {
2227		var $field = (!(selector instanceof jQuery)) ? $(selector) : selector;
2228		$field.each(function(idx) {
2229			var $this = $(this), $frmGroup = $this.closest(".form-group");
2230
2231			if ($frmGroup.hasClass("has-feedback")) {
2232				$frmGroup.data("hasFeedback", true);
2233			}
2234
2235			$frmGroup.removeClass("has-error has-success");
2236			if (!isEmpty(error)) {
2237				$frmGroup.removeClass("has-feedback").addClass("has-error").find(".help-block").html(error);
2238			} else {
2239				$frmGroup.find(".help-block").empty();
2240				if (isTrue($frmGroup.data("hasFeedback"))) {
2241					$frmGroup.addClass("has-feedback")
2242					$this.siblings('.form-control-feedback').show();
2243				} else {
2244					$this.siblings('.form-control-feedback').hide();
2245				}
2246			}
2247		});
2248	}
2249}
2250
2251function getBrowserLocale() {
2252	return (navigator.languages && navigator.languages.length)?navigator.languages[0]:navigator.language;
2253}
2254
2255var $memObj = $("<div></div>");
2256function htmlEncode(value) {
2257	if (isEmpty(value)) {
2258		return '';
2259	} else if ($.isArray(value)) {
2260		$.each(value, function(idx, val) {
2261			value[idx] = htmlEncode(val); // Recursively call encode
2262		});
2263		return value;
2264	} else if ($.type(value) === "string") {
2265		return $memObj.empty().text(value).html();
2266	} else {
2267		// If it's not an array or string don't corrupt it!
2268		return value;
2269	}
2270}
2271function htmlDecode(value) {
2272	if (isEmpty(value)) {
2273		return '';
2274	} else if ($.isArray(value)) {
2275		$.each(value, function(idx, val) {
2276			value[idx] = htmlDecode(val); // Recursively call decode
2277		});
2278		return value;
2279	} else if ($.type(value) === "string") {
2280		/* First attempt to use DOMParser if it is supported by the browser.
2281		 * innerHTML approach can execute script when value is img tag with
2282		 * onerror attribute, for example: <img src="xss" onerror="alert(1)">
2283		 * NOTE: This will also strip leading whitespace */
2284		try {
2285			var doc = new DOMParser().parseFromString(value, 'text/html');
2286			return (doc.body.textContent || "");
2287		} catch (exception) {}
2288
2289		/* If browser does not support DOMParser, fall back to innerHTML approach.
2290		 * jQuery will parse and execute script when using html() (at least in IE).
2291		 * Use the safer alternative [innerHTML] */
2292		$memObj.empty();
2293		try { $memObj[0].innerHTML = value; } catch (exception) {}
2294		return $memObj.text();
2295	} else {
2296		// If it's not an array or string don't corrupt it!
2297		return value;
2298	}
2299}
2300
2301function stripHtml(value) {
2302	/* This method was the same as htmlDecode. It has been maintained to
2303	 * avoid breaking any existing references, but its functionality has
2304	 * been forwarded along to htmlDecode. [ECoggin, 8/1/19] */
2305	return htmlDecode(value);
2306}
2307
2308function squishString(val){
2309	val = val.trim();
2310	return val.replace(/  +/g, ' ');
2311}
2312
2313function escapeUnsafeCharacters(val) {
2314	if (isEmpty(val))
2315		return "";
2316	val = val.replace(/[`']/g, "&#39;");
2317	val = val.replace(/=/g, "&#61;");
2318	return val;
2319}
2320
2321/* Creates and returns a new debounced version of the passed function that will postpone
2322 * its execution until after wait milliseconds have elapsed
2323 */
2324function debounce(fn, delay) {
2325	var timer = null;
2326	return function () {
2327		var context = this, args = arguments;
2328		clearTimeout(timer);
2329		timer = setTimeout(function () {
2330			fn.apply(context, args);
2331		}, delay);
2332	};
2333}
2334function isElementInScroll($elem) {
2335	if ((typeof $elem === 'undefined') || $elem == null) {
2336		return false;
2337	}
2338
2339	if (!($elem instanceof jQuery))
2340		$elem = $("#" + $elem);
2341
2342	if ($elem.length) {
2343		var $win = $(window),
2344			docViewTop = $win.scrollTop(),
2345			docViewBottom = docViewTop + $win.height(),
2346			elemTop = $elem.offset().top,
2347			elemBottom = elemTop + $elem.height();
2348		/*
2349		console.log('docViewTop: '+docViewTop);
2350		console.log('docViewBottom: '+docViewBottom);
2351		console.log('elemTop: '+elemTop);
2352		console.log('elemBottom: '+elemBottom);
2353		*/
2354			return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
2355	}
2356	return false;
2357}
2358function isElementInView($elem) {
2359	if ((typeof $elem === 'undefined') || $elem == null) {
2360		return false;
2361	}
2362	if (!($elem instanceof jQuery))
2363		$elem = $("#" + $elem);
2364
2365	//return $elem.offset().top - $(window).scrollTop() < $elem.height() ;
2366	var result = false, docViewTop = $(window).scrollTop(),
2367		docViewBottom = docViewTop + $(window).height();
2368
2369	//Support for multiple elements $(".many-elements")
2370	$elem.each(function() {
2371		var $this = $(this),
2372		elemTop = $this.offset().top, elemBottom = elemTop + $this.height();
2373
2374		if ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)) {
2375			result = true;
2376			return false; //break loop
2377		}
2378	});
2379
2380		return result;
2381}
2382/* Desktop Notifications WIP (Only supported in Chrome/Firefox as of OCT-2017 */
2383function showDesktopNotification(options){
2384	var notify;
2385
2386	options = $.extend({
2387		title: "MSB Notification",
2388		body: "Some message from MSB", //""
2389				//icon: "https://test.www.myschoolbucks.com/images/msb_logo.png",
2390				//icon: "https://test.www.myschoolbucks.com/images/favicon-32x32.png",
2391		delay:10000, //milliseconds delay to hide notification (default 10 seconds)
2392		nonSupportMessage: '', //'This browser does not support desktop notifications!', //alert if the browser does not support desktop notification (empty means no alert)
2393		onclick: function () { notify.close(); }
2394	}, options);
2395
2396		if (!("Notification" in window)) {
2397			if (!isEmpty(options.nonSupportMessage))
2398					alert(options.nonSupportMessage);
2399		} else if (Notification.permission === "granted") {
2400				notify = new Notification(options.title, options);
2401			if (options.delay > 0) {
2402					notify.onshow=function(){
2403				setTimeout(notify.close.on(notify), options.delay);
2404					}
2405			}
2406				notify.onclick = options.onclick;
2407		} else if (Notification.permission !== 'denied') {
2408				Notification.requestPermission(function (permission) {
2409						if (permission === "granted") {
2410								notify = new Notification(options.title, options);
2411						}
2412				});
2413		}
2414}
2415
2416function initSummernote($field, options) {
2417	if ($field == null) {
2418		console.error('utils.js:initSummernote - field is null')
2419		return;
2420	}
2421	if (!($field instanceof jQuery))
2422		$field = $("#" + $field);
2423
2424	if ($field.length) {
2425		options = $.extend({
2426			placeholder: '',
2427			height: 150,
2428			minHeight: null,
2429			maxHeight: null,
2430			focus: false,
2431			style: ['style', 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'clear'],
2432			color: ['color'],
2433			para: ['ul', 'ol', 'paragraph'],
2434			insert: ['hr'], //'link','table'
2435			misc: ['undo', 'redo', 'codeview'], //'fullscreen'
2436			callbacks: {}
2437		}, options);
2438
2439		$field.summernote({
2440			disableDragAndDrop: true,
2441			fontNames: ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New'],
2442			lang: (typeof msbApp !== 'undefined') ? msbApp.getScriptLang() : 'en-US',
2443			height: options.height,
2444			minHeight: options.minHeight,
2445			maxHeight: options.maxHeight,
2446			focus: options.focus,
2447			placeholder: options.placeholder,
2448			toolbar: [
2449				['style', options.style],
2450				['color', options.color],
2451				['para', options.para],
2452				['insert', options.insert],
2453				['misc', options.misc]
2454			],
2455			callbacks: options.callbacks,
2456			dialogsInBody: true
2457		});
2458	}
2459}
2460
2461function isSummernote($field) {
2462	if ($field == null) {
2463		console.error('utils.js:isSummernote - field is null')
2464		return false;
2465	}
2466	if (!($field instanceof jQuery))
2467		$field = $("#" + $field);
2468
2469	if ($field.length) {
2470		return ($field.data().summernote != undefined);
2471	}
2472
2473	return false;
2474}
2475
2476function isSummernoteEmpty($field) {
2477	if ($field == null) {
2478		console.error('utils.js:isSummernoteEmpty - field is null')
2479		return true;
2480	}
2481	if (!($field instanceof jQuery))
2482		$field = $("#" + $field);
2483
2484	if ($field.length) {
2485		if (isSummernote($field)) {
2486			return (isEmpty($field.val()) || ($field.val() == "<span class=\"p-span\"><br></span>") || $field.summernote('isEmpty'));
2487		} else {
2488			return (isEmpty($field.val()));
2489		}
2490	}
2491
2492	return true;
2493}
2494
2495function getCleanSummernoteValue($field) {
2496	if ($field == null) {
2497		console.error('utils.js:getCleanSummernoteValue - field is null')
2498		return "";
2499	}
2500	if (!($field instanceof jQuery))
2501		$field = $("#" + $field);
2502
2503	if ($field.length) {
2504		if (isSummernote($field)) {
2505			if (isSummernoteEmpty($field)) {
2506				return "";
2507			} else {
2508				/* Summernote uses <p> tags for line breaks, but we don't want to use them
2509				 * in order to avoid unnecessary spacing and because the content might
2510				 * already be inside a <p> tag on the parent side. Instead, replace the <p>
2511				 * tags with styled <span> tags
2512				 */
2513				var value = $field.val();
2514				value = value.replace(/<p>/g, "<span class=\"p-span\">")
2515				value = value.replace(/<\/p>/g, "</span>");
2516				return value;
2517			}
2518		} else {
2519			return $field.val();
2520		}
2521	}
2522
2523	return "";
2524}
2525
2526function destroySummernote($field) {
2527	if ($field == null) {
2528		console.error('utils.js:destroySummernote - field is null')
2529		return;
2530	}
2531	if (!($field instanceof jQuery))
2532		$field = $("#" + $field);
2533
2534	if ($field.length && isSummernote($field)) {
2535		$field.summernote('destroy');
2536		$field.val(''); // Clear value after destroying Summernote to avoid bug in IE
2537	}
2538}
2539
2540function sortSelectByText($sel) {
2541	if ($sel == null) {
2542		console.error('utils.js:sortSelectByText - sel is null')
2543		return;
2544	}
2545	if (!($sel instanceof jQuery))
2546		$sel = $("#" + $sel);
2547
2548	if ($sel.length) {
2549		$sel.html($sel.find('option').sort(function(x, y) {
2550			return ($(x).text() > $(y).text()) ? 1 : -1;
2551		}));
2552	}
2553}
2554
2555function mapURLParams(data)
2556{
2557	var newUrl = '';
2558	$.each(data, function(key, val){
2559		if (isEmpty(newUrl))
2560			newUrl += '?';
2561		else
2562			newUrl += '&';
2563		newUrl += key + "=" + val;
2564	});
2565
2566	return newUrl;
2567}
2568
2569function reloadSelect2(data, $control)
2570{
2571	clearSelectToggleProps($control, $control.data('select2').options.options);
2572	var arHtml = [];
2573	$.each(data, function(key,val){
2574				arHtml.push('<option value="'+key+'">'+val+'</option>');
2575		});
2576	$control.append(arHtml.join(''));
2577
2578	$control.select2($control.data('select2').options.options);
2579}
2580
2581// Takes 2 select2 controls and makes them mutually exclusive
2582function initXOrSelect2s($select1, $select2)
2583{
2584	$select1.change(function(){
2585		var $this = $(this);
2586
2587		if ($this.val() === $select2.val())
2588			$select2.val('');
2589
2590		$select2.find('option').removeAttr('disabled');
2591		$select2.find('option[value*="' + $this.val() + '"').attr('disabled', 'disabled');
2592		$select2.select2($select2.data('select2').options.options);
2593	})
2594
2595	$select2.change(function(){
2596		var $this = $(this);
2597
2598		if ($this.val() === $select1.val())
2599			$select1.val('');
2600
2601		$select1.find('option').removeAttr('disabled');
2602		$select1.find('option[value*="' + $this.val() + '"').attr('disabled', 'disabled');
2603		$select1.select2($select1.data('select2').options.options);
2604	})
2605
2606	$select1.change();
2607	$select2.change();
2608}
2609
2610function getDateTimePickerDefaultsBS5() {
2611	return {
2612		startDate: null, startDateDefault:null, startDateButtons:{},
2613		endDate: null, endDateDefault:null,
2614		minDate: moment().subtract(99, 'years').startOf('day'),
2615		maxDate: moment().add(99, 'years').endOf('day'),
2616		format: 'MM/DD/YYYY',
2617		widgetPositioning: { horizontal: "auto", vertical: "bottom" },
2618		onChangeCallback: null
2619		//,debug:true //for troubleshooting style, the date picker to stay open after a blur event
2620	};
2621}
2622
2623function initDateTimePickerBS5($input, options) {
2624	options = $.extend(true, getDateTimePickerDefaultsBS5(), options);
2625	//console.log('initDateTimePickerBS5 options:',options)
2626
2627	var $parent = $input.parent();
2628	var $target = getDateTimePickerTargetBS5($input);
2629	// We need to set attributes to make this all work
2630	// Is this a stand alone input, or an input group?
2631	if ($parent.hasClass('input-group')) {
2632		if (isEmpty($parent.attr('id')))
2633			$parent.attr('id', 'dtp' + $input.attr('id')).attr('data-target-input', 'nearest');
2634		$input.attr('data-target', '#' + $parent.attr('id'));
2635
2636		var $inputAddOn = isBootstrap5() ? $parent.find(".input-group-text") : $parent.find(".input-group-append");
2637		if ($inputAddOn.length)
2638			$inputAddOn.attr('data-toggle', 'datetimepicker').attr('data-target', $input.attr('data-target'));
2639	} else {
2640		$input.attr('data-target', '#' + $input.attr('id'));
2641	}
2642	$input.attr('data-toggle', 'datetimepicker');
2643	if (!$input.hasClass('datetimepicker-input'))
2644		$input.addClass('datetimepicker-input');
2645
2646	$target.datetimepicker(options);
2647}
2648
2649function initStartEndDateTimePickersBS5($startDate, $endDate, options) {
2650	options = $.extend(true, getDateTimePickerDefaultsBS5(), options);
2651
2652	if (options.endDate != null) {
2653		initDateTimePickerBS5($startDate, $.extend(true, {}, options, {useCurrent:false, maxDate:options.endDate, defaultDate:options.startDateDefault, buttons:options.startDateButtons}));
2654	} else {
2655		initDateTimePickerBS5($startDate, $.extend(true, {}, options, {defaultDate:options.startDateDefault, buttons:options.startDateButtons}));
2656	}
2657
2658	if (options.startDate != null) {
2659		initDateTimePickerBS5($endDate, $.extend(true, {}, options, {useCurrent:false, minDate:options.startDate}));
2660	} else {
2661		initDateTimePickerBS5($endDate, $.extend(true, {useCurrent: false}, options));
2662	}
2663
2664	if (!options.msbSkipChangeListeners) {
2665		const $startDateTarget = getDateTimePickerTargetBS5($startDate),
2666		$endDateTarget = getDateTimePickerTargetBS5($endDate);
2667
2668		$startDateTarget.on("change.datetimepicker", function (e) {
2669				if (e.date)
2670							$endDateTarget.datetimepicker('minDate', e.date);
2671					else if (options.minDate)
2672							$endDateTarget.datetimepicker('minDate', options.minDate);
2673			if (options.onChangeCallback && _.isFunction(options.onChangeCallback)) {
2674				options.onChangeCallback(e);
2675			}
2676			});
2677
2678			$endDateTarget.on("change.datetimepicker", function (e) {
2679					if (e.date)
2680							$startDateTarget.datetimepicker('maxDate', e.date);
2681					else if (options.maxDate)
2682							$startDateTarget.datetimepicker('maxDate', options.maxDate);
2683			if (options.onChangeCallback && _.isFunction(options.onChangeCallback)) {
2684				options.onChangeCallback(e);
2685			}
2686			});
2687	}
2688}
2689
2690function resetStartEndDateTimePickersBS5($startDate, $endDate) {
2691	$startDate.val('');
2692	$endDate.val('');
2693	const $startDateTarget = getDateTimePickerTargetBS5($startDate),
2694	$endDateTarget = getDateTimePickerTargetBS5($endDate);
2695	$startDateTarget.datetimepicker('destroy');
2696	$endDateTarget.datetimepicker('destroy');
2697}
2698
2699function getDateTimePickerTargetBS5($input) {
2700	if ($input.parent().hasClass('input-group'))
2701		return $input.parent();
2702	return $input;
2703}
2704
2705// Usage of this function is discouraged. Use only as a last resort.
2706function isIe11() {
2707	var ua = window.navigator.userAgent;
2708	if (ua.indexOf('Trident/') > 0) {
2709		var rv = ua.indexOf('rv:');
2710		// IE 11 => return version number
2711		return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10) === 11;
2712	}
2713	return false;
2714}
2715
2716
2717/**
2718 * ('{0} is {1}', ['cat', 'dog']) => 'cat is dog'
2719 *
2720 * @param {string} str Resource string which contains values like {0}, {1}, etc.
2721 * @param {string[]}
2721 replacementsArr Array of strings which should be used to
2722 * replace {0}, etc. The length of this array should match the number of
2723 * replacements that will occur.
2724 * @returns String generated from replacing {0}, etc. with values in
2725 * replacementsArr
2726 */
2727function resourceReplaceParams(str, replacementsArr) {
2728	let ret = '';
2729	let currentChunkStart = 0;
2730
2731	str.replace(/\{(\d+)\}/g, function (match, captureGroup, offset) {
2732		const replacementNo = Number(captureGroup);
2733
2734		ret += str.substring(currentChunkStart, offset);
2735
2736		if (replacementNo >= 0 && replacementNo < replacementsArr.length) {
2737			ret += replacementsArr[replacementNo];
2738		} else {
2739			ret += match;
2740		}
2741		currentChunkStart = offset + match.length;
2742	});
2743
2744	if (currentChunkStart > 0) {
2745		ret += str.substring(currentChunkStart);
2746	}
2747
2748	return ret;
2749}
2750
2751function checkBackgroundingStatus(clientKey, origin, itemCount, callback) {
2752	$.ajax({
2753		traditional : true,
2754		url : '/ver2/admin/scheduledjobs/checkbackgroundingstatus',
2755		data: { clientKey: clientKey, origin: origin, itemCount: itemCount },
2756		type : 'POST'
2757	}).done(function(data) {
2758		//console.log('checkBackgroundingStatus: success!');
2759		if (data && isTrue(data.shouldBackground) && !isTrue(data.canBackground)) {
2760			if (data.messages) {
2761				var out = (Array.isArray(data.messages)) ? data.messages.join('<br>') : data.messages;
2762				msbApp.notify({message:out},{delay:0,type:'danger'});
2763			}
2764			msbApp.hideWaitingDialog();
2765
2766		} else if (_.isFunction(callback)) { callback(); }
2767
2768	}).fail(function(xhr, textStatus, errorThrown) {
2769		console.log('checkBackgroundingStatus - error:', errorThrown);
2770	});
2771}
2772
2773function emptyDomElement(elm) {
2774	if (elm) {
2775		while(elm.firstChild) elm.removeChild(elm.firstChild);
2776	}
2777}
2778
2779/* Param String phone number
2780	 * Returns formated phone Number
2781	 * landline number - 2-digit area code followed by a non-breaking space. Then write the rest of the number in 2 chunks of 4 digits. e.g.  02 5550 4321
2782	 * mobile number - 1 chunk of 4 digits and then 2 chunks of 3 digits. e.g. 0491 570 159
2783	 * */
2784function formatAuPhone(value) {
2785
2786	if (!isEmpty(value)) {
2787		var formated_phone = value.replace(/\D/g,'');
2788		var areaCode = value.substr(0,2).replace(/\D/g,'');
2789		var phoneNumber = value.replace(/\D/g,'');
2790		var secondChunk ="";
2791		var thirdChunk ="";
2792		if(areaCode == '04' || areaCode == '05') {
2793			firstChunk = phoneNumber.substring(0,4);
2794			if(phoneNumber.substring(4,7) != '' && phoneNumber.substring(4,7).length <= 3){
2795				secondChunk = phoneNumber.substring(4,7);
2796			}
2797			if(phoneNumber.substring(7,10) != '' && phoneNumber.substring(7,10).length <= 3){
2798				thirdChunk = phoneNumber.substring(7,10);
2799			}
2800			if(formated_phone.length == 10) {
2801				formated_phone = firstChunk+ " "+secondChunk+ " "+ thirdChunk;
2802			}
2803		}
2804		if(areaCode == '02' || areaCode == '03' || areaCode == '07' || areaCode == '08' ) {
2805			firstChunk = phoneNumber.substring(0,2);
2806			if(phoneNumber.substring(2,6) != '' && phoneNumber.substring(2,6).length <= 4){
2807				secondChunk = phoneNumber.substring(2,6);
2808			}
2809			if(phoneNumber.substring(6,10) != '' && phoneNumber.substring(6,10).length <= 4){
2810				thirdChunk = phoneNumber.substring(6,10);
2811			}
2812			if(formated_phone.length == 10) {
2813				formated_phone = firstChunk+ " "+secondChunk+ " "+ thirdChunk;
2814			}
2815		}
2816		return formated_phone;
2817	}
2818}
2819
2820/* Param String phone number
2821	 * Returns formated phone Number
2822	 *mobile number - 1 chunk of 4 digits and then 2 chunks of 3 digits. e.g. 0491 570 159
2823	 * */
2824function formatAuMobile(value) {
2825
2826	if (!isEmpty(value)) {
2827		var formated_phone = value.replace(/\D/g,'');
2828		var areaCode = value.substr(0,2).replace(/\D/g,'');
2829		var phoneNumber = value.replace(/\D/g,'');
2830		var secondChunk ="";
2831		var thirdChunk ="";
2832		if(areaCode == '04' || areaCode == '05') {
2833			firstChunk = phoneNumber.substring(0,4);
2834			if(phoneNumber.substring(4,7) != '' && phoneNumber.substring(4,7).length <= 3){
2835				secondChunk = phoneNumber.substring(4,7);
2836			}
2837			if(phoneNumber.substring(7,10) != '' && phoneNumber.substring(7,10).length <= 3){
2838				thirdChunk = phoneNumber.substring(7,10);
2839			}
2840			if(formated_phone.length == 10) {
2841				formated_phone = firstChunk+ " "+secondChunk+ " "+ thirdChunk;
2842			}
2843		}
2844		return formated_phone;
2845	}
2846}
2847
2848function initBootstrap5Validator($form, options) {
2849	var datePickerFormat = "MM/DD/YYYY";
2850	if($("#pageValuesDatePickerFormat") && $("#pageValuesDatePickerFormat").val()) {
2851		datePickerFormat = $("#pageValuesDatePickerFormat").val();
2852	}
2853	if ($form == null) {
2854		console.error('utils.js:initBootstrap5Validator - form is null')
2855		return;
2856	}
2857	if (!($form instanceof jQuery))
2858		$form = $("#" + $form);
2859
2860	if ($form.length) {
2861		if (!shouldDisableBootstrapValidator()) {
2862			// Add Bootstrap 5 validation class
2863			$form.addClass('needs-validation');
2864			$form.attr('novalidate', true); // Disable default browser validation
2865
2866			// Default options for Bootstrap 5
2867			var newOptions = $.extend(true, {
2868				feedback: {
2869					success: 'fa-check-circle',
2870					error: ''
2871				}
2872			}, options);
2873
2874			// Custom validation functions for Bootstrap 5
2875			var customValidators = {
2876				match: function($el) {
2877					if ($el.is(":hidden")) return null;
2878					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2879
2880					var matchFieldID = $el.attr('data-match');
2881					var $matchField = $(matchFieldID);
2882					if (!($matchField instanceof jQuery))
2883						$matchField = $("#"+matchFieldID);
2884					if (!$matchField.length) return null;
2885
2886					return $el.val() !== $matchField.val() ? $el.attr('data-match-error') || 'Fields do not match' : null;
2887				},
2888				nomatch: function($el) {
2889					if ($el.is(":hidden")) return null;
2890					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2891
2892					var noMatchFieldID = $el.attr('data-nomatch');
2893					var $noMatchField = $(noMatchFieldID);
2894					if (!($noMatchField instanceof jQuery))
2895						$noMatchField = $("#"+noMatchFieldID);
2896					if (!$noMatchField.length) return null;
2897
2898					return $el.val() === $noMatchField.val() ? $el.attr('data-nomatch-error') || 'Fields must not match' : null;
2899				},
2900				minlength: function($el) {
2901					if ($el.is(":hidden")) return null;
2902					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2903
2904					var val = $el.val();
2905					if (isTrue($el.attr('data-minlength-squish')))
2906						val = squishString(val);
2907					var minlength = $el.attr('data-minlength');
2908					return val.length < minlength ? $el.attr('data-minlength-error') || ('Minimum length is ' + minlength) : null;
2909				},
2910				passwordpattern: function($el) {
2911					if ($el.is(":hidden")) return null;
2912					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2913
2914					var val = $el.val();
2915					const re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()+=-\?;,./{}|\":<>\[\]\\\' ~_])/;
2916					return !re.test(val) ? $el.attr('data-passwordpattern-error') || 'Password must contain uppercase, lowercase, number and special character' : null;
2917				},
2918				passwordwhitespace: function($el) {
2919					if ($el.is(":hidden")) return null;
2920					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2921
2922					var val = $el.val();
2923					if (val == null) return null;
2924					return val.trim().length !== val.length ? $el.attr('data-passwordwhitespace-error') || 'Password cannot contain leading or trailing spaces' : null;
2925				},
2926				maxlength: function($el) {
2927					if ($el.is(":hidden")) return null;
2928					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2929
2930					var val = $el.val();
2931					if (isTrue($el.attr('data-maxlength-squish')))
2932						val = squishString(val);
2933					var maxlength = $el.attr('data-maxlength');
2934					return val.length > maxlength ? $el.attr('data-maxlength-error') || ('Maximum length is ' + maxlength) : null;
2935				},
2936				greaterthan: function($el) {
2937					if ($el.is(":hidden")) return null;
2938					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2939
2940					var valueToBeGreaterThan = $el.attr('data-greaterthan');
2941					if (!isNumber(valueToBeGreaterThan) || !isNumber($el.val())) return null;
2942
2943					return parseFloat($el.val()) <= parseFloat(valueToBeGreaterThan) ? $el.attr('data-greaterthan-error') || ('Value must be greater than ' + valueToBeGreaterThan) : null;
2944				},
2945				greaterthanorequalto: function($el) {
2946					if ($el.is(":hidden")) return null;
2947					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2948
2949					var valueToBeGreaterThanOrEqualTo = $el.attr('data-greaterthanorequalto');
2950					if (!isNumber(valueToBeGreaterThanOrEqualTo) || !isNumber($el.val())) return null;
2951
2952					return parseFloat($el.val()) < parseFloat(valueToBeGreaterThanOrEqualTo) ? $el.attr('data-greaterthanorequalto-error') || ('Value must be greater than or equal to ' + valueToBeGreaterThanOrEqualTo) : null;
2953				},
2954				lessthan: function($el) {
2955					if ($el.is(":hidden")) return null;
2956					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2957
2958					var valueToBeLessThan = $el.attr('data-lessthan');
2959					if (!isNumber(valueToBeLessThan) || !isNumber($el.val())) return null;
2960
2961					return parseFloat($el.val()) >= parseFloat(valueToBeLessThan) ? $el.attr('data-lessthan-error') || ('Value must be less than ' + valueToBeLessThan) : null;
2962				},
2963				lessthanorequalto: function($el) {
2964					if ($el.is(":hidden")) return null;
2965					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2966
2967					var valueToBeLessThanOrEqualTo = $el.attr('data-lessthanorequalto');
2968					if (!isNumber(valueToBeLessThanOrEqualTo) || !isNumber($el.val())) return null;
2969
2970					return parseFloat($el.val()) > parseFloat(valueToBeLessThanOrEqualTo) ? $el.attr('data-lessthanorequalto-error') || ('Value must be less than or equal to ' + valueToBeLessThanOrEqualTo) : null;
2971				},
2972				between: function($el) {
2973					if ($el.is(":hidden")) return null;
2974					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2975
2976					var value = parseFloat($el.val());
2977					var range = $el.attr('data-between');
2978					var min = parseFloat(range.split('-')[0]);
2979					var max = parseFloat(range.split('-')[1]);
2980
2981					if (!isNumber(value) || !isNumber(min) || !isNumber(max)) return null;
2982
2983					if (max > min && (value < min || value > max)) {
2984						return $el.attr('data-range-error') || ('Value must be between ' + min + ' and ' + max);
2985					} else if (value < min) {
2986						return $el.attr('data-min-error') || ('Value must be greater than ' + min);
2987					}
2988
2989					return null;
2990				},
2991				numeric: function($el) {
2992					if ($el.is(":hidden")) return null;
2993					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
2994
2995					return !isNumber($el.val()) ? $el.attr('data-numeric-error') || 'Must be a number' : null;
2996				},
2997				containsnumber: function($el) {
2998					if ($el.is(":hidden")) return null;
2999					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3000
3001					return !containsNumber($el.val()) ? $el.attr('data-containsnumber-error') || 'Must contain a number' : null;
3002				},
3003				date: function($el) {
3004					if ($el.is(":hidden")) return null;
3005					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3006
3007					return !moment($el.val(), datePickerFormat, true).isValid() ? $el.attr('data-date-error') || 'Invalid date' : null;
3008				},
3009				dateafter: function($el) {
3010					if ($el.is(":hidden")) return null;
3011					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3012					if (($el.attr('data-date') || $el.attr('data-date') !== undefined) && !moment($el.val(), datePickerFormat, true).isValid()) return null;
3013
3014					var startDateFieldID = $el.attr('data-dateafter');
3015					var $startDateField = $(startDateFieldID);
3016					if (!($startDateField instanceof jQuery))
3017						$startDateField = $("#"+startDateFieldID);
3018					if (!$startDateField.length || $startDateField.val().length == 0 || !moment($startDateField.val(), datePickerFormat, true).isValid()) return null;
3019
3020					return !isValidDateRange($startDateField.val(), $el.val()) ? $el.attr('data-dateafter-error') || 'Date must be after start date' : null;
3021				},
3022				datebefore: function($el) {
3023					if ($el.is(":hidden")) return null;
3024					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3025					if (($el.attr('data-date') || $el.attr('data-date') !== undefined) && !moment($el.val(), datePickerFormat, true).isValid()) return null;
3026
3027					var endDateFieldID = $el.attr('data-datebefore');
3028					var $endDateField = $(endDateFieldID);
3029					if (!($endDateField instanceof jQuery))
3030						$endDateField = $("#"+endDateFieldID);
3031					if (!$endDateField.length || $endDateField.val().length == 0 || !moment($endDateField.val(), datePickerFormat, true).isValid()) return null;
3032
3033					return !isValidDateRange($el.val(), $endDateField.val()) ? $el.attr('data-datebefore-error') || 'Date must be before end date' : null;
3034				},
3035				dualrequired: function($el) {
3036					if ($el.is(":hidden")) return null;
3037					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3038
3039					var dualRequiredFieldID = $el.attr('data-dualrequired');
3040					var $dualRequiredField = $(dualRequiredFieldID);
3041					if (!($dualRequiredField instanceof jQuery))
3042						$dualRequiredField = $("#"+dualRequiredFieldID);
3043					if (!$dualRequiredField.length) return null;
3044
3045					return ($el.val().length == 0 || $dualRequiredField.val().length == 0) ? $el.attr('data-dualrequired-error') || 'Both fields are required' : null;
3046				},
3047				emailaddress: function($el) {
3048					if ($el.is(":hidden")) return null;
3049					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3050
3051					return !performEmailRegex($el.val()) ? $el.attr('data-emailaddress-error') || 'Invalid email address' : null;
3052				},
3053				singleemailaddress: function($el) {
3054					if ($el.is(":hidden")) return null;
3055					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3056
3057					return !isValidEmail($el.val()) ? $el.attr('data-singleemailaddress-error') || 'Invalid email address' : null;
3058				},
3059				username: function($el) {
3060					if ($el.is(":hidden")) return null;
3061					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3062
3063					return $el.val().indexOf(';') >= 0 ? $el.attr('data-username-error') || 'Username cannot contain semicolons' : null;
3064				},
3065				phonenumber: function($el) {
3066					if ($el.is(":hidden")) return null;
3067
3068					var val = $el.val().replace(/\D/g,'')
3069					var isRequired = ($el.attr('required') !== undefined);
3070					return ((isRequired || val.length > 0) && val.length != 10) ? $el.attr('data-phonenumber-error') || 'Phone number must be 10 digits' : null;
3071				},
3072				unlimitedornumeric: function($el) {
3073					if ($el.is(":hidden")) return null;
3074					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3075
3076					return (!isNumber($el.val()) && $el.val().toUpperCase() != "UNL") ? $el.attr('data-unlimitedornumeric-error') || 'Must be a number or UNL' : null;
3077				},
3078				routingnumber: function($el) {
3079					if ($el.is(":hidden")) return null;
3080					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3081
3082					var value = $el.val();
3083					var previousValue = $el.attr('data-routingnumber');
3084
3085					if (value != previousValue) {
3086						if (!isValidRoutingNumber(value))
3087							return $el.attr('data-routingnumber-error') || 'Invalid routing number';
3088					}
3089
3090					return null;
3091				},
3092				checkingacctnumber: function($el) {
3093					if ($el.is(":hidden")) return null;
3094					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3095
3096					var value = $el.val();
3097					var previousValue = $el.attr('data-checkingacctnumber');
3098
3099					if (value != previousValue) {
3100						var formattedValue = "";
3101						for (var i = 0; i < value.length; i++) {
3102							if (isNumber(value.charAt(i)))
3103								formattedValue = formattedValue + value.charAt(i);
3104						}
3105						return (formattedValue.length < 1 || formattedValue.length > 17) ? $el.attr('data-checkingacctnumber-error') || 'Account number must be between 1 and 17 digits' : null;
3106					}
3107
3108					return null;
3109				},
3110				creditcardlength: function($el) {
3111					if ($el.is(":hidden")) return null;
3112					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3113
3114					var data = $el.attr('data-creditcardnumber').split(';');
3115					if (data.length < 2) return null;
3116
3117					var cardTypeFieldID = data[0];
3118					var $cardTypeField = $(cardTypeFieldID);
3119					if (!($cardTypeField instanceof jQuery))
3120						$cardTypeField = $("#"+cardTypeFieldID);
3121					if (!$cardTypeField.length) return null;
3122					var previousValue = data[1];
3123
3124					if ($el.val() != previousValue) {
3125						var cardType = getCardType($el, $cardTypeField);
3126						if (!isValidCreditCardLength($el, cardType))
3127							return $el.attr('data-creditcardlength-error') || 'Invalid credit card length';
3128					}
3129					return null;
3130				},
3131				creditcardcvvlength: function($el) {
3132					if ($el.is(":hidden")) return null;
3133					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3134
3135					var cardType = 'invalid';
3136
3137					if (!isEmpty($el.attr('data-cardtypefield-id'))) {
3138						var cardTypeFieldID = $el.attr('data-cardtypefield-id');
3139						var $cardTypeField = $(cardTypeFieldID);
3140						if (!($cardTypeField instanceof jQuery))
3141							$cardTypeField = $("#"+cardTypeFieldID);
3142						if (!$cardTypeField.length) return null;
3143
3144						cardType = $cardTypeField.val();
3145					} else {
3146						var cardSelectID = $el.attr('data-cardselect-id');
3147						var selectedOption = $(cardSelectID + ' option:selected').text().toLowerCase();
3148						if (selectedOption.indexOf('visa') >= 0)
3149							cardType = 'visa';
3150						else if (selectedOption.indexOf('mastercard') >= 0)
3151							cardType = 'mastercard';
3152						else if (selectedOption.indexOf('discover') >= 0)
3153							cardType = 'discover';
3154						else if (selectedOption.indexOf('american express') >= 0)
3155							cardType = 'amex';
3156					}
3157
3158					if (!isValidCvvLength($el, cardType))
3159						return $el.attr('data-creditcardcvvlength-error') || 'Invalid CVV length';
3160
3161					return null;
3162				},
3163				creditcardnumber: function($el) {
3164					if ($el.is(":hidden")) return null;
3165					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3166
3167					var data = $el.attr('data-creditcardnumber').split(';');
3168					if (data.length < 2) return null;
3169
3170					var cardTypeFieldID = data[0];
3171					var $cardTypeField = $(cardTypeFieldID);
3172					if (!($cardTypeField instanceof jQuery))
3173						$cardTypeField = $("#"+cardTypeFieldID);
3174					if (!$cardTypeField.length) return null;
3175
3176					var cardType = getCardType($el, $cardTypeField);
3177					var acceptedCardTypes = $el.attr('data-acceptedcardtypes');
3178
3179					// Don't throw an error here if card type is invalid or not accepted - will be handled by acceptedcardtypes validator
3180					if (isEmpty(cardType) || (!isEmpty(acceptedCardTypes) && JSON.stringify(acceptedCardTypes).indexOf(cardType) < 0) || !isValidCreditCardLength($el, cardType))
3181						return null;
3182
3183					var value = $el.val();
3184					var previousValue = data[1];
3185
3186					if (value != previousValue) {
3187						if (!isValidCreditCardNumber(value))
3188							return $el.attr('data-creditcardnumber-error') || 'Invalid credit card number';
3189
3190						if (cardType == "visa" && value.indexOf("4") != 0)
3191							return $el.attr('data-creditcardnumber-error') || 'Invalid Visa card number';
3192						else if (cardType == "mastercard" && (value.indexOf("5") != 0 && value.indexOf("2") != 0))
3193							return $el.attr('data-creditcardnumber-error') || 'Invalid Mastercard number';
3194						else if (cardType == "amex" && value.indexOf("3") != 0)
3195							return $el.attr('data-creditcardnumber-error') || 'Invalid American Express number';
3196						else if (cardType == "discover" && value.indexOf("6") != 0)
3197							return $el.attr('data-creditcardnumber-error') || 'Invalid Discover card number';
3198					}
3199
3200					return null;
3201				},
3202				acceptedcardtypes: function($el) {
3203					if ($el.is(":hidden")) return null;
3204					if (($el.attr('required') || $el.attr('required') !== undefined) && $el.val().length == 0) return null;
3205
3206					var data = $el.attr('data-creditcardnumber').split(';');
3207					if (data.length < 2) return null;
3208
3209					var cardTypeFieldID = data[0];
3210					var $cardTypeField = $(cardTypeFieldID);
3211					if (!($cardTypeField instanceof jQuery))
3212						$cardTypeField = $("#"+cardTypeFieldID);
3213					if (!$cardTypeField.length) return null;
3214
3215					var acceptedCardTypes = $el.attr('data-acceptedcardtypes');
3216					if (!acceptedCardTypes.length) return null;
3217
3218					var cardType = getCardType($el, $cardTypeField);
3219					if (isEmpty(cardType)) return null;
3220
3221					if (acceptedCardTypes.indexOf(cardType) < 0) {
3222						var cardTypeMap = JSON.parse($el.attr('data-cardtypemap'));
3223						var cardTypeMapError = $el.attr('data-acceptedcardtypeserror-text');
3224
3225						if (!isEmpty(cardTypeMap) && !isEmpty(cardTypeMapError) && !isEmpty(cardTypeMap[cardType]))
3226							return cardTypeMapError.replace('{0}', cardTypeMap[cardType]);
3227						else
3228							return $el.attr('data-acceptedcardtypes-error') || 'Card type not accepted';
3229					}
3230
3231					return null;
3232				},
3233				paymentmethods_expirationdatemonth: function($el) {
3234					if ($el.is(":hidden")) return null;
3235
3236					var expYearFieldID = $el.attr('data-paymentmethods_expirationdatemonth');
3237					var $expYearField = $(expYearFieldID);
3238					if (!($expYearField instanceof jQuery))
3239						$expYearField = $("#"+expYearFieldID);
3240					if (!$expYearField.length) return null;
3241
3242					var selectedMonth = $el.val();
3243					var selectedYear = $expYearField.val();
3244					if (selectedMonth.length == 0 || selectedYear.length == 0) return null;
3245
3246					if (isNumber(selectedMonth) && isNumber(selectedYear)) {
3247						var date = new Date();
3248						return (selectedYear < date.getFullYear() || (selectedYear == date.getFullYear() && selectedMonth < date.getMonth())) ? $el.attr('data-paymentmethods_expirationdatemonth-error') || 'Expiration date has passed' : null;
3249					}
3250
3251					return null;
3252				},
3253				paymentmethods_expirationdateyear: function($el) {
3254					if ($el.is(":hidden")) return null;
3255
3256					var expMonthFieldID = $el.attr('data-paymentmethods_expirationdateyear');
3257					var $expMonthField = $(expMonthFieldID);
3258					if (!($expMonthField instanceof jQuery))
3259						$expMonthField = $("#"+expMonthFieldID);
3260					if (!$expMonthField.length) return null;
3261
3262					var selectedYear = $el.val();
3263					var selectedMonth = $expMonthField.val();
3264					if (selectedYear.length == 0 || selectedMonth.length == 0) return null;
3265
3266					if (isNumber(selectedYear) && isNumber(selectedMonth)) {
3267						var date = new Date();
3268						return (selectedYear < date.getFullYear() || (selectedYear == date.getFullYear() && selectedMonth < date.getMonth())) ? $el.attr('data-paymentmethods_expirationdateyear-error') || 'Expiration date has passed' : null;
3269					}
3270
3271					return null;
3272				},
3273				editautopaysettings_lowbalance: function($el) {
3274					if ($el.is(":hidden")) return null;
3275
3276					var name = $el.attr('data-editautopaysettings_lowbalance');
3277					var $fieldToCheck = $("input[name='"+name+"']:checked");
3278					if (!$fieldToCheck.length || $fieldToCheck.is(":hidden")) return null;
3279
3280					var lowBalanceThreshold = $fieldToCheck.val();
3281					if (lowBalanceThreshold == "other") {
3282						$fieldToCheck = $("input[name='OTHER_"+name+"']");
3283						if ($fieldToCheck.val().length == 0 || !isNumber($fieldToCheck.val())) return null;
3284						lowBalanceThreshold = $fieldToCheck.val();
3285
3286						var maxLowBalanceThreshold = $fieldToCheck.attr("data-lessthanorequalto");
3287						if (maxLowBalanceThreshold !== undefined && maxLowBalanceThreshold != null && isNumber(maxLowBalanceThreshold) && isNumber(lowBalanceThreshold) && (lowBalanceThreshold * 1 > maxLowBalanceThreshold * 1))
3288							return null;
3289					}
3290
3291					if ($el.is("[type='radio']") && !$el.is(":checked"))
3292						$el = $("input[name='"+$el.attr("name")+"']:checked");
3293					if ($el.val() == 0) return null;
3294
3295					var paymentAmount = $el.val();
3296					if (paymentAmount == "other") {
3297						$el = $("input[name='OTHER_"+$el.attr("name")+"']")
3298						if ($el.val().length == 0 || !isNumber($el.val())) return null;
3299						paymentAmount = $el.val();
3300					}
3301
3302					if (!isNumber(lowBalanceThreshold) || !isNumber(paymentAmount)) return null;
3303
3304					lowBalanceThreshold = lowBalanceThreshold * 2;
3305					if (paymentAmount < lowBalanceThreshold) {
3306						return $el.attr('data-editautopaysettings_lowbalance-error') || (pageValues.messages.invalidLowBalancePayment + " " + pageValues.currencySymbol + lowBalanceThreshold.toFixed(2));
3307					}
3308
3309					return null;
3310				},
3311				editlowbalancesettings_lowbalance: function($el) {
3312					if ($el.is(":hidden")) return null;
3313
3314					var paymentAmount = $el.val();
3315					if (paymentAmount.length == 0 || !isNumber(paymentAmount)) return null;
3316
3317					var lowBalanceFieldID = $el.attr('data-editlowbalancesettings_lowbalance');
3318					var $lowBalanceField = $(lowBalanceFieldID);
3319					if (!($lowBalanceField instanceof jQuery))
3320						$lowBalanceField = $("#"+lowBalanceFieldID);
3321					if (!$lowBalanceField.length || $lowBalanceField.is(":hidden")) return null;
3322
3323					var lowBalanceThreshold = $lowBalanceField.val();
3324					if (lowBalanceThreshold.length == 0 || !isNumber(lowBalanceThreshold)) return null;
3325
3326					lowBalanceThreshold = lowBalanceThreshold * 2;
3327					if (paymentAmount < lowBalanceThreshold) {
3328						return $el.attr('data-editlowbalancesettings_lowbalance-error') || (pageValues.messages.invalidLowBalancePayment + " " + pageValues.currencySymbol + lowBalanceThreshold.toFixed(2));
3329					}
3330
3331					return null;
3332				}
3333			};
3334
3335			function validateElement($el) {
3336				if ($el.is(':hidden')) {
3337					$el.get(0).setCustomValidity('');
3338					$el.removeClass('is-valid is-invalid');
3339					return;
3340				}
3341
3342				var isValid = true;
3343				var errorMessage = '';
3344
3345				$.each($el.get(0).attributes, function(index, attr) {
3346					if (attr.name.startsWith('data-') && customValidators[attr.name.substring(5)]) {
3347						var validatorName = attr.name.substring(5);
3348						var error = customValidators[validatorName]($el);
3349						if (error) {
3350							isValid = false;
3351							errorMessage = error;
3352							return false; // Break the loop
3353						}
3354					}
3355				});
3356
3357				$el.get(0).setCustomValidity(isValid ? '' : errorMessage);
3358
3359				var validationMessage = errorMessage || $el.get(0).validationMessage;
3360
3361				if ($el.data('bs5-interacted')) {
3362					if ($el.get(0).checkValidity()) {
3363						$el.removeClass('is-invalid').addClass('is-valid');
3364						showErrorMessage($el, '');
3365					} else {
3366						$el.removeClass('is-valid').addClass('is-invalid');
3367						showErrorMessage($el, validationMessage);
3368					}
3369				}
3370			}
3371
3372			function showErrorMessage($el, message) {
3373				var $feedback = $el.siblings('.invalid-feedback').first();
3374				if (!$feedback.length) {
3375					$feedback = $el.parent().find('.invalid-feedback').first();
3376				}
3377				if (!$feedback.length) {
3378					$feedback = $el.closest('.form-group, .mb-3, .form-floating, .input-group').find('.invali
3378d-feedback').first();
3379				}
3380
3381				if (!$feedback.length && $el.parent().hasClass('input-group')) {
3382					$feedback = $el.parent().parent().find('.invalid-feedback').first();
3383				}
3384
3385				if ($feedback.length) {
3386					if (message) {
3387						$feedback.text(message).show();
3388					} else {
3389						$feedback.text('').hide();
3390					}
3391				} else if (message) {
3392					$feedback = $('<div class="invalid-feedback d-block"></div>').text(message);
3393
3394					if ($el.parent().hasClass('input-group')) {
3395						$el.parent().after($feedback);
3396					} else {
3397						$el.after($feedback);
3398					}
3399				}
3400
3401				if (message) {
3402					$el.attr('title', message);
3403				} else {
3404					$el.removeAttr('title');
3405				}
3406			}
3407
3408			$form.find('input, select, textarea').each(function() {
3409				var $el = $(this);
3410
3411				$el.on('focus.bs5validator', function() {
3412					$(this).data('bs5-interacted', true);
3413				});
3414
3415				$el.on('input.bs5validator change.bs5validator blur.bs5validator', function() {
3416					if ($(this).data('bs5-interacted')) {
3417						validateElement($(this));
3418					}
3419				});
3420
3421				$el.get(0).setCustomValidity('');
3422				$el.removeClass('is-valid is-invalid');
3423			});
3424
3425			$("input.required-check-group").click(function() {
3426				var name = $(this).attr('name');
3427				if (!name || isEmpty(name)) return;
3428
3429				var $group = $('[name="' + name + '"]');
3430				var checked = $group.is(':checked');
3431				var errorMsg = checked ? '' : 'Please select at least one option';
3432
3433				$group.each(function(){
3434					var $this = $(this);
3435					$this.data('bs5-interacted', true);
3436					$this.get(0).setCustomValidity(errorMsg);
3437					$this.removeClass('is-valid is-invalid').addClass(checked ? 'is-valid' : 'is-invalid');
3438					showErrorMessage($this, errorMsg);
3439				});
3440			});
3441
3442			$("input.required-check-group").each(function() {
3443				var name = $(this).attr('name');
3444				if (name && !isEmpty(name)) {
3445					$('[name="' + name + '"]').each(function(){
3446						this.setCustomValidity('');
3447						$(this).removeClass('is-valid is-invalid');
3448					});
3449				}
3450			});
3451
3452			// Handle form submission with namespaced event
3453			$form.on('submit.bs5validator', function(e) {
3454				var form = this;
3455
3456				// Re-validate all elements before submission
3457				$form.find('input, select, textarea').each(function() {
3458					validateElement($(this));
3459				});
3460
3461				// Check only VISIBLE elements for validation
3462				var hasVisibleInvalidElements = false;
3463				$form.find('input, select, textarea').each(function() {
3464					var $el = $(this);
3465					if (!$el.is(':hidden') && !this.checkValidity()) {
3466						hasVisibleInvalidElements = true;
3467						return false; // Break the loop
3468					}
3469				});
3470
3471				if (hasVisibleInvalidElements) {
3472					e.preventDefault();
3473					e.stopPropagation();
3474				}
3475
3476				form.classList.add('was-validated');
3477			});
3478
3479			// Handle multi-checkbox range validation
3480			$('[data-multicheckbox_range]').each(function() {
3481				var $container = $(this);
3482				var range = $container.attr('data-multicheckbox_range').split(':');
3483				var min = parseInt(range[0]) || 0;
3484				var max = parseInt(range[1]) || 0;
3485
3486				function validateCheckboxRange() {
3487					var numChecked = $container.find('input[type="checkbox"]:checked').length;
3488					var isValid = (min <= 0 || numChecked >= min) && (max <= 0 || numChecked <= max);
3489
3490					var errorMessage = '';
3491					if (!isValid) {
3492						if (min > 0 && max > 0) {
3493							errorMessage = 'Please select between ' + min + ' and ' + max + ' options';
3494						} else if (min > 0) {
3495							errorMessage = 'Please select at least ' + min + ' option(s)';
3496						} else if (max > 0) {
3497							errorMessage = 'Please select no more than ' + max + ' option(s)';
3498						}
3499					}
3500
3501					// Set validity on the container or first checkbox
3502					var $firstCheckbox = $container.find('input[type="checkbox"]').first();
3503					if ($firstCheckbox.length) {
3504						$firstCheckbox.get(0).setCustomValidity(isValid ? '' : errorMessage);
3505						if (isValid) {
3506							$firstCheckbox.removeClass('is-invalid').addClass('is-valid');
3507						} else {
3508							$firstCheckbox.removeClass('is-valid').addClass('is-invalid');
3509						}
3510					}
3511				}
3512
3513				$container.find('input[type="checkbox"]').on('change', validateCheckboxRange);
3514				validateCheckboxRange(); // Initial validation
3515			});
3516		} else {
3517			$form.attr('novalidate', true); // Disable automatic native validation
3518		}
3519	}
3520}
3521
3522function updateBootstrap5Validator($form) {
3523	if ($form == null) {
3524		console.error('utils.js:updateBootstrap5Validator - form is null')
3525		return;
3526	}
3527	if (!($form instanceof jQuery))
3528		$form = $("#" + $form);
3529
3530	if ($form.length) {
3531		if (!shouldDisableBootstrapValidator()) {
3532			// Re-validate all form elements
3533			$form.find('input, select, textarea').each(function() {
3534				$(this).trigger('blur');
3535			});
3536		} else {
3537			$form.attr('novalidate', true); // Disable automatic native validation
3538		}
3539	}
3540}
3541
3542function validateBootstrap5Validator($form, $el, focusError) {
3543	if ($form == null) {
3544		console.error('utils.js:validateBootstrap5Validator - form is null')
3545		return false;
3546	}
3547	if (!($form instanceof jQuery))
3548		$form = $("#" + $form);
3549
3550	if ($form.length) {
3551		if (!shouldDisableBootstrapValidator()) {
3552			if ($el != null && ($el || $el !== undefined)) {
3553				if (!($el instanceof jQuery))
3554					$el = $("#" + $el);
3555
3556				if ($el.length) {
3557					$el.trigger('blur'); // Trigger validation on specific element
3558					var isValid = $el.get(0).checkValidity();
3559					if (focusError && !isValid) {
3560						$el.focus();
3561					}
3562					return isValid;
3563				} else {
3564					// Validate entire form
3565					$form.find('input, select, textarea').each(function() {
3566						$(this).trigger('blur');
3567					});
3568					var formIsValid = $form.get(0).checkValidity();
3569					if (focusError && !formIsValid) {
3570						var $firstInvalid = $form.find('.is-invalid').first();
3571						if ($firstInvalid.length) {
3572							$firstInvalid.focus();
3573						}
3574					}
3575					return formIsValid;
3576				}
3577			} else {
3578				$form.find('input, select, textarea').each(function() {
3579					var $el = $(this);
3580					if ($el.is(':hidden')) {
3581						this.setCustomValidity('');
3582						$el.removeClass('is-valid is-invalid');
3583					} else {
3584						$el.trigger('blur');
3585					}
3586				});
3587
3588				var invalidCount = 0;
3589				$form.find('input, select, textarea').each(function() {
3590					var $el = $(this);
3591					if (!$el.is(':hidden') && !this.checkValidity()) {
3592						invalidCount++;
3593					}
3594				});
3595
3596				var formIsValid = invalidCount === 0;
3597
3598				if (focusError && !formIsValid) {
3599					var $firstInvalid = $form.find('.is-invalid:visible').first();
3600					if ($firstInvalid.length) {
3601						$firstInvalid.focus();
3602					}
3603				}
3604				return formIsValid;
3605			}
3606		} else {
3607			$form.attr('novalidate', true);
3608			return true;
3609		}
3610	}
3611	return false;
3612}
3613
3614function destroyBootstrap5Validator($form) {
3615	if ($form == null) {
3616		console.error('utils.js:destroyBootstrap5Validator - form is null');
3617		return;
3618	}
3619	if (!($form instanceof jQuery))
3620		$form = $("#" + $form);
3621
3622	if ($form.length) {
3623		if (!shouldDisableBootstrapValidator()) {
3624			// Remove Bootstrap 5 validation classes and event handlers
3625			$form.removeClass('needs-validation was-validated');
3626			$form.removeAttr('novalidate');
3627
3628			// Clear validation states on all form elements
3629			$form.find('input, select, textarea').each(function() {
3630				var $el = $(this);
3631				$el.get(0).setCustomValidity('');
3632				$el.removeClass('is-valid is-invalid');
3633				$el.off('input.bs5validator change.bs5validator blur.bs5validator');
3634			});
3635
3636			// Remove form submission handler
3637			$form.off('submit.bs5validator');
3638		} else {
3639			$form.attr('novalidate', true);
3640		}
3641	}
3642}
3643
3644// End of Bootstrap 5 validator functions
3645
3646// Common method used for debug logging.
3647function isDebugHost() {
3648	return (window.location.protocol === 'http:') ||
3649		(window.location.hostname === 'local.www.myschoolbucks.com') ||
3650		(window.location.hostname === 'test.www.myschoolbucks.com') ||
3651		(window.location.hostname === 'localhost') ||
3652	    // [::1] is the IPv6 localhost address.
3653	    (window.location.hostname === '[::1]') ||
3654	    // 127.0.0.1/8 is considered localhost for IPv4.
3655	    window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/);
3656}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.