PageSourceSearch

https://www.honingonline.nl/wp-content/themes/honingonline/toevoeg…/product-mobile-buybar.js?ver=1790003633

js honingonline.nl collected 2026-09-25 08:55:32 UTC 17,843 bytes, 524 lines download raw bytes

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.