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.