PageSourceSearch

https://k.ua/wp-content/themes/konte-child/assets/js/modules/ui-utils.js?v=1.26.3

js k.ua collected 2026-10-09 23:19:33 UTC 21,050 bytes, 603 lines download raw bytes

1/**
2 * UI Utilities module
3 * Helper functions for UI interactions and utilities
4 */
5
6export function addAttributesToDisabledButtons() {
7    // No size chosen yet: that click must reach the inline validation, not open the cart panel.
8    jQuery(".single_add_to_cart_button.disabled:not(.wc-variation-selection-needed)").each(function () {
9        const $button = jQuery(this);
10        if (!$button.attr("data-toggle")) {
11            $button
12                .attr("data-toggle", "off-canvas")
13                .attr("data-target", "cart-panel");
14        }
15    });
16}
17
18export function loadRecaptcha(callback) {
19    if (!jQuery("#recaptcha-script").length) {
20        var script = document.createElement("script");
21        script.src =
22            "https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit";
23        script.id = "recaptcha-script";
24        document.head.appendChild(script);
25    } else if (callback) {
26        callback();
27    }
28}
29
30export function initializeRecaptcha() {
31    const $recaptchaElement = jQuery(".recaptcha-container");
32
33    if ($recaptchaElement.length) {
34        $recaptchaElement.each(function () {
35            const $item = jQuery(this);
36
37            if (!$item.data("widget-id")) {
38                const widgetId = grecaptcha.render(this, {
39                    sitekey: window.kapsula.captcha.sitekey,
40                    theme: "light",
41                });
42
43                $item.data("widget-id", widgetId);
44
45                $item
46                    .closest(".woocommerce-form-register.register")
47                    .on("submit", function (event) {
48                        const response = grecaptcha.getResponse(widgetId);
49                        if (response.length === 0) {
50                            event.preventDefault();
51                            alert("Будь ласка, підтвердіть, що ви не робот.");
52                        }
53                    });
54            }
55        });
56    }
57}
58
59export function checkCartPanelAndInitSlider() {
60    const cartPanel = document.getElementById("cart-panel");
61
62    if (cartPanel && cartPanel.classList.contains("open")) {
63        document.body.classList.add("offcanvas-opened");
64        if (window.initRecommendedProductsSlider) {
65            window.initRecommendedProductsSlider();
66        }
67    }
68}
69
70export function setupAccountNavScroll() {
71    const navAccountEdit = document.querySelector(
72        ".woocommerce-MyAccount-navigation"
73    );
74    const SCROLL_SPEED = 30;
75
76    if (navAccountEdit) {
77        navAccountEdit.addEventListener("wheel", (event) => {
78            event.preventDefault();
79            navAccountEdit.scrollLeft += event.deltaY * SCROLL_SPEED;
80        });
81
82        let isTouching = false;
83        let startX = 0;
84        let scrollLeft = 0;
85
86        navAccountEdit.addEventListener("touchstart", (event) => {
87            isTouching = true;
88            startX = event.touches[0].pageX;
89            scrollLeft = navAccountEdit.scrollLeft;
90        });
91
92        navAccountEdit.addEventListener("touchmove", (event) => {
93            if (!isTouching) return;
94            const moveX = event.touches[0].pageX - startX;
95            navAccountEdit.scrollLeft = scrollLeft - moveX;
96        });
97
98        navAccountEdit.addEventListener("touchend", () => {
99            isTouching = false;
100        });
101    }
102}
103
104export function setupSearchFieldBlur() {
105    const searchField = document.querySelector(".search-field");
106
107    if (searchField !== null) {
108        searchField.addEventListener("focusout", () => {
109            searchField.blur();
110        });
111
112        document.addEventListener("touchend", () => {
113            if (document.activeElement === searchField) {
114                searchField.blur();
115            }
116        });
117    }
118}
119
120export function setupEmailVerification() {
121    let isRegisterButtonActive = false;
122    jQuery("body").on(
123        'submit',
124        '.woocommerce-form.woocommerce-form-register',
125        function (e) {
126            e.preventDefault();
127
128            if (isRegisterButtonActive) {
129                return;
130            }
131
132            let $form = jQuery(this);
133
134            const printError = (message) => {
135                const errorMessage = $form.find('.woocommerce-error');
136                if (errorMessage) {
137                    errorMessage.remove();
138                }
139                $form.prepend(message);
140            }
141
142            isRegisterButtonActive = true;
143
144            jQuery.post(
145                window.kapsula.ajaxURL,
146                {
147                    'action': 'kapsula_create_customer',
148                    'email': $form.find('input[name="email"]').val(),
149                    'nonce': window.kapsula.nonce,
150                    '_wp_http_referer': $form.find('input[name="_wp_http_referer"]').val(),
151                }
152            ).done(function (response) {
153                if (response.success && response.data.redirect) {
154                    window.location.href = response.data.redirect;
155                }
156
157                printError(response.data.message);
158
159                isRegisterButtonActive = false;
160            }).fail(function () {
161                printError(
162                    '<div class="woocommerce-error" role="alert"><ul class="error-message">' +
163                    '<li>Запит не вдався. Спробуйте пізнішe.</li>' +
164                    '</ul></div>'
165                )
166
167                isRegisterButtonActive = false;
168            });
169        }
170    );
171}
172
173export function setupContactFormLoading() {
174    document
175        .querySelectorAll(".page-template-contacts .wpcf7-form")
176        .forEach((el) => {
177            const submitButton = el.querySelector('input[type="submit"]');
178
179            if (submitButton === null) {
180                return;
181            }
182
183            el.addEventListener("submit", () => {
184                submitButton.classList.add("loading");
185            });
186
187            el.addEventListener("wpcf7submit", () => {
188                submitButton.classList.remove("loading");
189            });
190        });
191}
192
193export function setupSearchPopular() {
194    jQuery(document).ready(function ($) {
195        const $searchPopular = $('[data-el="search-popular"]');
196        const $searchResult = $('[data-el="search-results"]');
197        const $searchResultEmpty = $('[data-el="search-results-empty"]');
198        if ($searchResult.length) {
199            let requestAction = null;
200            const inputWait = 500;
201            $('body')
202                .on('keyup', '.kapsula-search', function () {
203                    if (requestAction !== null) {
204                        clearTimeout(requestAction);
205                    }
206
207                    requestAction = setTimeout(() => {
208                        const $input = $(this);
209                        const $form = $input.closest('form');
210                        if ($input.val()) {
211                            $searchForm.addClass('search-loading');
212                            $.ajax({
213                                url: kapsula.ajaxURL,
214                                type: 'POST',
215                                data: {
216                                    'action': 'kapsula_search_categories',
217                                    'nonce': $form.find('[name="nonce"]').val(),
218                                    's': $input.val(),
219                                },
220                                success: (data) => {
221                                    if (data.data.html) {
222                                        $searchResultEmpty.addClass('hidden');
223                                        $searchPopular.addClass('hidden');
224                                        $searchResult.removeClass('hidden');
225                                    } else {
226                                        $searchPopular.removeClass('hidden');
227                                    }
228                                    $searchResult.html(data.data.html);
229                                    $searchBestsellersBlock.addClass('hidden');
230                                    $searchForm.removeClass('search-loading');
231                                },
232                                error: (data) => {
233                                    console.log('error', data.data.error);
234                                },
235                            });
236                        } else {
237                            $searchResultEmpty.addClass('hidden');
238                            $searchResult.addClass('hidden');
239                            $searchPopular.removeClass('hidden');
240                            $searchBestsellersBlock.removeClass('hidden');
241                        }
242                    }, inputWait);
243                })
244                .on('focusout', '.kapsula-search', function () {
245                    if (requestAction === null) {
246                        return;
247                    }
248
249                    clearTimeout(requestAction);
250                });
251        }
252
253        function closeSearch() {
254            $('[data-el="search-block"]').removeClass('opened');
255            $('body').removeClass('search-opened');
256            $('[data-el="search-results"]').html('');
257            $searchPopular.removeClass('hidden');
258            $searchBestsellersBlock.removeClass('hidden');
259            $searchForm.find('#kapsula-search').val('');
260        }
261
262        $('[data-event="close-search"]').on('click', function (e) {
263            e.stopPropagation();
264            closeSearch();
265        });
266
267        $(document).on('click', 'body.search-opened', function (e) {
268            if ($(e.target).closest('[data-el="search-block"]').length === 0
269                && !$(e.target).is('[data-event="open-search"]')
270                && !$(e.target).closest('[data-event="open-search"]').length) {
271                closeSearch();
272            }
273        });
274
275        const $searchForm = $('#kapsula-search-form');
276        if ($searchForm.length) {
277            $searchForm.on('submit', function (e) {
278                e.preventDefault();
279
280                const searched = $searchForm.find('#kapsula-search').val();
281                if (searched.length > 0) {
282                    window.location.href = 'https://' + window.location.hostname + '?post_type=product&s=' + searched;
283                }
284            });
285        }
286        const $searchBestsellers = $('[data-el="search-bestsellers"]');
287        const $searchBestsellersBlock = $('[data-el="search-bestsellers-wrapper"]');
288
289        $('[data-event="open-search"]').on('click', function () {
290            $('[data-el="search-block"]').addClass('opened');
291            $('body').addClass('search-opened');
292
293            if ($searchBestsellers.length && $searchBestsellers.children().length === 0) {
294                $searchBestsellers.addClass('search-loading');
295                $.ajax({
296                    url: kapsula.ajaxURL,
297                    type: 'POST',
298                    data: {
299                        'action': 'load_bestseller_products',
300                        'nonce': bestsellers.nonce ?? '',
301                    },
302                    success: (data) => {
303                        $searchBestsellers.html(data.data.html);
304                        $searchBestsellers.removeClass('search-loading');
305                    },
306                    error: (data) => {
307                        console.log('error', data.data.error);
308                    },
309                });
310            }
311        });
312    });
313}
314
315export function setupMenu() {
316    jQuery(document).ready(function ($) {
317        $('[data-event="toggle-gender"]').on('click', function () {
318            const gender = $(this).data('type');
319            $('[data-el="menu-block"], [data-event="toggle-gender"]').removeClass('active');
320            $(`[data-el="menu-block"][data-type="${gender}"]`).addClass('active');
321            $(this).addClass('active');
322        });
323
324        let menuTimer;
325        $('.menu-block .level-0.menu-item-has-children').hover(function () {
326            const $this = $(this);
327
328            clearTimeout(menuTimer);
329            menuTimer = setTimeout(() => {
330                $('.level-0.menu-item-has-children').not($this).removeClass('hover');
331                $this.addClass('hover');
332
333                $('body').addClass('menu-opened');
334            }, 200);
335        }, function () {
336            clearTimeout(menuTimer);
337            const $this = $(this);
338            menuTimer = setTimeout(() => {
339                $this.removeClass('hover');
340                if ($('.level-0.menu-item-has-children.hover').length === 0) {
341                    $('body').removeClass('menu-opened');
342                }
343            }, 300);
344        });
345
346        $('[data-event="open-mobile-menu"]').on('click', function () {
347            $('[data-el="mobile-menu"]').addClass('opened');
348            $('body').addClass('mobile-menu-opened');
349        });
350        $('[data-event="close-mobile-menu"]').on('click', function () {
351            $('[data-el="mobile-menu"]').removeClass('opened');
352            $('body').removeClass('mobile-menu-opened menu-opened');
353        });
354        $('.menu-item.level-0').on('click', function () {
355            $(this).addClass('opened');
356            $('[data-el="mobile-menu"]').addClass('submenu-opened');
357        });
358        $('[data-event="close-submenu"]').on('click', function (e) {
359            e.stopPropagation();
360            $(this).closest('.level-0').removeClass('opened');
361            $('[data-el="mobile-menu"]').removeClass('submenu-opened');
362        })
363
364        let touchstartX = 0;
365        let touchendX = 0;
366        const SWIPE_THRESHOLD = 50;
367
368        document.addEventListener('touchstart', (e) => {
369            const target = e.target.closest('.menu-item-has-children.opened');
370            if (!target) return;
371
372            touchstartX = e.changedTouches[0].screenX;
373        }, { passive: false });
374
375        document.addEventListener('touchend', (e) => {
376            const target = e.target.closest('.menu-item-has-children.opened');
377            if (!target) return;
378
379            touchendX = e.changedTouches[0].screenX;
380            handleSwipe(target);
381        }, { passive: false });
382
383        function handleSwipe(element) {
384            if (touchstartX - touchendX > SWIPE_THRESHOLD) {
385                element.classList.remove('opened');
386                $('[data-el="mobile-menu"]').removeClass('submenu-opened');
387            }
388        }
389    });
390}
391
392export function addDragScrollElement() {
393    const dragScrollContainers = document.querySelectorAll('[data-el="drag-scroll-container"]');
394
395    if (!dragScrollContainers.length || window.matchMedia('(pointer: coarse)').matches) return;
396
397    dragScrollContainers.forEach((dragScrollContainer) => {
398        let isDown = false;
399        let startX, startY;
400        let scrollLeft, scrollTop;
401        let moved = false;
402        let touchAxis = null;
403        const dragThreshold = 5;
404
405        let velocityX = 0;
406        let velocityY = 0;
407        let lastX = 0;
408        let lastY = 0;
409        let lastTime = 0;
410        let rafId = null;
411
412        const FRICTION = 0.92;
413        const MIN_VELOCITY = 0.5;
414
415        const velocityHistory = [];
416        const HISTORY_MS = 100;
417
418        function recordVelocity(x, y) {
419            const now = performance.now();
420            velocityHistory.push({ x, y, t: now });
421
422            while (velocityHistory.length > 1 && now - velocityHistory[0].t > HISTORY_MS) {
423                velocityHistory.shift();
424            }
425        }
426
427        function computeVelocity() {
428            if (velocityHistory.length < 2) return { vx: 0, vy: 0 };
429
430            const first = velocityHistory[0];
431            const last  = velocityHistory[velocityHistory.length - 1];
432            const dt    = last.t - first.t;
433
434            if (dt === 0) return { vx: 0, vy: 0 };
435
436            return {
437                vx: (last.x - first.x) / dt * 16, // scale to ~60fps frame
438                vy: (last.y - first.y) / dt * 16,
439            };
440        }
441
442        function startInertia() {
443            if (rafId) cancelAnimationFrame(rafId);
444
445            function loop() {
446                velocityX *= FRICTION;
447                velocityY *= FRICTION;
448
449                const absVX = Math.abs(velocityX);
450                const absVY = Math.abs(velocityY);
451
452                if (absVX < MIN_VELOCITY && absVY < MIN_VELOCITY) {
453                    dragScrollContainer.style.scrollBehavior = 'smooth';
454                    return;
455                }
456
457                dragScrollContainer.scrollLeft -= velocityX;
458                dragScrollContainer.scrollTop  -= velocityY;
459
460                rafId = requestAnimationFrame(loop);
461            }
462
463            rafId = requestAnimationFrame(loop);
464        }
465
466        function stopInertia() {
467            if (rafId) {
468                cancelAnimationFrame(rafId);
469                rafId = null;
470            }
471        }
472
473        function onStart(x, y) {
474            stopInertia();
475            isDown = true;
476            moved = false;
477            touchAxis = null;
478            velocityHistory.length = 0;
479            velocityX = 0;
480            velocityY = 0;
481            startX = x - dragScrollContainer.offsetLeft;
482            startY = y - dragScrollContainer.offsetTop;
483            scrollLeft = dragScrollContainer.scrollLeft;
484            scrollTop  = dragScrollContainer.scrollTop;
485            lastX = x;
486            lastY = y;
487            lastTime = performance.now();
488            dragScrollContainer.style.scrollBehavior = 'auto';
489        }
490
491        function onMove(x, y, e, inputType = 'mouse') {
492            if (!isDown) return;
493
494            const walkX = (x - dragScrollContainer.offsetLeft) - startX;
495            const walkY = (y - dragScrollContainer.offsetTop)  - startY;
496
497            if (Math.abs(walkX) > dragThreshold || Math.abs(walkY) > dragThreshold) {
498                moved = true;
499
500                if (inputType === 'touch' && !touchAxis) {
501                    touchAxis = Math.abs(walkX) >= Math.abs(walkY) ? 'x' : 'y';
502                }
503            }
504
505            if (moved) {
506                if (inputType === 'touch' && touchAxis === 'y') {
507                    isDown = false;
508                    return;
509                }
510
511                if (inputType === 'touch') {
512                    e.preventDefault();
513                }
514
515                recordVelocity(x, y);
516
517                dragScrollContainer.scrollLeft = scrollLeft - walkX;
518
519                if (inputType !== 'touch') {
520                    dragScrollContainer.scrollTop = scrollTop - walkY;
521                }
522            }
523        }
524
525        function onEnd(inputType = 'mouse') {
526            if (!isDown) return;
527            isDown = false;
528
529            if (moved) {
530                const { vx, vy } = computeVelocity();
531                velocityX = vx;
532
533                velocityY = inputType === 'touch' ? 0 : vy;
534
535                startInertia();
536            } else {
537                dragScrollContainer.style.scrollBehavior = 'smooth';
538            }
539        }
540
541        dragScrollContainer.addEventListener('mousedown', (e) => {
542            onStart(e.pageX, e.pageY);
543            const isLink = e.target.closest('a');
544            if (isLink) e.preventDefault();
545        });
546
547        dragScrollContainer.addEventListener('mousemove', (e) => {
548            onMove(e.pageX, e.pageY, e, 'mouse');
549        });
550
551        dragScrollContainer.addEventListener('mouseup',    () => onEnd('mouse'));
552        dragScrollContainer.addEventListener('mouseleave', () => onEnd('mouse'));
553
554        dragScrollContainer.addEventListener('click', (e) => {
555            if (moved) {
556                e.preventDefault();
557                e.stopPropagation();
558            }
559        }, true);
560
561        dragScrollContainer.addEventListener('touchstart', (e) => {
562            const touch = e.touches[0];
563            onStart(touch.pageX, touch.pageY);
564        }, { passive: false });
565
566        dragScrollContainer.addEventListener('touchmove', (e) => {
567            const touch = e.touches[0];
568            onMove(touch.pageX, touch.pageY, e, 'touch');
569        }, { passive: false });
570
571        dragScrollContainer.addEventListener('touchend', (e) => {
572            if (moved && touchAxis === 'x') {
573                e.preventDefault();
574            }
575            onEnd('touch');
576        }, { passive: false });
577
578        dragScrollContainer.addEventListener('touchcancel', () => onEnd('touch'));
579    });
580}
581
582export function stickyHeader() {
583    const body = document.querySelector('body');
584
585    let lastScrollY = window.scrollY;
586    function debounce(fn, delay) {
587        let timer;  return (...args) => {
588            clearTimeout(timer);
589            timer = setTimeout(() => fn(...args), delay);
590        };
591    }
592    const handleScroll = debounce(() => {
593        const currentScrollY = window.scrollY;
594        if (currentScrollY < lastScrollY) {
595            body.classList.add('header-sticky');
596        } else {
597            body.classList.remove('header-sticky');
598        }
599        lastScrollY = currentScrollY;
600    }, 50);
601
602    window.addEventListener('scroll', handleScroll);
603}

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.