@livenetworks/ashlar 1.3.2

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 (232) hide show
  1. package/README.md +177 -0
  2. package/js/COMPONENTS.md +1102 -0
  3. package/js/index.js +41 -0
  4. package/js/ln-accordion/README.md +137 -0
  5. package/js/ln-accordion/ln-accordion.js +1 -0
  6. package/js/ln-accordion/src/ln-accordion.js +41 -0
  7. package/js/ln-ajax/README.md +91 -0
  8. package/js/ln-ajax/ln-ajax.js +1 -0
  9. package/js/ln-ajax/src/ln-ajax.js +277 -0
  10. package/js/ln-api-connector/README.md +150 -0
  11. package/js/ln-api-connector/ln-api-connector.js +1 -0
  12. package/js/ln-api-connector/src/ln-api-connector.js +265 -0
  13. package/js/ln-autoresize/README.md +80 -0
  14. package/js/ln-autoresize/ln-autoresize.js +1 -0
  15. package/js/ln-autoresize/src/ln-autoresize.js +47 -0
  16. package/js/ln-autosave/README.md +92 -0
  17. package/js/ln-autosave/ln-autosave.js +1 -0
  18. package/js/ln-autosave/src/ln-autosave.js +147 -0
  19. package/js/ln-circular-progress/README.md +161 -0
  20. package/js/ln-circular-progress/ln-circular-progress.js +1 -0
  21. package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
  22. package/js/ln-confirm/README.md +86 -0
  23. package/js/ln-confirm/_ln-confirm.scss +13 -0
  24. package/js/ln-confirm/ln-confirm.js +1 -0
  25. package/js/ln-confirm/src/ln-confirm.js +131 -0
  26. package/js/ln-core/crypto.js +83 -0
  27. package/js/ln-core/helpers.js +411 -0
  28. package/js/ln-core/index.js +5 -0
  29. package/js/ln-core/persist.js +71 -0
  30. package/js/ln-core/positioning.js +207 -0
  31. package/js/ln-core/reactive.js +74 -0
  32. package/js/ln-couchdb-connector/README.md +156 -0
  33. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  34. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
  35. package/js/ln-data-coordinator/README.md +165 -0
  36. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
  37. package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
  38. package/js/ln-data-store/README.md +94 -0
  39. package/js/ln-data-store/ln-data-store.js +1 -0
  40. package/js/ln-data-store/src/ln-data-store.js +699 -0
  41. package/js/ln-data-table/README.md +110 -0
  42. package/js/ln-data-table/ln-data-table.js +1 -0
  43. package/js/ln-data-table/ln-data-table.scss +10 -0
  44. package/js/ln-data-table/src/ln-data-table.js +1103 -0
  45. package/js/ln-date/README.md +151 -0
  46. package/js/ln-date/ln-date.js +1 -0
  47. package/js/ln-date/src/ln-date.js +442 -0
  48. package/js/ln-dropdown/README.md +117 -0
  49. package/js/ln-dropdown/ln-dropdown.js +1 -0
  50. package/js/ln-dropdown/ln-dropdown.scss +15 -0
  51. package/js/ln-dropdown/src/ln-dropdown.js +174 -0
  52. package/js/ln-external-links/README.md +341 -0
  53. package/js/ln-external-links/ln-external-links.js +1 -0
  54. package/js/ln-external-links/src/ln-external-links.js +116 -0
  55. package/js/ln-filter/README.md +99 -0
  56. package/js/ln-filter/ln-filter.js +1 -0
  57. package/js/ln-filter/ln-filter.scss +7 -0
  58. package/js/ln-filter/src/ln-filter.js +404 -0
  59. package/js/ln-form/README.md +101 -0
  60. package/js/ln-form/ln-form.js +1 -0
  61. package/js/ln-form/src/ln-form.js +199 -0
  62. package/js/ln-http/README.md +89 -0
  63. package/js/ln-http/ln-http.js +1 -0
  64. package/js/ln-http/src/ln-http.js +219 -0
  65. package/js/ln-icons/README.md +88 -0
  66. package/js/ln-icons/ln-icons.js +1 -0
  67. package/js/ln-icons/src/ln-icons.js +169 -0
  68. package/js/ln-link/README.md +303 -0
  69. package/js/ln-link/ln-link.js +1 -0
  70. package/js/ln-link/src/ln-link.js +196 -0
  71. package/js/ln-modal/README.md +154 -0
  72. package/js/ln-modal/ln-modal.js +1 -0
  73. package/js/ln-modal/ln-modal.scss +11 -0
  74. package/js/ln-modal/src/ln-modal.js +201 -0
  75. package/js/ln-nav/README.md +70 -0
  76. package/js/ln-nav/ln-nav.js +1 -0
  77. package/js/ln-nav/src/ln-nav.js +177 -0
  78. package/js/ln-number/README.md +122 -0
  79. package/js/ln-number/ln-number.js +1 -0
  80. package/js/ln-number/src/ln-number.js +302 -0
  81. package/js/ln-popover/README.md +127 -0
  82. package/js/ln-popover/ln-popover.js +1 -0
  83. package/js/ln-popover/src/ln-popover.js +288 -0
  84. package/js/ln-progress/README.md +442 -0
  85. package/js/ln-progress/ln-progress.js +1 -0
  86. package/js/ln-progress/src/ln-progress.js +150 -0
  87. package/js/ln-search/README.md +83 -0
  88. package/js/ln-search/ln-search.js +1 -0
  89. package/js/ln-search/ln-search.scss +7 -0
  90. package/js/ln-search/src/ln-search.js +114 -0
  91. package/js/ln-sortable/README.md +95 -0
  92. package/js/ln-sortable/ln-sortable.js +1 -0
  93. package/js/ln-sortable/src/ln-sortable.js +203 -0
  94. package/js/ln-table/README.md +101 -0
  95. package/js/ln-table/ln-table-sort.js +1 -0
  96. package/js/ln-table/ln-table.js +1 -0
  97. package/js/ln-table/ln-table.scss +11 -0
  98. package/js/ln-table/src/ln-table-sort.js +168 -0
  99. package/js/ln-table/src/ln-table.js +473 -0
  100. package/js/ln-tabs/README.md +137 -0
  101. package/js/ln-tabs/ln-tabs.js +1 -0
  102. package/js/ln-tabs/src/ln-tabs.js +171 -0
  103. package/js/ln-time/README.md +81 -0
  104. package/js/ln-time/ln-time.js +1 -0
  105. package/js/ln-time/src/ln-time.js +192 -0
  106. package/js/ln-toast/README.md +122 -0
  107. package/js/ln-toast/ln-toast.js +15 -0
  108. package/js/ln-toast/src/ln-toast.js +210 -0
  109. package/js/ln-toast/template.html +14 -0
  110. package/js/ln-toggle/README.md +137 -0
  111. package/js/ln-toggle/ln-toggle.js +1 -0
  112. package/js/ln-toggle/src/ln-toggle.js +139 -0
  113. package/js/ln-tooltip/README.md +58 -0
  114. package/js/ln-tooltip/ln-tooltip.js +1 -0
  115. package/js/ln-tooltip/ln-tooltip.scss +9 -0
  116. package/js/ln-tooltip/src/ln-tooltip.js +169 -0
  117. package/js/ln-translations/README.md +96 -0
  118. package/js/ln-translations/ln-translations.js +1 -0
  119. package/js/ln-translations/src/ln-translations.js +275 -0
  120. package/js/ln-upload/README.md +180 -0
  121. package/js/ln-upload/ln-upload.js +1 -0
  122. package/js/ln-upload/ln-upload.scss +20 -0
  123. package/js/ln-upload/src/ln-upload.js +407 -0
  124. package/js/ln-validate/README.md +108 -0
  125. package/js/ln-validate/ln-validate.js +1 -0
  126. package/js/ln-validate/src/ln-validate.js +160 -0
  127. package/package.json +55 -0
  128. package/scss/base/_global.scss +83 -0
  129. package/scss/base/_reset.scss +17 -0
  130. package/scss/base/_typography.scss +125 -0
  131. package/scss/components/_accordion.scss +34 -0
  132. package/scss/components/_ajax.scss +15 -0
  133. package/scss/components/_alert.scss +5 -0
  134. package/scss/components/_app-shell.scss +15 -0
  135. package/scss/components/_avatar.scss +6 -0
  136. package/scss/components/_breadcrumbs.scss +33 -0
  137. package/scss/components/_button.scss +20 -0
  138. package/scss/components/_card.scss +10 -0
  139. package/scss/components/_chip.scss +5 -0
  140. package/scss/components/_circular-progress.scss +29 -0
  141. package/scss/components/_confirm.scss +5 -0
  142. package/scss/components/_data-table.scss +83 -0
  143. package/scss/components/_dropdown.scss +25 -0
  144. package/scss/components/_empty-state.scss +22 -0
  145. package/scss/components/_form.scss +100 -0
  146. package/scss/components/_layout.scss +8 -0
  147. package/scss/components/_link.scss +11 -0
  148. package/scss/components/_ln-table.scss +60 -0
  149. package/scss/components/_loader.scss +6 -0
  150. package/scss/components/_modal.scss +20 -0
  151. package/scss/components/_nav.scss +9 -0
  152. package/scss/components/_page-header.scss +10 -0
  153. package/scss/components/_popover.scss +10 -0
  154. package/scss/components/_progress.scss +17 -0
  155. package/scss/components/_prose.scss +5 -0
  156. package/scss/components/_scrollbar.scss +32 -0
  157. package/scss/components/_sections.scss +12 -0
  158. package/scss/components/_sidebar.scss +5 -0
  159. package/scss/components/_stat-card.scss +5 -0
  160. package/scss/components/_status-badge.scss +4 -0
  161. package/scss/components/_stepper.scss +5 -0
  162. package/scss/components/_table.scss +19 -0
  163. package/scss/components/_tabs.scss +21 -0
  164. package/scss/components/_timeline.scss +14 -0
  165. package/scss/components/_toast.scss +41 -0
  166. package/scss/components/_toggle.scss +81 -0
  167. package/scss/components/_tooltip.scss +18 -0
  168. package/scss/components/_translations.scss +111 -0
  169. package/scss/components/_upload.scss +51 -0
  170. package/scss/config/_breakpoints.scss +72 -0
  171. package/scss/config/_density.scss +117 -0
  172. package/scss/config/_icons.scss +37 -0
  173. package/scss/config/_mixins.scss +13 -0
  174. package/scss/config/_theme.scss +216 -0
  175. package/scss/config/_tokens.scss +419 -0
  176. package/scss/config/mixins/_accordion.scss +52 -0
  177. package/scss/config/mixins/_ajax.scss +39 -0
  178. package/scss/config/mixins/_alert.scss +82 -0
  179. package/scss/config/mixins/_app-shell.scss +312 -0
  180. package/scss/config/mixins/_avatar.scss +109 -0
  181. package/scss/config/mixins/_borders.scss +36 -0
  182. package/scss/config/mixins/_breadcrumbs.scss +72 -0
  183. package/scss/config/mixins/_breakpoints.scss +62 -0
  184. package/scss/config/mixins/_btn.scss +179 -0
  185. package/scss/config/mixins/_card.scss +338 -0
  186. package/scss/config/mixins/_chip.scss +66 -0
  187. package/scss/config/mixins/_circular-progress.scss +71 -0
  188. package/scss/config/mixins/_collapsible.scss +24 -0
  189. package/scss/config/mixins/_colors.scss +46 -0
  190. package/scss/config/mixins/_confirm.scss +31 -0
  191. package/scss/config/mixins/_data-table.scss +346 -0
  192. package/scss/config/mixins/_display.scss +32 -0
  193. package/scss/config/mixins/_dropdown.scss +143 -0
  194. package/scss/config/mixins/_empty-state.scss +30 -0
  195. package/scss/config/mixins/_focus.scss +55 -0
  196. package/scss/config/mixins/_footer.scss +42 -0
  197. package/scss/config/mixins/_form.scss +601 -0
  198. package/scss/config/mixins/_index.scss +58 -0
  199. package/scss/config/mixins/_interaction.scss +15 -0
  200. package/scss/config/mixins/_kbd.scss +22 -0
  201. package/scss/config/mixins/_layout.scss +117 -0
  202. package/scss/config/mixins/_link.scss +55 -0
  203. package/scss/config/mixins/_ln-table.scss +420 -0
  204. package/scss/config/mixins/_loader.scss +26 -0
  205. package/scss/config/mixins/_modal.scss +66 -0
  206. package/scss/config/mixins/_motion.scss +19 -0
  207. package/scss/config/mixins/_nav.scss +273 -0
  208. package/scss/config/mixins/_page-header.scss +69 -0
  209. package/scss/config/mixins/_popover.scss +25 -0
  210. package/scss/config/mixins/_position.scss +32 -0
  211. package/scss/config/mixins/_progress.scss +56 -0
  212. package/scss/config/mixins/_prose.scss +127 -0
  213. package/scss/config/mixins/_shadows.scss +8 -0
  214. package/scss/config/mixins/_sidebar.scss +95 -0
  215. package/scss/config/mixins/_sizing.scss +6 -0
  216. package/scss/config/mixins/_spacing.scss +19 -0
  217. package/scss/config/mixins/_stat-card.scss +68 -0
  218. package/scss/config/mixins/_status-badge.scss +83 -0
  219. package/scss/config/mixins/_stepper.scss +78 -0
  220. package/scss/config/mixins/_table.scss +215 -0
  221. package/scss/config/mixins/_tabs.scss +64 -0
  222. package/scss/config/mixins/_timeline.scss +69 -0
  223. package/scss/config/mixins/_toast.scss +148 -0
  224. package/scss/config/mixins/_tooltip.scss +111 -0
  225. package/scss/config/mixins/_transitions.scss +10 -0
  226. package/scss/config/mixins/_translations.scss +124 -0
  227. package/scss/config/mixins/_typography.scss +57 -0
  228. package/scss/config/mixins/_upload.scss +168 -0
  229. package/scss/ln-ashlar.scss +62 -0
  230. package/scss/tabler-icons.txt +5039 -0
  231. package/scss/utilities/_animations.scss +83 -0
  232. package/scss/utilities/_utilities.scss +49 -0
@@ -0,0 +1,168 @@
1
+ import { guardBody, dispatch } from '../../ln-core';
2
+ import { persistGet, persistSet } from '../../ln-core';
3
+
4
+ (function () {
5
+ const DOM_ATTRIBUTE = 'lnTableSort';
6
+ const SORT_ATTR = 'data-ln-sort';
7
+ const SORT_ACTIVE_ATTR = 'data-ln-sort-active';
8
+
9
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
10
+
11
+ // ─── Constructor ───────────────────────────────────────────
12
+ // Auto-initializes any <table> that contains th[data-ln-sort] headers.
13
+
14
+ function constructor(domRoot) {
15
+ findElements(domRoot);
16
+ }
17
+
18
+ // Local findElements — intentional divergence from ln-core helper: tag selector ('table') + two-arg constructor (table, ths).
19
+ function findElements(root) {
20
+ const tables = Array.from(root.querySelectorAll('table'));
21
+ if (root.tagName === 'TABLE') tables.push(root);
22
+
23
+ tables.forEach(function (table) {
24
+ if (table[DOM_ATTRIBUTE]) return;
25
+ const ths = Array.from(table.querySelectorAll('th[' + SORT_ATTR + ']'));
26
+ if (ths.length) table[DOM_ATTRIBUTE] = new _component(table, ths);
27
+ });
28
+ }
29
+
30
+ // ─── Component ─────────────────────────────────────────────
31
+
32
+ function _setSortIcon(th, dir) {
33
+ const icons = th.querySelectorAll('[data-ln-sort-icon]');
34
+ icons.forEach(function (icon) {
35
+ const val = icon.getAttribute('data-ln-sort-icon');
36
+ if (dir === null || dir === undefined) {
37
+ // Neutral: show the no-value icon, hide asc/desc
38
+ icon.classList.toggle('hidden', val !== null && val !== '');
39
+ } else {
40
+ // Show matching direction, hide others
41
+ icon.classList.toggle('hidden', val !== dir);
42
+ }
43
+ });
44
+ }
45
+
46
+ function _component(table, ths) {
47
+ this.table = table;
48
+ this.ths = ths;
49
+ this._col = -1;
50
+ this._dir = null;
51
+ const self = this;
52
+
53
+ ths.forEach(function (th, index) {
54
+ if (th[DOM_ATTRIBUTE + 'Bound']) return;
55
+ th[DOM_ATTRIBUTE + 'Bound'] = true;
56
+
57
+ th._lnSortClick = function (e) {
58
+ // Don't sort when user clicks an interactive child (filter button, etc.)
59
+ const interactive = e.target.closest('button, a, input, select, textarea, [data-ln-dropdown]');
60
+ if (interactive && interactive !== th) return;
61
+ self._handleClick(index, th);
62
+ };
63
+ th.addEventListener('click', th._lnSortClick);
64
+ });
65
+
66
+ // ─── Restore persisted sort ───────────────────────────────
67
+ const persistRoot = table.closest('[data-ln-table][data-ln-persist]');
68
+ if (persistRoot) {
69
+ const saved = persistGet('table-sort', persistRoot);
70
+ if (saved && saved.dir && saved.col >= 0 && saved.col < ths.length) {
71
+ // Programmatically trigger the saved sort
72
+ this._handleClick(saved.col, ths[saved.col]);
73
+ if (saved.dir === 'desc') {
74
+ this._handleClick(saved.col, ths[saved.col]);
75
+ }
76
+ }
77
+ }
78
+
79
+ return this;
80
+ }
81
+
82
+ _component.prototype._handleClick = function (colIndex, th) {
83
+ let newDir;
84
+
85
+ if (this._col !== colIndex) {
86
+ newDir = 'asc';
87
+ } else if (this._dir === 'asc') {
88
+ newDir = 'desc';
89
+ } else if (this._dir === 'desc') {
90
+ newDir = null;
91
+ } else {
92
+ newDir = 'asc';
93
+ }
94
+
95
+ this.ths.forEach(function (t) {
96
+ t.removeAttribute(SORT_ACTIVE_ATTR);
97
+ _setSortIcon(t, null);
98
+ });
99
+
100
+ if (newDir === null) {
101
+ this._col = -1;
102
+ this._dir = null;
103
+ } else {
104
+ this._col = colIndex;
105
+ this._dir = newDir;
106
+ th.setAttribute(SORT_ACTIVE_ATTR, newDir);
107
+ _setSortIcon(th, newDir);
108
+ }
109
+
110
+ dispatch(this.table, 'ln-table:sort', {
111
+ column: colIndex,
112
+ sortType: th.getAttribute(SORT_ATTR),
113
+ direction: newDir
114
+ });
115
+ const persistRoot = this.table.closest('[data-ln-table][data-ln-persist]');
116
+ if (persistRoot) {
117
+ if (newDir === null) {
118
+ persistSet('table-sort', persistRoot, null);
119
+ } else {
120
+ persistSet('table-sort', persistRoot, { col: colIndex, dir: newDir });
121
+ }
122
+ }
123
+ };
124
+
125
+ _component.prototype.destroy = function () {
126
+ if (!this.table[DOM_ATTRIBUTE]) return;
127
+ this.ths.forEach(function (th) {
128
+ if (th._lnSortClick) {
129
+ th.removeEventListener('click', th._lnSortClick);
130
+ delete th._lnSortClick;
131
+ }
132
+ delete th[DOM_ATTRIBUTE + 'Bound'];
133
+ });
134
+ delete this.table[DOM_ATTRIBUTE];
135
+ };
136
+
137
+ // ─── DOM Observer ──────────────────────────────────────────
138
+
139
+ function _domObserver() {
140
+ guardBody(function () {
141
+ const observer = new MutationObserver(function (mutations) {
142
+ mutations.forEach(function (mutation) {
143
+ if (mutation.type === 'childList') {
144
+ mutation.addedNodes.forEach(function (node) {
145
+ if (node.nodeType === 1) findElements(node);
146
+ });
147
+ } else if (mutation.type === 'attributes') {
148
+ findElements(mutation.target);
149
+ }
150
+ });
151
+ });
152
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [SORT_ATTR] });
153
+ }, 'ln-table-sort');
154
+ }
155
+
156
+ // ─── Init ──────────────────────────────────────────────────
157
+
158
+ window[DOM_ATTRIBUTE] = constructor;
159
+ _domObserver();
160
+
161
+ if (document.readyState === 'loading') {
162
+ document.addEventListener('DOMContentLoaded', function () {
163
+ constructor(document.body);
164
+ });
165
+ } else {
166
+ constructor(document.body);
167
+ }
168
+ })();
@@ -0,0 +1,473 @@
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
+ })();