@livenetworks/ashlar 1.5.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 +92 -2
- 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 -56
- 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 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- 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 +11 -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 +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- 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 +85 -60
- 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 +27 -9
- package/scss/ln-ashlar.scss +7 -1
- 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 -1087
- 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 -156
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// ─── Hash codec (URL fragment as namespaced state) ─────────
|
|
2
|
+
//
|
|
3
|
+
// Grammar: #nsA:valA&nsB:valB
|
|
4
|
+
// Each component owns ONE namespace, ignores all others, and PRESERVES
|
|
5
|
+
// foreign segments on write — this is what makes component isolation work.
|
|
6
|
+
//
|
|
7
|
+
// Three-state value model:
|
|
8
|
+
// absent → hashGet returns null (e.g. closed)
|
|
9
|
+
// #ns → hashGet returns '' (present, no value — e.g. "new")
|
|
10
|
+
// #ns:val → hashGet returns 'val' (present, with param)
|
|
11
|
+
//
|
|
12
|
+
// Write mechanism: `location.hash =` (fires hashchange; identical value is a
|
|
13
|
+
// no-op so there are no loops). Never pushState here.
|
|
14
|
+
|
|
15
|
+
function _strip(str) {
|
|
16
|
+
return (str || '').replace(/^#/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hashParse(str) {
|
|
20
|
+
const raw = (str === undefined) ? location.hash : str;
|
|
21
|
+
const map = {};
|
|
22
|
+
const h = _strip(raw);
|
|
23
|
+
if (!h) return map;
|
|
24
|
+
const parts = h.split('&');
|
|
25
|
+
for (let i = 0; i < parts.length; i++) {
|
|
26
|
+
const part = parts[i];
|
|
27
|
+
if (!part) continue;
|
|
28
|
+
const sep = part.indexOf(':');
|
|
29
|
+
const ns = (sep > -1 ? part.slice(0, sep) : part);
|
|
30
|
+
const val = (sep > -1 ? part.slice(sep + 1) : '');
|
|
31
|
+
if (!ns) continue;
|
|
32
|
+
try { map[ns] = decodeURIComponent(val); }
|
|
33
|
+
catch (e) { map[ns] = val; }
|
|
34
|
+
}
|
|
35
|
+
return map;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hashGet(ns) {
|
|
39
|
+
if (!ns) return null;
|
|
40
|
+
const map = hashParse();
|
|
41
|
+
return (ns in map) ? map[ns] : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hashSet(ns, value) {
|
|
45
|
+
if (!ns) return;
|
|
46
|
+
const map = hashParse();
|
|
47
|
+
if (value === null || value === undefined) {
|
|
48
|
+
delete map[ns];
|
|
49
|
+
} else {
|
|
50
|
+
map[ns] = String(value);
|
|
51
|
+
}
|
|
52
|
+
const keys = Object.keys(map);
|
|
53
|
+
const next = keys.map(function (k) {
|
|
54
|
+
const v = map[k];
|
|
55
|
+
return (v === '') ? k : (k + ':' + encodeURIComponent(v));
|
|
56
|
+
}).join('&');
|
|
57
|
+
// Setting an identical hash is a no-op (no hashchange) → no loops.
|
|
58
|
+
if (_strip(location.hash) === next) return;
|
|
59
|
+
location.hash = next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Hash-link click guard (shared by ln-tabs & ln-modal) ──
|
|
63
|
+
//
|
|
64
|
+
// A "hash link" is an <a href="#…"> whose click a component wants to drive
|
|
65
|
+
// through the codec instead of native fragment navigation (native nav replaces
|
|
66
|
+
// the WHOLE fragment and wipes foreign segments). Both ln-tabs and ln-modal
|
|
67
|
+
// funnel their own anchor clicks through here so the guard + preventDefault
|
|
68
|
+
// live in ONE place.
|
|
69
|
+
//
|
|
70
|
+
// Returns:
|
|
71
|
+
// false → modifier / middle / shift-click: caller must return and let the
|
|
72
|
+
// browser do native open-in-new-tab / new-window. No preventDefault.
|
|
73
|
+
// true → ordinary click: preventDefault() has been called; caller proceeds
|
|
74
|
+
// with its OWN write (hashSet merge, or a component-specific branch
|
|
75
|
+
// such as ln-tabs' already-current setAttribute).
|
|
76
|
+
//
|
|
77
|
+
// Deliberately does NOT call hashSet — ln-tabs needs an "already-current value
|
|
78
|
+
// → setAttribute active directly (no hash write)" branch, so the write differs
|
|
79
|
+
// per component. The shared, reusable surface is the guard + preventDefault.
|
|
80
|
+
export function hashLinkClick(e) {
|
|
81
|
+
if (e.button === 1 || e.ctrlKey || e.metaKey || e.shiftKey) return false;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof window !== 'undefined') {
|
|
87
|
+
window.lnCore = window.lnCore || {};
|
|
88
|
+
window.lnCore.hashParse = hashParse;
|
|
89
|
+
window.lnCore.hashGet = hashGet;
|
|
90
|
+
window.lnCore.hashSet = hashSet;
|
|
91
|
+
window.lnCore.hashLinkClick = hashLinkClick;
|
|
92
|
+
}
|
package/js/ln-core/helpers.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
// ─── Global Console Warning Interceptor (Production Mode) ──
|
|
2
|
+
if (typeof window !== 'undefined') {
|
|
3
|
+
const originalWarn = console.warn;
|
|
4
|
+
console.warn = function (...args) {
|
|
5
|
+
const isLibraryWarning = typeof args[0] === 'string' &&
|
|
6
|
+
(args[0].startsWith('[ln-') || args[0].startsWith('[lnCore'));
|
|
7
|
+
|
|
8
|
+
if (isLibraryWarning) {
|
|
9
|
+
const isDebug =
|
|
10
|
+
document.documentElement.hasAttribute('data-ln-debug') ||
|
|
11
|
+
(document.body && document.body.hasAttribute('data-ln-debug'));
|
|
12
|
+
|
|
13
|
+
if (!isDebug) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
originalWarn.apply(console, args);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
1
21
|
// ─── Template Cache ────────────────────────────────────────
|
|
2
22
|
const _tmplCache = {};
|
|
3
23
|
|
|
@@ -36,6 +56,40 @@ export function dispatchCancelable(element, eventName, detail) {
|
|
|
36
56
|
return event;
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Re-filter / re-sort / re-render a data-driven list-like component, then
|
|
61
|
+
* notify the coordinator that fresh data is requested.
|
|
62
|
+
*
|
|
63
|
+
* Shared by ln-list and ln-table. The passed `component` MUST expose this
|
|
64
|
+
* instance contract:
|
|
65
|
+
* - _applyFilterAndSort() method
|
|
66
|
+
* - _render() method
|
|
67
|
+
* - _updateFooter() method
|
|
68
|
+
* - _vStart, _vEnd number (reset to -1 to force a full re-render)
|
|
69
|
+
* - dom the component root element (dispatch target)
|
|
70
|
+
* - name the component's name (becomes detail[keyName])
|
|
71
|
+
* - currentSort, currentFilters, currentSearch
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} component component instance satisfying the contract above
|
|
74
|
+
* @param {string} eventName e.g. 'ln-table:request-data' / 'ln-list:request-data'
|
|
75
|
+
* @param {string} keyName payload key for the name, e.g. 'table' / 'list'
|
|
76
|
+
*/
|
|
77
|
+
export function requestData(component, eventName, keyName) {
|
|
78
|
+
component._applyFilterAndSort();
|
|
79
|
+
component._vStart = -1;
|
|
80
|
+
component._vEnd = -1;
|
|
81
|
+
component._render();
|
|
82
|
+
component._updateFooter();
|
|
83
|
+
|
|
84
|
+
const detail = {
|
|
85
|
+
sort: component.currentSort,
|
|
86
|
+
filters: component.currentFilters,
|
|
87
|
+
search: component.currentSearch
|
|
88
|
+
};
|
|
89
|
+
detail[keyName] = component.name;
|
|
90
|
+
dispatch(component.dom, eventName, detail);
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
// ─── Declarative DOM Binding ───────────────────────────────
|
|
40
94
|
|
|
41
95
|
export function fill(root, data) {
|
|
@@ -96,10 +150,54 @@ export function fill(root, data) {
|
|
|
96
150
|
return root;
|
|
97
151
|
}
|
|
98
152
|
|
|
153
|
+
// ─── Event-driven fill fan-out ─────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Dispatch `ln-fill` at every [data-ln-form] and [data-ln-fillable]
|
|
157
|
+
* descendant of `container`. Fillables self-handle; null → clear/reset.
|
|
158
|
+
*/
|
|
159
|
+
export function lnFill(container, record) {
|
|
160
|
+
// Dispatch at container itself when it is a form or fillable.
|
|
161
|
+
// Enables lnFill(formEl, record) to reach ln-form's listener, which
|
|
162
|
+
// guards e.target === self.dom. Modal callers (lnFill(modalEl, …)
|
|
163
|
+
// where modalEl is [data-ln-modal]) do not match, so they are unaffected.
|
|
164
|
+
if (container.matches && container.matches('[data-ln-form], [data-ln-fillable]')) {
|
|
165
|
+
container.dispatchEvent(new CustomEvent('ln-fill', { detail: record ?? null, bubbles: true }));
|
|
166
|
+
}
|
|
167
|
+
const targets = container.querySelectorAll('[data-ln-form], [data-ln-fillable]');
|
|
168
|
+
for (let i = 0; i < targets.length; i++) {
|
|
169
|
+
targets[i].dispatchEvent(new CustomEvent('ln-fill', { detail: record ?? null, bubbles: true }));
|
|
170
|
+
}
|
|
171
|
+
return container;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Delegated display-fillable handler — installed once per page (idempotent
|
|
175
|
+
// across standalone component bundles that each inline ln-core helpers).
|
|
176
|
+
// Forms are NOT [data-ln-fillable]; ln-form's own listener handles them.
|
|
177
|
+
if (typeof window !== 'undefined') {
|
|
178
|
+
window.lnCore = window.lnCore || {};
|
|
179
|
+
if (!window.lnCore._fillBound) {
|
|
180
|
+
window.lnCore._fillBound = true;
|
|
181
|
+
document.addEventListener('ln-fill', function (e) {
|
|
182
|
+
if (!e.target.matches || !e.target.matches('[data-ln-fillable]')) return;
|
|
183
|
+
if (e.detail) {
|
|
184
|
+
fill(e.target, e.detail);
|
|
185
|
+
} else {
|
|
186
|
+
const fields = e.target.querySelectorAll('[data-ln-field]');
|
|
187
|
+
for (let i = 0; i < fields.length; i++) {
|
|
188
|
+
fields[i].textContent = '';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
99
195
|
// ─── Template Text-Node Placeholders ──────────────────────
|
|
100
196
|
|
|
101
197
|
export function fillTemplate(clone, data) {
|
|
102
198
|
if (!clone || !data) return clone;
|
|
199
|
+
|
|
200
|
+
// ── Pass 1: text nodes ──────────────────────────────────
|
|
103
201
|
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
|
|
104
202
|
while (walker.nextNode()) {
|
|
105
203
|
const node = walker.currentNode;
|
|
@@ -110,6 +208,27 @@ export function fillTemplate(clone, data) {
|
|
|
110
208
|
);
|
|
111
209
|
}
|
|
112
210
|
}
|
|
211
|
+
|
|
212
|
+
// ── Pass 2: element attributes ──────────────────────────
|
|
213
|
+
// Uses setAttribute — never innerHTML — so injection risk is the same
|
|
214
|
+
// as data-ln-attr (the existing declarative attribute-fill mechanism).
|
|
215
|
+
// Complements data-ln-table-cell-attr (field→attr stamping); both coexist.
|
|
216
|
+
const _replaceTokens = function (_, key) { return data[key] !== undefined ? data[key] : ''; };
|
|
217
|
+
const elements = Array.from(clone.querySelectorAll('*'));
|
|
218
|
+
// Include the root itself when clone is an Element (e.g. a <tr> passed
|
|
219
|
+
// directly by ln-table._fillRow) — querySelectorAll does not include root.
|
|
220
|
+
if (clone.nodeType === 1) elements.push(clone);
|
|
221
|
+
for (let i = 0; i < elements.length; i++) {
|
|
222
|
+
const el = elements[i];
|
|
223
|
+
const attrs = el.attributes;
|
|
224
|
+
for (let j = 0; j < attrs.length; j++) {
|
|
225
|
+
const attr = attrs[j];
|
|
226
|
+
if (attr.value.indexOf('{{') !== -1) {
|
|
227
|
+
el.setAttribute(attr.name, attr.value.replace(/\{\{\s*(\w+)\s*\}\}/g, _replaceTokens));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
113
232
|
return clone;
|
|
114
233
|
}
|
|
115
234
|
|
|
@@ -223,17 +342,46 @@ export function isVisible(el) {
|
|
|
223
342
|
|
|
224
343
|
// ─── Form Serialization ───────────────────────────────────
|
|
225
344
|
|
|
226
|
-
|
|
345
|
+
// Effective HTTP method for a form: hidden _method input (non-empty) wins,
|
|
346
|
+
// else the form's own `method` attribute. Shared by ln-form's submit gate
|
|
347
|
+
// and ln-data-coordinator's native-submit claim so both read the identical
|
|
348
|
+
// literal DOM state — no drift between the two independent checks.
|
|
349
|
+
export function resolveFormMethod(form) {
|
|
350
|
+
const methodInput = form.querySelector('input[name="_method"]');
|
|
351
|
+
const raw = (methodInput && methodInput.value !== '') ? methodInput.value : form.method;
|
|
352
|
+
return (raw || '').toUpperCase();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function serializeForm(form, opts) {
|
|
356
|
+
const typed = !!(opts && opts.typed);
|
|
357
|
+
const exclude = opts && opts.exclude;
|
|
227
358
|
const data = {};
|
|
228
359
|
const elements = form.elements;
|
|
229
360
|
|
|
361
|
+
// Pre-count checkbox names in typed mode to distinguish single-checkbox
|
|
362
|
+
// (→ boolean) from same-name checkbox groups (→ array of checked values).
|
|
363
|
+
const checkboxCounts = {};
|
|
364
|
+
if (typed) {
|
|
365
|
+
for (let i = 0; i < elements.length; i++) {
|
|
366
|
+
const el = elements[i];
|
|
367
|
+
if (el.name && el.type === 'checkbox' && !el.disabled) {
|
|
368
|
+
checkboxCounts[el.name] = (checkboxCounts[el.name] || 0) + 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
230
373
|
for (let i = 0; i < elements.length; i++) {
|
|
231
374
|
const el = elements[i];
|
|
232
375
|
if (!el.name || el.disabled || el.type === 'file' || el.type === 'submit' || el.type === 'button') continue;
|
|
376
|
+
if (exclude && el.matches && el.matches(exclude)) continue;
|
|
233
377
|
|
|
234
378
|
if (el.type === 'checkbox') {
|
|
235
|
-
if (
|
|
236
|
-
|
|
379
|
+
if (typed && checkboxCounts[el.name] === 1) {
|
|
380
|
+
data[el.name] = el.checked; // single checkbox → boolean
|
|
381
|
+
} else {
|
|
382
|
+
if (!data[el.name]) data[el.name] = [];
|
|
383
|
+
if (el.checked) data[el.name].push(el.value); // group → array (unchanged)
|
|
384
|
+
}
|
|
237
385
|
} else if (el.type === 'radio') {
|
|
238
386
|
if (el.checked) data[el.name] = el.value;
|
|
239
387
|
} else if (el.type === 'select-multiple') {
|
|
@@ -241,6 +389,11 @@ export function serializeForm(form) {
|
|
|
241
389
|
for (let j = 0; j < el.options.length; j++) {
|
|
242
390
|
if (el.options[j].selected) data[el.name].push(el.options[j].value);
|
|
243
391
|
}
|
|
392
|
+
} else if (typed && el.type === 'hidden') {
|
|
393
|
+
data[el.name] = el.value; // hidden → always string (never coerce)
|
|
394
|
+
} else if (typed && (el.type === 'number' || el.type === 'range')) {
|
|
395
|
+
const n = Number(el.value);
|
|
396
|
+
data[el.name] = (el.value === '' || isNaN(n)) ? null : n; // number/range → Number | null
|
|
244
397
|
} else {
|
|
245
398
|
data[el.name] = el.value;
|
|
246
399
|
}
|
|
@@ -249,18 +402,57 @@ export function serializeForm(form) {
|
|
|
249
402
|
return data;
|
|
250
403
|
}
|
|
251
404
|
|
|
405
|
+
// Coerce a value to boolean for single-checkbox fill.
|
|
406
|
+
// Flat data-* values arrive as strings; "false", "0", "", "off", "no"
|
|
407
|
+
// (case-insensitive, trimmed) are treated as unchecked.
|
|
408
|
+
function _coerceBool(value) {
|
|
409
|
+
if (typeof value !== 'string') return !!value;
|
|
410
|
+
const v = value.trim().toLowerCase();
|
|
411
|
+
return v !== 'false' && v !== '0' && v !== '' && v !== 'off' && v !== 'no';
|
|
412
|
+
}
|
|
413
|
+
|
|
252
414
|
export function populateForm(form, data) {
|
|
253
415
|
const elements = form.elements;
|
|
254
416
|
const filled = [];
|
|
255
417
|
|
|
418
|
+
// Pre-count checkbox names to distinguish single-checkbox (→ bool)
|
|
419
|
+
// from same-name checkbox groups (→ comma-separated list membership).
|
|
420
|
+
// NOTE: unlike serializeForm, populateForm fills disabled elements too,
|
|
421
|
+
// so the count must include disabled members (no !el.disabled guard) —
|
|
422
|
+
// otherwise a group with a disabled box reverts to single-checkbox coercion.
|
|
423
|
+
const checkboxCounts = {};
|
|
256
424
|
for (let i = 0; i < elements.length; i++) {
|
|
257
425
|
const el = elements[i];
|
|
258
|
-
if (
|
|
426
|
+
if (el.name && el.type === 'checkbox') {
|
|
427
|
+
checkboxCounts[el.name] = (checkboxCounts[el.name] || 0) + 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
259
430
|
|
|
260
|
-
|
|
431
|
+
for (let i = 0; i < elements.length; i++) {
|
|
432
|
+
const el = elements[i];
|
|
433
|
+
if (el.type === 'file' || el.type === 'submit' || el.type === 'button') continue;
|
|
434
|
+
|
|
435
|
+
// Decoupled-key fill: data-ln-fill-as wins over name for record matching.
|
|
436
|
+
// Write (fill) path only — serializeForm stays name-based.
|
|
437
|
+
const matchKey = el.getAttribute('data-ln-fill-as') || el.name;
|
|
438
|
+
if (!matchKey || !(matchKey in data)) continue;
|
|
439
|
+
|
|
440
|
+
const value = data[matchKey];
|
|
261
441
|
|
|
262
442
|
if (el.type === 'checkbox') {
|
|
263
|
-
|
|
443
|
+
if (Array.isArray(value)) {
|
|
444
|
+
// Programmatic array path — membership check.
|
|
445
|
+
el.checked = value.indexOf(el.value) !== -1;
|
|
446
|
+
} else if (checkboxCounts[el.name] > 1) {
|
|
447
|
+
// Same-name checkbox group + scalar: treat as comma-separated list.
|
|
448
|
+
// "admin,editor" → ['admin','editor'] → membership check.
|
|
449
|
+
const list = String(value).split(',').map(function (s) { return s.trim(); });
|
|
450
|
+
el.checked = list.indexOf(el.value) !== -1;
|
|
451
|
+
} else {
|
|
452
|
+
// Single checkbox — boolean coercion.
|
|
453
|
+
// "false"/"0"/"off"/"no"/"" → unchecked; anything else → checked.
|
|
454
|
+
el.checked = _coerceBool(value);
|
|
455
|
+
}
|
|
264
456
|
filled.push(el);
|
|
265
457
|
} else if (el.type === 'radio') {
|
|
266
458
|
el.checked = el.value === String(value);
|
|
@@ -283,9 +475,83 @@ export function populateForm(form, data) {
|
|
|
283
475
|
|
|
284
476
|
// ─── Locale Detection ─────────────────────────────────────
|
|
285
477
|
|
|
478
|
+
const _standardTerritories = {
|
|
479
|
+
mk: 'mk-MK',
|
|
480
|
+
de: 'de-DE',
|
|
481
|
+
fr: 'fr-FR',
|
|
482
|
+
es: 'es-ES',
|
|
483
|
+
it: 'it-IT',
|
|
484
|
+
nl: 'nl-NL',
|
|
485
|
+
pt: 'pt-PT',
|
|
486
|
+
ru: 'ru-RU',
|
|
487
|
+
bg: 'bg-BG',
|
|
488
|
+
hr: 'hr-HR',
|
|
489
|
+
sr: 'sr-RS',
|
|
490
|
+
sq: 'sq-AL',
|
|
491
|
+
el: 'el-GR',
|
|
492
|
+
en: 'en-US'
|
|
493
|
+
};
|
|
494
|
+
|
|
286
495
|
export function getLocale(el) {
|
|
287
|
-
const langEl = el.closest('[lang]');
|
|
288
|
-
|
|
496
|
+
const langEl = el ? el.closest('[lang]') : null;
|
|
497
|
+
const raw = (langEl ? (langEl.getAttribute('lang') || langEl.lang) : null)
|
|
498
|
+
|| (document.documentElement ? (document.documentElement.getAttribute('lang') || document.documentElement.lang) : null)
|
|
499
|
+
|| navigator.language;
|
|
500
|
+
if (!raw) return 'en-US';
|
|
501
|
+
const lower = raw.trim().toLowerCase();
|
|
502
|
+
if (lower.indexOf('-') === -1 && _standardTerritories[lower]) {
|
|
503
|
+
return _standardTerritories[lower];
|
|
504
|
+
}
|
|
505
|
+
return raw;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ─── Raw Value Read ───────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Read the raw machine value behind a formatted cell/item display.
|
|
512
|
+
* Returns the `data-ln-value` attribute if present, else trimmed textContent.
|
|
513
|
+
* Single read path for value-based sort/filter across components.
|
|
514
|
+
*/
|
|
515
|
+
export function readValue(el) {
|
|
516
|
+
return el.hasAttribute('data-ln-value')
|
|
517
|
+
? el.getAttribute('data-ln-value')
|
|
518
|
+
: el.textContent.trim();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ─── Value Comparison Primitives ───────────────────────────
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Detect the comparison type for a column/field from its current value set.
|
|
525
|
+
* Type is locked ONCE per sort operation (not per pair) — comparing types
|
|
526
|
+
* per-pair breaks comparator transitivity and produces wrong ordering.
|
|
527
|
+
* All non-empty values must be finite numbers for 'number'; otherwise 'string'.
|
|
528
|
+
*/
|
|
529
|
+
export function detectValueType(values) {
|
|
530
|
+
let hasValue = false;
|
|
531
|
+
for (let i = 0; i < values.length; i++) {
|
|
532
|
+
const v = values[i];
|
|
533
|
+
if (v === '' || v == null) continue;
|
|
534
|
+
hasValue = true;
|
|
535
|
+
if (!Number.isFinite(Number(v))) return 'string';
|
|
536
|
+
}
|
|
537
|
+
return hasValue ? 'number' : 'string';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Compare two raw values given a pre-locked type (see detectValueType).
|
|
542
|
+
* Pass an Intl.Collator instance for locale-aware string comparison —
|
|
543
|
+
* built once by the caller outside the sort loop, never per pair.
|
|
544
|
+
*/
|
|
545
|
+
export function compareValues(a, b, type, collator) {
|
|
546
|
+
if (type === 'number') {
|
|
547
|
+
const na = parseFloat(a);
|
|
548
|
+
const nb = parseFloat(b);
|
|
549
|
+
return (isNaN(na) ? 0 : na) - (isNaN(nb) ? 0 : nb);
|
|
550
|
+
}
|
|
551
|
+
const strA = a != null ? String(a) : '';
|
|
552
|
+
const strB = b != null ? String(b) : '';
|
|
553
|
+
if (collator) return collator.compare(strA, strB);
|
|
554
|
+
return strA < strB ? -1 : strA > strB ? 1 : 0;
|
|
289
555
|
}
|
|
290
556
|
|
|
291
557
|
// ─── Value Property Interception ───────────────────────────
|
|
@@ -407,6 +673,30 @@ export function registerComponent(selector, attribute, ComponentFn, componentTag
|
|
|
407
673
|
|
|
408
674
|
// ─── HTTP / URL Helpers ────────────────────────────────────
|
|
409
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Check if a click event on a link should be intercepted by an in-app navigator.
|
|
678
|
+
* Intercept only left clicks, same-origin, not target="_blank", no download,
|
|
679
|
+
* not mailto/tel, and not pure hash/empty.
|
|
680
|
+
*
|
|
681
|
+
* @param {MouseEvent} event
|
|
682
|
+
* @param {HTMLAnchorElement} anchor
|
|
683
|
+
* @returns {boolean}
|
|
684
|
+
*/
|
|
685
|
+
export function shouldInterceptLink(event, anchor) {
|
|
686
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button !== 0) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (!anchor) return false;
|
|
690
|
+
const href = anchor.getAttribute('href');
|
|
691
|
+
if (!href) return false;
|
|
692
|
+
if (anchor.getAttribute('target') === '_blank') return false;
|
|
693
|
+
if (anchor.hasAttribute('download')) return false;
|
|
694
|
+
if (href.startsWith('mailto:') || href.startsWith('tel:')) return false;
|
|
695
|
+
if (href === '#' || href.startsWith('#')) return false;
|
|
696
|
+
if (anchor.hostname && anchor.hostname !== window.location.hostname) return false;
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
|
|
410
700
|
/**
|
|
411
701
|
* Build a URL from segments, stripping duplicate slashes.
|
|
412
702
|
*/
|
|
@@ -450,10 +740,49 @@ export function getDataMapper(name) {
|
|
|
450
740
|
return _dataMappers[name] || { ingress: r => r, egress: r => r };
|
|
451
741
|
}
|
|
452
742
|
|
|
743
|
+
// ─── Locale Fallback Registry ───────────────────────────────
|
|
744
|
+
const _localeFallbacks = {};
|
|
745
|
+
|
|
746
|
+
export function registerLocaleFallback(langPrefix, dictionary) {
|
|
747
|
+
if (!langPrefix || typeof dictionary !== 'object') return;
|
|
748
|
+
const prefix = langPrefix.toLowerCase().split('-')[0];
|
|
749
|
+
_localeFallbacks[prefix] = dictionary;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function getLocaleFallback(lang) {
|
|
753
|
+
if (!lang) return null;
|
|
754
|
+
const prefix = lang.toLowerCase().split('-')[0];
|
|
755
|
+
return _localeFallbacks[prefix] || null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Default Macedonian locale fallback registration
|
|
759
|
+
registerLocaleFallback('mk', {
|
|
760
|
+
monthsLong: [
|
|
761
|
+
'јануари', 'февруари', 'март', 'април', 'мај', 'јуни',
|
|
762
|
+
'јули', 'август', 'септември', 'октомври', 'ноември', 'декември'
|
|
763
|
+
],
|
|
764
|
+
monthsShort: [
|
|
765
|
+
'јан', 'фев', 'мар', 'апр', 'мај', 'јун',
|
|
766
|
+
'јул', 'авг', 'септ', 'окт', 'ноем', 'дек'
|
|
767
|
+
],
|
|
768
|
+
daysLong: [
|
|
769
|
+
'недела', 'понеделник', 'вторник', 'среда', 'четврток', 'петок', 'сабота'
|
|
770
|
+
],
|
|
771
|
+
daysShort: [
|
|
772
|
+
'нед', 'пон', 'вт', 'ср', 'чет', 'пет', 'саб'
|
|
773
|
+
]
|
|
774
|
+
});
|
|
775
|
+
|
|
453
776
|
if (typeof window !== 'undefined') {
|
|
454
777
|
window.lnCore = window.lnCore || {};
|
|
455
778
|
window.lnCore.registerDataMapper = registerDataMapper;
|
|
456
779
|
window.lnCore.getDataMapper = getDataMapper;
|
|
780
|
+
window.lnCore.registerLocaleFallback = registerLocaleFallback;
|
|
781
|
+
window.lnCore.getLocaleFallback = getLocaleFallback;
|
|
782
|
+
window.lnCore.fillTemplate = fillTemplate;
|
|
783
|
+
window.lnCore.fill = fill;
|
|
784
|
+
window.lnCore.lnFill = lnFill;
|
|
785
|
+
window.lnCore.renderList = renderList;
|
|
457
786
|
}
|
|
458
787
|
|
|
459
788
|
|
package/js/ln-core/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export { cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, isVisible, serializeForm, populateForm, getLocale, registerComponent, buildUrl, getHeaders, parseHeaders, registerDataMapper, getDataMapper, interceptValueProperty } from './helpers.js';
|
|
1
|
+
export { cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, requestData, fill, lnFill, fillTemplate, renderList, buildDict, guardBody, findElements, isVisible, serializeForm, populateForm, resolveFormMethod, getLocale, readValue, detectValueType, compareValues, registerComponent, shouldInterceptLink, buildUrl, getHeaders, parseHeaders, registerDataMapper, getDataMapper, interceptValueProperty, registerLocaleFallback, getLocaleFallback } from './helpers.js';
|
|
2
2
|
export { reactiveState, deepReactive, createBatcher } from './reactive.js';
|
|
3
|
+
export { createWindowCache } from './window-cache.js';
|
|
3
4
|
export { persistGet, persistSet, persistRemove, persistClear } from './persist.js';
|
|
4
|
-
export {
|
|
5
|
+
export { hashParse, hashGet, hashSet, hashLinkClick } from './hash.js';
|
|
6
|
+
export { computePlacement, measureHidden } from './positioning.js';
|
|
5
7
|
export { setCryptoKey, getCryptoKey, encryptData, decryptData } from './crypto.js';
|
package/js/ln-core/persist.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Key format: ln:{component}:{pathname}:{elementId}
|
|
4
4
|
// Example: ln:filter:/admin/users:status-filter
|
|
5
5
|
//
|
|
6
|
-
// Each component passes its own name ("filter", "
|
|
6
|
+
// Each component passes its own name ("filter", "sort", "toggle", "tabs"),
|
|
7
7
|
// so keys are namespaced per component — no cross-component collision risk.
|
|
8
8
|
// Element ID comes from data-ln-persist="key" or el.id.
|
|
9
9
|
//
|
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
|
|
13
13
|
const PREFIX = 'ln:';
|
|
14
14
|
|
|
15
|
+
let _isStorageAvailable = null;
|
|
16
|
+
function _checkStorage() {
|
|
17
|
+
if (_isStorageAvailable !== null) return _isStorageAvailable;
|
|
18
|
+
try {
|
|
19
|
+
if (typeof localStorage === 'undefined') {
|
|
20
|
+
_isStorageAvailable = false;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const testKey = '__ln_test__';
|
|
24
|
+
localStorage.setItem(testKey, testKey);
|
|
25
|
+
localStorage.removeItem(testKey);
|
|
26
|
+
_isStorageAvailable = true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
_isStorageAvailable = false;
|
|
29
|
+
}
|
|
30
|
+
return _isStorageAvailable;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
function _pageKey() {
|
|
16
34
|
const path = location.pathname.replace(/\/+$/, '').toLowerCase();
|
|
17
35
|
return path || '/';
|
|
@@ -28,6 +46,7 @@ function _resolveKey(component, el) {
|
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
export function persistGet(component, el) {
|
|
49
|
+
if (!_checkStorage()) return null;
|
|
31
50
|
const key = _resolveKey(component, el);
|
|
32
51
|
if (!key) return null;
|
|
33
52
|
try {
|
|
@@ -39,16 +58,22 @@ export function persistGet(component, el) {
|
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export function persistSet(component, el, value) {
|
|
61
|
+
if (!_checkStorage()) return;
|
|
42
62
|
const key = _resolveKey(component, el);
|
|
43
63
|
if (!key) return;
|
|
44
64
|
try {
|
|
45
|
-
|
|
65
|
+
if (value === null || value === undefined) {
|
|
66
|
+
localStorage.removeItem(key);
|
|
67
|
+
} else {
|
|
68
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
69
|
+
}
|
|
46
70
|
} catch (e) {
|
|
47
|
-
// localStorage full
|
|
71
|
+
// localStorage full — silent
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
export function persistRemove(component, el) {
|
|
76
|
+
if (!_checkStorage()) return;
|
|
52
77
|
const key = _resolveKey(component, el);
|
|
53
78
|
if (!key) return;
|
|
54
79
|
try {
|
|
@@ -57,6 +82,7 @@ export function persistRemove(component, el) {
|
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
export function persistClear(component) {
|
|
85
|
+
if (!_checkStorage()) return;
|
|
60
86
|
try {
|
|
61
87
|
const prefix = PREFIX + component + ':';
|
|
62
88
|
const toRemove = [];
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Pure helpers for floating UI (popovers, tooltips, dropdowns).
|
|
4
4
|
// No DOM side effects in computePlacement — it takes a rect and
|
|
5
|
-
// returns coordinates.
|
|
6
|
-
// because they have to.
|
|
5
|
+
// returns coordinates. measureHidden touches DOM because it has to.
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Compute viewport coordinates for a floating element relative to an
|
|
@@ -138,39 +137,6 @@ export function computePlacement(anchorRect, floatingSize, preferred, offset) {
|
|
|
138
137
|
return { top: top, left: left, placement: chosen.side };
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
/**
|
|
142
|
-
* Move an element into <body>, leaving a placeholder comment so
|
|
143
|
-
* teleportBack can put it back exactly where it was. Returns a
|
|
144
|
-
* cleanup function — call it to restore the element to its origin.
|
|
145
|
-
*
|
|
146
|
-
* The element receives `position: fixed` style automatically (the
|
|
147
|
-
* caller is responsible for top/left). To avoid violating the
|
|
148
|
-
* "no inline styles" rule, the caller should set position via a
|
|
149
|
-
* CSS rule on its component selector (e.g. [data-ln-popover]
|
|
150
|
-
* already has position: fixed in its co-located scss). teleportToBody
|
|
151
|
-
* itself does NOT set any inline styles.
|
|
152
|
-
*
|
|
153
|
-
* @param {HTMLElement} el
|
|
154
|
-
* @returns {Function} cleanup — restores el to its original parent.
|
|
155
|
-
*/
|
|
156
|
-
export function teleportToBody(el) {
|
|
157
|
-
if (!el || el.parentNode === document.body) {
|
|
158
|
-
// Already in body or no element — return a no-op cleanup.
|
|
159
|
-
return function () {};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const originalParent = el.parentNode;
|
|
163
|
-
const placeholder = document.createComment('ln-teleport');
|
|
164
|
-
originalParent.insertBefore(placeholder, el);
|
|
165
|
-
document.body.appendChild(el);
|
|
166
|
-
|
|
167
|
-
return function restore() {
|
|
168
|
-
if (!placeholder.parentNode) return;
|
|
169
|
-
placeholder.parentNode.insertBefore(el, placeholder);
|
|
170
|
-
placeholder.parentNode.removeChild(placeholder);
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
140
|
/**
|
|
175
141
|
* Measure an element that may be hidden via display:none.
|
|
176
142
|
* Temporarily applies visibility:hidden + display:block so we can
|