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="rtl:sp-hidden"><i class="fas fa-chevron-right" aria-hidden="true"></i></span><span class="ltr:sp-hidden"><i class="fas fa-chevron-left" aria-hidden="true"></i></span>', 72 prevArrow: '<span class="rtl:sp-hidden"><i class="fas fa-chevron-left" aria-hidden="true"></i></span><span class="ltr:sp-hidden"><i class="fas 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 * @returns {number} 207 */ 208function addNewItem(className, container) { 209 // Clone the element 210 var newElem = $(className + ':first').clone(); 211 212 // Clear the input values from the cloned DOM 213 newElem.removeClass('first'); 214 215 // Update the index.. god damn you Laravel 216 // Longwinded but ensures a unique key 217 // Find the highest index first and add one 218 var re = /^\w+\[(\d+)?]\[\w+]?$/; 219 var m, 220 index = 0; 221 $(className + ' :input[name$="[id]"]').each(function () { 222 if ((m = re.exec($(this).attr('name'))) !== null) { 223 if (typeof m[1] != 'undefined') { 224 if ((m = parseInt(m[1])) >= index) { 225 index = m + 1; 226 } 227 } 228 } 229 }); 230 231 // Update all the indexes in the new element 232 newElem.find(':input, label').each(function () { 233 var elem = $(this); 234 elem.prop('disabled', false); 235 ['name', 'for', 'id'].map(function (attribute) { 236 var attr = elem.attr(attribute); 237 if (/^\w+\[(\d+)?](\[[\w:-]+])*(\[\])?$/g.test(attr)) { 238 elem.attr(attribute, attr.replace(/\[(\d+)?]/, '[' + index + ']')); 239 } 240 }); 241 }); 242 243 // Where do we want to put it? 244 if (typeof container !== 'undefined') { 245 // Append cloned DOM to the end of the parent container 246 $(container).append(newElem); 247 } else { 248 // Append cloned DOM to the end of the list 249 $(className + ':last').after(newElem); 250 } 251 252 // Make it visible 253 newElem.removeClass('sp-hidden'); 254 255 // Auto select first option of dropdowns - fix for firefox 256 newElem.find('select').each(function () { 257 $(this).find('option:first').prop('selected', 'selected'); 258 }); 259 return index; 260} 261 262// Wait for DOM to load before running the below. 263$(function () { 264 // Tooltip - tippy.js 265 tippy.delegate(document.body, { 266 content: function (reference) { 267 var title = reference.getAttribute('title'); 268 reference.removeAttribute('title'); 269 reference.setAttribute('data-tippy-content', title); 270 return title; 271 }, 272 onShow: function (instance) { 273 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; 274 }, 275 target: '[title]', 276 touch: ['hold', 500], 277 zIndex: 10052 278 }); 279 280 // Logout handler. 281 $('.logout-link').on('click', function (e) { 282 e.preventDefault(); 283 var $form = $('<form>', { 284 method: $(this).data('method'), 285 action: $(this).data('url') 286 }).hide().append($('<input>', { 287 type: "hidden", 288 name: "_token", 289 value: $('meta[name=csrf_token]').prop('content') 290 })); 291 $(document.body).append($form); 292 $form.submit(); 293 }); 294 295 // Global event events. 296 // Avoid polluting the $(document) space with too many event listeners, try to place as close to element as possible 297 // otherwise performance will be affected (https://api.jquery.com/on/). 298 $(document.body) 299 // Tabs toggling 300 .on('click', 'ul.sp-tabs li', function () { 301 // Don't do this if it's an actual link. 302 if ($(this).find('a[href]:not([href="#"])').length) { 303 return; 304 } 305 306 // Get name of tab 307 var name = $(this).attr('id'); 308 309 // Hide current div 310 $(this).parent().siblings('div.sp-tab-content').hide(); 311 // Show new div 312 $('#tab' + name).show(); 313 314 // Remove active from old tab 315 $(this).parent().find('li.sp-active').removeClass('sp-active'); 316 // Set to active 317 $(this).addClass('sp-active'); 318 }) 319 // For dropdowns 320 .on('click', '.sp-dropdown-container .sp-dropdown li.sp-dropdown-item-interactive', function (e) { 321 e.stopImmediatePropagation(); 322 }).on('click', '.sp-dropdown-container .sp-button, .sp-dropdown-container .sp-action, .sp-dropdown-container .sp-dropdown li', function () { 323 var $dropdown = $(this).parents('.sp-dropdown-container').find('.sp-dropdown'); 324 if ($dropdown.is(':visible')) { 325 $dropdown.hide(); 326 $dropdown.trigger('sp.dropdown.hide'); 327 } else { 328 $dropdown.show(); 329 $dropdown.trigger('sp.dropdown.show'); 330 } 331 }) 332 // Hide dropdown if clicking outside the dropdown div 333 .on('click', function (event) { 334 $('.sp-dropdown-container').not($(event.target).closest('.sp-dropdown-container')).find('.sp-dropdown:visible').hide(); 335 }) 336 // For opening/collapsing collapsible boxes 337 .on('click', '.sp-collapsible', function () { 338 $(this).find('.fas.fa-chevron-down, .fas.fa-chevron-up').toggleClass('fa-chevron-down fa-chevron-up'); 339 $(this).next().toggle(500); 340 }) 341 // Smooth scrolling for anchors 342 .on('click', 'a[href^="#"]', function (event) { 343 event.preventDefault(); 344 var target = document.getElementById($.attr(this, 'href').substring(1)); 345 if (target !== null) { 346 $('html, body, #content').animate({ 347 scrollTop: $(target).offset().top - $('header.sp-sticky').height() - 24 348 }, 500); 349 } 350 }) 351 // Close alerts 352 .on('click', '.sp-alert .sp-alert-close', function () { 353 $(this).parent().slideToggle(300); 354 }); 355 356 // Handle anchors on page load 357 const hash = window.location.hash.substring(1); 358 var target; 359 if (hash !== '' && (target = document.getElementById(hash)) !== null) { 360 // Now scroll to anchor 361 $('html, body, #content').animate({ 362 scrollTop: $(target).offset().top - $('header.sp-sticky').height() - 24 363 }, 500); 364 } 365 366 // Global AJAX setup handler to add the CSRF token to ALL POST requests. 367 $.ajaxPrefilter(function (options, originalOptions, jqXHR) { 368 if (options.type.toLowerCase() === "post" || options.type.toLowerCase() === "put" || options.type.toLowerCase() === "delete") { 369 jqXHR.setRequestHeader('X-CSRF-Token', $('meta[name=csrf_token]').prop('content')); 370 } 371 }); 372 373 // Register localisation settings. 374 if (typeof Lang !== 'undefined') { 375 // Timeago 376 timeago.register('supportpal', function (number, index, total_sec) { 377 // Convert weeks to days. 378 if ([8, 9].indexOf(index) !== -1) { 379 total_sec = parseInt(total_sec / 86400); 380 } 381 return [[Lang.get('general.just_now'), Lang.get('general.shortly')], [L
381ang.choice('general.minutes_ago', 1, { 382 'number': 1 383 }), Lang.choice('general.in_minutes', 1, { 384 'number': 1 385 })], [Lang.choice('general.minutes_ago', 1, { 386 'number': 1 387 }), Lang.choice('general.in_minutes', 1, { 388 'number': 1 389 })], [Lang.choice('general.minutes_ago', 2, { 390 'number': '%s' 391 }), Lang.choice('general.in_minutes', 2, { 392 'number': '%s' 393 })], [Lang.choice('general.hours_ago', 1, { 394 'number': 1 395 }), Lang.choice('general.in_hours', 1, { 396 'number': 1 397 })], [Lang.choice('general.hours_ago', 2, { 398 'number': '%s' 399 }), Lang.choice('general.in_hours', 2, { 400 'number': '%s' 401 })], [Lang.choice('general.days_ago', 1, { 402 'number': 1 403 }), Lang.choice('general.in_days', 1, { 404 'number': 1 405 })], [Lang.choice('general.days_ago', 2, { 406 'number': '%s' 407 }), Lang.choice('general.in_days', 2, { 408 'number': '%s' 409 })], [Lang.choice('general.days_ago', 2, { 410 'number': total_sec 411 }), Lang.choice('general.in_days', 2, { 412 'number': total_sec 413 })], [Lang.choice('general.days_ago', 2, { 414 'number': total_sec 415 }), Lang.choice('general.in_days', 2, { 416 'number': total_sec 417 })], [Lang.choice('general.months_ago', 1, { 418 'number': 1 419 }), Lang.choice('general.in_months', 1, { 420 'number': 1 421 })], [Lang.choice('general.months_ago', 2, { 422 'number': '%s' 423 }), Lang.choice('general.in_months', 2, { 424 'number': '%s' 425 })], [Lang.choice('general.years_ago', 1, { 426 'number': 1 427 }), Lang.choice('general.in_years', 1, { 428 'number': 1 429 })], [Lang.choice('general.years_ago', 2, { 430 'number': '%s' 431 }), Lang.choice('general.in_years', 2, { 432 'number': '%s' 433 })]][index]; 434 }); 435 436 // Initialise global timeAgo variable (used in several other files). 437 window.timeAgo = new timeago(); 438 timeAgo.setLocale("supportpal"); 439 440 // Hide/Show Password library defaults. 441 $.extend(true, $.fn.hideShowPassword.defaults, { 442 show: false, 443 innerToggle: 'focus', 444 states: { 445 shown: { 446 toggle: { 447 content: Lang.get('general.hide'), 448 attr: { 449 title: '' 450 } 451 } 452 }, 453 hidden: { 454 toggle: { 455 content: Lang.get('general.show'), 456 attr: { 457 title: '' 458 } 459 } 460 } 461 }, 462 toggle: { 463 className: 'sp-button-sm' 464 } 465 }); 466 $('input[type=password]').hideShowPassword(); 467 468 // Selectize defaults. 469 $.extend(true, $.fn.selectize.defaults, { 470 render: { 471 option_create: function (data, escape) { 472 var item = Lang.get('core.add_selectize', { 473 item: escape(data.input) 474 }); 475 return '<div class="create">' + item + '</div>'; 476 } 477 } 478 }); 479 } 480 481 /** 482 * Get the form row that the error element sits in. 483 * 484 * @param {HTMLElement} element 485 * @returns {jQuery} 486 */ 487 function getErrorElementRow(element) { 488 return $(element).closest('.sp-form-row'); 489 } 490 491 /** 492 * Get the wrapper element around the element that failed validation. 493 * 494 * @param {HTMLElement} element 495 * @returns {jQuery} 496 */ 497 function getErrorElementWrapper(element) { 498 if ($(element).parents('.sp-validation-container').length) { 499 return $(element).parents('.sp-validation-container'); 500 } 501 if ($(element).parents('.sp-email-address').length) { 502 return $(element).parents('.sp-email-address'); 503 } 504 if ($(element).parents('.sp-phone-number').length) { 505 return $(element).parents('.sp-phone-number'); 506 } 507 var $row = getErrorElementRow(element); 508 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; 509 } 510 511 /** 512 * Get the container that we're going to add the error message after. 513 * 514 * @param element 515 * @returns {*} 516 */ 517 function getErrorContainer(element) { 518 var position = element; 519 520 // If it's codemirror, show/hide button, phone input, input group container, recaptcha, a checkbox or radio, add after parent 521 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') { 522 position = element.parent(); 523 } 524 525 // If it's editor, selectize, add after sibling. 526 if (element.next().hasClass('tox-tinymce') || element.next().hasClass('selectize-control')) { 527 position = element.next(); 528 } 529 530 // If it's got a translatable model, add afterwards. 531 if (element.next().hasClass('fa-language')) { 532 position = element.next().next(); 533 } 534 535 // If it's phone input, add after row. 536 if (element.parent('.iti').length) { 537 position = position.parent().parent(); 538 } 539 540 // If it's a checkbox or radio, make sure we put the error after the last element. 541 if (position.is('label') && (element.prop('type') === 'checkbox' || element.prop('type') === 'radio')) { 542 position = position.siblings().last('label'); 543 } 544 return position; 545 } 546 547 // jQuery validate. 548 $.validator.setDefaults({ 549 ignore: '[contenteditable=\'true\']', 550 errorElement: 'span', 551 errorClass: 'sp-input-error', 552 wrapper: 'div', 553 errorPlacement: function (error, element) { 554 error.insertAfter(getErrorContainer(element)); 555 }, 556 highlight: function (element, errorClass, validClass) { 557 getErrorElementWrapper(element).addClass('sp-input-has-error'); 558 }, 559 unhighlight: function (element, errorClass, validClass) { 560 getErrorElementWrapper(element).removeClass('sp-input-has-error'); 561 562 // If phone input, it's the next element which is the hidden input. 563 if ($(element).parent('.iti').length) { 564 element = $(element).next(); 565 } 566 567 // Hide error if it's "pending" (remote validation). 568 var describer = $(element).attr("aria-describedby"); 569 if (describer) { 570 getErrorElementRow(element).find('#' + this.escapeCssMeta(describer)).hide(); 571 } 572 }, 573 success: function (label, element) { 574 // 575 }, 576 // Custom submit handler. 577 submitHandler: function (form) { 578 // CodeMirror saves when form submit events are fired. We need to manually trigger an event to update it, 579 // so that jquery-validation has the correct form data. 580 $(form).find('.sc-textarea').each((i, el) => el.sourcecode.save()); 581 $(form).find('input[type="submit"], button[type="submit"]').prop('disabled', true); 582 583 // Validate the form. 584 if (this.form()) { 585 // Handle remote validation rules. 586 if (this.pendingRequest) { 587 this.formSubmitted = true; 588 return false;
589 } 590 591 // Form is valid, trigger form submission event. 592 $(form).trigger('form:submit'); 593 594 // If the form is marked as using AJAX, then return false to prevent the page refreshing. 595 if (typeof $(form).data('ajax') !== "undefined") { 596 return false; 597 } 598 599 // Submit the form (will cause the page to refresh etc). 600 form.submit(); 601 } else { 602 $(form).find('input[type="submit"], button[type="submit"]').prop('disabled', false); 603 this.focusInvalid(); 604 return false; 605 } 606 }, 607 focusInvalid: true, 608 invalidHandler: function (event, validator) { 609 // Enable submit button again (necessary for invalid remote validation). 610 $(this).find(':submit').prop('disabled', false); 611 }, 612 showErrors: function (errorMap, errorList) { 613 // Run default error message handler. 614 this.defaultShowErrors(); 615 616 // Move the screen to show the first error message. 617 $.each(errorList, function (key, error) { 618 var $elm = getErrorContainer($(error.element)); 619 620 // If the error is on another tab, switch to it. 621 if ($elm.parents('.sp-tab-content').length) { 622 var id = $elm.parents('.sp-tab-content').attr('id'); 623 if (typeof id !== 'undefined' && id.substring(0, 3) === 'tab') { 624 $('.sp-tabs li#' + id.substring(3)).trigger('click'); 625 } 626 } 627 628 // If the error is in a translatable model, open it. 629 if ($elm.parent().hasClass('sp-translation')) { 630 var $translatableInput = $elm.parents('.sp-input-translatable'); 631 $translatableInput.find('.fa-language').click(); 632 633 // If the translation is not visible, select it from the dropdown. 634 if (!$elm.parent().is(':visible')) { 635 var $selectize = $translatableInput.find('select[name=sp-translation-add]')[0].selectize; 636 if (typeof $selectize !== 'undefined') { 637 $selectize.addItem($elm.parent().data('locale'), false); 638 } 639 } 640 } 641 642 // Make sure the element is visible otherwise it will scroll to the top of the page. 643 if ($elm.is(':visible')) { 644 $('html, body, #content').animate({ 645 scrollTop: $elm.position().top - 44 646 }, 1000); 647 648 // Break loop. 649 return false; 650 } 651 }); 652 } 653 }); 654});
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.