1// SupportPal global namespace, we use App because the help widget uses SupportPal. 2var App = function () { 3 'use strict'; 4 5 // Create a public methods object 6 var modules = { 7 env: $('meta[name="environment"]').prop('content') || "production" 8 }; 9 10 /** 11 * Add a module or function to the namespace. 12 * 13 * @param {String} name The name of the module. 14 * @param {Object} obj The module object. 15 */ 16 modules.extend = function (name, obj) { 17 if (typeof modules[name] !== "undefined") { 18 throw new Error("A module with name '" + name + "' already exists."); 19 } 20 modules[name] = obj; 21 }; 22 23 // Return public methods object 24 return modules; 25}(); 26 27// flatpickr. 28if (typeof Lang !== 'undefined') { 29 flatpickr.localize({ 30 weekdays: { 31 shorthand: [Lang.get('general.sun'), Lang.get('general.mon'), Lang.get('general.tue'), Lang.get('general.wed'), Lang.get('general.thu'), Lang.get('general.fri'), Lang.get('general.sat')], 32 longhand: [Lang.get('general.sunday'), Lang.get('general.monday'), Lang.get('general.tuesday'), Lang.get('general.wednesday'), Lang.get('general.thursday'), Lang.get('general.friday'), Lang.get('general.saturday')] 33 }, 34 months: { 35 shorthand: [Lang.get('general.jan'), Lang.get('general.feb'), Lang.get('general.mar'), Lang.get('general.apr'), Lang.get('general.may'), Lang.get('general.jun'), Lang.get('general.jul'), Lang.get('general.aug'), Lang.get('general.sep'), Lang.get('general.oct'), Lang.get('general.nov'), Lang.get('general.dec')], 36 longhand: [Lang.get('general.january'), Lang.get('general.february'), Lang.get('general.march'), Lang.get('general.april'), Lang.get('general.may'), Lang.get('general.june'), Lang.get('general.july'), Lang.get('general.august'), Lang.get('general.september'), Lang.get('general.october'), Lang.get('general.november'), Lang.get('general.december')] 37 }, 38 firstDayOfWeek: 1, 39 ordinal: function (nth) { 40 var s = nth % 100; 41 if (s > 3 && s < 21) return Lang.get('general.ordinal_th'); 42 switch (s % 10) { 43 case 1: 44 return Lang.get('general.ordinal_st'); 45 case 2: 46 return Lang.get('general.ordinal_nd'); 47 case 3: 48 return Lang.get('general.ordinal_rd'); 49 default: 50 return Lang.get('general.ordinal_th'); 51 } 52 }, 53 rangeSeparator: Lang.get('general.range_separator'), 54 weekAbbreviation: Lang.get('general.week_abbr'), 55 scrollTitle: Lang.get('general.scroll_to_increment'), 56 toggleTitle: Lang.get('general.click_to_toggle'), 57 amPM: [Lang.get('general.am'), Lang.get('general.pm')], 58 yearAriaLabel: Lang.choice('general.year', 1), 59 hourAriaLabel: Lang.choice('general.hour', 1), 60 minuteAriaLabel: Lang.choice('general.min', 1), 61 time_24hr: true 62 }); 63} 64 65// Date picker. 66$.fn.datepicker = function (options) { 67 var defaults = { 68 // Convert from PHP date format to flatpickr format. 69 dateFormat: $('meta[name=date_format]').prop('content').replace('jS', 'J').replace(/([^a-zA-Z])/g, "\\\\$1"), 70 // RTL support for the arrows. 71 nextArrow: '<span class="sp:rtl:hidden"><i class="fa-solid fa-chevron-right" aria-hidden="true"></i></span><span class="sp:ltr:hidden"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i></span>', 72 prevArrow: '<span class="sp:rtl:hidden"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i></span><span class="sp:ltr:hidden"><i class="fa-solid fa-chevron-right" aria-hidden="true"></i></span>' 73 }; 74 return $(this).each(function () { 75 // Quick fix to stop flatpickr being loaded twice on an input 76 if (this.className.indexOf('flatpickr-input') === -1) { 77 $(this).flatpickr($.extend(true, defaults, options)); 78 } 79 }); 80}; 81 82// Time picker. 83$.fn.timepicker = function (options) { 84 var defaults = { 85 // Convert from PHP date format to flatpickr format. 86 dateFormat: $('meta[name=time_format]').prop('content').replace('g', 'h').replace('a', 'K').replace('A', 'K').replace(/([^a-zA-Z])/g, "\\\\$1"), 87 enableTime: true, 88 noCalendar: true, 89 time_24hr: !($('meta[name=time_format]').prop('content').indexOf('g') > -1) 90 }; 91 return $(this).each(function () { 92 // Quick fix to stop flatpickr being loaded twice on an input 93 if (this._flatpickr === undefined) { 94 $(this).flatpickr($.extend(true, defaults, options)); 95 } 96 }); 97}; 98
99// isWindow was deprecated in jQuery 3.3.0 100function isWindow(obj) { 101 return obj !== null && obj !== undefined && obj === obj.window; 102} 103 104// jQuery print event callback helpers. 105// - https://gist.github.com/shaliko/4110822#gistcomment-1543771 106// - https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/ 107$.fn.beforeprint = function (callback) { 108 return $(this).each(function () { 109 if (!isWindow(this)) { 110 return; 111 } 112 if (this.onbeforeprint !== undefined) { 113 $(this).on('beforeprint', callback); 114 } else if (this.matchMedia) { 115 this.matchMedia('print').addListener(callback); 116 } 117 }); 118}; 119$.fn.afterprint = function (callback) { 120 return $(this).each(function () { 121 if (!isWindow(this)) { 122 return; 123 } 124 if (this.onafterprint !== undefined) { 125 $(this).on('afterprint', callback); 126 } else if (this.matchMedia) { 127 $(this).one('mouseover', callback); // https://stackoverflow.com/a/15662720/2653593 128 } 129 }); 130}; 131 132// Polyfill for matches, closest and find (IE11), needed for tippy.js 133// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 134if (!Element.prototype.matches) { 135 Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 136} 137// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill 138if (!Element.prototype.closest) { 139 Element.prototype.closest = function (s) { 140 var el = this; 141 do { 142 if (el.matches(s)) return el; 143 el = el.parentElement || el.parentNode; 144 } while (el !== null && el.nodeType === 1); 145 return null; 146 }; 147} 148// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill 149if (!Array.prototype.find) { 150 Object.defineProperty(Array.prototype, 'find', { 151 value: function (predicate) { 152 // 1. Let O be ? ToObject(this value). 153 if (this == null) { 154 throw TypeError('"this" is null or not defined'); 155 } 156 var o = Object(this); 157 158 // 2. Let len be ? ToLength(? Get(O, "length")). 159 var len = o.length >>> 0; 160 161 // 3. If IsCallable(predicate) is false, throw a TypeError exception. 162 if (typeof predicate !== 'function') { 163 throw TypeError('predicate must be a function'); 164 } 165 166 // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 167 var thisArg = arguments[1]; 168 169 // 5. Let k be 0. 170 var k = 0; 171 172 // 6. Repeat, while k < len 173 while (k < len) { 174 // a. Let Pk be ! ToString(k). 175 // b. Let kValue be ? Get(O, Pk). 176 // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 177 // d. If testResult is true, return kValue. 178 var kValue = o[k]; 179 if (predicate.call(thisArg, kValue, k, o)) { 180 return kValue; 181 } 182 // e. Increase k by 1. 183 k++; 184 } 185 186 // 7. Return undefined. 187 return undefined; 188 }, 189 configurable: true, 190 writable: true 191 }); 192} 193 194// Reverse items in selector. 195// https://www.mail-archive.com/[email protected]/msg04272.html 196$.fn.reverse = function () { 197 return this.pushStack(this.get().reverse(), arguments); 198}; 199 200/** 201 * Add a new item to DOM container. This function expects a classname:input[name$="[id]"] to be present 202 * for every unique DOM item within the container. 203 * 204 * @param className 205 * @param container 206 * @param $idxElms 207 * @param attrMapperFn 208 * @returns {number} 209 */ 210function addNewItem(className, container, $idxElms, attrMapperFn) { 211 const $class = typeof className === 'string' ? $(className) : className; 212 $idxElms = typeof $idxElms !== 'undefined' ? $idxElms : $class.find(':input[name$="[id]"]'); 213 214 // Clone the element 215 var newElem = $class.first().clone(); 216 217 // Clear the input values from the cloned DOM 218 newElem.removeClass('first'); 219 220 // Ensure a unique key: find the highest index first and add one 221 var re = /\[(\d+)?]\[id]$/; 222 var m, 223 index = 0; 224 $idxElms.each(function () { 225 if ((m = re.exec($(this).attr('name'))) !== null) { 226 if (typeof m[1] != 'undefined') { 227 if ((m = parseInt(m[1])) >= index) { 228 index = m + 1; 229 } 230 } 231 } 232 }); 233 234 // Update all the indexes in the new element 235 newElem.find(':input, label').each(function () { 236 var elem = $(this); 237 elem.prop('disabled', false); 238 ['name', 'for', 'id'].map(function (attribute) { 239 var attr = elem.attr(attribute); 240 if (attrMapperFn) { 241 elem.attr(attribute, attrMapperFn(attr, index)); 242 } else { 243 if (/^\w+\[(\d+)?](\[[\w:-]+])*(\[\])?$/g.test(attr)) { 244 elem.attr(attribute, attr.replace(/\[(\d+)?]/, '[' + index + ']')); 245 } 246 } 247 }); 248 }); 249 250 // Where do we want to put it? 251 if (typeof container !== 'undefined') { 252 // Append cloned DOM to the end of the parent container 253 $(container).append(newElem); 254 } else { 255 // Append cloned DOM to the end of the list 256 $class.last().after(newElem); 257 } 258 259 // Make it visible 260 newElem.removeClass('sp:hidden'); 261 262 // Auto select first option of dropdowns - fix for firefox 263 newElem.find('select').each(function () { 264 $(this).find('option:first').prop('selected', 'selected'); 265 }); 266 return index; 267} 268 269// Wait for DOM to load before running the below. 270$(function () { 271 // Tooltip - tippy.js 272 tippy.delegate(document.body, { 273 content: function (reference) { 274 var title = reference.getAttribute('title'); 275 reference.removeAttribute('title'); 276 reference.setAttribute('data-tippy-content', title); 277 return title; 278 }, 279 onShow: function (instance) { 280 return instance.reference.hasAttribute('data-tippy-content') && !instance.reference.classList.contains('tox-edit-area__iframe') && !instance.reference.classList.contains('tox-collection__item') && (!instance.reference.classList.contains('tox-button') || instance.reference.classList.contains('tox-button--icon')) && instance.reference.getAttribute('data-tippy-content').length > 0; 281 }, 282 target: '[title]', 283 touch: ['hold', 500], 284 zIndex: 10052 285 }); 286 287 // Logout handler. 288 $('.logout-link').on('click', function (e) { 289 e.preventDefault(); 290 var $form = $('<form>', { 291 method: $(this).data('method'), 292 action: $(this).data('url') 293 }).hide().append($('<input>', { 294 type: "hidden", 295 name: "_token", 296 value: $('meta[name=csrf_token]').prop('content') 297 })); 298 $(document.body).append($form); 299 $form.trigger('submit'); 300 }); 301 302 // Global event events. 303 // Avoid polluting the $(document) space with too many event listeners, try to place as close to element as possible 304 // otherwise performance will be affected (https://api.jquery.com/on/). 305 $(document.body) 306 // Tabs toggling 307 .on('click', 'ul.sp-tabs li', function () { 308 // Don't do this if it's an actual link. 309 if ($(this).find('a[href]:not([href="#"])').length) { 310 return; 311 } 312 313 // Get name of tab 314 var name = $(this).attr('id'); 315 316 // Hide current div 317 $(this).parent().siblings('div.sp-tab-content').hide(); 318 // Show new div 319 $('#tab' + name).show(); 320 321 // Remove active from old tab 322 $(this).parent().find('li.sp-active').removeClass('sp-active'); 323 // Set to active 324 $(this).addClass('sp-active'); 325 }) 326 // For dropdowns 327 .on('click', '.sp-dropdown-container .sp-dropdown li.sp-dropdown-item-interactive', function (e) { 328 e.stopImmediatePropagation(); 329 }).on('click', '.sp-dropdown-container .sp-button, .sp-dropdown-container .sp-action, .sp-dropdown-container .sp-dropdown li', function () { 330 var $dropdown = $(this).parents('.sp-dropdown-container').find('.sp-dropdown'); 331 if ($dropdown.is(':visible')) { 332 $dropdown.hide(); 333 $dropdown.trigger('sp.dropdown.hide'); 334 } else { 335 $dropdown.show(); 336 $dropdown.trigger('sp.dropdown.show'); 337 } 338 }) 339 // Hide dropdown if clicking outside the dropdown div 340 .on('click', function (event) { 341 $('.sp-dropdown-container').not($(event.target).closest('.sp-dropdown-container')).find('.sp-dropdown:visible').hide(); 342 }) 343 // For opening/collapsing collapsible boxes 344 .on('click', '.sp-collapsible', function () { 345 $(this).find('.fa-solid.fa-chevron-down, .fa-solid.fa-chevron-up').toggleClass('fa-chevron-down fa-chevron-up'); 346 $(this).next().slideToggle('fast'); 347 }) 348 // Smooth scrolling for anchors 349 .on('click', 'a[href^="#"]', function (event) { 350 event.preventDefault(); 351 var target = document.getElementById($.attr(this, 'href').substring(1)); 352 if (target !== null) { 353 $('html, body, #content').animate({ 354 scrollTop: $(target).offset().top - $('header.sp:sticky').height() - 24 355 }, 500); 356 } 357 }) 358 // Close alerts 359 .on('click', '.sp-alert .sp-alert-close', function () { 360 $(this).parent().slideToggle(300); 361 }); 362 363 // Handle anchors on page load 364 const hash = window.location.hash.substring(1); 365 var target; 366 if (hash !== '' && (target = document.getElementById(hash)) !== null) { 367 // Now scroll to anchor 368 $('html, body, #content').animate({ 369 scrollTop: $(target).offset().top - $('header.sp:sticky').height() - 24 370 }, 500); 371 } 372 373 // Global AJAX setup handler to add the CSRF token to ALL POST requests. 374 $.ajaxPrefilter(function (options, originalOptions, jqXHR) { 375 if (options.type.toLowerCase() === "post" || options.type.toLowerCase() === "put" || options.type.toLowerCase() === "delete") { 376 jqXHR.setRequestHeader('X-CSRF-Token', $('meta[name=csrf_token]').prop('content')); 377 } 378 }); 379 380 // Register localisation settings. 381 if (typeof Lang !== 'undefined') { 382 // Hide/Show Password library defaults. 383 $.extend(true, $.fn.hideShowPassword.defaults, { 384 show: false, 385 innerToggle: 'focus', 386 states: { 387 shown: { 388 toggle: { 389 content: Lang.get('general.hide'), 390 attr: { 391 title: '' 392 } 393 } 394 }, 395 hidden: { 396 toggle: { 397 content: Lang.get('general.show'), 398 attr: { 399 title: '' 400 } 401 } 402 } 403 }, 404 toggle: { 405 className: 'sp-button-sm' 406 } 407 }); 408 $('input[type=password]').hideShowPassword(); 409 } 410 411 /** 412 * Get the form row that the error element sits in. 413 * 414 * @param {HTMLElement} element 415 * @returns {jQuery} 416 */ 417 function getErrorElementRow(element) { 418 return $(element).closest('.sp-form-row'); 419 } 420 421 /** 422 * Get the wrapper element around the element that failed validation. 423 * 424 * @param {HTMLElement} element 425 * @returns {jQuery} 426 */ 427 function getErrorElementWrapper(element) {
428 if ($(element).parents('.sp-validation-container').length) { 429 return $(element).parents('.sp-validation-container'); 430 } 431 if ($(element).parents('.sp-email-address').length) { 432 return $(element).parents('.sp-email-address'); 433 } 434 if ($(element).parents('.sp-phone-number').length) { 435 return $(element).parents('.sp-phone-number'); 436 } 437 var $row = getErrorElementRow(element); 438 return $row.find(':input:not(:button)').length > 1 && !$(element).parent('.sp-editor-container').length && !$(element).parent('.hideShowPassword-wrapper').length && !$(element).parent('.sp-input-translatable-container').length && !$(element).parent('.sp-input-group').length && !$(element).next().has('selectize') && $(element).prop('type') !== 'checkbox' && $(element).prop('type') !== 'radio' ? $(element) : $row; 439 } 440 441 /** 442 * Get the container that we're going to add the error message after. 443 * 444 * @param element 445 * @returns {*} 446 */ 447 function getErrorContainer(element) { 448 var position = element; 449 450 // If it's codemirror, show/hide button, phone input, input group container, recaptcha, a checkbox or radio, add after parent 451 if (element.parent('.sp-editor-container').length || element.parent('.codemirror-box').length || element.parent('.hideShowPassword-wrapper').length || element.parent('.iti').length || element.parent('.sp-input-group').length || element.parent().parent('.g-recaptcha').length || element.prop('type') === 'checkbox' || element.prop('type') === 'radio') { 452 position = element.parent(); 453 } 454 455 // If it's editor or tom-select, add after sibling. 456 if (element.next().hasClass('tox-tinymce') || element.next().hasClass('ts-control')) { 457 position = element.next(); 458 } 459 460 // If it's got a translatable model, add afterwards. 461 if (element.next().hasClass('fa-language')) { 462 position = element.next().next(); 463 } 464 465 // If it's phone input, add after row. 466 if (element.parent('.iti').length) { 467 position = position.parent().parent(); 468 } 469 470 // If it's a checkbox or radio, make sure we put the error after the last element. 471 if (position.is('label') && (element.prop('type') === 'checkbox' || element.prop('type') === 'radio')) { 472 position = position.siblings().last('label'); 473 } 474 return position; 475 } 476 477 // jQuery validate. 478 $.validator.setDefaults({ 479 ignore: '[contenteditable=\'true\']', 480 errorElement: 'span', 481 errorClass: 'sp-input-error', 482 wrapper: 'div', 483 errorPlacement: function (error, element) { 484 error.insertAfter(getErrorContainer(element)); 485 }, 486 highlight: function (element, errorClass, validClass) { 487 getErrorElementWrapper(element).addClass('sp-input-has-error'); 488 }, 489 unhighlight: function (element, errorClass, validClass) { 490 getErrorElementWrapper(element).removeClass('sp-input-has-error'); 491 492 // If phone input, it's the next element which is the hidden input. 493 if ($(element).parent('.iti').length) { 494 element = $(element).next(); 495 } 496 497 // Hide error if it's "pending" (remote validation). 498 var describer = $(element).attr("aria-describedby"); 499 if (describer) { 500 getErrorElementRow(element).find('#' + this.escapeCssMeta(describer)).hide(); 501 } 502 }, 503 success: function (label, element) { 504 // 505 }, 506 // Custom submit handler. 507 submitHandler: function (form) { 508 // CodeMirror saves when form submit events are fired. We need to manually trigger an event to update it, 509 // so that jquery-validation has the correct form data. 510 $(form).find('.sc-textarea').each((i, el) => el.sourcecode.save()); 511 $(form).find('input[type="submit"], button[type="submit"]').prop('disabled', true); 512 513 // Validate the form. 514 if (this.form()) { 515 // Handle remote validation rules. 516 if (this.pendingRequest) { 517 this.formSubmitted = true; 518 return false; 519 } 520 521 // Form is valid, trigger form submission event. 522 $(form).trigger('form:submit'); 523 524 // If the form is marked as using AJAX, then return false to prevent the page refreshing. 525 if (typeof $(form).data('ajax') !== "undefined") { 526 return false;
527 } 528 529 // Submit the form (will cause the page to refresh etc). 530 form.submit(); 531 } else { 532 $(form).find('input[type="submit"], button[type="submit"]').prop('disabled', false); 533 this.focusInvalid(); 534 return false; 535 } 536 }, 537 focusInvalid: true, 538 invalidHandler: function (event, validator) { 539 // Enable submit button again (necessary for invalid remote validation). 540 $(this).find(':submit').prop('disabled', false); 541 }, 542 showErrors: function (errorMap, errorList) { 543 // Run default error message handler. 544 this.defaultShowErrors(); 545 546 // Get the form as a jQuery object. 547 var $form = $(this.currentForm); 548 549 // Move the screen to show the first error message. 550 $.each(errorList, function (key, error) { 551 var $elm = getErrorContainer($(error.element)); 552 553 // If the error is on another tab, switch to it. 554 if ($elm.parents('.sp-tab-content').length) { 555 var id = $elm.parents('.sp-tab-content').attr('id'); 556 if (typeof id !== 'undefined' && id.substring(0, 3) === 'tab') { 557 $('.sp-tabs li#' + id.substring(3)).trigger('click'); 558 } 559 } 560 561 // If the error is in a translatable model, open it. 562 if ($elm.parent().hasClass('sp-translation')) { 563 var $translatableInput = $elm.parents('.sp-input-translatable'); 564 $translatableInput.find('.fa-language').trigger('click'); 565 566 // If the translation is not visible, select it from the dropdown. 567 if (!$elm.parent().is(':visible')) { 568 var $selectize = $translatableInput.find('select[name=sp-translation-add]')[0].selectize; 569 if (typeof $selectize !== 'undefined') { 570 $selectize.addItem($elm.parent().data('locale'), false); 571 } 572 } 573 } 574 $form.trigger('showErrors', [$elm, error]); 575 576 // Make sure the element is visible otherwise it will scroll to the top of the page. 577 if ($elm.is(':visible')) { 578 $elm[0].scrollIntoView({ 579 behavior: 'smooth', 580 block: 'center' 581 }); 582 583 // Break loop. 584 return false; 585 } 586 }); 587 } 588 }); 589});
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.