PageSourceSearch

https://msdm.ru/local/components/msdm/schedule/templates/.default/schedule-block.js?v=20260826-chapel-nikolay

js msdm.ru collected 2026-10-02 05:19:30 UTC 19,935 bytes, 460 lines download raw bytes

1(function () {
2    function ready(fn) {
3        if (document.readyState !== 'loading') fn();
4        else document.addEventListener('DOMContentLoaded', fn);
5    }
6
7    function initScheduleBlocks() {
8        var scriptVersion = '20260826-chapel-nikolay';
9        var roots = Array.prototype.slice.call(document.querySelectorAll('[data-msdm-schedule]'));
10        if (!roots.length) return;
11
12        roots.forEach(function (root) {
13            if (root.getAttribute('data-msdm-schedule-initialized') === '1'
14                && root.getAttribute('data-msdm-schedule-version') === scriptVersion) {
15                if (typeof root.msdmScheduleRefresh === 'function') {
16                    root.msdmScheduleRefresh();
17                }
18                return;
19            }
20
21        root.setAttribute('data-msdm-schedule-version', scriptVersion);
22
23        var headDate = root.querySelector('[data-msdm-schedule-head-date]');
24        var tabs = Array.prototype.slice.call(root.querySelectorAll('[data-msdm-schedule-tab]'));
25        var groups = Array.prototype.slice.call(root.querySelectorAll('[data-msdm-schedule-group]'));
26        var templeTabs = Array.prototype.slice.call(root.querySelectorAll('[data-msdm-schedule-temple-tab]'));
27        var templeTabWraps = Array.prototype.slice.call(root.querySelectorAll('[data-msdm-schedule-temple-tab-wrap]'));
28        var mobileTemple = root.querySelector('[data-msdm-schedule-temple-mobile]');
29        var mobileTempleCurrent = root.querySelector('[data-msdm-schedule-temple-mobile-current]');
30        var mobileTempleToggle = root.querySelector('[data-msdm-schedule-temple-mobile-toggle]');
31        var mobileTempleMenu = root.querySelector('[data-msdm-schedule-temple-mobile-menu]');
32        var templeTabsIndicator = root.querySelector('[data-msdm-schedule-temple-tabs-indicator]');
33        var activeTempleGroup = '';
34        var prevBtn = root.querySelector('[data-msdm-schedule-prev]');
35        var nextBtn = root.querySelector('[data-msdm-schedule-next]');
36        var swiperEl = root.querySelector('[data-msdm-schedule-swiper]');
37        var fullVisibleItemsCount = 3;
38        var currentDayPastFullItemsCount = 2;
39        var nowMinutes = parseInt(root.getAttribute('data-msdm-schedule-now-minutes') || '', 10);
40        var todayYmd = root.getAttribute('data-msdm-schedule-today') || '';
41
42        if (!tabs.length) return;
43        root.setAttribute('data-msdm-schedule-initialized', '1');
44        if (isNaN(nowMinutes)) nowMinutes = 0;
45
46        function formatHeaderDate(ymd) {
47            var parts = (ymd || '').split('-');
48            if (parts.length !== 3) return ymd;
49
50            var y = parseInt(parts[0], 10);
51            var m = parseInt(parts[1], 10);
52            var d = parseInt(parts[2], 10);
53
54            var months = {
55                1: 'Января', 2: 'Февраля', 3: 'Марта', 4: 'Апреля', 5: 'Мая', 6: 'Июня',
56                7: 'Июля', 8: 'Августа', 9: 'Сентября', 10: 'Октября', 11: 'Ноября', 12: 'Декабря'
57            };
58
59            return d + ' ' + (months[m] || '') + ' ' + y + ' г.';
60        }
61
62        function getActiveIndex() {
63            var idx = tabs.findIndex(function (t) {
64                return t.getAttribute('aria-selected') === 'true';
65            });
66            return idx >= 0 ? idx : 0;
67        }
68
69        function getWindowStart(items) {
70            if (!items.length || items.length <= fullVisibleItemsCount) {
71                return 0;
72            }
73
74            var nextIndex = items.findIndex(function (item) {
75                var itemMinutes = parseInt(item.getAttribute('data-msdm-schedule-minutes') || '', 10);
76                return !isNaN(itemMinutes) && itemMinutes >= nowMinutes;
77            });
78
79            if (nextIndex === -1) {
80                nextIndex = items.length;
81            }
82
83            var startIndex = nextIndex - (currentDayPastFullItemsCount + 1);
84
85            if (startIndex < 0) startIndex = 0;
86
87            return startIndex;
88        }
89
90        function getNextIndex(items) {
91            var nextIndex = items.findIndex(function (item) {
92                var itemMinutes = parseInt(item.getAttribute('data-msdm-schedule-minutes') || '', 
9210);
93                return !isNaN(itemMinutes) && itemMinutes >= nowMinutes;
94            });
95
96            return nextIndex === -1 ? items.length : nextIndex;
97        }
98
99        function getCurrentDayWindow(items) {
100            var lastFullStartIndex = Math.max(0, items.length - fullVisibleItemsCount);
101            var nextIndex = getNextIndex(items);
102
103            if (nextIndex >= items.length - 2) {
104                return {
105                    startIndex: lastFullStartIndex > 0 ? lastFullStartIndex - 1 : lastFullStartIndex,
106                    hasTopClip: lastFullStartIndex > 0,
107                    hasBottomClip: false
108                };
109            }
110
111            var startIndex = getWindowStart(items);
112            var fullItemsStartIndex = startIndex;
113
114            if (fullItemsStartIndex > lastFullStartIndex) {
115                fullItemsStartIndex = lastFullStartIndex;
116            }
117
118            var hasTopClip = fullItemsStartIndex > 0;
119            var hasBottomClip = (fullItemsStartIndex + fullVisibleItemsCount) < (items.length - 1);
120
121            if (!hasBottomClip && (fullItemsStartIndex + fullVisibleItemsCount) > items.length) {
122                fullItemsStartIndex = lastFullStartIndex;
123                hasTopClip = fullItemsStartIndex > 0;
124            }
125
126            return {
127                startIndex: hasTopClip ? fullItemsStartIndex - 1 : fullItemsStartIndex,
128                hasTopClip: hasTopClip,
129                hasBottomClip: hasBottomClip
130            };
131        }
132
133        function getOtherDayWindow(items) {
134            return {
135                startIndex: 0,
136                hasTopClip: false,
137                hasBottomClip: items.length > (fullVisibleItemsCount + 1)
138            };
139        }
140
141        function isCurrentDateGroup(group) {
142            var dateKey = group.getAttribute('data-msdm-schedule-group') || '';
143            return dateKey !== '' && dateKey === todayYmd;
144        }
145
146        function getAvailableTempleGroups(group) {
147            var available = {};
148            if (!group) return available;
149
150            group.querySelectorAll('[data-msdm-schedule-item][data-msdm-schedule-temple-group]').forEach(function (item) {
151                var templeGroup = item.getAttribute('data-msdm-schedule-temple-group') || '';
152                if (templeGroup) available[templeGroup] = true;
153            });
154
155            return available;
156        }
157
158        function updateTempleTabs(activeGroup) {
159            if (!templeTabs.length) return;
160
161            var available = getAvailableTempleGroups(activeGroup);
162            if (!available[activeTempleGroup]) {
163                activeTempleGroup = templeTabs.reduce(function (firstAvailable, tab) {
164                    var value = tab.getAttribute('data-msdm-schedule-temple-tab') || '';
165                    return firstAvailable || (available[value] ? value : '');
166                }, '');
167            }
168
169            templeTabWraps.forEach(function (wrap) {
170                var tab = wrap.querySelector('[data-msdm-schedule-temple-tab]');
171                var value = tab ? (tab.getAttribute('data-msdm-schedule-temple-tab') || '') : '';
172                var isVisible = !!available[value];
173                var isActive = isVisible && value === activeTempleGroup;
174
175                wrap.hidden = !isVisible;
176                wrap.classList.toggle('active', isActive);
177                if (tab) tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
178            });
179            renderMobileTempleMenu();
180            updateTempleIndicator(true);
181        }
182
183        function updateTempleIndicator(animate) {
184            if (!templeTabsIndicator || window.innerWidth <= 998) return;
185            var active = root.querySelector('[data-msdm-schedule-temple-tab][aria-selected="true"]');
186            var tabsWrap = root.querySelector('.msdm-schedule-temple-tabs');
187            if (!active || !tabsWrap) return;
188            var wrapRect = tabsWrap.getBoundingClientRect();
189            var rect = active.getBoundingClientRect();
190            if (!animate) templeTabsIndicator.style.transition = 'none';
191            templeTabsIndicator.style.width = rect.width + 'px';
192            templeTabsIndicator.style.height = rect.height + 'px';
193            templeTabsIndicator.style.transform = 'translate3d(' + (rect.left - wrapRect.left) + 'px,' + (rect.top - wrapRect.top) + 'px,0)';
194            if (!animate) requestAnimationFrame(function () { templeTabsIndicator.style.transition = ''; });
195        }
196
197        function closeMobileTempleMenu() {
198            if (!mobileTempleMenu || !mobileTempleToggle) return;
199            mobileTempleMenu.hidden = true;
200            mobileTempleToggle.classList.remove('is-open');
201            mobileTempleToggle.setAttribute('aria-expanded', 'false');
202        }
203
204        function renderMobileTempleMenu() {
205            if (!mobileTempleCurrent || !mobileTempleMenu) return;
206            var active = root.querySelector('[data-msdm-schedule-temple-tab][aria-selected="true"]');
207            mobileTempleCurrent.textContent = active ? active.textContent.trim() : '';
208            mobileTempleMenu.innerHTML = '';
209            templeTabs.forEach(function (tab) {
210                if (tab.closest('[hidden]') || tab.getAttribute('aria-selected') === 'true') return;
211                var option = document.createElement('div');
212                option.className = 'msdm-schedule-temple-mobile__option';
213                option.setAttribute('role', 'button');
214                option.setAttribute('tabindex', '0');
215                var details = tab.closest('.msdm-schedule-temple-tabs__item').querySelector('.msdm-schedule-temple-tabs__details');
216                option.innerHTML = '<span class="msdm-schedule-temple-mobile__option-main"><strong>' + tab.textContent.trim()
217                    + '</strong><span class="msdm-schedule-temple-mobile__option-arrow" aria-hidden="true">›</span></span>'
218                    + '<span class="msdm-schedule-temple-mobile__option-meta">' + (details ? details.innerHTML : '') + '</span>';
219                Array.prototype.slice.call(option.querySelectorAll('a')).forEach(function (link) {
220                    link.target = '_blank';
221                    link.rel = 'noopener';
222                    link.addEventListener('click', function (event) { event.stopPropagation(); });
223                });
224                option.addEventListener('click', function () {
225                    activeTempleGroup = tab.getAttribute('data-msdm-schedule-temple-tab') || '';
226                    var activeGroup = groups.find(function (group) { return group.style.display !== 'none'; });
227                    updateTempleTabs(activeGroup);
228                    applyVisibleWindow(activeGroup);
229                    closeMobileTempleMenu();
230                });
231                option.addEventListener('keydown', function (event) {
232                    if (event.key === 'Enter' || event.key === ' ') option.click();
233                });
234                mobileTempleMenu.appendChild(option);
235            });
236        }
237
238        function applyVisibleWindow(activeGroup) {
239            groups.forEach(function (group) {
240                var items = Array.prototype.slice.call(group.querySelectorAll('[data-msdm-schedule-item]'));
241                items.forEach(function (item) {
242                    var matchesTemple = !activeTempleGroup
243                        || (item.getAttribute('data-msdm-schedule-temple-group') || '') 
243=== activeTempleGroup;
244                    item.hidden = !matchesTemple;
245                    item.classList.toggle('calendar-item_hidden', !matchesTemple);
246                });
247            });
248
249            root.classList.remove('msdm-schedule_has-top-fade', 'msdm-schedule_has-bottom-fade');
250
251            if (!activeGroup) {
252                return;
253            }
254
255            var items = Array.prototype.slice.call(activeGroup.querySelectorAll('[data-msdm-schedule-item]')).filter(function (item) {
256                return !item.hidden;
257            });
258            if (items.length <= fullVisibleItemsCount) {
259                return;
260            }
261
262            var isCurrentGroup = isCurrentDateGroup(activeGroup);
263            var windowConfig = isCurrentGroup ? getCurrentDayWindow(items) : getOtherDayWindow(items);
264            var startIndex = windowConfig.startIndex;
265            var hasTopClip = windowConfig.hasTopClip;
266            var hasBottomClip = windowConfig.hasBottomClip;
267            var visibleItemsCount = fullVisibleItemsCount + (hasTopClip ? 1 : 0) + (hasBottomClip ? 1 : 0);
268            var endIndex = startIndex + visibleItemsCount;
269
270            items.forEach(function (item, index) {
271                var isVisible = index >= startIndex && index < endIndex;
272                item.hidden = !isVisible;
273                item.classList.toggle('calendar-item_hidden', !isVisible);
274            });
275
276            if (hasTopClip) {
277                root.classList.add('msdm-schedule_has-top-fade');
278            }
279
280            if (hasBottomClip) {
281                root.classList.add('msdm-schedule_has-bottom-fade');
282            }
283        }
284
285        function setActiveByIndex(index, options) {
286            if (index < 0 || index >= tabs.length) return;
287            var opts = options || {};
288            var skipTabScroll = !!opts.skipTabScroll;
289
290            var dateKey = tabs[index].getAttribute('data-msdm-schedule-tab');
291
292            tabs.forEach(function (btn, i) {
293                var isActive = (i === index);
294                btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
295
296                // на всякий случай — разные стили могут ждать разные классы
297                btn.classList.toggle('is-active', isActive);
298                btn.classList.toggle('active', isActive);
299            });
300
301            var activeGroup = null;
302            groups.forEach(function (g) {
303                var isActive = g.getAttribute('data-msdm-schedule-group') === dateKey;
304                g.style.display = isActive ? '' : 'none';
305                if (isActive) {
306                    activeGroup = g;
307                }
308            });
309
310            updateTempleTabs(activeGroup);
311            applyVisibleWindow(activeGroup);
312
313            if (headDate) headDate.textContent = formatHeaderDate(dateKey);
314
315            // скролл табов к активному
316            if (skipTabScroll) {
317                return;
318            }
319
320            if (swiper && typeof swiper.slideTo === 'function') {
321                swiper.slideTo(index);
322            } else {
323                tabs[index].scrollIntoView({behavior: 'smooth', inline: 'center', block: 'nearest'});
324            }
325        }
326
327        // клики по табам
328        tabs.forEach(function (btn, index) {
329            btn.addEventListener('click', function () {
330                setActiveByIndex(index);
331            });
332        });
333
334        templeTabs.forEach(function (btn) {
335            btn.addEventListener('click', function () {
336                var templeGroup = btn.getAttribute('data-msdm-schedule-temple-tab') || '';
337                if (!templeGroup || templeGroup === activeTempleGroup) return;
338
339                activeTempleGroup = templeGroup;
340                var activeGroup = groups.find(function (group) {
341                    return group.style.display !== 'none';
342                });
343                updateTempleTabs(activeGroup);
344                applyVisibleWindow(activeGroup);
345            });
346        });
347
348        if (mobileTempleToggle && mobileTempleMenu) {
349            mobileTempleToggle.addEventListener('click', function () {
350                var isOpen = mobileTempleMenu.hidden;
351                mobileTempleMenu.hidden = !isOpen;
352                mobileTempleToggle.classList.toggle('is-open', isOpen);
353                mobileTempleToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
354            });
355            document.addEventListener('click', function (event) {
356                if (mobileTemple && !mobileTemple.contains(event.target)) closeMobileTempleMenu();
357            });
358        }
359
360        // стрелки переключают табы
361        if (prevBtn) {
362            prevBtn.addEventListener('click', function (e) {
363                e.preventDefault();
364                var i = getActiveIndex();
365                if (i > 0) setActiveByIndex(i - 1);
366            });
367        }
368
369        if (nextBtn) {
370            nextBtn.addEventListener('click', function (e) {
371                e.preventDefault();
372                var i = getActiveIndex();
373                if (i < tabs.length - 1) setActiveByIndex(i + 1);
374            });
375        }
376
377        // клик по карточке ведёт на data-link
378        root.addEventListener('click', function (e) {
379            if (e.target.closest('a')) return;
380
381            var item = e.target.closest('.calendar-item[role="link"]');
382            if (!item || !root.contains(item)) return;
383
384            var link = item.getAttribute('data-link') || '';
385            if (!link) return;
386
387            window.location.href = link;
388        });
389
390        // Swiper (если подключен)
391        var swiper = null;
392        if (window.Swiper && swiperEl) {
393            try {
394                if (swiperEl.swiper) {
395                    swiper = swiperEl.swiper;
396                } else {
397                    swiper = new window.Swiper(swiperEl, {
398                        slidesPerView: 3,
399                        spaceBetween: 22,
400                        watchOverflow: true,
401                        breakpoints: {
402                            768: {
403                                slidesPerView: 7,
404                            }
405                        },
406                    });
407                }
408            } catch (err) {
409                swiper = null;
410            }
411        }
412
413        // гарантируем корректную инициализацию активного таба
414        setActiveByIndex(getActiveIndex(), {skipTabScroll: true});
415        requestAnimationFrame(function () { updateTempleIndicator(false); });
416        root.msdmScheduleRefresh = function () {
417            setActiveByIndex(getActiveIndex(), {skipTabScroll: true});
418        };
419        window.addEventListener('resize', function () { updateTempleIndicator(false); });
420        });
421    }
422
423    ready(initScheduleBlocks);
424
425    if (window.BX && typeof window.BX.addCustomEvent === 'function') {
426        window.BX.addCustomEvent('onFrameDataReceived', function () {
427            window.setTimeout(initScheduleBlocks, 0);
428        });
429    }
430
431    if ('MutationObserver' in window) {
432        var observerTimer = 0;
433        var observer = new MutationObserver(function (mutations) {
434            var hasScheduleChanges = mutations.some(function (mutation) {
435                return Array.prototype.slice.call(mutation.addedNodes || []).some(function (node) {
436                    if (!node || node.nodeType !== 1) return false;
437
438                    return (node.matches && node.matches('[data-msdm-schedule]'))
439                        || (node.querySelector && node.querySelector('[data-msdm-schedule]'));
440                });
441            });
442
443            if (!hasScheduleChanges) return;
444
445            window.clearTimeout(observerTimer);
446            observerTimer = window.setTimeout(initScheduleBlocks, 0);
447        });
448
449        ready(function () {
450            if (document.body) {
451                observer.observe(document.body, {childList: true, subtree: true});
452            }
453        });
454    }
455
456    window.setTimeout(initScheduleBlocks, 100);
457    window.setTimeout(initScheduleBlocks, 500);
458    window.setTimeout(initScheduleBlocks, 1500);
459    window.msdmInitScheduleBlocks = initScheduleBlocks;
460})();

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.