1/* global wc_cart_params */ 2jQuery( function ( $ ) { 3 // wc_cart_params is required to continue, ensure the object exists 4 if ( typeof wc_cart_params === 'undefined' ) { 5 return false; 6 } 7 8 // Utility functions for the file. 9 10 /** 11 * Gets a url for a given AJAX endpoint. 12 * 13 * @param {String} endpoint The AJAX Endpoint 14 * @return {String} The URL to use for the request 15 */ 16 var get_url = function ( endpoint ) { 17 return wc_cart_params.wc_ajax_url 18 .toString() 19 .replace( '%%endpoint%%', endpoint ); 20 }; 21 22 /** 23 * Check if a node is blocked for processing. 24 * 25 * @param {JQuery Object} $node 26 * @return {bool} True if the DOM Element is UI Blocked, false if not. 27 */ 28 var is_blocked = function ( $node ) { 29 return ( 30 $node.is( '.processing' ) || $node.parents( '.processing' ).length 31 ); 32 }; 33 34 /** 35 * Block a node visually for processing. 36 * 37 * @param {JQuery Object} $node 38 */ 39 var block = function ( $node ) { 40 if ( ! is_blocked( $node ) ) { 41 $node.addClass( 'processing' ).block( { 42 message: null, 43 overlayCSS: { 44 background: '#fff', 45 opacity: 0.6, 46 }, 47 } ); 48 } 49 }; 50 51 /** 52 * Unblock a node after processing is complete. 53 * 54 * @param {JQuery Object} $node 55 */ 56 var unblock = function ( $node ) { 57 $node.removeClass( 'processing' ).unblock(); 58 }; 59 60 /** 61 * Removes duplicate notices. 62 * 63 * @param {JQuery Object} $notices 64 */ 65 var remove_duplicate_notices = function ( $notices ) { 66 var seen = new Set(); 67 var deduplicated_notices = []; 68 69 $notices.each( function () { 70 const text = $( this ).text(); 71 72 if ( ! seen.has( text ) ) { 73 seen.add( text ); 74 deduplicated_notices.push( this ); 75 } 76 } ); 77 78 return $( deduplicated_notices ); 79 }; 80 81 /** 82 * Update the .woocommerce div with a string of html. 83 * 84 * @param {String} html_str The HTML string with which to replace the div. 85 * @param {bool} preserve_notices Should notices be kept? False by default. 86 */ 87 var update_wc_div = function ( html_str, preserve_notices ) { 88 var $html = $.parseHTML( html_str ); 89 var $new_form = $( '.woocommerce-cart-form', $html ); 90 var $new_totals = $( '.cart_totals', $html ); 91 var $notices = remove_duplicate_notices( 92 $( 93 '.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success', 94 $html 95 ) 96 ); 97 98 // No form, cannot do this. 99 if ( $( '.woocommerce-cart-form' ).length === 0 ) { 100 window.location.reload(); 101 return; 102 } 103 104 // Remove errors 105 if ( ! preserve_notices ) { 106 $( 107 '.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success, .coupon-error-notice' 108 ).remove(); 109 } 110 111 if ( $new_form.length === 0 ) { 112 // If the checkout is also displayed on this page, trigger reload instead. 113 if ( $( '.woocommerce-checkout' ).length ) { 114 window.location.reload(); 115 return; 116 } 117 118 // No items to display now! Replace all cart content. 119 var $cart_html = $( '.wc-empty-cart-message', $html ).closest( 120 '.woocommerce' 121 ); 122 $( '.woocommerce-cart-form__contents' ) 123 .closest( '.woocommerce' ) 124 .replaceWith( $cart_html ); 125 126 // Display errors 127 if ( $notices.length > 0 ) { 128 show_notice( $notices ); 129 } 130 131 // Notify plugins that the cart was emptied. 132 $( document.body ).trigger( 'wc_cart_emptied' ); 133 } else { 134 // If the checkout is also displayed on this page, trigger update event. 135 if ( $( '.woocommerce-checkout' ).length ) { 136 $( document.body ).trigger( 'update_checkout' ); 137 } 138 139 // Store the old coupon error message and value before the 140 // .woocommerce-cart-form is replaced with the new form. 141 var $old_coupon_field_val = $( '#coupon_code' ).val(); 142 var $old_coupon_error_msg = $( '#coupon_code' ) 143 .closest( '.coupon' ) 144 .find( '.coupon-error-notice' ); 145 146 $( '.woocommerce-cart-form' ).replaceWith( $new_form ); 147 $( '.woocommerce-cart-form' ) 148 .find( ':input[name="update_cart"]' ) 149 .prop( 'disabled', true ); 150 151 if ( preserve_notices && $old_coupon_error_msg.length > 0 ) { 152 var $new_coupon_field = $( '.woocommerce-cart-form' ).find( '#coupon_code' ); 153 var $new_coupon_field_wrapper = $new_coupon_field.closest( '.coupon' ); 154 155 $new_coupon_field.val( $old_coupon_field_val ); 156 // The coupon input with error needs to be focused before adding the live region 157 // with the error message, otherwise the screen reader won't read it. 158 $new_coupon_field.focus(); 159 show_coupon_error( $old_coupon_error_msg, $new_coupon_field_wrapper, true ); 160 } 161 162 if ( $notices.length > 0 ) { 163 show_notice( $notices ); 164 } 165 166 update_cart_totals_div( $new_totals ); 167 } 168 169 $( document.body ).trigger( 'updated_wc_div' ); 170 }; 171 172 /** 173 * Update the .cart_totals div with a string of html. 174 * 175 * @param {String} html_str The HTML string with which to replace the div. 176 */ 177 var update_cart_totals_div = function ( html_str ) { 178 $( '.cart_totals' ).replaceWith( html_str ); 179 $( document.body ).trigger( 'updated_cart_totals' ); 180 }; 181 182 /** 183 * Shows new notices on the page. 184 * 185 * @param {Object} The Notice HTML Element in string or object form. 186 */ 187 var show_notice = function ( html_element, $target ) { 188 if ( ! $target ) { 189 $target = 190 $( '.woocommerce-notices-wrapper:first' ) || 191 $( '.wc-empty-cart-message' ).closest( '.woocommerce' ) || 192 $( '.woocommerce-cart-form' ); 193 } 194 $target.prepend( html_element ); 195 }; 196 197 /** 198 * Shows coupon form errors. 199 * 200 * @param {string|object}
200 html_element The HTML string response after applying an invalid coupon or a jQuery element. 201 * @param {Object} $target Coupon field wrapper jQuery element. 202 * @param {boolean} is_live_region Whether role="alert" should be added or not. 203 */ 204 var show_coupon_error = function ( html_element, $target, is_live_region ) { 205 if ( $target.length === 0 ) { 206 return; 207 } 208 209 var $coupon_error_el = html_element; 210 211 if ( typeof html_element === 'string' ) { 212 var msg = $( $.parseHTML( html_element ) ).text().trim(); 213 214 if ( msg === '' ) { 215 return; 216 } 217 218 $coupon_error_el = $('<p>', { 219 class: 'coupon-error-notice', 220 id: 'coupon-error-notice', 221 text: msg 222 }); 223 } 224 225 if ( is_live_region ) { 226 $coupon_error_el.attr( 'role', 'alert' ); 227 } 228 229 $target.find( '#coupon_code' ) 230 .addClass( 'has-error' ) 231 .attr( 'aria-invalid', 'true' ) 232 .attr( 'aria-describedby', 'coupon-error-notice' ); 233 $target.append( $coupon_error_el ); 234 }; 235 236 /** 237 * Object to handle AJAX calls for cart shipping changes. 238 */ 239 var cart_shipping = { 240 /** 241 * Initialize event handlers and UI state. 242 */ 243 init: function ( cart ) { 244 this.cart = cart; 245 this.toggle_shipping = this.toggle_shipping.bind( this ); 246 this.shipping_method_selected = 247 this.shipping_method_selected.bind( this ); 248 this.shipping_calculator_submit = 249 this.shipping_calculator_submit.bind( this ); 250 251 $( document ).on( 252 'click', 253 '.shipping-calculator-button', 254 this.toggle_shipping 255 ); 256 $( document ).on( 257 'change', 258 'select.shipping_method, :input[name^=shipping_method]', 259 this.shipping_method_selected 260 ); 261 $( document ).on( 262 'submit', 263 'form.woocommerce-shipping-calculator', 264 this.shipping_calculator_submit 265 ); 266 267 $( '.shipping-calculator-form' ).hide(); 268 }, 269 270 /** 271 * Toggle Shipping Calculator panel 272 */ 273 toggle_shipping: function ( event ) { 274 var $target = $( event.currentTarget ); 275 276 $( '.shipping-calculator-form' ).slideToggle( 'slow', function () { 277 var self = this; 278 279 setTimeout( function () { 280 var $form = $( self ); 281 282 $target.attr( 'aria-expanded', $form.is( ':visible' ) ? 'true' : 'false' ); 283 }, 0 ); 284 } ); 285 286 $( 'select.country_to_state, input.country_to_state' ).trigger( 287 'change' 288 ); 289 $( document.body ).trigger( 'country_to_state_changed' ); // Trigger select2 to load. 290 return false; 291 }, 292 293 /** 294 * Handles when a shipping method is selected. 295 */ 296 shipping_method_selected: function ( event ) { 297 var shipping_methods = {}; 298 299 // eslint-disable-next-line max-len 300 $( 301 'select.shipping_method, :input[name^=shipping_method][type=radio]:checked, :input[name^=shipping_method][type=hidden]' 302 ).each( function () { 303 shipping_methods[ $( this ).data( 'index' ) ] = $( this ).val(); 304 } ); 305 306 block( $( 'div.cart_totals' ) ); 307 308 var data = { 309 security: wc_cart_params.update_shipping_method_nonce, 310 shipping_method: shipping_methods, 311 }; 312 313 $.ajax( { 314 type: 'post', 315 url: get_url( 'update_shipping_method' ), 316 data: data, 317 dataType: 'html', 318 success: function ( response ) { 319 update_cart_totals_div( response ); 320 321 var newCurrentTarget = document.getElementById( event.currentTarget.id ); 322 323 if ( newCurrentTarget ) { 324 newCurrentTarget.focus(); 325 } 326 }, 327 complete: function () { 328 unblock( $( 'div.cart_totals' ) ); 329 $( document.body ).trigger( 'updated_shipping_method' ); 330 }, 331 } ); 332 }, 333 334 /** 335 * Handles a shipping calculator form submit. 336 * 337 * @param {Object} evt The JQuery event. 338 */ 339 shipping_calculator_submit: function ( evt ) { 340 evt.preventDefault(); 341 342 var $form = $( evt.currentTarget ); 343 344 block( $( 'div.cart_totals' ) ); 345 block( $form ); 346 347 // Provide the submit button value because wc-form-handler expects it. 348 $( '<input />' ) 349 .attr( 'type', 'hidden' ) 350 .attr( 'name', 'calc_shipping' ) 351 .attr( 'value', 'x' ) 352 .appendTo( $form ); 353 354 // Make call to actual form post URL. 355 $.ajax( { 356 type: $form.attr( 'method' ), 357 url: $form.attr( 'action' ), 358 data: $form.serialize(), 359 dataType: 'html', 360 success: function ( response ) { 361 update_wc_div( response ); 362 }, 363 complete: function () { 364 unblock( $form ); 365 unblock( $( 'div.cart_totals' ) ); 366 }, 367 } ); 368 }, 369 }; 370 371 /** 372 * Object to handle cart UI. 373 */ 374 var cart = { 375 /** 376 * Initialize cart UI events. 377 */ 378 init: function () { 379 this.update_cart_totals = this.update_cart_totals.bind( this ); 380 this.input_keypress = this.input_keypress.bind( this ); 381 this.cart_submit = this.cart_submit.bind( this ); 382 this.submit_click = this.submit_click.bind( this ); 383 this.apply_coupon = this.apply_coupon.bind( this ); 384 this.remove_coupon_clicked = 385 this.remove_coupon_clicked.bind( this ); 386 this.remove_coupon_error = this.remove_coupon_error.bind( this ); 387 this.quantity_update = this.quantity_update.bind( this ); 388 this.item_remove_clicked = this.item_remove_clicked.bind( this ); 389 this.item_restore_clicked = this.item_restore_clicked.bind( this ); 390 this.update_cart = this.update_cart.bind( this ); 391 392 $( document ).on( 'wc_update_cart added_to_cart', function () { 393 cart.update_cart.apply( cart, [].slice.call( arguments, 1 ) ); 394 } ); 395 $( document ).on( 396 'click', 397 '.woocommerce-cart-form :input[type=submit]', 398 this.submit_click 399 ); 400 $( document ).on( 401 'keypress', 402 '.woocommerce-cart-form :input[type=number]', 403 this.input_keypress 404 ); 405 $( document ).on( 406 'submit', 407 '.woocommerce-cart-form', 408 this.cart_submit 409 ); 410 $( document ).on( 411 'click', 412 'a.woocommerce-remove-coupon', 413 this.remove_coupon_clicked 414 ); 415 $( document ).on( 416 'keydown', 417 'a.woocommerce-remove-coupon', 418 this.on_keydown_remove_coupon 419 ); 420 $( document ).on( 421 'click', 422 '.woocommerce-cart-form .product-remove > a', 423 this.item_remove_clicked 424 ); 425 $( document ).on( 426 'keydown', 427 '.woocommerce-cart-form .product-remove > a', 428 this.on_keydown_remove_item 429 ); 430 $( document ).on( 431 'click', 432 '.woocommerce-cart .restore-item', 433 this.item_restore_clicked 434 ); 435 $( document ).on( 436 'change input', 437 '.woocommerce-cart-form .cart_item :input', 438 this.input_changed 439 ); 440 $( document ).on( 441 'change input', 442 '#coupon_code', 443 this.remove_coupon_error 444 ); 445 446 $( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 447 'disabled', 448 true 449 ); 450 }, 451 452 /** 453 * After an input is changed, enable the update cart button. 454 */ 455 input_changed: function () { 456 $( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 457 'disabled', 458 false 459 ); 460 }, 461 462 /** 463 * Update entire cart via ajax. 464 */ 465 update_cart: function ( preserve_notices ) { 466 var $form = $( '.woocommerce-cart-form' ); 467 468 block( $form ); 469 block( $( 'div.cart_totals' ) ); 470 471 // Make call to actual form post URL. 472 $.ajax( { 473 type: $form.attr( 'method' ), 474 url: $form.attr( 'action' ), 475 data: $form.serialize(), 476 dataType: 'html', 477 success: function ( response ) { 478 update_wc_div( response, preserve_notices ); 479 }, 480 complete: function () { 481 unblock( $form ); 482 unblock( $( 'div.cart_totals' ) ); 483 $.scroll_to_notices( $( '[role="alert"]' ) ); 484 }, 485 } ); 486 }, 487 488 /** 489 * Update the cart after something has changed. 490 */ 491 update_cart_totals: function () { 492 block( $( 'div.cart_totals' ) ); 493 494 $.ajax( { 495 url: get_url( 'get_cart_totals' ), 496 dataType: 'html', 497 success: function ( response ) { 498 update_cart_totals_div( response ); 499 }, 500 complete: function () { 501 unblock( $( 'div.cart_totals' ) ); 502 }, 503 } ); 504 }, 505 506 /** 507 * Handle the <ENTER> key for quantity fields. 508 * 509 * @param {Object} evt The JQuery event 510 * 511 * For IE, if you hit enter on a quantity field, it makes the 512 * document.activeElement the first submit button it finds. 513 * For us, that is the Apply Coupon button. This is required 514 * to catch the event before that happens. 515 */ 516 input_keypress: function ( evt ) { 517 // Catch the enter key and don't let it submit the form. 518 if ( 13 === evt.keyCode ) { 519 var $form = $( evt.currentTarget ).parents( 'form' ); 520 521 try { 522 // If there are no validation errors, handle the submit. 523 if ( $form[ 0 ].checkValidity() ) { 524 evt.preventDefault(); 525 this.cart_submit( evt ); 526 } 527 } catch ( err ) { 528 evt.preventDefault(); 529 this.cart_submit( evt ); 530 } 531 } 532 }, 533 534 /** 535 * Handle cart form submit and route to correct logic. 536 * 537 * @param {Object} evt The JQuery event 538 */ 539 cart_submit: function ( evt ) { 540 var $submit = $( document.activeElement ), 541 $clicked = $( ':input[type=submit][clicked=true]' ), 542 $form = $( evt.currentTarget ); 543 544 // For submit events, currentTarget is form.
545 // For keypress events, currentTarget is input. 546 if ( ! $form.is( 'form' ) ) { 547 $form = $( evt.currentTarget ).parents( 'form' ); 548 } 549 550 if ( 551 0 === $form.find( '.woocommerce-cart-form__contents' ).length 552 ) { 553 return; 554 } 555 556 if ( is_blocked( $form ) ) { 557 return false; 558 } 559 560 if ( 561 $clicked.is( ':input[name="update_cart"]' ) || 562 $submit.is( 'input.qty' ) 563 ) { 564 evt.preventDefault(); 565 this.quantity_update( $form ); 566 } else if ( 567 $clicked.is( ':input[name="apply_coupon"]' ) || 568 $submit.is( '#coupon_code' ) 569 ) { 570 evt.preventDefault(); 571 this.apply_coupon( $form ); 572 } 573 }, 574 575 /** 576 * Special handling to identify which submit button was clicked. 577 * 578 * @param {Object} evt The JQuery event 579 */ 580 submit_click: function ( evt ) { 581 $( 582 ':input[type=submit]', 583 $( evt.target ).parents( 'form' ) 584 ).removeAttr( 'clicked' ); 585 $( evt.target ).attr( 'clicked', 'true' ); 586 }, 587 588 /** 589 * Apply Coupon code 590 * 591 * @param {JQuery Object} $form The cart form. 592 */ 593 apply_coupon: function ( $form ) { 594 block( $form ); 595 596 var cart = this; 597 var $text_field = $( '#coupon_code' ); 598 var coupon_code = $text_field.val(); 599 600 var data = { 601 security: wc_cart_params.apply_coupon_nonce, 602 coupon_code: coupon_code, 603 }; 604 605 $.ajax( { 606 type: 'POST', 607 url: get_url( 'apply_coupon' ), 608 data: data, 609 dataType: 'html', 610 success: function ( response ) { 611 $( 612 '.woocommerce-error, .woocommerce-message, .woocommerce-info, ' + 613 '.is-error, .is-info, .is-success, .coupon-error-notice' 614 ).remove(); 615 616 // We only want to show coupon notices if they are not errors. 617 // Coupon errors are shown under the input. 618 if ( response.indexOf( 'woocommerce-error' ) === -1 && response.indexOf( 'is-error' ) === -1 ) { 619 show_notice( response ); 620 } else { 621 var $coupon_wrapper = $text_field.closest( '.coupon' ); 622 623 if ( $coupon_wrapper.length > 0 ) { 624 show_coupon_error( response, $coupon_wrapper, false ); 625 } 626 } 627 628 $( document.body ).trigger( 'applied_coupon', [ 629 coupon_code, 630 ] ); 631 }, 632 complete: function () { 633 unblock( $form ); 634 cart.update_cart( true ); 635 }, 636 } ); 637 }, 638 639 /** 640 * Handle when a remove coupon link is clicked. 641 * 642 * @param {Object} evt The JQuery event 643 */ 644 remove_coupon_clicked: function ( evt ) { 645 evt.preventDefault(); 646 647 var cart = this; 648 var $wrapper = $( evt.currentTarget ).closest( '.cart_totals' ); 649 var coupon = $( evt.currentTarget ).attr( 'data-coupon' ); 650 651 block( $wrapper ); 652 653 var data = { 654 security: wc_cart_params.remove_coupon_nonce, 655 coupon: coupon, 656 }; 657 658 $.ajax( { 659 type: 'POST', 660 url: get_url( 'remove_coupon' ), 661 data: data, 662 dataType: 'html', 663 success: function ( response ) { 664 $( 665 '.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success' 666 ).remove(); 667 show_notice( response ); 668 $( document.body ).trigger( 'removed_coupon', [ coupon ] ); 669 $( '#coupon_code' ) 670 .val('') 671 .removeClass('has-error') 672 .removeAttr('aria-invalid') 673 .removeAttr('aria-describedby') 674 .closest('.coupon') 675 .find('.coupon-error-notice') 676 .remove(); 677 unblock( $wrapper ); 678 }, 679 complete: function () { 680 cart.update_cart( true ); 681 }, 682 } ); 683 }, 684 685 /** 686 * Handle when pressing the Space key on the remove coupon link. 687 * This is necessary because the link got the role="button" attribute 688 * and needs to act like a button. 689 * 690 * @param {Object} evt The JQuery event 691 */ 692 on_keydown_remove_coupon: function ( evt ) { 693 if ( evt.key === ' ' ) { 694 evt.preventDefault(); 695 $( evt.currentTarget ).trigger( 'click' ); 696 } 697 }, 698 699 /** 700 * Handle when the coupon input loses focus. 701 * 702 * @param {Object} evt The JQuery event 703 */ 704 remove_coupon_error: function ( evt ) { 705 $( evt.currentTarget ) 706 .removeClass( 'has-error' ) 707 .removeAttr( 'aria-invalid' ) 708 .removeAttr( 'aria-describedby' ) 709 .closest( '.coupon' ) 710 .find( '.coupon-error-notice' ) 711 .remove(); 712 }, 713 714 /**
715 * Handle a cart Quantity Update 716 * 717 * @param {JQuery Object} $form The cart form. 718 */ 719 quantity_update: function ( $form ) { 720 block( $form ); 721 block( $( 'div.cart_totals' ) ); 722 723 // Provide the submit button value because wc-form-handler expects it. 724 $( '<input />' ) 725 .attr( 'type', 'hidden' ) 726 .attr( 'name', 'update_cart' ) 727 .attr( 'value', 'Update Cart' ) 728 .appendTo( $form ); 729 730 // Make call to actual form post URL. 731 $.ajax( { 732 type: $form.attr( 'method' ), 733 url: $form.attr( 'action' ), 734 data: $form.serialize(), 735 dataType: 'html', 736 success: function ( response ) { 737 update_wc_div( response ); 738 }, 739 complete: function () { 740 unblock( $form ); 741 unblock( $( 'div.cart_totals' ) ); 742 $.scroll_to_notices( $( '[role="alert"]' ) ); 743 }, 744 } ); 745 }, 746 747 /** 748 * Handle when a remove item link is clicked. 749 * 750 * @param {Object} evt The JQuery event 751 */ 752 item_remove_clicked: function ( evt ) { 753 evt.preventDefault(); 754 755 var $a = $( evt.currentTarget ); 756 var $form = $a.parents( 'form' ); 757 758 block( $form ); 759 block( $( 'div.cart_totals' ) ); 760 761 $.ajax( { 762 type: 'GET', 763 url: $a.attr( 'href' ), 764 dataType: 'html', 765 success: function ( response ) { 766 update_wc_div( response ); 767 }, 768 complete: function () { 769 unblock( $form ); 770 unblock( $( 'div.cart_totals' ) ); 771 $.scroll_to_notices( $( '[role="alert"]' ) ); 772 $( document.body ).trigger( 'item_removed_from_classic_cart'); 773 }, 774 } ); 775 }, 776 777 /** 778 * Handle when pressing the Space key on the remove item link. 779 * This is necessary because the link got the role="button" attribute 780 * and needs to act like a button. 781 * 782 * @param {Object} evt The JQuery event 783 */ 784 on_keydown_remove_item: function ( event ) { 785 if ( event.key === ' ' ) { 786 event.preventDefault(); 787 $( event.currentTarget ).trigger( 'click' ); 788 } 789 }, 790 791 /** 792 * Handle when a restore item link is clicked. 793 * 794 * @param {Object} evt The JQuery event 795 */ 796 item_restore_clicked: function ( evt ) { 797 evt.preventDefault(); 798 799 var $a = $( evt.currentTarget ); 800 var $form = $( 'form.woocommerce-cart-form' ); 801 802 block( $form ); 803 block( $( 'div.cart_totals' ) ); 804 805 $.ajax( { 806 type: 'GET', 807 url: $a.attr( 'href' ), 808 dataType: 'html', 809 success: function ( response ) { 810 update_wc_div( response ); 811 }, 812 complete: function () { 813 unblock( $form ); 814 unblock( $( 'div.cart_totals' ) ); 815 }, 816 } ); 817 }, 818 }; 819 820 cart_shipping.init( cart ); 821 cart.init(); 822} );
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.