@livenetworks/ashlar 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +189 -99
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -55
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +48 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +47 -0
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +232 -38
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1103
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -1,473 +1,1493 @@
1
- import { dispatch, registerComponent } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-table';
5
- const DOM_ATTRIBUTE = 'lnTable';
6
- const SORT_ATTR = 'data-ln-sort'; // read-only: column type metadata (number/date/string) used when parsing rows
7
- const EMPTY_TEMPLATE = 'data-ln-table-empty';
8
- // Tuning constant — duplicated in ln-data-table for component independence
9
- const VIRTUAL_THRESHOLD = 200;
10
- const BUFFER_ROWS = 15;
11
-
12
- if (window[DOM_ATTRIBUTE] !== undefined) return;
13
-
14
- // Singleton — same lang for all table instances on the page
15
- const _collator = typeof Intl !== 'undefined'
16
- ? new Intl.Collator(document.documentElement.lang || undefined, { sensitivity: 'base' })
17
- : null;
18
-
19
- // ─── Component ─────────────────────────────────────────────
20
-
21
- function _component(dom) {
22
- this.dom = dom;
23
- this.table = dom.querySelector('table');
24
- this.tbody = dom.querySelector('tbody');
25
- this.thead = dom.querySelector('thead');
26
- // Scope to the column-headers row (last <tr> in thead). The
27
- // first thead row may be a colspan toolbar that does not map to
28
- // a data column.
29
- const colHeaderRow = this.thead ? this.thead.querySelector('tr:last-child') : null;
30
- this.ths = colHeaderRow ? Array.from(colHeaderRow.querySelectorAll('th')) : [];
31
-
32
- this._data = [];
33
- this._filteredData = [];
34
- this._searchTerm = '';
35
- this._sortCol = -1;
36
- this._sortDir = null;
37
- this._sortType = null;
38
- this._columnFilters = {};
39
-
40
- // Virtual scroll state
41
- this._virtual = false;
42
- this._rowHeight = 0;
43
- this._vStart = -1;
44
- this._vEnd = -1;
45
- this._rafId = null;
46
- this._scrollHandler = null;
47
- this._colgroup = null;
48
-
49
- const self = this;
50
-
51
- // MutationObserver that waits for AJAX-loaded rows. Stored on the
52
- // instance so destroy() can disconnect it if rows never arrive.
53
- this._emptyTbodyObserver = null;
54
-
55
- if (this.tbody && this.tbody.rows.length > 0) {
56
- this._parseRows();
57
- } else if (this.tbody) {
58
- this._emptyTbodyObserver = new MutationObserver(function () {
59
- if (self.tbody.rows.length > 0) {
60
- self._emptyTbodyObserver.disconnect();
61
- self._emptyTbodyObserver = null;
62
- self._parseRows();
63
- }
64
- });
65
- this._emptyTbodyObserver.observe(this.tbody, { childList: true });
66
- }
67
-
68
- // ─── Search — ln-search dispatches ln-search:change on the target ─────
69
- // preventDefault() tells ln-search to skip its own DOM show/hide logic.
70
-
71
- this._onSearch = function (e) {
72
- e.preventDefault();
73
- self._searchTerm = e.detail.term;
74
- self._applyFilterAndSort();
75
- self._vStart = -1;
76
- self._vEnd = -1;
77
- self._render();
78
- dispatch(dom, 'ln-table:filter', {
79
- term: self._searchTerm,
80
- matched: self._filteredData.length,
81
- total: self._data.length
82
- });
83
- };
84
- dom.addEventListener('ln-search:change', this._onSearch);
85
-
86
- // ─── Coordinate with ln-table-sort ─────────────────────
87
-
88
- this._onSort = function (e) {
89
- self._sortCol = e.detail.direction === null ? -1 : e.detail.column;
90
- self._sortDir = e.detail.direction;
91
- self._sortType = e.detail.sortType;
92
- self._applyFilterAndSort();
93
- self._vStart = -1;
94
- self._vEnd = -1;
95
- self._render();
96
- dispatch(dom, 'ln-table:sorted', {
97
- column: e.detail.column,
98
- direction: e.detail.direction,
99
- matched: self._filteredData.length,
100
- total: self._data.length
101
- });
102
- };
103
- dom.addEventListener('ln-table:sort', this._onSort);
104
-
105
- // ─── Column filters — ln-filter dispatches on target via bubbling ──
106
-
107
- this._onColumnFilter = function (e) {
108
- const key = e.detail.key;
109
-
110
- // Only handle keys that map to a table column via data-ln-filter-col.
111
- // If unmapped, let ln-filter handle row visibility via DOM attributes.
112
- let hasMappedColumn = false;
113
- for (let i = 0; i < self.ths.length; i++) {
114
- if (self.ths[i].getAttribute('data-ln-filter-col') === key) {
115
- hasMappedColumn = true;
116
- break;
117
- }
118
- }
119
- if (!hasMappedColumn) return;
120
-
121
- const values = e.detail.values;
122
- if (!values || values.length === 0) {
123
- delete self._columnFilters[key];
124
- } else {
125
- const lower = [];
126
- for (let i = 0; i < values.length; i++) {
127
- lower.push(values[i].toLowerCase());
128
- }
129
- self._columnFilters[key] = lower;
130
- }
131
-
132
- // Mark <th> with active filter indicator
133
- const th = self.dom.querySelector('th[data-ln-filter-col="' + key + '"]');
134
- if (th) {
135
- if (values && values.length > 0) {
136
- th.setAttribute('data-ln-filter-active', '');
137
- } else {
138
- th.removeAttribute('data-ln-filter-active');
139
- }
140
- }
141
-
142
- self._applyFilterAndSort();
143
- self._vStart = -1;
144
- self._vEnd = -1;
145
- self._render();
146
- dispatch(dom, 'ln-table:filter', {
147
- term: self._searchTerm,
148
- matched: self._filteredData.length,
149
- total: self._data.length
150
- });
151
- };
152
- dom.addEventListener('ln-filter:changed', this._onColumnFilter);
153
-
154
- // ─── Clear all — reset search + column filters ──────────────
155
-
156
- this._onClear = function (e) {
157
- const btn = e.target.closest('[data-ln-table-clear]');
158
- if (!btn) return;
159
-
160
- // Clear search state + input
161
- self._searchTerm = '';
162
- const searchEl = document.querySelector('[data-ln-search="' + dom.id + '"]');
163
- if (searchEl) {
164
- const input = searchEl.tagName === 'INPUT' ? searchEl : searchEl.querySelector('input');
165
- if (input) input.value = '';
166
- }
167
-
168
- // Clear column filter state + active indicators
169
- self._columnFilters = {};
170
- for (let i = 0; i < self.ths.length; i++) {
171
- self.ths[i].removeAttribute('data-ln-filter-active');
172
- }
173
-
174
- // Reset filter component UI — synthetic change event on the "All"
175
- // sentinel checkbox of each connected filter.
176
- const filters = document.querySelectorAll('[data-ln-filter="' + dom.id + '"]');
177
- for (let i = 0; i < filters.length; i++) {
178
- const resetInput = filters[i].querySelector('[data-ln-filter-reset]');
179
- if (resetInput) {
180
- resetInput.checked = true;
181
- resetInput.dispatchEvent(new Event('change', { bubbles: true }));
182
- }
183
- }
184
-
185
- // Re-render
186
- self._applyFilterAndSort();
187
- self._vStart = -1;
188
- self._vEnd = -1;
189
- self._render();
190
- dispatch(dom, 'ln-table:filter', {
191
- term: '',
192
- matched: self._filteredData.length,
193
- total: self._data.length
194
- });
195
- };
196
- dom.addEventListener('click', this._onClear);
197
-
198
- return this;
199
- }
200
-
201
- // ─── Parse rows into in-memory array ───────────────────────
202
-
203
- _component.prototype._parseRows = function () {
204
- const rows = this.tbody.rows;
205
- const ths = this.ths;
206
- this._data = [];
207
-
208
- const sortTypes = [];
209
- for (let c = 0; c < ths.length; c++) {
210
- sortTypes[c] = ths[c].getAttribute(SORT_ATTR);
211
- }
212
-
213
- if (rows.length > 0) this._rowHeight = rows[0].offsetHeight || 40;
214
- this._lockColumnWidths();
215
-
216
- for (let i = 0; i < rows.length; i++) {
217
- const tr = rows[i];
218
- const sortKeys = [];
219
- const rawTexts = [];
220
- const searchParts = [];
221
-
222
- for (let j = 0; j < tr.cells.length; j++) {
223
- const td = tr.cells[j];
224
- const text = td.textContent.trim();
225
- const raw = td.hasAttribute('data-ln-value') ? td.getAttribute('data-ln-value') : text;
226
- const type = sortTypes[j];
227
-
228
- rawTexts[j] = text.toLowerCase();
229
-
230
- if (type === 'number' || type === 'date') {
231
- sortKeys[j] = parseFloat(raw) || 0;
232
- } else if (type === 'string') {
233
- sortKeys[j] = String(raw);
234
- } else {
235
- sortKeys[j] = null;
236
- }
237
-
238
- if (j < tr.cells.length - 1) searchParts.push(text.toLowerCase());
239
- }
240
-
241
- this._data.push({
242
- sortKeys: sortKeys,
243
- rawTexts: rawTexts,
244
- html: tr.outerHTML,
245
- searchText: searchParts.join(' ')
246
- });
247
- }
248
-
249
- this._filteredData = this._data.slice();
250
- this._render();
251
-
252
- dispatch(this.dom, 'ln-table:ready', {
253
- total: this._data.length
254
- });
255
- };
256
-
257
- // ─── Filter + Sort ─────────────────────────────────────────
258
-
259
- _component.prototype._applyFilterAndSort = function () {
260
- const term = this._searchTerm;
261
- const colFilters = this._columnFilters;
262
- const hasColFilters = Object.keys(colFilters).length > 0;
263
- const ths = this.ths;
264
-
265
- // Build column index lookup: { "status": 6, "dept": 2 }
266
- const colIndexByKey = {};
267
- if (hasColFilters) {
268
- for (let i = 0; i < ths.length; i++) {
269
- const filterKey = ths[i].getAttribute('data-ln-filter-col');
270
- if (filterKey) colIndexByKey[filterKey] = i;
271
- }
272
- }
273
-
274
- if (!term && !hasColFilters) {
275
- this._filteredData = this._data.slice();
276
- } else {
277
- this._filteredData = this._data.filter(function (row) {
278
- if (term && row.searchText.indexOf(term) === -1) return false;
279
- if (hasColFilters) {
280
- for (const key in colFilters) {
281
- const idx = colIndexByKey[key];
282
- if (idx !== undefined) {
283
- if (colFilters[key].indexOf(row.rawTexts[idx]) === -1) return false;
284
- }
285
- }
286
- }
287
- return true;
288
- });
289
- }
290
-
291
- if (this._sortCol < 0 || !this._sortDir) return;
292
-
293
- const colIndex = this._sortCol;
294
- const multiplier = this._sortDir === 'desc' ? -1 : 1;
295
- const isNumeric = (this._sortType === 'number' || this._sortType === 'date');
296
- const compare = _collator
297
- ? _collator.compare
298
- : function (a, b) { return a < b ? -1 : a > b ? 1 : 0; };
299
-
300
- this._filteredData.sort(function (a, b) {
301
- const aKey = a.sortKeys[colIndex];
302
- const bKey = b.sortKeys[colIndex];
303
- if (isNumeric) return (aKey - bKey) * multiplier;
304
- return compare(aKey, bKey) * multiplier;
305
- });
306
- };
307
-
308
- // ─── Column width locking ──────────────────────────────────
309
-
310
- _component.prototype._lockColumnWidths = function () {
311
- if (!this.table || !this.thead || this._colgroup) return;
312
-
313
- const colgroup = document.createElement('colgroup');
314
- this.ths.forEach(function (th) {
315
- const col = document.createElement('col');
316
- col.style.width = th.offsetWidth + 'px';
317
- colgroup.appendChild(col);
318
- });
319
-
320
- this.table.insertBefore(colgroup, this.table.firstChild);
321
- this.table.style.tableLayout = 'fixed';
322
- this._colgroup = colgroup;
323
- };
324
-
325
- // ─── Render ────────────────────────────────────────────────
326
-
327
- _component.prototype._render = function () {
328
- if (!this.tbody) return;
329
- const count = this._filteredData.length;
330
-
331
- if (count === 0 && (this._searchTerm || Object.keys(this._columnFilters).length > 0)) {
332
- this._disableVirtualScroll();
333
- this._showEmptyState();
334
- } else if (count > VIRTUAL_THRESHOLD) {
335
- this._enableVirtualScroll();
336
- this._renderVirtual();
337
- } else {
338
- this._disableVirtualScroll();
339
- this._renderAll();
340
- }
341
- };
342
-
343
- _component.prototype._renderAll = function () {
344
- const html = [];
345
- const data = this._filteredData;
346
- for (let i = 0; i < data.length; i++) html.push(data[i].html);
347
- this.tbody.innerHTML = html.join('');
348
- };
349
-
350
- // ─── Virtual scroll ────────────────────────────────────────
351
-
352
- _component.prototype._enableVirtualScroll = function () {
353
- if (this._virtual) return;
354
- this._virtual = true;
355
- const self = this;
356
-
357
- this._scrollHandler = function () {
358
- if (self._rafId) return;
359
- self._rafId = requestAnimationFrame(function () {
360
- self._rafId = null;
361
- self._renderVirtual();
362
- });
363
- };
364
-
365
- window.addEventListener('scroll', this._scrollHandler, { passive: true });
366
- window.addEventListener('resize', this._scrollHandler, { passive: true });
367
- };
368
-
369
- _component.prototype._disableVirtualScroll = function () {
370
- if (!this._virtual) return;
371
- this._virtual = false;
372
-
373
- if (this._scrollHandler) {
374
- window.removeEventListener('scroll', this._scrollHandler);
375
- window.removeEventListener('resize', this._scrollHandler);
376
- this._scrollHandler = null;
377
- }
378
- if (this._rafId) {
379
- cancelAnimationFrame(this._rafId);
380
- this._rafId = null;
381
- }
382
- this._vStart = -1;
383
- this._vEnd = -1;
384
- };
385
-
386
- _component.prototype._renderVirtual = function () {
387
- const data = this._filteredData;
388
- const total = data.length;
389
- const rowH = this._rowHeight;
390
- if (!rowH || !total) return;
391
-
392
- const tableRect = this.table.getBoundingClientRect();
393
- const tableTopInPage = tableRect.top + window.scrollY;
394
- const theadH = this.thead ? this.thead.offsetHeight : 0;
395
- const dataStartInPage = tableTopInPage + theadH;
396
-
397
- const scrollIntoData = window.scrollY - dataStartInPage;
398
- const startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
399
- const endRow = Math.min(startRow + Math.ceil(window.innerHeight / rowH) + (BUFFER_ROWS * 2), total);
400
-
401
- if (startRow === this._vStart && endRow === this._vEnd) return;
402
- this._vStart = startRow;
403
- this._vEnd = endRow;
404
-
405
- const colSpan = this.ths.length || 1;
406
- const topH = startRow * rowH;
407
- const bottomH = (total - endRow) * rowH;
408
- let html = '';
409
-
410
- if (topH > 0) {
411
- html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
412
- colSpan + '" style="height:' + topH + 'px;padding:0;border:none"></td></tr>';
413
- }
414
- for (let i = startRow; i < endRow; i++) html += data[i].html;
415
- if (bottomH > 0) {
416
- html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
417
- colSpan + '" style="height:' + bottomH + 'px;padding:0;border:none"></td></tr>';
418
- }
419
-
420
- this.tbody.innerHTML = html;
421
- };
422
-
423
- // ─── Empty state ───────────────────────────────────────────
424
- // Clones <template data-ln-table-empty> if present, then dispatches ln-table:empty.
425
-
426
- _component.prototype._showEmptyState = function () {
427
- const colSpan = this.ths.length || 1;
428
- const tpl = this.dom.querySelector('template[' + EMPTY_TEMPLATE + ']');
429
-
430
- const td = document.createElement('td');
431
- td.setAttribute('colspan', String(colSpan));
432
- if (tpl) td.appendChild(document.importNode(tpl.content, true));
433
-
434
- const tr = document.createElement('tr');
435
- tr.className = 'ln-table__empty';
436
- tr.appendChild(td);
437
-
438
- this.tbody.innerHTML = '';
439
- this.tbody.appendChild(tr);
440
-
441
- dispatch(this.dom, 'ln-table:empty', {
442
- term: this._searchTerm,
443
- total: this._data.length
444
- });
445
- };
446
-
447
- // ─── Destroy ───────────────────────────────────────────────
448
-
449
- _component.prototype.destroy = function () {
450
- if (!this.dom[DOM_ATTRIBUTE]) return;
451
- this._disableVirtualScroll();
452
- if (this._emptyTbodyObserver) {
453
- this._emptyTbodyObserver.disconnect();
454
- this._emptyTbodyObserver = null;
455
- }
456
- this.dom.removeEventListener('ln-search:change', this._onSearch);
457
- this.dom.removeEventListener('ln-table:sort', this._onSort);
458
- this.dom.removeEventListener('ln-filter:changed', this._onColumnFilter);
459
- this.dom.removeEventListener('click', this._onClear);
460
- if (this._colgroup) {
461
- this._colgroup.remove();
462
- this._colgroup = null;
463
- }
464
- if (this.table) this.table.style.tableLayout = '';
465
- this._data = [];
466
- this._filteredData = [];
467
- delete this.dom[DOM_ATTRIBUTE];
468
- };
469
-
470
- // ─── Init ──────────────────────────────────────────────────
471
-
472
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-table');
473
- })();
1
+ import { cloneTemplateScoped, dispatch, requestData, fill, fillTemplate, registerComponent, readValue, createWindowCache, createBatcher, getLocale, detectValueType, compareValues } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-table';
5
+ const DOM_ATTRIBUTE = 'lnTable';
6
+ const EMPTY_TEMPLATE = 'data-ln-table-empty';
7
+ // Tuning constant — duplicated in ln-data-table for component independence
8
+ const VIRTUAL_THRESHOLD = 200;
9
+ const BUFFER_ROWS = 15;
10
+ const WINDOW_DEFAULT = 1000;
11
+ const WINDOW_PAGE = 200;
12
+ const FETCH_DEBOUNCE = 120;
13
+
14
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
15
+
16
+ // Singleton — same lang for all table instances on the page
17
+ const _collator = typeof Intl !== 'undefined'
18
+ ? new Intl.Collator(document.documentElement.lang || undefined, { sensitivity: 'base' })
19
+ : null;
20
+
21
+ function _formatNum(n, dom) {
22
+ if (n == null || isNaN(n)) return '';
23
+ try {
24
+ return new Intl.NumberFormat(getLocale(dom)).format(n);
25
+ } catch (e) {
26
+ return String(n);
27
+ }
28
+ }
29
+
30
+ function _findScrollContainer(el) {
31
+ let p = el.parentElement;
32
+ while (p && p !== document.body && p !== document.documentElement) {
33
+ const cs = getComputedStyle(p);
34
+ const oy = cs.overflowY;
35
+ if (oy === 'auto' || oy === 'scroll') return p;
36
+ p = p.parentElement;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function _saveScroll(self) {
42
+ const sc = self._scrollContainer || _findScrollContainer(self.dom);
43
+ return {
44
+ container: sc,
45
+ top: sc ? sc.scrollTop : window.scrollY
46
+ };
47
+ }
48
+
49
+ function _restoreScroll(state) {
50
+ if (state.container) {
51
+ state.container.scrollTop = state.top;
52
+ } else {
53
+ window.scrollTo(window.scrollX, state.top);
54
+ }
55
+ }
56
+
57
+
58
+ // ─── Component ─────────────────────────────────────────────
59
+
60
+ function _component(dom) {
61
+ this.dom = dom;
62
+ this.table = dom.querySelector('table');
63
+ this.tbody = dom.querySelector('[data-ln-table-body]') || dom.querySelector('tbody');
64
+ this.thead = dom.querySelector('thead');
65
+ // Scope to the column-headers row (last <tr> in thead).
66
+ const colHeaderRow = this.thead ? this.thead.querySelector('tr:last-child') : null;
67
+ this.ths = colHeaderRow ? Array.from(colHeaderRow.querySelectorAll('th')) : [];
68
+
69
+ // Footer elements — both modes
70
+ this._totalSpan = dom.querySelector('[data-ln-table-total]');
71
+ this._filteredSpan = dom.querySelector('[data-ln-table-filtered]');
72
+ if (this._filteredSpan) {
73
+ this._filteredWrap = this._filteredSpan.parentElement !== dom
74
+ ? this._filteredSpan.parentElement
75
+ : null;
76
+ }
77
+ this._selectedSpan = dom.querySelector('[data-ln-table-selected]');
78
+ if (this._selectedSpan) {
79
+ this._selectedWrap = this._selectedSpan.parentElement !== dom
80
+ ? this._selectedSpan.parentElement
81
+ : null;
82
+ }
83
+
84
+ this.isDataDriven = dom.hasAttribute('data-ln-table-source');
85
+ this.name = dom.getAttribute(DOM_SELECTOR) || '';
86
+ this.source = dom.getAttribute('data-ln-table-source') || '';
87
+
88
+ this._data = [];
89
+ this._filteredData = [];
90
+ this._searchTerm = '';
91
+ this._sortCol = -1;
92
+ this._sortDir = null;
93
+ this._columnFilters = {};
94
+ this.selectedIds = new Set();
95
+
96
+ // Virtual scroll state
97
+ this._virtual = false;
98
+ this._rowHeight = 0;
99
+ this._vStart = -1;
100
+ this._vEnd = -1;
101
+ this._rafId = null;
102
+ this._scrollHandler = null;
103
+ this._scrollContainer = null;
104
+ this._colgroup = null;
105
+
106
+ const self = this;
107
+
108
+ // --- Layer 1 Command / Request Event Handlers ---
109
+
110
+ this._onSetSearch = function (e) {
111
+ const term = (e.detail && e.detail.query != null ? e.detail.query : (e.detail && e.detail.term != null ? e.detail.term : '')).trim();
112
+ if (self.isDataDriven) {
113
+ self.currentSearch = term;
114
+ dispatch(dom, 'ln-table:search', {
115
+ table: self.name,
116
+ query: self.currentSearch
117
+ });
118
+ self._requestData();
119
+ } else {
120
+ self._searchTerm = term.toLowerCase();
121
+ self._applyFilterAndSort();
122
+ self._vStart = -1;
123
+ self._vEnd = -1;
124
+ self._render();
125
+ self._updateFooter();
126
+ dispatch(dom, 'ln-table:filter', {
127
+ term: self._searchTerm,
128
+ matched: self._filteredData.length,
129
+ total: self._data.length
130
+ });
131
+ }
132
+ };
133
+ dom.addEventListener('ln-table:set-search', this._onSetSearch);
134
+
135
+ this._onSetFilter = function (e) {
136
+ if (!e.detail) return;
137
+ const key = e.detail.key;
138
+ const values = e.detail.values;
139
+
140
+ if (self.isDataDriven) {
141
+ if (!values || values.length === 0) {
142
+ delete self.currentFilters[key];
143
+ } else {
144
+ self.currentFilters[key] = values;
145
+ }
146
+ self._requestData();
147
+ } else {
148
+ if (!values || values.length === 0) {
149
+ delete self._columnFilters[key];
150
+ } else {
151
+ const lower = [];
152
+ for (let i = 0; i < values.length; i++) {
153
+ lower.push(values[i].toLowerCase());
154
+ }
155
+ self._columnFilters[key] = lower;
156
+ }
157
+ self._applyFilterAndSort();
158
+ self._vStart = -1;
159
+ self._vEnd = -1;
160
+ self._render();
161
+ dispatch(dom, 'ln-table:filter', {
162
+ term: self._searchTerm,
163
+ matched: self._filteredData.length,
164
+ total: self._data.length
165
+ });
166
+ }
167
+ };
168
+ dom.addEventListener('ln-table:set-filter', this._onSetFilter);
169
+
170
+ this._onRequestClearFilters = function () {
171
+ if (self.isDataDriven) {
172
+ self.currentFilters = {};
173
+ self.currentSearch = '';
174
+ dispatch(dom, 'ln-table:clear-filters', { table: self.name });
175
+ self._requestData();
176
+ } else {
177
+ self._searchTerm = '';
178
+ self._columnFilters = {};
179
+ self._applyFilterAndSort();
180
+ self._vStart = -1;
181
+ self._vEnd = -1;
182
+ self._render();
183
+ self._updateFooter();
184
+ dispatch(dom, 'ln-table:filter', {
185
+ term: '',
186
+ matched: self._filteredData.length,
187
+ total: self._data.length
188
+ });
189
+ }
190
+ };
191
+ dom.addEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
192
+
193
+ // --- Selection (both modes) ---
194
+ this._selectable = dom.hasAttribute('data-ln-table-selectable');
195
+ this._selectableActive = false;
196
+ if (this._selectable) {
197
+ this._enableSelection();
198
+ }
199
+
200
+ if (this.isDataDriven) {
201
+ this.isLoaded = false;
202
+ this.totalCount = 0;
203
+ this.visibleCount = 0;
204
+ this.currentSort = null;
205
+ this.currentFilters = {};
206
+ this.currentSearch = '';
207
+
208
+ this._lastTotal = 0;
209
+ this._lastFiltered = 0;
210
+
211
+ // Windowed (sliding-window) server-side virtualization — opt-in
212
+ this._windowed = false;
213
+ this._cache = null;
214
+ if (this.isDataDriven && dom.hasAttribute('data-ln-table-window')) this._enterWindowedMode();
215
+
216
+ // --- Event listeners ---
217
+ this._onSetData = function (e) {
218
+ const detail = e.detail || {};
219
+ if (self._windowed) {
220
+ dom.classList.remove('ln-table--loading');
221
+ self._cache.ingest(detail);
222
+ return;
223
+ }
224
+ self._data = detail.data || [];
225
+ self._lastTotal = detail.total != null ? detail.total : self._data.length;
226
+ self._lastFiltered = detail.filtered != null ? detail.filtered : self._data.length;
227
+
228
+ self.totalCount = self._lastTotal;
229
+ self.visibleCount = self._lastFiltered;
230
+ self.isLoaded = true;
231
+
232
+ dom.classList.remove('ln-table--loading');
233
+
234
+ self._vStart = -1;
235
+ self._vEnd = -1;
236
+
237
+ self._applyFilterAndSort();
238
+ self._render();
239
+ self._updateFooter();
240
+
241
+ dispatch(dom, 'ln-table:rendered', {
242
+ table: self.name,
243
+ total: self.totalCount,
244
+ visible: self.visibleCount
245
+ });
246
+ };
247
+ dom.addEventListener('ln-table:set-data', this._onSetData);
248
+
249
+ this._onSetLoading = function (e) {
250
+ const loading = e.detail && e.detail.loading;
251
+ dom.classList.toggle('ln-table--loading', !!loading);
252
+ if (loading) {
253
+ self.isLoaded = false;
254
+ }
255
+ };
256
+ dom.addEventListener('ln-table:set-loading', this._onSetLoading);
257
+
258
+ this._onPageFailed = function (e) {
259
+ if (!self._windowed || !self._cache) return;
260
+ self._cache.release(e.detail && e.detail.offset);
261
+ };
262
+ dom.addEventListener('ln-table:page-failed', this._onPageFailed);
263
+
264
+ this._onRequestRevalidate = function () {
265
+ if (!self._windowed || !self._cache) return;
266
+ self._cache.revalidate();
267
+ };
268
+ dom.addEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
269
+
270
+ // --- Sort ---
271
+ this._onSort = function (e) {
272
+ e.preventDefault();
273
+ self.currentSort = e.detail.direction === 'none' ? null : { field: e.detail.field, direction: e.detail.direction };
274
+ self._requestData();
275
+ };
276
+ dom.addEventListener('ln-sort:change', this._onSort);
277
+
278
+ // D4 — windowed tables cannot select rows they have never fetched
279
+ if (this._windowed && this._selectable && this._selectAllCheckbox) {
280
+ this._selectAllCheckbox.classList.add('hidden');
281
+ }
282
+
283
+ // --- Row Click & Actions ---
284
+ this._onRowClick = function (e) {
285
+ if (e.target.closest('[data-ln-table-row-select]')) return;
286
+ if (e.target.closest('[data-ln-table-row-action]')) return;
287
+ if (e.target.closest('a') || e.target.closest('button')) return;
288
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
289
+
290
+ const tr = e.target.closest('[data-ln-table-row]');
291
+ if (!tr) return;
292
+
293
+ const id = tr.getAttribute('data-ln-table-row-id');
294
+ const record = tr._lnRecord || {};
295
+
296
+ dispatch(dom, 'ln-table:row-click', {
297
+ table: self.name,
298
+ id: id,
299
+ record: record
300
+ });
301
+ };
302
+ if (this.tbody) this.tbody.addEventListener('click', this._onRowClick);
303
+
304
+ this._onRowAction = function (e) {
305
+ const btn = e.target.closest('[data-ln-table-row-action]');
306
+ if (!btn) return;
307
+
308
+ e.stopPropagation();
309
+ const tr = btn.closest('[data-ln-table-row]');
310
+ if (!tr) return;
311
+
312
+ const action = btn.getAttribute('data-ln-table-row-action');
313
+ const id = tr.getAttribute('data-ln-table-row-id');
314
+ const record = tr._lnRecord || {};
315
+
316
+ dispatch(dom, 'ln-table:row-action', {
317
+ table: self.name,
318
+ id: id,
319
+ action: action,
320
+ record: record
321
+ });
322
+ };
323
+ if (this.tbody) this.tbody.addEventListener('click', this._onRowAction);
324
+
325
+ // --- Keyboard navigation (tbody rows) ---
326
+ this._focusedRowIndex = -1;
327
+ this._onKeydown = function (e) {
328
+ if (!dom.contains(document.activeElement) && document.activeElement !== document.body) return;
329
+ if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return;
330
+
331
+ const rows = self.tbody ? Array.from(self.tbody.querySelectorAll('[data-ln-table-row]')) : [];
332
+ if (!rows.length) return;
333
+
334
+ switch (e.key) {
335
+ case 'ArrowDown':
336
+ e.preventDefault();
337
+ self._focusedRowIndex = Math.min(self._focusedRowIndex + 1, rows.length - 1);
338
+ self._focusRow(rows);
339
+ break;
340
+ case 'ArrowUp':
341
+ e.preventDefault();
342
+ self._focusedRowIndex = Math.max(self._focusedRowIndex - 1, 0);
343
+ self._focusRow(rows);
344
+ break;
345
+ case 'Home':
346
+ e.preventDefault();
347
+ self._focusedRowIndex = 0;
348
+ self._focusRow(rows);
349
+ break;
350
+ case 'End':
351
+ e.preventDefault();
352
+ self._focusedRowIndex = rows.length - 1;
353
+ self._focusRow(rows);
354
+ break;
355
+ case 'Enter':
356
+ if (self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
357
+ e.preventDefault();
358
+ const tr = rows[self._focusedRowIndex];
359
+ dispatch(dom, 'ln-table:row-click', {
360
+ table: self.name,
361
+ id: tr.getAttribute('data-ln-table-row-id'),
362
+ record: tr._lnRecord || {}
363
+ });
364
+ }
365
+ break;
366
+ case ' ':
367
+ if (self._selectable && self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
368
+ e.preventDefault();
369
+ const cb = rows[self._focusedRowIndex].querySelector('[data-ln-table-row-select]');
370
+ if (cb) {
371
+ cb.checked = !cb.checked;
372
+ cb.dispatchEvent(new Event('change', { bubbles: true }));
373
+ }
374
+ }
375
+ break;
376
+ }
377
+ };
378
+ document.addEventListener('keydown', this._onKeydown);
379
+
380
+ // Local hydration of initial SSR rows
381
+ if (this.tbody && this.tbody.rows.length > 0) {
382
+ this._parseRows();
383
+ }
384
+
385
+ // Initial request-data
386
+ if (this._windowed) {
387
+ this._kickWindowInitial();
388
+ } else {
389
+ dispatch(dom, 'ln-table:request-data', {
390
+ table: this.name,
391
+ sort: this.currentSort,
392
+ filters: this.currentFilters,
393
+ search: this.currentSearch
394
+ });
395
+ }
396
+
397
+ } else {
398
+ // SSR Mode
399
+ this._emptyTbodyObserver = null;
400
+
401
+ if (this.tbody && this.tbody.rows.length > 0) {
402
+ this._parseRows();
403
+ } else if (this.tbody) {
404
+ this._emptyTbodyObserver = new MutationObserver(function () {
405
+ if (self.tbody.rows.length > 0) {
406
+ self._emptyTbodyObserver.disconnect();
407
+ self._emptyTbodyObserver = null;
408
+ self._parseRows();
409
+ }
410
+ });
411
+ this._emptyTbodyObserver.observe(this.tbody, { childList: true });
412
+ }
413
+
414
+ this._onSort = function (e) {
415
+ e.preventDefault();
416
+ const direction = e.detail.direction === 'none' ? null : e.detail.direction;
417
+ self._sortCol = direction === null ? -1 : e.detail.column;
418
+ self._sortDir = direction;
419
+ self._applyFilterAndSort();
420
+ self._vStart = -1;
421
+ self._vEnd = -1;
422
+ self._render();
423
+ dispatch(dom, 'ln-table:sorted', {
424
+ column: e.detail.column,
425
+ direction: e.detail.direction,
426
+ matched: self._filteredData.length,
427
+ total: self._data.length
428
+ });
429
+ };
430
+ dom.addEventListener('ln-sort:change', this._onSort);
431
+
432
+ this._onSearchChange = function (e) {
433
+ e.preventDefault();
434
+ self._onSetSearch(e);
435
+ };
436
+ dom.addEventListener('ln-search:change', this._onSearchChange);
437
+ }
438
+
439
+ return this;
440
+ }
441
+
442
+ // ─── Parse rows into in-memory array ───────────────────────
443
+
444
+ _component.prototype._parseRows = function () {
445
+ const rows = this.tbody.rows;
446
+ const ths = this.ths;
447
+ this._data = [];
448
+
449
+ if (rows.length > 0) this._rowHeight = rows[0].offsetHeight || 40;
450
+ this._lockColumnWidths();
451
+
452
+ for (let i = 0; i < rows.length; i++) {
453
+ const tr = rows[i];
454
+ const values = [];
455
+ const rawTexts = [];
456
+ const searchParts = [];
457
+
458
+ for (let j = 0; j < tr.cells.length; j++) {
459
+ const td = tr.cells[j];
460
+ const text = td.textContent.trim();
461
+
462
+ values[j] = readValue(td);
463
+ rawTexts[j] = text.toLowerCase();
464
+
465
+ if (j < tr.cells.length - 1) searchParts.push(text.toLowerCase());
466
+ }
467
+
468
+ let record = null;
469
+ if (this.isDataDriven) {
470
+ record = {};
471
+ const id = tr.getAttribute('data-ln-table-row-id');
472
+ if (id != null) record.id = id;
473
+
474
+ for (let j = 0; j < ths.length; j++) {
475
+ const field = ths[j].getAttribute('data-ln-table-col');
476
+ if (field) {
477
+ const cellIndex = j;
478
+ if (cellIndex < tr.cells.length) {
479
+ const td = tr.cells[cellIndex];
480
+ record[field] = readValue(td);
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ this._data.push({
487
+ values: values,
488
+ rawTexts: rawTexts,
489
+ html: tr.outerHTML,
490
+ searchText: searchParts.join(' '),
491
+ id: this.isDataDriven && record ? record.id : undefined,
492
+ ...record
493
+ });
494
+ }
495
+
496
+ this._filteredData = this._data.slice();
497
+
498
+ if (this.isDataDriven) {
499
+ this._lastTotal = this._data.length;
500
+ this._lastFiltered = this._data.length;
501
+ this.totalCount = this._data.length;
502
+ this.visibleCount = this._data.length;
503
+ this._updateFooter();
504
+ }
505
+
506
+ this._render();
507
+
508
+ dispatch(this.dom, 'ln-table:ready', {
509
+ total: this._data.length
510
+ });
511
+ };
512
+
513
+ // ─── Filter + Sort ─────────────────────────────────────────
514
+
515
+ _component.prototype._applyFilterAndSort = function () {
516
+ if (this.isDataDriven) {
517
+ const term = (this.currentSearch || '').trim().toLowerCase();
518
+ const filters = this.currentFilters || {};
519
+ const hasFilters = Object.keys(filters).length > 0;
520
+
521
+ // 1. Filter
522
+ this._filteredData = this._data.filter(function (row) {
523
+ if (term) {
524
+ let match = false;
525
+ for (const key in row) {
526
+ if (row.hasOwnProperty(key) && typeof row[key] === 'string' && key !== 'html' && key !== 'searchText') {
527
+ if (row[key].toLowerCase().indexOf(term) !== -1) {
528
+ match = true;
529
+ break;
530
+ }
531
+ }
532
+ }
533
+ if (!match) return false;
534
+ }
535
+
536
+ if (hasFilters) {
537
+ for (const field in filters) {
538
+ const activeVals = filters[field];
539
+ if (activeVals && activeVals.length > 0) {
540
+ const val = row[field];
541
+ const sVal = val != null ? String(val) : '';
542
+ if (activeVals.indexOf(sVal) === -1) return false;
543
+ }
544
+ }
545
+ }
546
+ return true;
547
+ });
548
+
549
+ this.visibleCount = this._filteredData.length;
550
+
551
+ // 2. Sort
552
+ if (!this.currentSort || !this.currentSort.field || !this.currentSort.direction) return;
553
+
554
+ const field = this.currentSort.field;
555
+ const direction = this.currentSort.direction;
556
+ const multiplier = direction === 'desc' ? -1 : 1;
557
+
558
+ const values = this._filteredData.map(function (row) { return row[field]; });
559
+ const type = detectValueType(values);
560
+
561
+ this._filteredData.sort(function (a, b) {
562
+ return compareValues(a[field], b[field], type, _collator) * multiplier;
563
+ });
564
+
565
+ } else {
566
+ const term = this._searchTerm;
567
+ const colFilters = this._columnFilters;
568
+ const hasColFilters = Object.keys(colFilters).length > 0;
569
+ const ths = this.ths;
570
+
571
+ const colIndexByKey = {};
572
+ if (hasColFilters) {
573
+ for (let i = 0; i < ths.length; i++) {
574
+ const filterKey = ths[i].getAttribute('data-ln-table-filter-col');
575
+ if (filterKey) colIndexByKey[filterKey] = i;
576
+ }
577
+ }
578
+
579
+ if (!term && !hasColFilters) {
580
+ this._filteredData = this._data.slice();
581
+ } else {
582
+ this._filteredData = this._data.filter(function (row) {
583
+ if (term && row.searchText.indexOf(term) === -1) return false;
584
+ if (hasColFilters) {
585
+ for (const key in colFilters) {
586
+ const idx = colIndexByKey[key];
587
+ if (idx !== undefined) {
588
+ if (colFilters[key].indexOf(row.rawTexts[idx]) === -1) return false;
589
+ }
590
+ }
591
+ }
592
+ return true;
593
+ });
594
+ }
595
+
596
+ if (this._sortCol < 0 || !this._sortDir) return;
597
+
598
+ const colIndex = this._sortCol;
599
+ const multiplier = this._sortDir === 'desc' ? -1 : 1;
600
+ const values = this._filteredData.map(function (row) { return row.values[colIndex]; });
601
+ const type = detectValueType(values);
602
+
603
+ this._filteredData.sort(function (a, b) {
604
+ return compareValues(a.values[colIndex], b.values[colIndex], type, _collator) * multiplier;
605
+ });
606
+ }
607
+ };
608
+
609
+ // ─── Column width locking ──────────────────────────────────
610
+
611
+ _component.prototype._lockColumnWidths = function () {
612
+ if (!this.table || !this.thead || this._colgroup) return;
613
+
614
+ const colgroup = document.createElement('colgroup');
615
+ this.ths.forEach(function (th) {
616
+ const col = document.createElement('col');
617
+ col.style.width = th.offsetWidth + 'px';
618
+ colgroup.appendChild(col);
619
+ });
620
+
621
+ this.table.insertBefore(colgroup, this.table.firstChild);
622
+ this.table.style.tableLayout = 'fixed';
623
+ this._colgroup = colgroup;
624
+ };
625
+
626
+ // ─── Render ────────────────────────────────────────────────
627
+
628
+ _component.prototype._render = function () {
629
+ if (!this.tbody) return;
630
+
631
+ if (this.isDataDriven) {
632
+ if (this._windowed) { this._renderWindowed(); return; }
633
+ const total = this._lastTotal;
634
+ const filtered = this.visibleCount;
635
+
636
+ if (total === 0) {
637
+ this._disableVirtualScroll();
638
+ this._showEmptyState();
639
+ return;
640
+ }
641
+
642
+ if (this._filteredData.length === 0 || filtered === 0) {
643
+ this._disableVirtualScroll();
644
+ this._showEmptyState();
645
+ return;
646
+ }
647
+
648
+ if (this._filteredData.length > VIRTUAL_THRESHOLD) {
649
+ this._enableVirtualScroll();
650
+ this._renderVirtual();
651
+ } else {
652
+ this._disableVirtualScroll();
653
+ this._renderAll();
654
+ }
655
+ } else {
656
+ const count = this._filteredData.length;
657
+
658
+ if (count === 0 && (this._searchTerm || Object.keys(this._columnFilters).length > 0)) {
659
+ this._disableVirtualScroll();
660
+ this._showEmptyState();
661
+ } else if (count > VIRTUAL_THRESHOLD) {
662
+ this._enableVirtualScroll();
663
+ this._renderVirtual();
664
+ } else {
665
+ this._disableVirtualScroll();
666
+ this._renderAll();
667
+ }
668
+ }
669
+ };
670
+
671
+ _component.prototype._renderAll = function () {
672
+ if (this.isDataDriven) {
673
+ const data = this._filteredData;
674
+ const frag = document.createDocumentFragment();
675
+
676
+ for (let i = 0; i < data.length; i++) {
677
+ const tr = this._buildRow(data[i]);
678
+ if (!tr) break;
679
+ frag.appendChild(tr);
680
+ }
681
+
682
+ const scrollState = _saveScroll(this);
683
+ this.tbody.textContent = '';
684
+ this.tbody.appendChild(frag);
685
+ _restoreScroll(scrollState);
686
+
687
+ if (this._selectable) this._updateSelectAll();
688
+ } else {
689
+ const html = [];
690
+ const data = this._filteredData;
691
+ for (let i = 0; i < data.length; i++) html.push(data[i].html);
692
+
693
+ const scrollState = _saveScroll(this);
694
+ this.tbody.innerHTML = html.join('');
695
+ _restoreScroll(scrollState);
696
+
697
+ if (this._selectable) this._restoreSelection();
698
+ }
699
+ };
700
+
701
+ // ─── Virtual scroll ────────────────────────────────────────
702
+
703
+ _component.prototype._enableVirtualScroll = function () {
704
+ if (this._virtual) return;
705
+ this._virtual = true;
706
+ this._vStart = -1;
707
+ this._vEnd = -1;
708
+ const self = this;
709
+
710
+ if (!this._rowHeight) {
711
+ if (this._windowed) {
712
+ let tempRow = null;
713
+ const sample = this._cache.peek();
714
+ if (sample) {
715
+ tempRow = this._buildRow(sample);
716
+ } else {
717
+ tempRow = this._buildPlaceholderRow();
718
+ }
719
+ if (tempRow) {
720
+ this.tbody.textContent = '';
721
+ this.tbody.appendChild(tempRow);
722
+ this._rowHeight = tempRow.offsetHeight || 40;
723
+ this.tbody.textContent = '';
724
+ }
725
+ } else if (this.isDataDriven) {
726
+ if (this._data.length > 0) {
727
+ const tempRow = this._buildRow(this._data[0]);
728
+ if (tempRow) {
729
+ this.tbody.textContent = '';
730
+ this.tbody.appendChild(tempRow);
731
+ this._rowHeight = tempRow.offsetHeight || 40;
732
+ this.tbody.textContent = '';
733
+ }
734
+ }
735
+ } else {
736
+ const rows = this.tbody ? this.tbody.rows : [];
737
+ if (rows.length > 0) {
738
+ this._rowHeight = rows[0].offsetHeight || 40;
739
+ }
740
+ }
741
+ }
742
+
743
+ if (this.isDataDriven) {
744
+ this._scrollContainer = _findScrollContainer(this.dom);
745
+ } else {
746
+ this._scrollContainer = null;
747
+ }
748
+ const scrollTarget = this._scrollContainer || window;
749
+
750
+ this._scrollHandler = function () {
751
+ if (self._rafId) return;
752
+ self._rafId = requestAnimationFrame(function () {
753
+ self._rafId = null;
754
+ self._windowed ? self._renderWindowed() : self._renderVirtual();
755
+ });
756
+ };
757
+
758
+ scrollTarget.addEventListener('scroll', this._scrollHandler, { passive: true });
759
+ window.addEventListener('resize', this._scrollHandler, { passive: true });
760
+ };
761
+
762
+ _component.prototype._disableVirtualScroll = function () {
763
+ if (!this._virtual) return;
764
+ this._virtual = false;
765
+
766
+ if (this._scrollHandler) {
767
+ const scrollTarget = this._scrollContainer || window;
768
+ scrollTarget.removeEventListener('scroll', this._scrollHandler);
769
+ window.removeEventListener('resize', this._scrollHandler);
770
+ this._scrollHandler = null;
771
+ }
772
+ this._scrollContainer = null;
773
+ if (this._rafId) {
774
+ cancelAnimationFrame(this._rafId);
775
+ this._rafId = null;
776
+ }
777
+ this._vStart = -1;
778
+ this._vEnd = -1;
779
+ };
780
+
781
+ _component.prototype._renderVirtual = function () {
782
+ const data = this._filteredData;
783
+ const total = data.length;
784
+ const rowH = this._rowHeight;
785
+ if (!rowH || !total) return;
786
+
787
+ const theadH = this.thead ? this.thead.offsetHeight : 0;
788
+ const sc = this._scrollContainer;
789
+ let scrollIntoData;
790
+ let viewportH;
791
+
792
+ if (sc) {
793
+ const tableRect = this.table.getBoundingClientRect();
794
+ const scRect = sc.getBoundingClientRect();
795
+ const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
796
+ scrollIntoData = sc.scrollTop - dataStartInContainer;
797
+ viewportH = sc.clientHeight;
798
+ } else {
799
+ const tableRect = this.table.getBoundingClientRect();
800
+ const tableTopInPage = tableRect.top + window.scrollY;
801
+ const dataStartInPage = tableTopInPage + theadH;
802
+ scrollIntoData = window.scrollY - dataStartInPage;
803
+ viewportH = window.innerHeight;
804
+ }
805
+
806
+ let startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
807
+ startRow = Math.min(startRow, total);
808
+ const endRow = Math.min(startRow + Math.ceil(viewportH / rowH) + (BUFFER_ROWS * 2), total);
809
+
810
+ if (startRow === this._vStart && endRow === this._vEnd) return;
811
+ this._vStart = startRow;
812
+ this._vEnd = endRow;
813
+
814
+ const colSpan = this.ths.length || 1;
815
+ const topH = startRow * rowH;
816
+ const bottomH = (total - endRow) * rowH;
817
+
818
+ if (this.isDataDriven) {
819
+ const frag = document.createDocumentFragment();
820
+
821
+ if (topH > 0) {
822
+ const topSpacer = document.createElement('tr');
823
+ topSpacer.className = 'ln-table__spacer';
824
+ topSpacer.setAttribute('aria-hidden', 'true');
825
+ const topTd = document.createElement('td');
826
+ topTd.setAttribute('colspan', colSpan);
827
+ topTd.style.height = topH + 'px';
828
+ topSpacer.appendChild(topTd);
829
+ frag.appendChild(topSpacer);
830
+ }
831
+
832
+ for (let i = startRow; i < endRow; i++) {
833
+ const tr = this._buildRow(data[i]);
834
+ if (tr) frag.appendChild(tr);
835
+ }
836
+
837
+ if (bottomH > 0) {
838
+ const bottomSpacer = document.createElement('tr');
839
+ bottomSpacer.className = 'ln-table__spacer';
840
+ bottomSpacer.setAttribute('aria-hidden', 'true');
841
+ const bottomTd = document.createElement('td');
842
+ bottomTd.setAttribute('colspan', colSpan);
843
+ bottomTd.style.height = bottomH + 'px';
844
+ bottomSpacer.appendChild(bottomTd);
845
+ frag.appendChild(bottomSpacer);
846
+ }
847
+
848
+ const scrollState = _saveScroll(this);
849
+ this.tbody.textContent = '';
850
+ this.tbody.appendChild(frag);
851
+ _restoreScroll(scrollState);
852
+
853
+ if (this._selectable) this._updateSelectAll();
854
+ } else {
855
+ let html = '';
856
+
857
+ if (topH > 0) {
858
+ html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
859
+ colSpan + '" style="height:' + topH + 'px;padding:0;border:none"></td></tr>';
860
+ }
861
+ for (let i = startRow; i < endRow; i++) html += data[i].html;
862
+ if (bottomH > 0) {
863
+ html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
864
+ colSpan + '" style="height:' + bottomH + 'px;padding:0;border:none"></td></tr>';
865
+ }
866
+
867
+ const scrollState = _saveScroll(this);
868
+ this.tbody.innerHTML = html;
869
+ _restoreScroll(scrollState);
870
+
871
+ if (this._selectable) this._restoreSelection();
872
+ }
873
+ };
874
+
875
+ // ─── Windowed virtualization (opt-in sliding window) ───────
876
+
877
+ _component.prototype._buildPlaceholderRow = function () {
878
+ const tr = document.createElement('tr');
879
+ tr.className = 'ln-table__placeholder';
880
+ tr.setAttribute('aria-hidden', 'true');
881
+ const td = document.createElement('td');
882
+ td.setAttribute('colspan', this.ths.length || 1);
883
+ td.style.height = this._rowHeight + 'px';
884
+ tr.appendChild(td);
885
+ return tr;
886
+ };
887
+
888
+ _component.prototype._renderWindowed = function () {
889
+ if (this.isLoaded && this._cache.logicalTotal === 0) {
890
+ this._disableVirtualScroll();
891
+ this._showEmptyState();
892
+ return;
893
+ }
894
+
895
+ if (!this._virtual) this._enableVirtualScroll();
896
+
897
+ const rowH = this._rowHeight;
898
+ if (!rowH) return;
899
+
900
+ const total = this._cache.logicalTotal;
901
+ const theadH = this.thead ? this.thead.offsetHeight : 0;
902
+ const sc = this._scrollContainer;
903
+ let scrollIntoData;
904
+ let viewportH;
905
+
906
+ if (sc) {
907
+ const tableRect = this.table.getBoundingClientRect();
908
+ const scRect = sc.getBoundingClientRect();
909
+ const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
910
+ scrollIntoData = sc.scrollTop - dataStartInContainer;
911
+ viewportH = sc.clientHeight;
912
+ } else {
913
+ const tableRect = this.table.getBoundingClientRect();
914
+ const tableTopInPage = tableRect.top + window.scrollY;
915
+ const dataStartInPage = tableTopInPage + theadH;
916
+ scrollIntoData = window.scrollY - dataStartInPage;
917
+ viewportH = window.innerHeight;
918
+ }
919
+
920
+ let startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
921
+ startRow = Math.min(startRow, total);
922
+ const endRow = Math.min(startRow + Math.ceil(viewportH / rowH) + (BUFFER_ROWS * 2), total);
923
+
924
+ const colSpan = this.ths.length || 1;
925
+ const topH = startRow * rowH;
926
+ const bottomH = (total - endRow) * rowH;
927
+
928
+ const frag = document.createDocumentFragment();
929
+
930
+ if (topH > 0) {
931
+ const topSpacer = document.createElement('tr');
932
+ topSpacer.className = 'ln-table__spacer';
933
+ topSpacer.setAttribute('aria-hidden', 'true');
934
+ const topTd = document.createElement('td');
935
+ topTd.setAttribute('colspan', colSpan);
936
+ topTd.style.height = topH + 'px';
937
+ topSpacer.appendChild(topTd);
938
+ frag.appendChild(topSpacer);
939
+ }
940
+
941
+ for (let i = startRow; i < endRow; i++) {
942
+ if (this._cache.has(i)) {
943
+ const tr = this._buildRow(this._cache.get(i));
944
+ if (tr) frag.appendChild(tr);
945
+ } else {
946
+ frag.appendChild(this._buildPlaceholderRow());
947
+ }
948
+ }
949
+
950
+ if (bottomH > 0) {
951
+ const bottomSpacer = document.createElement('tr');
952
+ bottomSpacer.className = 'ln-table__spacer';
953
+ bottomSpacer.setAttribute('aria-hidden', 'true');
954
+ const bottomTd = document.createElement('td');
955
+ bottomTd.setAttribute('colspan', colSpan);
956
+ bottomTd.style.height = bottomH + 'px';
957
+ bottomSpacer.appendChild(bottomTd);
958
+ frag.appendChild(bottomSpacer);
959
+ }
960
+
961
+ const scrollState = _saveScroll(this);
962
+ this.tbody.textContent = '';
963
+ this.tbody.appendChild(frag);
964
+ _restoreScroll(scrollState);
965
+
966
+ // Select-all is disabled in windowed mode (D4) — no _updateSelectAll() call.
967
+ this._vStart = startRow;
968
+ this._vEnd = endRow;
969
+
970
+ this._cache.ensure(startRow, endRow);
971
+ };
972
+
973
+ // ─── Empty state ───────────────────────────────────────────
974
+
975
+ _component.prototype._showEmptyState = function () {
976
+ const colSpan = this.ths.length || 1;
977
+ this.tbody.textContent = '';
978
+
979
+ let clone = null;
980
+
981
+ if (this.isDataDriven) {
982
+ const total = this._lastTotal != null ? this._lastTotal : this._data.length;
983
+ const filtered = this.visibleCount;
984
+ const isFiltered = (this.currentSearch || Object.keys(this.currentFilters).length > 0) && (filtered < total || filtered === 0);
985
+
986
+ const templateName = isFiltered ? (this.name + '-empty-filtered') : (this.name + '-empty');
987
+ clone = cloneTemplateScoped(this.dom, templateName, 'ln-table');
988
+
989
+ if (!clone) {
990
+ const genericTpl = this.dom.querySelector('template[data-ln-table-empty]');
991
+ if (genericTpl) {
992
+ const whenVal = isFiltered ? 'search' : 'initial';
993
+ const subEl = genericTpl.content.querySelector('[data-ln-table-empty-when="' + whenVal + '"]')
994
+ || genericTpl.content.firstElementChild;
995
+ if (subEl) {
996
+ clone = document.importNode(subEl, true);
997
+ }
998
+ }
999
+ }
1000
+
1001
+ if (clone) {
1002
+ if (clone.tagName === 'TR') {
1003
+ this.tbody.appendChild(clone);
1004
+ } else {
1005
+ const td = document.createElement('td');
1006
+ td.setAttribute('colspan', String(colSpan));
1007
+ td.appendChild(clone);
1008
+ const tr = document.createElement('tr');
1009
+ tr.className = 'ln-table__empty';
1010
+ tr.appendChild(td);
1011
+ this.tbody.appendChild(tr);
1012
+ }
1013
+ }
1014
+ } else {
1015
+ const tpl = this.dom.querySelector('template[' + EMPTY_TEMPLATE + ']');
1016
+ const td = document.createElement('td');
1017
+ td.setAttribute('colspan', String(colSpan));
1018
+ if (tpl) td.appendChild(document.importNode(tpl.content, true));
1019
+
1020
+ const tr = document.createElement('tr');
1021
+ tr.className = 'ln-table__empty';
1022
+ tr.appendChild(td);
1023
+ this.tbody.appendChild(tr);
1024
+ }
1025
+
1026
+ dispatch(this.dom, 'ln-table:empty', {
1027
+ term: this.isDataDriven ? (this.currentSearch || '') : this._searchTerm,
1028
+ total: this.isDataDriven ? (this._lastTotal != null ? this._lastTotal : this._data.length) : this._data.length
1029
+ });
1030
+ };
1031
+
1032
+ // ─── Templating / Row helpers ──────────────────────────────
1033
+
1034
+ _component.prototype._fillRow = function (tr, record) {
1035
+ fillTemplate(tr, record);
1036
+
1037
+ const cellAttrs = tr.querySelectorAll('[data-ln-table-cell-attr]');
1038
+ for (let i = 0; i < cellAttrs.length; i++) {
1039
+ const el = cellAttrs[i];
1040
+ const pairs = el.getAttribute('data-ln-table-cell-attr').split(',');
1041
+ for (let j = 0; j < pairs.length; j++) {
1042
+ const parts = pairs[j].trim().split(':');
1043
+ if (parts.length !== 2) continue;
1044
+ const field = parts[0].trim();
1045
+ const attr = parts[1].trim();
1046
+ if (record[field] != null) {
1047
+ el.setAttribute(attr, record[field]);
1048
+ }
1049
+ }
1050
+ }
1051
+ };
1052
+
1053
+ _component.prototype._buildRow = function (record) {
1054
+ const clone = cloneTemplateScoped(this.dom, this.name + '-row', 'ln-table');
1055
+ if (!clone) return null;
1056
+
1057
+ const tr = clone.querySelector('[data-ln-table-row]') || clone.firstElementChild;
1058
+ if (!tr) return null;
1059
+
1060
+ this._fillRow(tr, record);
1061
+
1062
+ tr._lnRecord = record;
1063
+ if (record.id != null) {
1064
+ tr.setAttribute('data-ln-table-row-id', record.id);
1065
+ }
1066
+
1067
+ if (this._selectable && record.id != null && this.selectedIds.has(String(record.id))) {
1068
+ tr.classList.add('ln-row-selected');
1069
+ const rowCb = tr.querySelector('[data-ln-table-row-select]');
1070
+ if (rowCb) rowCb.checked = true;
1071
+ }
1072
+
1073
+ return tr;
1074
+ };
1075
+
1076
+ _component.prototype._requestData = function () {
1077
+ if (this._windowed) {
1078
+ this.dom.classList.add('ln-table--loading');
1079
+ this._cache.invalidate({
1080
+ sort: this.currentSort,
1081
+ filters: this.currentFilters,
1082
+ search: this.currentSearch
1083
+ });
1084
+ return;
1085
+ }
1086
+ requestData(this, 'ln-table:request-data', 'table');
1087
+ };
1088
+
1089
+ // ─── Windowed Mode — enter/exit/seed (live toggle) ──────────
1090
+
1091
+ _component.prototype._enterWindowedMode = function () {
1092
+ const self = this;
1093
+ const dom = this.dom;
1094
+ const winAttr = parseInt(dom.getAttribute('data-ln-table-window'), 10);
1095
+ const pageAttr = parseInt(dom.getAttribute('data-ln-table-window-page'), 10);
1096
+ const threshAttr = parseInt(dom.getAttribute('data-ln-table-window-threshold'), 10);
1097
+
1098
+ // Cache change → sync totals off the cache, re-render (pull), notify.
1099
+ this._onCacheChange = function () {
1100
+ if (!self._windowed || !self._cache) return;
1101
+ self.totalCount = self._cache.grandTotal;
1102
+ self.visibleCount = self._cache.logicalTotal;
1103
+ self._lastTotal = self._cache.grandTotal;
1104
+ self.isLoaded = true;
1105
+ self._vStart = -1;
1106
+ self._vEnd = -1;
1107
+ self._render();
1108
+ self._updateFooter();
1109
+ dispatch(dom, 'ln-table:rendered', {
1110
+ table: self.name,
1111
+ total: self.totalCount,
1112
+ visible: self.visibleCount
1113
+ });
1114
+ };
1115
+ this._renderBatch = createBatcher(this._onCacheChange);
1116
+
1117
+ this._cache = createWindowCache({
1118
+ windowSize: winAttr > 0 ? winAttr : WINDOW_DEFAULT,
1119
+ pageSize: pageAttr > 0 ? pageAttr : WINDOW_PAGE,
1120
+ threshold: threshAttr >= 0 ? threshAttr : 25,
1121
+ fetchDebounce: FETCH_DEBOUNCE,
1122
+ requestPage: function (query, offset, limit) {
1123
+ dispatch(dom, 'ln-table:request-data', {
1124
+ table: self.name,
1125
+ sort: query.sort,
1126
+ filters: query.filters,
1127
+ search: query.search,
1128
+ offset: offset,
1129
+ limit: limit,
1130
+ queryGen: self._cache.queryGen
1131
+ });
1132
+ },
1133
+ onChange: this._renderBatch
1134
+ });
1135
+
1136
+ this._windowed = true;
1137
+
1138
+ // D4 — windowed tables cannot select rows they have never fetched
1139
+ if (this._selectable && this._selectAllCheckbox) {
1140
+ this._selectAllCheckbox.classList.add('hidden');
1141
+ }
1142
+ };
1143
+
1144
+ _component.prototype._kickWindowInitial = function () {
1145
+ if (this._data.length > 0) {
1146
+ // SSR-seeded / warm-seeded: page 0 is already resident, the grand
1147
+ // total is declared in markup — no initial fetch needed. No
1148
+ // queryGen on the seed — it must never be dropped as stale.
1149
+ let declaredTotal = parseInt(this.dom.getAttribute('data-ln-table-count'), 10);
1150
+ if (isNaN(declaredTotal) && this._totalSpan) {
1151
+ const spanText = this._totalSpan.textContent.replace(/[^\d]/g, '');
1152
+ if (spanText) declaredTotal = parseInt(spanText, 10);
1153
+ }
1154
+ const seedTotal = (declaredTotal > 0) ? declaredTotal : this._data.length;
1155
+ this._cache.ingest({
1156
+ data: this._data,
1157
+ offset: 0,
1158
+ total: seedTotal,
1159
+ filtered: seedTotal
1160
+ });
1161
+ } else {
1162
+ this.dom.classList.add('ln-table--loading');
1163
+ this._cache.requestInitial({
1164
+ sort: this.currentSort,
1165
+ filters: this.currentFilters,
1166
+ search: this.currentSearch
1167
+ });
1168
+ }
1169
+ };
1170
+
1171
+ _component.prototype._exitWindowedMode = function () {
1172
+ this._disableVirtualScroll();
1173
+ if (this._cache) this._cache.destroy();
1174
+ this._cache = null;
1175
+ this._windowed = false;
1176
+ this._renderBatch = null;
1177
+ this._onCacheChange = null;
1178
+ if (this._selectAllCheckbox) {
1179
+ this._selectAllCheckbox.classList.remove('hidden');
1180
+ }
1181
+ this._rowHeight = 0;
1182
+ this._vStart = -1;
1183
+ this._vEnd = -1;
1184
+ this._data = [];
1185
+ this._filteredData = [];
1186
+ this.dom.classList.add('ln-table--loading');
1187
+ this._requestData();
1188
+ };
1189
+
1190
+ // ─── Selection Helpers ─────────────────────────────────────
1191
+
1192
+ _component.prototype._updateSelectAll = function () {
1193
+ if (!this._selectAllCheckbox || !this.tbody) return;
1194
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1195
+ let allSelected = rows.length > 0;
1196
+ for (let i = 0; i < rows.length; i++) {
1197
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1198
+ if (id != null && !this.selectedIds.has(id)) {
1199
+ allSelected = false;
1200
+ break;
1201
+ }
1202
+ }
1203
+ this._selectAllCheckbox.checked = allSelected;
1204
+ };
1205
+
1206
+ // SSR re-render replays cached row HTML — reapply live selection state.
1207
+ _component.prototype._restoreSelection = function () {
1208
+ if (!this.tbody) return;
1209
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1210
+ for (let i = 0; i < rows.length; i++) {
1211
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1212
+ const selected = id != null && this.selectedIds.has(id);
1213
+ rows[i].classList.toggle('ln-row-selected', selected);
1214
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1215
+ if (cb) cb.checked = selected;
1216
+ }
1217
+ this._updateSelectAll();
1218
+ };
1219
+
1220
+ Object.defineProperty(_component.prototype, 'selectedCount', {
1221
+ get: function () { return this.selectedIds.size; },
1222
+ set: function () { /* computed from selectedIds */ }
1223
+ });
1224
+
1225
+ _component.prototype._enableSelection = function () {
1226
+ if (this._selectableActive) return;
1227
+ this._selectableActive = true;
1228
+
1229
+ const self = this;
1230
+ this._onSelectionChange = function (e) {
1231
+ const checkbox = e.target.closest('[data-ln-table-row-select]');
1232
+ if (!checkbox) return;
1233
+ const tr = checkbox.closest('[data-ln-table-row]');
1234
+ if (!tr) return;
1235
+ const id = tr.getAttribute('data-ln-table-row-id');
1236
+ if (id == null) return;
1237
+
1238
+ if (checkbox.checked) {
1239
+ self.selectedIds.add(id);
1240
+ tr.classList.add('ln-row-selected');
1241
+ } else {
1242
+ self.selectedIds.delete(id);
1243
+ tr.classList.remove('ln-row-selected');
1244
+ }
1245
+
1246
+ self.selectedCount = self.selectedIds.size;
1247
+ self._updateSelectAll();
1248
+ self._updateFooter();
1249
+
1250
+ dispatch(self.dom, 'ln-table:select', {
1251
+ table: self.name,
1252
+ selectedIds: self.selectedIds,
1253
+ count: self.selectedCount
1254
+ });
1255
+ };
1256
+ if (this.tbody) this.tbody.addEventListener('change', this._onSelectionChange);
1257
+
1258
+ this._selectAllCheckbox = this.dom.querySelector('[data-ln-table-col-select] input[type="checkbox"]')
1259
+ || this.dom.querySelector('[data-ln-table-col-select]');
1260
+ if (this._selectAllCheckbox && this._selectAllCheckbox.tagName === 'TH') {
1261
+ const cb = document.createElement('input');
1262
+ cb.type = 'checkbox';
1263
+ const dictEl = self.dom.querySelector('[data-ln-table-dict="select-all"]');
1264
+ const label = self.dom.getAttribute('data-ln-table-select-all-label') || (dictEl ? dictEl.textContent.trim() : null) || 'Select all';
1265
+ cb.setAttribute('aria-label', label);
1266
+ this._selectAllCheckbox.appendChild(cb);
1267
+ this._selectAllCheckbox = cb;
1268
+ }
1269
+
1270
+ if (this._selectAllCheckbox) {
1271
+ this._onSelectAll = function () {
1272
+ const checked = self._selectAllCheckbox.checked;
1273
+ const rows = self.tbody ? self.tbody.querySelectorAll('[data-ln-table-row]') : [];
1274
+
1275
+ for (let i = 0; i < rows.length; i++) {
1276
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1277
+ const rowCb = rows[i].querySelector('[data-ln-table-row-select]');
1278
+ if (id == null) continue;
1279
+
1280
+ if (checked) {
1281
+ self.selectedIds.add(id);
1282
+ rows[i].classList.add('ln-row-selected');
1283
+ } else {
1284
+ self.selectedIds.delete(id);
1285
+ rows[i].classList.remove('ln-row-selected');
1286
+ }
1287
+ if (rowCb) rowCb.checked = checked;
1288
+ }
1289
+
1290
+ self.selectedCount = self.selectedIds.size;
1291
+ dispatch(self.dom, 'ln-table:select-all', {
1292
+ table: self.name,
1293
+ selected: checked
1294
+ });
1295
+ dispatch(self.dom, 'ln-table:select', {
1296
+ table: self.name,
1297
+ selectedIds: self.selectedIds,
1298
+ count: self.selectedCount
1299
+ });
1300
+ self._updateFooter();
1301
+ };
1302
+ this._selectAllCheckbox.addEventListener('change', this._onSelectAll);
1303
+ }
1304
+
1305
+ if (this.tbody) {
1306
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1307
+ for (let i = 0; i < rows.length; i++) {
1308
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1309
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1310
+ if (cb && cb.checked && id != null) {
1311
+ this.selectedIds.add(id);
1312
+ rows[i].classList.add('ln-row-selected');
1313
+ }
1314
+ }
1315
+ this.selectedCount = this.selectedIds.size;
1316
+ if (this.selectedCount > 0) this._updateSelectAll();
1317
+ }
1318
+ };
1319
+
1320
+ _component.prototype._disableSelection = function () {
1321
+ if (!this._selectableActive) return;
1322
+ this._selectableActive = false;
1323
+
1324
+ if (this.tbody && this._onSelectionChange) {
1325
+ this.tbody.removeEventListener('change', this._onSelectionChange);
1326
+ }
1327
+ if (this._selectAllCheckbox && this._onSelectAll) {
1328
+ this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1329
+ }
1330
+
1331
+ const th = this.dom.querySelector('[data-ln-table-col-select]');
1332
+ if (th) {
1333
+ const cb = th.querySelector('input[type="checkbox"]');
1334
+ if (cb) {
1335
+ cb.remove();
1336
+ }
1337
+ }
1338
+ this._selectAllCheckbox = null;
1339
+
1340
+ this.selectedIds.clear();
1341
+ this.selectedCount = 0;
1342
+
1343
+ if (this.tbody) {
1344
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1345
+ for (let i = 0; i < rows.length; i++) {
1346
+ rows[i].classList.remove('ln-row-selected');
1347
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1348
+ if (cb) cb.checked = false;
1349
+ }
1350
+ }
1351
+
1352
+ this._updateFooter();
1353
+ };
1354
+
1355
+ // ─── Footer Helpers ────────────────────────────────────────
1356
+
1357
+ _component.prototype._updateFooter = function () {
1358
+ let total = 0;
1359
+ let filtered = 0;
1360
+
1361
+ if (this.isDataDriven) {
1362
+ total = this._lastTotal != null ? this._lastTotal : this._data.length;
1363
+ filtered = this.visibleCount;
1364
+ } else {
1365
+ total = this._data.length;
1366
+ filtered = this._filteredData.length;
1367
+ }
1368
+
1369
+ const isFiltered = filtered < total;
1370
+
1371
+ if (this._totalSpan) {
1372
+ this._totalSpan.textContent = _formatNum(total, this.dom);
1373
+ }
1374
+
1375
+ if (this._filteredSpan) {
1376
+ this._filteredSpan.textContent = isFiltered ? _formatNum(filtered, this.dom) : '';
1377
+ }
1378
+
1379
+ if (this._filteredWrap) {
1380
+ this._filteredWrap.classList.toggle('hidden', !isFiltered);
1381
+ }
1382
+
1383
+ if (this._selectedSpan) {
1384
+ const count = this.selectedIds ? this.selectedIds.size : 0;
1385
+ this._selectedSpan.textContent = count > 0 ? _formatNum(count, this.dom) : '';
1386
+ if (this._selectedWrap) {
1387
+ this._selectedWrap.classList.toggle('hidden', count === 0);
1388
+ }
1389
+ }
1390
+ };
1391
+
1392
+ // ─── Keyboard Focus Helpers ────────────────────────────────
1393
+
1394
+ _component.prototype._focusRow = function (rows) {
1395
+ for (let i = 0; i < rows.length; i++) {
1396
+ rows[i].classList.remove('ln-row-focused');
1397
+ rows[i].removeAttribute('tabindex');
1398
+ }
1399
+
1400
+ if (this._focusedRowIndex >= 0 && this._focusedRowIndex < rows.length) {
1401
+ const row = rows[this._focusedRowIndex];
1402
+ row.classList.add('ln-row-focused');
1403
+ row.setAttribute('tabindex', '0');
1404
+ row.focus();
1405
+ row.scrollIntoView({ block: 'nearest' });
1406
+ }
1407
+ };
1408
+
1409
+ // ─── Destroy ───────────────────────────────────────────────
1410
+
1411
+ _component.prototype.destroy = function () {
1412
+ if (!this.dom[DOM_ATTRIBUTE]) return;
1413
+ this._disableVirtualScroll();
1414
+
1415
+ this.dom.removeEventListener('ln-table:set-search', this._onSetSearch);
1416
+ this.dom.removeEventListener('ln-table:set-filter', this._onSetFilter);
1417
+ this.dom.removeEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
1418
+
1419
+ if (this.isDataDriven) {
1420
+ this.dom.removeEventListener('ln-table:set-data', this._onSetData);
1421
+ this.dom.removeEventListener('ln-table:set-loading', this._onSetLoading);
1422
+ this.dom.removeEventListener('ln-table:page-failed', this._onPageFailed);
1423
+ this.dom.removeEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
1424
+ this.dom.removeEventListener('ln-sort:change', this._onSort);
1425
+ document.removeEventListener('keydown', this._onKeydown);
1426
+ if (this.tbody) {
1427
+ this.tbody.removeEventListener('click', this._onRowClick);
1428
+ this.tbody.removeEventListener('click', this._onRowAction);
1429
+ }
1430
+ if (this._cache) this._cache.destroy();
1431
+ } else {
1432
+ if (this._emptyTbodyObserver) {
1433
+ this._emptyTbodyObserver.disconnect();
1434
+ this._emptyTbodyObserver = null;
1435
+ }
1436
+ this.dom.removeEventListener('ln-sort:change', this._onSort);
1437
+ this.dom.removeEventListener('ln-search:change', this._onSearchChange);
1438
+ }
1439
+
1440
+ if (this._onSelectionChange && this.tbody) this.tbody.removeEventListener('change', this._onSelectionChange);
1441
+ if (this._selectAllCheckbox && this._onSelectAll) this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1442
+
1443
+ if (this._colgroup) {
1444
+ this._colgroup.remove();
1445
+ this._colgroup = null;
1446
+ }
1447
+ if (this.table) this.table.style.tableLayout = '';
1448
+ this._data = [];
1449
+ this._filteredData = [];
1450
+ delete this.dom[DOM_ATTRIBUTE];
1451
+ };
1452
+
1453
+ // ─── Init ──────────────────────────────────────────────────
1454
+
1455
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-table', {
1456
+ extraAttributes: [
1457
+ 'data-ln-table-window',
1458
+ 'data-ln-table-window-page',
1459
+ 'data-ln-table-window-threshold',
1460
+ 'data-ln-table-count'
1461
+ ],
1462
+ onAttributeChange: function (el, attrName) {
1463
+ const inst = el[DOM_ATTRIBUTE];
1464
+ if (!inst || !inst.isDataDriven) return;
1465
+
1466
+ if (attrName === 'data-ln-table-window') {
1467
+ const present = el.hasAttribute('data-ln-table-window');
1468
+ if (present && !inst._windowed) {
1469
+ inst._enterWindowedMode();
1470
+ inst._kickWindowInitial();
1471
+ } else if (!present && inst._windowed) {
1472
+ inst._exitWindowedMode();
1473
+ } else if (present && inst._windowed) {
1474
+ const v = parseInt(el.getAttribute('data-ln-table-window'), 10);
1475
+ if (v > 0) inst._cache.configure({ windowSize: v });
1476
+ }
1477
+ return;
1478
+ }
1479
+
1480
+ if (!inst._windowed || !inst._cache) return;
1481
+ if (attrName === 'data-ln-table-window-page') {
1482
+ const v = parseInt(el.getAttribute('data-ln-table-window-page'), 10);
1483
+ if (v > 0) inst._cache.configure({ pageSize: v });
1484
+ } else if (attrName === 'data-ln-table-window-threshold') {
1485
+ const v = parseInt(el.getAttribute('data-ln-table-window-threshold'), 10);
1486
+ if (v >= 0) inst._cache.configure({ threshold: v });
1487
+ } else if (attrName === 'data-ln-table-count') {
1488
+ const v = parseInt(el.getAttribute('data-ln-table-count'), 10);
1489
+ if (v >= 0) inst._cache.setGrandTotal(v);
1490
+ }
1491
+ }
1492
+ });
1493
+ })();