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.