1(function ($) { 2 'use strict'; 3 4 var MOBILE_QUERY = '(max-width: 992px)'; 5 var settings = window.HoningOnlineBuybar || {}; 6 var observers = []; 7 var scheduled = new WeakMap(); 8 9 function isMobile() { 10 return window.matchMedia(MOBILE_QUERY).matches; 11 } 12 13 function cleanText(node) { 14 return node ? String(node.textContent || '').replace(/\s+/g, ' ').trim() : ''; 15 } 16 17 function firstAmountOutsideDel(root) { 18 if (!root) { return ''; } 19 var candidates = root.querySelectorAll('.woocommerce-Price-amount, .amount'); 20 for (var i = 0; i < candidates.length; i += 1) { 21 if (!candidates[i].closest('del')) { 22 return cleanText(candidates[i]); 23 } 24 } 25 return ''; 26 } 27 28 function parsePriceNode(root) { 29 if (!root) { return null; } 30 31 var oldPrice = cleanText(root.querySelector('del')); 32 var currentPrice = cleanText(root.querySelector('ins')); 33 34 if (!currentPrice) { 35 currentPrice = firstAmountOutsideDel(root); 36 } 37 38 if (!currentPrice) { 39 var text = cleanText(root); 40 if (text && /[â¬$£]|\d[\d.,]*\s*(EUR|USD|GBP)?/i.test(text)) { 41 currentPrice = text; 42 } 43 } 44 45 if (!currentPrice) { return null; } 46 47 return { 48 oldPrice: oldPrice, 49 currentPrice: currentPrice 50 }; 51 } 52 53 function parsePriceHtml(html) { 54 if (!html) { return null; } 55 var holder = document.createElement('div'); 56 holder.innerHTML = html; 57 return parsePriceNode(holder); 58 } 59 60 function formatEuro(value) { 61 var number = parseFloat(value); 62 if (!isFinite(number)) { return ''; } 63 try { 64 return new Intl.NumberFormat('nl-NL', { 65 style: 'currency', 66 currency: 'EUR' 67 }).format(number).replace(/\s/g, ''); 68 } catch (e) { 69 return 'â¬' + number.toFixed(2).replace('.', ','); 70 } 71 } 72 73 function selectedWpcPrice($form) { 74 var option = $form.find('.woovr-variation-select option:selected').get(0); 75 if (!option) { return null; } 76 77 var fromHtml = parsePriceHtml(option.getAttribute('data-pricehtml') || ''); 78 if (fromHtml) { return fromHtml; } 79 80 var regular = parseFloat(option.getAttribute('data-regular-price')); 81 var current = parseFloat(option.getAttribute('data-price')); 82 if (!isFinite(current)) { return null; } 83 84 return { 85 oldPrice: isFinite(regular) && regular > current ? formatEuro(regular) : '', 86 currentPrice: formatEuro(current) 87 }; 88 } 89 90 function sourcePriceForSimple($form) { 91 return $form.closest('.product').find('.summary > p.price, .summary p.price').first(); 92 } 93 94 function priceForVariable($form) { 95 var nativePrice = $form.find('.woocommerce-variation-price').first().get(0); 96 var parsed = parsePriceNode(nativePrice); 97 if (parsed) { return parsed; } 98 99 parsed = selectedWpcPrice($form); 100 if (parsed) { return parsed; } 101 102 return parsePriceNode( 103 $form.closest('.product').find('.summary > p.price, .summary p.price').first().get(0) 104 ); 105 } 106 107 function priceForSimple($form) { 108 return parsePriceNode(sourcePriceForSimple($form).get(0)); 109 } 110 111 function sourcePriceForBundle($form) { 112 var $source = sourcePriceForSimple($form); 113 if ($source.length) { $source.addClass('ho-mobile-source-price'); } 114 return $source; 115 } 116 117 function priceForBundle($form) { 118 var parsed = parsePriceNode(sourcePriceForBundle($form).get(0)); 119 if (parsed) { return parsed; } 120 121 var $state = $form.find('.hg-bundle-sticky-price-state').first(); 122 if (!$state.length) { return null; } 123 124 var regular = parseFloat($state.attr('data-regular')); 125 var current = parseFloat($state.attr('data-current')); 126 if (!isFinite(current)) { return null; } 127 128 return { 129 oldPrice: isFinite(regular) && regular > current ? formatEuro(regular) : '', 130 currentPrice: formatEuro(current) 131 }; 132 } 133 134 function ensurePriceRow($form) { 135 var $row = $form.children('.ho-mobile-price-row').first(); 136 if ($row.length) { return $row; } 137 138 $row = $( 139 '<div class="ho-mobile-price-row" aria-live="polite">' + 140 '<span class="ho-mobile-price-label">Prijs per stuk:</span>' + 141 '<span class="ho-mobile-price-values"></span>' + 142 '</div>' 143 ); 144 145 $form.prepend($row); 146 return $row; 147 } 148 149 function renderPrice($form) { 150 var type = String($form.attr('data-ho-buybar-type') || ''); 151 if (!type) { return; } 152 153 var price = type === 'variable' ? priceForVariable($form) : (type === 'bundle' ? priceForBundle($form) : priceForSimple($form)); 154 if (!price || !price.currentPrice) { return; } 155 156 var $values = ensurePriceRow($form).find('.ho-mobile-price-values').empty(); 157 158 if (price.oldPrice && price.oldPrice !== price.currentPrice) { 159 $('<span class="ho-mobile-old-price"></span>').text(price.oldPrice).appendTo($values); 160 $('<span class="ho-mobile-price-word">voor</span>').appendTo($values); 161 } 162 163 $('<span class="ho-mobile-current-price"></span>').text(price.currentPrice).appendTo($values); 164 } 165 166 function scheduleRender($form) { 167 if (!$form || !$form.length) { return; } 168 var node = $form.get(0); 169 if (scheduled.get(node)) { return; } 170 scheduled.set(node, true); 171 window.requestAnimationFrame(function () { 172 scheduled.delete(node); 173 renderPrice($form);
174 if (String($form.attr('data-ho-buybar-type') || '') === 'bundle') { 175 syncBundlePrompt($form); 176 } 177 syncHgHeight($form); 178 }); 179 } 180 181 function ensureButtonArt($form) { 182 var $button = $form.find('.single_add_to_cart_button').first(); 183 if (!$button.length) { return; } 184 185 if (!$button.children('.ho-mobile-cart-icon').length && settings.cartIcon) { 186 $('<img class="ho-mobile-cart-icon" alt="" aria-hidden="true">') 187 .attr('src', settings.cartIcon) 188 .appendTo($button); 189 } 190 191 if (!$button.children('.ho-mobile-cart-bee').length && settings.beeIcon) { 192 $('<img class="ho-mobile-cart-bee" alt="" aria-hidden="true">') 193 .attr('src', settings.beeIcon) 194 .appendTo($button); 195 } 196 } 197 198 function syncHgHeight($form) { 199 if (!isMobile() || !$form || !$form.length) { return; } 200 window.requestAnimationFrame(function () { 201 var height = Math.ceil($form.outerHeight() || 0); 202 if (height > 0) { 203 document.documentElement.style.setProperty('--hg-mobile-buybar-height', height + 'px'); 204 } 205 }); 206 } 207 208 function watchNode(node, $form) { 209 if (!node || !window.MutationObserver) { return; } 210 var observer = new MutationObserver(function () { 211 scheduleRender($form); 212 }); 213 observer.observe(node, { 214 childList: true, 215 subtree: true, 216 characterData: true 217 }); 218 observers.push(observer); 219 } 220 221 function ensureOutOfStockSimpleBar($product) { 222 if (!isMobile() || !$product || !$product.length) { return; } 223 if ($product.hasClass('product-type-hg_bundle') || !$product.hasClass('product-type-simple')) { return; } 224 225 var $summary = $product.find('.summary').first(); 226 if (!$summary.length || $summary.find('form.cart').length) { return; } 227 228 var isOutOfStock = $product.hasClass('outofstock') || $summary.find('.stock.out-of-stock').length > 0; 229 if (!isOutOfStock) { return; } 230 231 var $existing = $summary.children('form.cart.ho-mobile-buybar--out-of-stock').first(); 232 if ($existing.length) { return; } 233 234 var $source = $summary.children('p.price').first(); 235 if (!$source.length) { $source = $summary.find('p.price').first(); } 236 if (!$source.length) { return; } 237 238 var $form = $( 239 '<form class="cart ho-mobile-buybar ho-mobile-buybar--simple ho-mobile-buybar--out-of-stock" ' + 240 'data-ho-buybar-type="simple" data-ho-buybar-initialized="1" ' + 241 'data-ho-generated-out-of-stock="1" aria-label="Tijdelijk uitverkocht">' + 242 '<div class="quantity">' + 243 '<input type="number" class="input-text qty text" value="1" min="1" step="1" disabled ' + 244 'aria-label="Aantal (tijdelijk uitverkocht)">' + 245 '</div>' + 246 '<button type="button" class="single_add_to_cart_button button alt ho-mobile-out-of-stock-button" ' + 247 'disabled aria-disabled="true" title="Tijdelijk uitverkocht">' + 248 '<span class="screen-reader-text">Tijdelijk uitverkocht</span>' + 249 '</button>' + 250 '</form>' 251 ); 252 253 // WooCommerce does not output form.cart for an out-of-stock simple product. 254 // Add a non-submitting visual buy bar so price/layout stay consistent, while 255 // keeping every purchase control disabled. 256 $summary.append($form); 257 $source.addClass('ho-mobile-source-price'); 258 259 ensurePriceRow($form); 260 ensureButtonArt($form); 261 renderPrice($form); 262 watchNode($source.get(0), $form); 263 syncHgHeight($form); 264 } 265 266 function initOutOfStockSimpleBars() { 267 if (!isMobile()) { return; } 268 $('.woocommerce.single-product .product.product-type-simple').each(function () { 269 ensureOutOfStockSimpleBar($(this)); 270 }); 271 } 272 273 function bundleChoiceValid($choice) { 274 if (!$choice || !$choice.length) { return true; } 275 var min = parseInt($choice.attr('data-min'), 10) || 1; 276 var max = parseInt($choice.attr('data-max'), 10) || 0; 277 var count = $choice.find('.hg-bundle-choice-input:checked').length; 278 return count >= min && (!max || count <= max); 279 } 280 281 function syncBundlePrompt($form) { 282 if (!isMobile() || !$form || !$form.length) { return; } 283 284 var $product = $form.closest('.product'); 285 var $choice = $product.find('.hg-bundle-choice').first(); 286 var $button = $form.find('.single_add_to_cart_button').first(); 287 if (!$button.length) { return; } 288 289 if (!$choice.length || bundleChoiceValid($choice)) { 290 $button.removeClass('ho-mobile-bundle-needs-selection'); 291 if ($choice.length) { 292 $button.removeAttr('data-hg-config-target').attr('aria-disabled', 'false').prop('disabled', false).removeClass('disabled'); 293 } 294 if (!$button.children('.ho-mobile-cart-icon').length) { 295 $button.empty().append('<span class="screen-reader-text">
295Toevoegen aan winkelwagen</span>'); 296 ensureButtonArt($form); 297 } 298 return; 299 } 300 301 // Honing Gemak already owns the scroll-to-choice behaviour through this 302 // data attribute. We only give that existing state the approved compact UI. 303 $button 304 .addClass('ho-mobile-bundle-needs-selection') 305 .attr('data-hg-config-target', '.hg-bundle-choice') 306 .attr('aria-disabled', 'true') 307 .prop('disabled', false) 308 .removeClass('disabled wc-variation-selection-needed') 309 .html('<span class="ho-mobile-select-label">Selecteer</span><span class="ho-mobile-select-arrow" aria-hidden="true">â</span>'); 310 } 311 312 function initBundle($form) { 313 var $product = $form.closest('.product'); 314 if (!$product.hasClass('product-type-hg_bundle')) { return false; } 315 316 $form 317 .addClass('ho-mobile-buybar ho-mobile-buybar--bundle') 318 .attr('data-ho-buybar-type', 'bundle'); 319 320 var $source = sourcePriceForBundle($form); 321 ensurePriceRow($form); 322 ensureButtonArt($form); 323 renderPrice($form); 324 syncBundlePrompt($form); 325 326 if ($source.length) { watchNode($source.get(0), $form); } 327 syncHgHeight($form); 328 return true; 329 } 330 331 function initSimple($form) { 332 var $product = $form.closest('.product'); 333 if ($product.hasClass('product-type-hg_bundle')) { return false; } 334 if (!$product.hasClass('product-type-simple') || $form.hasClass('variations_form')) { return false; } 335 if ($form.find('.woovr-variations').length) { return false; } 336 337 $form 338 .addClass('ho-mobile-buybar ho-mobile-buybar--simple') 339 .attr('data-ho-buybar-type', 'simple'); 340 341 var $source = sourcePriceForSimple($form); 342 $source.addClass('ho-mobile-source-price'); 343 344 ensurePriceRow($form); 345 ensureButtonArt($form); 346 renderPrice($form); 347 watchNode($source.get(0), $form); 348 syncHgHeight($form); 349 return true; 350 } 351 352 function initVariable($form) { 353 var $product = $form.closest('.product'); 354 if ($product.hasClass('product-type-hg_bundle')) { return false; } 355 356 var $picker = $form.find('.woovr-variations.woovr-variations-select2').first(); 357 if (!$picker.length) { return false; } 358 359 $form 360 .addClass('ho-mobile-buybar ho-mobile-buybar--variable') 361 .attr('data-ho-buybar-type', 'variable'); 362 363 ensurePriceRow($form); 364 ensureButtonArt($form); 365 renderPrice($form); 366 367 watchNode($form.find('.single_variation_wrap').first().get(0), $form); 368 syncHgHeight($form); 369 return true; 370 } 371 372 function initForm(form) { 373 var $form = $(form); 374 if ($form.attr('data-ho-buybar-initialized') === '1') { 375 ensureButtonArt($form); 376 scheduleRender($form); 377 return; 378 } 379 380 if (initBundle($form) || initVariable($form) || initSimple($form)) { 381 $form.attr('data-ho-buybar-initialized', '1'); 382 } 383 } 384 385 function initAll() { 386 if (!isMobile()) { return; } 387 initOutOfStockSimpleBars(); 388 $('.woocommerce.single-product .summary form.cart').each(function () { 389 initForm(this); 390 }); 391 } 392 393 function refreshFromEvent(target) { 394 var $form = $(target).closest('form.cart'); 395 if (!$form.length) { $form = $('.woocommerce.single-product .summary form.cart').first(); } 396 if (!$form.length || !$form.hasClass('ho-mobile-buybar')) { return; } 397 398 window.setTimeout(function () { scheduleRender($form); }, 0); 399 window.setTimeout(function () { scheduleRender($form); }, 30); 400 window.setTimeout(function () { scheduleRender($form); }, 120); 401 } 402 403 $(function () { 404 window.setTimeout(initAll, 0); 405 window.setTimeout(initAll, 250); 406 window.setTimeout(initAll, 900); 407 }); 408 409 function mobileSelect2SearchField(select) { 410 if (!select) { return $(); } 411 412 var instance = $(select).data('select2'); 413 if (!instance || !instance.dropdown || !instance.dropdown.$search) { 414 return $(); 415 } 416 417 return instance.dropdown.$search; 418 } 419 420 function prepareVariationSearch(select) { 421 var $search = mobileSelect2SearchField(select); 422 if (!$search.length) { return $search; } 423 424 $search 425 .attr('placeholder', 'Zoek jouw variatie') 426 .attr('aria-label', 'Zoek jouw variatie'); 427 428 if (isMobile()) { 429 // Select2 focust het zoekveld automatisch bij openen. Houd dat eerste 430 // programmatische focusmoment read-only zodat het mobiele toetsenbord 431 // niet vanzelf verschijnt. Een bewuste tik op het zoekveld ontgrendelt 432 // het hieronder direct weer. 433 $search 434 .attr('readonly', 'readonly') 435 .attr('inputmode', 'none') 436 .attr('aria-readonly', 'true') 437 .attr('data-ho-mobile-search-locked', '1'); 438 } else { 439 $search 440 .removeAttr('readonly') 441 .attr('inputmode', 'search') 442 .removeAttr('aria-readonly') 443 .removeAttr('data-ho-mobile-search-locked'); 444 } 445 446 return $search; 447 } 448 449 // De zoekhint geldt voor WPC Select2 op desktop en mobiel. Op mobiel wordt 450 // alleen de automatische focus bij het openen geblokkeerd; de zoekfunctie 451 // zelf blijft beschikbaar zodra de bezoeker bewust in de zoekbalk tikt. 452 $(document).on('select2:opening.hoMobileBuybar select2:open.ho
452MobileBuybar', '.woovr-variation-select', function () { 453 prepareVariationSearch(this); 454 }); 455 456 $(document).on( 457 'pointerdown.hoMobileBuybar mousedown.hoMobileBuybar touchstart.hoMobileBuybar', 458 '.wpc-select2-dropdown .select2-search__field[data-ho-mobile-search-locked="1"]', 459 function () { 460 if (!isMobile()) { return; } 461 462 var input = this; 463 var $input = $(input); 464 465 $input 466 .removeAttr('readonly') 467 .attr('inputmode', 'search') 468 .removeAttr('aria-readonly') 469 .removeAttr('data-ho-mobile-search-locked'); 470 471 // Het veld kan door Select2 al focus hebben terwijl het nog read-only was. 472 // Door die focus tijdens de echte aanraking los te laten kan de normale 473 // tap het veld opnieuw focussen en mag het toetsenbord juist wel openen. 474 if (document.activeElement === input) { 475 input.blur(); 476 } 477 } 478 ); 479 480 $(document).on('select2:close.hoMobileBuybar', '.woovr-variation-select', function () { 481 mobileSelect2SearchField(this) 482 .removeAttr('readonly') 483 .removeAttr('inputmode') 484 .removeAttr('aria-readonly') 485 .removeAttr('data-ho-mobile-search-locked'); 486 }); 487 488 $(document).on('change.hoMobileBuybar', '.ho-mobile-buybar .woovr-variation-select', function () { 489 refreshFromEvent(this); 490 }); 491 492 $(document).on('woovr_selected.hoMobileBuybar', function (event, selected, variations, variationsForm) { 493 var target = variationsForm && variationsForm.length ? variationsForm : variations; 494 refreshFromEvent(target); 495 }); 496 497 $(document.body).on('found_variation.hoMobileBuybar show_variation.hoMobileBuybar reset_data.hoMobileBuybar hide_variation.hoMobileBuybar', function (event) { 498 refreshFromEvent(event.target); 499 }); 500 501 $(document).on('change.hoMobileBuybar', '.hg-bundle-choice-input', function () { 502 var $form = $(this).closest('.product').find('form.cart.ho-mobile-buybar--bundle').first(); 503 if (!$form.length) { return; } 504 window.setTimeout(function () { scheduleRender($form); }, 0); 505 window.setTimeout(function () { scheduleRender($form); }, 40); 506 window.setTimeout(function () { scheduleRender($form); }, 140); 507 }); 508 509 $(document).on('change.hoMobileBuybar', '.hg-mode-input, .hg-frequency-input', function () { 510 refreshFromEvent(this); 511 }); 512 513 $(window).on('resize.hoMobileBuybar orientationchange.hoMobileBuybar', function () { 514 var $form = $('.woocommerce.single-product .summary form.cart.ho-mobile-buybar').first(); 515 if (isMobile()) { 516 initAll(); 517 syncHgHeight($form); 518 } else { 519 $('.woocommerce.single-product form.cart[data-ho-generated-out-of-stock="1"]').remove(); 520 document.documentElement.style.removeProperty('--hg-mobile-buybar-height'); 521 } 522 }); 523 524})(jQuery);
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.