PageSourceSearch

https://www.advantech.com/2026/js/customize-filter.js?v=NSKE6hxDIaQUliKri1xEyhGc6LMOJwmF_Ymb2N2eJ4M

js advantech.com collected 2026-10-09 21:46:03 UTC 54,043 bytes, 1,263 lines download raw bytes

1/* =============================================
2   customize-filter.js
3   Filter Modal + Personalize Toast + Coachmark
4============================================= */
5(function ($) {
6    'use strict';
7
8    // 多語系文字:由 _CustomizeFilter.cshtml 在 #modal-filter 輸出 data-i18n-*(LanguageMulti 的 CustomizeFilter project)。
9    // 屬性不存在或為空時退回英文,避免 partial 沒渲染時顯示空白。
10    function _t(key, fallback) {
11        var el = document.getElementById('modal-filter');
12        var value = el && el.getAttribute('data-i18n-' + key);
13        return value || fallback;
14    }
15
16    /* =============================================
17       ui-modal 通用開關(事件委派)
18    ============================================= */
19    function openModal(modal) {
20        modal.style.display = 'flex';
21        requestAnimationFrame(function () {
22            requestAnimationFrame(function () { modal.classList.add('is-open'); });
23        });
24    }
25    function closeModal(modal) {
26        modal.classList.remove('is-open');
27        modal.addEventListener('transitionend', function handler(e) {
28            if (e.target === modal) {
29                modal.style.display = 'none';
30                modal.removeEventListener('transitionend', handler);
31            }
32        });
33    }
34    document.addEventListener('click', function (event) {
35        var trigger = event.target.closest('[data-ui-modal-open]');
36        if (trigger) {
37            var modal = document.getElementById(trigger.getAttribute('data-ui-modal-open'));
38            if (modal) openModal(modal);
39            return;
40        }
41        var closeBtn = event.target.closest('.ui-modal__close, [data-ui-modal-close]');
42        if (closeBtn) {
43            var m = closeBtn.closest('.ui-modal');
44            if (m && m.id !== 'modal-filter') closeModal(m);
45            return;
46        }
47        if (event.target.classList.contains('ui-modal')) {
48            // modal-filter 的背景點擊由專屬 handler 處理(觸發 coachmark)
49            if (event.target.id === 'modal-filter') return;
50            closeModal(event.target);
51        }
52    });
53
54    /* =============================================
55       UIAccordion 元件
56    ============================================= */
57    function Accordion($el) {
58        this.$el = $el;
59        this.multiple = $el.hasClass('ui-accordion--multiple');
60        this.$triggers = this.$el.find('.ui-accordion__trigger');
61        this.bindEvents();
62        this.init();
63    }
64
65    Accordion.prototype.init = function () {
66        this.$triggers.each(function () {
67            var $trigger = $(this);
68            if ($trigger.attr('aria-expanded') === 'true') {
69                var $item = $trigger.closest('.ui-accordion__item');
70                var $panel = $item.find('.ui-accordion__panel');
71                $item.addClass('is-open');
72                $panel.height($panel[0].scrollHeight);
73            }
74        });
75    };
76
77    Accordion.prototype.bindEvents = function () {
78        var self = this;
79        this.$triggers.on('click', function () {
80            self.toggle($(this));
81        });
82    };
83
84    Accordion.prototype.toggle = function ($trigger) {
85        var $item = $trigger.closest('.ui-accordion__item');
86        var $panel = $item.find('.ui-accordion__panel');
87        var isOpen = $item.hasClass('is-open');
88
89        if (isOpen) {
90            $panel.height($panel[0].scrollHeight);
91            requestAnimationFrame(function () {
92                $item.removeClass('is-open');
93                $panel.height(0);
94            });
95            $trigger.attr('aria-expanded', 'false');
96        } else {
97            if (!this.multiple) {
98                var $groupScope = this.$el.closest('.filter-accordion-grid').length
99                    ? this.$el.closest('.filter-accordion-grid')
100                    : this.$el;
101
102                $groupScope.find('.ui-accordion__item.is-open').each(function () {
103                    var $otherItem = $(this);
104                    var $otherPanel = $otherItem.find('.ui-accordion__panel');
105                    var $otherTrigger = $otherItem.find('.ui-accordion__trigger');
106
107                    $otherPanel.height($otherPanel[0].scrollHeight);
108                    requestAnimationFrame(function () {
109                        $otherItem.removeClass('is-open');
110                        $otherPanel.height(0);
111                    });
112                    $otherTrigger.attr('aria-expanded', 'false');
113                });
114            }
115
116            $item.addClass('is-open');
117            $panel.height($panel[0].scrollHeight);
118            $trigger.attr('aria-expanded', 'true');
119        }
120    };
121
122    window.UIAccordion = Accordion;
123
124    /* =============================================
125       DOM Ready
126    ============================================= */
127    $(function () {
128        var $modal = $('#modal-filter');
129        if (!$modal.length) return;
130
131        /* --- Topics API 設定(透過後端 proxy 避免 CORS) --- */
132        var TOPICS_API_URL = '/api/track/topics';
133        var _topicsCache = null;   // API 資料快取(fetch 一次即可)
134        var _topicsLoading = false;
135
136        /* --- 多語系欄位對應 --- */
137        var _langFieldMap = {
138            'en':    'topicNameEn',
139            'en-us': 'topicNameEn',
140            'zh-tw': 'topicNameZhTw',
141            'zh-cn': 'topicNameZhCn',
142            'ja-jp': 'topicNameJa',
143            'ko-kr': 'topicNameKo',
144            'th':    'topicNameTh'
145        };
146        var _currentLang = ($('#hidGlobalLang').val() || 'en').toLowerCase();
147
148        function _getTopicLabel(topic) {
149            var field = _langFieldMap[_currentLang];
150            return (field && topic[field]) || topic.topicNameEn || '';
151        }
152
153        /* --- 首次 / 回訪狀態管理(localStorage 持久化) --- */
154        var STORAGE_KEY_APPLIED  = 'adv_filter_applied';       // '1' = 曾 Apply 過
155        var STORAGE_KEY_PREFS    = 'adv_filter_preferences';   // JSON 陣列(topicId)
156
157        var _hasApplied = localStorage.getItem(STORAGE_KEY_APPLIED) === '1';
158        var _savedSelections = (function () {
159            try { return JSON.parse(localStorage.getItem(STORAGE_KEY_PREFS)) || []; }
160            catch (e) { return []; }
161        })();
162        var _isRevisit = false;
163
164        var _preferenceRevision = 0;
165        var _publishedIds = null;
166        var _confirmedSelections = _savedSelections.slice();
167        var _savingPreferences = false;
168        var _tagRequests = new Set();
169
170        function _consentGranted() {
171            return !!(window.AdvCookieConsent && window.AdvCookieConsent.isConsentGranted());
172        }
173
174        function _publishPreferences(ids) {
175            _publishedIds = ids.slice();
176            document.dispatchEvent(new CustomEvent('adv:preferences-changed', { detail: { topicIds: ids.slice() } }));
177        }
178        window.AdvHomePreferences = {
179            getSnapshot: function () { return _publishedIds === null ? null : _publishedIds.slice(); }
180        };
181
182        function _persistPreferences() {
183            if (_savingPreferences || !_consentGranted()) return Promise.resolve(false);
184            _savingPreferences = true;
185            var revision = ++_preferenceRevision;
186            var ids = _savedSelections.slice();
187            var $buttons = $modal.find('#btn-apply-preferences, #btn-revisit-done');
188            $buttons.prop('disabled', true);
189            return _requestTags('POST', ids).then(function () {
190                if (revision !== _preferenceRevision || !_consentGranted()) return false;
191                _confirmedSelections = ids.slice();
192                _serverTagsSynced = true;
193                try {
194                    localStorage.setItem(STORAGE_KEY_APPLIED, '1');
195                    localStorage.setItem(STORAGE_KEY_PREFS, JSON.stringify(ids));
196                } catch (e) { /* Preferences remain available for this page when storage is unavailable. */ }
197                _publishPreferences(ids);
198                return true;
199            }).catch(function () {
200                if (revision !== _preferenceRevision) return false;
201                _savedSelections = _confirmedSelections.slice();
202                if (_consentGranted())
203                    $modal.find('.filter-focus__placeholder')
204                        .text('Unable to save preferences. Please try again.').addClass('is-error').show();
205                return false;
206            }).then(function (ok) {
207                _savingPreferences = false;
208                $buttons.prop('disabled', false);
209                return ok;
210            });
211        }
212
213        var MA_TAGS_ENDPOINT = '/api/home/preferences';
214        var MA_TAGS_WEBSITE = 'Corp';
215
216        // Keep the SDK's visitor identity and API headers, with explicit status and cancellation handling.
217        function _requestTags(method, ids) {
218            if (!_consentGranted() || typeof AdvMarketingAutomationTracker === 'undefined')
219                return Promise.reject(new Error('Preferences are unavailable.'));
220            var controller = new AbortController();
221            _tagRequests.add(controller);
222            var timer = setTimeout(function () { controller.abort(); }, 8000);
223            var timeout = new Promise(function (_, reject) {
224                controller.signal.addEventListener('abort', function () { reject(new Error('Preferences request cancelled.')); });
225            });
226            var request = Promise.resolve().then(function () {
227                return new AdvMarketingAutomationTracker().getVisitorId();
228            }).then(function (visitorId) {
229                if (controller.signal.aborted || !_consentGranted()) throw new Error('Preferences request cancelled.');
230                var options = {
231                    method: method, signal: controller.signal, cache: 'no-store',
232                    headers: { 'X-Requested-With': 'AI-Tags' }
233                };
234                var endpoint = MA_TAGS_ENDPOINT;
235                if (method === 'POST') {
236                    options.headers['Content-Type'] = 'application/json';
237                    options.body = JSON.stringify({ visitorId: visitorId, website: MA_TAGS_WEBSITE,
238                        topicId: ids, visitedAt: new Date().toISOString() });
239                } else endpoint += '?visitorId=' + encodeURIComponent(visitorId);
240                return fetch(endpoint, options);
241            }).then(function (response) {
242                if (method === 'GET' && response.status === 404) return [];
243                if (!response.ok) throw new Error('Preferences request failed.');
244                return method === 'GET' ? response.json() : true;
245            });
246            return Promise.race([request, timeout]).finally(function () {
247                clearTimeout(timer);
248                _tagRequests.delete(controller);
249            });
250        }
251
252        var _serverTagsSynced = false;
253        var _serverSyncPromise = null;
254
255        function _syncTagsFromServer(done) {
256            if (_serverTagsSynced || !_consentGranted()) { done(); return; }
257            if (!_serverSyncPromise) {
258                var revision = _preferenceRevision;
259                var pending = _requestTags('GET').then(function (data) {
260                    if (!Array.isArray(data) || !data.every(function (id) { return /^[0-9]{1,10}$/.test(String(id)); }))
261                        throw new Error('Invalid preferences response.');
262                    if (revision !== _preferenceRevision || !_consentGranted()) return;
263                    var ids = Array.from(new Set(data.map(String))).sort();
264                    _savedSelections = ids;
265                    _confirmedSelections = ids.slice();
266                    _hasApplied = ids.length > 0;
267                    _serverTagsSynced = true;
268                    try {
269                        localStorage.setItem(STORAGE_KEY_PREFS, JSON.stringify(ids));
270                        if (_hasApplied) localStorage.setItem(STORAGE_KEY_APPLIED, '1');
271                        else localStorage.removeItem(STORAGE_KEY_APPLIED);
272                    } catch (e) { /* Storage is optional. */ }
273                    _publishPreferences(ids);
274                }).catch(function () {
275                    if (revision === _preferenceRevision) _publishPreferences([]);
276                }).then(function () {
277                    if (_serverSyncPromise === pending) _serverSyncPromise = null;
278                });
279                _serverSyncPromise = pending;
280            }
281            _serverSyncPromise.then(done);
282        }
283
284        var _lastConsent = null;
285        function _bootstrapServerSync() {
286            var allowed = _consentGranted();
287            if (allowed === _lastConsent) return;
288            _lastConsent = allowed;
289            if (!allowed) {
290                ++_preferenceRevision;
291                _tagRequests.forEach(function (controller) { controller.abort(); });
292                _serverSyncPromise = null;
293                _serverTagsSynced = false;
294                _publishPreferences([]);
295                return;
296            }
297            window.AdvCookieConsent.holdToast();
298            _syncTagsFromServer(function () { window.AdvCookieConsent.releaseToast(); });
299        }
300        document.addEventListener('cookieyes_consent_update', _bootstrapServerSync);
301        window.addEventListener('cookieyes_consent_update', _bootstrapServerSync);
302        _bootstrapServerSync();
303
304        // 取得目前所有勾選的 checkbox topicId
305        function _getCurrentSelections() {
306            var selections = [];
307            $modal.find('.ui-checkbox__input:checked').each(function () {
308                var id = $(this).attr('data-topic-id');
309                if (id) selections.push(id);
310            });
311            return selections.sort();
312        }
313
314        // 比對兩組 selections 是否相同
315        function _selectionsChanged(a, b) {
316            if (a.length !== b.length) return true;
317            for (var i = 0; i < a.length; i++) {
318                if (a[i] !== b[i]) return true;
319            }
320            return false;
321        }
322
323        // 載入儲存的勾選狀態到 UI
324        function _restoreSavedSelections() {
325            $modal.find('.ui-checkbox__input').prop('checked', false);
326            $modal.find('.ui-checkbox__input[data-topic-id]').each(function () {
327                var id = $(this).attr('data-topic-id');
328                if (_savedSelections.indexOf(id) > -1) {
329                    $(this).prop('checked', true);
330                }
331            });
332            // 重建右側 chips 與 badge
333            rebuildFocusList();
334        }
335
336        /* --- 從 API 載入 Topics 並渲染 Accordion --- */
337        function _fetchAndRenderTopics(callback) {
338            var $grid = $modal.find('.filter-accordion-grid');
339            var $loading = $grid.find('.filter-loading');
340
341            // 已有快取 → 直接用
342            if (_topicsCache) {
343                _renderAccordions(_topicsCache);
344                if (callback) callback();
345                return;
346            }
347
348            // 正在載入中 → 等待
349            if (_topicsLoading) return;
350            _topicsLoading = true;
351
352            $loading.show();
353
354            $.ajax({
355                url: TOPICS_API_URL,
356                dataType: 'json',
357                timeout: 15000
358            }).done(function (data) {
359                _topicsCache = data || [];
360                $loading.hide();
361                _renderAccordions(_topicsCache);
362                if (callback) callback();
363            }).fail(function () {
364                $loading.hide();
365                $grid.append('<p class="filter-loading" style="color:var(--color-text-secondary);">Failed to load topics. Please try again later.</p>');
366            }).always(function () {
367                _topicsLoading = false;
368            });
369        }
370
371        function _renderAccordions(topics) {
372            var $grid = $modal.find('.filter-accordion-grid');
373
374            $grid.find('.ui-accordion, .filter-loading').remove();
375
376            // 依 topicGroupName 分組
377            var groups = [];
378            var groupMap = {};
379            topics.forEach(function (t) {
380                var gName = t.topicGroupName || 'Other';
381                if (!groupMap[gName]) {
382                    groupMap[gName] = { name: gName, items: [] };
383                    groups.push(groupMap[gName]);
384                }
385                groupMap[gName].items.push(t);
386            });
387
388            groups.sort(function (a, b) { return a.name.localeCompare(b.name); });
389            var lang = ($('#hidGlobalLang').val() || 'en').toLowerCase();
390            groups.forEach(function (g) {
391                g.items.sort(function (a, b) {
392                    return _getTopicLabel(a).localeCompare(_getTopicLabel(b));
393                });
394            });
395
396            var col1Groups = [];
397            var col2Groups = [];
398            groups.forEach(function (g, i) {
399                if (i % 2 === 0) {
400                    col1Groups.push(g);
401                } else {
402                    col2Groups.push(g);
403                }
404            });
405
406            var $col1 = $('<div class="ui-accordion"></div>');
407            var $col2 = $('<div class="ui-accordion"></div>');
408
409            _buildColumn($col1, col1Groups);
410            _buildColumn($col2, col2Groups);
411
412            $grid.append($col1).append($col2);
413
414            // 初始化 Accordion 元件
415            $grid.find('.ui-accordion').each(function () {
416                new UIAccordion($(this));
417            });
418            filterInited = true;
419        }
420
421        function _buildColumn($col, groups) {
422            groups.forEach(function (group) {
423                var safeGroupName = $('<span>').text(group.name).html();
424                var html = '<div class="ui-accordion__item">' +
425                    '<button class="ui-accordion__trigger" aria-expanded="false">' +
426                    '<h4>' + safeGroupName + '</h4>' +
427                    '</button>' +
428                    '<div class="ui-accordion__panel">' +
429                    '<div class="ui-accordion__content">' +
430                    '<div class="ui-field-group">';
431
432                group.items.forEach(function (topic) {
433                    var label = _getTopicLabel(topic);
434                    var safeLabel = $('<span>').text(label).html();
435                    html += '<label class="ui-checkbox">' +
436                        '<input type="checkbox" class="ui-checkbox__input" data-topic-id="' + topic.topicId + '">' +
437                        '<span class="ui-checkbox__box"></span>' +
438                        '<span class="ui-checkbox__label">' + safeLabel + '</span>' +
439                        '</label>';
440                });
441
442                html += '</div></div></div></div>';
443                $col.append(html);
444            });
445        }
446
447        // 切換 Footer 模式
448        function _setFooterMode(mode) {
449            // mode: 'first' (步驟 1+2 流程) | 'revisit'
450            $modal.find('.filter-step-footer').removeClass('is-active');
451            if (mode === 'revisit') {
452                $modal.find('[data-step-footer="revisit"]').addClass('is-active');
453            } else {
454                $modal.find('[data-step-footer="1"]').addClass('is-active');
455            }
456        }
457
458        // 靜默關閉 modal(不觸發 coachmark)
459        function _closeModalSilently() {
460            var $m = $('#modal-filter');
461            $m.removeClass('is-open');
462            $m.one('transitionend', function () {
463                $m.css('display', 'none');
464            });
465        }
466
467        /* --- Accordion 初始化(延遲至 Modal 首次開啟,透過 API 動態載入) --- */
468        var filterInited = false;
469
470        function _onModalReady() {
471            // 每次開啟先重置 placeholder 警告狀態
472            $modal.find('.filter-focus__placeholder')
473                .text(_t('no-topics-selected', 'No topics selected yet.'))
474                .removeClass('is-error');
475
476            // 判斷首次 vs 回訪
477            if (_hasApplied) {
478                _isRevisit = true;
479                goToStep(1);
480                _setFooterMode('revisit');
481                _restoreSavedSelections(); // → rebuildFocusList → updateFocusCount
482            } else {
483                _isRevisit = false;
484                goToStep(1);
485                _setFooterMode('first');
486                rebuildFocusList();
487                _hasApplied = true;
488                localStorage.setItem(STORAGE_KEY_APPLIED, '1');
489            }
490        }
491
492        $(document).on('click', '[data-ui-modal-open="modal-filter"]', function () {
493            $('#preferences-toast').removeClass('is-visible');
494
495            if (typeof window.closeCoachmark === 'function') {
496                window.closeCoachmark();
497            }
498
499            if (!filterInited) {
500                _fetchAndRenderTopics(function () {
501                    _syncTagsFromServer(function () {
502                        _onModalReady();
503                    });
504                });
505            } else {
506                _onModalReady();
507            }
508        });
509
510        /* --- Mobile Tab 切換 --- */
511        $modal.on('click', '.filter-tabs__tab', function () {
512            var tab = $(this).data('tab');
513            $modal.find('.filter-tabs__tab').removeClass('is-active');
514            $(this).addClass('is-active');
515            $modal.find('.filter-grid').attr('data-active-tab', tab);
516        });
517
518        /* --- Collapse / Expand All --- */
519        function _syncCollapseAllBtn() {
520            var $btn = $modal.find('.filter-collapse-all');
521            var $visibleItems = $modal.find('.ui-accordion__item:visible');
522            var allExpanded = $visibleItems.length > 0 && $visibleItems.filter('.is-open').length === $visibleItems.length;
523
524            $btn.attr('aria-pressed', allExpanded ? 'true' : 'false');
525            $btn.find('.filter-collapse-all__label').text(allExpanded ? _t('collapse-
525all', 'Collapse All') : _t('expand-all', 'Expand All'));
526            $btn.find('i')
527                .removeClass('fa-chevron-up fa-chevron-down')
528                .addClass(allExpanded ? 'fa-chevron-up' : 'fa-chevron-down');
529        }
530
531        $modal.on('click', '.filter-collapse-all', function () {
532            var $btn = $(this);
533            var isExpanding = $btn.attr('aria-pressed') === 'false';
534
535            $modal.find('.ui-accordion__item:visible').each(function () {
536                var $item = $(this);
537                var $trigger = $item.find('.ui-accordion__trigger');
538                var $panel = $item.find('.ui-accordion__panel');
539
540                if (isExpanding) {
541                    $item.addClass('is-open');
542                    $panel.height($panel[0].scrollHeight);
543                    $trigger.attr('aria-expanded', 'true');
544                } else {
545                    $panel.height($panel[0].scrollHeight);
546                    requestAnimationFrame(function () {
547                        $item.removeClass('is-open');
548                        $panel.height(0);
549                    });
550                    $trigger.attr('aria-expanded', 'false');
551                }
552            });
553
554            $btn.attr('aria-pressed', isExpanding ? 'true' : 'false');
555            $btn.find('.filter-collapse-all__label').text(isExpanding ? _t('collapse-
555all', 'Collapse All') : _t('expand-all', 'Expand All'));
556            $btn.find('i')
557                .removeClass('fa-chevron-up fa-chevron-down')
558                .addClass(isExpanding ? 'fa-chevron-up' : 'fa-chevron-down');
559        });
560
561        /* --- 移除 chip --- */
562        $modal.on('click', '.ui-tag__remove', function () {
563            var $chip = $(this).closest('.ui-tag');
564            var topicId = $chip.attr('data-topic-id');
565            if (topicId) {
566                $modal.find('.ui-checkbox__input[data-topic-id="' + topicId + '"]').prop('checked', false);
567            } else {
568                var name = $chip.attr('data-filter-name');
569                $modal.find('.ui-checkbox').each(function () {
570                    if ($(this).find('.ui-checkbox__label').text().trim() === name) {
571                        $(this).find('.ui-checkbox__input').prop('checked', false);
572                    }
573                });
574            }
575            $chip.remove();
576            // 重新計算所有 L1 的 filter-count badge
577            $modal.find('.ui-accordion__item').each(function () {
578                var $item = $(this);
579                var count = $item.find('.ui-checkbox__input:checked').length;
580                var $badge = $item.find('.ui-accordion__trigger .filter-count');
581                if (count > 0) {
582                    if ($badge.length) { $badge.text(count); }
583            else { $item.find('.ui-accordion__trigger h4').append(' <span class="filter-count ui-badge ui-badge--soft">' + count + '</span>'); }
584                } else {
585                    $badge.remove();
586                }
587            });
588            updateFocusCount();
589        });
590
591        /* --- Clear All --- */
592        $modal.on('click', '.filter-focus__clear', function () {
593            $modal.find('.filter-focus__list .ui-tag').remove();
594            $modal.find('.ui-checkbox__input[data-topic-id]').prop('checked', false);
595            $modal.find('.ui-accordion__trigger .filter-count').remove();
596            updateFocusCount();
597        });
598
599        /* --- Topic Checkbox 變更 → 重建 chips --- */
600        $modal.on('change', '.ui-checkbox__input[data-topic-id]', function () {
601            rebuildFocusList();
602        });
603
604        function rebuildFocusList() {
605            var $list = $modal.find('.filter-focus__list');
606            $list.find('.ui-tag').remove();
607
608            // 收集所有勾選項目及其 L1 (category) / L2 (name)
609            var checkedItems = [];
610            $modal.find('.ui-accordion__item').each(function () {
611                var $item = $(this);
612                var category = $.trim($item.find('.ui-accordion__trigger h4').contents().filter(function () { return this.nodeType === 3; }).text());
613                var count = 0;
614                $item.find('.ui-checkbox__input:checked').each(function () {
615                    var $cb = $(this);
616                    checkedItems.push({
617                        category: category,
618                        topicId: $cb.attr('data-topic-id') || '',
619                        name: $.trim($cb.closest('.ui-checkbox').find('.ui-checkbox__label').text())
620                    });
621                    count++;
622                });
623                var $badge = $item.find('.ui-accordion__trigger .filter-count');
624                if (count > 0) {
625                    if ($badge.length) { $badge.text(count); }
626                    // 0831:計數 badge 改吃元件庫的 ui-badge,filter-count 保留當 JS/CSS 的鉤子
627            else { $item.find('.ui-accordion__trigger h4').append(' <span class="filter-count ui-badge ui-badge--soft">' + count + '</span>'); }
628                } else {
629                    $badge.remove();
630                }
631            });
632
633            // 依 L1 字母排序,相同 L1 再依 L2 字母排序
634            checkedItems.sort(function (a, b) {
635                var cmp = a.category.localeCompare(b.category);
636                if (cmp !== 0) return cmp;
637                return a.name.localeCompare(b.name);
638            });
639
640            // 依排序結果渲染 chips
641            checkedItems.forEach(function (item) {
642                var safeName = $('<span>').text(item.name).html();
643                var safeCat = $('<span>').text(item.category).html();
644                var chip = '<div class="ui-tag" data-filter-name="' + safeName + '" data-topic-id="' + item.topicId + '">' +
645                    '<span class="ui-tag__text">' +
646                    '<span class="ui-tag__category">' + safeCat + '</span>' +
647                    '<span class="ui-tag__name">' + safeName + '</span>' +
648                    '</span>' +
649                    '<button type="button" class="ui-tag__remove" aria-label="Remove"><i class="fa-regular fa-xmark" aria-hidden="true"></i></button>' +
650                    '</div>';
651                $list.append(chip);
652            });
653
654            updateFocusCount();
655        }
656
657        function updateFocusCount() {
658            var n = $modal.find('.filter-focus__list .ui-tag').length;
659
660            var $placeholder = $modal.find('.filter-focus__placeholder');
661            if (n === 0) {
662                $placeholder.text(_t('no-topics-selected', 'No topics selected yet.')).removeClass('is-error').show();
663            } else {
664                $placeholder.hide();
665            }
666
667            // Clear All:有項目時顯示,無項目時隱藏
668            $modal.find('.filter-focus__clear').css('display', n > 0 ? '' : 'none');
669
670            var $tabBadges = $modal.find('[data-focus-count-tab]');
671            $tabBadges.text(n);
672            $tabBadges.css('display', n > 0 ? 'inline-flex' : 'none');
673
674            var $sidebarBadges = $modal.find('[data-focus-count]');
675            $sidebarBadges.text(n);
676            $sidebarBadges.css('visibility', n > 0 ? 'visible' : 'hidden');
677        }
678
679        /* --- Step 切換 --- */
680        function goToStep(stepNum) {
681            $modal.find('.filter-step').removeClass('is-active');
682            $modal.find('.filter-step[data-step="' + stepNum + '"]').addClass('is-active');
683            $modal.find('.filter-step-footer').removeClass('is-active');
684            $modal.find('.filter-step-footer[data-step-footer="' + stepNum + '"]').addClass('is-active');
685        }
686
687
688        /* --- 搜尋列過濾 --- */
689
690        var _accordionStateBeforeSearch = null;
691
692        function _saveAccordionState() {
693            if (_accordionStateBeforeSearch) return; 
694            _accordionStateBeforeSearch = [];
695            $modal.find('.ui-accordion__item').each(function () {
696                _accordionStateBeforeSearch.push($(this).hasClass('is-open'));
697            });
698            _flattenForSearch();
699        }
700
701        function _restoreAccordionState() {
702            if (!_accordionStateBeforeSearch) return;
703            $modal.find('.ui-accordion__item').each(function (i) {
704                var $item = $(this);
705                var $panel = $item.find('.ui-accordion__panel');
706                var $trigger = $item.find('.ui-accordion__trigger');
707                var wasOpen = _accordionStateBeforeSearch[i] || false;
708
709                $item.show();
710                $item.find('.ui-field-group .ui-checkbox').show();
711
712                if (wasOpen) {
713                    $item.addClass('is-open');
714                    $panel.css('height', 'auto');
715                    $panel.height($panel[0].scrollHeight);
716                    $trigger.attr('aria-expanded', 'true');
717                } else {
718                    $item.removeClass('is-open');
719                    $panel.height(0);
720                    $trigger.attr('aria-expanded', 'false');
721                }
722            });
723            _accordionStateBeforeSearch = null;
724        }
725
726        function _expandItem($item) {
727            var $panel = $item.find('.ui-accordion__panel');
728            var $trigger = $item.find('.ui-accordion__trigger');
729            $item.addClass('is-open');
730            $panel.css('height', 'auto');
731            var h = $panel[0].scrollHeight;
732            $panel.height(h);
733            $trigger.attr('aria-expanded', 'true');
734        }
735
736        function _collapseItem($item) {
737            var $panel = $item.find('.ui-accordion__panel');
738            var $trigger = $item.find('.ui-accordion__trigger');
739            $item.removeClass('is-open');
740            $panel.height(0);
741            $trigger.attr('aria-expanded', 'false');
742        }
743
744        function _flattenForSearch() {
745            var $grid = $modal.find('.filter-accordion-grid');
746            var $accordions = $grid.find('.ui-accordion');
747            if ($accordions.length < 2) return;
748
749            var $col1 = $accordions.eq(0);
750            var allItems = $grid.find('.ui-accordion__item').toArray();
751
752            // 依 L1 header A-Z 排序
753            allItems.sort(function (a, b) {
754                var nameA = $(a).find('.ui-accordion__trigger h4').text().trim();
755                var nameB = $(b).find('.ui-accordion__trigger h4').text().trim();
756                return nameA.localeCompare(nameB);
757            });
758
759            // 全部移至 col1(col2 變空)
760            allItems.forEach(function (item) {
761                $col1.append(item);
762            });
763        }
764
765        $modal.on('input', '.filter-search .ui-input', function () {
766            var keyword = $(this).val().toLowerCase().trim();
767            var $clearBtn = $modal.find('.filter-search .ui-input-group__clear');
768            var $noResults = $modal.find('.filter-no-results');
769            var $grid = $modal.find('.filter-accordion-grid');
770
771            // 清除按鈕:有文字時顯示,無文字時隱藏
772            $clearBtn.prop('hidden', keyword.length === 0);
773
774            // 無關鍵字 → 從快取重建雙欄佈局(保留勾選狀態)
775            if (!keyword) {
776                var currentChecked = _getCurrentSelections();
777                _renderAccordions(_topicsCache);
778                currentChecked.forEach(function (id) {
779                    $modal.find('.ui-checkbox__input[data-topic-id="' + id + '"]').prop('checked', true);
780                });
781                rebuildFocusList();
782                _accordionStateBeforeSearch = null;
783                $noResults.hide();
784                $grid.show();
785                $grid.removeClass('search-mode');
786                _syncCollapseAllBtn();
787                return;
788            }
789
790            // 首次輸入時儲存展開狀態,並啟用搜尋模式(item 自動左右排列)
791            _saveAccordionState();
792            $grid.addClass('search-mode');
793
794            // 先確保 grid 可見,否則 scrollHeight 在 display:none 下為 0
795            $noResults.hide();
796            $grid.show();
797
798            var totalMatch = false;
799
800            $modal.find('.ui-accordion__item').each(function () {
801                var $item = $(this);
802                var categoryText = $item.find('.ui-accordion__trigger h4').text().toLowerCase();
803                var hasMatch = false;
804
805                // 只依 L2 文字過濾:L1 名稱不決定是否顯示全部 L2
806                    $item.find('.ui-field-group .ui-checkbox').each(function () {
807                        var text = $(this).text().toLowerCase();
808                        if (text.indexOf(keyword) > -1) {
809                            $(this).show();
810                            hasMatch = true;
811                        } else {
812                            $(this).hide();
813                        }
814                    });
815
816                    if (hasMatch) {
817                        $item.show();
818                        _expandItem($item);
819                    } else {
820                        $item.hide();
821                        _collapseItem($item);
822                    }
823
824                if (hasMatch) totalMatch = true;
825            });
826
827            // Rule 3:全部都不匹配時顯示引導訊息
828            if (!totalMatch) {
829                $grid.hide();
830                $noResults.show();
831            }
832
833            _syncCollapseAllBtn();
834        });
835
836        /* --- 搜尋框清除按鈕 --- */
837        $modal.on('click', '.filter-search .ui-input-group__clear', function () {
838            var $input = $modal.find('.filter-search .ui-input');
839            $input.val('').trigger('input');
840            $input.focus();
841        });
842
843        /* =============================================================
844           步驟三:飛行動畫 + Tooltip 指向提示
845           source: 'apply' | 'skip' | 'close'
846        ============================================================= */
847
848        // Tooltip 文案依觸發來源
849        var _coachmarkContent = {
850            apply: {
851                title: _t('saved-title', 'Your preferences are saved!'),
852                desc:  _t('saved-desc', 'Click Customize View anytime to update your preferences.')
853            },
854            skip: {
855                title: _t('coachmark-title', 'Want to customize?'),
856                desc:  _t('coachmark-desc', 'You can personalize your experience anytime by clicking the Customize View button.')
857            },
858            close: {
859                title: _t('coachmark-title', 'Want to customize?'),
860                desc:  _t('coachmark-desc', 'You can personalize your experience anytime by clicking the Customize View button.')
861            }
862        };
863
864        function _closeModalAndShowCoachmark(source) {
865            var headerBtn = document.getElementById('header-customize');
866            if (!headerBtn || headerBtn.getClientRects().length === 0) {
867                _closeModalSilently();
868                if (source === 'apply') window.showPreferencesToast();
869                return;
870            }
871
872            window._coachmarkSource = source || 'close';
873
874            var $m = $('#modal-filter');
875            $m.removeClass('is-open');
876            $m.one('transitionend', function () {
877                $m.css('display', 'none');
878            });
879
880            var content = _coachmarkContent[window._coachmarkSource] || _coachmarkContent.close;
881            $('#customize-coachmark').find('.customize-coachmark__title').text(content.title);
882            $('#customize-coachmark').find('.customize-coachmark__desc').text(content.desc);
883
884            _startFlyAnimation();
885        }
886
887        function _startFlyAnimation() {
888            var $flyIcon = $('#coachmark-fly-icon');
889            var $overlay = $('#coachmark-overlay');
890            var $headerBtn = $('#header-customize');
891
892            window.scrollTo({ top: 0, behavior: 'smooth' });
893
894            var btnRect = $headerBtn[0].getBoundingClientRect();
895            var targetX = btnRect.left + btnRect.width / 2;
896            var targetY = btnRect.top + btnRect.height / 2;
897
898            var startX = window.innerWidth / 2;
899            var startY = window.innerHeight / 2;
900
901            $flyIcon.css({
902                left: startX + 'px',
903                top: startY + 'px',
904                transform: 'translate(-50%, -50%) scale(0)',
905                opacity: 0
906            });
907
908            $overlay.addClass('is-visible');
909
910            requestAnimationFrame(function () {
911                $flyIcon.css({
912                    transition: 'opacity 0.2s ease, transform 0.3s ease',
913                    opacity: 1,
914                    transform: 'translate(-50%, -50%) scale(1.3)'
915                });
916
917                setTimeout(function () {
918                    $flyIcon.css({
919                        transition: 'left 0.6s cubic-bezier(0.4,0,0.2,1), top 0.6s cubic-bezier(0.4,0,0.2,1), transform 0.6s cubic-bezier(0.4,0,0.2,1), opacity 0.5s ease 0.3s',
920                        left: targetX + 'px',
921                        top: targetY + 'px',
922                        transform: 'translate(-50%, -50%) scale(0.3)',
923                        opacity: 0
924                    });
925
926                    setTimeout(function () {
927                        $flyIcon.css({ transition: 'none', opacity: 0, transform: 'scale(0)' });
928                        _startCoachmarkTooltip();
929                    }, 650);
930                }, 350);
931            });
932        }
933
934        function _startCoachmarkTooltip() {
935            var $overlay = $('#coachmark-overlay');
936            var $headerCustomize = $('#header-customize');
937            var $coachmark = $('#customize-coachmark');
938
939            $headerCustomize.addClass('coachmark-active');
940            $headerCustomize.find('a, i').css('pointer-events', 'none');
941
942            $coachmark.removeClass('is-visible border-spin');
943            void $coachmark[0].offsetWidth;
944
945            requestAnimationFrame(function () {
946                $coachmark.addClass('is-visible');
947                _triggerBorderSpin();
948            });
949        }
950
951        function _triggerBorderSpin() {
952            var $coachmark = $('#customize-coachmark');
953            $coachmark.removeClass('border-spin');
954            void $coachmark[0].offsetWidth;
955            $coachmark.addClass('border-spin');
956            $coachmark.one('animationend', function () {
957                $coachmark.removeClass('border-spin');
958                if ($coachmark.hasClass('is-visible')) {
959                    window._coachmarkSpinTimer = setTimeout(_triggerBorderSpin, 3000);
960                }
961            });
962        }
963        window._triggerBorderSpin = _triggerBorderSpin;
964
965        $('#btn-apply-preferences').on('click', function () {
966            var hasChecked = $modal.find('.ui-checkbox__input[data-topic-id]:checked').length > 0;
967
968            if (!hasChecked) {
969                var $placeholder = $modal.find('.filter-focus__placeholder');
970                $placeholder.text(_t('select-at-least-one', 'Please select at least one option.')).addClass('is-error').show();
971                $placeholder[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
972                return;
973            }
974
975            _savedSelections = _getCurrentSelections();
976            _hasApplied = true;
977            _persistPreferences().then(function (ok) {
978                if (ok) _closeModalAndShowCoachmark('apply');
979            });
980        });
981
982        /* --- 回訪 Done 按鈕:儲存變更;空選擇恢復預設首頁 --- */
983        $modal.on('click', '#btn-revisit-done', function () {
984            var current = _getCurrentSelections();
985            var changed = _selectionsChanged(current, _savedSelections);
986
987            if (changed) {
988                // 有變更 → 儲存 → 步驟四成功通知
989                _savedSelections = current;
990                _persistPreferences().then(function (ok) {
991                    if (ok) { _closeModalSilently(); window.showPreferencesToast(); }
992                });
993            } else {
994                // 無變更 → 靜默關閉
995                _closeModalSilently();
996            }
997        });
998
999        /* --- 回訪 Cancel 按鈕:不儲存 → 靜默關閉 --- */
1000        $modal.on('click', '#btn-revisit-cancel', function () {
1001            _closeModalSilently();
1002        });
1003
1004        /* --- 勾選任何 topic checkbox 時清除紅字錯誤 --- */
1005        $modal.on('change', '.ui-checkbox__input[data-topic-id]', function () {
1006            if ($modal.find('.ui-checkbox__input[data-topic-id]:checked').length > 0) {
1007                $modal.find('.filter-focus__error').hide();
1008            }
1009        });
1010
1011        /* --- Skip 按鈕(只在首次出現):不儲存 → 關閉 → 步驟三(skip) --- */
1012        $modal.on('click', '#btn-skip', function () {
1013            _closeModalAndShowCoachmark('skip');
1014        });
1015
1016        /* --- X 關閉按鈕:首次 → 不儲存 → 關閉 → 步驟三(close);回訪 → 靜默關閉(同 Cancel) --- */
1017        $modal.on('click', '.ui-modal__close', function (e) {
1018            e.stopPropagation();
1019            if (_isRevisit) _closeModalSilently();
1020            else _closeModalAndShowCoachmark('close');
1021        });
1022
1023    });
1024
1025    /* =============================================
1026       closeCoachmark + Got it(全域)
1027    ============================================= */
1028    window._coachmarkSpinTimer = null;
1029    window._coachmarkSource = 'close';  
1030
1031    window.closeCoachmark = function () {
1032        var coachmark = document.getElementById('customize-coachmark');
1033        var overlay = document.getElementById('coachmark-overlay');
1034        var headerBtn = document.getElementById('header-customize');
1035        if (window._coachmarkSpinTimer) {
1036            clearTimeout(window._coachmarkSpinTimer);
1037            window._coachmarkSpinTimer = null;
1038        }
1039        if (coachmark) coachmark.classList.remove('is-visible', 'border-spin');
1040        if (overlay) overlay.classList.remove('is-visible');
1041        if (headerBtn) {
1042            headerBtn.classList.remove('coachmark-active');
1043            // 恢復 Customize View 連結的點擊
1044            $(headerBtn).find('a, i').css('pointer-events', '');
1045        }
1046    };
1047
1048    // Got it 按鈕的 handler(從 inline onclick 改為此處統一管理)
1049    window.handleCoachmarkGotIt = function () {
1050        window.closeCoachmark();
1051
1052        // 若來自 Apply → 顯示步驟四 Preferences Applied toast
1053        if (window._coachmarkSource === 'apply') {
1054            window.showPreferencesToast();
1055        }
1056    };
1057
1058    // 步驟四:顯示 Preferences Applied toast,5 秒後自動消失
1059    window.showPreferencesToast = function () {
1060        var $toast = $('#preferences-toast');
1061        $toast.addClass('is-visible');
1062        setTimeout(function () {
1063            $toast.removeClass('is-visible');
1064        }, 5000);
1065    };
1066
1067    /* 點擊遮罩不關閉 coachmark — 僅允許 Got it 按鈕關閉 */
1068    $(function () {
1069        $('#coachmark-overlay').on('click', function (e) {
1070            e.stopPropagation();
1071            // 不做任何事,強制使用者點 Got it
1072        });
1073    });
1074
1075    /* 供 Personalize Toast 關閉時呼叫(在 jQuery IIFE 外部無法存取 closure) */
1076    window.triggerCoachmarkFromToast = function () {
1077        $(function () {
1078            var content = {
1079                title: _t('coachmark-title', 'Want to customize?'),
1080                desc: _t('coachmark-desc', 'You can personalize your experience anytime by clicking the Customize View button.')
1081            };
1082            $('#customize-coachmark').find('.customize-coachmark__title').text(content.title);
1083            $('#customize-coachmark').find('.customize-coachmark__desc').text(content.desc);
1084            window._coachmarkSource = 'close';
1085
1086            var $overlay = $('#coachmark-overlay');
1087            var $headerCustomize = $('#header-customize');
1088            $headerCustomize.addClass('coachmark-active');
1089            $headerCustomize.find('a, i').css('pointer-events', 'none');
1090            $overlay.addClass('is-visible');
1091
1092            var $coachmark = $('#customize-coachmark');
1093            $coachmark.removeClass('is-visible border-spin');
1094            void $coachmark[0].offsetWidth;
1095            $coachmark.addClass('is-visible');
1096
1097            // 只有邊框旋轉動畫需要等下一個 frame 才重新啟動(動畫 restart 的標準做法)。
1098            requestAnimationFrame(function () {
1099                if (window._triggerBorderSpin) {
1100                    window._triggerBorderSpin();
1101                }
1102            });
1103        });
1104    };
1105
1106})(jQuery);
1107
1108/* =============================================
1109   Personalize Toast 顯示條件控制
1110   條件:CookieYes functional+analytics 同意、
1111         未曾關閉過、未曾進入 filter、filter 未開啟
1112============================================= */
1113(function () {
1114    'use strict';
1115
1116    var TOAST_DISMISSED_KEY = 'adv_personalize_toast_dismissed';
1117    var FILTER_VISITED_KEY = 'adv_filter_visited';
1118    var STORAGE_KEY_APPLIED = 'adv_filter_applied';
1119    var COOKIEYES_COOKIE = 'cookieyes-consent';
1120    var COOKIEYES_EVENT = 'cookieyes_consent_update';
1121
1122    /* --- 讀取 CookieYes cookie 中指定 key 的值 --- */
1123    function getCookieYesValue(key) {
1124        var found = document.cookie.split(';').map(function (c) {
1125            return c.trim();
1126        }).find(function (c) {
1127            return c.indexOf(COOKIEYES_COOKIE + '=') === 0;
1128        });
1129        if (!found) return undefined;
1130        var raw = decodeURIComponent(found.split('=').slice(1).join('='));
1131        var obj = {};
1132        raw.split(',').forEach(function (pair) {
1133            var kv = pair.split(':');
1134            if (kv.length === 2) obj[kv[0].trim()] = kv[1].trim();
1135        });
1136        return obj[key];
1137    }
1138
1139    /* --- 條件 1:functional 且 analytics 均同意 --- */
1140    function isConsentGranted() {
1141        return getCookieYesValue('functional') === 'yes' &&
1142            getCookieYesValue('analytics') === 'yes';
1143    }
1144
1145    /* =============================================
1146       Cookie Consent:Customize View 按鈕顯示/隱藏
1147    ============================================= */
1148    function isCustomizeAllowed() {
1149        return isConsentGranted();
1150    }
1151
1152    function updateCustomizeVisibility() {
1153        var btn = document.getElementById('header-customize');
1154        if (!btn) return;
1155        if (isCustomizeAllowed()) {
1156            btn.style.display = '';
1157        } else {
1158            btn.style.display = 'none';
1159            var modal = document.getElementById('modal-filter');
1160            if (modal && modal.classList.contains('is-open')) {
1161                modal.classList.remove('is-open');
1162                modal.style.display = 'none';
1163            }
1164        }
1165    }
1166
1167    // 公開 API:供其他模組查詢
1168    window.AdvCookieConsent = {
1169        getCookieYesValue: getCookieYesValue,
1170        isConsentGranted:  isConsentGranted,
1171        isCustomizeAllowed: isCustomizeAllowed,
1172        refresh: function () { maybeShowToast(); },
1173        holdToast: function (ms) { holdToast(ms); },
1174        releaseToast: function () { releaseToast(); }
1175    };
1176
1177    /* --- 綜合判斷是否應顯示 personalize toast --- */
1178    function shouldShowToast() {
1179        if (_toastHeld) return false;                                   // 正在等 MA 回應,先不要跳
1180        if (!isCustomizeAllowed()) return false;                        // consent 未同意
1181        if (localStorage.getItem(TOAST_DISMISSED_KEY)) return false;    // 已關閉過(按 X)
1182        if (localStorage.getItem(FILTER_VISITED_KEY)) return false;     // 已進入過 filter
1183        if (localStorage.getItem(STORAGE_KEY_APPLIED)) return false;    // 已 Apply 過(選過偏好)
1184        var modal = document.getElementById('modal-filter');
1185        if (modal && modal.classList.contains('is-open')) return false;
1185 // filter 開啟中
1186        return true;
1187    }
1188
1189    function maybeShowToast() {
1190        var toast = document.getElementById('personalize-toast');
1191        if (!toast) return;
1192        toast.style.display = shouldShowToast() ? 'flex' : 'none';
1193        updateCustomizeVisibility();
1194    }
1195
1196
1197    var HOLD_MAX_MS = 2500;
1198    var _toastHeld = false;
1199    var _toastHoldTimer = null;
1200
1201    function holdToast(ms) {
1202        _toastHeld = true;
1203        var toast = document.getElementById('personalize-toast');
1204        if (toast) toast.style.display = 'none';
1205        if (_toastHoldTimer) clearTimeout(_toastHoldTimer);
1206        _toastHoldTimer = setTimeout(releaseToast, ms || HOLD_MAX_MS);
1207    }
1208
1209    function releaseToast() {
1210        if (_toastHoldTimer) { clearTimeout(_toastHoldTimer); _toastHoldTimer = null; }
1211        if (!_toastHeld) return;
1212        _toastHeld = false;
1213        maybeShowToast();
1214    }
1215
1216    function _scrollToTop() {
1217        var reduceMotion = window.matchMedia &&
1218            window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1219        try {
1220            window.scrollTo({ top: 0, left: 0, behavior: reduceMotion ? 'instant' : 'smooth' });
1221        } catch (e) {
1222            window.scrollTo(0, 0);
1223        }
1224    }
1225
1226    /* --- 關閉按鈕:記錄「已關閉」→ 永不再顯示 → 先出遮罩與對話框,再平滑捲頂 --- */
1227    document.addEventListener('click', function (e) {
1228        if (e.target.closest('#personalize-toast .personalize-toast__close')) {
1229            localStorage.setItem(TOAST_DISMISSED_KEY, '1');
1230            var toast = document.getElementById('personalize-toast');
1231            if (toast) toast.style.display = 'none';
1232
1233            if (typeof window.triggerCoachmarkFromToast === 'function') {
1234                window.triggerCoachmarkFromToast();
1235            }
1236            _scrollToTop();
1237        }
1238    });
1239
1240    /* --- 開啟 filter modal(任何入口):記錄「已進入」並立即隱藏 toast --- */
1241    document.addEventListener('click', function (e) {
1242        if (e.target.closest('[data-ui-modal-open="modal-filter"]')) {
1243            localStorage.setItem(FILTER_VISITED_KEY, '1');
1244            var toast = document.getElementById('personalize-toast');
1245            if (toast) toast.style.display = 'none';
1246            // 同時隱藏 Preferences Applied toast
1247            var prefToast = document.getElementById('preferences-toast');
1248            if (prefToast) prefToast.classList.remove('is-visible');
1249        }
1250    });
1251
1252    /* --- 監聽 CookieYes 同意變更事件(新訪客同意後立即觸發) --- */
1253    document.addEventListener(COOKIEYES_EVENT, maybeShowToast);
1254    window.addEventListener(COOKIEYES_EVENT, maybeShowToast);
1255
1256    /* --- 頁面載入時判斷(回訪者已有 consent cookie) --- */
1257    if (document.readyState === 'loading') {
1258        document.addEventListener('DOMContentLoaded', maybeShowToast);
1259    } else {
1260        maybeShowToast();
1261    }
1262
1263}());

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.