PageSourceSearch

https://alpinabook.ru/assets/global.js

js alpinabook.ru collected 2026-09-24 08:56:26 UTC 59,215 bytes, 1,608 lines download raw bytes

1var globObj = {
2    breakpoints: {
3        xxs: 0,
4        xs: 330,
5        sm: 575,
6        md: 767,
7        lg: 1039,
8        xl: 1259
9    },
10    container: {
11        sm: 540,
12        md: 720,
13        lg: 1000,
14        xl: 1220
15    }
16};
17
18(function () {
19    //Тачскрин или десктоп
20    var supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints;
21    var root = document.getElementsByTagName('html')[0];
22    root.classList.add(supportsTouch ? 'touch' : 'no-touch');
23})();
24
25// !!! на бою переменные globDev и globDevAjax всегда должны быть false !!!
26let globDev = false,
27    globDevAjax = false;
28
29let isGlobalReceivedResponseChangeBasked = true;
30let isGlobalNoBasketTimeout = true;
31let isGlobalBasketRefresh = false;
32
33// константы
34// ToDo: получать из env-переменных
35const AJAX_URL = '/ajax/'
36const VK_PRICE_LIST_ID = 160575;
37const OK_ACCOUNT_ID = 3244329;
38const OK_LIST_ID = 1;
39const DADATA_TOKEN = 'ca01079585fc9867a25e215ef68a36671bb6a0a5';
40// todo нужно переделать это на вывод из twig-переменной globalStatic.labels или придумать вариант получше
41const LABELS = {
42    bestseller: {class: '_green', text: 'Бестселлер'},
43    latest: {class: '_blue', text: 'Новинка'},
44    week: {class: '_violet', text: 'Книга недели'},
45    preorder: {class: '_orange', text: 'Предзаказ'}
46}
47// if(typeof searchType == 'undefined') {
48//     var searchType = 'book';
49// }
50// глобальные функции
51let globalFunctions = {
52    
53    initBasket: function () {
54        globalApp.basket = {};
55    },
56
57    formatPrice: function (num, separator = ' ') {
58        return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1'+separator)
59    },
60    
61    isFunction: function (functionToCheck) {
62        return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
63    },
64    
65    /**
66     * Проверяет, является ли переданный объект узлом DOM.
67     *
68     * @param {Object} obj - Объект для проверки.
69     * @return {boolean} true, если объект является узлом DOM.
70     */
71    isDomNode: function(obj) {
72        return obj instanceof Element;
73    },
74    
75    delay: function (fn, ms) {
76        let timer = 0;
77        return function(...args) {
78            clearTimeout(timer);
79            timer = setTimeout(fn.bind(this, ...args), ms || 0);
80        }
81    },
82    
83    /**
84     * Native scrollTo with callback
85     * @param offset - offset to scroll to
86     * @param callback - callback function
87     *
88     * https://stackoverflow.com/questions/52292603/is-there-a-callback-for-window-scrollto
89     */
90    
91    scrollTo: function (offset, callback) {
92        const fixedOffset = offset.toFixed (),
93            onScroll = function () {
94                if (window.pageYOffset.toFixed () === fixedOffset) {
95                    window.removeEventListener ('scroll', onScroll);
96                    callback ()
97                }
98            };
99        
100        window.addEventListener ('scroll', onScroll);
101        onScroll ();
102        window.scrollTo ({
103            top: offset,
104            behavior: 'smooth'
105        });
106    },
107    
108    escapeStr: function (str) {
109        return String(str).replace(/[&<>"']/g, function (s) {
110            return {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'}[s];
111        });
112    },
113
114    declension: function (number, forms, isNumber = false) {
115        const cases = [2, 0, 1, 1, 1, 2];
116        const formIndex = ((number % 100 > 4) && (number % 100 < 20)) ? 2 : cases[Math.min (number % 10, 5)];
117        
118        let returnVal = forms[formIndex];
119        if (isNumber) {
120            returnVal = number + " " + returnVal;
121        }
122        
123        return returnVal;
124    },
125    
126    /*
127    Анимация для height: 0 -> height: auto
128    Адаптировано решение отсюда
129    https://css-tricks.com/using-css-transitions-auto-dimensions/#comment-1607371
130    */
131    heightOpenClose: function () {
132        if ($ (this).hasClass ('_open')) { // close
133            $ (this).each (function () {
134                $ (this).height ($ (this).get (0).scrollHeight).height ('').removeClass ('_open');
135            });
136        }
137        else { // open
138            $ (this).each (function () {
139                $ (this).height ($ (this).get (0).scrollHeight).addClass ('_open'); // get height and open
140                $ (this).one ('webkitTransitionEnd oTransitionEnd MSTransitionEnd transitionEnd', function () { // after transition complete
141                    $ (this).height (''); // revert to CSS-set height
142                });
143            });
144        }
145    },
146    
147    makeSrcset: function (objImg) {
148        let srcset = '';
149        if (objImg.x3) {
150            srcset += objImg.x3 + ' 3x, ';
151        }
152        if (objImg.x2) {
153            srcset += objImg.x2 + ' 2x, ';
154        }
155        srcset += objImg.x1;
156        return srcset;
157    },
158
159    bytesToSize: function (bytes) {
160        const kilobyte = 1024
161        const megabyte = kilobyte * 1024
162        const gigabyte = megabyte * 1024
163        const terabyte = gigabyte * 1024
164        let formatted = ''
165        
166        if (bytes < kilobyte) {
167            formatted = bytes + ' байт'
168        } else if (bytes < megabyte) {
169            formatted = (bytes / kilobyte).toFixed(2) + ' Кб'
170        } else if (bytes < gigabyte) {
171            formatted = (bytes / megabyte).toFixed(2) + ' Мб'
172        } else if (bytes < terabyte) {
173            formatted = (bytes / gigabyte).toFixed(2) + ' Гб'
174        } else {
175            formatted = (bytes / terabyte).toFixed(2) + ' Тб'
176        }
177        
178        return formatted
179    },
180    
181    // https://css-tricks.com/scroll-triggered-animation-vanilla-javascript/
182    scrollTrigger: (els, options = {}) => {
183        els = Array.from(els)
184        els.forEach(el => {
185            if(!('IntersectionObserver' in window)){
186                if(options.cb){
187                    options.cb(el)
188                } else {
189                    entry.target.classList.add('active')
190                }
191                return
192            }
193            //this takes a callback function which receives two arguments: the elemts list and the observer instance
194            let observer = new IntersectionObserver((entries, observer) => {
195                entries.forEach(entry => {
196                    if(entry.isIntersecting){
197                        if(options.cb){
198                            options.cb(el)
199                        } else {
200                            entry.target.classList.add('active')
201                        }
202                        observer.unobserve(entry.target)
203                    }
204                })
205            }, options)
206            observer.observe(el)
207        })
208    },
209    
210    // форматирование урла для ресайзера на картинку произвольного размера
211    getResizedImgUrl: (url, width, height) => {
212        const imgSplit = url.split('/')
213        imgSplit[2] = width + 'x' + height
214        return imgSplit.join('/')
215    },
216    
217    // проверка user-agent на бота
218    isUserBot: () => {
219        const botPattern = "(googlebot\/|bot|Googlebot-Mobile|Googlebot-Image|Google favicon|Mediapartners-Google|bingbot|slurp|java|wget|curl|Commons-HttpClient|Python-urllib|libwww|httpunit|nutch|phpcrawl|msnbot|jyxobot|FAST-WebCrawler|FAST Enterprise Crawler|biglotron|teoma|convera|seekbot|gigablast|exabot|ngbot|ia_archiver|GingerCrawler|webmon |httrack|webcrawler|grub.org|UsineNouvelleCrawler|antibot|netresearchserver|speedy|fluffy|bibnum.bnf|findlink|msrbot|panscient|yacybot|AISearchBot|IOI|ips-agent|tagoobot|MJ12bot|dotbot|woriobot|yanga|buzzbot|mlbot|yandexbot|purebot|Linguee Bot|Voyager|CyberPatrol|voilabot|baiduspider|citeseerxbot|spbot|twengabot|postrank|turnitinbot|scribdbot|page2rss|sitebot|linkdex|Adidxbot|blekkobot|ezooms|dotbot|Mail.RU_Bot|discobot|heritrix|findthatfile|europarchive.org|NerdByNature.Bot|sistrix crawler|ahrefsbot|Aboundex|domaincrawler|wbsearchbot|summify|ccbot|edisterbot|seznambot|ec2linkfinder|gslfbot|aihitbot|intelium_bot|facebookexternalhit|yeti|RetrevoPageAnalyzer|lb-spider|sogou|lssbot|careerbot|wotbox|wocbot|ichiro|DuckDuckBot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnam gnam spider|web-archive-net.com.bot|backlinkcrawler|coccoc|integromedb|content crawler spider|toplistbot|seokicks-robot|it2media-domain-crawler|ip-web-crawler.com|siteexplorer.info|elisabot|proximic|changedetection|blexbot|arabot|WeSEE:Search|niki-bot|CrystalSemanticsBot|rogerbot|360Spider|psbot|InterfaxScanBot|Lipperhey SEO Service|CC Metadata Scaper|g00g1e.net|GrapeshotCrawler|urlappendbot|brainobot|fr-crawler|binlar|SimpleCrawler|Livelapbot|Twitterbot|cXensebot|smtbot|bnf.fr_bot|A6-Indexer|ADmantX|Facebot|Twitterbot|OrangeBot|memorybot|AdvBot|MegaIndex|SemanticScholarBot|ltx71|nerdybot|xovibot|BUbiNG|Qwantify|archive.org_bot|Applebot|TweetmemeBot|crawler4j|findxbot|SemrushBot|yoozBot|lipperhey|y!j-asr|Domain Re-Animator Bot|AddThis)"
220        const re = new RegExp(botPattern, 'i')
221        const userAgent = navigator.userAgent
222        return re.test(userAgent)
223    },
224    
225    // троттлинг функции
226    throttle: (func, delay) => {
227        let prev = 0;
228        return (...args) => {
229            let now = new Date().getTime();
230            if(now - prev> delay){
231                prev = now;
232                return func(...args);
233            }
234        }
235    },
236    
237    // изменить гет-параметр урла
238    updateGetParam: (url, param, value) => {
239        const urlObject = new URL(url)
240        urlObject.searchParams.set(param, value)
241        return urlObject.href
242    },
243    
244    // удалить гет-параметр урла
245    deleteGetParam: (url, param) => {
246        const urlObject = new URL(url)
247        urlObject.searchParams.delete(param)
248        return urlObject.href
249    },
250    
251    // сделать первую букву строки прописной
252    capitalizeFirstLetter: (string) => {
253        return string.charAt(0).toUpperCase() + string.slice(1)
254    },
255    
256    /**
257     Загружает скрипт по заданному URL-адресу и возвращает Promise с результатом загрузки.
258     @param {string} FILE_URL - URL-адрес файла скрипта.
259     @param {boolean} async - Определяет, должен ли загружаться скрипт асинхронно. По умолчанию true.
260     @param {string} type - Тип скрипта. По умолчанию "text/javascript".
261     @returns {Promise} - Promise с объектом статуса загрузки (status) и сообщением об ошибке (message), если возникла ошибка.
262     */
263    // https://www.educative.io/answers/how-to-dynamically-load-a-js-file-in-javascript
264    loadScript: (FILE_URL, async = true, type = "text/javascript") => {
265        return new Promise((resolve, reject) => {
266            try {
267                const scriptEle = document.createElement("script");
268                scriptEle.type = type;
269                scriptEle.async = async;
270                scriptEle.src =FILE_URL;
271            
272                scriptEle.addEventListener("load", (event) => {
273                    resolve({ status: true });
274                });
275            
276                scriptEle.addEventListener("error", (event) => {
277                    reject({
278                        status: false,
279                        message: `Failed to load the script ${FILE_URL}`
280                    });
281                });
282            
283                document.body.appendChild(scriptEle);
284            } catch (error) {
285                reject(error);
286            }
287        });
288    },
289    
290    /**
291     * Проверяет, поддерживает ли устройство ввод touch
292     * @return {boolean} true, если устройство поддерживает touch, false в противном случае
293     */
294    isTouchDevice: () => {
295        return ('ontouchstart' in window) ||
296            (navigator.maxTouchPoints > 0) ||
297            (navigator.msMaxTouchPoints > 0);
298    },
299    
300    /**
301     * Разделяет массив на равные части
302     * @param {Array} arr - исходный массив
303     * @param {number} parts - количество частей, на которые нужно разделить массив
304     * @returns {Array} - массив из parts подмассивов
305     */
306    splitArray: (arr, parts) => {
307        const result = [];
308        const chunkSize = Math.ceil(arr.length / parts);
309        for (let i = 0; i < parts; i++) {
310            const start = i * chunkSize;
311            const end = start + chunkSize;
312            result.push(arr.slice(start, end));
313        }
314        return result;
315    },
316    
317    /**
318     * Форматирует число секунд в отображение таймера
319     * Например, 95 сек => 01:35 или 3615 сек => 01:00:15
320     * @param {number} seconds - секунды, натуральное число
321     * @returns {string} - строка с форматированным отображением
322     */
323    formatSecondsToTime: (seconds) => {
324        const date = new Date(0);
325        date.setSeconds(seconds);
326        if (seconds > 3599) {
327            return date.toISOString().substr(11, 8);
328        } else {
329            return date.toISOString().substr(14, 5);
330        }
331    },
332    
333    /**
334     * Возвращает название дня недели на русском языке на основе его числового представления.
335     *
336     * @param {number} num - Числовое представление дня недели (от 0 до 6, где 0 - воскресенье, 1 - понедельник и т. д.).
337     * @returns {Object} - Объект, содержащий название дня недели в именительном и родительном падежах.
338     */
339    // ToDo добавить падежи, если нужно
340    getDayInRus: (num) => {
341        return [
342            {
343                caseNominative: 'воскресенье',
344                caseGenitive: 'воскресенья'
345            },
346            {
347                caseNominative: 'понедельник',
348                caseGenitive: 'понедельника'
349            },
350            {
351                caseNominative: 'вторник',
352                caseGenitive: 'вторника'
353            },
354            {
355                caseNominative: 'среда',
356                caseGenitive: 'среды'
357            },
358            {
359                caseNominative: 'четверг',
360                caseGenitive: 'четверга'
361            },
362            {
363                caseNominative: 'пятница',
364                caseGenitive: 'пятницы'
365            },
366            {
367                caseNominative: 'суббота',
368                caseGenitive: 'субботы'
369            },
370        ][num];
371    },
372    
373    /**
374     * Возвращает название месяца на русском языке на основе его числового представления.
375     *
376     * @param {number} num - Числовое представление месяца (от 0 до 11).
377     * @returns {Object} - Объект, содержащий название месяца в именительном и родительном падежах.
378     */
379    // ToDo добавить падежи, если нужно
380    getMonthInRus: (num) => {
381        return [
382            {
383                caseNominative: 'январь',
384                caseGenitive: 'января'
385            },
386            {
387                caseNominative: 'февраль',
388                caseGenitive: 'февраля'
389            },
390            {
391                caseNominative: 'март',
392                caseGenitive: 'марта'
393            },
394            {
395                caseNominative: 'апрель',
396                caseGenitive: 'апреля'
397            },
398            {
399                caseNominative: 'май',
400                caseGenitive: 'мая'
401            },
402            {
403                caseNominative: 'июнь',
404                caseGenitive: 'июня'
405            },
406            {
407                caseNominative: 'июль',
408                caseGenitive: 'июля'
409            },
410            {
411                caseNominative: 'август',
412                caseGenitive: 'августа'
413            },
414            {
415                caseNominative: 'сентябрь',
416                caseGenitive: 'сентября'
417            },
418            {
419                caseNominative: 'октябрь',
420                caseGenitive: 'октября'
421            },
422            {
423                caseNominative: 'ноябрь',
424                caseGenitive: 'ноября'
425            },
426            {
427                caseNominative: 'декабрь',
428                caseGenitive: 'декабря'
429            },
430        ][num];
431    }
432};
433
434let globalApp = {
435    
436    init: function () {
437
438    },
439    
440    // обновление get-параметра в урле
441    // https://stackoverflow.com/questions/1090948/change-url-parameters/10997390#10997390
442    updateURLParameter: function (url, param, paramVal) {
443        let TheAnchor = null;
444        let newAdditionalURL = "";
445        let tempArray = url.split ("?");
446        let baseURL = tempArray[0];
447        let additionalURL = tempArray[1];
448        let temp = "";
449        
450        if (additionalURL) {
451            let tmpAnchor = additionalURL.split ("#");
452            let TheParams = tmpAnchor[0];
453            TheAnchor = tmpAnchor[1];
454            if (TheAnchor)
455                additionalURL = TheParams;
456            
457            tempArray = additionalURL.split ("&");
458            
459            // запись в урл параметров, отличных от переданного
460            for (let i = 0; i < tempArray.length; i++) {
461                if (tempArray[i].split ('=')[0] !== param) {
462                    newAdditionalURL += temp + tempArray[i];
463                    temp = "&";
464                }
465            }
466        }
467        else {
468            let tmpAnchor = baseURL.split ("#");
469            let TheParams = tmpAnchor[0];
470            TheAnchor = tmpAnchor[1];
471            
472            if (TheParams)
473                baseURL = TheParams;
474        }
475        
476        if (TheAnchor)
477            paramVal += "#" + TheAnchor;
478        
479        let rows_txt = (paramVal !== '') ? temp + "" + param + "=" + paramVal : '';
480        return baseURL + ((newAdditionalURL !== "" || rows_txt !== "") ? "?" + newAdditionalURL + rows_txt : "");
481    },
482    
483    catalogFilter: {
484        init: false,
485        filterGroups: {},
486        filterCurrentGroups: {},
487        filterData: {},
488        sortVal: '',
489        noPages: false,
490        ajaxUrl: '',
491        $itemsContainer: $('.js-catalog-items'),
492        
493        setUrl: function (url) {
494            let sortVal = globalApp.catalogFilter.sortVal;
495            
496            if (sortVal !== '') {
497                url = globalApp.updateURLParameter(url, 'sort', sortVal);
498            }
499            
500            window.history.replaceState('', '', url);
501        },
502        sendRequest: function () {
503            if (globalApp.catalogFilter.init === true) {
504                let sortVal = globalApp.catalogFilter.sortVal,
505                    noPages = globalApp.catalogFilter.noPages,
506                    ajaxData = JSON.parse(JSON.stringify(globalApp.catalogFilter.filterData));
507                
508                if (sortVal !== '') {
509                    ajaxData.sortVal = globalApp.catalogFilter.sortVal;
510                }
511                
512                if (noPages !== false) {
513                    ajaxData.noPages = true;
514                }
515                
516                $.ajax({
517                    method: 'POST',
518                    url: globalApp.catalogFilter.ajaxUrl,
519                    data: ajaxData,
520                    dataType: 'html',
521                    success: function (data) {
522                        globalApp.catalogFilter.outputItems(data);
523                    },
524                    complete: function () {
525                    
526                    },
527                    error: function (xhr, str) {
528                    
529                    }
530                });
531            }
532        },
533        outputItems: function (html, add = false) {
534            let $cont = globalApp.catalogFilter.$itemsContainer;
535            let $contItems = $('.js-catalog-items');
536            if ($cont.length > 0) {
537                if (!add) {
538                    if(($cont.hasClass('js-aside-wrap') && !$('.catalog-empty',html).length) || !$cont.hasClass('js-aside-wrap')) {
539                        $cont.html(html);
540                        if($cont.hasClass('js-aside-wrap')) {
541                            $(document).trigger('init-filter')
542                        }
543                    } else {
544                        $contItems.html(html);
545                    }
546                    $('.js-catalog-items').trigger('init-books')
547                } else {
548
549                }
550            }
551        }
552    }
553};
554
555// Статистика по кликам с баннеров
556function statClick(id) {
557    $.ajax({
558        method: 'POST',
559        url: '/api/stat_banner/',
560        data: {id: id},
561        dataType: 'json'
562    });
563}
564
565(function () {
566    $(function () {
567        $('.js-header-droplist-opener').click(function (e) {
568            let $_this = $(this);
569
570            $_this.toggleClass('_opened');
571        });
572
573        let headSearch = function ($obj) {
574            if ($obj instanceof jQuery) {
575                let $_this = $obj,
576                    $header = $('.js-header-top'),
577                    $input = $_this.find('.js-headsearch__input'),
578                    $delBtn = $_this.find('.js-headsearch__del'),
579                    
580                    focusClass = '_searching';
581                
582                $delBtn.click(function(e){
583                    e.preventDefault();
584                    $input.val('');
585                    $header.removeClass(focusClass);
586                });
587                
588                $input.focus(function(e){
589                    if (! $header.hasClass(focusClass)) {
590                        $header.addClass(focusClass);
591                    }
592                })
593            }
594        };
595        
596        headSearch($('.js-headsearch'));
597    });
598}());
599
600(function () {
601	$(function () {
602        
603	    /*
604	    
605	    Решение для десктопной фиксации положения боковой панели с фильтром в каталоге, без потери возможности её
606	    проскроллить
607	    
608	    Для корректной работы, при ресайзе и изменениях высоты блока, присваиваем обёртке min-height с высотой боковой
609	    панели, иначе при коротком списке товаров фильтр невозможно доскроллить до конца
610	    
611	    При загрузке страницы ждём, пока инициализируются все ползунки и каты в фильтре, и только затем инициализируем
612	    минимальную высоту блока (иначе будет некорректное значение)
613	    
614	     */
615	    
616	    // TODO прелоадер блока
617        
618        let lastScrollTop = $ (window).scrollTop ();
619        let wasScrollingDown = true;
620        const $sidebar = $ ('.js-aside-bar');
621        const $wrap = $ ('.js-aside-wrap');
622        const $desktopFilter = $sidebar.find ('.js-filt');
623        let initialSidebarTop;
624        
625        if ($sidebar.length === 0 || $wrap.length === 0 || $desktopFilter.length === 0) return false;
626        
627        
628        // проверка, все ли ползунки и каты заинитились
629        function isReadyForInit () {
630            const $ranges = $desktopFilter.find('.js-filt-range');
631            const $rangesInited = $ranges.filter('._inited');
632            const $groups = $desktopFilter.find('.js-filt-group');
633            const $groupsInited = $groups.filter('._inited');
634            
635            return $ranges.length === $rangesInited.length && $groups.length === $groupsInited.length;
636        }
637        
638        function recalcHeight () {
639            $wrap.css('min-height', $sidebar.innerHeight() + 'px');
640        }
641        
642        function recalcPosition () {
643            let windowHeight = $(window).height(),
644                wrapHeight = $wrap.outerHeight(),
645                wrapTop = $wrap.offset().top,
646                wrapBottom = wrapTop + wrapHeight;
647    
648            let scrollTop = $(window).scrollTop(),
649                scrollBottom = scrollTop + windowHeight;
650    
651            let sidebarHeight = $sidebar.outerHeight();
652    
653            let sidebarTop = $sidebar.offset().top,
654                sidebarBottom = sidebarTop + sidebarHeight;
655    
656            let heightDelta = Math.abs(windowHeight - sidebarHeight),
657                scrollDelta = lastScrollTop - scrollTop;
658    
659            let isScrollingDown = (scrollTop > lastScrollTop),
660                isWindowLarger = (windowHeight > sidebarHeight);
661    
662            let stopDragging = (scrollBottom >= wrapBottom);
663            
664            // Проверка на слишком маленькую высоту
665            if ($wrap.outerHeight() <= 1000) {
666                // Если высота слишком маленькая, фильтр остаётся на месте
667            } else if (stopDragging) {
668                $sidebar.removeClass('_fixed').addClass('_stopped').css('top', 'auto');
669            } else {
670                if ($sidebar.hasClass('_stopped')) {
671                    $sidebar.addClass('_fixed').removeClass('_stopped').css('top', -heightDelta);
672                } else {
673                    if ((isWindowLarger && scrollTop > initialSidebarTop) || (!isWindowLarger && scrollBottom > initialSidebarTop + sidebarHeight)) {
674                        $sidebar.addClass('_fixed');
675                    } else if (!isScrollingDown && scrollTop <= initialSidebarTop) {
676                        $sidebar.removeClass('_fixed');
677                    }
678            
679                    let dragBottomDown = (sidebarBottom <= scrollBottom && isScrollingDown),
680                        dragTopUp = (sidebarTop >= scrollTop && !isScrollingDown);
681            
682                    if (dragBottomDown) {
683                        if (isWindowLarger) {
684                            $sidebar.css('top', 0);
685                        } else {
686                            $sidebar.css('top', -heightDelta);
687                        }
688                    } else if (dragTopUp) {
689                        $sidebar.css('top', 0);
690                    } else if ($sidebar.hasClass('_fixed')) {
691                        let currentTop = parseInt($sidebar.css('top'), 10);
692                
693                        let minTop = -heightDelta,
694                            scrolledTop = currentTop + scrollDelta;
695                
696                        let isPageAtBottom = (scrollTop + windowHeight >= $(document).height()),
697                            newTop = (isPageAtBottom) ? minTop : scrolledTop;
698                
699                        $sidebar.css('top', newTop);
700                    }
701                }
702            }
703    
704            lastScrollTop = scrollTop;
705            wasScrollingDown = isScrollingDown;
706        }
707        
708        // инит
709        
710        /*
711        Здесь, ввиду значительного влияния на производительность, снижаем частоту триггера ресайза
712        https://www.onlywebpro.com/2017/03/25/optimize-scrolling-performance-by-debouncing-scroll-event-calls/
713         */
714        
715        $desktopFilter.find ('.js-filt-group, .js-filt-range').on ('is:inited', () => {
716            if (isReadyForInit ()) {
717                initialSidebarTop = $sidebar.offset ().top;
718                
719                recalcHeight ();
720                if (window.innerWidth > 1039) {
721                    recalcPosition ();
722                }
723                
724                let debounceTimerResize;
725                $ (window).on('resize', (e) => {
726                    if (debounceTimerResize) {
727                        window.clearTimeout (debounceTimerResize);
728                    }
729                    debounceTimerResize = window.setTimeout (function () {
730                        if (window.innerWidth > 1039) {
731                            recalcHeight ();
732                            recalcPosition ();
733                        }
734                    }, 100);
735                });
736                
737                $sidebar.on ('click', '.js-filt-group__expand', (e) => {
738                    setTimeout (() => {
739                        recalcHeight ();
740                    }, 600);
741                });
742                
743                $ (window).on ('scroll', () => {
744                    if (window.innerWidth > 1039) {
745                        recalcPosition ();
746                    }
747                });
748            }
749        });
750    });
751}());
752
753
754(function () {
755    $(function () {
756        
757        $(document).on("basketItemChange", function(e, id, offerId, q, data) {
758            let $btns = $('.js-btn-buy[data-offer-id=' + offerId + ']'),
759                $btnsQuantity = $('.js-btn-buy-quantity[data-offer-id=' + offerId + ']');
760
761            if ($btns.length > 0) {
762                $btns.each(function(){
763                    if (q > 0) {
764                        if (!$(this).is('._added')) {
765                            $(this).addClass('_added');
766                        }
767                    } else if (q === 0) {
768                        if ($(this).is('._added')) {
769                            $(this).removeClass('_added');
770                        }
771                    }
772                });
773            }
774
775            if ($btnsQuantity.length > 0) {
776                $btnsQuantity.each(function(){
777                    $(this).find("input").val(q).trigger("change");
778                });
779            }
780        });
781        
782        $(document).on("change", ".js-btn-buy-quantity input", function(e){
783            let $wrap = $(this).parents(".js-btn-buy-quantity"),
784                $qText = $wrap.find(".js-btn-buy-quantity-val");
785
786            $qText.text($(this).val());
787        });
788	});
789}());
790
791(function () {
792    $(function () {
793        /*
794        * Проверка кук киалабса и обновление значений в localStorage, если не совпадает
795        * */
796        let cookieKeaUserId = $.cookie('kea_user_id')
797        let cookieKeaCartId = $.cookie('kea_cart_id')
798        let storageKeaUserId = localStorage.getItem('kea_user_id')
799        let storageKeaCartId = localStorage.getItem('kea_cart_id')
800        if (cookieKeaUserId !== storageKeaUserId) {
801            if (cookieKeaUserId === undefined) {
802                localStorage.removeItem('kea_user_id')
803            } else {
804                localStorage.setItem('kea_user_id', cookieKeaUserId)
805            }
806        }
807        if (cookieKeaCartId !== storageKeaCartId) {
808            if (cookieKeaUserId === undefined) {
809                localStorage.removeItem('kea_cart_id')
810            } else {
811                localStorage.setItem('kea_cart_id', cookieKeaCartId)
812            }
813        }
814    });
815}());
816
817function updateWindowScroll (disable) {
818    if (disable) {
819        $('html').addClass('_filter-open');
820    } else {
821        $('html').removeClass('_filter-open');
822    }
823    
824    disableBodyScroll(disable);
825}
826
827/**
828 * Prevent body scroll and overscroll.
829 * Tested on mac, iOS chrome / Safari, Android Chrome.
830 *
831 * Based on: https://benfrain.com/preventing-body-scroll-for-modals-in-ios/
832 *           https://stackoverflow.com/a/41601290
833 *
834 * Use in combination with:
835 * html, body {overflow: hidden;}
836 *
837 * and: -webkit-overflow-scrolling: touch; for the element that should scroll.
838 *
839 * disableBodyScroll(true, '.i-can-scroll');
840 */
841var disableBodyScroll = (function () {
842    
843    /**
844     * Private variables
845     */
846    var _selector = false,
847        _element = false,
848        _clientY;
849    
850    /**
851     * Polyfills for Element.matches and Element.closest
852     */
853    if (!Element.prototype.matches)
854        Element.prototype.matches = Element.prototype.msMatchesSelector ||
855            Element.prototype.webkitMatchesSelector;
856    
857    if (!Element.prototype.closest)
858        Element.prototype.closest = function (s) {
859            var ancestor = this;
860            if (!document.documentElement.contains(el)) return null;
861            do {
862                if (ancestor.matches(s)) return ancestor;
863                ancestor = ancestor.parentElement;
864            } while (ancestor !== null);
865            return el;
866        };
867    
868    /**
869     * Prevent default unless within _selector
870     *
871     * @param  event object event
872     * @return void
873     */
874    var preventBodyScroll = function (event) {
875        if (false === _element || !event.target.closest(_selector)) {
876            event.preventDefault();
877        }
878    };
879    
880    /**
881     * Cache the clientY co-ordinates for
882     * comparison
883     *
884     * @param  event object event
885     * @return void
886     */
887    var captureClientY = function (event) {
888        // only respond to a single touch
889        if (event.targetTouches.length === 1) {
890            _clientY = event.targetTouches[0].clientY;
891        }
892    };
893    
894    /**
895     * Detect whether the element is at the top
896     * or the bottom of their scroll and prevent
897     * the user from scrolling beyond
898     *
899     * @param  event object event
900     * @return void
901     */
902    var preventOverscroll = function (event) {
903        // only respond to a single touch
904        if (event.targetTouches.length !== 1) {
905            return;
906        }
907        
908        var clientY = event.targetTouches[0].clientY - _clientY;
909        
910        // The element at the top of its scroll,
911        // and the user scrolls down
912        if (_element.scrollTop === 0 && clientY > 0) {
913            event.preventDefault();
914        }
915        
916        // The element at the bottom of its scroll,
917        // and the user scrolls up
918        // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
919        if ((_element.scrollHeight - _element.scrollTop <= _element.clientHeight) && clientY < 0) {
920            event.preventDefault();
921        }
922        
923    };
924    
925    /**
926     * Disable body scroll. Scrolling with the selector is
927     * allowed if a selector is porvided.
928     *
929     * @param  boolean allow
930     * @param  string selector Selector to element to change scroll permission
931     * @return void
932     */
933    return function (allow, selector) {
934        if (typeof selector !== "undefined") {
935            _selector = selector;
936            _element = document.querySelector(selector);
937        }
938        
939        if (true === allow) {
940            if (false !== _element) {
941                _element.addEventListener('touchstart', captureClientY, false);
942                _element.addEventListener('touchmove', preventOverscroll, {passive: false});
943            }
944            document.body.addEventListener("touchmove", preventBodyScroll, {passive: false});
945        } else {
946            if (false !== _element) {
947                _element.removeEventListener('touchstart', captureClientY, false);
948                _element.removeEventListener('touchmove', preventOverscroll, {passive: false});
949            }
950            document.body.removeEventListener("touchmove", preventBodyScroll, {passive: false});
951        }
952    };
953}());
954
955
956// https://gist.github.com/AlexEmashev/ee8302b5036b01362f63dab35948401f
957// Demo http://codepen.io/AlexEmashev/pen/BKgQdx
958(function ($) {
959    $.fn.swipeDetector = function (options) {
960        // States: 0 - no swipe, 1 - swipe started, 2 - swipe released
961        var swipeState = 0;
962        // Coordinates when swipe started
963        var startX = 0;
964        var startY = 0;
965        // Distance of swipe
966        var pixelOffsetX = 0;
967        var pixelOffsetY = 0;
968        // Target element which should detect swipes.
969        var swipeTarget = this;
970        var defaultSettings = {
971            // Amount of pixels, when swipe don't count.
972            swipeThreshold: 70,
973            // Flag that indicates that plugin should react only on touch events.
974            // Not on mouse events too.
975            useOnlyTouch: false
976        };
977        
978        // Initializer
979        (function init() {
980            options = $.extend(defaultSettings, options);
981            // Support touch and mouse as well.
982            swipeTarget.on('mousedown touchstart', swipeStart);
983            $('html').on('mouseup touchend', swipeEnd);
984            $('html').on('mousemove touchmove', swiping);
985        })();
986        
987        function swipeStart(event) {
988            if (options.useOnlyTouch && !event.originalEvent.touches)
989                return;
990            
991            if (event.originalEvent.touches)
992                event = event.originalEvent.touches[0];
993            
994            if (swipeState === 0) {
995                swipeState = 1;
996                startX = event.clientX;
997                startY = event.clientY;
998            }
999        }
1000        
1001        function swipeEnd(event) {
1002            if (swipeState === 2) {
1003                swipeState = 0;
1004                
1005                if (Math.abs(pixelOffsetX) > Math.abs(pixelOffsetY) &&
1006                    Math.abs(pixelOffsetX) > options.swipeThreshold) { // Horizontal Swipe
1007                    if (pixelOffsetX < 0) {
1008                        swipeTarget.trigger($.Event('swipeLeft.sd'));
1009                    } else {
1010                        swipeTarget.trigger($.Event('swipeRight.sd'));
1011                    }
1012                } else if (Math.abs(pixelOffsetY) > options.swipeThreshold) { // Vertical swipe
1013                    if (pixelOffsetY < 0) {
1014                        swipeTarget.trigger($.Event('swipeUp.sd'));
1015                    } else {
1016                        swipeTarget.trigger($.Event('swipeDown.sd'));
1017                    }
1018                }
1019            }
1020        }
1021        
1022        function swiping(event) {
1023            // If swipe don't occuring, do nothing.
1024            if (swipeState !== 1)
1025                return;
1026            
1027            
1028            if (event.originalEvent.touches) {
1029                event = event.originalEvent.touches[0];
1030            }
1031            
1032            var swipeOffsetX = event.clientX - startX;
1033            var swipeOffsetY = event.clientY - startY;
1034            
1035            if ((Math.abs(swipeOffsetX) > options.swipeThreshold) ||
1036                (Math.abs(swipeOffsetY) > options.swipeThreshold)) {
1037                swipeState = 2;
1038                pixelOffsetX = swipeOffsetX;
1039                pixelOffsetY = swipeOffsetY;
1040            }
1041        }
1042        
1043        return swipeTarget; // Return element available for chaining.
1044    }
1045    
1046}(jQuery));
1047
1048
1049// https://medium.com/talk-like/detecting-if-an-element-is-in-the-viewport-jquery-a6a4405a3ea2
1050(function ($) {
1051    $.fn.isInViewport = function () {
1052        var elementTop = $ (this).offset ().top;
1053        var elementBottom = elementTop + $ (this).outerHeight ();
1054        var viewportTop = $ (window).scrollTop ();
1055        var viewportBottom = viewportTop + $ (window).height ();
1056        return elementBottom > viewportTop && elementTop < viewportBottom;
1057    };
1058} (jQuery));
1059
1060
1061(function () {
1062    /**
1063     * Добавляет на страницу обёртку для нижних баров
1064     * и позволяет вставлять туда и удалять оттуда контент
1065     */
1066    $(function () {
1067        let inited = false
1068        function init() {
1069            if ($('.js-site-bottom-wrapper').length === 0) {
1070                inited = true
1071                $('<div class="site-bottom-wrapper js-site-bottom-wrapper"></div>').appendTo($('body'))
1072            }
1073        }
1074
1075        /**
1076         * Добавляет в начало списка
1077         */
1078        $.fn.prependToBottomWrapper = function () {
1079            if (!inited)
1080                init()
1081            const $_this = $(this).detach()
1082            return $_this.prependTo('.js-site-bottom-wrapper')
1083        }
1084        /**
1085         * Добавляет в конец списка
1086         */
1087        $.fn.appendToBottomWrapper = function () {
1088            if (!inited)
1089                init()
1090            const $_this = $(this).detach()
1091            return $_this.appendTo('.js-site-bottom-wrapper')
1092        }
1093        /**
1094         * Вырезает элемент и возвращает его
1095         */
1096        $.fn.detachFromBottomWrapper = function () {
1097            const $_this = $(this)
1098            if ($_this.closest('.js-site-bottom-wrapper')) {
1099                return $_this.detach()
1100            }
1101        }
1102    });
1103    
1104    /**
1105     * Таймаут ввода в инпуты
1106     * https://github.com/andrescevp/jquery.donetyping/blob/master/jquery.donetyping.js
1107     */
1108    $.fn.extend({
1109        donetyping: function(callback,timeout){
1110            timeout = timeout || 1e3; // 1 second default timeout
1111            var timeoutReference,
1112                doneTyping = function(el){
1113                    if (!timeoutReference) return;
1114                    timeoutReference = null;
1115                    callback.call(el);
1116                };
1117            return this.each(function(i,el){
1118                var $el = $(el);
1119    
1120                ($el.is(':input') || $el.is('textarea')) && $el.on('keyup keypress',function(e){
1121                    var excludedKeys = [
1122                        16, 17, 18, 20, 35, 36, 37,
1123                        38, 39, 40, 45, 144, 225
1124                    ];
1125                    // This catches the backspace button in chrome, but also prevents
1126                    // the event from triggering too premptively. Without this line,
1127                    // using tab/shift+tab will make the focused element fire the callback.
1128                    if (e.type=='keyup' &&  $.inArray( e.keyCode, excludedKeys ) !== -1) return;
1129                
1130                    // Check if timeout has been set. If it has, "reset" the clock and
1131                    // start over again.
1132                    if (timeoutReference) clearTimeout(timeoutReference);
1133                    timeoutReference = setTimeout(function(){
1134                        // if we made it here, our timeout has elapsed. Fire the
1135                        // callback
1136                        doneTyping(el);
1137                    }, timeout);
1138                }).on('blur',function(){
1139                    // If we can, fire the event since we're leaving the field
1140                    doneTyping(el);
1141                });
1142            });
1143        }
1144    });
1145}());
1146
1147document.addEventListener("DOMContentLoaded", function() {
1148    // Проверяем, есть ли в URL хэш
1149    if(window.location.hash) {
1150        // Получаем значение хэша
1151        var hash = window.location.hash.substring(1);  // Обрезаем первый символ '#'
1152        // Находим элемент по id и прокручиваем к нему
1153        var element = document.getElementById(hash);
1154        if (element) {
1155            setTimeout(function() {
1156                element.scrollIntoView();
1157            }, 1000);
1158        }
1159    }
1160});
1161
1162// Кастомизация настроек виджета чат-бота
1163document.addEventListener("DOMContentLoaded", function() {
1164    let isInited = false;
1165    const initialWidgetSettings = {
1166        offsetX: 20,
1167        offsetY: 20,
1168    };
1169    
1170    const recountWidgetSettings = () => {
1171        const newWidgetSettings = JSON.parse(JSON.stringify(initialWidgetSettings))
1172        let yValue = initialWidgetSettings.offsetY;
1173        const hasCookieBar = !!document.body.classList.contains('body_global-hasBottomBar');
1174        const hasMobileNav = !!document.body.classList.contains('body_has-bottom-nav');
1175        
1176        // Устанавливается дефолтное значение для состояния с плашкой про куки и/или моб.навигацией
1177        if (hasCookieBar) {
1178            yValue = (window.innerWidth > globObj.breakpoints.sm) ? 100 : 160;
1179        }
1180        if (hasMobileNav && window.innerWidth <= globObj.breakpoints.lg) {
1181            yValue = hasCookieBar ? 220 : 80;
1182        }
1183        
1184        if (document.body.classList.contains('body_page-basket-digital')) {
1185            // Для страницы электронной корзин
1185ы
1186            if (window.innerWidth <= globObj.breakpoints.lg) {
1187                yValue = (hasCookieBar) ? 320 : 180;
1188            }
1189            
1190        } else if (['body_page-book', 'ab-component__leap', 'body_page-basket'].some(className => document.body.classList.contains(className))) {
1191            // Для страниц с лип-меню
1192            if (window.innerWidth <= globObj.breakpoints.lg) {
1193                yValue = (hasCookieBar) ? 240 : 100;
1194            }
1195        }
1196        
1197        newWidgetSettings.offsetY = yValue;
1198        return newWidgetSettings;
1199    };
1200    
1201    const setSettings = (settings) => {
1202        if (window.fasttrack && window.fasttrack.setSettings) {
1203            window.fasttrack.setSettings(settings);
1204        }
1205    };
1206    
1207    // События onReady и onMounted в JS API виджета не срабатывают в такой реализации (возможно, нужно вручную инитить
1208    // виджет).
1209    // Поэтому приходится вручную проверять готовность виджета.
1210    // Сначала проверяем, получен ли объект виджета. Если нет, то делаем 500 попыток раз в 200мс.
1211    // Если сразу же после этого применить нужные настройки, то виджет иногда может применить свои настройки позднее.
1212    // Поэтому жёстко применяем настройки вручную раз в 500мс 5 раз. На третью итерацию показываем кнопку.
1213    // В таком случае минимизируется вероятность того, что виджет будет менять позиционирование при загрузке страницы.
1214    let initIntervalCnt = 0;
1215    const initInterval = setInterval(() => {
1216        if (window.fasttrack) {
1217            clearInterval(initInterval);
1218            
1219            let mountIntervalCnt = 0;
1220            const mountInterval = setInterval(() => {
1221                setSettings(recountWidgetSettings());
1222                
1223                if (!isInited && mountIntervalCnt > 2) {
1224                    const widget = document.querySelector('fstrk-widget');
1225                    if (widget) {
1226                        widget.setAttribute('data-status', 'initialized');
1227                        isInited = true;
1228                    }
1229                }
1230                
1231                if (++mountIntervalCnt > 5) {
1232                    clearInterval(mountInterval);
1233                }
1234            }, 500);
1235        }
1236        
1237        if (++initIntervalCnt > 500) {
1238            clearInterval(initInterval);
1239        }
1240    }, 200);
1241    
1242    // Ресайз окна
1243    window.addEventListener('resize', globalFunctions.throttle(function(event){
1244        setSettings(recountWidgetSettings());
1245    }, 100));
1246    
1247    // Отлавливается событие скрытия плашки
1248    document.addEventListener('ab.message.cookies.hide', () => {
1249        setSettings(recountWidgetSettings());
1250    });
1251});
1252
1253
1254/**
1255 * ============================================================================
1256 * Page Performance Orchestration (PPO)
1257 * ============================================================================
1258 *
1259 * Назначение
1260 * ----------
1261 * PPO — централизованный диспетчер выполнения клиентских задач в зависимости
1262 * от состояния загрузки страницы и пользовательского взаимодействия.
1263 *
1264 * Позволяет отказаться от разрозненных:
1265 *  - setTimeout(...)
1266 *  - requestIdleCallback(...)
1267 *  - обработчиков DOMContentLoaded
1268 *  - обработчиков window.load
1269 *  - PerformanceObserver
1270 * внутри отдельных компонентов.
1271 *
1272 * Вместо этого каждый компонент описывает только условия своего запуска,
1273 * а PPO самостоятельно принимает решение, когда выполнить задачу.
1274 *
1275 *
1276 * ---------------------------------------------------------------------------
1277 * Архитектура
1278 * ---------------------------------------------------------------------------
1279 *
1280 * PPO состоит из трех основных частей:
1281 *
1282 * 1. State
1283 *    Хранит текущее состояние страницы.
1284 *
1285 * 2. Event Sources
1286 *    Обновляют состояние страницы посредством PPO.set(...).
1287 *
1288 * 3. Tasks
1289 *    Независимые задачи, зарегистрированные через PPO.register(...).
1290 *    После каждого изменения состояния PPO проверяет условия выполнения
1291 *    всех зарегистрированных задач.
1292 *
1293 *
1294 * ---------------------------------------------------------------------------
1295 * State
1296 * ---------------------------------------------------------------------------
1297 *
1298 * lcp
1299 *      Largest Contentful Paint произошел.
1300 *
1301 * idle
1302 *      Страница считается свободной для выполнения второстепенных задач.
1303 *
1304 * interaction
1305 *      Пользователь начал взаимодействовать со страницей.
1306 *
1307 * domReady
1308 *      DOM полностью построен (DOMContentLoaded).
1309 *
1310 * load
1311 *      Полностью завершена загрузка страницы (window.load).
1312 *
1313 *
1314 * ---------------------------------------------------------------------------
1315 * API
1316 * ---------------------------------------------------------------------------
1317 *
1318 * PPO.set(key, value)
1319 *
1320 *      Изменяет значение состояния и инициирует повторную проверку
1321 *      зарегистрированных задач.
1322 *
1323 *
1324 * PPO.register({
1325 *      condition(state) {},
1326 *      action(state) {}
1327 * })
1328 *
1329 *      Регистрирует новую задачу.
1330 *
1331 *      condition(state)
1332 *          Функция должна вернуть true, если задача может быть выполнена.
1333 *
1334 *      action(state)
1335 *          Код, который необходимо выполнить после выполнения условия.
1336 *
1337 *      Каждая задача выполняется только один раз.
1338 *
1339 *
1340 * PPO.debug()
1341 *
1342 *      Выводит текущее внутреннее состояние PPO.
1343 *
1344 *
1345 * ---------------------------------------------------------------------------
1346 * Пример использования
1347 * ---------------------------------------------------------------------------
1348 *
1349 * PPO.register({
1350 *     condition(state) {
1351 *         return state.lcp && state.load;
1352 *     },
1353 *
1354 *     action() {
1355 *         initSlider();
1356 *     }
1357 * });
1358 *
1359 *
1360 * ---------------------------------------------------------------------------
1361 * Принципы использования
1362 * ---------------------------------------------------------------------------
1363 *
1364 * • PPO не знает ничего о конкретных компонентах сайта.
1365 *
1366 * • Компоненты не должны самостоятельно отслеживать загрузку страницы,
1367 *   если соответствующее состояние уже поддерживается PPO.
1368 *
1369 * • PPO является единым источником информации о состоянии жизненного цикла
1370 *   страницы.
1371 *
1372 * • Любые новые состояния рекомендуется добавлять именно в PPO, а не
1373 *   создавать собственные независимые механизмы отслеживания.
1374 *
1375 *
1376 * ---------------------------------------------------------------------------
1377 * Текущее состояние реализации
1378 * ---------------------------------------------------------------------------
1379 *
1380 * Реализация является MVP и покрывает основные сценарии проекта.
1381 *
1382 * Архитектура допускает дальнейшее расширение без изменения публичного API,
1383 * например:
1384 *
1385 *  - приоритеты выполнения задач;
1386 *  - группировку задач;
1387 *  - блокирующие задачи;
1388 *  - удаление зарегистрированных задач;
1389 *  - пользовательские события состояния;
1390 *  - дополнительные состояния жизненного цикла страницы.
1391 *
1392 * ============================================================================
1393 */
1394
1395window.PPO = window.PPO || (function () {
1396
1397    const state = {
1398        lcp: false,
1399        idle: false,
1400        interaction: false,
1401        domReady: false,
1402        load: false
1403    };
1404
1405    const tasks = [];
1406
1407    function set(key, value) {
1408        state[key] = value;
1409        evaluate();
1410    }
1411
1412    function register(task) {
1413        task.done = false;
1414
1415        task.run = function (state) {
1416            if (this.done) return;
1417
1418            if (task.condition(state)) {
1419                task.done = true;
1420                task.action(state);
1421            }
1422        };
1423
1424        tasks.push(task);
1425    }
1426
1427    function evaluate() {
1428        for (let i = 0; i < tasks.length; i++) {
1429            const t = tasks[i];
1430            if (!t.done) {
1431                t.run(state);
1432            }
1433        }
1434    }
1435
1436    function debug() {
1437        console.log('PPO state:', state);
1438        console.log('Tasks:', tasks);
1439    }
1440
1441    return {
1442        state,
1443        set,
1444        register,
1445        debug,
1446    };
1447})();
1448
1449
1450/**
1451 * ============================================================================
1452 * LCP tracking
1453 * ============================================================================
1454 */
1455
1456new PerformanceObserver((list) => {
1457    for (const entry of list.getEntries()) {
1458        if (entry.entryType === 'largest-contentful-paint') {
1459            window.PPO.set('lcp', true);
1460        }
1461    }
1462}).observe({ type: 'largest-contentful-paint', buffered: true });
1463
1464
1465/**
1466 * ============================================================================
1467 * Interaction + Idle source (единый источник событий)
1468 * ============================================================================
1469 */
1470
1471(function () {
1472
1473    let lastInteraction = Date.now();
1474    let idleScheduled = false;
1475    let idleTimer = null;
1476
1477    const IDLE_DELAY = 1500;
1478    const INTERACTION_COOLDOWN = 2000;
1479
1480    const passiveOptions = { passive: true };
1481
1482    function notifyInteraction() {
1483
1484        const now = Date.now();
1485        lastInteraction = now;
1486
1487        // фиксируем первое взаимодействие
1488        if (!PPO.state.interaction) {
1489            PPO.set('interaction', true);
1490        }
1491
1492        scheduleIdleCheck();
1493    }
1494
1495    function scheduleIdleCheck() {
1496
1497        if (idleScheduled) return;
1498        idleScheduled = true;
1499
1500        const check = () => {
1501
1502            const now = Date.now();
1503            const timeSinceInteraction = now - lastInteraction;
1504
1505            // если недавно было взаимодействие — откладываем
1506            if (timeSinceInteraction < INTERACTION_COOLDOWN) {
1507                idleTimer = setTimeout(check, INTERACTION_COOLDOWN);
1508                return;
1509            }
1510
1511            // LCP обязателен для idle
1512            if (!PPO.state.lcp) {
1513                idleScheduled = false;
1514                return;
1515            }
1516
1517            if ('requestIdleCallback' in window) {
1518                requestIdleCallback(() => {
1519                    PPO.set('idle', true);
1520                }, { timeout: 2000 });
1521            } else {
1522                setTimeout(() => {
1523                    PPO.set('idle', true);
1524                }, IDLE_DELAY);
1525            }
1526        };
1527
1528        idleTimer = setTimeout(check, IDLE_DELAY);
1529    }
1530
1531    const events = [
1532        'pointerdown',
1533        'keydown',
1534        'touchstart',
1535        'wheel'
1536    ];
1537
1538    events.forEach(evt => {
1539        window.addEventListener(evt, notifyInteraction, passiveOptions);
1540    });
1541
1542    window.addEventListener('DOMContentLoaded', scheduleIdleCheck);
1543    window.addEventListener('load', scheduleIdleCheck);
1544
1545})();
1546
1547
1548/**
1549 * ============================================================================
1550 * DOM lifecycle states
1551 * ============================================================================
1552 */
1553
1554document.addEventListener('DOMContentLoaded', function () {
1555    window.PPO.set('domReady', true);
1556});
1557
1558window.addEventListener('load', function () {
1559    window.PPO.set('load', true);
1560});
1561
1562/* END PPO */
1563
1564
1565/**
1566 * ============================================================================
1567 * Local Storage Helper
1568 * ============================================================================
1569 */
1570
1571window.abStorageHelper = {
1572    has(key) {
1573        const value = localStorage.getItem(key);
1574
1575        if (!value) {
1576            return false;
1577        }
1578
1579        try {
1580            const { expiresAt } = JSON.parse(value);
1581
1582            if (expiresAt && Date.now() >= expiresAt) {
1583                localStorage.removeItem(key);
1584
1585                return false;
1586            }
1587
1588            return true;
1589        } catch {
1590            localStorage.removeItem(key);
1591
1592            return false;
1593        }
1594    },
1595
1596    set(key, ttl) {
1597        localStorage.setItem(
1598            key,
1599            JSON.stringify({
1600                expiresAt: Date.now() + ttl,
1601            })
1602        );
1603    },
1604};
1605
1606/* END Local Storage Helper */
1607
1608//# sourceMappingURL=maps/global.js.map

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.