@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.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +189 -99
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -55
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -0
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +14 -5
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +68 -1
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +48 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +47 -0
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +4 -1
- package/scss/config/mixins/_ln-table.scss +51 -93
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +232 -38
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +39 -0
- package/scss/ln-ashlar.scss +8 -2
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1103
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -168
- package/scss/components/_data-table.scss +0 -83
- package/scss/config/mixins/_data-table.scss +0 -346
|
@@ -1,1103 +0,0 @@
|
|
|
1
|
-
import { cloneTemplateScoped, dispatch, fill, fillTemplate, registerComponent } from '../../ln-core';
|
|
2
|
-
|
|
3
|
-
(function () {
|
|
4
|
-
const DOM_SELECTOR = 'data-ln-data-table';
|
|
5
|
-
const DOM_ATTRIBUTE = 'lnDataTable';
|
|
6
|
-
|
|
7
|
-
// Tuning constant — duplicated in ln-table for component independence
|
|
8
|
-
const VIRTUAL_THRESHOLD = 200;
|
|
9
|
-
const BUFFER_ROWS = 15;
|
|
10
|
-
|
|
11
|
-
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
12
|
-
|
|
13
|
-
// Singleton formatter — same locale for all instances
|
|
14
|
-
const _numFmt = typeof Intl !== 'undefined'
|
|
15
|
-
? new Intl.NumberFormat(document.documentElement.lang || undefined)
|
|
16
|
-
: null;
|
|
17
|
-
|
|
18
|
-
function _formatNum(n) {
|
|
19
|
-
return _numFmt ? _numFmt.format(n) : String(n);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Walk ancestors, find the nearest element that establishes a vertical
|
|
23
|
-
// scroll context. Returns null if no such ancestor exists (the page
|
|
24
|
-
// itself is the scroll surface — caller falls back to window).
|
|
25
|
-
function _findScrollContainer(el) {
|
|
26
|
-
let p = el.parentElement;
|
|
27
|
-
while (p && p !== document.body && p !== document.documentElement) {
|
|
28
|
-
const cs = getComputedStyle(p);
|
|
29
|
-
const oy = cs.overflowY;
|
|
30
|
-
if (oy === 'auto' || oy === 'scroll') return p;
|
|
31
|
-
p = p.parentElement;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ─── Component ─────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
function _component(dom) {
|
|
39
|
-
this.dom = dom;
|
|
40
|
-
this.name = dom.getAttribute(DOM_SELECTOR) || '';
|
|
41
|
-
this.table = dom.querySelector('table');
|
|
42
|
-
this.tbody = dom.querySelector('[data-ln-data-table-body]') || dom.querySelector('tbody');
|
|
43
|
-
this.thead = dom.querySelector('thead');
|
|
44
|
-
this.ths = this.thead ? Array.from(this.thead.querySelectorAll('th')) : [];
|
|
45
|
-
|
|
46
|
-
// Instance state
|
|
47
|
-
this.isLoaded = false;
|
|
48
|
-
this.totalCount = 0;
|
|
49
|
-
this.visibleCount = 0;
|
|
50
|
-
this.currentSort = null;
|
|
51
|
-
this.currentFilters = {};
|
|
52
|
-
this.currentSearch = '';
|
|
53
|
-
this.selectedIds = new Set();
|
|
54
|
-
|
|
55
|
-
// Internal data cache (last received payload)
|
|
56
|
-
this._data = [];
|
|
57
|
-
this._lastTotal = 0;
|
|
58
|
-
this._lastFiltered = 0;
|
|
59
|
-
|
|
60
|
-
// Column filter options cache — grows additively; never shrinks on filtered payloads
|
|
61
|
-
this._filterOptions = {};
|
|
62
|
-
this._filterableFields = this.ths
|
|
63
|
-
.filter(function (th) {
|
|
64
|
-
return th.getAttribute('data-ln-col') && th.querySelector('[data-ln-col-filter]');
|
|
65
|
-
})
|
|
66
|
-
.map(function (th) { return th.getAttribute('data-ln-col'); });
|
|
67
|
-
|
|
68
|
-
// Virtual scroll state
|
|
69
|
-
this._virtual = false;
|
|
70
|
-
this._rowHeight = 0;
|
|
71
|
-
this._vStart = -1;
|
|
72
|
-
this._vEnd = -1;
|
|
73
|
-
this._rafId = null;
|
|
74
|
-
this._scrollHandler = null;
|
|
75
|
-
this._scrollContainer = null;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Footer elements
|
|
80
|
-
this._totalSpan = dom.querySelector('[data-ln-data-table-total]');
|
|
81
|
-
this._filteredSpan = dom.querySelector('[data-ln-data-table-filtered]');
|
|
82
|
-
|
|
83
|
-
// Filtered separator — the parent that wraps "· X filtered" text
|
|
84
|
-
// Hide it when there's no active filtering
|
|
85
|
-
if (this._filteredSpan) {
|
|
86
|
-
this._filteredWrap = this._filteredSpan.parentElement !== dom
|
|
87
|
-
? this._filteredSpan.closest('[data-ln-data-table-filtered-wrap]') || this._filteredSpan.parentNode
|
|
88
|
-
: null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Selected count — the parent that wraps "· X selected" text
|
|
92
|
-
// Hidden when nothing is selected
|
|
93
|
-
this._selectedSpan = dom.querySelector('[data-ln-data-table-selected]');
|
|
94
|
-
if (this._selectedSpan) {
|
|
95
|
-
this._selectedWrap = this._selectedSpan.parentElement !== dom
|
|
96
|
-
? this._selectedSpan.closest('[data-ln-data-table-selected-wrap]') || this._selectedSpan.parentNode
|
|
97
|
-
: null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const self = this;
|
|
101
|
-
|
|
102
|
-
// ─── set-data event ────────────────────────────────────
|
|
103
|
-
this._onSetData = function (e) {
|
|
104
|
-
const detail = e.detail || {};
|
|
105
|
-
self._data = detail.data || [];
|
|
106
|
-
self._lastTotal = detail.total != null ? detail.total : self._data.length;
|
|
107
|
-
self._lastFiltered = detail.filtered != null ? detail.filtered : self._data.length;
|
|
108
|
-
|
|
109
|
-
self.totalCount = self._lastTotal;
|
|
110
|
-
self.visibleCount = self._lastFiltered;
|
|
111
|
-
self.isLoaded = true;
|
|
112
|
-
|
|
113
|
-
self._updateFilterOptions(detail.filterOptions);
|
|
114
|
-
|
|
115
|
-
// Invalidate virtual-scroll window cache: data changed, so the
|
|
116
|
-
// cached startRow/endRow in _renderVirtual no longer reflect
|
|
117
|
-
// the current _data. Without this, sort/filter changes that
|
|
118
|
-
// preserve the viewport window would early-return and keep
|
|
119
|
-
// stale rows.
|
|
120
|
-
self._vStart = -1;
|
|
121
|
-
self._vEnd = -1;
|
|
122
|
-
|
|
123
|
-
self._renderRows();
|
|
124
|
-
self._updateFooter();
|
|
125
|
-
|
|
126
|
-
dispatch(dom, 'ln-data-table:rendered', {
|
|
127
|
-
table: self.name,
|
|
128
|
-
total: self.totalCount,
|
|
129
|
-
visible: self.visibleCount
|
|
130
|
-
});
|
|
131
|
-
};
|
|
132
|
-
dom.addEventListener('ln-data-table:set-data', this._onSetData);
|
|
133
|
-
|
|
134
|
-
// ─── set-loading event ─────────────────────────────────
|
|
135
|
-
this._onSetLoading = function (e) {
|
|
136
|
-
const loading = e.detail && e.detail.loading;
|
|
137
|
-
dom.classList.toggle('ln-data-table--loading', !!loading);
|
|
138
|
-
if (loading) {
|
|
139
|
-
self.isLoaded = false;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
dom.addEventListener('ln-data-table:set-loading', this._onSetLoading);
|
|
143
|
-
|
|
144
|
-
// ─── Sort — click on [data-ln-col-sort] buttons ───────
|
|
145
|
-
this._sortButtons = Array.from(dom.querySelectorAll('[data-ln-col-sort]'));
|
|
146
|
-
this._onSortClick = function (e) {
|
|
147
|
-
const btn = e.target.closest('[data-ln-col-sort]');
|
|
148
|
-
if (!btn) return;
|
|
149
|
-
const th = btn.closest('th');
|
|
150
|
-
if (!th) return;
|
|
151
|
-
const field = th.getAttribute('data-ln-col');
|
|
152
|
-
if (!field) return;
|
|
153
|
-
self._handleSort(field, th);
|
|
154
|
-
};
|
|
155
|
-
if (this.thead) {
|
|
156
|
-
this.thead.addEventListener('click', this._onSortClick);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ─── Filter — click on [data-ln-col-filter] buttons ───
|
|
160
|
-
this._activeDropdown = null; // { field, th, el }
|
|
161
|
-
this._onFilterClick = function (e) {
|
|
162
|
-
const btn = e.target.closest('[data-ln-col-filter]');
|
|
163
|
-
if (!btn) return;
|
|
164
|
-
e.stopPropagation();
|
|
165
|
-
const th = btn.closest('th');
|
|
166
|
-
if (!th) return;
|
|
167
|
-
const field = th.getAttribute('data-ln-col');
|
|
168
|
-
if (!field) return;
|
|
169
|
-
|
|
170
|
-
// Toggle: close if already open for this field
|
|
171
|
-
if (self._activeDropdown && self._activeDropdown.field === field) {
|
|
172
|
-
self._closeFilterDropdown();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
self._openFilterDropdown(field, th, btn);
|
|
176
|
-
};
|
|
177
|
-
if (this.thead) {
|
|
178
|
-
this.thead.addEventListener('click', this._onFilterClick);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Outside click closes filter dropdown
|
|
182
|
-
this._onDocClick = function () {
|
|
183
|
-
if (self._activeDropdown) self._closeFilterDropdown();
|
|
184
|
-
};
|
|
185
|
-
document.addEventListener('click', this._onDocClick);
|
|
186
|
-
|
|
187
|
-
// Clear all filters button
|
|
188
|
-
this._onClearAll = function (e) {
|
|
189
|
-
const btn = e.target.closest('[data-ln-data-table-clear-all]');
|
|
190
|
-
if (!btn) return;
|
|
191
|
-
self.currentFilters = {};
|
|
192
|
-
self._updateFilterIndicators();
|
|
193
|
-
dispatch(dom, 'ln-data-table:clear-filters', { table: self.name });
|
|
194
|
-
self._requestData();
|
|
195
|
-
};
|
|
196
|
-
dom.addEventListener('click', this._onClearAll);
|
|
197
|
-
|
|
198
|
-
// ─── Selection — row checkboxes + select-all ──────────
|
|
199
|
-
this._selectable = dom.hasAttribute('data-ln-data-table-selectable');
|
|
200
|
-
this._selectableActive = false;
|
|
201
|
-
if (this._selectable) {
|
|
202
|
-
this._enableSelection();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ─── Row click + Row actions ───────────────────────────
|
|
206
|
-
this._onRowClick = function (e) {
|
|
207
|
-
// Skip if clicking checkbox, action button, link, or button
|
|
208
|
-
if (e.target.closest('[data-ln-row-select]')) return;
|
|
209
|
-
if (e.target.closest('[data-ln-row-action]')) return;
|
|
210
|
-
if (e.target.closest('a') || e.target.closest('button')) return;
|
|
211
|
-
|
|
212
|
-
// Allow ctrl/meta/middle-click to pass through
|
|
213
|
-
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
|
214
|
-
|
|
215
|
-
const tr = e.target.closest('[data-ln-row]');
|
|
216
|
-
if (!tr) return;
|
|
217
|
-
|
|
218
|
-
const id = tr.getAttribute('data-ln-row-id');
|
|
219
|
-
const record = tr._lnRecord || {};
|
|
220
|
-
|
|
221
|
-
dispatch(dom, 'ln-data-table:row-click', {
|
|
222
|
-
table: self.name,
|
|
223
|
-
id: id,
|
|
224
|
-
record: record
|
|
225
|
-
});
|
|
226
|
-
};
|
|
227
|
-
if (this.tbody) this.tbody.addEventListener('click', this._onRowClick);
|
|
228
|
-
|
|
229
|
-
// Row action buttons
|
|
230
|
-
this._onRowAction = function (e) {
|
|
231
|
-
const btn = e.target.closest('[data-ln-row-action]');
|
|
232
|
-
if (!btn) return;
|
|
233
|
-
|
|
234
|
-
e.stopPropagation();
|
|
235
|
-
const tr = btn.closest('[data-ln-row]');
|
|
236
|
-
if (!tr) return;
|
|
237
|
-
|
|
238
|
-
const action = btn.getAttribute('data-ln-row-action');
|
|
239
|
-
const id = tr.getAttribute('data-ln-row-id');
|
|
240
|
-
const record = tr._lnRecord || {};
|
|
241
|
-
|
|
242
|
-
dispatch(dom, 'ln-data-table:row-action', {
|
|
243
|
-
table: self.name,
|
|
244
|
-
id: id,
|
|
245
|
-
action: action,
|
|
246
|
-
record: record
|
|
247
|
-
});
|
|
248
|
-
};
|
|
249
|
-
if (this.tbody) this.tbody.addEventListener('click', this._onRowAction);
|
|
250
|
-
|
|
251
|
-
// ─── Search — input on [data-ln-data-table-search] ────
|
|
252
|
-
this._searchInput = dom.querySelector('[data-ln-data-table-search]');
|
|
253
|
-
if (this._searchInput) {
|
|
254
|
-
this._onSearchInput = function () {
|
|
255
|
-
self.currentSearch = self._searchInput.value;
|
|
256
|
-
dispatch(dom, 'ln-data-table:search', {
|
|
257
|
-
table: self.name,
|
|
258
|
-
query: self.currentSearch
|
|
259
|
-
});
|
|
260
|
-
self._requestData();
|
|
261
|
-
};
|
|
262
|
-
this._searchInput.addEventListener('input', this._onSearchInput);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ─── Keyboard navigation ──────────────────────────────
|
|
266
|
-
this._focusedRowIndex = -1;
|
|
267
|
-
|
|
268
|
-
this._onKeydown = function (e) {
|
|
269
|
-
// Only handle keys when table area has focus
|
|
270
|
-
if (!dom.contains(document.activeElement) && document.activeElement !== document.body) return;
|
|
271
|
-
// Don't intercept when typing in search/filter inputs
|
|
272
|
-
if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return;
|
|
273
|
-
|
|
274
|
-
// / focuses search (only when no input is active — checked above)
|
|
275
|
-
if (e.key === '/') {
|
|
276
|
-
if (self._searchInput) {
|
|
277
|
-
e.preventDefault();
|
|
278
|
-
self._searchInput.focus();
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const rows = self.tbody ? Array.from(self.tbody.querySelectorAll('[data-ln-row]')) : [];
|
|
284
|
-
if (!rows.length) return;
|
|
285
|
-
|
|
286
|
-
switch (e.key) {
|
|
287
|
-
case 'ArrowDown':
|
|
288
|
-
e.preventDefault();
|
|
289
|
-
self._focusedRowIndex = Math.min(self._focusedRowIndex + 1, rows.length - 1);
|
|
290
|
-
self._focusRow(rows);
|
|
291
|
-
break;
|
|
292
|
-
case 'ArrowUp':
|
|
293
|
-
e.preventDefault();
|
|
294
|
-
self._focusedRowIndex = Math.max(self._focusedRowIndex - 1, 0);
|
|
295
|
-
self._focusRow(rows);
|
|
296
|
-
break;
|
|
297
|
-
case 'Home':
|
|
298
|
-
e.preventDefault();
|
|
299
|
-
self._focusedRowIndex = 0;
|
|
300
|
-
self._focusRow(rows);
|
|
301
|
-
break;
|
|
302
|
-
case 'End':
|
|
303
|
-
e.preventDefault();
|
|
304
|
-
self._focusedRowIndex = rows.length - 1;
|
|
305
|
-
self._focusRow(rows);
|
|
306
|
-
break;
|
|
307
|
-
case 'Enter':
|
|
308
|
-
if (self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
|
|
309
|
-
e.preventDefault();
|
|
310
|
-
const tr = rows[self._focusedRowIndex];
|
|
311
|
-
dispatch(dom, 'ln-data-table:row-click', {
|
|
312
|
-
table: self.name,
|
|
313
|
-
id: tr.getAttribute('data-ln-row-id'),
|
|
314
|
-
record: tr._lnRecord || {}
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
break;
|
|
318
|
-
case ' ':
|
|
319
|
-
if (self._selectable && self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
|
|
320
|
-
e.preventDefault();
|
|
321
|
-
const cb = rows[self._focusedRowIndex].querySelector('[data-ln-row-select]');
|
|
322
|
-
if (cb) {
|
|
323
|
-
cb.checked = !cb.checked;
|
|
324
|
-
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
break;
|
|
328
|
-
case 'Escape':
|
|
329
|
-
if (self._activeDropdown) {
|
|
330
|
-
self._closeFilterDropdown();
|
|
331
|
-
}
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
document.addEventListener('keydown', this._onKeydown);
|
|
336
|
-
|
|
337
|
-
// Fire initial request-data so coordinator knows table is ready
|
|
338
|
-
dispatch(dom, 'ln-data-table:request-data', {
|
|
339
|
-
table: this.name,
|
|
340
|
-
sort: this.currentSort,
|
|
341
|
-
filters: this.currentFilters,
|
|
342
|
-
search: this.currentSearch
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
return this;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ─── Sort ──────────────────────────────────────────────────
|
|
349
|
-
|
|
350
|
-
_component.prototype._handleSort = function (field, th) {
|
|
351
|
-
let newDir;
|
|
352
|
-
|
|
353
|
-
if (!this.currentSort || this.currentSort.field !== field) {
|
|
354
|
-
newDir = 'asc';
|
|
355
|
-
} else if (this.currentSort.direction === 'asc') {
|
|
356
|
-
newDir = 'desc';
|
|
357
|
-
} else {
|
|
358
|
-
newDir = null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Reset all sort classes
|
|
362
|
-
for (let i = 0; i < this.ths.length; i++) {
|
|
363
|
-
this.ths[i].classList.remove('ln-sort-asc', 'ln-sort-desc');
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (newDir) {
|
|
367
|
-
this.currentSort = { field: field, direction: newDir };
|
|
368
|
-
th.classList.add(newDir === 'asc' ? 'ln-sort-asc' : 'ln-sort-desc');
|
|
369
|
-
} else {
|
|
370
|
-
this.currentSort = null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
dispatch(this.dom, 'ln-data-table:sort', {
|
|
374
|
-
table: this.name,
|
|
375
|
-
field: field,
|
|
376
|
-
direction: newDir
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
this._requestData();
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// ─── Request data (shared helper) ──────────────────────────
|
|
383
|
-
|
|
384
|
-
_component.prototype._requestData = function () {
|
|
385
|
-
dispatch(this.dom, 'ln-data-table:request-data', {
|
|
386
|
-
table: this.name,
|
|
387
|
-
sort: this.currentSort,
|
|
388
|
-
filters: this.currentFilters,
|
|
389
|
-
search: this.currentSearch
|
|
390
|
-
});
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
// ─── Selection helpers ─────────────────────────────────────
|
|
394
|
-
|
|
395
|
-
_component.prototype._updateSelectAll = function () {
|
|
396
|
-
if (!this._selectAllCheckbox || !this.tbody) return;
|
|
397
|
-
const rows = this.tbody.querySelectorAll('[data-ln-row]');
|
|
398
|
-
let allSelected = rows.length > 0;
|
|
399
|
-
for (let i = 0; i < rows.length; i++) {
|
|
400
|
-
const id = rows[i].getAttribute('data-ln-row-id');
|
|
401
|
-
if (id != null && !this.selectedIds.has(id)) {
|
|
402
|
-
allSelected = false;
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
this._selectAllCheckbox.checked = allSelected;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
Object.defineProperty(_component.prototype, 'selectedCount', {
|
|
410
|
-
get: function () { return this.selectedIds.size; },
|
|
411
|
-
set: function () { /* computed from selectedIds */ }
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
_component.prototype._enableSelection = function () {
|
|
415
|
-
if (this._selectableActive) return;
|
|
416
|
-
this._selectableActive = true;
|
|
417
|
-
|
|
418
|
-
const self = this;
|
|
419
|
-
this._onSelectionChange = function (e) {
|
|
420
|
-
const checkbox = e.target.closest('[data-ln-row-select]');
|
|
421
|
-
if (!checkbox) return;
|
|
422
|
-
const tr = checkbox.closest('[data-ln-row]');
|
|
423
|
-
if (!tr) return;
|
|
424
|
-
const id = tr.getAttribute('data-ln-row-id');
|
|
425
|
-
if (id == null) return;
|
|
426
|
-
|
|
427
|
-
if (checkbox.checked) {
|
|
428
|
-
self.selectedIds.add(id);
|
|
429
|
-
tr.classList.add('ln-row-selected');
|
|
430
|
-
} else {
|
|
431
|
-
self.selectedIds.delete(id);
|
|
432
|
-
tr.classList.remove('ln-row-selected');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
self.selectedCount = self.selectedIds.size;
|
|
436
|
-
self._updateSelectAll();
|
|
437
|
-
self._updateFooter();
|
|
438
|
-
|
|
439
|
-
dispatch(self.dom, 'ln-data-table:select', {
|
|
440
|
-
table: self.name,
|
|
441
|
-
selectedIds: self.selectedIds,
|
|
442
|
-
count: self.selectedCount
|
|
443
|
-
});
|
|
444
|
-
};
|
|
445
|
-
if (this.tbody) this.tbody.addEventListener('change', this._onSelectionChange);
|
|
446
|
-
|
|
447
|
-
// Select-all checkbox in header
|
|
448
|
-
this._selectAllCheckbox = this.dom.querySelector('[data-ln-col-select] input[type="checkbox"]')
|
|
449
|
-
|| this.dom.querySelector('[data-ln-col-select]');
|
|
450
|
-
if (this._selectAllCheckbox && this._selectAllCheckbox.tagName === 'TH') {
|
|
451
|
-
// Create a checkbox inside the th if it doesn't have one
|
|
452
|
-
const cb = document.createElement('input');
|
|
453
|
-
cb.type = 'checkbox';
|
|
454
|
-
cb.setAttribute('aria-label', 'Select all');
|
|
455
|
-
this._selectAllCheckbox.appendChild(cb);
|
|
456
|
-
this._selectAllCheckbox = cb;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (this._selectAllCheckbox) {
|
|
460
|
-
this._onSelectAll = function () {
|
|
461
|
-
const checked = self._selectAllCheckbox.checked;
|
|
462
|
-
const rows = self.tbody ? self.tbody.querySelectorAll('[data-ln-row]') : [];
|
|
463
|
-
|
|
464
|
-
for (let i = 0; i < rows.length; i++) {
|
|
465
|
-
const id = rows[i].getAttribute('data-ln-row-id');
|
|
466
|
-
const rowCb = rows[i].querySelector('[data-ln-row-select]');
|
|
467
|
-
if (id == null) continue;
|
|
468
|
-
|
|
469
|
-
if (checked) {
|
|
470
|
-
self.selectedIds.add(id);
|
|
471
|
-
rows[i].classList.add('ln-row-selected');
|
|
472
|
-
} else {
|
|
473
|
-
self.selectedIds.delete(id);
|
|
474
|
-
rows[i].classList.remove('ln-row-selected');
|
|
475
|
-
}
|
|
476
|
-
if (rowCb) rowCb.checked = checked;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
self.selectedCount = self.selectedIds.size;
|
|
480
|
-
dispatch(self.dom, 'ln-data-table:select-all', {
|
|
481
|
-
table: self.name,
|
|
482
|
-
selected: checked
|
|
483
|
-
});
|
|
484
|
-
dispatch(self.dom, 'ln-data-table:select', {
|
|
485
|
-
table: self.name,
|
|
486
|
-
selectedIds: self.selectedIds,
|
|
487
|
-
count: self.selectedCount
|
|
488
|
-
});
|
|
489
|
-
self._updateFooter();
|
|
490
|
-
};
|
|
491
|
-
this._selectAllCheckbox.addEventListener('change', this._onSelectAll);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Sync initial checkbox state (browser form restore)
|
|
495
|
-
if (this.tbody) {
|
|
496
|
-
const rows = this.tbody.querySelectorAll('[data-ln-row]');
|
|
497
|
-
for (let i = 0; i < rows.length; i++) {
|
|
498
|
-
const cb = rows[i].querySelector('[data-ln-row-select]');
|
|
499
|
-
const id = rows[i].getAttribute('data-ln-row-id');
|
|
500
|
-
if (cb && cb.checked && id != null) {
|
|
501
|
-
this.selectedIds.add(id);
|
|
502
|
-
rows[i].classList.add('ln-row-selected');
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
this.selectedCount = this.selectedIds.size;
|
|
506
|
-
if (this.selectedCount > 0) this._updateSelectAll();
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
_component.prototype._disableSelection = function () {
|
|
511
|
-
if (!this._selectableActive) return;
|
|
512
|
-
this._selectableActive = false;
|
|
513
|
-
|
|
514
|
-
if (this.tbody && this._onSelectionChange) {
|
|
515
|
-
this.tbody.removeEventListener('change', this._onSelectionChange);
|
|
516
|
-
}
|
|
517
|
-
if (this._selectAllCheckbox && this._onSelectAll) {
|
|
518
|
-
this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Remove dynamically created checkbox in TH
|
|
522
|
-
const th = this.dom.querySelector('[data-ln-col-select]');
|
|
523
|
-
if (th) {
|
|
524
|
-
const cb = th.querySelector('input[type="checkbox"]');
|
|
525
|
-
if (cb) {
|
|
526
|
-
cb.remove();
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
this._selectAllCheckbox = null;
|
|
530
|
-
|
|
531
|
-
// Clear selections
|
|
532
|
-
this.selectedIds.clear();
|
|
533
|
-
this.selectedCount = 0;
|
|
534
|
-
|
|
535
|
-
// Clear visual state of rows
|
|
536
|
-
if (this.tbody) {
|
|
537
|
-
const rows = this.tbody.querySelectorAll('[data-ln-row]');
|
|
538
|
-
for (let i = 0; i < rows.length; i++) {
|
|
539
|
-
rows[i].classList.remove('ln-row-selected');
|
|
540
|
-
const cb = rows[i].querySelector('[data-ln-row-select]');
|
|
541
|
-
if (cb) cb.checked = false;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
this._updateFooter();
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
// ─── Keyboard focus ───────────────────────────────────────
|
|
549
|
-
|
|
550
|
-
_component.prototype._focusRow = function (rows) {
|
|
551
|
-
// Remove focus from all
|
|
552
|
-
for (let i = 0; i < rows.length; i++) {
|
|
553
|
-
rows[i].classList.remove('ln-row-focused');
|
|
554
|
-
rows[i].removeAttribute('tabindex');
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (this._focusedRowIndex >= 0 && this._focusedRowIndex < rows.length) {
|
|
558
|
-
const row = rows[this._focusedRowIndex];
|
|
559
|
-
row.classList.add('ln-row-focused');
|
|
560
|
-
row.setAttribute('tabindex', '0');
|
|
561
|
-
row.focus();
|
|
562
|
-
row.scrollIntoView({ block: 'nearest' });
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
// ─── Filter dropdown ───────────────────────────────────────
|
|
567
|
-
|
|
568
|
-
_component.prototype._openFilterDropdown = function (field, th, btn) {
|
|
569
|
-
this._closeFilterDropdown();
|
|
570
|
-
|
|
571
|
-
// Try table-scoped template first, fall back to generic
|
|
572
|
-
const clone = cloneTemplateScoped(this.dom, this.name + '-column-filter', 'ln-data-table')
|
|
573
|
-
|| cloneTemplateScoped(this.dom, 'column-filter', 'ln-data-table');
|
|
574
|
-
if (!clone) return;
|
|
575
|
-
|
|
576
|
-
const dropdown = clone.firstElementChild;
|
|
577
|
-
if (!dropdown) return;
|
|
578
|
-
|
|
579
|
-
// Populate checkboxes with unique values from data
|
|
580
|
-
const uniqueValues = this._getUniqueValues(field);
|
|
581
|
-
const optionsList = dropdown.querySelector('[data-ln-filter-options]');
|
|
582
|
-
const searchInput = dropdown.querySelector('[data-ln-filter-search]');
|
|
583
|
-
const activeValues = this.currentFilters[field] || [];
|
|
584
|
-
const self = this;
|
|
585
|
-
|
|
586
|
-
// Hide search if <=8 values
|
|
587
|
-
if (searchInput && uniqueValues.length <= 8) {
|
|
588
|
-
searchInput.classList.add('hidden');
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Build checkbox items
|
|
592
|
-
if (optionsList) {
|
|
593
|
-
// Update the "All" reset checkbox status if statically present in template
|
|
594
|
-
const resetCheckbox = optionsList.querySelector('[data-ln-filter-reset]');
|
|
595
|
-
if (resetCheckbox) {
|
|
596
|
-
resetCheckbox.checked = activeValues.length === 0;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Get the options item template (scoped first, fall back to generic)
|
|
600
|
-
const itemTmpl = cloneTemplateScoped(dropdown, this.name + '-column-filter-item', 'ln-data-table')
|
|
601
|
-
|| cloneTemplateScoped(dropdown, 'column-filter-item', 'ln-data-table');
|
|
602
|
-
|
|
603
|
-
if (itemTmpl) {
|
|
604
|
-
for (let i = 0; i < uniqueValues.length; i++) {
|
|
605
|
-
const val = uniqueValues[i];
|
|
606
|
-
const itemClone = itemTmpl.cloneNode(true);
|
|
607
|
-
fill(itemClone, { value: val });
|
|
608
|
-
|
|
609
|
-
const checkbox = itemClone.querySelector('input[type="checkbox"]');
|
|
610
|
-
if (checkbox) {
|
|
611
|
-
checkbox.value = val;
|
|
612
|
-
checkbox.checked = activeValues.length > 0 && activeValues.indexOf(val) !== -1;
|
|
613
|
-
}
|
|
614
|
-
optionsList.appendChild(itemClone);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Checkbox change handler — mutual exclusion + filter update
|
|
619
|
-
optionsList.addEventListener('change', function (e) {
|
|
620
|
-
if (e.target.type !== 'checkbox') return;
|
|
621
|
-
self._applyFilterMutualExclusion(e.target, optionsList);
|
|
622
|
-
self._onFilterChange(field, optionsList);
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Search within dropdown values
|
|
627
|
-
if (searchInput) {
|
|
628
|
-
searchInput.addEventListener('input', function () {
|
|
629
|
-
const term = searchInput.value.toLowerCase();
|
|
630
|
-
const items = optionsList.querySelectorAll('li');
|
|
631
|
-
for (let i = 0; i < items.length; i++) {
|
|
632
|
-
const text = items[i].textContent.toLowerCase();
|
|
633
|
-
items[i].classList.toggle('hidden', term && text.indexOf(term) === -1);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Clear filter button
|
|
639
|
-
const clearBtn = dropdown.querySelector('[data-ln-filter-clear]');
|
|
640
|
-
if (clearBtn) {
|
|
641
|
-
clearBtn.addEventListener('click', function () {
|
|
642
|
-
delete self.currentFilters[field];
|
|
643
|
-
self._closeFilterDropdown();
|
|
644
|
-
self._updateFilterIndicators();
|
|
645
|
-
dispatch(self.dom, 'ln-data-table:filter', {
|
|
646
|
-
table: self.name,
|
|
647
|
-
field: field,
|
|
648
|
-
values: []
|
|
649
|
-
});
|
|
650
|
-
self._requestData();
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Append to th — CSS handles positioning (th is relative, dropdown is absolute)
|
|
655
|
-
th.appendChild(dropdown);
|
|
656
|
-
|
|
657
|
-
this._activeDropdown = { field: field, th: th, el: dropdown };
|
|
658
|
-
|
|
659
|
-
// Stop click propagation inside dropdown
|
|
660
|
-
dropdown.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
_component.prototype._closeFilterDropdown = function () {
|
|
664
|
-
if (!this._activeDropdown) return;
|
|
665
|
-
if (this._activeDropdown.el && this._activeDropdown.el.parentNode) {
|
|
666
|
-
this._activeDropdown.el.parentNode.removeChild(this._activeDropdown.el);
|
|
667
|
-
}
|
|
668
|
-
this._activeDropdown = null;
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
// ─── Filter sentinel mutual exclusion ──────────────────────
|
|
672
|
-
// Same pattern as ln-filter: "All" sentinel + value checkboxes.
|
|
673
|
-
|
|
674
|
-
_component.prototype._applyFilterMutualExclusion = function (cb, optionsList) {
|
|
675
|
-
const isReset = cb.hasAttribute('data-ln-filter-reset');
|
|
676
|
-
const resetCb = optionsList.querySelector('[data-ln-filter-reset]');
|
|
677
|
-
const valueCbs = optionsList.querySelectorAll('input[type="checkbox"]:not([data-ln-filter-reset])');
|
|
678
|
-
|
|
679
|
-
if (isReset) {
|
|
680
|
-
// "All" clicked — force checked, uncheck all values
|
|
681
|
-
cb.checked = true;
|
|
682
|
-
for (let i = 0; i < valueCbs.length; i++) valueCbs[i].checked = false;
|
|
683
|
-
} else if (cb.checked) {
|
|
684
|
-
// Value checked — uncheck "All"
|
|
685
|
-
if (resetCb) resetCb.checked = false;
|
|
686
|
-
} else {
|
|
687
|
-
// Value unchecked — if none left, re-check "All"
|
|
688
|
-
let any = false;
|
|
689
|
-
for (let i = 0; i < valueCbs.length; i++) {
|
|
690
|
-
if (valueCbs[i].checked) { any = true; break; }
|
|
691
|
-
}
|
|
692
|
-
if (!any && resetCb) resetCb.checked = true;
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
_component.prototype._onFilterChange = function (field, optionsList) {
|
|
697
|
-
const resetCb = optionsList.querySelector('[data-ln-filter-reset]');
|
|
698
|
-
const valueCbs = optionsList.querySelectorAll('input[type="checkbox"]:not([data-ln-filter-reset])');
|
|
699
|
-
const checked = [];
|
|
700
|
-
|
|
701
|
-
for (let i = 0; i < valueCbs.length; i++) {
|
|
702
|
-
if (valueCbs[i].checked) checked.push(valueCbs[i].value);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// "All" checked or nothing checked = no filter active
|
|
706
|
-
const isReset = (resetCb && resetCb.checked) || checked.length === 0;
|
|
707
|
-
if (isReset) {
|
|
708
|
-
delete this.currentFilters[field];
|
|
709
|
-
} else {
|
|
710
|
-
this.currentFilters[field] = checked;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
this._updateFilterIndicators();
|
|
714
|
-
|
|
715
|
-
dispatch(this.dom, 'ln-data-table:filter', {
|
|
716
|
-
table: this.name,
|
|
717
|
-
field: field,
|
|
718
|
-
values: isReset ? [] : checked
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
this._requestData();
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
_component.prototype._updateFilterOptions = function (authoritative) {
|
|
725
|
-
if (authoritative !== null && typeof authoritative === 'object' && !Array.isArray(authoritative)) {
|
|
726
|
-
// Coordinator-supplied authoritative values — replace per-field
|
|
727
|
-
const keys = Object.keys(authoritative);
|
|
728
|
-
for (let i = 0; i < keys.length; i++) {
|
|
729
|
-
const field = keys[i];
|
|
730
|
-
const vals = authoritative[field];
|
|
731
|
-
if (!Array.isArray(vals)) continue;
|
|
732
|
-
const seen = {};
|
|
733
|
-
const unique = [];
|
|
734
|
-
for (let j = 0; j < vals.length; j++) {
|
|
735
|
-
const s = String(vals[j]);
|
|
736
|
-
if (!seen[s]) { seen[s] = true; unique.push(s); }
|
|
737
|
-
}
|
|
738
|
-
this._filterOptions[field] = unique.sort();
|
|
739
|
-
}
|
|
740
|
-
} else {
|
|
741
|
-
// Auto-derive from current payload — additive merge only
|
|
742
|
-
const fields = this._filterableFields;
|
|
743
|
-
const data = this._data;
|
|
744
|
-
for (let fi = 0; fi < fields.length; fi++) {
|
|
745
|
-
const field = fields[fi];
|
|
746
|
-
if (!this._filterOptions[field]) this._filterOptions[field] = [];
|
|
747
|
-
const existing = this._filterOptions[field];
|
|
748
|
-
const seen = {};
|
|
749
|
-
for (let k = 0; k < existing.length; k++) { seen[existing[k]] = true; }
|
|
750
|
-
for (let i = 0; i < data.length; i++) {
|
|
751
|
-
const val = data[i][field];
|
|
752
|
-
if (val != null) {
|
|
753
|
-
const s = String(val);
|
|
754
|
-
if (!seen[s]) { seen[s] = true; existing.push(s); }
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
existing.sort();
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
_component.prototype._getUniqueValues = function (field) {
|
|
763
|
-
return (this._filterOptions[field] || []).slice().sort();
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
_component.prototype._updateFilterIndicators = function () {
|
|
767
|
-
const ths = this.ths;
|
|
768
|
-
for (let i = 0; i < ths.length; i++) {
|
|
769
|
-
const th = ths[i];
|
|
770
|
-
const field = th.getAttribute('data-ln-col');
|
|
771
|
-
if (!field) continue;
|
|
772
|
-
const btn = th.querySelector('[data-ln-col-filter]');
|
|
773
|
-
if (!btn) continue;
|
|
774
|
-
const hasFilter = this.currentFilters[field] && this.currentFilters[field].length > 0;
|
|
775
|
-
btn.classList.toggle('ln-filter-active', !!hasFilter);
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
|
|
779
|
-
// ─── Row rendering ─────────────────────────────────────────
|
|
780
|
-
|
|
781
|
-
_component.prototype._renderRows = function () {
|
|
782
|
-
if (!this.tbody) return;
|
|
783
|
-
|
|
784
|
-
const data = this._data;
|
|
785
|
-
const total = this._lastTotal;
|
|
786
|
-
const filtered = this._lastFiltered;
|
|
787
|
-
|
|
788
|
-
// Empty state: no data at all
|
|
789
|
-
if (total === 0) {
|
|
790
|
-
this._disableVirtualScroll();
|
|
791
|
-
this._showEmptyState(this.name + '-empty');
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Empty state: filters returned zero
|
|
796
|
-
if (data.length === 0 || filtered === 0) {
|
|
797
|
-
this._disableVirtualScroll();
|
|
798
|
-
this._showEmptyState(this.name + '-empty-filtered');
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (data.length > VIRTUAL_THRESHOLD) {
|
|
803
|
-
this._enableVirtualScroll();
|
|
804
|
-
this._renderVirtual();
|
|
805
|
-
} else {
|
|
806
|
-
this._disableVirtualScroll();
|
|
807
|
-
this._renderAll();
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
_component.prototype._renderAll = function () {
|
|
812
|
-
const data = this._data;
|
|
813
|
-
const frag = document.createDocumentFragment();
|
|
814
|
-
|
|
815
|
-
for (let i = 0; i < data.length; i++) {
|
|
816
|
-
const tr = this._buildRow(data[i]);
|
|
817
|
-
if (!tr) break;
|
|
818
|
-
frag.appendChild(tr);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
this.tbody.textContent = '';
|
|
822
|
-
this.tbody.appendChild(frag);
|
|
823
|
-
|
|
824
|
-
if (this._selectable) this._updateSelectAll();
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
_component.prototype._buildRow = function (record) {
|
|
828
|
-
const clone = cloneTemplateScoped(this.dom, this.name + '-row', 'ln-data-table');
|
|
829
|
-
if (!clone) return null;
|
|
830
|
-
|
|
831
|
-
const tr = clone.querySelector('[data-ln-row]') || clone.firstElementChild;
|
|
832
|
-
if (!tr) return null;
|
|
833
|
-
|
|
834
|
-
this._fillRow(tr, record);
|
|
835
|
-
|
|
836
|
-
tr._lnRecord = record;
|
|
837
|
-
if (record.id != null) {
|
|
838
|
-
tr.setAttribute('data-ln-row-id', record.id);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Restore selection state
|
|
842
|
-
if (this._selectable && record.id != null && this.selectedIds.has(String(record.id))) {
|
|
843
|
-
tr.classList.add('ln-row-selected');
|
|
844
|
-
const rowCb = tr.querySelector('[data-ln-row-select]');
|
|
845
|
-
if (rowCb) rowCb.checked = true;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return tr;
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
// ─── Virtual scroll ────────────────────────────────────────
|
|
852
|
-
|
|
853
|
-
_component.prototype._enableVirtualScroll = function () {
|
|
854
|
-
if (this._virtual) return;
|
|
855
|
-
this._virtual = true;
|
|
856
|
-
this._vStart = -1;
|
|
857
|
-
this._vEnd = -1;
|
|
858
|
-
const self = this;
|
|
859
|
-
|
|
860
|
-
// Measure row height from first row
|
|
861
|
-
if (!this._rowHeight) {
|
|
862
|
-
const tempRow = this._buildRow(this._data[0]);
|
|
863
|
-
if (tempRow) {
|
|
864
|
-
this.tbody.textContent = '';
|
|
865
|
-
this.tbody.appendChild(tempRow);
|
|
866
|
-
this._rowHeight = tempRow.offsetHeight || 40;
|
|
867
|
-
this.tbody.textContent = '';
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Resolve the scroll target every time we enable — the DOM may have
|
|
872
|
-
// moved between the previous disable and now (re-parenting, modal
|
|
873
|
-
// open, list crosses VIRTUAL_THRESHOLD again after a filter).
|
|
874
|
-
// null → page scrolls; we listen on window.
|
|
875
|
-
this._scrollContainer = _findScrollContainer(this.dom);
|
|
876
|
-
const scrollTarget = this._scrollContainer || window;
|
|
877
|
-
|
|
878
|
-
this._scrollHandler = function () {
|
|
879
|
-
if (self._rafId) return;
|
|
880
|
-
self._rafId = requestAnimationFrame(function () {
|
|
881
|
-
self._rafId = null;
|
|
882
|
-
self._renderVirtual();
|
|
883
|
-
});
|
|
884
|
-
};
|
|
885
|
-
|
|
886
|
-
scrollTarget.addEventListener('scroll', this._scrollHandler, { passive: true });
|
|
887
|
-
// Viewport resize always matters (changes visible-row count), even
|
|
888
|
-
// when scrolling happens inside a container.
|
|
889
|
-
window.addEventListener('resize', this._scrollHandler, { passive: true });
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
_component.prototype._disableVirtualScroll = function () {
|
|
893
|
-
if (!this._virtual) return;
|
|
894
|
-
this._virtual = false;
|
|
895
|
-
|
|
896
|
-
if (this._scrollHandler) {
|
|
897
|
-
const scrollTarget = this._scrollContainer || window;
|
|
898
|
-
scrollTarget.removeEventListener('scroll', this._scrollHandler);
|
|
899
|
-
window.removeEventListener('resize', this._scrollHandler);
|
|
900
|
-
this._scrollHandler = null;
|
|
901
|
-
}
|
|
902
|
-
this._scrollContainer = null;
|
|
903
|
-
if (this._rafId) {
|
|
904
|
-
cancelAnimationFrame(this._rafId);
|
|
905
|
-
this._rafId = null;
|
|
906
|
-
}
|
|
907
|
-
this._vStart = -1;
|
|
908
|
-
this._vEnd = -1;
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
_component.prototype._renderVirtual = function () {
|
|
912
|
-
const data = this._data;
|
|
913
|
-
const total = data.length;
|
|
914
|
-
const rowH = this._rowHeight;
|
|
915
|
-
if (!rowH || !total) return;
|
|
916
|
-
|
|
917
|
-
const theadH = this.thead ? this.thead.offsetHeight : 0;
|
|
918
|
-
const sc = this._scrollContainer;
|
|
919
|
-
let scrollIntoData;
|
|
920
|
-
let viewportH;
|
|
921
|
-
|
|
922
|
-
if (sc) {
|
|
923
|
-
// Scrolling happens inside an ancestor element. Compute table
|
|
924
|
-
// position relative to that container, using its scrollTop and
|
|
925
|
-
// clientHeight as the viewport.
|
|
926
|
-
const tableRect = this.table.getBoundingClientRect();
|
|
927
|
-
const scRect = sc.getBoundingClientRect();
|
|
928
|
-
const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
|
|
929
|
-
scrollIntoData = sc.scrollTop - dataStartInContainer;
|
|
930
|
-
viewportH = sc.clientHeight;
|
|
931
|
-
} else {
|
|
932
|
-
// No scrolling ancestor — page scrolls. Use window coords.
|
|
933
|
-
const tableRect = this.table.getBoundingClientRect();
|
|
934
|
-
const tableTopInPage = tableRect.top + window.scrollY;
|
|
935
|
-
const dataStartInPage = tableTopInPage + theadH;
|
|
936
|
-
scrollIntoData = window.scrollY - dataStartInPage;
|
|
937
|
-
viewportH = window.innerHeight;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
let startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
|
|
941
|
-
startRow = Math.min(startRow, total);
|
|
942
|
-
const endRow = Math.min(startRow + Math.ceil(viewportH / rowH) + (BUFFER_ROWS * 2), total);
|
|
943
|
-
|
|
944
|
-
if (startRow === this._vStart && endRow === this._vEnd) return;
|
|
945
|
-
this._vStart = startRow;
|
|
946
|
-
this._vEnd = endRow;
|
|
947
|
-
|
|
948
|
-
const colSpan = this.ths.length || 1;
|
|
949
|
-
const topH = startRow * rowH;
|
|
950
|
-
const bottomH = (total - endRow) * rowH;
|
|
951
|
-
const frag = document.createDocumentFragment();
|
|
952
|
-
|
|
953
|
-
// Top spacer
|
|
954
|
-
if (topH > 0) {
|
|
955
|
-
const topSpacer = document.createElement('tr');
|
|
956
|
-
topSpacer.className = 'ln-data-table__spacer';
|
|
957
|
-
topSpacer.setAttribute('aria-hidden', 'true');
|
|
958
|
-
const topTd = document.createElement('td');
|
|
959
|
-
topTd.setAttribute('colspan', colSpan);
|
|
960
|
-
topTd.style.height = topH + 'px';
|
|
961
|
-
topSpacer.appendChild(topTd);
|
|
962
|
-
frag.appendChild(topSpacer);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Visible rows
|
|
966
|
-
for (let i = startRow; i < endRow; i++) {
|
|
967
|
-
const tr = this._buildRow(data[i]);
|
|
968
|
-
if (tr) frag.appendChild(tr);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Bottom spacer
|
|
972
|
-
if (bottomH > 0) {
|
|
973
|
-
const bottomSpacer = document.createElement('tr');
|
|
974
|
-
bottomSpacer.className = 'ln-data-table__spacer';
|
|
975
|
-
bottomSpacer.setAttribute('aria-hidden', 'true');
|
|
976
|
-
const bottomTd = document.createElement('td');
|
|
977
|
-
bottomTd.setAttribute('colspan', colSpan);
|
|
978
|
-
bottomTd.style.height = bottomH + 'px';
|
|
979
|
-
bottomSpacer.appendChild(bottomTd);
|
|
980
|
-
frag.appendChild(bottomSpacer);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
this.tbody.textContent = '';
|
|
984
|
-
this.tbody.appendChild(frag);
|
|
985
|
-
|
|
986
|
-
if (this._selectable) this._updateSelectAll();
|
|
987
|
-
};
|
|
988
|
-
|
|
989
|
-
// ─── Fill row cells ────────────────────────────────────────
|
|
990
|
-
|
|
991
|
-
_component.prototype._fillRow = function (tr, record) {
|
|
992
|
-
// Native double curly brace interpolation: {{ field }}
|
|
993
|
-
fillTemplate(tr, record);
|
|
994
|
-
|
|
995
|
-
// data-ln-cell-attr="field:attr" → setAttribute
|
|
996
|
-
const cellAttrs = tr.querySelectorAll('[data-ln-cell-attr]');
|
|
997
|
-
for (let i = 0; i < cellAttrs.length; i++) {
|
|
998
|
-
const el = cellAttrs[i];
|
|
999
|
-
const pairs = el.getAttribute('data-ln-cell-attr').split(',');
|
|
1000
|
-
for (let j = 0; j < pairs.length; j++) {
|
|
1001
|
-
const parts = pairs[j].trim().split(':');
|
|
1002
|
-
if (parts.length !== 2) continue;
|
|
1003
|
-
const field = parts[0].trim();
|
|
1004
|
-
const attr = parts[1].trim();
|
|
1005
|
-
if (record[field] != null) {
|
|
1006
|
-
el.setAttribute(attr, record[field]);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
|
-
// ─── Empty state ───────────────────────────────────────────
|
|
1013
|
-
|
|
1014
|
-
_component.prototype._showEmptyState = function (templateName) {
|
|
1015
|
-
const clone = cloneTemplateScoped(this.dom, templateName, 'ln-data-table');
|
|
1016
|
-
this.tbody.textContent = '';
|
|
1017
|
-
if (clone) {
|
|
1018
|
-
this.tbody.appendChild(clone);
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
|
-
// ─── Footer ────────────────────────────────────────────────
|
|
1023
|
-
|
|
1024
|
-
_component.prototype._updateFooter = function () {
|
|
1025
|
-
const total = this._lastTotal;
|
|
1026
|
-
const filtered = this._lastFiltered;
|
|
1027
|
-
const isFiltered = filtered < total;
|
|
1028
|
-
|
|
1029
|
-
if (this._totalSpan) {
|
|
1030
|
-
this._totalSpan.textContent = _formatNum(total);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (this._filteredSpan) {
|
|
1034
|
-
this._filteredSpan.textContent = isFiltered ? _formatNum(filtered) : '';
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Hide filtered wrapper when not filtering
|
|
1038
|
-
if (this._filteredWrap) {
|
|
1039
|
-
this._filteredWrap.classList.toggle('hidden', !isFiltered);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
if (this._selectedSpan) {
|
|
1043
|
-
const count = this.selectedIds.size;
|
|
1044
|
-
this._selectedSpan.textContent = count > 0 ? _formatNum(count) : '';
|
|
1045
|
-
if (this._selectedWrap) {
|
|
1046
|
-
this._selectedWrap.classList.toggle('hidden', count === 0);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
// ─── Destroy ───────────────────────────────────────────────
|
|
1052
|
-
|
|
1053
|
-
_component.prototype.destroy = function () {
|
|
1054
|
-
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
1055
|
-
this.dom.removeEventListener('ln-data-table:set-data', this._onSetData);
|
|
1056
|
-
this.dom.removeEventListener('ln-data-table:set-loading', this._onSetLoading);
|
|
1057
|
-
if (this.thead) {
|
|
1058
|
-
this.thead.removeEventListener('click', this._onSortClick);
|
|
1059
|
-
this.thead.removeEventListener('click', this._onFilterClick);
|
|
1060
|
-
}
|
|
1061
|
-
document.removeEventListener('click', this._onDocClick);
|
|
1062
|
-
document.removeEventListener('keydown', this._onKeydown);
|
|
1063
|
-
if (this._searchInput) this._searchInput.removeEventListener('input', this._onSearchInput);
|
|
1064
|
-
if (this.tbody) {
|
|
1065
|
-
this.tbody.removeEventListener('click', this._onRowClick);
|
|
1066
|
-
this.tbody.removeEventListener('click', this._onRowAction);
|
|
1067
|
-
}
|
|
1068
|
-
if (this._onSelectionChange && this.tbody) this.tbody.removeEventListener('change', this._onSelectionChange);
|
|
1069
|
-
if (this._selectAllCheckbox && this._onSelectAll) this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
|
|
1070
|
-
this.dom.removeEventListener('click', this._onClearAll);
|
|
1071
|
-
|
|
1072
|
-
this._closeFilterDropdown();
|
|
1073
|
-
this._disableVirtualScroll();
|
|
1074
|
-
this._data = [];
|
|
1075
|
-
delete this.dom[DOM_ATTRIBUTE];
|
|
1076
|
-
};
|
|
1077
|
-
|
|
1078
|
-
// ─── Attribute Sync ────────────────────────────────────────
|
|
1079
|
-
|
|
1080
|
-
function _syncAttribute(el, attrName) {
|
|
1081
|
-
const instance = el[DOM_ATTRIBUTE];
|
|
1082
|
-
if (!instance) return;
|
|
1083
|
-
|
|
1084
|
-
if (attrName === 'data-ln-data-table-selectable') {
|
|
1085
|
-
const hasSelectable = el.hasAttribute('data-ln-data-table-selectable');
|
|
1086
|
-
if (hasSelectable !== instance._selectable) {
|
|
1087
|
-
instance._selectable = hasSelectable;
|
|
1088
|
-
if (hasSelectable) {
|
|
1089
|
-
instance._enableSelection();
|
|
1090
|
-
} else {
|
|
1091
|
-
instance._disableSelection();
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// ─── Init ──────────────────────────────────────────────────
|
|
1098
|
-
|
|
1099
|
-
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-table', {
|
|
1100
|
-
extraAttributes: ['data-ln-data-table-selectable'],
|
|
1101
|
-
onAttributeChange: _syncAttribute
|
|
1102
|
-
});
|
|
1103
|
-
})();
|