1/*! 2 jQuery Wookmark plugin 3 @name wookmark.js 4 @author Christoph Ono ([email protected] or @gbks) 5 @author Sebastian Helzle ([email protected] or @sebobo) 6 @version 2.0.0 7 @date 02/15/2015 8 @category jQuery plugin 9 @copyright (c) 2009-2015 Christoph Ono (www.wookmark.com) 10 @license Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. 11*/ 12/*global define, window, jQuery*/ 13/*jslint plusplus: true, bitwise: true */ 14(function (factory) { 15 if (typeof define === 'function' && define.amd) { 16 define(['window', 'document'], factory); 17 } else { 18 factory(window, document); 19 } 20}(function (window, document) { 21 22 // Wookmark default options 23 // ------------------------ 24 var defaultOptions = { 25 align: 'center', 26 autoResize: true, 27 comparator: null, 28 direction: undefined, 29 ignoreInactiveItems: true, 30 inactiveClass: 'wookmark-inactive', 31 itemSelector: undefined, 32 itemWidth: 0, 33 fillEmptySpace: false, 34 flexibleWidth: 0, 35 offset: 5, 36 outerOffset: 0, 37 onLayoutChanged: undefined, 38 placeholderClass: 'wookmark-placeholder', 39 possibleFilters: [], 40 resizeDelay: 50, 41 verticalOffset: undefined 42 }; 43 44 // Helper functions 45 // ---------------- 46 47 // Bind function to set the context for the Wookmark instance function 48 function __bind(fn, me) { 49 return function () { 50 return fn.apply(me, arguments); 51 }; 52 } 53 54 // Function for executing css writes to dom on the next animation frame if supported 55 var executeNextFrame = window.requestAnimationFrame || function (callback) { callback(); }; 56 57 // Update multiple css values on an object 58 function setCSS(el, properties) { 59 var key; 60 for (key in properties) { 61 if (properties.hasOwnProperty(key)) { 62 el.style[key] = properties[key]; 63 } 64 } 65 } 66 67 // Update the css properties of multiple elements at the same time 68 // befor the browsers next animation frame. 69 // The parameter `data` has to be an array containing objects, each 70 // with the element and the desired css properties. 71 function bulkUpdateCSS(data) { 72 executeNextFrame(function () { 73 var i, item; 74 for (i = 0; i < data.length; i++) { 75 item = data[i]; 76 setCSS(item.el, item.css); 77 } 78 }); 79 } 80 81 // Remove whitespace around filter names 82 function cleanFilterName(filterName) { 83 return filterName.replace(/^\s+|\s+$/g, '').toLowerCase(); 84 } 85 86 // Remove listener from an element (IE8 compatible) 87 function removeEventListener(el, eventName, handler) { 88 if (el.removeEventListener) { 89 el.removeEventListener(eventName, handler); 90 } else { 91 el.detachEvent('on' + eventName, handler); 92 } 93 } 94 95 // Add listener to an element (IE8 compatible) 96 function addEventListener(el, eventName, handler) { 97 removeEventListener(el, eventName, handler); 98 if (el.addEventListener) { 99 el.addEventListener(eventName, handler); 100 } else { 101 el.attachEvent('on' + eventName, function () { 102 handler.call(el); 103 }); 104 } 105 } 106 107 // Checks if element `el` is not visible in the browser 108 function isHidden(el) { 109 return el.offsetParent === null; 110 } 111 112 // Returns the elements height without margin 113 function getHeight(el) { 114 return el.offsetHeight; 115 } 116 117 // Returns the elements width without margin 118 function getWidth(el) { 119 return el.offsetWidth; 120 } 121 122 // Return true if element has class 123 function hasClass(el, className) { 124 if (el.classList) { 125 return el.classList.contains(className); 126 } 127 return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); 128 } 129 130 // Add class to element (IE8+) 131 function addClass(el, className) { 132 if (el.classList) { 133 el.classList.add(className); 134 } else { 135 el.className += ' ' + className; 136 } 137 } 138 139 // Remove class from element (IE8+) 140 function removeClass(el, className) { 141 if (el.classList) { 142 el.classList.remove(className); 143 } else { 144 el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 145 } 146 } 147 148 // Get value of specified data attribute 149 function getData(el, attr, isInt, prefix) { 150 if (prefix === undefined) { 151 prefix = 'wookmark-'; 152 } 153 var val = el.getAttribute('data-' + prefix + attr); 154 if (isInt === true) { 155 return parseInt(val, 10); 156 } 157 return val; 158 } 159 160 // Set value of specified data attribute 161 function setData(el, attr, val, prefix) { 162 if (prefix === undefined) { 163 prefix = 'wookmark-'; 164 } 165 el.setAttribute('data-' + prefix + attr, val); 166 } 167 168 // Remove duplicates from given array 169 function removeDuplicates(items) { 170 var temp = {}, result = [], x, i = items.length; 171 while (i--) { 172 x = getData(items[i], 'id', true); 173 if (!temp.hasOwnProperty(x)) { 174 temp[x] = 1; 175 result.push(items[i]); 176 } 177 } 178 return result; 179 } 180 181 // Get the computed style from an element (IE 8 compatible) 182 function getStyle(el, prop) { 183 return window.getComputedStyle !== undefined ? window.getComputedStyle(el, null).getPropertyValue(prop) : el.currentStyle[prop]; 184 } 185 186 187 // IE 8 compatible indexOf 188 function indexOf(items, item) { 189 var len = items.length, i; 190 for (i = 0; i < len; i++) { 191 if (items[i] === item) { 192 return i; 193 } 194 } 195 return -1; 196 } 197 198 // Main wookmark plugin class 199 // -------------------------- 200 function Wookmark(container, options) { 201 options = options || {}; 202 203 if (typeof container === 'string') { 204 container = document.querySelector(container); 205 } 206 207 // Instance variables. 208 this.container = container; 209 this.columns = this.containerWidth = this.resizeTimer = null;
210 this.activeItemCount = 0; 211 this.placeholders = []; 212 this.itemHeightsInitialized = false; 213 this.itemHeightsDirty = false; 214 this.elementTag = 'div'; 215 216 // Bind instance methods 217 this.initItems = __bind(this.initItems, this); 218 this.updateOptions = __bind(this.updateOptions, this); 219 this.onResize = __bind(this.onResize, this); 220 this.onRefresh = __bind(this.onRefresh, this); 221 this.getItemWidth = __bind(this.getItemWidth, this); 222 this.layout = __bind(this.layout, this); 223 this.layoutFull = __bind(this.layoutFull, this); 224 this.layoutColumns = __bind(this.layoutColumns, this); 225 this.filter = __bind(this.filter, this); 226 this.clear = __bind(this.clear, this); 227 this.getActiveItems = __bind(this.getActiveItems, this); 228 this.refreshPlaceholders = __bind(this.refreshPlaceholders, this); 229 this.sortElements = __bind(this.sortElements, this); 230 this.updateFilterClasses = __bind(this.updateFilterClasses, this); 231 232 // Initialize children of the container 233 this.initItems(); 234 235 // Initial update and layout 236 this.container.style.display = 'block'; 237 this.updateOptions(options); 238 239 // Collect filter classes after items have been initialized 240 this.updateFilterClasses(); 241 242 // Listen to resize event of the browser if enabled 243 if (this.autoResize) { 244 addEventListener(window, 'resize', this.onResize); 245 } 246 247 // Listen to external refresh event 248 addEventListener(this.container, 'refreshWookmark', this.onRefresh); 249 } 250 251 // Get all valid children of the container object and store them 252 Wookmark.prototype.initItems = function () { 253 // By select all children of the container if no selector is specified 254 if (this.itemSelector === undefined) { 255 var items = [], child, children = this.container.children, 256 i = children.length; 257 while (i--) { 258 child = children[i]; 259 // Skip comment nodes on IE8 260 if (child.nodeType !== 8)Â { 261 // Show item 262 child.style.display = ''; 263 setData(child, 'id', i); 264 items.unshift(child); 265 } 266 } 267 this.items = items; 268 } else { 269 this.items = this.container.querySelectorAll(this.itemSelector); 270 } 271 272 if (this.items.length) { 273 this.elementTag = this.items[0].tagName; 274 } 275 this.itemHeightsDirty = true; 276 }; 277 278 // Reload all filter classes from all items and cache them 279 Wookmark.prototype.updateFilterClasses = function () { 280 // Collect filter data 281 var i = this.items.length, j, filterClasses = {}, itemFilterClasses, 282 item, filterClass, possibleFilters = this.possibleFilters, 283 k = possibleFilters.length, possibleFilter; 284 285 while (i--) { 286 item = this.items[i]; 287 288 // Read filter classes and globally store each filter class as object and the fitting items in the array 289 itemFilterClasses = JSON.parse(getData(item, 'filter-class', false, '')); 290 if (itemFilterClasses && typeof itemFilterClasses === 'object') { 291 j = itemFilterClasses.length; 292 while (j--) { 293 filterClass = cleanFilterName(itemFilterClasses[j]); 294 if (!filterClasses.hasOwnProperty(filterClass)) { 295 filterClasses[filterClass] = []; 296 } 297 filterClasses[filterClass].push(item); 298 } 299 } 300 } 301 302 while (k--) { 303 possibleFilter = cleanFilterName(possibleFilters[k]); 304 if (!filterClasses.hasOwnProperty(possibleFilter)) { 305 filterClasses[possibleFilter] = []; 306 } 307 } 308 309 this.filterClasses = filterClasses; 310 }; 311 312 // Method for updating the plugins options 313 Wookmark.prototype.updateOptions = function (options) { 314 var key; 315 this.itemHeightsDirty = true; 316 options = options ||Â {}; 317 318 // Overwrite non existing instance variables with the ones from options or the defaults 319 for (key in defaultOptions) { 320 if (defaultOptions.hasOwnProperty(key)) { 321 if (options.hasOwnProperty(key)) { 322 this[key] = options[key]; 323 } else if (!this.hasOwnProperty(key)) { 324 this[key] = defaultOptions[key]; 325 } 326 } 327 } 328 329 // Vertical offset uses a fallback to offset 330 this.verticalOffset = this.verticalOffset || this.offset; 331 332 // Update layout so changes apply 333 this.layout(true); 334 }; 335 336 // This timer ensures that layout is not continuously called as window is being dragged. 337 Wookmark.prototype.onResize = function () { 338 clearTimeout(this.resizeTimer);
339 this.itemHeightsDirty = this.flexibleWidth !== 0; 340 this.resizeTimer = setTimeout(this.layout, this.resizeDelay); 341 }; 342 343 // Marks the items heights as dirty and does a relayout 344 Wookmark.prototype.onRefresh = function () { 345 this.itemHeightsDirty = true; 346 this.layout(); 347 }; 348 349 // Filters the active items with the given string filters. 350 // @param filters array of string 351 // @param mode 'or' or 'and' 352 Wookmark.prototype.filter = function (filters, mode, dryRun) { 353 var activeFilters = [], activeFiltersLength, activeItems = [], 354 i, j, k, filter; 355 356 filters = filters || []; 357 mode = mode || 'or'; 358 dryRun = dryRun || false; 359 360 if (filters.length) { 361 // Collect active filters 362 for (i = 0; i < filters.length; i++) { 363 filter = cleanFilterName(filters[i]); 364 if (this.filterClasses.hasOwnProperty(filter)) { 365 activeFilters.push(this.filterClasses[filter]); 366 } 367 } 368 369 // Get items for active filters with the selected mode 370 i = activeFiltersLength = activeFilters.length; 371 if (mode === 'or' || activeFiltersLength === 1) { 372 // Set all items in all active filters active 373 while (i--) { 374 activeItems = activeItems.concat(activeFilters[i]); 375 } 376 } else if (mode === 'and') { 377 var shortestFilter = activeFilters[0], itemValid = true, 378 foundInFilter, currentItem, currentFilter; 379 380 // Find shortest filter class 381 while (i--) { 382 if (activeFilters[i].length < shortestFilter.length) { 383 shortestFilter = activeFilters[i]; 384 } 385 } 386 387 // Iterate over shortest filter and find elements in other filter classes 388 shortestFilter = shortestFilter || []; 389 i = shortestFilter.length; 390 while (i--) { 391 currentItem = shortestFilter[i]; 392 j = activeFiltersLength; 393 itemValid = true; 394 395 while (j-- && itemValid) { 396 currentFilter = activeFilters[j]; 397 if (shortestFilter !== currentFilter) { 398 // Search for current item in each active filter class 399 foundInFilter = false; 400 k = currentFilter.length; 401 while (k-- && !foundInFilter) { 402 foundInFilter = currentFilter[k] === currentItem; 403 } 404 itemValid &= foundInFilter; 405 } 406 } 407 408 if (itemValid) { 409 activeItems = activeItems.concat(shortestFilter[i]); 410 } 411 } 412 } 413 414 // Remove duplicates from active items 415 if (activeFiltersLength > 1) { 416 activeItems = removeDuplicates(activeItems); 417 } 418 419 // Hide inactive items 420 if (!dryRun) { 421 i = this.items.length; 422 while (i--) { 423 if (indexOf(activeItems, this.items[i]) === -1) { 424 addClass(this.items[i], this.inactiveClass); 425 } 426 } 427 } 428 } else { 429 // Show all items if no filter is selected 430 activeItems = this.items; 431 } 432 433 // Show active items 434 if (!dryRun) { 435 i = activeItems.length; 436 while (i--) { 437 removeClass(activeItems[i], this.inactiveClass); 438 } 439 // Unset columns and refresh grid for a full layout 440 this.columns = null; 441 this.layout(); 442 } 443 return activeItems; 444 }; 445 446 // Creates or updates existing placeholders to create columns of even height 447 Wookmark.prototype.refreshPlaceholders = function (columnWidth, sideOffset) { 448 var i, 449 containerHeight = getHeight(this.container), 450 columnsLength = this.columns.length, 451 column, 452 height, 453 innerOffset, 454 lastColumnItem, 455 placeholdersHtml = '', 456 placeholder, 457 top; 458 459 // Add more placeholders if necessary 460 if (this.placeholders.length < columnsLength) { 461 for (i = 0; i < columnsLength - this.placeholders.length; i++) { 462 placeholdersHtml += '<' + this.elementTag + ' class="' + this.placeholderClass + '"/>'; 463 } 464 this.container.insertAdjacentHTML('beforeend', placeholdersHtml); 465 this.placeholders = this.container.querySelectorAll('.' + this.placeholderClass); 466 } 467 468 innerOffset = (this.offset + parseInt(getStyle(this.placeholders[0], 'border-left-width'), 10) * 2) || 0; 469 innerOffset += (parseInt(getStyle(this.placeholders[0], 'padding-left'), 10) * 2) || 0; 470 471 // Update each placeholder 472 for (i = 0; i < this.placeholders.length; i++) { 473 placeholder = this.placeholders[i]; 474 column = this.columns[i]; 475 476 if (i >= columnsLength || column.length === 0) { 477 placeholder.style.display = 'none'; 478 } else { 479 lastColumnItem = column[column.length - 1]; 480 top = getData(lastColumnItem, 'top', true) + getData(lastColumnItem, 'height', true) + this.verticalOffset; 481 height = Math.max(0, containerHeight - top - innerOffset); 482 483 setCSS(placeholder, { 484 position: 'absolute', 485 display: height > 0 ? 'block' : 'none', 486 left: (i * columnWidth + sideOffset) + 'px', 487 top: top + 'px', 488 width: (columnWidth - innerOffset) + 'px', 489 height: height + 'px' 490 }); 491 } 492 } 493 }; 494 495 // Method the get active items which are not disabled and visible 496 Wookmark.prototype.getActiveItems = function () { 497 var inactiveClass = this.inactiveClass, 498 i, 499 result = [], 500 item, 501 items = this.items; 502 503 if (this.ignoreInactiveItems) { 504 for (i = 0; i < items.length; i++) { 505 item = items[i]; 506 if (!hasClass(item, inactiveClass)) { 507 result.push(item); 508 } 509 } 510 } 511 return result; 512 }; 513 514 // Method to get the standard item width 515 Wookmark.prototype.getItemWidth = function () { 516 var itemWidth = this.itemWidth, 517 innerWidth = getWidth(this.container) - 2 * this.outerOffset, 518 flexibleWidth = this.flexibleWidth; 519 520 if (typeof itemWidth === 'function') { 521 itemWidth = this.itemWidth(); 522 } 523 524 if (this.items.length > 0 && (itemWidth === undefined || (itemWidth === 0 && !this.flexibleWidth))) { 525 itemWidth = getWidth(this.items[0]); 526 } else if (typeof itemWidth === 'string' && itemWidth.indexOf('%') >= 0) { 527 itemWidth = parseFloat(this.itemWidth) / 100 * innerWidth; 528 } 529 530 // Calculate flexible item width if option is set 531 if (flexibleWidth) { 532 if (typeof flexibleWidth === 'function') { 533 flexibleWidth = flexibleWidth(); 534 } 535 536 if (typeof flexibleWidth === 'string' && flexibleWidth.indexOf('%') >= 0) { 537 flexibleWidth = parseFloat(flexibleWidth) / 100 * innerWidth; 538 } 539 540 // Find highest column count 541 var paddedInnerWidth = (innerWidth + this.offset), 542 flexibleColumns = Math.floor(0.5 + paddedInnerWidth / (flexibleWidth + this.offset)), 543 fixedColumns = Math.floor(paddedInnerWidth / (itemWidth + this.offset)), 544 columns = Math.max(flexibleColumns, fixedColumns), 545 columnWidth = Math.min(flexibleWidth, Math.floor((innerWidth - (columns - 1) * this.offset) / columns)); 546 547 itemWidth = Math.max(itemWidth, columnWidth); 548 } 549 550 return itemWidth; 551 }; 552 553 // Main layout method. 554 Wookmark.prototype.layout = function (force, callback) {
555 // Do nothing if container isn't visible 556 if (isHidden(this.container)) { return; } 557 558 // Calculate basic layout parameters. 559 var calculatedItemWidth = this.getItemWidth(), 560 columnWidth = calculatedItemWidth + this.offset, 561 containerWidth = getWidth(this.container), 562 innerWidth = containerWidth - 2 * this.outerOffset, 563 columns = Math.floor((innerWidth + this.offset) / columnWidth), 564 offset = 0, 565 maxHeight = 0, 566 activeItems = this.getActiveItems(), 567 activeItemsLength = activeItems.length, 568 item, 569 i = activeItemsLength; 570 571 // Cache item heights 572 if (this.itemHeightsDirty || !this.itemHeightsInitialized) { 573 while (i--) { 574 item = activeItems[i]; 575 576 if (this.flexibleWidth) { 577 // Stretch items to fill calculated width 578 item.style.width = calculatedItemWidth + 'px'; 579 } 580 setData(item, 'height', item.offsetHeight); 581 } 582 this.itemHeightsDirty = false; 583 this.itemHeightsInitialized = true; 584 } 585 586 // Use less columns if there are to few items 587 columns = Math.max(1, Math.min(columns, activeItemsLength)); 588 589 // Calculate the offset based on the alignment of columns to the parent container 590 offset = this.outerOffset; 591 if (this.align === 'center') { 592 offset += Math.floor(0.5 + (innerWidth - (columns * columnWidth - this.offset)) >> 1); 593 } 594 595 // Get direction for positioning 596 this.direction = this.direction || (this.align === 'right' ? 'right' : 'left'); 597 598 // If container and column count hasn't changed, we can only update the columns. 599 if (!force && this.columns !== null && this.columns.length === columns && this.activeItemCount === activeItemsLength) { 600 maxHeight = this.layoutColumns(columnWidth, offset); 601 } else { 602 maxHeight = this.layoutFull(columnWidth, columns, offset); 603 } 604 this.activeItemCount = activeItemsLength; 605 606 // Set container height to height of the grid. 607 this.container.style.height = maxHeight + 'px'; 608 609 // Update placeholders 610 if (this.fillEmptySpace) { 611 this.refreshPlaceholders(columnWidth, offset); 612 } 613 614 if (this.onLayoutChanged !== undefined && typeof this.onLayoutChanged === 'function') { 615 this.onLayoutChanged(); 616 } 617 618 // Run optional callback 619 if (typeof callback === 'function') { 620 callback(); 621 } 622 }; 623 624 // Sort elements with configurable comparator 625 Wookmark.prototype.sortElements = function (elements) { 626 return typeof this.comparator === 'function' ? elements.sort(this.comparator) : elements; 627 }; 628 629 // Perform a full layout update. 630 Wookmark.prototype.layoutFull = function (columnWidth, columns, offset) { 631 var item, k = 0, i = 0, activeItems, activeItemCount, shortest = null, shortestIndex = null, 632 sideOffset, heights = [], itemBulkCSS = [], leftAligned = this.align === 'left'; 633 634 this.columns = []; 635 636 // Sort elements before layouting 637 activeItems = this.sortElements(this.getActiveItems()); 638 activeItemCount = activeItems.length; 639 640 // Prepare arrays to store height of columns and items. 641 while (heights.length < columns) { 642 heights.push(this.outerOffset); 643 this.columns.push([]); 644 } 645 646 // Loop over items. 647 while (i < activeItemCount) { 648 item = activeItems[i]; 649 650 // Find the shortest column. 651 shortest = heights[0]; 652 shortestIndex = 0; 653 for (k = 0; k < columns; k++) { 654 if (heights[k] < shortest) { 655 shortest = heights[k]; 656 shortestIndex = k; 657 } 658 } 659 setData(item, 'top', shortest); 660 661 // stick to left side if alignment is left and this is the first column 662 sideOffset = offset; 663 if (shortestIndex > 0 || !leftAligned) { 664 sideOffset += shortestIndex * columnWidth; 665 } 666 667 // Position the item. 668 itemBulkCSS[i] = { 669 el: item, 670 css: { 671 position: 'absolute', 672 top: shortest + 'px' 673 } 674 }; 675 itemBulkCSS[i].css[this.direction] = sideOffset + 'px'; 676 677 // Update column height and store item in shortest column 678 heights[shortestIndex] += getData(item, 'height', true) + this.verticalOffset; 679 this.columns[shortestIndex].push(item); 680 i++; 681 } 682 683 bulkUpdateCSS(itemBulkCSS); 684 685 // Return longest column 686 return Math.max.apply(Math, heights); 687 }; 688 689 // This layout method only updates the vertical position of the 690 // existing column assignments. 691 Wookmark.prototype.layoutColumns = function (columnWidth, offset) { 692 var heights = [], itemBulkCSS = [], k = 0, j = 0, 693 i = this.columns.length, currentHeight, 694 column, item, sideOffset; 695 696 while (i--) { 697 currentHeight = this.outerOffset; 698 heights.push(currentHeight); 699 column = this.columns[i]; 700 sideOffset = i * columnWidth + offset; 701 702 for (k = 0; k < column.length; k++, j++) { 703 item = column[k]; 704 setData(item, 'top', currentHeight); 705 itemBulkCSS[j] = { 706 el: item, 707 css: { 708 top: currentHeight + 'px' 709 } 710 }; 711 itemBulkCSS[j].css[this.direction] = sideOffset + 'px'; 712 713 currentHeight += getData(item, 'height', true) + this.verticalOffset; 714 } 715 heights[i] = currentHeight; 716 } 717 718 bulkUpdateCSS(itemBulkCSS); 719 720 // Return longest column 721 return Math.max.apply(Math, heights); 722 }; 723 724 // Clear event listeners and time outs and the instance itself 725 Wookmark.prototype.clear = function () { 726 clearTimeout(this.resizeTimer); 727 var i = this.placeholders.length; 728 while (i--) { 729 this.container.removeChild(this.placeholders[i]); 730 } 731 removeEventListener(window, 'resize', this.onResize); 732 removeEventListener(this.container, 'refreshWookmark', this.onRefresh); 733 }; 734 735 // Register as jQuery plugin if jQuery is loaded 736 if (window.jQuery !== undefined) { 737 jQuery.fn.wookmark = function (options) { 738 var i = this.length; 739 740 // Use first element if container is an jQuery object 741 if (options !== undefined && options.container instanceof jQuery) { 742 options.container = options.container[0]; 743 } 744 745 // Call plugin multiple times if there are multiple elements selected 746 if (i > 1) { 747 while (i--) { 748 this[i].wookmark(options); 749 } 750 } else if (i === 1) { 751 // Create a wookmark instance or update an existing one 752 if (!this.wookmarkInstance) { 753 this.wookmarkInstance = new Wookmark(this[0], options || {}); 754 } else { 755 this.wookmarkInstance.update(options || {}); 756 } 757 } 758 return this; 759 }; 760 } 761 762 window.Wookmark = Wookmark; 763 return Wookmark; 764}));
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.