PageSourceSearch

https://shop.friheden.dk/assets/standard-shop/scripts/common-f723588bdff377748a15d95d24e38b67.js

js friheden.dk collected 2026-10-02 10:36:04 UTC 28,061 bytes, 1,037 lines download raw bytes

1/* global menu, tags, messages, modal, tracking, utils, resources, bootstrap */
2/* exported common */
3
4/**
5 * @typedef {object} AjaxLoaderOptions
6 * @property {string} url The URL to GET.
7 * @property {object|string|Array} [data] The data to POST.
8 * @property {string} [method] HTTP method. GET, POST, etc.
9 * @property {boolean} [async] Perform an Asynchronous call. Default is true.
10 * @property {string} [contentType] The contentType.
11 * @property {boolean} [processData] Do processData.
12 * @property {HTMLElement|JQuery|string} [feedback] This is where the feedback is shown.
13 * 		If none is given, loader is searched for an element with a data-type="feedback" attribute.
14 * 		If none is found a message is shown according to successFeedbackStyle.
15 * 		If false is given, no feedback will be done.
16 * @property {boolean|string} [successFeedbackStyle] How successful messages should be shown, if feedback is not specified. If false no message will be shown.
17 * @property {boolean|string} [notSuccessFeedbackStyle] How not successful messages should be shown. If false no message will be shown.
18 * @property {boolean} [loader] If none given no loader will be added.
19 * @property {boolean} [container] If none given the loader is used.
20 * @property {ajaxCallback} [progress] Called on progress event.
21 * @property {ajaxCallback} [success] Called if "response.success" is true.
22 * @property {ajaxCallback} [notSuccess] Called if "response.success" is false.
23 * @property {function} [cleanup] Called on both success and error.
24 */
25/**
26 * @typedef {AjaxLoaderOptions} AjaxFormSubmitterOptions
27 * @property {HTMLFormElement} [form] Sets loader, url, and data from the form
28 * @property {string} [url] The URL to POST to.
29 */
30
31const common = {
32
33	/**
34	 * The base URL to use for all AJAX requests.
35	 * @var {string}
36	 */
37	baseAjaxUrl: '',
38
39	/**
40	 * The i18n prefix appended with a slash, or empty if the shop has no i18n prefix.
41	 * @var {string}
42	 */
43	i18nPrefix: '',
44
45	/**
46	 * The frame style to use for all links.
47	 *
48	 * @var {string}
49	 */
50	frameStyle: '',
51
52	/**
53	 * @var {object[]}
54	 */
55	modules: [],
56
57	/**
58	 * Register a module with its config.
59	 *
60	 * @param {string} label
61	 * @param {object} config
62	 */
63	registerModule: function(label, config) {
64		config = config || {};
65		config.label = label;
66		this.modules.push(config);
67	},
68
69	/**
70	 * Initialize the page.
71	 */
72	init: function() {
73		this.initScope();
74		if (typeof menu !== 'undefined') {
75			menu.init();
76		}
77		if (typeof tags !== 'undefined') {
78			tags.init();
79		}
80		if (typeof messages !== 'undefined') {
81			messages.init();
82		}
83		if (typeof modal !== 'undefined') {
84			modal.init();
85		}
86		this.highlight.init();
87		if (typeof tracking !== 'undefined') {
88			tracking.init();
89		}
90	},
91
92	/**
93	 * Initialize elements in the specified scope.
94	 *
95	 * @param {string|JQuery} [scope] If not specified the scope is the whole DOM tree.
96	 */
97	initScope: function(scope) {
98		this.toggler.init(scope);
99		this.stripedTable.init(scope);
100		this.initTooltips(scope);
101		this.initPopovers(scope);
102		this.initOffcanvas(scope);
103		this.initNumberInputAutoSelectOnFocus(scope);
104		this.initModules(scope);
105		this.initFrameStyle(scope);
106		this.initDeviceBlocks(scope);
107	},
108
109	/**
110	 * Initialize tooltips.
111	 *
112	 * @param {(string|JQuery)} [scope]
113	 */
114	initTooltips: function(scope) {
115		$('[data-bs-toggle="tooltip"]', scope).each(function() {
116			new bootstrap.Tooltip(this); // eslint-disable-line no-new
117		});
118	},
119
120	/**
121	 * Initialize popovers.
122	 *
123	 * @param {(string|JQuery)} [scope]
124	 */
125	initPopovers: function(scope) {
126		$('[data-bs-toggle="popover"]', scope).each(function() {
127			new bootstrap.Popover(this); // eslint-disable-line no-new
128		});
129	},
130
131	/**
132	 * Initialize offcanvas.
133	 *
134	 * @param {(string|JQuery)} [scope]
135	 */
136	initOffcanvas: function(scope) {
137		$('.offcanvas', scope).each(function() {
138			new bootstrap.Offcanvas(this); // eslint-disable-line no-new
139		});
140	},
141
142	/**
143	 * Initialize number-inputs such that their content is auto-selected when the input gets focu
143s.
144	 *
145	 * @param {(string|JQuery)} [scope]
146	 */
147	initNumberInputAutoSelectOnFocus: function(scope) {
148		$('input[type="number"]', scope).on('focus', function() {
149			$(this)[0].select();
150		});
151	},
152
153	/**
154	 * Initialize registered modules.
155	 *
156	 * @param {(string|JQuery)} [scope]
157	 */
158	initModules: function(scope) {
159		for (let module of this.modules) {
160			if (typeof module.init === 'function') {
161				module.init(scope);
162			}
163		}
164	},
165
166	/**
167	 * Add frame parameter to all URL elements.
168	 * @param {(string|JQuery)} [scope]
169	 */
170	initFrameStyle: function(scope) {
171		if (this.frameStyle === '') {
172			return;
173		}
174
175		$('a[href]', scope).each(function() {
176			let href = $(this).attr('href');
177			if ( ! href.startsWith('mailto:') && ! href.startsWith('tel:')) {
178				$(this).attr('href', common.addFrameStyle(href));
179			}
180		});
181		$('form[action]', scope).each(function() {
182			let action = $(this).attr('action');
183			if ( ! action.includes('/ajax/')) {
184				$(this).attr('action', common.addFrameStyle(action));
185			}
186		});
187	},
188
189	/**
190	 * Display blocks based on the device.
191	 * Hides blocks for all other devices.
192	 * Detects the following devices:
193	 *	- apple (iPad, iPhone, iPod)
194	 *	- android (Android)
195	 *	- mobile (BlackBerry, Windows Phone, Opera Mini/Mobile, WebOS)
196	 *	- desktop (everything else)
197	 *
198	 * @param {string|JQuery} [scope] If not specified the scope is the whole DOM tree.
199	 */
200	initDeviceBlocks: function(scope) {
201		const blocks = $('[data-device]', scope).hide();
202		if (blocks.length === 0) {
203			return;
204		}
205		const userAgent = navigator.userAgent.toLowerCase();
206		if ((/ipad|iphone|ipod/i).test(userAgent)) { // Apple device
207			blocks.filter('[data-device~="apple"]').show();
208		}
209		else if ((/android/i).test(userAgent)) { // Android device
210			blocks.filter('[data-device~="android"]').show();
211		}
212		else if ((/blackberry|phone|mobile|opera mini|webos/i).test(userAgent)) { // Other mobile devices
213			blocks.filter('[data-device~="mobile"]').show();
214		}
215		else { // Probably desktop
216			blocks.filter('[data-device~="desktop"]').show();
217		}
218	},
219
220	/**
221	 * Add the frame parameter to the query string of a URL.
222	 *
223	 * @param {string} url
224	 * @returns {string}
225	 */
226	addFrameStyle: function(url) {
227		return url + (url.includes('?') ? '&' : '?') + 'frame=' + common.frameStyle;
228	},
229
230	/**
231	 * @var {string|null}
232	 */
233	languageCode: null,
234
235	/**
236	 * Format newlines for ajax responses.
237	 *
238	 * @param {string} message
239	 * @returns {string}
240	 */
241	formatAjaxMessage: function(message) {
242		return message.replaceAll('\\n', '\n').replaceAll('\n', '<br />');
243	},
244
245	/**
246	 * Get the code of the active language.
247	 *
248	 * @returns {string}
249	 */
250	getLanguageCode: function() {
251		if (this.languageCode === null) {
252			const locale = $('html').attr('lang');
253			if (locale.includes('-')) {
254				this.languageCode = locale.split('-')[0];
255			}
256			else {
257				this.languageCode = locale;
258			}
259		}
260
261		return this.languageCode;
262	},
263
264	/**
265	 * Get the value from a key-value in the hash part.
266	 *
267	 * @param {string} key
268	 * @param {string} [defaultValue]
269	 * @returns {string}
270	 */
271	getHashValue: function(key, defaultValue) {
272		if (window.location.hash && window.location.hash.length > 1) {
273			const hashValue = window.location.hash.substring(1); // remove the #
274			const hashes = hashValue.split('&');
275			for (let i = 0; i < hashes.length; i++) {
276				const hashData = hashes[i].split('=', 2);
277				if (hashData.length === 2) {
278					if (hashData[0] === key) {
279						return hashData[1];
280					}
281				}
282			}
283		}
284
285		return defaultValue;
286	},
287
288	/**
289	 * Get a resource.
290	 *
291	 * @param {string} name
292	 * @returns {string}
293	 */
294	getResource: function(name) {
295		if (resources[name] === undefined) {
296			console.error('Unknown template: ' + name); // eslint-disable-line no-console
297
298			return '[' + name + '] IS NOT DEFINED!';
299		}
300
301		return this.replaceCommonTemplateVariables(resources[name]);
302	},
303
304	/**
305	 * Replace common variables in a template.
306	 *
307	 * @param {string} string
308	 * @returns {string}
309	 */
310	replaceCommonTemplateVariables: function(string) {
311		return string
312			.replace('{i18nPrefix}', common.i18nPrefix);
313	},
314
315	/**
316	 * Inserts object data into a template.
317	 *
318	 * @param {object} object A simple data object.
319	 * @param {string} template A string with {key} markers where data should be inserted.
320	 * @param {boolean|object} [encode]
321	 * 		True => All values are encoded with HTML entities before insertion.
322	 * 		Object: Values with a key set to true are encoded. Example {name: true, alias: true}
323	 * @returns {string}
324	 */
325	insertObjectDataInTemplate: function(object, template, encode) {
326		let $converter;
327		if (encode) {
328			$converter = $('<div>');
329		}
330		for (const key in object) {
331			let value = object[key] === null ? '' : object[key];
332			if (typeof value === 'object') { // Ignore composite values
333				continue;
334			}
335			const regexp = new RegExp('{' + key + '}', 'g');
336			if (encode === true || (encode && encode[key])) {
337				value = $converter.text(value).html();
338			}
339			template = template.replace(regexp, value);
340		}
341
342		return template;
343	},
344
345	/**
346	 * Iterates over objects and inserts object data into the template for each object.
347	 *
348	 * @param {object[]} objects Array or object of simple data objects.
349	 * @param {string} template A string with {key} markers where data should be inserted.
350	 * @param {boolean} [encode] See common.insertObjectDataInTemplate()
351	 * @returns {string[]}
352	 */
353	iterateAndInsertObjectDataInTemplate: function(objects, template, encode) {
354		const result = [];
355		for (const object of objects) {
356			result.push(common.insertObjectDataInTemplate(object, template, encode));
357		}
358
359		return result;
360	},
361
362	/**
363	 * Request a URL using AJAX with loader etc.
364	 *
365	 * @param {AjaxLoaderOptions} options
366	 */
367	ajaxLoader: function(options) { // eslint-disable-line max-statements
368		let loader = null;
369		if (options.loader !== false) {
370			loader = options.loader;
371		}
372		if (loader) {
373			common.loader.show(loader);
374		}
375
376		let $container = null;
377		if (options.container) {
378			$container = $(options.container);
379		}
380		else if (loader) {
381			$container = $(loader);
382			if ($container.length === 0) {
383				$container = null;
384			}
385		}
386
387		const successFeedbackStyle = options.successFeedbackStyle === undefined ? 'toast' : options.successFeedbackStyle;
388		const notSuccessFeedbackStyle = options.notSuccessFeedbackStyle === undefined ? 'toast' : options.notSuccessFeedbackStyle;
389
390		let $feedback = false;
391		if (options.feedback !== false) {
392			$feedback = null;
393			if (options.feedback !== undefined) {
394				$feedback = $(options.feedback);
395			}
396			else if ($container) {
397				$feedback = $container.find('[data-type="feedback"]');
398			}
399			if ($feedback && $feedback.length < 1) {
400				$feedback = null;
401			}
402		}
403
404		// Hide old feedback and remove state
405		if ($feedback) {
406			$feedback.hide().removeClass('alert-success alert-danger');
407		}
408		if ($container) {
409			$container.find('.has-error').removeClass('has-error');
410		}
411
412		let xhr;
413		if (options.progress) {
414			xhr = function() {
415				const XMLHttpRequest = new window.XMLHttpRequest();
416				XMLHttpRequest.upload.addEventListener('progress', function(event) {
417					if (event.lengthComputable) {
418						options.progress(event);
419					}
420				}, false);
421
422				return XMLHttpRequest;
423			};
424		}
425
426		$.ajax({
427			url: this.prependBaseAjaxUrl(options.url),
428			type: options.method === undefined ? 'GET' : options.method,
429			data: options.data === undefined ? [] : options.data,
430			dataType: 'json',
431			async: options.async === undefined ? true : options.async,
432			contentType: options.contentType === undefined ? undefined : options.contentType,
433			processData: options.processData === undefined ? true : options.processData,
434			xhr: xhr,
435			success: function(response, textStatus, jqXHR) { // eslint-disable-line complexity
436				let successFeedback = false;
437				if (response === null) { // Fake a response
438					response = {
439						message: 'Received an empty response from the server.'
440					};
441				}
442				if (response.success) {
443					successFeedback = true;
444					if (options.success) {
445						options.success(response, textStatus, jqXHR);
446					}
447				}
448				else if (options.notSuccess) {
449					options.notSuccess(response, textStatus, jqXHR);
450				}
451
452				if (response.message !== undefined && response.message !== null && response.message !== '') {
453					if ($feedback) {
454						if (successFeedback) {
455							$feedback.addClass('alert-success');
456						}
457						else {
458							$feedback.addClass('alert-danger');
459						}
460						$feedback.html(common.formatAjaxMessage(response.message)).fadeIn('fast');
461					}
462					else if ($feedback === null && successFeedback && successFeedbackStyle !== false) {
463						messages.addSuccess(common.formatAjaxMessage(response.message), 'clean', successFeedbackStyle);
464					}
465					else if ($feedback === null && ! successFeedback && notSuccessFeedbackStyle !== false) {
466						messages.addError(common.formatAjaxMessage(response.message), 'clean', notSuccessFeedbackStyle);
467					}
468				}
469
470				if (response.messages !== undefined && response.messages !== null) {
471					let isFirst = true;
472					for (let messageType in response.messages) {
473						for (let message of response.messages[messageType]) {
474							messages.add(common.formatAjaxMessage(message), messageType, isFirst ? 'clean' : true);
475							isFirst = false;
476						}
477					}
478				}
479
480				if (response.redirectUrl !== undefined && response.redirectUrl) {
481					window.location.href = response.redirectUrl;
482				}
483			},
484			error: function(jqXHR, textStatus, errorThrown) {
485				// typically only one of textStatus or errorThrown will have info.
486				// this: the options for this ajax request
487
488				// Build the error message:
489				const message = ['Error: '];
490				if (textStatus !== '') {
491					message.push(textStatus + '<br/>');
492				}
493				if (errorThrown) {
494					message.push(errorThrown + '<br/>');
495				}
496				if (jqXHR.responseText) {
497					try {
498						const response = JSON.parse(jqXHR.responseText);
499						if (response.errorCode === undefined) {
500							message.push('<br/>' + jqXHR.responseText);
501						}
502						else {
503							message.push('<br/>' + response.errorCode + ' - ' + response.error);
504							if (response.trace) {
505								message.push('<br /><br />');
506								for (let traceIndex = 0; traceIndex < response.trace.length; traceIndex++) {
507									const trace = response.trace[traceIndex];
508									message.push('#' + traceIndex + ': ' + trace.class + trace.type + trace.function + '(');
509									message.push(trace.args.join(', ') + ')<br />');
510									message.push('in ' + trace.file + ' line ' + trace.line + '<br />');
511								}
512							}
513						}
514					}
515					catch (exception) {
516						message.push('<br/>' + jqXHR.responseText);
517					}
518				}
519				const messageString = message.join('');
520
521				if ($feedback) {
522					$feedback.addClass('alert-danger').html(messageString).fadeIn('fast');
523				}
524				else {
525					messages.addError(messageString, true, 'alert');
526				}
527			},
528			complete: function() {
529				if (loader) {
530					common.loader.hide(loader);
531				}
532				if (options.cleanup) {
533					options.cleanup();
534				}
535			}
536		});
537	},
538
539	/**
540	 * Submit a form using AJAX with loader etc.
541	 *
542	 * @param {AjaxFormSubmitterOptions} options
543	 */
544	ajaxFormSubmitter: function(options) {
545		let $form = null;
546		if (options.form !== undefined) {
547			$form = $(options.form);
548		}
549
550		const hasFileUpload = $form && $form.attr('enctype') === 'multipart/form-data';
551		let data;
552		if ($form) {
553			data = hasFileUpload ? new FormData($form.get(0)) : $form.serialize();
554		}
555		const defaultOptions = {
556			method: $form ? $form.attr('method') : 'POST',
557			loader: $form ? $form.closest('.loader-container') : undefined,
558			url: $form ? $form.attr('action') : undefined,
559			data: data,
560			notSuccess: $form ? function(response) {
561				if (response.content !== undefined && response.content !== null && response.content !== '') {
562					$form.replaceWith(response.content);
563				}
564			} : undefined,
565			processData: ! hasFileUpload,
566			contentType: hasFileUpload ? false : 'application/x-www-form-urlencoded; charset=UTF-8'
567		};
568		for (const key in defaultOptions) {
569			if (options[key] === undefined) {
570				options[key] = defaultOptions[key];
571			}
572		}
573
574		this.ajaxLoader(options);
575	},
576
577	/**
578	 * Submit the specified form using AJAX with loader etc.
579	 *
580	 * @param {HTMLFormElement} form
581	 * @param {AjaxFormSubmitterOptions} [options]
582	 */
583	submitForm: function(form, options) {
584		if (form.reportValidity() === false) {
585			$(form).addClass('was-validated');
586
587			return;
588		}
589
590		options = options || {};
591		options.form = form;
592		this.ajaxFormSubmitter(options);
593	},
594
595	/**
596	 * Parse a URL and prepend the base AJAX URL if needed.
597	 *
598	 * @param {string} url
599	 * @returns {string}
600	 */
601	prependBaseAjaxUrl: function(url) {
602		if (url.startsWith(this.baseAjaxUrl)
603			|| url.startsWith('https://')
604			|| url.startsWith('http://')) {
605			return url;
606		}
607
608		return this.baseAjaxUrl + url;
609	},
610
611	/**
612	 * Check all visible checkboxes matching namePrefix (unless they're all already checked - then uncheck them).
613	 *
614	 * @param {HTMLElement} element The element clicked. Used for finding the checkboxes in the same form.
615	 * @param {string} namePrefix
616	 * @param {string} containerSelector Optional. Default 'form'
617	 */
618	toggleAllCheckboxes: function(element, namePrefix, containerSelector) {
619		if (undefined === containerSelector) {
620			containerSelector = 'form';
621		}
622		const $container = $(element).closest(containerSelector);
623		const $checkboxes = $container.find('input[type="checkbox"][name^="' + namePrefix + '"]:visible');
624		if ($checkboxes.not(':checked').length > 0) {
625			$checkboxes.prop('checked', true);
626		}
627		else {
628			$checkboxes.prop('checked', false);
629		}
630	},
631
632	/**
633	 * Methods for showing a loader effect.
634	 */
635	loader: {
636
637		/**
638		 * Show a loader for the specified element.
639		 * It is preferred to use a selector, as the parent might have disappeared (if it is a select/input), which prevents the hide() method from working.
640		 *
641		 * @param {JQuery|string} element
642		 */
643		show: function(element) {
644			let $element, $parent, $loader;
645			if (typeof element === 'object') {
646				$element = element; // Already a JQuery
647			}
648			else {
649				$element = $(element);
650			}
651			if ($element.length === 0) {
652				return;
653			}
654			switch ($element.get(0).nodeName) {
655				case 'SELECT':
656				case 'INPUT':
657					$parent = $element.parent();
658					$loader = $('<span class="input-group-text" data-content="loader"><span class="spinner-border spinner-border-sm"></span></span>');
659					if ($parent.hasClass('input-group')) {
660						// Append the loader
661						$parent.append($loader);
662						$parent.data('loader', 'sibling');
663					}
664					else {
665						// Create an input group with the select/input and the loader
666						const index = $element.index();
667						const $inputGroup = $('<div class="input-group"></div>');
668						$inputGroup.append($element.detach());
669						$inputGroup.append($loader);
670						$inputGroup.data('loader', 'container');
671						if (index === 0) {
672							$parent.prepend($inputGroup);
673						}
674						else {
675							$parent.children().eq(index).before($inputGroup);
676						}
677						$parent.append($inputGroup);
678					}
679					$element.prop('disabled', true);
680					break;
681				case 'BUTTON':
682					$loader = $('<span class="spinner-border spinner-border-sm" data-content="loader"></span>');
683					$element.prepend($loader);
684					$element.prop('disabled', true);
685					break;
686				default:
687					// Old way - that does not work yet
688					if ($element.hasClass('loader-container')) {
689						$element.addClass('loader');
690					}
691					break;
692			}
693		},
694
695		/**
696		 * Hide a loader for the specified element.
697		 * It is preferred to use a selector, as the parent might have disappeared (if it is a select/input) when the show() method were called.
698		 *
699		 * @param {JQuery|string} element
700		 */
701		hide: function(element) {
702			let $element, $parent;
703			if (typeof element === 'object') {
704				$element = element; // Already a JQuery
705			}
706			else {
707				$element = $(element);
708			}
709			if ($element.length === 0) {
710				return;
711			}
712			switch ($element.get(0).nodeName) {
713				case 'SELECT':
714				case 'INPUT':
715					$parent = $element.parent();
716					switch ($parent.data('loader')) {
717						case 'container':
718							// We created it, so just remove it
719							$element.detach();
720							$parent.replaceWith($element);
721							break;
722						case 'sibling':
723							// It already existed, so remove our addition
724							$element.siblings('[data-content="loader"]').remove();
725							break;
726						default:
727							break;
728					}
729					$element.prop('disabled', false);
730					break;
731				case 'BUTTON':
732					$element.find('[data-content="loader"]').remove();
733					$element.prop('disabled', false);
734					break;
735				default:
736					// Old way - that does not work yet
737					$element.removeClass('loader');
738					break;
739			}
740		}
741
742	},
743
744	/**
745	 * Helper to easily hide/show content based on state of a select.
746	 *
747	 * Usage:
748	 * 		Add data-toggle="content" to the select or input that should trigger toggling.
749	 * 		Add style="display: none;" data-toggler="$togglerData" to the elements that should be toggled.
750	 * 		If the toggler is an input (radio or checkbox) then $togglerData is the ID of the input.
751	 * 		If the toggler is a select then $togglerData is "<$idOfSelect>:<$optionValue>".
752	 * 		By default, any required input, select or textarea hidden by this will also be disabled.
753	 * 		This can be changed by setting data-disable="false" (or data-disable="true" to disable all).
754	 *
755	 * @type object
756	 */
757	toggler: {
758
759		/**
760		 * Initialize the toggler.
761		 *
762		 * @param {string|JQuery} [scope]
763		 */
764		init: function(scope) {
765			if ( ! scope) {
766				$(document).on('change', '[data-toggle="content"]', this.onChange);
767			}
768			$('[data-toggle="content"]', scope).trigger('change');
769		},
770
771		/**
772		 * Event handler for the checkbox that toggles showing/hiding elements.
773		 */
774		onChange: function() {
775			let disable = $(this).data('disable');
776			let disableSelector = '';
777			if (disable === true) {
778				disableSelector = 'input,select,textarea';
779			}
780			else if (disable === undefined || disable === 'required') {
781				disableSelector = 'input[required],select[required],textarea[required]';
782			}
783			if (this.type === 'select-one' || this.type === 'select-multiple') {
784				const selectedValues = [];
785				$(this).find('option:selected').each(function() {
786					selectedValues.push($(this).val());
787				});
788				common.toggler.toggleValues(this.id, selectedValues, disableSelector);
789			}
790			else if (this.type === 'radio') {
791				$('[name="' + this.name + '"]').each(function() {
792					common.toggler.toggle(this.id, this.checked, disableSelector);
793				});
794			}
795			else {
796				common.toggler.toggle(this.id, this.checked, disableSelector);
797			}
798		},
799
800		/**
801		 * Toggle showing/hiding elements with the given data-toggler="<id>" depending on the checked.
802		 *
803		 * @param {String} id
804		 * @param {Boolean} checked
805		 * @param {string} disableSelector
806		 */
807		toggle: function(id, checked, disableSelector) {
808			const $elements = $('[data-toggler="' + id + '"]');
809			const $reverseElements = $('[data-toggler-reverse="' + id + '"]');
810			if (checked) {
811				$elements.show();
812				$reverseElements.hide();
813			}
814			else {
815				$elements.hide();
816				$reverseElements.show();
817			}
818			if (disableSelector !== '') {
819				$elements.find(disableSelector).prop('disabled', ! checked);
820				$reverseElements.find(disableSelector).prop('disabled', checked);
821			}
822		},
823
824		/**
825		 * Toggle showing elements with the given data-toggler="<id>:<value>" and hide all other values.
826		 * If an element should be shown if any of multiple values are selected, then data-toggler="<id>: <value> <value> ..." should be used.
827		 *
828		 * @param {String} id
829		 * @param {Array} selectedValues
830		 * @param {string} disableSelector
831		 */
832		toggleValues: function(id, selectedValues, disableSelector) {
833			const $elements = $('[data-toggler^="' + id + ':"]');
834			$elements.hide();
835			if (disableSelector !== '') {
836				$elements.find(disableSelector).prop('disabled', true);
837			}
838			for (const i in selectedValues) {
839				const $thisValue = $elements.filter('[data-toggler="' + id + ':' + selectedValues[i] + '"]'); // For this single value
840				const $otherValues = $elements.filter('[data-toggler~="' + selectedValues[i] + '"]'); // For multiple values, including this one
841				$thisValue.show();
842				$otherValues.show();
843				if (disableSelector !== '') {
844					$thisValue.find(disableSelector).prop('disabled', false);
845					$otherValues.find(disableSelector).prop('disabled', false);
846				}
847			}
848		}
849
850	}, // End toggler
851
852	/**
853	 * Methods related to highlighting
854	 */
855	highlight: {
856
857		/**
858		 * Initialize highlighting.
859		 */
860		init: function() {
861			const highlight = common.getHashValue('highlight');
862			if (highlight !== undefined) {
863				this.perform(highlight);
864			}
865		},
866
867		/**
868		 * Do the highlighting for the specified elements.
869		 *
870		 * @param {string} highlight
871		 */
872		perform: function(highlight) {
873			const identifiers = highlight.split(',');
874			let highlightedAny = false;
875			$('.highlight[data-highlight]').removeClass('highlight');
876
877			let $focusElement = null;
878			for (const identifier of identifiers) {
879				const $element = $('[data-highlight="' + identifier + '"]');
880				if ($element.length === 0) {
881					continue;
882				}
883
884				// Highlight
885				$element.addClass('highlight');
886
887				highlightedAny = true;
888				if ( ! $focusElement) {
889					$focusElement = $element;
890				}
891			}
892			if ( ! highlightedAny) {
893				return;
894			}
895
896			// Center it on screen
897			utils.scrollToElement($focusElement);
898		}
899
900	},
901
902	/**
903	 * Methods related to striping rows of a table were some rows might be hidden.
904	 * The table should have the class "table-striped-visible".
905	 * The striped rows will be given the class "striped".
906	 */
907	stripedTable: {
908
909		/**
910		 * Initialize the striping of the tables.
911		 *
912		 * @param {string|JQuery} [scope]
913		 */
914		init: function(scope) {
915			this.update('.table-striped-visible', scope);
916		},
917
918		/**
919		 * Update the striping of the specified table.
920		 *
921		 * @param {string} selector
922		 * @param {string|JQuery} [scope]
923		 */
924		update: function(selector, scope) {
925			$(selector, scope).each(function() {
926				$(this).find('tr:visible').each(function(index) {
927					if (index % 2 === 0) {
928						$(this).removeClass('striped');
929					}
930					else {
931						$(this).addClass('striped');
932					}
933				});
934			});
935		}
936
937	},
938
939	/**
940	 * Methods related to sessionStorage and localStorage.
941	 */
942	storage: {
943
944		/**
945		 * @var {boolean|null}
946		 */
947		hasFeature: null,
948
949		/**
950		 * Check if the browser even supports localStorage and sessionStorage.
951		 *
952		 * @returns {boolean}
953		 */
954		hasStorage: function() {
955			if (this.hasFeature === null) {
956				this.hasFeature = typeof Storage !== 'undefined';
957			}
958
959			return this.hasFeature;
960		},
961
962		/**
963		 * Get a value from the sessionStorage.
964		 *
965		 * @param {string} key
966		 * @returns {null|string}
967		 */
968		getSessionItem: function(key) {
969			if (this.hasStorage()) {
970				return sessionStorage.getItem(key);
971			}
972
973			return null;
974		},
975
976		/**
977		 * Set a value in the sessionStorage.
978		 *
979		 * @param {string} key
980		 * @param {string} value
981		 */
982		setSessionItem: function(key, value) {
983			if (this.hasStorage()) {
984				sessionStorage.setItem(key, value);
985			}
986		},
987
988		/**
989		 * Remove a value from the sessionStorage.
990		 *
991		 * @param {string} key
992		 */
993		removeSessionItem: function(key) {
994			if (this.hasStorage()) {
995				sessionStorage.removeItem(key);
996			}
997		},
998
999		/**
1000		 * Get a value from the localStorage.
1001		 *
1002		 * @param {string} key
1003		 * @returns {null|string}
1004		 */
1005		getLocalItem: function(key) {
1006			if (this.hasStorage()) {
1007				return localStorage.getItem(key);
1008			}
1009
1010			return null;
1011		},
1012
1013		/**
1014		 * Set a value in the localStorage.
1015		 *
1016		 * @param {string} key
1017		 * @param {string} value
1018		 */
1019		setLocalItem: function(key, value) {
1020			if (this.hasStorage()) {
1021				localStorage.setItem(key, value);
1022			}
1023		},
1024
1025		/**
1026		 * Remove a value from the localStorage.
1027		 *
1028		 * @param {string} key
1029		 */
1030		removeLocalItem: function(key) {
1031			if (this.hasStorage()) {
1032				localStorage.removeItem(key);
1033			}
1034		}
1035	}
1036
1037};

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.