PageSourceSearch

https://sere.plus/js/sidebar.js?v=20260919-navigation-help

js sere.plus collected 2026-10-09 17:22:04 UTC 7,477 bytes, 200 lines download raw bytes

1(function (root, factory) {
2    if (typeof module === 'object' && module.exports) {
3        module.exports = factory();
4    } else {
5        root.sereSidebar = factory();
6    }
7})(typeof window !== 'undefined' ? window : globalThis, function () {
8    const COLLECTION_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9    const ALLOWED_VIEWS = new Set(['home-view', 'station-list-view']);
10    const ALLOWED_TYPES = new Set(['view', 'category', 'collection', 'section', 'divider', 'action']);
11    const ALLOWED_ACTIONS = new Set(['about', 'settings', 'help']);
12    const DEFAULT_UTILITY_ITEMS = [
13        { type: 'action', label: 'About SERE+', icon: 'info-circle', action: 'about' },
14        { type: 'action', label: 'Settings', icon: 'cog', action: 'settings' },
15        { type: 'action', label: 'Help & Support', icon: 'question-circle', action: 'help' }
16    ];
17
18    function defaultSidebarItems(collections) {
19        const items = [
20            { type: 'view', label: 'Home', icon: 'home', view: 'home-view' },
21            { type: 'view', label: 'All Stations', icon: 'broadcast-tower', view: 'station-list-view' },
22            { type: 'section', label: 'Categories', icon: '' },
23            { type: 'category', label: 'Music', icon: 'music', filter: 'Music' },
24            { type: 'category', label: 'Talk', icon: 'microphone', filter: 'Talk' },
25            { type: 'category', label: 'News', icon: 'newspaper', filter: 'News' },
26            { type: 'category', label: 'Sports', icon: 'futbol', filter: 'Sports' }
27        ];
28
29        if (Array.isArray(collections)) {
30            collections.forEach(collection => {
31                if (!collection || !COLLECTION_ID_PATTERN.test(String(collection.id || ''))) {
32                    return;
33                }
34
35                const title = String(collection.title || '').trim();
36                if (title === '') {
37                    return;
38                }
39
40                items.push({
41                    type: 'collection',
42                    label: title,
43                    icon: 'users',
44                    collection_id: String(collection.id)
45                });
46            });
47        }
48
49        items.push({ type: 'divider', label: '', icon: '' }, ...DEFAULT_UTILITY_ITEMS);
50
51        return items;
52    }
53
54    function normalizeSidebarItems(items, collections) {
55        const availableCollections = new Set(
56            Array.isArray(collections)
57                ? collections
58                    .filter(collection => collection && COLLECTION_ID_PATTERN.test(String(collection.id || '')))
59                    .map(collection => String(collection.id))
60                : []
61        );
62        const source = Array.isArray(items) && items.length > 0
63            ? items
64            : defaultSidebarItems(collections);
65
66        const normalized = source
67            .filter(item => item && item.enabled !== false)
68            .map((item, index) => normalizeSidebarItem(item, index, availableCollections))
69            .filter(Boolean)
70            .sort((left, right) => left.sort_order - right.sort_order);
71
72        if (normalized.length === 0) {
73            return defaultSidebarItems(collections);
74        }
75
76        return appendMissingUtilityItems(normalized, source);
77    }
78
79    function normalizeSidebarItem(item, index, availableCollections) {
80        const type = String(item.type || '').trim().toLowerCase();
81        if (!ALLOWED_TYPES.has(type)) {
82            return null;
83        }
84
85        const sortOrder = Number.isFinite(Number(item.sort_order))
86            ? Number(item.sort_order)
87            : index * 10;
88
89        if (type === 'divider') {
90            return { type, label: '', icon: '', sort_order: sortOrder };
91        }
92
93        const label = String(item.label || '').trim();
94        if (label === '') {
95            return null;
96        }
97
98        const base = {
99            type,
100            label,
101            icon: cleanIcon(item.icon),
102            sort_order: sortOrder
103        };
104
105        if (type === 'section') {
106            return base;
107        }
108
109        if (type === 'view') {
110            const view = String(item.view || '').trim();
111            return ALLOWED_VIEWS.has(view) ? { ...base, view } : null;
112        }
113
114        if (type === 'category') {
115            const filter = String(item.filter || label).trim();
116            return filter !== '' ? { ...base, filter } : null;
117        }
118
119        if (type === 'action') {
120            const action = String(item.action || '').trim().toLowerCase();
121            return ALLOWED_ACTIONS.has(action) ? { ...base, action } : null;
122        }
123
124        const collectionId = String(item.collection_id || '').trim();
125        if (!availableCollections.has(collectionId)) {
126            return null;
127        }
128
129        return COLLECTION_ID_PATTERN.test(collectionId)
130            ? { ...base, collection_id: collectionId }
131            : null;
132    }
133
134    function cleanIcon(icon) {
135        const value = String(icon || '').trim().toLowerCase();
136        return /^[a-z0-9-]{0,40}$/.test(value) ? value : '';
137    }
138
139    function appendMissingUtilityItems(items, source) {
140        const presentActions = new Set(
141            items
142                .filter(item => item.type === 'action')
143                .map(item => item.action)
144        );
145        const disabledActions = disabledUtilityActions(source);
146        const missingActions = DEFAULT_UTILITY_ITEMS
147            .filter(item => !presentActions.has(item.action) && !disabledActions.has(item.action));
148
149        if (missingActions.length === 0) {
150            return items;
151        }
152
153        const restored = [...items];
154        let sortOrder = restored.reduce((max, item) => Math.max(max, Number(item.sort_order) || 0), 0) + 10;
155
156        if (restored.length > 0 && restored[restored.length - 1].type !== 'divider') {
157            restored.push({ type: 'divider', label: '', icon: '', sort_order: sortOrder });
158            sortOrder += 10;
159        }
160
161        missingActions.forEach(item => {
162            restored.push({ ...item, sort_order: sortOrder });
163            sortOrder += 10;
164        });
165
166        return restored;
167    }
168
169    function disabledUtilityActions(source) {
170        if (!Array.isArray(source)) {
171            return new Set();
172        }
173
174        return new Set(
175            source
176                .filter(item => item && item.enabled === false && String(item.type || '').toLowerCase() === 'action')
177                .map(item => String(item.action || '').trim().toLowerCase())
178                .filter(action => ALLOWED_ACTIONS.has(action))
179        );
180    }
181
182    function groupSidebarItems(items) {
183        const primary = ['home-view', 'station-list-view'].map(view =>
184            items.find(item => item.type === 'view' && item.view === view)
185                || defaultSidebarItems([]).find(item => item.type === 'view' && item.view === view)
186        );
187        const heading = items.find(item => item.type === 'section')
188            || { type: 'section', label: 'Categories', icon: '' };
189        const categories = items.filter(item => item.type !== 'view' && item.type !== 'action' && item !== heading);
190        while (categories[0]?.type === 'divider') categories.shift();
191        while (categories[categories.length - 1]?.type === 'divider') categories.pop();
192        return { primary, heading, categories, utilities: items.filter(item => item.type === 'action') };
193    }
194
195    return {
196        groupSidebarItems,
197        defaultSidebarItems,
198        normalizeSidebarItems
199    };
200});

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.