@livenetworks/ashlar 1.5.1 → 1.6.1

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 (217) 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 +165 -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 +82 -42
  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 +92 -2
  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 -56
  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 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  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 +11 -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 +3 -5
  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 +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  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 +85 -60
  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 +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  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 -1087
  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 -156
@@ -0,0 +1,1216 @@
1
+ import { cloneTemplateScoped, dispatch, dispatchCancelable, requestData, fill, fillTemplate, registerComponent, createWindowCache, createBatcher, getLocale, detectValueType, compareValues } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-list';
5
+ const DOM_ATTRIBUTE = 'lnList';
6
+ const EMPTY_TEMPLATE = 'data-ln-list-empty';
7
+ const VIRTUAL_THRESHOLD = 200;
8
+ const BUFFER_ROWS = 15;
9
+ const WINDOW_DEFAULT = 1000;
10
+ const WINDOW_PAGE = 200;
11
+ const FETCH_DEBOUNCE = 120;
12
+
13
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
14
+
15
+ function _formatNum(n, dom) {
16
+ if (n == null || isNaN(n)) return '';
17
+ try {
18
+ return new Intl.NumberFormat(getLocale(dom)).format(n);
19
+ } catch (e) {
20
+ return String(n);
21
+ }
22
+ }
23
+
24
+ function _findScrollContainer(el) {
25
+ let p = el;
26
+ while (p && p !== document.body && p !== document.documentElement) {
27
+ const cs = getComputedStyle(p);
28
+ const oy = cs.overflowY;
29
+ if (oy === 'auto' || oy === 'scroll') return p;
30
+ p = p.parentElement;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function _saveScroll(self) {
36
+ const sc = self._scrollContainer || _findScrollContainer(self.dom);
37
+ return {
38
+ container: sc,
39
+ top: sc ? sc.scrollTop : window.scrollY
40
+ };
41
+ }
42
+
43
+ function _restoreScroll(state) {
44
+ if (state.container) {
45
+ state.container.scrollTop = state.top;
46
+ } else {
47
+ window.scrollTo(window.scrollX, state.top);
48
+ }
49
+ }
50
+
51
+
52
+ function _getOuterHeight(el) {
53
+ if (!el) return 0;
54
+ const cs = getComputedStyle(el);
55
+ const mt = parseFloat(cs.marginTop) || 0;
56
+ const mb = parseFloat(cs.marginBottom) || 0;
57
+ return el.offsetHeight + mt + mb;
58
+ }
59
+
60
+ // ─── Component ─────────────────────────────────────────────
61
+
62
+ function _component(dom) {
63
+ this.dom = dom;
64
+ this.tbody = dom.querySelector('[data-ln-list-body]') || dom;
65
+ this.isDataDriven = dom.hasAttribute('data-ln-list-source');
66
+ this.name = dom.getAttribute(DOM_SELECTOR) || '';
67
+ this.source = dom.getAttribute('data-ln-list-source') || '';
68
+
69
+ // Footer elements — both modes
70
+ this._totalSpan = dom.querySelector('[data-ln-list-total]');
71
+ this._filteredSpan = dom.querySelector('[data-ln-list-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-list-selected]');
78
+ if (this._selectedSpan) {
79
+ this._selectedWrap = this._selectedSpan.parentElement !== dom
80
+ ? this._selectedSpan.parentElement
81
+ : null;
82
+ }
83
+
84
+ this._data = [];
85
+ this._filteredData = [];
86
+ this._searchTerm = '';
87
+ this._columnFilters = {};
88
+ this.selectedIds = new Set();
89
+
90
+ // Virtual scroll state
91
+ this._virtual = false;
92
+ this._itemHeight = 0;
93
+ this._vStart = -1;
94
+ this._vEnd = -1;
95
+ this._rafId = null;
96
+ this._scrollHandler = null;
97
+ this._resizeHandler = null;
98
+ this._scrollContainer = null;
99
+ this.isUl = this.tbody.tagName === 'UL' || this.tbody.tagName === 'OL';
100
+
101
+ const self = this;
102
+
103
+ // --- Selection (both modes) ---
104
+ this._selectable = dom.hasAttribute('data-ln-list-selectable');
105
+ this._selectableActive = false;
106
+ if (this._selectable) {
107
+ this._enableSelection();
108
+ }
109
+
110
+ if (this.isDataDriven) {
111
+ this.isLoaded = false;
112
+ this.totalCount = 0;
113
+ this.visibleCount = 0;
114
+ this.currentSort = null;
115
+ this.currentFilters = {};
116
+ this.currentSearch = '';
117
+
118
+ this._windowed = false;
119
+ this._cache = null;
120
+ if (this.isDataDriven && dom.hasAttribute('data-ln-list-window')) this._enterWindowedMode();
121
+
122
+ this._lastTotal = 0;
123
+ this._lastFiltered = 0;
124
+
125
+ // --- Event listeners ---
126
+ this._onSetData = function (e) {
127
+ const detail = e.detail || {};
128
+ if (self._windowed) {
129
+ dom.classList.remove('ln-list--loading');
130
+ self._cache.ingest(detail);
131
+ return;
132
+ }
133
+ self._data = detail.data || [];
134
+ self._lastTotal = detail.total != null ? detail.total : self._data.length;
135
+ self._lastFiltered = detail.filtered != null ? detail.filtered : self._data.length;
136
+
137
+ self.totalCount = self._lastTotal;
138
+ self.visibleCount = self._lastFiltered;
139
+ self.isLoaded = true;
140
+
141
+ dom.classList.remove('ln-list--loading');
142
+
143
+ self._vStart = -1;
144
+ self._vEnd = -1;
145
+
146
+ self._applyFilterAndSort();
147
+ self._render();
148
+ self._updateFooter();
149
+
150
+ dispatch(dom, 'ln-list:rendered', {
151
+ list: self.name,
152
+ total: self.totalCount,
153
+ visible: self.visibleCount
154
+ });
155
+ };
156
+ dom.addEventListener('ln-list:set-data', this._onSetData);
157
+
158
+ this._onSetLoading = function (e) {
159
+ const loading = e.detail && e.detail.loading;
160
+ dom.classList.toggle('ln-list--loading', !!loading);
161
+ if (loading) {
162
+ self.isLoaded = false;
163
+ }
164
+ };
165
+ dom.addEventListener('ln-list:set-loading', this._onSetLoading);
166
+
167
+ this._onPageFailed = function (e) {
168
+ if (!self._windowed || !self._cache) return;
169
+ self._cache.release(e.detail && e.detail.offset);
170
+ };
171
+ dom.addEventListener('ln-list:page-failed', this._onPageFailed);
172
+
173
+ this._onRequestRevalidate = function () {
174
+ if (!self._windowed || !self._cache) return;
175
+ self._cache.revalidate();
176
+ };
177
+ dom.addEventListener('ln-list:request-revalidate', this._onRequestRevalidate);
178
+
179
+ // Clear all
180
+ this._onClearAll = function (e) {
181
+ const btn = e.target.closest('[data-ln-list-clear-all]');
182
+ if (!btn) return;
183
+ self.currentFilters = {};
184
+ dispatch(dom, 'ln-list:clear-filters', { list: self.name });
185
+ self._requestData();
186
+ };
187
+ dom.addEventListener('click', this._onClearAll);
188
+
189
+ // --- Sort ---
190
+ // Mirrors ln-table's own data-driven ln-sort:change handler: windowed lists
191
+ // invalidate the sliding-window cache and re-fetch (via the existing
192
+ // _requestData(), which already branches on this._windowed — see its
193
+ // definition further down this file); non-windowed lists re-sort the
194
+ // already-fetched in-memory data locally, no server round-trip.
195
+ this._onSort = function (e) {
196
+ // Index-only event (field === null) has nothing to key a record by — ignore.
197
+ if (e.detail.field == null) return;
198
+ e.preventDefault();
199
+ self.currentSort = e.detail.direction === 'none' ? null : { field: e.detail.field, direction: e.detail.direction };
200
+ if (self._windowed) {
201
+ self._requestData();
202
+ } else {
203
+ self._applyFilterAndSort();
204
+ self._vStart = -1;
205
+ self._vEnd = -1;
206
+ self._render();
207
+ }
208
+ };
209
+ dom.addEventListener('ln-sort:change', this._onSort);
210
+
211
+ // D4 — windowed lists cannot select rows they have never fetched
212
+ if (this._windowed && this._selectable && this._selectAllCheckbox) {
213
+ this._selectAllCheckbox.classList.add('hidden');
214
+ }
215
+
216
+ // --- Item Click & Actions ---
217
+ this._onItemClick = function (e) {
218
+ if (e.target.closest('[data-ln-item-select]')) return;
219
+ if (e.target.closest('[data-ln-item-action]')) return;
220
+ if (e.target.closest('a') || e.target.closest('button')) return;
221
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
222
+
223
+ const itemEl = e.target.closest('[data-ln-item]');
224
+ if (!itemEl) return;
225
+
226
+ const id = itemEl.getAttribute('data-ln-item-id');
227
+ const record = itemEl._lnRecord || {};
228
+
229
+ dispatch(dom, 'ln-list:item-click', {
230
+ list: self.name,
231
+ id: id,
232
+ record: record
233
+ });
234
+ };
235
+ if (this.tbody) this.tbody.addEventListener('click', this._onItemClick);
236
+
237
+ this._onItemAction = function (e) {
238
+ const btn = e.target.closest('[data-ln-item-action]');
239
+ if (!btn) return;
240
+
241
+ e.stopPropagation();
242
+ const itemEl = btn.closest('[data-ln-item]');
243
+ if (!itemEl) return;
244
+
245
+ const action = btn.getAttribute('data-ln-item-action');
246
+ const id = itemEl.getAttribute('data-ln-item-id');
247
+ const record = itemEl._lnRecord || {};
248
+
249
+ dispatch(dom, 'ln-list:item-action', {
250
+ list: self.name,
251
+ id: id,
252
+ action: action,
253
+ record: record
254
+ });
255
+ };
256
+ if (this.tbody) this.tbody.addEventListener('click', this._onItemAction);
257
+
258
+ // --- Search Change ---
259
+ this._onSearchChange = function (e) {
260
+ e.preventDefault();
261
+ self.currentSearch = (e.detail && e.detail.term) || '';
262
+ dispatch(dom, 'ln-list:search', {
263
+ list: self.name,
264
+ query: self.currentSearch
265
+ });
266
+ self._requestData();
267
+ };
268
+ dom.addEventListener('ln-search:change', this._onSearchChange);
269
+
270
+ // Local hydration of initial items
271
+ if (this.tbody && this.tbody.children.length > 0) {
272
+ this._parseChildren();
273
+ }
274
+
275
+ // Initial request-data
276
+ if (this._windowed) {
277
+ this._kickWindowInitial();
278
+ } else {
279
+ dispatch(dom, 'ln-list:request-data', {
280
+ list: this.name,
281
+ sort: this.currentSort,
282
+ filters: this.currentFilters,
283
+ search: this.currentSearch
284
+ });
285
+ }
286
+
287
+ } else {
288
+ // SSR Mode
289
+ this._emptyObserver = null;
290
+
291
+ if (this.tbody && this.tbody.children.length > 0) {
292
+ this._parseChildren();
293
+ } else if (this.tbody) {
294
+ this._emptyObserver = new MutationObserver(function () {
295
+ if (self.tbody.children.length > 0) {
296
+ self._emptyObserver.disconnect();
297
+ self._emptyObserver = null;
298
+ self._parseChildren();
299
+ }
300
+ });
301
+ this._emptyObserver.observe(this.tbody, { childList: true });
302
+ }
303
+
304
+ this._onSearch = function (e) {
305
+ e.preventDefault();
306
+ self._searchTerm = (e.detail && e.detail.term) || '';
307
+ self._applyFilterAndSort();
308
+ self._vStart = -1;
309
+ self._vEnd = -1;
310
+ self._render();
311
+ self._updateFooter();
312
+ dispatch(dom, 'ln-list:filter', {
313
+ term: self._searchTerm,
314
+ matched: self._filteredData.length,
315
+ total: self._data.length
316
+ });
317
+ };
318
+ dom.addEventListener('ln-search:change', this._onSearch);
319
+ }
320
+
321
+ this._onClear = function (e) {
322
+ const btn = e.target.closest('[data-ln-list-clear]');
323
+ if (!btn) return;
324
+
325
+ const before = dispatchCancelable(dom, 'ln-list:before-clear-search', { list: self.name });
326
+ if (before.defaultPrevented) return;
327
+
328
+ if (self.isDataDriven) {
329
+ self.currentSearch = '';
330
+ } else {
331
+ self._searchTerm = '';
332
+ }
333
+
334
+ const searchEl = document.querySelector('[data-ln-search="' + dom.id + '"]');
335
+ if (searchEl) {
336
+ const input = searchEl.tagName === 'INPUT' ? searchEl : searchEl.querySelector('input');
337
+ if (input) {
338
+ input.value = '';
339
+ input.dispatchEvent(new Event('input', { bubbles: true }));
340
+ }
341
+ }
342
+
343
+ if (self.isDataDriven) {
344
+ dispatch(dom, 'ln-list:search', {
345
+ list: self.name,
346
+ query: ''
347
+ });
348
+ self._requestData();
349
+ } else {
350
+ self._applyFilterAndSort();
351
+ self._vStart = -1;
352
+ self._vEnd = -1;
353
+ self._render();
354
+ self._updateFooter();
355
+ dispatch(dom, 'ln-list:filter', {
356
+ term: '',
357
+ matched: self._filteredData.length,
358
+ total: self._data.length
359
+ });
360
+ }
361
+ };
362
+ dom.addEventListener('click', this._onClear);
363
+
364
+ return this;
365
+ }
366
+
367
+ // ─── Parse Children into in-memory array ───────────────────
368
+
369
+ _component.prototype._parseChildren = function () {
370
+ const children = Array.from(this.tbody.children).filter(el => !el.classList.contains('ln-list__spacer'));
371
+ this._data = [];
372
+
373
+ if (children.length > 0) this._itemHeight = _getOuterHeight(children[0]) || 50;
374
+
375
+ for (let i = 0; i < children.length; i++) {
376
+ const el = children[i];
377
+ const id = el.getAttribute('data-ln-item-id') || el.getAttribute('id');
378
+ const text = el.textContent.trim().toLowerCase();
379
+
380
+ let record = null;
381
+ if (this.isDataDriven) {
382
+ record = {};
383
+ if (id != null) record.id = id;
384
+
385
+ const fields = el.querySelectorAll('[data-ln-list-field]');
386
+ for (let j = 0; j < fields.length; j++) {
387
+ const f = fields[j];
388
+ const prop = f.getAttribute('data-ln-list-field');
389
+ if (prop) {
390
+ record[prop] = f.textContent.trim();
391
+ }
392
+ }
393
+ }
394
+
395
+ this._data.push({
396
+ html: el.outerHTML,
397
+ searchText: text,
398
+ id: id,
399
+ ...record
400
+ });
401
+ }
402
+
403
+ this._filteredData = this._data.slice();
404
+
405
+ if (this.isDataDriven) {
406
+ this._lastTotal = this._data.length;
407
+ this._lastFiltered = this._data.length;
408
+ this.totalCount = this._data.length;
409
+ this.visibleCount = this._data.length;
410
+ this._updateFooter();
411
+ }
412
+
413
+ this._render();
414
+
415
+ dispatch(this.dom, 'ln-list:ready', {
416
+ total: this._data.length
417
+ });
418
+ };
419
+
420
+ // ─── Filter + Sort ─────────────────────────────────────────
421
+
422
+ _component.prototype._applyFilterAndSort = function () {
423
+ if (this.isDataDriven) {
424
+ const term = (this.currentSearch || '').trim().toLowerCase();
425
+ const filters = this.currentFilters || {};
426
+ const hasFilters = Object.keys(filters).length > 0;
427
+
428
+ // 1. Filter
429
+ this._filteredData = this._data.filter(function (row) {
430
+ if (term) {
431
+ let match = false;
432
+ for (const key in row) {
433
+ if (row.hasOwnProperty(key) && typeof row[key] === 'string' && key !== 'html' && key !== 'searchText') {
434
+ if (row[key].toLowerCase().indexOf(term) !== -1) {
435
+ match = true;
436
+ break;
437
+ }
438
+ }
439
+ }
440
+ if (!match) return false;
441
+ }
442
+
443
+ if (hasFilters) {
444
+ for (const field in filters) {
445
+ const activeVals = filters[field];
446
+ if (activeVals && activeVals.length > 0) {
447
+ const val = row[field];
448
+ const sVal = val != null ? String(val) : '';
449
+ if (activeVals.indexOf(sVal) === -1) return false;
450
+ }
451
+ }
452
+ }
453
+ return true;
454
+ });
455
+
456
+ this.visibleCount = this._filteredData.length;
457
+
458
+ // 2. Sort
459
+ if (!this.currentSort || !this.currentSort.field || !this.currentSort.direction) return;
460
+
461
+ const field = this.currentSort.field;
462
+ const multiplier = this.currentSort.direction === 'desc' ? -1 : 1;
463
+
464
+ const values = this._filteredData.map(function (row) { return row[field]; });
465
+ const type = detectValueType(values);
466
+ const collator = typeof Intl !== 'undefined'
467
+ ? new Intl.Collator(getLocale(this.dom), { sensitivity: 'base' })
468
+ : null;
469
+
470
+ this._filteredData.sort(function (a, b) {
471
+ return compareValues(a[field], b[field], type, collator) * multiplier;
472
+ });
473
+
474
+ } else {
475
+ const term = this._searchTerm;
476
+ if (!term) {
477
+ this._filteredData = this._data.slice();
478
+ } else {
479
+ this._filteredData = this._data.filter(function (row) {
480
+ return row.searchText.indexOf(term) !== -1;
481
+ });
482
+ }
483
+ }
484
+ };
485
+
486
+ // ─── Render ────────────────────────────────────────────────
487
+
488
+ _component.prototype._render = function () {
489
+ if (!this.tbody) return;
490
+
491
+ if (this.isDataDriven) {
492
+ if (this._windowed) { this._renderWindowed(); return; }
493
+ const total = this._lastTotal;
494
+ const filtered = this.visibleCount;
495
+
496
+ if (total === 0 || this._filteredData.length === 0 || filtered === 0) {
497
+ this._disableVirtualScroll();
498
+ this._showEmptyState();
499
+ return;
500
+ }
501
+
502
+ if (this._filteredData.length > VIRTUAL_THRESHOLD) {
503
+ this._enableVirtualScroll();
504
+ this._renderVirtual();
505
+ } else {
506
+ this._disableVirtualScroll();
507
+ this._renderAll();
508
+ }
509
+ } else {
510
+ const count = this._filteredData.length;
511
+
512
+ if (count === 0 && this._searchTerm) {
513
+ this._disableVirtualScroll();
514
+ this._showEmptyState();
515
+ } else if (count > VIRTUAL_THRESHOLD) {
516
+ this._enableVirtualScroll();
517
+ this._renderVirtual();
518
+ } else {
519
+ this._disableVirtualScroll();
520
+ this._renderAll();
521
+ }
522
+ }
523
+ };
524
+
525
+ _component.prototype._renderAll = function () {
526
+ if (this.isDataDriven) {
527
+ const data = this._filteredData;
528
+ const frag = document.createDocumentFragment();
529
+
530
+ for (let i = 0; i < data.length; i++) {
531
+ const el = this._buildItem(data[i]);
532
+ if (!el) break;
533
+ frag.appendChild(el);
534
+ }
535
+
536
+ const scrollState = _saveScroll(this);
537
+ this.tbody.textContent = '';
538
+ this.tbody.appendChild(frag);
539
+ _restoreScroll(scrollState);
540
+
541
+ if (this._selectable) this._updateSelectAll();
542
+ } else {
543
+ const html = [];
544
+ const data = this._filteredData;
545
+ for (let i = 0; i < data.length; i++) html.push(data[i].html);
546
+
547
+ const scrollState = _saveScroll(this);
548
+ this.tbody.innerHTML = html.join('');
549
+ _restoreScroll(scrollState);
550
+
551
+ if (this._selectable) this._restoreSelection();
552
+ }
553
+ };
554
+
555
+ // ─── Virtual scroll ────────────────────────────────────────
556
+
557
+ _component.prototype._readGridLayout = function () {
558
+ const cs = getComputedStyle(this.tbody);
559
+ const tracks = cs.gridTemplateColumns;
560
+ let columns = 1;
561
+ if (tracks && tracks !== 'none') {
562
+ const parts = tracks.trim().split(/\s+/).filter(Boolean);
563
+ if (parts.length > 0) columns = parts.length;
564
+ }
565
+ const rowGap = parseFloat(cs.rowGap);
566
+ return { columns: columns, rowGap: isNaN(rowGap) ? 0 : rowGap };
567
+ };
568
+
569
+ _component.prototype._measureItemHeight = function () {
570
+ if (this._windowed) {
571
+ const sample = this._cache.peek();
572
+ const el = sample ? this._buildItem(sample) : this._buildPlaceholderItem();
573
+ if (el) {
574
+ this.tbody.textContent = '';
575
+ this.tbody.appendChild(el);
576
+ this._itemHeight = _getOuterHeight(el) || 50;
577
+ this.tbody.textContent = '';
578
+ }
579
+ } else if (this.isDataDriven) {
580
+ if (this._data.length > 0) {
581
+ const el = this._buildItem(this._data[0]);
582
+ if (el) {
583
+ this.tbody.textContent = '';
584
+ this.tbody.appendChild(el);
585
+ this._itemHeight = _getOuterHeight(el) || 50;
586
+ this.tbody.textContent = '';
587
+ }
588
+ }
589
+ } else {
590
+ const children = this.tbody.children;
591
+ if (children.length > 0) {
592
+ this._itemHeight = _getOuterHeight(children[0]) || 50;
593
+ }
594
+ }
595
+ };
596
+
597
+ _component.prototype._enableVirtualScroll = function () {
598
+ if (this._virtual) return;
599
+ this._virtual = true;
600
+ this._vStart = -1;
601
+ this._vEnd = -1;
602
+ const self = this;
603
+
604
+ if (!this._itemHeight) {
605
+ this._measureItemHeight();
606
+ }
607
+
608
+ this._scrollContainer = _findScrollContainer(this.dom);
609
+ const container = this._scrollContainer || window;
610
+
611
+ this._scrollHandler = function () {
612
+ if (!self._rafId) {
613
+ self._rafId = requestAnimationFrame(function () {
614
+ self._rafId = null;
615
+ self._windowed ? self._renderWindowed() : self._renderVirtual();
616
+ });
617
+ }
618
+ };
619
+
620
+ this._resizeHandler = function () {
621
+ self._itemHeight = 0;
622
+ self._measureItemHeight();
623
+ self._vStart = -1;
624
+ self._vEnd = -1;
625
+ self._windowed ? self._renderWindowed() : self._renderVirtual();
626
+ };
627
+
628
+ container.addEventListener('scroll', this._scrollHandler, { passive: true });
629
+ window.addEventListener('resize', this._resizeHandler, { passive: true });
630
+ };
631
+
632
+ _component.prototype._disableVirtualScroll = function () {
633
+ if (!this._virtual) return;
634
+ this._virtual = false;
635
+
636
+ if (this._scrollHandler) {
637
+ const container = this._scrollContainer || window;
638
+ container.removeEventListener('scroll', this._scrollHandler);
639
+ this._scrollHandler = null;
640
+ }
641
+ if (this._resizeHandler) {
642
+ window.removeEventListener('resize', this._resizeHandler);
643
+ this._resizeHandler = null;
644
+ }
645
+ this._scrollContainer = null;
646
+
647
+ if (this._rafId) {
648
+ cancelAnimationFrame(this._rafId);
649
+ this._rafId = null;
650
+ }
651
+ this._vStart = -1;
652
+ this._vEnd = -1;
653
+ };
654
+
655
+ _component.prototype._renderVirtual = function () {
656
+ const data = this._filteredData;
657
+ const count = data.length;
658
+ const itemHeight = this._itemHeight;
659
+ if (!itemHeight || !count) return;
660
+
661
+ const container = this._scrollContainer;
662
+ let scrollTop, viewportHeight;
663
+
664
+ if (container) {
665
+ const rect = this.tbody.getBoundingClientRect();
666
+ const containerRect = container.getBoundingClientRect();
667
+ const relativeTop = (container === this.tbody) ? 0 : (rect.top - containerRect.top + container.scrollTop);
668
+ scrollTop = container.scrollTop - relativeTop;
669
+ viewportHeight = container.clientHeight;
670
+ } else {
671
+ const rect = this.tbody.getBoundingClientRect();
672
+ const absoluteTop = rect.top + window.scrollY;
673
+ scrollTop = window.scrollY - absoluteTop;
674
+ viewportHeight = window.innerHeight;
675
+ }
676
+
677
+ const layout = this._readGridLayout();
678
+ const columns = layout.columns;
679
+ const rowGap = layout.rowGap;
680
+ const rowHeight = itemHeight + rowGap;
681
+ const totalRows = Math.ceil(count / columns);
682
+
683
+ let firstRow = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
684
+ firstRow = Math.min(firstRow, totalRows);
685
+ const visibleRows = Math.ceil(viewportHeight / rowHeight) + BUFFER_ROWS * 2;
686
+ const lastRow = Math.min(firstRow + visibleRows, totalRows);
687
+
688
+ const start = Math.min(firstRow * columns, count);
689
+ const end = Math.min(lastRow * columns, count);
690
+
691
+ if (start === this._vStart && end === this._vEnd) return;
692
+
693
+ this._vStart = start;
694
+ this._vEnd = end;
695
+
696
+ const topSpacerHeight = firstRow * rowHeight;
697
+ const bottomSpacerHeight = (totalRows - lastRow) * rowHeight;
698
+
699
+ if (this.isDataDriven) {
700
+ const frag = document.createDocumentFragment();
701
+
702
+ if (topSpacerHeight > 0) {
703
+ const topSpacer = document.createElement(this.isUl ? 'li' : 'div');
704
+ topSpacer.className = 'ln-list__spacer';
705
+ topSpacer.style.height = topSpacerHeight + 'px';
706
+ frag.appendChild(topSpacer);
707
+ }
708
+
709
+ for (let i = start; i < end; i++) {
710
+ const el = this._buildItem(data[i]);
711
+ if (el) frag.appendChild(el);
712
+ }
713
+
714
+ if (bottomSpacerHeight > 0) {
715
+ const bottomSpacer = document.createElement(this.isUl ? 'li' : 'div');
716
+ bottomSpacer.className = 'ln-list__spacer';
717
+ bottomSpacer.style.height = bottomSpacerHeight + 'px';
718
+ frag.appendChild(bottomSpacer);
719
+ }
720
+
721
+ const scrollState = _saveScroll(this);
722
+ this.tbody.textContent = '';
723
+ this.tbody.appendChild(frag);
724
+ _restoreScroll(scrollState);
725
+
726
+ if (this._selectable) this._updateSelectAll();
727
+ } else {
728
+ let html = '';
729
+ if (topSpacerHeight > 0) {
730
+ html += `<${this.isUl ? 'li' : 'div'} class="ln-list__spacer" style="height:${topSpacerHeight}px;padding:0;border:none"></${this.isUl ? 'li' : 'div'}>`;
731
+ }
732
+ for (let i = start; i < end; i++) {
733
+ html += data[i].html;
734
+ }
735
+ if (bottomSpacerHeight > 0) {
736
+ html += `<${this.isUl ? 'li' : 'div'} class="ln-list__spacer" style="height:${bottomSpacerHeight}px;padding:0;border:none"></${this.isUl ? 'li' : 'div'}>`;
737
+ }
738
+
739
+ const scrollState = _saveScroll(this);
740
+ this.tbody.innerHTML = html;
741
+ _restoreScroll(scrollState);
742
+
743
+ if (this._selectable) this._restoreSelection();
744
+ }
745
+ };
746
+
747
+ _component.prototype._buildPlaceholderItem = function () {
748
+ const el = document.createElement(this.isUl ? 'li' : 'div');
749
+ el.className = 'ln-list__placeholder';
750
+ el.setAttribute('aria-hidden', 'true');
751
+ el.style.height = this._itemHeight + 'px';
752
+ return el;
753
+ };
754
+
755
+ _component.prototype._renderWindowed = function () {
756
+ if (this.isLoaded && this._cache.logicalTotal === 0) {
757
+ this._disableVirtualScroll();
758
+ this._showEmptyState();
759
+ return;
760
+ }
761
+ if (!this._virtual) this._enableVirtualScroll();
762
+
763
+ const itemHeight = this._itemHeight;
764
+ if (!itemHeight) return;
765
+
766
+ const container = this._scrollContainer;
767
+ let scrollTop, viewportHeight;
768
+ if (container) {
769
+ const rect = this.tbody.getBoundingClientRect();
770
+ const containerRect = container.getBoundingClientRect();
771
+ const relativeTop = (container === this.tbody) ? 0 : (rect.top - containerRect.top + container.scrollTop);
772
+ scrollTop = container.scrollTop - relativeTop;
773
+ viewportHeight = container.clientHeight;
774
+ } else {
775
+ const rect = this.tbody.getBoundingClientRect();
776
+ const absoluteTop = rect.top + window.scrollY;
777
+ scrollTop = window.scrollY - absoluteTop;
778
+ viewportHeight = window.innerHeight;
779
+ }
780
+
781
+ const layout = this._readGridLayout();
782
+ const columns = layout.columns;
783
+ const rowGap = layout.rowGap;
784
+ const rowHeight = itemHeight + rowGap;
785
+ const total = this._cache.logicalTotal;
786
+ const totalRows = Math.ceil(total / columns);
787
+
788
+ let firstRow = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
789
+ firstRow = Math.min(firstRow, totalRows);
790
+ const visibleRows = Math.ceil(viewportHeight / rowHeight) + BUFFER_ROWS * 2;
791
+ const lastRow = Math.min(firstRow + visibleRows, totalRows);
792
+
793
+ const start = Math.min(firstRow * columns, total);
794
+ const end = Math.min(lastRow * columns, total);
795
+
796
+ // No start===_vStart early-return: windowed must re-render when a page
797
+ // splices in even if the range is unchanged.
798
+
799
+ const topSpacerHeight = firstRow * rowHeight;
800
+ const bottomSpacerHeight = (totalRows - lastRow) * rowHeight;
801
+
802
+ const frag = document.createDocumentFragment();
803
+ if (topSpacerHeight > 0) {
804
+ const topSpacer = document.createElement(this.isUl ? 'li' : 'div');
805
+ topSpacer.className = 'ln-list__spacer';
806
+ topSpacer.style.height = topSpacerHeight + 'px';
807
+ frag.appendChild(topSpacer);
808
+ }
809
+ for (let i = start; i < end; i++) {
810
+ if (this._cache.has(i)) {
811
+ const el = this._buildItem(this._cache.get(i));
812
+ if (el) frag.appendChild(el);
813
+ } else {
814
+ frag.appendChild(this._buildPlaceholderItem());
815
+ }
816
+ }
817
+ if (bottomSpacerHeight > 0) {
818
+ const bottomSpacer = document.createElement(this.isUl ? 'li' : 'div');
819
+ bottomSpacer.className = 'ln-list__spacer';
820
+ bottomSpacer.style.height = bottomSpacerHeight + 'px';
821
+ frag.appendChild(bottomSpacer);
822
+ }
823
+
824
+ const scrollState = _saveScroll(this);
825
+ this.tbody.textContent = '';
826
+ this.tbody.appendChild(frag);
827
+ _restoreScroll(scrollState);
828
+
829
+ // Select-all disabled in windowed mode — no _updateSelectAll().
830
+ this._vStart = start;
831
+ this._vEnd = end;
832
+
833
+ this._cache.ensure(start, end);
834
+ };
835
+
836
+ _component.prototype._showEmptyState = function () {
837
+ this.tbody.textContent = '';
838
+
839
+ let el = null;
840
+ if (this.isDataDriven) {
841
+ const total = this._lastTotal != null ? this._lastTotal : this._data.length;
842
+ const visible = this.visibleCount;
843
+ const isFiltered = this.currentSearch && (visible < total || visible === 0);
844
+ const templateName = isFiltered ? this.name + '-empty-filtered' : this.name + '-empty';
845
+
846
+ el = cloneTemplateScoped(this.dom, templateName, 'ln-list');
847
+ if (!el) {
848
+ const fallback = this.dom.querySelector('template[data-ln-empty]');
849
+ if (fallback) {
850
+ const condition = isFiltered ? 'search' : 'initial';
851
+ const matched = fallback.content.querySelector(`[data-ln-empty-when="${condition}"]`) || fallback.content.firstElementChild;
852
+ if (matched) el = document.importNode(matched, true);
853
+ }
854
+ }
855
+ } else {
856
+ const fallback = this.dom.querySelector(`template[${EMPTY_TEMPLATE}]`);
857
+ if (fallback) {
858
+ el = document.importNode(fallback.content, true);
859
+ }
860
+ }
861
+
862
+ if (el) {
863
+ if (el.tagName === 'LI' || el.tagName === 'TR') {
864
+ this.tbody.appendChild(el);
865
+ } else {
866
+ const item = document.createElement(this.isUl ? 'li' : 'div');
867
+ item.appendChild(el);
868
+ this.tbody.appendChild(item);
869
+ }
870
+ }
871
+
872
+ dispatch(this.dom, 'ln-list:empty', {
873
+ term: this.isDataDriven ? this.currentSearch : this._searchTerm,
874
+ total: this.isDataDriven ? (this._lastTotal != null ? this._lastTotal : this._data.length) : this._data.length
875
+ });
876
+ };
877
+
878
+ _component.prototype._buildItem = function (item) {
879
+ const clone = cloneTemplateScoped(this.dom, this.name + '-row', 'ln-list');
880
+ if (!clone) return null;
881
+
882
+ const el = clone.querySelector('[data-ln-item]') || clone.firstElementChild;
883
+ if (!el) return null;
884
+
885
+ fillTemplate(el, item);
886
+ fill(el, item);
887
+
888
+ el._lnRecord = item;
889
+ if (item.id != null) {
890
+ el.setAttribute('data-ln-item-id', item.id);
891
+ if (this._selectable && this.selectedIds.has(String(item.id))) {
892
+ el.classList.add('ln-item-selected');
893
+ const cb = el.querySelector('[data-ln-item-select]');
894
+ if (cb) cb.checked = true;
895
+ }
896
+ }
897
+
898
+ return el;
899
+ };
900
+
901
+ // ─── Selection Helpers ─────────────────────────────────────
902
+
903
+ _component.prototype._enableSelection = function () {
904
+ if (this._selectableActive) return;
905
+ this._selectableActive = true;
906
+
907
+ const self = this;
908
+
909
+ this._onSelectionChange = function (e) {
910
+ const cb = e.target.closest('[data-ln-item-select]');
911
+ if (!cb) return;
912
+
913
+ const itemEl = cb.closest('[data-ln-item]');
914
+ if (!itemEl) return;
915
+
916
+ const id = itemEl.getAttribute('data-ln-item-id');
917
+ if (id == null) return;
918
+
919
+ if (cb.checked) {
920
+ self.selectedIds.add(String(id));
921
+ itemEl.classList.add('ln-item-selected');
922
+ } else {
923
+ self.selectedIds.delete(String(id));
924
+ itemEl.classList.remove('ln-item-selected');
925
+ }
926
+
927
+ self._updateSelectAll();
928
+ self._updateFooter();
929
+
930
+ dispatch(self.dom, 'ln-list:select', {
931
+ list: self.name,
932
+ selectedIds: self.selectedIds,
933
+ count: self.selectedIds.size
934
+ });
935
+ };
936
+ this.tbody.addEventListener('change', this._onSelectionChange);
937
+
938
+ this._selectAllCheckbox = this.dom.querySelector('[data-ln-list-select-all]');
939
+ if (this._selectAllCheckbox) {
940
+ this._onSelectAll = function () {
941
+ const checked = self._selectAllCheckbox.checked;
942
+ const items = self.tbody.querySelectorAll('[data-ln-item]');
943
+ for (let i = 0; i < items.length; i++) {
944
+ const itemEl = items[i];
945
+ const id = itemEl.getAttribute('data-ln-item-id');
946
+ const cb = itemEl.querySelector('[data-ln-item-select]');
947
+ if (id != null) {
948
+ if (checked) {
949
+ self.selectedIds.add(String(id));
950
+ itemEl.classList.add('ln-item-selected');
951
+ } else {
952
+ self.selectedIds.delete(String(id));
953
+ itemEl.classList.remove('ln-item-selected');
954
+ }
955
+ if (cb) cb.checked = checked;
956
+ }
957
+ }
958
+
959
+ dispatch(self.dom, 'ln-list:select-all', { list: self.name, selected: checked });
960
+ dispatch(self.dom, 'ln-list:select', {
961
+ list: self.name,
962
+ selectedIds: self.selectedIds,
963
+ count: self.selectedIds.size
964
+ });
965
+ self._updateFooter();
966
+ };
967
+ this._selectAllCheckbox.addEventListener('change', this._onSelectAll);
968
+ }
969
+ };
970
+
971
+ _component.prototype._updateSelectAll = function () {
972
+ if (!this._selectAllCheckbox) return;
973
+ const items = this.tbody.querySelectorAll('[data-ln-item]');
974
+ let allSelected = items.length > 0;
975
+ for (let i = 0; i < items.length; i++) {
976
+ const id = items[i].getAttribute('data-ln-item-id');
977
+ if (id != null && !this.selectedIds.has(String(id))) {
978
+ allSelected = false;
979
+ break;
980
+ }
981
+ }
982
+ this._selectAllCheckbox.checked = allSelected;
983
+ };
984
+
985
+ // SSR re-render replays cached item HTML — reapply live selection state.
986
+ _component.prototype._restoreSelection = function () {
987
+ if (!this.tbody) return;
988
+ const items = this.tbody.querySelectorAll('[data-ln-item]');
989
+ for (let i = 0; i < items.length; i++) {
990
+ const id = items[i].getAttribute('data-ln-item-id');
991
+ const selected = id != null && this.selectedIds.has(String(id));
992
+ items[i].classList.toggle('ln-item-selected', selected);
993
+ const cb = items[i].querySelector('[data-ln-item-select]');
994
+ if (cb) cb.checked = selected;
995
+ }
996
+ this._updateSelectAll();
997
+ };
998
+
999
+ _component.prototype._requestData = function () {
1000
+ if (this._windowed) {
1001
+ this.dom.classList.add('ln-list--loading');
1002
+ this._cache.invalidate({
1003
+ sort: this.currentSort,
1004
+ filters: this.currentFilters,
1005
+ search: this.currentSearch
1006
+ });
1007
+ return;
1008
+ }
1009
+ requestData(this, 'ln-list:request-data', 'list');
1010
+ };
1011
+
1012
+ // ─── Windowed Mode — enter/exit/seed (live toggle) ──────────
1013
+
1014
+ _component.prototype._enterWindowedMode = function () {
1015
+ const self = this;
1016
+ const dom = this.dom;
1017
+ const winAttr = parseInt(dom.getAttribute('data-ln-list-window'), 10);
1018
+ const pageAttr = parseInt(dom.getAttribute('data-ln-list-window-page'), 10);
1019
+ const threshAttr = parseInt(dom.getAttribute('data-ln-list-window-threshold'), 10);
1020
+
1021
+ this._onCacheChange = function () {
1022
+ if (!self._windowed || !self._cache) return;
1023
+ self.totalCount = self._cache.grandTotal;
1024
+ self.visibleCount = self._cache.logicalTotal;
1025
+ self._lastTotal = self._cache.grandTotal;
1026
+ self.isLoaded = true;
1027
+ self._vStart = -1;
1028
+ self._vEnd = -1;
1029
+ self._render();
1030
+ self._updateFooter();
1031
+ dispatch(dom, 'ln-list:rendered', {
1032
+ list: self.name,
1033
+ total: self.totalCount,
1034
+ visible: self.visibleCount
1035
+ });
1036
+ };
1037
+ this._renderBatch = createBatcher(this._onCacheChange);
1038
+
1039
+ this._cache = createWindowCache({
1040
+ windowSize: winAttr > 0 ? winAttr : WINDOW_DEFAULT,
1041
+ pageSize: pageAttr > 0 ? pageAttr : WINDOW_PAGE,
1042
+ threshold: threshAttr >= 0 ? threshAttr : 25,
1043
+ fetchDebounce: FETCH_DEBOUNCE,
1044
+ requestPage: function (query, offset, limit) {
1045
+ dispatch(dom, 'ln-list:request-data', {
1046
+ list: self.name,
1047
+ sort: query.sort,
1048
+ filters: query.filters,
1049
+ search: query.search,
1050
+ offset: offset,
1051
+ limit: limit,
1052
+ queryGen: self._cache.queryGen
1053
+ });
1054
+ },
1055
+ onChange: this._renderBatch
1056
+ });
1057
+
1058
+ this._windowed = true;
1059
+
1060
+ if (this._selectable && this._selectAllCheckbox) {
1061
+ this._selectAllCheckbox.classList.add('hidden');
1062
+ }
1063
+ };
1064
+
1065
+ _component.prototype._kickWindowInitial = function () {
1066
+ if (this._data.length > 0) {
1067
+ // SSR-seeded / warm-seeded: page 0 is already resident, the grand
1068
+ // total is declared in markup — no initial fetch needed. No
1069
+ // queryGen on the seed — it must never be dropped as stale.
1070
+ const declaredTotal = parseInt(this.dom.getAttribute('data-ln-list-count'), 10);
1071
+ const seedTotal = declaredTotal > 0 ? declaredTotal : this._data.length;
1072
+ this._cache.ingest({
1073
+ data: this._data,
1074
+ offset: 0,
1075
+ total: seedTotal,
1076
+ filtered: seedTotal
1077
+ });
1078
+ } else {
1079
+ this.dom.classList.add('ln-list--loading');
1080
+ this._cache.requestInitial({
1081
+ sort: this.currentSort,
1082
+ filters: this.currentFilters,
1083
+ search: this.currentSearch
1084
+ });
1085
+ }
1086
+ };
1087
+
1088
+ _component.prototype._exitWindowedMode = function () {
1089
+ this._disableVirtualScroll();
1090
+ if (this._cache) this._cache.destroy();
1091
+ this._cache = null;
1092
+ this._windowed = false;
1093
+ this._renderBatch = null;
1094
+ this._onCacheChange = null;
1095
+ if (this._selectAllCheckbox) {
1096
+ this._selectAllCheckbox.classList.remove('hidden');
1097
+ }
1098
+ this._itemHeight = 0;
1099
+ this._vStart = -1;
1100
+ this._vEnd = -1;
1101
+ this._data = [];
1102
+ this._filteredData = [];
1103
+ this.dom.classList.add('ln-list--loading');
1104
+ this._requestData();
1105
+ };
1106
+
1107
+ _component.prototype._updateFooter = function () {
1108
+ let total = 0;
1109
+ let filtered = 0;
1110
+
1111
+ if (this.isDataDriven) {
1112
+ total = this._lastTotal != null ? this._lastTotal : this._data.length;
1113
+ filtered = this.visibleCount;
1114
+ } else {
1115
+ total = this._data.length;
1116
+ filtered = this._filteredData.length;
1117
+ }
1118
+
1119
+ const hasActiveFilter = filtered < total;
1120
+
1121
+ if (this._totalSpan) this._totalSpan.textContent = _formatNum(total, this.dom);
1122
+ if (this._filteredSpan) this._filteredSpan.textContent = hasActiveFilter ? _formatNum(filtered, this.dom) : '';
1123
+ if (this._filteredWrap) this._filteredWrap.classList.toggle('hidden', !hasActiveFilter);
1124
+
1125
+ if (this._selectedSpan) {
1126
+ const selected = this.selectedIds ? this.selectedIds.size : 0;
1127
+ this._selectedSpan.textContent = selected > 0 ? _formatNum(selected, this.dom) : '';
1128
+ if (this._selectedWrap) {
1129
+ this._selectedWrap.classList.toggle('hidden', selected === 0);
1130
+ }
1131
+ }
1132
+ };
1133
+
1134
+ _component.prototype.destroy = function () {
1135
+ if (!this.dom[DOM_ATTRIBUTE]) return;
1136
+
1137
+ this._disableVirtualScroll();
1138
+
1139
+ if (this.isDataDriven) {
1140
+ if (this._cache) this._cache.destroy();
1141
+ this.dom.removeEventListener('ln-list:set-data', this._onSetData);
1142
+ this.dom.removeEventListener('ln-list:set-loading', this._onSetLoading);
1143
+ this.dom.removeEventListener('ln-list:page-failed', this._onPageFailed);
1144
+ this.dom.removeEventListener('ln-list:request-revalidate', this._onRequestRevalidate);
1145
+ this.dom.removeEventListener('ln-sort:change', this._onSort);
1146
+ this.dom.removeEventListener('click', this._onClearAll);
1147
+
1148
+ if (this.tbody) {
1149
+ this.tbody.removeEventListener('click', this._onItemClick);
1150
+ this.tbody.removeEventListener('click', this._onItemAction);
1151
+ }
1152
+
1153
+ this.dom.removeEventListener('ln-search:change', this._onSearchChange);
1154
+ } else {
1155
+ if (this._emptyObserver) {
1156
+ this._emptyObserver.disconnect();
1157
+ this._emptyObserver = null;
1158
+ }
1159
+ this.dom.removeEventListener('ln-search:change', this._onSearch);
1160
+ }
1161
+
1162
+ if (this._onSelectionChange && this.tbody) {
1163
+ this.tbody.removeEventListener('change', this._onSelectionChange);
1164
+ }
1165
+ if (this._selectAllCheckbox && this._onSelectAll) {
1166
+ this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1167
+ }
1168
+
1169
+ if (this._onClear) {
1170
+ this.dom.removeEventListener('click', this._onClear);
1171
+ }
1172
+
1173
+ this._data = [];
1174
+ this._filteredData = [];
1175
+ delete this.dom[DOM_ATTRIBUTE];
1176
+ };
1177
+
1178
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-list', {
1179
+ extraAttributes: [
1180
+ 'data-ln-list-window',
1181
+ 'data-ln-list-window-page',
1182
+ 'data-ln-list-window-threshold',
1183
+ 'data-ln-list-count'
1184
+ ],
1185
+ onAttributeChange: function (el, attrName) {
1186
+ const inst = el[DOM_ATTRIBUTE];
1187
+ if (!inst || !inst.isDataDriven) return;
1188
+
1189
+ if (attrName === 'data-ln-list-window') {
1190
+ const present = el.hasAttribute('data-ln-list-window');
1191
+ if (present && !inst._windowed) {
1192
+ inst._enterWindowedMode();
1193
+ inst._kickWindowInitial();
1194
+ } else if (!present && inst._windowed) {
1195
+ inst._exitWindowedMode();
1196
+ } else if (present && inst._windowed) {
1197
+ const v = parseInt(el.getAttribute('data-ln-list-window'), 10);
1198
+ if (v > 0) inst._cache.configure({ windowSize: v });
1199
+ }
1200
+ return;
1201
+ }
1202
+
1203
+ if (!inst._windowed || !inst._cache) return;
1204
+ if (attrName === 'data-ln-list-window-page') {
1205
+ const v = parseInt(el.getAttribute('data-ln-list-window-page'), 10);
1206
+ if (v > 0) inst._cache.configure({ pageSize: v });
1207
+ } else if (attrName === 'data-ln-list-window-threshold') {
1208
+ const v = parseInt(el.getAttribute('data-ln-list-window-threshold'), 10);
1209
+ if (v >= 0) inst._cache.configure({ threshold: v });
1210
+ } else if (attrName === 'data-ln-list-count') {
1211
+ const v = parseInt(el.getAttribute('data-ln-list-count'), 10);
1212
+ if (v >= 0) inst._cache.setGrandTotal(v);
1213
+ }
1214
+ }
1215
+ });
1216
+ })();