@livenetworks/ashlar 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- package/scss/utilities/_utilities.scss +49 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { guardBody } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-nav';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnNav';
|
|
6
|
+
|
|
7
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
|
+
|
|
9
|
+
const navInstances = new WeakMap();
|
|
10
|
+
|
|
11
|
+
// ─── pushState singleton patch ──────────────────────────────
|
|
12
|
+
const _pushStateCallbacks = [];
|
|
13
|
+
|
|
14
|
+
if (!history._lnNavPatched) {
|
|
15
|
+
const _origPushState = history.pushState;
|
|
16
|
+
history.pushState = function () {
|
|
17
|
+
_origPushState.apply(history, arguments);
|
|
18
|
+
for (const cb of _pushStateCallbacks) { cb(); }
|
|
19
|
+
};
|
|
20
|
+
history._lnNavPatched = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Constructor ───────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function constructor(navElement) {
|
|
26
|
+
if (!navElement.hasAttribute(DOM_SELECTOR)) return;
|
|
27
|
+
if (navInstances.has(navElement)) return;
|
|
28
|
+
|
|
29
|
+
const activeClass = navElement.getAttribute(DOM_SELECTOR);
|
|
30
|
+
if (!activeClass) return;
|
|
31
|
+
|
|
32
|
+
const instance = _initializeNav(navElement, activeClass);
|
|
33
|
+
navInstances.set(navElement, instance);
|
|
34
|
+
navElement[DOM_ATTRIBUTE] = instance;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _initializeNav(navElement, activeClass) {
|
|
38
|
+
let links = Array.from(navElement.querySelectorAll('a'));
|
|
39
|
+
|
|
40
|
+
_updateActiveState(links, activeClass, window.location.pathname);
|
|
41
|
+
|
|
42
|
+
const updateHandler = function () {
|
|
43
|
+
links = Array.from(navElement.querySelectorAll('a'));
|
|
44
|
+
_updateActiveState(links, activeClass, window.location.pathname);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('popstate', updateHandler);
|
|
48
|
+
_pushStateCallbacks.push(updateHandler);
|
|
49
|
+
|
|
50
|
+
const observer = new MutationObserver(function (mutations) {
|
|
51
|
+
for (const mutation of mutations) {
|
|
52
|
+
if (mutation.type === 'childList') {
|
|
53
|
+
for (const node of mutation.addedNodes) {
|
|
54
|
+
if (node.nodeType === 1) {
|
|
55
|
+
if (node.tagName === 'A') {
|
|
56
|
+
links.push(node);
|
|
57
|
+
_updateActiveState([node], activeClass, window.location.pathname);
|
|
58
|
+
} else if (node.querySelectorAll) {
|
|
59
|
+
const newLinks = Array.from(node.querySelectorAll('a'));
|
|
60
|
+
links = links.concat(newLinks);
|
|
61
|
+
_updateActiveState(newLinks, activeClass, window.location.pathname);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const node of mutation.removedNodes) {
|
|
67
|
+
if (node.nodeType === 1) {
|
|
68
|
+
if (node.tagName === 'A') {
|
|
69
|
+
links = links.filter(function (link) { return link !== node; });
|
|
70
|
+
} else if (node.querySelectorAll) {
|
|
71
|
+
const removedLinks = Array.from(node.querySelectorAll('a'));
|
|
72
|
+
links = links.filter(function (link) {
|
|
73
|
+
return !removedLinks.includes(link);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
observer.observe(navElement, { childList: true, subtree: true });
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
navElement: navElement,
|
|
86
|
+
activeClass: activeClass,
|
|
87
|
+
observer: observer,
|
|
88
|
+
updateHandler: updateHandler,
|
|
89
|
+
destroy: function () {
|
|
90
|
+
observer.disconnect();
|
|
91
|
+
window.removeEventListener('popstate', updateHandler);
|
|
92
|
+
const idx = _pushStateCallbacks.indexOf(updateHandler);
|
|
93
|
+
if (idx !== -1) _pushStateCallbacks.splice(idx, 1);
|
|
94
|
+
navInstances.delete(navElement);
|
|
95
|
+
delete navElement[DOM_ATTRIBUTE];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _normalizeUrl(url) {
|
|
101
|
+
try {
|
|
102
|
+
const urlObj = new URL(url, window.location.href);
|
|
103
|
+
return urlObj.pathname.replace(/\/$/, '') || '/';
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return url.replace(/\/$/, '') || '/';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _updateActiveState(links, activeClass, currentPath) {
|
|
110
|
+
const normalizedCurrent = _normalizeUrl(currentPath);
|
|
111
|
+
|
|
112
|
+
for (const link of links) {
|
|
113
|
+
const href = link.getAttribute('href');
|
|
114
|
+
if (!href) continue;
|
|
115
|
+
|
|
116
|
+
const normalizedHref = _normalizeUrl(href);
|
|
117
|
+
|
|
118
|
+
link.classList.remove(activeClass);
|
|
119
|
+
|
|
120
|
+
const isExact = normalizedHref === normalizedCurrent;
|
|
121
|
+
const isParent = normalizedHref !== '/' && normalizedCurrent.startsWith(normalizedHref + '/');
|
|
122
|
+
|
|
123
|
+
if (isExact || isParent) {
|
|
124
|
+
link.classList.add(activeClass);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Global DOM Observer ───────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function _domObserver() {
|
|
132
|
+
guardBody(function () {
|
|
133
|
+
const observer = new MutationObserver(function (mutations) {
|
|
134
|
+
for (const mutation of mutations) {
|
|
135
|
+
if (mutation.type === 'childList') {
|
|
136
|
+
for (const node of mutation.addedNodes) {
|
|
137
|
+
if (node.nodeType === 1) {
|
|
138
|
+
if (node.hasAttribute && node.hasAttribute(DOM_SELECTOR)) {
|
|
139
|
+
constructor(node);
|
|
140
|
+
}
|
|
141
|
+
if (node.querySelectorAll) {
|
|
142
|
+
for (const el of node.querySelectorAll('[' + DOM_SELECTOR + ']')) {
|
|
143
|
+
constructor(el);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if (mutation.type === 'attributes') {
|
|
149
|
+
if (mutation.target.hasAttribute && mutation.target.hasAttribute(DOM_SELECTOR)) {
|
|
150
|
+
constructor(mutation.target);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [DOM_SELECTOR] });
|
|
157
|
+
}, 'ln-nav');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
window[DOM_ATTRIBUTE] = constructor;
|
|
163
|
+
|
|
164
|
+
function _initializeAll() {
|
|
165
|
+
for (const el of document.querySelectorAll('[' + DOM_SELECTOR + ']')) {
|
|
166
|
+
constructor(el);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_domObserver();
|
|
171
|
+
|
|
172
|
+
if (document.readyState === 'loading') {
|
|
173
|
+
document.addEventListener('DOMContentLoaded', _initializeAll);
|
|
174
|
+
} else {
|
|
175
|
+
_initializeAll();
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# ln-number
|
|
2
|
+
|
|
3
|
+
Real-time locale-aware number formatting for input fields.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<input type="number" name="amount" data-ln-number>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The component creates a hidden input that holds the raw numeric value for form submission and formats the visible input with locale-aware thousand separators. See [`docs/js/number.md`](../../docs/js/number.md#html) for the resulting DOM.
|
|
12
|
+
|
|
13
|
+
## Loading & Source Files
|
|
14
|
+
|
|
15
|
+
### Loading the Component
|
|
16
|
+
|
|
17
|
+
#### 1. In-Bundle (Standard Integration)
|
|
18
|
+
To load `ln-number` as part of the main `ln-ashlar` bundle, include the compiled bundle script:
|
|
19
|
+
```html
|
|
20
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
#### 2. Standalone (Zero-Dependency IIFE)
|
|
24
|
+
To load `ln-number` as a standalone component, include its compiled IIFE under the component directory:
|
|
25
|
+
```html
|
|
26
|
+
<script src="js/ln-number/ln-number.js" defer></script>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Source Files
|
|
30
|
+
|
|
31
|
+
* **Active Development Source**: [js/ln-number/src/ln-number.js](file:///c:/laragon/www/ln-ashlar/js/ln-number/src/ln-number.js) (source of truth)
|
|
32
|
+
* **Compiled Standalone**: [js/ln-number/ln-number.js](file:///c:/laragon/www/ln-ashlar/js/ln-number/ln-number.js)
|
|
33
|
+
|
|
34
|
+
## Attributes
|
|
35
|
+
|
|
36
|
+
| Attribute | On | Description |
|
|
37
|
+
|-----------|-----|-------------|
|
|
38
|
+
| `data-ln-number` | `<input>` | Enables number formatting |
|
|
39
|
+
| `data-ln-number-decimals` | `<input>` | Max decimal places (default: unlimited) |
|
|
40
|
+
| `data-ln-number-min` | `<input>` | Minimum allowed value |
|
|
41
|
+
| `data-ln-number-max` | `<input>` | Maximum allowed value |
|
|
42
|
+
|
|
43
|
+
## Events
|
|
44
|
+
|
|
45
|
+
| Event | Bubbles | Cancelable | Detail |
|
|
46
|
+
|-------|---------|------------|--------|
|
|
47
|
+
| `ln-number:input` | yes | no | `{ value: Number, formatted: String }` |
|
|
48
|
+
| `ln-number:destroyed` | yes | no | `{ target: Element }` |
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
const el = document.querySelector('[data-ln-number]');
|
|
54
|
+
|
|
55
|
+
el.lnNumber.value; // get raw number (Number or NaN if empty)
|
|
56
|
+
el.lnNumber.value = 1234.56; // set value programmatically — formats display
|
|
57
|
+
el.lnNumber.formatted; // get formatted display string
|
|
58
|
+
|
|
59
|
+
el.lnNumber.destroy(); // remove component, restore original input
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Locale
|
|
63
|
+
|
|
64
|
+
The component reads the nearest ancestor `[lang]` attribute (typically `<html lang>`); falls back to `navigator.language`. Locale changes propagate live — re-formatting all instances when `<html lang>` changes.
|
|
65
|
+
|
|
66
|
+
| `lang` | Display |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `mk` | `1.234.567` |
|
|
69
|
+
| `en-US` | `1,234,567` |
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- Basic -->
|
|
75
|
+
<div class="form-element">
|
|
76
|
+
<label for="amount">Amount</label>
|
|
77
|
+
<input type="number" id="amount" name="amount" data-ln-number>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- With decimal limit -->
|
|
81
|
+
<div class="form-element">
|
|
82
|
+
<label for="price">Price</label>
|
|
83
|
+
<input type="number" id="price" name="price"
|
|
84
|
+
data-ln-number data-ln-number-decimals="2">
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- With min/max -->
|
|
88
|
+
<div class="form-element">
|
|
89
|
+
<label for="quantity">Quantity</label>
|
|
90
|
+
<input type="number" id="quantity" name="quantity"
|
|
91
|
+
data-ln-number data-ln-number-min="0" data-ln-number-max="999999">
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Pre-filled value -->
|
|
95
|
+
<div class="form-element">
|
|
96
|
+
<label for="budget">Budget</label>
|
|
97
|
+
<input type="number" id="budget" name="budget" value="1500000"
|
|
98
|
+
data-ln-number>
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Integration with ln-validate
|
|
103
|
+
|
|
104
|
+
Place `data-ln-validate` on the same input. The `required` attribute stays
|
|
105
|
+
on the visible input and works as expected:
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<div class="form-element">
|
|
109
|
+
<label for="salary">Salary</label>
|
|
110
|
+
<input type="number" id="salary" name="salary"
|
|
111
|
+
required data-ln-validate data-ln-number>
|
|
112
|
+
<ul data-ln-validate-errors>
|
|
113
|
+
<li class="hidden" data-ln-validate-error="required">Required field</li>
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Integration with ln-form
|
|
119
|
+
|
|
120
|
+
Works automatically. `serializeForm()` reads the hidden input (which has the
|
|
121
|
+
`name`). `populateForm()` sets the hidden input's value, which triggers the
|
|
122
|
+
display update.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function N(a,i,u){a.dispatchEvent(new CustomEvent(i,{bubbles:!0,detail:u||{}}))}function x(a,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){x(a,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}function S(a,i,u,g){if(a.nodeType!==1)return;const v=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",f=Array.from(a.querySelectorAll(v));a.matches&&a.matches(v)&&f.push(a);for(const m of f)m[u]||(m[u]=new g(m))}function w(a){const i=a.closest("[lang]");return(i?i.lang:null)||navigator.language}function O(a,i,u,g,b={}){const v=b.extraAttributes||[],f=b.onAttributeChange||null,m=b.onInit||null;function t(o){const e=o||document.body;S(e,a,i,u),m&&m(e)}return x(function(){const o=new MutationObserver(function(l){for(let r=0;r<l.length;r++){const n=l[r];if(n.type==="childList")for(let d=0;d<n.addedNodes.length;d++){const p=n.addedNodes[d];p.nodeType===1&&(S(p,a,i,u),m&&m(p))}else n.type==="attributes"&&(f&&n.target[i]?f(n.target,n.attributeName):(S(n.target,a,i,u),m&&m(n.target)))}});let e=[];if(a.indexOf("[")!==-1){const l=/\[([\w-]+)/g;let r;for(;(r=l.exec(a))!==null;)e.push(r[1])}else e.push(a);o.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:e.concat(v)})},g),window[i]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}const A={};function I(a,i){A[a]=i}function C(a){return A[a]||{ingress:i=>i,egress:i=>i}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=I,window.lnCore.getDataMapper=C),(function(){const a="data-ln-number",i="lnNumber";if(window[i]!==void 0)return;const u={},g=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");function b(t){if(!u[t]){const o=new Intl.NumberFormat(t,{useGrouping:!0}),e=o.formatToParts(1234.5);let l="",r=".";for(let n=0;n<e.length;n++)e[n].type==="group"&&(l=e[n].value),e[n].type==="decimal"&&(r=e[n].value);u[t]={fmt:o,groupSep:l,decimalSep:r}}return u[t]}function v(t,o,e){if(e!==null){const l=parseInt(e,10),r=t+"|d"+l;return u[r]||(u[r]=new Intl.NumberFormat(t,{useGrouping:!0,minimumFractionDigits:0,maximumFractionDigits:l})),u[r].format(o)}return b(t).fmt.format(o)}function f(t){if(t.tagName!=="INPUT")return console.warn("[ln-number] Can only be applied to <input>, got:",t.tagName),this;this.dom=t;const o=document.createElement("input");o.type="hidden",o.name=t.name,t.removeAttribute("name"),t.type="text",t.setAttribute("inputmode","decimal"),t.insertAdjacentElement("afterend",o),this._hidden=o;const e=this;Object.defineProperty(o,"value",{get:function(){return g.get.call(o)},set:function(r){g.set.call(o,r),r!==""&&!isNaN(parseFloat(r))?e._displayFormatted(parseFloat(r)):r===""&&(e.dom.value="")}}),this._onInput=function(){e._handleInput()},t.addEventListener("input",this._onInput),this._onPaste=function(r){r.preventDefault();const n=(r.clipboardData||window.clipboardData).getData("text"),d=b(w(t)),p=d.decimalSep.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let c=n.replace(new RegExp("[^0-9\\-"+p+".]","g"),"");d.groupSep&&(c=c.split(d.groupSep).join("")),d.decimalSep!=="."&&(c=c.replace(d.decimalSep,"."));const _=parseFloat(c);isNaN(_)?(t.value="",e._hidden.value=""):e.value=_},t.addEventListener("paste",this._onPaste);const l=t.value;if(l!==""){const r=parseFloat(l);isNaN(r)||(this._displayFormatted(r),g.set.call(o,String(r)))}return this}f.prototype._handleInput=function(){const t=this.dom,o=b(w(t)),e=t.value;if(e===""){this._hidden.value="",N(t,"ln-number:input",{value:NaN,formatted:""});return}if(e==="-"){this._hidden.value="";return}const l=t.selectionStart;let r=0;for(let s=0;s<l;s++)/[0-9]/.test(e[s])&&r++;let n=e;if(o.groupSep&&(n=n.split(o.groupSep).join("")),n=n.replace(o.decimalSep,"."),e.endsWith(o.decimalSep)||e.endsWith(".")){const s=n.replace(/\.$/,""),h=parseFloat(s);isNaN(h)||this._setHiddenRaw(h);return}const d=n.indexOf(".");if(d!==-1&&n.slice(d+1).endsWith("0")){const h=parseFloat(n);isNaN(h)||this._setHiddenRaw(h);return}const p=t.getAttribute("data-ln-number-decimals");if(p!==null&&d!==-1){const s=parseInt(p,10);n.slice(d+1).length>s&&(n=n.slice(0,d+1+s))}const c=parseFloat(n);if(isNaN(c))return;const _=t.getAttribute("data-ln-number-min"),E=t.getAttribute("data-ln-number-max");if(_!==null&&c<parseFloat(_)||E!==null&&c>parseFloat(E))return;let y;if(p!==null)y=v(w(t),c,p);else{const s=d!==-1?n.slice(d+1).length:0;if(s>0){const h=w(t)+"|u"+s;u[h]||(u[h]=new Intl.NumberFormat(w(t),{useGrouping:!0,minimumFractionDigits:s,maximumFractionDigits:s})),y=u[h].format(c)}else y=o.fmt.format(c)}t.value=y;let F=r,D=0;for(let s=0;s<y.length&&F>0;s++)D=s+1,/[0-9]/.test(y[s])&&F--;F>0&&(D=y.length),t.setSelectionRange(D,D),this._setHiddenRaw(c),N(t,"ln-number:input",{value:c,formatted:y})},f.prototype._setHiddenRaw=function(t){g.set.call(this._hidden,String(t))},f.prototype._displayFormatted=function(t){this.dom.value=v(w(this.dom),t,this.dom.getAttribute("data-ln-number-decimals"))},Object.defineProperty(f.prototype,"value",{get:function(){const t=this._hidden.value;return t===""?NaN:parseFloat(t)},set:function(t){if(typeof t!="number"||isNaN(t)){this.dom.value="",this._setHiddenRaw("");return}this._displayFormatted(t),this._setHiddenRaw(t),N(this.dom,"ln-number:input",{value:t,formatted:this.dom.value})}}),Object.defineProperty(f.prototype,"formatted",{get:function(){return this.dom.value}}),f.prototype.destroy=function(){this.dom[i]&&(this.dom.removeEventListener("input",this._onInput),this.dom.removeEventListener("paste",this._onPaste),this.dom.name=this._hidden.name,this.dom.type="number",this.dom.removeAttribute("inputmode"),this._hidden.remove(),N(this.dom,"ln-number:destroyed",{target:this.dom}),delete this.dom[i])};function m(){new MutationObserver(function(){const t=document.querySelectorAll("["+a+"]");for(let o=0;o<t.length;o++){const e=t[o][i];e&&!isNaN(e.value)&&e._displayFormatted(e.value)}}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"]})}O(a,i,f,"ln-number"),m()})()})();
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { dispatch, getLocale, registerComponent } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-number';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnNumber';
|
|
6
|
+
|
|
7
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
|
+
|
|
9
|
+
// ─── Formatter Cache ──────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const _formatters = {};
|
|
12
|
+
const _inputValueDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
|
|
13
|
+
|
|
14
|
+
function _getFormatter(locale) {
|
|
15
|
+
if (!_formatters[locale]) {
|
|
16
|
+
const fmt = new Intl.NumberFormat(locale, { useGrouping: true });
|
|
17
|
+
const parts = fmt.formatToParts(1234.5);
|
|
18
|
+
let groupSep = '';
|
|
19
|
+
let decimalSep = '.';
|
|
20
|
+
for (let i = 0; i < parts.length; i++) {
|
|
21
|
+
if (parts[i].type === 'group') groupSep = parts[i].value;
|
|
22
|
+
if (parts[i].type === 'decimal') decimalSep = parts[i].value;
|
|
23
|
+
}
|
|
24
|
+
_formatters[locale] = { fmt: fmt, groupSep: groupSep, decimalSep: decimalSep };
|
|
25
|
+
}
|
|
26
|
+
return _formatters[locale];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _formatNum(locale, num, maxDecimals) {
|
|
30
|
+
if (maxDecimals !== null) {
|
|
31
|
+
const max = parseInt(maxDecimals, 10);
|
|
32
|
+
const key = locale + '|d' + max;
|
|
33
|
+
if (!_formatters[key]) {
|
|
34
|
+
_formatters[key] = new Intl.NumberFormat(locale, { useGrouping: true, minimumFractionDigits: 0, maximumFractionDigits: max });
|
|
35
|
+
}
|
|
36
|
+
return _formatters[key].format(num);
|
|
37
|
+
}
|
|
38
|
+
return _getFormatter(locale).fmt.format(num);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Component ─────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function _component(dom) {
|
|
44
|
+
if (dom.tagName !== 'INPUT') {
|
|
45
|
+
console.warn('[ln-number] Can only be applied to <input>, got:', dom.tagName);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.dom = dom;
|
|
50
|
+
|
|
51
|
+
// ── Create hidden input ─────────────────────────────
|
|
52
|
+
const hidden = document.createElement('input');
|
|
53
|
+
hidden.type = 'hidden';
|
|
54
|
+
hidden.name = dom.name;
|
|
55
|
+
dom.removeAttribute('name');
|
|
56
|
+
dom.type = 'text';
|
|
57
|
+
dom.setAttribute('inputmode', 'decimal');
|
|
58
|
+
dom.insertAdjacentElement('afterend', hidden);
|
|
59
|
+
this._hidden = hidden;
|
|
60
|
+
|
|
61
|
+
// ── Intercept programmatic value sets on hidden input ──
|
|
62
|
+
const self = this;
|
|
63
|
+
Object.defineProperty(hidden, 'value', {
|
|
64
|
+
get: function () {
|
|
65
|
+
return _inputValueDesc.get.call(hidden);
|
|
66
|
+
},
|
|
67
|
+
set: function (val) {
|
|
68
|
+
_inputValueDesc.set.call(hidden, val);
|
|
69
|
+
// If set programmatically (e.g., populateForm), update display
|
|
70
|
+
if (val !== '' && !isNaN(parseFloat(val))) {
|
|
71
|
+
self._displayFormatted(parseFloat(val));
|
|
72
|
+
} else if (val === '') {
|
|
73
|
+
self.dom.value = '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Bind input event ────────────────────────────────
|
|
79
|
+
this._onInput = function () {
|
|
80
|
+
self._handleInput();
|
|
81
|
+
};
|
|
82
|
+
dom.addEventListener('input', this._onInput);
|
|
83
|
+
|
|
84
|
+
// ── Bind paste event ────────────────────────────────
|
|
85
|
+
this._onPaste = function (e) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const pasted = (e.clipboardData || window.clipboardData).getData('text');
|
|
88
|
+
const info = _getFormatter(getLocale(dom));
|
|
89
|
+
const decSepEscaped = info.decimalSep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
90
|
+
// Strip everything except digits, minus, and decimal separators
|
|
91
|
+
let cleaned = pasted.replace(new RegExp('[^0-9\\-' + decSepEscaped + '.]', 'g'), '');
|
|
92
|
+
// Strip group separators before normalizing decimal
|
|
93
|
+
if (info.groupSep) {
|
|
94
|
+
cleaned = cleaned.split(info.groupSep).join('');
|
|
95
|
+
}
|
|
96
|
+
// Normalize: if locale decimal is not '.', replace it
|
|
97
|
+
if (info.decimalSep !== '.') {
|
|
98
|
+
cleaned = cleaned.replace(info.decimalSep, '.');
|
|
99
|
+
}
|
|
100
|
+
const num = parseFloat(cleaned);
|
|
101
|
+
if (!isNaN(num)) {
|
|
102
|
+
self.value = num;
|
|
103
|
+
} else {
|
|
104
|
+
dom.value = '';
|
|
105
|
+
self._hidden.value = '';
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
dom.addEventListener('paste', this._onPaste);
|
|
109
|
+
|
|
110
|
+
// ── Handle pre-filled value ─────────────────────────
|
|
111
|
+
const initial = dom.value;
|
|
112
|
+
if (initial !== '') {
|
|
113
|
+
const num = parseFloat(initial);
|
|
114
|
+
if (!isNaN(num)) {
|
|
115
|
+
this._displayFormatted(num);
|
|
116
|
+
_inputValueDesc.set.call(hidden, String(num));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_component.prototype._handleInput = function () {
|
|
124
|
+
const dom = this.dom;
|
|
125
|
+
const info = _getFormatter(getLocale(dom));
|
|
126
|
+
const raw = dom.value;
|
|
127
|
+
|
|
128
|
+
// Edge case: empty
|
|
129
|
+
if (raw === '') {
|
|
130
|
+
this._hidden.value = '';
|
|
131
|
+
dispatch(dom, 'ln-number:input', { value: NaN, formatted: '' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Edge case: just minus sign
|
|
136
|
+
if (raw === '-') {
|
|
137
|
+
this._hidden.value = '';
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Save cursor context: count digits to the left of cursor
|
|
142
|
+
const cursorPos = dom.selectionStart;
|
|
143
|
+
let digitsBeforeCursor = 0;
|
|
144
|
+
for (let i = 0; i < cursorPos; i++) {
|
|
145
|
+
if (/[0-9]/.test(raw[i])) digitsBeforeCursor++;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse: strip group separators, normalize decimal
|
|
149
|
+
let cleaned = raw;
|
|
150
|
+
if (info.groupSep) {
|
|
151
|
+
cleaned = cleaned.split(info.groupSep).join('');
|
|
152
|
+
}
|
|
153
|
+
cleaned = cleaned.replace(info.decimalSep, '.');
|
|
154
|
+
|
|
155
|
+
// Edge case: trailing decimal separator (user about to type decimals)
|
|
156
|
+
if (raw.endsWith(info.decimalSep) || raw.endsWith('.')) {
|
|
157
|
+
const beforeDecimal = cleaned.replace(/\.$/, '');
|
|
158
|
+
const num = parseFloat(beforeDecimal);
|
|
159
|
+
if (!isNaN(num)) {
|
|
160
|
+
this._setHiddenRaw(num);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Edge case: trailing zeros after decimal (user still typing)
|
|
166
|
+
const decimalIndex = cleaned.indexOf('.');
|
|
167
|
+
if (decimalIndex !== -1) {
|
|
168
|
+
const afterDecimal = cleaned.slice(decimalIndex + 1);
|
|
169
|
+
if (afterDecimal.endsWith('0')) {
|
|
170
|
+
const num = parseFloat(cleaned);
|
|
171
|
+
if (!isNaN(num)) {
|
|
172
|
+
this._setHiddenRaw(num);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Enforce decimal limit
|
|
179
|
+
const maxDecimals = dom.getAttribute('data-ln-number-decimals');
|
|
180
|
+
if (maxDecimals !== null && decimalIndex !== -1) {
|
|
181
|
+
const allowed = parseInt(maxDecimals, 10);
|
|
182
|
+
const afterDec = cleaned.slice(decimalIndex + 1);
|
|
183
|
+
if (afterDec.length > allowed) {
|
|
184
|
+
cleaned = cleaned.slice(0, decimalIndex + 1 + allowed);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const num = parseFloat(cleaned);
|
|
189
|
+
if (isNaN(num)) return;
|
|
190
|
+
|
|
191
|
+
// Enforce min/max
|
|
192
|
+
const minAttr = dom.getAttribute('data-ln-number-min');
|
|
193
|
+
const maxAttr = dom.getAttribute('data-ln-number-max');
|
|
194
|
+
if (minAttr !== null && num < parseFloat(minAttr)) return;
|
|
195
|
+
if (maxAttr !== null && num > parseFloat(maxAttr)) return;
|
|
196
|
+
|
|
197
|
+
// Format
|
|
198
|
+
let formatted;
|
|
199
|
+
if (maxDecimals !== null) {
|
|
200
|
+
formatted = _formatNum(getLocale(dom), num, maxDecimals);
|
|
201
|
+
} else {
|
|
202
|
+
// Preserve the user's decimal places
|
|
203
|
+
const userDecimals = decimalIndex !== -1 ? cleaned.slice(decimalIndex + 1).length : 0;
|
|
204
|
+
if (userDecimals > 0) {
|
|
205
|
+
const key = getLocale(dom) + '|u' + userDecimals;
|
|
206
|
+
if (!_formatters[key]) {
|
|
207
|
+
_formatters[key] = new Intl.NumberFormat(getLocale(dom), { useGrouping: true, minimumFractionDigits: userDecimals, maximumFractionDigits: userDecimals });
|
|
208
|
+
}
|
|
209
|
+
formatted = _formatters[key].format(num);
|
|
210
|
+
} else {
|
|
211
|
+
formatted = info.fmt.format(num);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
dom.value = formatted;
|
|
216
|
+
|
|
217
|
+
// Restore cursor position
|
|
218
|
+
let targetDigits = digitsBeforeCursor;
|
|
219
|
+
let newPos = 0;
|
|
220
|
+
for (let i = 0; i < formatted.length && targetDigits > 0; i++) {
|
|
221
|
+
newPos = i + 1;
|
|
222
|
+
if (/[0-9]/.test(formatted[i])) targetDigits--;
|
|
223
|
+
}
|
|
224
|
+
// If we didn't consume all digits, put cursor at end
|
|
225
|
+
if (targetDigits > 0) newPos = formatted.length;
|
|
226
|
+
dom.setSelectionRange(newPos, newPos);
|
|
227
|
+
|
|
228
|
+
// Update hidden input (bypass our setter to avoid feedback loop)
|
|
229
|
+
this._setHiddenRaw(num);
|
|
230
|
+
|
|
231
|
+
dispatch(dom, 'ln-number:input', { value: num, formatted: formatted });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
_component.prototype._setHiddenRaw = function (num) {
|
|
235
|
+
_inputValueDesc.set.call(this._hidden, String(num));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
_component.prototype._displayFormatted = function (num) {
|
|
239
|
+
this.dom.value = _formatNum(getLocale(this.dom), num, this.dom.getAttribute('data-ln-number-decimals'));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// ─── Public API ───────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
Object.defineProperty(_component.prototype, 'value', {
|
|
245
|
+
get: function () {
|
|
246
|
+
const raw = this._hidden.value;
|
|
247
|
+
return raw === '' ? NaN : parseFloat(raw);
|
|
248
|
+
},
|
|
249
|
+
set: function (num) {
|
|
250
|
+
if (typeof num !== 'number' || isNaN(num)) {
|
|
251
|
+
this.dom.value = '';
|
|
252
|
+
this._setHiddenRaw('');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this._displayFormatted(num);
|
|
256
|
+
this._setHiddenRaw(num);
|
|
257
|
+
dispatch(this.dom, 'ln-number:input', {
|
|
258
|
+
value: num,
|
|
259
|
+
formatted: this.dom.value
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
Object.defineProperty(_component.prototype, 'formatted', {
|
|
265
|
+
get: function () {
|
|
266
|
+
return this.dom.value;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
_component.prototype.destroy = function () {
|
|
271
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
272
|
+
this.dom.removeEventListener('input', this._onInput);
|
|
273
|
+
this.dom.removeEventListener('paste', this._onPaste);
|
|
274
|
+
// Restore name to visible input
|
|
275
|
+
this.dom.name = this._hidden.name;
|
|
276
|
+
this.dom.type = 'number';
|
|
277
|
+
this.dom.removeAttribute('inputmode');
|
|
278
|
+
// Remove hidden input
|
|
279
|
+
this._hidden.remove();
|
|
280
|
+
dispatch(this.dom, 'ln-number:destroyed', { target: this.dom });
|
|
281
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ─── Locale Observer ──────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
function _localeObserver() {
|
|
287
|
+
new MutationObserver(function () {
|
|
288
|
+
const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
|
|
289
|
+
for (let i = 0; i < els.length; i++) {
|
|
290
|
+
const inst = els[i][DOM_ATTRIBUTE];
|
|
291
|
+
if (inst && !isNaN(inst.value)) {
|
|
292
|
+
inst._displayFormatted(inst.value);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-number');
|
|
301
|
+
_localeObserver();
|
|
302
|
+
})();
|