1/* global wc_checkout_params */ 2jQuery( function ( $ ) { 3 // wc_checkout_params is required to continue, ensure the object exists 4 if ( typeof wc_checkout_params === 'undefined' ) { 5 return false; 6 } 7 8 $.blockUI.defaults.overlayCSS.cursor = 'default'; 9 10 /** 11 * Create the API object passed to custom place order button render callbacks. 12 * This is checkout-specific and includes form validation. 13 * 14 * @return {Object} API object with validate and submit methods 15 */ 16 function createCheckoutPlaceOrderApi() { 17 var $form = window.wc.customPlaceOrderButton.__getForm(); 18 19 return { 20 /** 21 * Validate the checkout form. 22 * This gets a little tricky. 23 * The existing checkout.js does NOT have a "validate everything before submit" function - it's not needed. 24 * Validation is done: 25 * - Field-by-field via `validate_field` on blur/change 26 * - Server-side on form submission (errors are returned in AJAX response). 27 * This function tries to mimic client-side validation, but WooCommerce's real validation happens server-side. 28 * 29 * @return {Promise<{hasError: boolean}>} Promise resolving to validation result 30 */ 31 validate: function () { 32 return new Promise( function ( resolve ) { 33 var hasError = false; 34 35 // On a "normal" shortcode checkout page, the page validates this server-side only (not via validate_field). 36 // We do client-side validation here for a better UX with custom place order buttons. 37 // Clearing any stale invalid state before re-validating, to ensure a clean slate.
38 var $termsCheckbox = $form.find( 'input[name="terms"]:visible' ); 39 if ( $termsCheckbox.length ) { 40 $termsCheckbox.closest( '.form-row' ).removeClass( 'woocommerce-invalid' ); 41 } 42 43 // Trigger field-level validation (which adds `.woocommerce-invalid` to invalid fields) 44 $form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' ); 45 46 // Check for validation errors (from validate_field handler). 47 // Only consider visible fields: `validate_field` flags any empty 48 // required field regardless of visibility, so hidden fields (e.g. 49 // the collapsed "Ship to a different address?" shipping fields) 50 // would otherwise block submission even when the visible form is 51 // valid. The `.woocommerce-invalid` class lives on the `.form-row`, 52 // which is hidden when its section is collapsed. 53 if ( $form.find( '.woocommerce-invalid:visible' ).length > 0 ) { 54 hasError = true; 55 } 56 57 // Check required fields (adds .woocommerce-invalid if not already set) 58 $form.find( '.validate-required:visible' ).each( function () { 59 var $field = $( this ); 60 var $input = $field.find( 'input.input-text, select, input:checkbox' ); 61 62 if ( $input.length === 0 ) { 63 return; 64 } 65 66 var isEmpty; 67 if ( $input.is( ':checkbox' ) ) { 68 isEmpty = ! $input.is( ':checked' ); 69 } else { 70 isEmpty = $input.val() === '' || $input.val() === null; 71 } 72 73 if ( isEmpty ) { 74 hasError = true; 75 $field.addClass( 'woocommerce-invalid woocommerce-invalid-required-field' ); 76 } 77 } ); 78 79 // Check terms checkbox - this is our client-side validation for better UX 80 // (WC Core only validates terms server-side) 81 if ( $termsCheckbox.length && ! $termsCheckbox.is( ':checked' ) ) { 82 hasError = true; 83 $termsCheckbox.closest( '.form-row' ).addClass( 'woocommerce-invalid' ); 84 } 85 86 // Scroll to the first invalid field in DOM order 87 if ( hasError ) { 88 var $firstInvalidField = $form.find( '.woocommerce-invalid:visible' ).first(); 89 if ( $firstInvalidField.length ) { 90 $( 'html, body' ).animate( 91 { 92 scrollTop: $firstInvalidField.offset().top - 100, 93 }, 94 500 95 ); 96 } 97 } 98 99 resolve( { hasError: hasError } ); 100 } ); 101 }, 102 103 /** 104 * Submit the checkout form. 105 * Triggers the same logic as clicking the default place order button. 106 */ 107 submit: function () { 108 $form.trigger( 'submit' ); 109 }, 110 }; 111 } 112 113 // Clean up custom place order button before checkout update destroys the DOM. 114 // After the update, init_payment_methods() will trigger payment method selection, 115 // which will call render() again for the active gateway. 116 $( document.body ).on( 'update_checkout', function () { 117 window.wc.customPlaceOrderButton.__cleanup(); 118 } ); 119 120 // When a gateway registers after a page load, render its button if it's selected. 121 $( document.body ).on( 'wc_custom_place_order_button_registered', function ( e, gatewayId ) { 122 window.wc.customPlaceOrderButton.__maybeShow( gatewayId, createCheckoutPlaceOrderApi() ); 123 } ); 124 125 var wc_checkout_form = { 126 updateTimer: false, 127 dirtyInput: false, 128 selectedPaymentMethod: false, 129 xhr: false, 130 $order_review: $( '#order_review' ), 131 $checkout_form: $( 'form.checkout' ), 132 init: function () { 133 $( document.body ).on( 'update_checkout', this.update_checkout ); 134 $( document.body ).on( 'init_checkout', this.init_checkout ); 135 136 // Payment methods 137 this.$checkout_form.on( 138 'click', 139 'input[name="payment_method"]', 140 this.payment_method_selected 141 ); 142 143 if ( $( document.body ).hasClass( 'woocommerce-order-pay' ) ) { 144 this.$order_review.on( 145 'click', 146 'input[name="payment_method"]', 147 this.payment_method_selected 148 ); 149 this.$order_review.on( 'submit', this.submitOrder ); 150 this.$order_review.attr( 'novalidate', 'novalidate' ); 151 152 // Initialize the custom place order button for the "order-pay" page 153 var $orderPayMethod = this.$order_review.find( 'input[name="payment_method"]:checked' ); 154 if ( $orderPayMethod.length ) { 155 window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( $orderPayMethod.val() ); 156 $orderPayMethod.trigger( 'click' ); 157 } 158 } 159 160 // Prevent HTML5 validation which can conflict. 161 this.$checkout_form.attr( 'novalidate', 'novalidate' ); 162 163 // Form submission 164 this.$checkout_form.on( 'submit', this.submit ); 165 166 // Inline validation 167 this.$checkout_form.on( 168 'input validate change focusout', 169 '.input-text, select, input:checkbox', 170 this.validate_field 171 ); 172 173 // Manual trigger 174 this.$checkout_form.on( 'update', this.trigger_update_checkout ); 175
176 // Inputs/selects which update totals 177 this.$checkout_form.on( 178 'change', 179 // eslint-disable-next-line max-len 180 'select.shipping_method, input[name^="shipping_method"], #ship-to-different-address input, .update_totals_on_change select, .update_totals_on_change input[type="radio"], .update_totals_on_change input[type="checkbox"]', 181 this.trigger_update_checkout 182 ); 183 this.$checkout_form.on( 184 'change', 185 '.address-field select', 186 this.input_changed 187 ); 188 this.$checkout_form.on( 189 'change', 190 '.address-field input.input-text, .update_totals_on_change input.input-text', 191 this.maybe_input_changed 192 ); 193 this.$checkout_form.on( 194 'keydown', 195 '.address-field input.input-text, .update_totals_on_change input.input-text', 196 this.queue_update_checkout 197 ); 198 199 // Handle blur on address_1 fields when autocomplete provider is available 200 this.$checkout_form.on( 201 'blur', 202 '#billing_address_1, #shipping_address_1', 203 this.address_field_blur 204 ); 205 206 // Address fields 207 this.$checkout_form.on( 208 'change', 209 '#ship-to-different-address input', 210 this.ship_to_different_address 211 ); 212 213 // Trigger events 214 this.$checkout_form 215 .find( '#ship-to-different-address input' ) 216 .trigger( 'change' ); 217 this.init_payment_methods(); 218 219 // Update on page load 220 if ( wc_checkout_params.is_checkout === '1' ) { 221 $( document.body ).trigger( 'init_checkout' ); 222 } 223 if ( wc_checkout_params.option_guest_checkout === 'yes' ) { 224 $( 'input#createaccount' ) 225 .on( 'change', this.toggle_create_account ) 226 .trigger( 'change' ); 227 } 228 }, 229 init_payment_methods: function () { 230 var $payment_methods = $( '.woocommerce-checkout' ).find( 231 'input[name="payment_method"]' 232 ); 233 234 // If there is one method, we can hide the radio input 235 if ( 1 === $payment_methods.length ) { 236 $payment_methods.eq( 0 ).hide(); 237 } 238 239 // If there was a previously selected method, check that one. 240 if ( wc_checkout_form.selectedPaymentMethod ) { 241 $( '#' + wc_checkout_form.selectedPaymentMethod ).prop( 242 'checked', 243 true 244 ); 245 } 246 247 // If there are none selected, select the first. 248 if ( 0 === $payment_methods.filter( ':checked' ).length ) { 249 $payment_methods.eq( 0 ).prop( 'checked', true ); 250 } 251 252 // Get name of new selected method. 253 var checkedPaymentMethod = $payment_methods 254 .filter( ':checked' ) 255 .eq( 0 ) 256 .prop( 'id' ); 257 258 if ( $payment_methods.length > 1 ) { 259 // Hide open descriptions. 260 $( 'div.payment_box:not(".' + checkedPaymentMethod + '")' ) 261 .filter( ':visible' ) 262 .slideUp( 0 ); 263 } 264 265 // Check if initially selected gateway has custom place order button (via server-side flag) 266 // This hides the default button immediately to prevent flash while the gateway JS loads 267 var $selectedMethod = $payment_methods.filter( ':checked' ).eq( 0 ); 268 if ( $selectedMethod.length ) { 269 window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( $selectedMethod.val() ); 270 } 271 272 // Trigger click event for selected method 273 $payment_methods.filter( ':checked' ).eq( 0 ).trigger( 'click' ); 274 }, 275 get_payment_method: function () { 276 return wc_checkout_form.$checkout_form
277 .find( 'input[name="payment_method"]:checked' ) 278 .val(); 279 }, 280 payment_method_selected: function ( e ) { 281 e.stopPropagation(); 282 283 if ( $( '.payment_methods input.input-radio' ).length > 1 ) { 284 var target_payment_box = $( 285 'div.payment_box.' + $( this ).attr( 'ID' ) 286 ), 287 is_checked = $( this ).is( ':checked' ); 288 289 if ( is_checked && ! target_payment_box.is( ':visible' ) ) { 290 $( 'div.payment_box' ).filter( ':visible' ).slideUp( 230 ); 291 292 if ( is_checked ) { 293 target_payment_box.slideDown( 230 ); 294 } 295 } 296 } else { 297 $( 'div.payment_box' ).show(); 298 } 299 300 if ( $( this ).data( 'order_button_text' ) ) { 301 $( '#place_order' ).text( 302 $( this ).data( 'order_button_text' ) 303 ); 304 } else { 305 $( '#place_order' ).text( $( '#place_order' ).data( 'value' ) ); 306 } 307 308 var selectedPaymentMethod = $( 309 '.woocommerce-checkout input[name="payment_method"]:checked' 310 ).attr( 'id' ); 311 312 if ( 313 selectedPaymentMethod !== wc_checkout_form.selectedPaymentMethod 314 ) { 315 $( document.body ).trigger( 'payment_method_selected' ); 316 } 317 318 // Handle custom place order button 319 var gatewayId = $( this ).val(); 320 window.wc.customPlaceOrderButton.__maybeShow( gatewayId, createCheckoutPlaceOrderApi() ); 321 322 wc_checkout_form.selectedPaymentMethod = selectedPaymentMethod; 323 }, 324 toggle_create_account: function () { 325 $( 'div.create-account' ).hide(); 326 327 if ( $( this ).is( ':checked' ) ) { 328 // Ensure password is not pre-populated. 329 $( '#account_password' ).val( '' ); 330 $( 'div.create-account' ).slideDown(); 331 } 332 }, 333 init_checkout: function () { 334 $( document.body ).trigger( 'update_checkout' ); 335 }, 336 /** 337 * Check if an address_1 field has an active autocomplete provider and should skip checkout updates 338 * @param {Event} e - The event object 339 * @return {boolean} true if updates should be skipped, false otherwise 340 */ 341 should_skip_address_update: function ( e ) { 342 var target = e.target || e.srcElement; 343 if ( 344 target && 345 ( target.id === 'billing_address_1' || 346 target.id === 'shipping_address_1' ) 347 ) { 348 // Skip if we're manipulating the DOM for autocomplete 349 if ( 350 target.getAttribute( 'data-autocomplete-manipulating' ) === 351 'true' 352 ) { 353 return true; 354 } 355 356 var type = target.id.replace( '_address_1', '' ); 357 358 // Check if window.wc.addressAutocomplete exists and has an active provider 359 if ( 360 window.wc && 361 window.wc.addressAutocomplete && 362 window.wc.addressAutocomplete.activeProvider 363 ) { 364 if ( 365 window.wc.addressAutocomplete.activeProvider[ type ] 366 ) { 367 return true; 368 } 369 } 370 } 371 return false; 372 }, 373 /** 374 * Check if an address_1 field has an active autocomplete provider and should trigger checkout updates on blur 375 * @param {Event} e - The event object 376 * @return {boolean} true if updates should be triggered, false otherwise 377 */ 378 should_trigger_address_blur_update: function ( e ) { 379 var target = e.target || e.srcElement; 380 if ( 381 target && 382 ( target.id === 'billing_address_1' || 383 target.id === 'shipping_address_1' ) 384 ) { 385 // Skip if we're manipulating the DOM for autocomplete 386 if ( 387 target.getAttribute( 'data-autocomplete-manipulating' ) === 388 'true' 389 ) { 390 return false; 391 } 392 393 var type = target.id.replace( '_address_1', '' ); 394 395 // Check if window.wc.addressAutocomplete exists and has an active provider 396 if ( 397 window.wc && 398 window.wc.addressAutocomplete && 399 window.wc.addressAutocomplete.activeProvider 400 ) { 401 if ( 402 window.wc.addressAutocomplete.activeProvider[ type ] 403 ) { 404 return true; 405 } 406 } 407 } 408 return false; 409 }, 410 maybe_input_changed: function ( e ) { 411 if ( wc_checkout_form.should_skip_address_update( e ) ) { 412 return; 413 } 414 415 if ( wc_checkout_form.dirtyInput ) { 416 wc_checkout_form.input_changed( e ); 417 } 418 }, 419 input_changed: function ( e ) { 420 if ( wc_checkout_form.should_skip_address_update( e ) ) { 421 return; 422 } 423 424 wc_checkout_form.dirtyInput = e.target; 425 wc_checkout_form.maybe_update_checkout(); 426 }, 427 address_field_blur: function ( e ) { 428 if ( wc_checkout_form.should_trigger_address_blur_update( e ) ) { 429 wc_checkout_form.dirtyInput = e.target; 430 wc_checkout_form.maybe_update_checkout(); 431 } 432 }, 433 queue_update_checkout: function ( e ) { 434 var code = e.keyCode || e.which || 0; 435 436 if ( code === 9 ) { 437 return true; 438 } 439 440 // Check if we're in an address_1 field with an active provider 441 var target = e.target || e.srcElement; 442 if ( 443 target && 444 ( target.id === 'billing_address_1' || 445 target.id === 'shipping_address_1' ) 446 ) { 447 // Check if an autocomplete provider is available for this field 448 var type = target.id.replace( '_address_1', '' ); 449 450 // Check if window.wc.addressAutocomplete exists and has an active provider 451 if ( 452 window.wc && 453 window.wc.addressAutocomplete && 454 window.wc.addressAutocomplete.activeProvider 455 ) { 456 if ( 457 window.wc.addressAutocomplete.activeProvider[ type ] 458 ) { 459 // Provider is available - don't queue updates while typing 460 // Updates will be triggered on blur instead 461 return true; 462 } 463 } 464 } 465 466 wc_checkout_form.dirtyInput = this; 467 wc_checkout_form.reset_update_checkout_timer(); 468 wc_checkout_form.updateTimer = setTimeout( 469 wc_checkout_form.maybe_update_checkout, 470 '1000' 471 ); 472 }, 473 trigger_update_checkout: function ( event ) { 474 wc_checkout_form.reset_update_checkout_timer(); 475 wc_checkout_form.dirtyInput = false;
476 $( document.body ).trigger( 'update_checkout', { 477 current_target: event ? event.currentTarget : null, 478 } ); 479 }, 480 maybe_update_checkout: function () { 481 var update_totals = true; 482 483 if ( $( wc_checkout_form.dirtyInput ).length ) { 484 var $required_inputs = $( wc_checkout_form.dirtyInput ) 485 .closest( 'div' ) 486 .find( '.address-field.validate-required' ); 487 488 if ( $required_inputs.length ) { 489 $required_inputs.each( function () { 490 if ( 491 $( this ).find( 'input.input-text' ).val() === '' 492 ) { 493 update_totals = false; 494 } 495 } ); 496 } 497 } 498 if ( update_totals ) { 499 wc_checkout_form.trigger_update_checkout(); 500 } 501 }, 502 ship_to_different_address: function () { 503 $( 'div.shipping_address' ).hide(); 504 if ( $( this ).is( ':checked' ) ) { 505 $( 'div.shipping_address' ).slideDown(); 506 } 507 }, 508 reset_update_checkout_timer: function () { 509 clearTimeout( wc_checkout_form.updateTimer ); 510 }, 511 is_valid_json: function ( raw_json ) { 512 try { 513 var json = JSON.parse( raw_json ); 514 515 return json && 'object' === typeof json; 516 } catch ( e ) { 517 return false; 518 } 519 }, 520 validate_field: function ( e ) { 521 var $this = $( this ), 522 $parent = $this.closest( '.form-row' ), 523 validated = true, 524 validate_required = $parent.is( '.validate-required' ), 525 validate_email = $parent.is( '.validate-email' ), 526 validate_phone = $parent.is( '.validate-phone' ), 527 pattern = '', 528 event_type = e.type; 529 530 if ( 'input' === event_type ) { 531 $this 532 .removeAttr( 'aria-invalid' ) 533 .removeAttr( 'aria-describedby' ); 534 $parent.find( '.checkout-inline-error-message' ).remove(); 535 $parent.removeClass( 536 // eslint-disable-next-line max-len 537 'woocommerce-invalid woocommerce-invalid-required-field woocommerce-invalid-email woocommerce-invalid-phone woocommerce-validated' 538 ); 539 } 540 541 if ( 542 'validate' === event_type || 543 'change' === event_type ||
544 'focusout' === event_type 545 ) { 546 if ( validate_required ) { 547 if ( 548 ( 'checkbox' === $this.attr( 'type' ) && 549 ! $this.is( ':checked' ) ) || 550 $this.val() === '' 551 ) { 552 $this.attr( 'aria-invalid', 'true' ); 553 $parent 554 .removeClass( 'woocommerce-validated' ) 555 .addClass( 556 'woocommerce-invalid woocommerce-invalid-required-field' 557 ); 558 validated = false; 559 } 560 } 561 562 if ( validate_email ) { 563 if ( $this.val() ) { 564 /* https://stackoverflow.com/questions/2855865/jquery-validate-e-mail-address-regex */ 565 pattern = new RegExp( 566 // eslint-disable-next-line max-len 567 /^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i 568 ); // eslint-disable-line max-len 569 570 if ( ! pattern.test( $this.val() ) ) { 571 $this.attr( 'aria-invalid', 'true' ); 572 $parent 573 .removeClass( 'woocommerce-validated' ) 574 .addClass( 575 'woocommerce-invalid woocommerce-invalid-email' 576 ); // eslint-disable-line max-len 577 validated = false; 578 } 579 } 580 } 581 582 if ( validate_phone ) { 583 pattern = new RegExp( /[\s\#0-9_\-\+\/\(\)\.]/g ); 584 585 if ( 0 < $this.val().replace( pattern, '' ).length ) { 586 $this.attr( 'aria-invalid', 'true' ); 587 $parent 588 .removeClass( 'woocommerce-validated' ) 589 .addClass( 590 'woocommerce-invalid woocommerce-invalid-phone' 591 ); 592 validated = false; 593 } 594 } 595 596 if ( validated ) { 597 $this 598 .removeAttr( 'aria-invalid' ) 599 .removeAttr( 'aria-describedby' ); 600 $parent.find( '.checkout-inline-error-message' ).remove(); 601 $parent 602 .removeClass( 603 'woocommerce-invalid woocommerce-invalid-required-field woocommerce-invalid-email woocommerce-invalid-phone' 604 ) 605 .addClass( 'woocommerce-validated' ); // eslint-disable-line max-len 606 } 607 } 608 }, 609 update_checkout: function ( event, args ) { 610 // Small timeout to prevent multiple requests when several fields update at the same time 611 wc_checkout_form.reset_update_checkout_timer(); 612 wc_checkout_form.updateTimer = setTimeout( 613 wc_checkout_form.update_checkout_action, 614 '5', 615 args 616 ); 617 }, 618 update_checkout_action: function ( args ) { 619 if ( wc_checkout_form.xhr ) { 620 wc_checkout_form.xhr.abort(); 621 } 622 623 if ( $( 'form.checkout' ).length === 0 ) { 624 return; 625 } 626 627 args = 628 typeof args !== 'undefined' 629 ? args 630 : { 631 update_shipping_method: true, 632 }; 633 634 var country = $( '#billing_country' ).val(), 635 state = $( '#billing_state' ).val(), 636 postcode = $( ':input#billing_postcode' ).val(), 637 city = $( '#billing_city' ).val(), 638 address = $( ':input#billing_address_1' ).val(), 639 address_2 = $( ':input#billing_address_2' ).val(), 640 s_country = country, 641 s_state = state, 642 s_postcode = postcode, 643 s_city = city, 644 s_address = address, 645 s_address_2 = address_2, 646 $required_inputs = $( wc_checkout_form.$checkout_form ).find( 647 '.address-field.validate-required:visible' 648 ), 649 has_full_address = true; 650 651 if ( $required_inputs.length ) { 652 $required_inputs.each( function () { 653 if ( $( this ).find( ':input' ).val() === '' ) { 654 has_full_address = false; 655 } 656 } ); 657 } 658 659 if ( 660 $( '#ship-to-different-address' ) 661 .find( 'input' ) 662 .is( ':checked' ) 663 ) { 664 s_country = $( '#shipping_country' ).val(); 665 s_state = $( '#shipping_state' ).val(); 666 s_postcode = $( ':input#shipping_postcode' ).val(); 667 s_city = $( '#shipping_city' ).val(); 668 s_address = $( ':input#shipping_address_1' ).val(); 669 s_address_2 = $( ':input#shipping_address_2' ).val(); 670 } 671 672 var data = { 673 security: wc_checkout_params.update_order_review_nonce, 674 payment_method: wc_checkout_form.get_payment_method(), 675 country: country, 676 state: state, 677 postcode: postcode, 678 city: city, 679 address: address, 680 address_2: address_2, 681 s_country: s_country, 682 s_state: s_state, 683 s_postcode: s_postcode, 684 s_city: s_city, 685 s_address: s_address, 686 s_address_2: s_address_2, 687 has_full_address: has_full_address,
688 post_data: $( 'form.checkout' ).serialize(), 689 }; 690 691 if ( false !== args.update_shipping_method ) { 692 var shipping_methods = {}; 693 694 $( 695 // eslint-disable-next-line max-len 696 'select.shipping_method, input[name^="shipping_method"][type="radio"]:checked, input[name^="shipping_method"][type="hidden"]' 697 ).each( function () { 698 shipping_methods[ $( this ).data( 'index' ) ] = 699 $( this ).val(); 700 } ); 701 702 data.shipping_method = shipping_methods; 703 } 704 705 $( 706 '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' 707 ).block( { 708 message: null, 709 overlayCSS: { 710 background: '#fff', 711 opacity: 0.6, 712 }, 713 } ); 714 715 wc_checkout_form.xhr = $.ajax( { 716 type: 'POST', 717 url: wc_checkout_params.wc_ajax_url 718 .toString() 719 .replace( '%%endpoint%%', 'update_order_review' ), 720 data: data, 721 success: function ( data ) { 722 // Reload the page if requested 723 if ( data && true === data.reload ) { 724 window.location.reload(); 725 return; 726 } 727 728 // Remove any notices added previously 729 $( '.woocommerce-NoticeGroup-updateOrderReview' ).remove(); 730 731 var termsCheckBoxChecked = $( '#terms' ).prop( 'checked' ); 732 733 // Save payment details to a temporary object 734 var paymentDetails = {}; 735 $( '.payment_box :input' ).each( function () { 736 var ID = $( this ).attr( 'id' ); 737 738 if ( ID ) { 739 if ( 740 $.inArray( $( this ).attr( 'type' ), [ 741 'checkbox', 742 'radio', 743 ] ) !== -1 744 ) { 745 paymentDetails[ ID ] = 746 $( this ).prop( 'checked' ); 747 } else { 748 paymentDetails[ ID ] = $( this ).val(); 749 } 750 } 751 } ); 752 753 // Always update the fragments 754 if ( data && data.fragments ) { 755 $.each( data.fragments, function ( key, value ) { 756 if ( 757 ! wc_checkout_form.fragments || 758 wc_checkout_form.fragments[ key ] !== value 759 ) { 760 $( key ).replaceWith( value ); 761 } 762 $( key ).unblock(); 763 } ); 764 wc_checkout_form.fragments = data.fragments; 765 } 766 767 // Recheck the terms and conditions box, if needed 768 if ( termsCheckBoxChecked ) { 769 $( '#terms' ).prop( 'checked', true ); 770 } 771 772 // Fill in the payment details if possible without overwriting data if set. 773 if ( ! $.isEmptyObject( paymentDetails ) ) { 774 $( '.payment_box :input' ).each( function () { 775 var ID = $( this ).attr( 'id' ); 776 if ( ID ) { 777 if ( 778 $.inArray( $( this ).attr( 'type' ), [ 779 'checkbox', 780 'radio', 781 ] ) !== -1 782 ) { 783 $( this ) 784 .prop( 'checked', paymentDetails[ ID ] ) 785 .trigger( 'change' ); 786 } else if ( 787 $.inArray( $( this ).attr( 'type' ), [ 788 'select', 789 ] ) !== -1 790 ) { 791 $( this ) 792 .val( paymentDetails[ ID ] ) 793 .trigger( 'change' ); 794 } else if ( 795 null !== $( this ).val() && 796 0 === $( this ).val().length 797 ) { 798 $( this ) 799 .val( paymentDetails[ ID ] ) 800 .trigger( 'change' ); 801 } 802 } 803 } ); 804 } 805 806 // Check for error 807 if ( data && 'failure' === data.result ) { 808 var $form = $( 'form.checkout' ); 809 810 // Remove notices from all sources 811 $( 812 '.woocommerce-error, .woocommerce-message, .is-error, .is-success' 813 ).remove(); 814 815 // Add new errors returned by this event 816 if ( data.messages ) { 817 $form.prepend( 818 '<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-updateOrderReview">' + 819 data.messages + 820 '</div>' 821 ); // eslint-disable-line max-len 822 } else { 823 $form.prepend( data ); 824 } 825 826 // Lose focus for all fields 827 $form 828 .find( '.input-text, select, input:checkbox' ) 829 .trigger( 'validate' ) 830 .trigger( 'blur' ); 831 832 wc_checkout_form.scroll_to_notices(); 833 } 834 835 // Re-init methods 836 wc_checkout_form.init_payment_methods(); 837 838 // If there is no errors and the checkout update was triggered by changing the shipping method, focus its radio input. 839 if ( 840 data && 841 'success' === data.result && 842 args.current_target && 843 args.current_target.id.indexOf( 'shipping_method' ) !== 844 -1 845 ) { 846 document 847 .getElementById( args.current_target.id ) 848 .focus(); 849 } 850 851 // Fire updated_checkout event. 852 $( document.body ).trigger( 'updated_checkout', [ data ] ); 853 }, 854 } ); 855 }, 856 handleUnloadEvent: function ( e ) { 857 // Modern browsers have their own standard generic messages that they will display. 858 // Confirm, alert, prompt or custom message are not allowed during the unload event 859 // Browsers will display their own standard messages 860 861 // Check if the browser is Internet Explorer 862 if ( 863 navigator.userAgent.indexOf( 'MSIE' ) !== -1 || 864 !! document.documentMode 865 ) { 866 // IE handles unload events differently than modern browsers 867 e.preventDefault(); 868 return undefined; 869 } 870 871 return true; 872 }, 873 attachUnloadEventsOnSubmit: function () { 874 $( window ).on( 'beforeunload', this.handleUnloadEvent ); 875 }, 876 detachUnloadEventsOnSubmit: function () { 877 $( window ).off( 'beforeunload', this.handleUnloadEvent ); 878 }, 879 blockOnSubmit: function ( $form ) { 880 var isBlocked = $form.data( 'blockUI.isBlocked' ); 881 882 if ( 1 !== isBlocked ) { 883 $form.block( { 884 message: null, 885 overlayCSS: { 886 background: '#fff', 887 opacity: 0.6, 888 }, 889 } ); 890 } 891 }, 892 submitOrder: function () { 893 wc_checkout_form.blockOnSubmit( $( this ) ); 894 }, 895 submit: function () { 896 wc_checkout_form.reset_update_checkout_timer(); 897 var $form = $( this ); 898 899 if ( $form.is( '.processing' ) ) { 900 return false;
901 } 902 903 // Trigger a handler to let gateways manipulate the checkout if needed 904 if ( 905 $form.triggerHandler( 'checkout_place_order', [ 906 wc_checkout_form, 907 ] ) !== false && 908 $form.triggerHandler( 909 'checkout_place_order_' + 910 wc_checkout_form.get_payment_method(), 911 [ wc_checkout_form ] 912 ) !== false 913 ) { 914 $form.addClass( 'processing' ); 915 916 wc_checkout_form.blockOnSubmit( $form ); 917 918 // Attach event to block reloading the page when the form has been submitted 919 wc_checkout_form.attachUnloadEventsOnSubmit(); 920 921 // ajaxSetup is global, but we use it to ensure JSON is valid once returned. 922 $.ajaxSetup( { 923 dataFilter: function ( raw_response, dataType ) { 924 // We only want to work with JSON 925 if ( 'json' !== dataType ) { 926 return raw_response; 927 } 928 929 if ( wc_checkout_form.is_valid_json( raw_response ) ) { 930 return raw_response; 931 } else { 932 // Attempt to fix the malformed JSON 933 var maybe_valid_json = 934 raw_response.match( /{"result.*}/ ); 935 936 if ( null === maybe_valid_json ) { 937 console.log( 938 'Unable to fix malformed JSON #1' 939 ); 940 } else if ( 941 wc_checkout_form.is_valid_json( 942 maybe_valid_json[ 0 ] 943 ) 944 ) { 945 console.log( 946 'Fixed malformed JSON. Original:' 947 ); 948 console.log( raw_response ); 949 raw_response = maybe_valid_json[ 0 ]; 950 } else { 951 console.log( 952 'Unable to fix malformed JSON #2' 953 ); 954 } 955 } 956 957 return raw_response; 958 }, 959 } ); 960 961 $.ajax( { 962 type: 'POST', 963 url: wc_checkout_params.checkout_url, 964 data: $form.serialize(), 965 dataType: 'json', 966 success: function ( result ) { 967 // Detach the unload handler that prevents a reload / redirect 968 wc_checkout_form.detachUnloadEventsOnSubmit(); 969 970 $( '.checkout-inline-error-message' ).remove(); 971 972 try { 973 if ( 974 'success' === result.result && 975 $form.triggerHandler( 976 'checkout_place_order_success', 977 [ result, wc_checkout_form ] 978 ) !== false 979 ) { 980 if ( 981 -1 === 982 result.redirect.indexOf( 'https://' ) || 983 -1 === result.redirect.indexOf( 'http://' ) 984 ) { 985 window.location = result.redirect; 986 } else { 987 window.location = decodeURI( 988 result.redirect 989 ); 990 } 991 } else if ( 'failure' === result.result ) { 992 throw 'Result failure'; 993 } else { 994 throw 'Invalid response'; 995 } 996 } catch ( err ) { 997 // Reload page 998 if ( true === result.reload ) { 999 window.location.reload(); 1000 return; 1001 } 1002 1003 // Trigger update in case we need a fresh nonce 1004 if ( true === result.refresh ) { 1005 $( document.body ).trigger( 'update_checkout' ); 1006 } 1007 1008 // Add new errors 1009 if ( result.messages ) { 1010 var $msgs = $( result.messages ) 1011 // The error notice template (plugins/woocommerce/templates/notices/error.php) 1012 // adds the role="alert" to a list HTML element. This becomes a problem in this context 1013 // because screen readers won't read the list content correctly if its role is not "list". 1014 .removeAttr( 'role' ) 1015 .attr( 'tabindex', '-1' ); 1016 var $msgsWithLink = 1017 wc_checkout_form.wrapMessagesInsideLink( 1018 $msgs 1019 ); 1020 var $msgsWrapper = $( 1021 '<div role="alert"></div>' 1022 ).append( $msgsWithLink ); 1023 1024 wc_checkout_form.submit_error( 1025 $msgsWrapper.prop( 'outerHTML' ) 1026 ); 1027 wc_checkout_form.show_inline_errors( $msgs ); 1028 } else { 1029 wc_checkout_form.submit_error( 1030 '<div class="woocommerce-error">' + 1031 wc_checkout_params.i18n_checkout_error + 1032 '</div>' 1033 ); // eslint-disable-line max-len 1034 } 1035 } 1036 }, 1037 error: function ( jqXHR, textStatus, errorThrown ) { 1038 // Detach the unload handler that prevents a reload / redirect 1039 wc_checkout_form.detachUnloadEventsOnSubmit(); 1040 1041 // This is just a technical error fallback. i18_checkout_error is expected to be al
1041ways defined and localized. 1042 var errorMessage = errorThrown; 1043 1044 if ( 1045 typeof wc_checkout_params === 'object' && 1046 wc_checkout_params !== null && 1047 wc_checkout_params.hasOwnProperty( 1048 'i18n_checkout_error' 1049 ) && 1050 typeof wc_checkout_params.i18n_checkout_error === 1051 'string' && 1052 wc_checkout_params.i18n_checkout_error.trim() !== '' 1053 ) { 1054 errorMessage = 1055 wc_checkout_params.i18n_checkout_error; 1056 } 1057 1058 wc_checkout_form.submit_error( 1059 '<div class="woocommerce-error">' + 1060 errorMessage + 1061 '</div>' 1062 ); 1063 }, 1064 } ); 1065 } 1066 1067 return false; 1068 }, 1069 submit_error: function ( error_message ) { 1070 $( 1071 '.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message, .is-error, .is-success' 1072 ).remove(); 1073 wc_checkout_form.$checkout_form.prepend( 1074 '<div class="woocommerce-NoticeGroup woocommerce-NoticeGroup-checkout">' + 1075 error_message + 1076 '</div>' 1077 ); // eslint-disable-line max-len 1078 wc_checkout_form.$checkout_form 1079 .removeClass( 'processing' ) 1080 .unblock(); 1081 wc_checkout_form.$checkout_form 1082 .find( '.input-text, select, input:checkbox' ) 1083 .trigger( 'validate' ) 1084 .trigger( 'blur' ); 1085 wc_checkout_form.scroll_to_notices(); 1086 wc_checkout_form.$checkout_form 1087 .find( 1088 '.woocommerce-error[tabindex="-1"], .wc-block-components-notice-banner.is-error[tabindex="-1"]' 1089 ) 1090 .trigger( 'focus' ); 1091 $( document.body ).trigger( 'checkout_error', [ error_message ] ); 1092 }, 1093 wrapMessagesInsideLink: function ( $msgs ) { 1094 $msgs.find( 'li[data-id]' ).each( function () { 1095 const $this = $( this ); 1096 const dataId = $this.attr( 'data-id' ); 1097 if ( dataId ) { 1098 const $link = $( '<a>', { 1099 href: '#' + dataId, 1100 html: $this.html(), 1101 } ); 1102 $this.empty().append( $link ); 1103 } 1104 } ); 1105 1106 return $msgs; 1107 }, 1108 show_inline_errors: function ( $messages ) { 1109 $messages.find( 'li[data-id]' ).each( function () { 1110 const $this = $( this ); 1111 const dataId = $this.attr( 'data-id' ); 1112 const $field = $( '#' + dataId ); 1113 1114 if ( $field.length === 1 ) { 1115 const descriptionId = dataId + '_description'; 1116 const msg = $this.text().trim(); 1117 const $formRow = $field.closest( '.form-row' ); 1118 1119 const errorMessage = document.createElement( 'p' ); 1120 errorMessage.id = descriptionId; 1121 errorMessage.className = 'checkout-inline-error-message'; 1122 errorMessage.textContent = msg; 1123 1124 if ( $formRow && errorMessage.textContent.length > 0 ) { 1125 $formRow.append( errorMessage ); 1126 } 1127 1128 $field.attr( 'aria-describedby', descriptionId ); 1129 $field.attr( 'aria-invalid', 'true' ); 1130 } 1131 } ); 1132 }, 1133 scroll_to_notices: function () { 1134 var scrollElement = $( 1135 '.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout' 1136 ); 1137 1138 if ( ! scrollElement.length ) { 1139 scrollElement = $( 'form.checkout' ); 1140 } 1141 $.scroll_to_notices( scrollElement ); 1142 }, 1143 }; 1144 1145 var wc_checkout_coupons = { 1146 init: function () { 1147 $( document.body ).on( 1148 'click', 1149 'a.showcoupon', 1150 this.show_coupon_form 1151 ); 1152 $( document.body ).on( 1153 'click', 1154 '.woocommerce-remove-coupon', 1155 this.remove_coupon 1156 ); 1157 $( document.body ).on( 1158 'keydown', 1159 '.woocommerce-remove-coupon', 1160 this.on_keydown_remove_coupon 1161 ); 1162 $( document.body ).on( 1163 'change input', 1164 '#coupon_code', 1165 this.remove_coupon_error 1166 ); 1167 $( 'form.checkout_coupon' ) 1168 .hide() 1169 .on( 'submit', this.submit.bind( this ) ); 1170 }, 1171 show_coupon_form: function () { 1172 var $showcoupon = $( this ); 1173 1174 $( '.checkout_coupon' ).slideToggle( 400, function () { 1175 var $coupon_form = $( this ); 1176 1177 if ( $coupon_form.is( ':visible' ) ) { 1178 $showcoupon.attr( 'aria-expanded', 'true' ); 1179 $coupon_form.find( ':input:eq(0)' ).trigger( 'focus' ); 1180 } else { 1181 $showcoupon.attr( 'aria-expanded', 'false' ); 1182 } 1183 } ); 1184 return false; 1185 }, 1186 show_coupon_error: function ( html_element, $target ) { 1187 if ( $target.length === 0 ) { 1188 return; 1189 } 1190 1191 this.remove_coupon_error(); 1192 1193 var msg = $( $.parseHTML( html_element ) ).text().trim(); 1194 1195 if ( msg === '' ) { 1196 return; 1197 } 1198 1199 $target 1200 .find( '#coupon_code' ) 1201 .trigger( 'focus' ) 1202 .addClass( 'has-error' ) 1203 .attr( 'aria-invalid', 'true' ) 1204 .attr( 'aria-describedby', 'coupon-error-notice' ); 1205 1206 $( '<span>', { 1207 class: 'coupon-error-notice', 1208 id: 'coupon-error-notice', 1209 role: 'alert', 1210 text: msg, 1211 } ).appendTo( $target ); 1212 }, 1213 remove_coupon_error: function () { 1214 var $coupon_field = $( '#coupon_code' ); 1215 1216 if ( $coupon_field.length === 0 ) { 1217 return; 1218 } 1219 1220 $coupon_field 1221 .removeClass( 'has-error' ) 1222 .removeAttr( 'aria-invalid' ) 1223 .removeAttr( 'aria-describedby' ) 1224 .next( '.coupon-error-notice' ) 1225 .remove(); 1226 }, 1227 1228 clear_coupon_input: function () { 1229 const $coupon_field = $( '#coupon_code' ); 1230 $coupon_field 1231 .val( '' ) 1232 .removeClass( 'has-error' ) 1233 .removeAttr( 'aria-invalid' ) 1234 .removeAttr( 'aria-describedby' ) 1235 .next( '.coupon-error-notice' ) 1236 .remove(); 1237 }, 1238 submit: function ( evt ) { 1239 var $form = $( evt.currentTarget ); 1240 var $coupon_field = $form.find( '#coupon_code' ); 1241 var self = this; 1242 1243 self.remove_coupon_error(); 1244 1245 if ( $form.is( '.processing' ) ) { 1246 return false;
1247 } 1248 1249 $form.addClass( 'processing' ).block( { 1250 message: null, 1251 overlayCSS: { 1252 background: '#fff', 1253 opacity: 0.6, 1254 }, 1255 } ); 1256 1257 var data = { 1258 security: wc_checkout_params.apply_coupon_nonce, 1259 coupon_code: $form.find( 'input[name="coupon_code"]' ).val(), 1260 billing_email: wc_checkout_form.$checkout_form 1261 .find( 'input[name="billing_email"]' ) 1262 .val(), 1263 }; 1264 1265 $.ajax( { 1266 type: 'POST', 1267 url: wc_checkout_params.wc_ajax_url 1268 .toString() 1269 .replace( '%%endpoint%%', 'apply_coupon' ), 1270 data: data, 1271 success: function ( response ) { 1272 $( 1273 '.woocommerce-error, .woocommerce-message, .is-error, .is-success, .checkout-inline-error-message' 1274 ).remove(); 1275 $form.removeClass( 'processing' ).unblock(); 1276 1277 if ( response ) { 1278 // We only want to show coupon notices if they are no errors. 1279 // Coupon errors are shown under the input. 1280 if ( 1281 response.indexOf( 'woocommerce-error' ) === -1 && 1282 response.indexOf( 'is-error' ) === -1 1283 ) { 1284 $form.slideUp( 400, function () { 1285 $( 'a.showcoupon' ).attr( 1286 'aria-expanded', 1287 'false' 1288 ); 1289 $form.before( response ); 1290 } ); 1291 self.clear_coupon_input(); 1292 } else { 1293 self.show_coupon_error( 1294 response, 1295 $coupon_field.parent() 1296 ); 1297 } 1298 1299 $( document.body ).trigger( 1300 'applied_coupon_in_checkout', 1301 [ data.coupon_code ] 1302 ); 1303 $( document.body ).trigger( 'update_checkout', { 1304 update_shipping_method: false, 1305 } ); 1306 } 1307 }, 1308 dataType: 'html', 1309 } ); 1310 1311 return false; 1312 }, 1313 remove_coupon: function ( e ) { 1314 e.preventDefault(); 1315 1316 var container = $( this ).parents( 1317 '.woocommerce-checkout-review-order' 1318 ), 1319 coupon = $( this ).data( 'coupon' ); 1320 1321 container.addClass( 'processing' ).block( { 1322 message: null, 1323 overlayCSS: { 1324 background: '#fff', 1325 opacity: 0.6, 1326 }, 1327 } ); 1328 1329 var data = { 1330 security: wc_checkout_params.remove_coupon_nonce, 1331 coupon: coupon, 1332 }; 1333 1334 $.ajax( { 1335 type: 'POST', 1336 url: wc_checkout_params.wc_ajax_url 1337 .toString() 1338 .replace( '%%endpoint%%', 'remove_coupon' ), 1339 data: data, 1340 success: function ( code ) { 1341 $( 1342 '.woocommerce-error, .woocommerce-message, .is-error, .is-success' 1343 ).remove(); 1344 container.removeClass( 'processing' ).unblock(); 1345 1346 if ( code ) { 1347 $( 'form.woocommerce-checkout' ).before( code ); 1348 1349 $( document.body ).trigger( 1350 'removed_coupon_in_checkout', 1351 [ data.coupon ] 1352 ); 1353 $( document.body ).trigger( 'update_checkout', { 1354 update_shipping_method: false, 1355 } ); 1356 1357 // Remove coupon code from coupon field 1358 wc_checkout_coupons.clear_coupon_input();
1359 $( 'form.checkout_coupon' ).slideUp( 400, function () { 1360 $( 'a.showcoupon' ).attr( 1361 'aria-expanded', 1362 'false' 1363 ); 1364 } ); 1365 } 1366 }, 1367 error: function ( jqXHR ) { 1368 if ( wc_checkout_params.debug_mode ) { 1369 /* jshint devel: true */ 1370 console.log( jqXHR.responseText ); 1371 } 1372 }, 1373 dataType: 'html', 1374 } ); 1375 }, 1376 /** 1377 * Handle when pressing the Space key on the remove coupon link. 1378 * This is necessary because the link got the role="button" attribute 1379 * and needs to act like a button. 1380 * 1381 * @param {Object} e The JQuery event 1382 */ 1383 on_keydown_remove_coupon: function ( e ) { 1384 if ( e.key === ' ' ) { 1385 e.preventDefault(); 1386 $( this ).trigger( 'click' ); 1387 } 1388 }, 1389 }; 1390 1391 var wc_checkout_login_form = { 1392 init: function () { 1393 $( document.body ).on( 1394 'click', 1395 'a.showlogin', 1396 this.show_login_form 1397 ); 1398 }, 1399 show_login_form: function () { 1400 var $form = $( 'form.login, form.woocommerce-form--login' ); 1401 if ( $form.is( ':visible' ) ) { 1402 // If already visible, hide it. 1403 $form.slideToggle( { 1404 duration: 400, 1405 } ); 1406 } else { 1407 // If not visible, show it and then scroll 1408 $form.slideToggle( { 1409 duration: 400, 1410 complete: function () { 1411 if ( $form.is( ':visible' ) ) { 1412 $( 'html, body' ).animate( 1413 { 1414 scrollTop: $form.offset().top - 50, 1415 }, 1416 300 1417 ); 1418 } 1419 }, 1420 } ); 1421 } 1422 return false; 1423 }, 1424 }; 1425 1426 var wc_terms_toggle = { 1427 init: function () { 1428 $( document.body ).on( 1429 'click', 1430 'a.woocommerce-terms-and-conditions-link', 1431 this.toggle_terms 1432 ); 1433 }, 1434 1435 toggle_terms: function () { 1436 if ( $( '.woocommerce-terms-and-conditions' ).length ) { 1437 $( '.woocommerce-terms-and-conditions' ).slideToggle( 1438 function () { 1439 var link_toggle = $( 1440 '.woocommerce-terms-and-conditions-link' 1441 ); 1442 1443 if ( 1444 $( '.woocommerce-terms-and-conditions' ).is( 1445 ':visible' 1446 ) 1447 ) { 1448 link_toggle.addClass( 1449 'woocommerce-terms-and-conditions-link--open' 1450 ); 1451 link_toggle.removeClass( 1452 'woocommerce-terms-and-conditions-link--closed' 1453 ); 1454 } else { 1455 link_toggle.removeClass( 1456 'woocommerce-terms-and-conditions-link--open' 1457 ); 1458 link_toggle.addClass( 1459 'woocommerce-terms-and-conditions-link--closed' 1460 ); 1461 } 1462 } 1463 ); 1464 1465 return false; 1466 } 1467 }, 1468 }; 1469 1470 wc_checkout_form.init(); 1471 wc_checkout_coupons.init(); 1472 wc_checkout_login_form.init(); 1473 wc_terms_toggle.init(); 1474} );
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.