PageSourceSearch

https://somersetnlv.org/apps-shared/js/responsive_table/script.js

js somersetnlv.org collected 2026-10-06 06:46:31 UTC 18,185 bytes, 509 lines download raw bytes

1// Utility: Debounce function to limit event handler calls
2function debounce(func, wait = 150) {
3    let timeout;
4    return function executedFunction(...args) {
5        const later = () => {
6            clearTimeout(timeout);
7            func.apply(this, args);
8        };
9        clearTimeout(timeout);
10        timeout = setTimeout(later, wait);
11    };
12}
13
14// Constants for table attributes and configuration
15const RESPONSIVE_TABLE = {
16    BREAKPOINT: 'en-responsive-table-breakpoint',
17    MOBILE_BREAKPOINT: 'en-responsive-table-mobile-breakpoint',
18    INITIALIZED: 'data-rt-initialized',
19    HEADER_SELECTOR: 'header-selector'
20};
21
22// Default breakpoint values and validation ranges
23const DEFAULT_BREAKPOINTS = {
24    TABLET: 750,
25    MOBILE: 450,
26    MIN: 300,
27    MAX: 1100
28};
29
30class ResponsiveTableBuilder {
31
32    TABLET_BREAK_POINT = DEFAULT_BREAKPOINTS.TABLET;
33    MOBILE_BREAK_POINT = DEFAULT_BREAKPOINTS.MOBILE;
34
35    fromSelector() {
36
37        if (!arguments || arguments.length === 0) {
38            return;
39        }
40
41        Array.from(arguments).forEach(arg => {
42
43            if (!arg.selectorList || arg.selectorList.length === 0) {
44                return;
45            }
46            let tabletBreakPoint = null;
47            let mobileBreakPoint = null;
48            let firstRowHeader = arg.firstRowHeader || false;
49            let headerSelector = arg.headerSelector || null;
50
51            // this logic is related backward compatibility
52            // Previous version contains only 1 mod which is tablet-view and called mobileBreakPoint
53            // This version contains 2 version which are tablet view(tabletBreakPoint) and mobile view(mobileBreakPoint).
54            // If there is no tablet view value, we accept mobile breakpoint as tablet View and assign the value as tabletBreakPoint
55            if (arg.tabletBreakPoint && arg.mobileBreakPoint) {
56                tabletBreakPoint = arg.tabletBreakPoint;
57                mobileBreakPoint = arg.mobileBreakPoint;
58            } else if (!arg.tabletBreakPoint && arg.mobileBreakPoint) {
59                tabletBreakPoint = arg.mobileBreakPoint;
60            }
61            arg.selectorList.forEach((selector) => {
62                const tableList = document.querySelectorAll(selector);
63                tableList.forEach(table => {
64                    this.fromTable({table, tabletBreakPoint, mobileBreakPoint, firstRowHeader, headerSelector});
65                })
66            })
67
68        })
69        console.log(arguments);
70    }
71
72    fromTable({table, tabletBreakPoint, mobileBreakPoint, firstRowHeader, headerSelector}) {
73        tabletBreakPoint = this.validateTabletViewBreakPoint(tabletBreakPoint)
74        mobileBreakPoint = this.validateMobileViewBreakPoint(mobileBreakPoint);
75
76        // Apply firstRowHeader attribute if specified
77        if (firstRowHeader && !table.hasAttribute('first-row-header')) {
78            table.setAttribute('first-row-header', '');
79        }
80
81        // Apply headerSelector attribute if specified - use HEADER_SELECTOR constant for table-level attribute
82        if (headerSelector && !table.hasAttribute(RESPONSIVE_TABLE.HEADER_SELECTOR)) {
83            table.setAttribute(RESPONSIVE_TABLE.HEADER_SELECTOR, headerSelector);
84        }
85
86        new ResponsiveTable({table, tabletBreakPoint, mobileBreakPoint})
87        this.loadCSS(tabletBreakPoint, mobileBreakPoint);
88    }
89
90    validateBreakPoint(value, defaultValue) {
91        if (value === null || value === undefined || value === '') {
92            return defaultValue;
93        }
94
95        const numericValue = Number(value);
96        if (!Number.isFinite(numericValue)) {
97            console.warn(`Invalid break point value: ${value}. Default value is used ${defaultValue}`)
98            return defaultValue;
99        }
100        if (numericValue < DEFAULT_BREAKPOINTS.MIN || numericValue > DEFAULT_BREAKPOINTS.MAX) {
101            console.warn(`Invalid break point value ${numericValue}. The range is between ${DEFAULT_BREAKPOINTS.MIN} and ${DEFAULT_BREAKPOINTS.MAX}. Default value is used ${defaultValue}`)
102            return defaultValue;
103        }
104
105        return numericValue;
106    }
107
108    validateTabletViewBreakPoint(tabletBreakPoint) {
109        return this.validateBreakPoint(tabletBreakPoint, this.TABLET_BREAK_POINT);
110    }
111
112    validateMobileViewBreakPoint(mobileBreakPoint) {
113        return this.validateBreakPoint(mobileBreakPoint, this.MOBILE_BREAK_POINT);
114    }
115
116    loadCSS(tabletBreakPoint, mobileBreakPoint) {
117
118        this.loadTabletViewCSS(tabletBreakPoint);
119        this.loadMobileViewCSS(mobileBreakPoint);
120    }
121
122    loadTabletViewCSS(tabletBreakPoint) {
123        const cssId = `responsive-table-${tabletBreakPoint}`;
124        if (document.getElementById(cssId)) return;
125
126        const style = document.createElement("style");
127        style.setAttribute(`media`, `all and (max-width: ${tabletBreakPoint}px)`)
128        style.id = cssId;
129        const styleNode = document.createTextNode(this.createCssTabletView(tabletBreakPoint))
130        style.appendChild(styleNode);
131
132        document.getElementsByTagName("head")[0].append(style);
133    }
134
135    loadMobileViewCSS(mobileBreakPoint) {
136        const cssId = `responsive-table-mobile-${mobileBreakPoint}`;
137        if (document.getElementById(cssId)) return;
138
139        const style = document.createElement("style");
140        style.setAttribute(`media`, `all and (max-width: ${mobileBreakPoint}px)`)
141        style.id = cssId;
142        const styleNode = document.createTextNode(this.createCssMobileView(mobileBreakPoint))
143        style.appendChild(styleNode);
144
145        document.getElementsByTagName("head")[0].append(style);
146    }
147
148    createCssTabletView(tabletBreakPoint) {
149        // Create reusable selector to avoid repetition
150        const selector = `[${RESPONSIVE_TABLE.BREAKPOINT}="${tabletBreakPoint}"].en-responsive-table`;
151
152        return `
153        ${selector},
154        ${selector} * {
155         box-sizing: border-box;
156        }
157        
158        ${selector},
159        ${selector} table {
160            border-collapse: collapse;
161        }
162        
163        ${selector} > thead {
164            display: none;
165        }
166        
167        ${selector} tbody > tr[data-header-row="true"] {
168            display: none;
169        }
170        
171        ${selector} tbody > tr[en-rt-header] {
172            display: none;
173        }
174       
175       ${selector} tbody > tr[data-header-row="true"] + tr.en-responsive-table-tr {
176           border-top: var(--en-rt-row-border);
177       }
178       
179       ${selector} tbody > tr:not([data-header-row]).en-responsive-table-tr:first-child {
180           border-top: var(--en-rt-row-border);
181       }
182       
183       ${selector} .en-responsive-table-tr {
184            display: flex;
185            flex-direction: column;
186            border-bottom: var(--en-rt-row-border);
187            border-left: var(--en-rt-row-border);
188            border-right: var(--en-rt-row-border);
189            height: auto !important;
190        }
191        
192       ${selector} .en-responsive-table-td {
193            display: flex;
194            align-items: stretch;
195            padding: 0;
196            border-bottom: var(--en-rt-cell-border);
197        }
198        
199        ${selector} .en-responsive-table-td:last-child {
200            border-bottom: none;
201        }
202        
203        
204        ${selector} {
205            width: 100%;
206        }
207        
208       ${selector} .en-responsive-table-header {
209            width: 30%;
210            display: flex;
211            align-items: flex-start;
212            justify-content: flex-start;
213            text-align: left;
214            padding: var(--en-rt-cell-padding);
215            background: var(--en-rt-header-bg);
216            border: none;
217        }
218        
219       ${selector} .en-responsive-table-content {
220            flex: 1;
221            border: none;
222            padding: var(--en-rt-cell-padding);
223            display: flex;
224            flex-direction: row;
225            flex-wrap: wrap;
226            box-sizing: border-box;
227        }
228        
229        ${selector} colgroup {
230            display: none;
231        }
232        `
233    }
234
235    createCssMobileView(mobileBreakPoint) {
236        const selector = `[${RESPONSIVE_TABLE.MOBILE_BREAKPOINT}="${mobileBreakPoint}"].en-responsive-table`;
237        return `
238           #pageWrapperTable ${selector} .en-responsive-table-td,
239            ${selector} .en-responsive-table-td {
240             flex-direction: column;
241             box-sizing: border-box;
242            }
243          
244          ${selector} .en-responsive-table-header {
245            width: 100%;
246            background: var(--en-rt-mobile-header-bg);
247          }
248          `;
249    }
250
251    initialize() {
252        // Idempotent: prevent double initialization
253        if (this._initialized) return;
254        this._initialized = true;
255
256        const initTables = () => {
257            document.querySelectorAll(`[${RESPONSIVE_TABLE.BREAKPOINT}]`).forEach(table => {
258                const tabletBreakPoint = table.getAttribute(RESPONSIVE_TABLE.BREAKPOINT);
259                const mobileBreakPoint = table.getAttribute(RESPONSIVE_TABLE.MOBILE_BREAKPOINT);
260                this.fromTable({table, tabletBreakPoint, mobileBreakPoint});
261            });
262        };
263
264        // Check if DOM is already ready (for async/defer scripts)
265        if (document.readyState === 'loading') {
266            document.addEventListener("DOMContentLoaded", initTables);
267        } else {
268            initTables();
269        }
270    }
271}
272
273// Auto-initialize on script load - create single instance
274const responsiveTableBuilder = new ResponsiveTableBuilder();
275responsiveTableBuilder.initialize();
276
277class ResponsiveTable {
278
279    headers = [];
280
281    table;
282    tabletBreakPoint;
283    mobileBreakPoint;
284    currentView = null; // Track current view state: 'desktop' or 'mobile'
285    mediaQueryListener = null; // Store listener reference for cleanup
286    mediaQuery = null; // Store media query object
287
288
289    constructor({table, tabletBreakPoint, mobileBreakPoint}) {
290        if (!table) return;
291        if ('TABLE' !== table.nodeName.toUpperCase()) {
292            return;
293        }
294
295        if (table.hasAttribute(RESPONSIVE_TABLE.INITIALIZED)) {
296            console.warn('ResponsiveTable: Table already initialized, skipping...', table);
297            return;
298        }
299
300        this.table = table;
301        if (tabletBreakPoint) this.tabletBreakPoint = tabletBreakPoint;
302        if (mobileBreakPoint) this.mobileBreakPoint = mobileBreakPoint;
303
304        table.setAttribute(RESPONSIVE_TABLE.INITIALIZED, 'true');
305        table.setAttribute(RESPONSIVE_TABLE.BREAKPOINT, tabletBreakPoint);
306        table.setAttribute(RESPONSIVE_TABLE.MOBILE_BREAKPOINT, mobileBreakPoint);
307
308        this.collectHeaders();
309
310        this.table.classList.add('en-responsive-table')
311
312        // CRITICAL POINT: Use max-width to match CSS behavior
313        // CSS uses: max-width: 750px (mobile at 750px and below)
314        // JS must use: max-width: 750px (mobile at 750px and below)
315        this.mediaQuery = window.matchMedia(`(max-width: ${this.tabletBreakPoint}px)`);
316
317        this.mediaQueryListener = debounce((e) => this.viewPortWidthListener(e), 150);
318
319        this.mediaQuery.addEventListener('change', this.mediaQueryListener);
320
321        // Initial check: if window width is <= breakpoint, we're in mobile/tablet mode
322        const isTabletOrMobile = window.innerWidth <= this.tabletBreakPoint;
323        this.viewPortWidthListener({matches: isTabletOrMobile})
324
325    }
326
327    destroy() {
328        // ...existing code...
329    }
330
331    viewPortWidthListener(e) {
332        if (e.matches) {
333            // max-width matched: screen <= breakpoint -> Mobile/Tablet view
334            if (this.currentView !== 'mobile') {
335                this.createMobileTable();
336                this.currentView = 'mobile';
337            }
338        } else {
339            // max-width NOT matched: screen > breakpoint -> Desktop view
340            if (this.currentView !== 'desktop') {
341                this.createDesktopTable();
342                this.currentView = 'desktop';
343            }
344        }
345    }
346
347
348    collectHeaders() {
349        // Check if table has header-selector attribute with CSS selector value (table-level)
350        const headerSelector = this.table.getAttribute(RESPONSIVE_TABLE.HEADER_SELECTOR);
351        if (headerSelector) {
352            try {
353                const headerRows = this.table.querySelectorAll(`table > tbody > tr${headerSelector}`);
354                if (headerRows.length > 0) {
355                    // Multiple headers mode using CSS selector
356                    // Mark all matching rows with en-rt-header attribute for CSS (row-level)
357                    headerRows.forEach(row => {
358                        row.setAttribute('en-rt-header', '');
359                    });
360                    this.multipleHeaders = true;
361                    this.headers = []; // Will be set dynamically per section
362                    return;
363                }
364            } catch (e) {
365                console.warn(`Invalid selector in header-selector: "${headerSelector}"`, e);
366            }
367        }
368
369        // Check if table has rows with en-rt-header attribute (already marked) - row-level
370        const headerRows = this.table.querySelectorAll('table > tbody > tr[en-rt-header]');
371        if (headerRows.length > 0) {
372            // Multiple headers mode
373            this.multipleHeaders = true;
374            this.headers = []; // Will be set dynamically per section
375            return;
376        }
377
378        // Check if table uses first row as header (when thead is missing)
379        if (this.table.hasAttribute('first-row-header') || this.table.hasAttribute('data-first-row-header')) {
380            const firstRow = this.table.querySelector('table > tbody > tr:first-child');
381            if (firstRow) {
382                this.headers = firstRow.querySelectorAll(':is(td, th)');
383                // Mark the first row so we can skip it during mobile transformation
384                firstRow.setAttribute('data-header-row', 'true');
385            }
386        } else {
387            // Standard thead approach
388            this.headers = this.table.querySelectorAll('table thead tr th');
389        }
390    }
391
392    createMobileTable() {
393        // Handle multiple headers mode
394        if (this.multipleHeaders) {
395            this.createMobileTableWithMultipleHeaders();
396            return;
397        }
398
399        // Standard single header mode
400        let trList = this.table.querySelectorAll('table > tbody > tr');
401        trList.forEach(tr => {
402            // Skip if already transformed to mobile view
403            if (tr.classList.contains('en-responsive-table-tr')) {
404                return;
405            }
406
407            // Skip if this is the header row (first row being used as header)
408            if (tr.hasAttribute('data-header-row')) {
409                return;
410            }
411
412            let tdList = tr.querySelectorAll('tr > :is(td, th)');
413            tdList.forEach((td, key) => {
414                tr.append(this.createMobileCell(td, key));
415            });
416            tr.classList.add('en-responsive-table-tr');
417        });
418    }
419
420    createMobileTableWithMultipleHeaders() {
421        let trList = this.table.querySelectorAll('table > tbody > tr');
422        let currentHeaders = [];
423
424        trList.forEach(tr => {
425            // Check if this is a header row (has en-rt-header attribute)
426            if (tr.hasAttribute('en-rt-header')) {
427                currentHeaders = tr.querySelectorAll(':is(td, th)');
428                tr.setAttribute('data-header-row', 'true');
429                return;
430            }
431
432            // Skip if already transformed to mobile view
433            if (tr.classList.contains('en-responsive-table-tr')) {
434                return;
435            }
436
437            // Skip spacer rows (rows with single colspan cell)
438            const cells = tr.querySelectorAll(':is(td, th)');
439            if (cells.length === 1 && cells[0].hasAttribute('colspan')) {
440                return;
441            }
442
443            // Transform regular data rows using current section's headers
444            let tdList = tr.querySelectorAll('tr > :is(td, th)');
445            tdList.forEach((td, key) => {
446                const header = currentHeaders[key];
447                tr.append(this.createMobileCellWithHeader(td, header));
448            });
449            tr.classList.add('en-responsive-table-tr')
450        });
451    }
452
453    createMobileCellWithHeader(contentNode, headerNode) {
454        let headerHTML = null;
455        if (headerNode) {
456            headerHTML = headerNode.innerHTML;
457        }
458        return this.buildCell(contentNode, headerHTML);
459    }
460
461    createMobileCell(contentNode, index) {
462        let headerHTML = null;
463        if (this.headers[index]) {
464            headerHTML = this.headers[index].innerHTML;
465        }
466        return this.buildCell(contentNode, headerHTML);
467    }
468
469    buildCell(contentNode, headerHtml) {
470        const td = document.createElement('td');
471        td.classList.add('en-responsive-table-td');
472        td.setAttribute('style', 'display:flex !important');
473
474        const header = document.createElement('th');
475        header.classList.add('en-responsive-table-header');
476
477        if (headerHtml) {
478            header.innerHTML = headerHtml;
479        } else {
480            header.classList.add(`en-responsive-table-empty-header`)
481            header.innerHTML = '';
482            if (this.table.hasAttribute('hide-empty-header')) {
483                header.style.display = 'none';
484            }
485        }
486        td.append(header);
487
488        contentNode.classList.add('en-responsive-table-content');
489        td.append(contentNode);
490
491        return td;
492    }
493
494    createDesktopTable() {
495        const trList = this.table.querySelectorAll(`.en-responsive-table-tr`);
496        trList.forEach(tr => {
497
498            const mobileCellList = tr.querySelectorAll('.en-responsive-table-td');
499            mobileCellList.forEach(cell => {
500                const td = cell.querySelector('.en-responsive-table-content');
501                td.classList.remove('en-responsive-table-content');
502                tr.append(td);
503            });
504
505            mobileCellList.forEach(cell => cell.remove());
506            tr.classList.remove(`en-responsive-table-tr`)
507        })
508    }
509}

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.