@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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -1,350 +1,443 @@
1
- import { dispatch, getLocale, registerComponent, interceptValueProperty } 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
- if (dom[DOM_ATTRIBUTE]) return dom[DOM_ATTRIBUTE];
50
- dom[DOM_ATTRIBUTE] = this;
51
-
52
- this.dom = dom;
53
-
54
- // ── Create hidden input ─────────────────────────────
55
- const hidden = document.createElement('input');
56
- hidden.type = 'hidden';
57
- hidden.name = dom.name;
58
- dom.removeAttribute('name');
59
- dom.type = 'text';
60
- dom.setAttribute('inputmode', 'decimal');
61
- dom.insertAdjacentElement('afterend', hidden);
62
- this._hidden = hidden;
63
-
64
- // ── Intercept programmatic value sets on hidden input ──
65
- const self = this;
66
- Object.defineProperty(hidden, 'value', {
67
- get: function () {
68
- return _inputValueDesc.get.call(hidden);
69
- },
70
- set: function (val) {
71
- _inputValueDesc.set.call(hidden, val);
72
- // If set programmatically (e.g., populateForm), update display
73
- if (val !== '' && !isNaN(parseFloat(val))) {
74
- const num = parseFloat(val);
75
- self._displayFormatted(num);
76
- dispatch(self.dom, 'ln-number:input', { value: num, formatted: self.dom.value });
77
- self.dom.dispatchEvent(new Event('input', { bubbles: true }));
78
- } else if (val === '') {
79
- self.dom.value = '';
80
- dispatch(self.dom, 'ln-number:input', { value: NaN, formatted: '' });
81
- self.dom.dispatchEvent(new Event('input', { bubbles: true }));
82
- }
83
- }
84
- });
85
-
86
- // ── Intercept programmatic value sets on visible input (2-way binding) ──
87
- interceptValueProperty(dom, _inputValueDesc, {
88
- get: function () {
89
- return _inputValueDesc.get.call(dom);
90
- },
91
- set: function (val, originalSet) {
92
- if (self._isFormatting) {
93
- originalSet(val);
94
- return;
95
- }
96
- if (val === '') {
97
- originalSet('');
98
- self._setHiddenRaw('');
99
- dispatch(dom, 'ln-number:input', { value: NaN, formatted: '' });
100
- dom.dispatchEvent(new Event('input', { bubbles: true }));
101
- return;
102
- }
103
- // Parse and format
104
- let num = typeof val === 'number' ? val : parseFloat(String(val).replace(/[^\d.-]/g, ''));
105
- if (isNaN(num)) {
106
- originalSet(String(val));
107
- self._setHiddenRaw('');
108
- dispatch(dom, 'ln-number:input', { value: NaN, formatted: String(val) });
109
- dom.dispatchEvent(new Event('input', { bubbles: true }));
110
- } else {
111
- self._setHiddenRaw(num);
112
- const formatted = _formatNum(getLocale(dom), num, dom.getAttribute('data-ln-number-decimals'));
113
- originalSet(formatted);
114
- dispatch(dom, 'ln-number:input', { value: num, formatted: formatted });
115
- dom.dispatchEvent(new Event('input', { bubbles: true }));
116
- }
117
- }
118
- });
119
-
120
- // ── Bind input event ────────────────────────────────
121
- this._onInput = function () {
122
- self._handleInput();
123
- };
124
- dom.addEventListener('input', this._onInput);
125
-
126
- // ── Bind paste event ────────────────────────────────
127
- this._onPaste = function (e) {
128
- e.preventDefault();
129
- const pasted = (e.clipboardData || window.clipboardData).getData('text');
130
- const info = _getFormatter(getLocale(dom));
131
- const decSepEscaped = info.decimalSep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
132
- // Strip everything except digits, minus, and decimal separators
133
- let cleaned = pasted.replace(new RegExp('[^0-9\\-' + decSepEscaped + '.]', 'g'), '');
134
- // Strip group separators before normalizing decimal
135
- if (info.groupSep) {
136
- cleaned = cleaned.split(info.groupSep).join('');
137
- }
138
- // Normalize: if locale decimal is not '.', replace it
139
- if (info.decimalSep !== '.') {
140
- cleaned = cleaned.replace(info.decimalSep, '.');
141
- }
142
- const num = parseFloat(cleaned);
143
- if (!isNaN(num)) {
144
- self.value = num;
145
- } else {
146
- dom.value = '';
147
- self._hidden.value = '';
148
- }
149
- };
150
- dom.addEventListener('paste', this._onPaste);
151
-
152
- // ── Handle pre-filled value ─────────────────────────
153
- const initial = dom.value;
154
- if (initial !== '') {
155
- const num = parseFloat(initial);
156
- if (!isNaN(num)) {
157
- this._displayFormatted(num);
158
- _inputValueDesc.set.call(hidden, String(num));
159
- dispatch(dom, 'ln-number:input', { value: num, formatted: dom.value });
160
- dom.dispatchEvent(new Event('input', { bubbles: true }));
161
- }
162
- }
163
-
164
- return this;
165
- }
166
-
167
- _component.prototype._handleInput = function () {
168
- const dom = this.dom;
169
- const info = _getFormatter(getLocale(dom));
170
- const raw = dom.value;
171
-
172
- // Edge case: empty
173
- if (raw === '') {
174
- this._hidden.value = '';
175
- dispatch(dom, 'ln-number:input', { value: NaN, formatted: '' });
176
- return;
177
- }
178
-
179
- // Edge case: just minus sign
180
- if (raw === '-') {
181
- this._hidden.value = '';
182
- return;
183
- }
184
-
185
- // Save cursor context: count digits to the left of cursor
186
- const cursorPos = dom.selectionStart;
187
- let digitsBeforeCursor = 0;
188
- for (let i = 0; i < cursorPos; i++) {
189
- if (/[0-9]/.test(raw[i])) digitsBeforeCursor++;
190
- }
191
-
192
- // Parse: strip group separators, normalize decimal
193
- let cleaned = raw;
194
- if (info.groupSep) {
195
- cleaned = cleaned.split(info.groupSep).join('');
196
- }
197
- cleaned = cleaned.replace(info.decimalSep, '.');
198
-
199
- // Edge case: trailing decimal separator (user about to type decimals)
200
- if (raw.endsWith(info.decimalSep) || raw.endsWith('.')) {
201
- const beforeDecimal = cleaned.replace(/\.$/, '');
202
- const num = parseFloat(beforeDecimal);
203
- if (!isNaN(num)) {
204
- this._setHiddenRaw(num);
205
- }
206
- return;
207
- }
208
-
209
- // Edge case: trailing zeros after decimal (user still typing)
210
- const decimalIndex = cleaned.indexOf('.');
211
- if (decimalIndex !== -1) {
212
- const afterDecimal = cleaned.slice(decimalIndex + 1);
213
- if (afterDecimal.endsWith('0')) {
214
- const num = parseFloat(cleaned);
215
- if (!isNaN(num)) {
216
- this._setHiddenRaw(num);
217
- }
218
- return;
219
- }
220
- }
221
-
222
- // Enforce decimal limit
223
- const maxDecimals = dom.getAttribute('data-ln-number-decimals');
224
- if (maxDecimals !== null && decimalIndex !== -1) {
225
- const allowed = parseInt(maxDecimals, 10);
226
- const afterDec = cleaned.slice(decimalIndex + 1);
227
- if (afterDec.length > allowed) {
228
- cleaned = cleaned.slice(0, decimalIndex + 1 + allowed);
229
- }
230
- }
231
-
232
- const num = parseFloat(cleaned);
233
- if (isNaN(num)) return;
234
-
235
- // Enforce min/max
236
- const minAttr = dom.getAttribute('data-ln-number-min');
237
- const maxAttr = dom.getAttribute('data-ln-number-max');
238
- if (minAttr !== null && num < parseFloat(minAttr)) return;
239
- if (maxAttr !== null && num > parseFloat(maxAttr)) return;
240
-
241
- // Format
242
- let formatted;
243
- if (maxDecimals !== null) {
244
- formatted = _formatNum(getLocale(dom), num, maxDecimals);
245
- } else {
246
- // Preserve the user's decimal places
247
- const userDecimals = decimalIndex !== -1 ? cleaned.slice(decimalIndex + 1).length : 0;
248
- if (userDecimals > 0) {
249
- const key = getLocale(dom) + '|u' + userDecimals;
250
- if (!_formatters[key]) {
251
- _formatters[key] = new Intl.NumberFormat(getLocale(dom), { useGrouping: true, minimumFractionDigits: userDecimals, maximumFractionDigits: userDecimals });
252
- }
253
- formatted = _formatters[key].format(num);
254
- } else {
255
- formatted = info.fmt.format(num);
256
- }
257
- }
258
-
259
- dom.value = formatted;
260
-
261
- // Restore cursor position
262
- let targetDigits = digitsBeforeCursor;
263
- let newPos = 0;
264
- for (let i = 0; i < formatted.length && targetDigits > 0; i++) {
265
- newPos = i + 1;
266
- if (/[0-9]/.test(formatted[i])) targetDigits--;
267
- }
268
- // If we didn't consume all digits, put cursor at end
269
- if (targetDigits > 0) newPos = formatted.length;
270
- dom.setSelectionRange(newPos, newPos);
271
-
272
- // Update hidden input (bypass our setter to avoid feedback loop)
273
- this._setHiddenRaw(num);
274
-
275
- dispatch(dom, 'ln-number:input', { value: num, formatted: formatted });
276
- };
277
-
278
- _component.prototype._setHiddenRaw = function (num) {
279
- _inputValueDesc.set.call(this._hidden, String(num));
280
- };
281
-
282
- _component.prototype._displayFormatted = function (num) {
283
- this._isFormatting = true;
284
- this.dom.value = _formatNum(getLocale(this.dom), num, this.dom.getAttribute('data-ln-number-decimals'));
285
- this._isFormatting = false;
286
- };
287
-
288
- // ─── Public API ───────────────────────────────────────────
289
-
290
- Object.defineProperty(_component.prototype, 'value', {
291
- get: function () {
292
- const raw = this._hidden.value;
293
- return raw === '' ? NaN : parseFloat(raw);
294
- },
295
- set: function (num) {
296
- if (typeof num !== 'number' || isNaN(num)) {
297
- this.dom.value = '';
298
- this._setHiddenRaw('');
299
- this.dom.dispatchEvent(new Event('input', { bubbles: true }));
300
- return;
301
- }
302
- this._displayFormatted(num);
303
- this._setHiddenRaw(num);
304
- dispatch(this.dom, 'ln-number:input', {
305
- value: num,
306
- formatted: this.dom.value
307
- });
308
- this.dom.dispatchEvent(new Event('input', { bubbles: true }));
309
- }
310
- });
311
-
312
- Object.defineProperty(_component.prototype, 'formatted', {
313
- get: function () {
314
- return this.dom.value;
315
- }
316
- });
317
-
318
- _component.prototype.destroy = function () {
319
- if (!this.dom[DOM_ATTRIBUTE]) return;
320
- this.dom.removeEventListener('input', this._onInput);
321
- this.dom.removeEventListener('paste', this._onPaste);
322
- // Restore name to visible input
323
- this.dom.name = this._hidden.name;
324
- this.dom.type = 'number';
325
- this.dom.removeAttribute('inputmode');
326
- // Remove hidden input
327
- this._hidden.remove();
328
- dispatch(this.dom, 'ln-number:destroyed', { target: this.dom });
329
- delete this.dom[DOM_ATTRIBUTE];
330
- };
331
-
332
- // ─── Locale Observer ──────────────────────────────────────
333
-
334
- function _localeObserver() {
335
- new MutationObserver(function () {
336
- const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
337
- for (let i = 0; i < els.length; i++) {
338
- const inst = els[i][DOM_ATTRIBUTE];
339
- if (inst && !isNaN(inst.value)) {
340
- inst._displayFormatted(inst.value);
341
- }
342
- }
343
- }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
344
- }
345
-
346
- // ─── Init ──────────────────────────────────────────────────
347
-
348
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-number');
349
- _localeObserver();
350
- })();
1
+ import { dispatch, getLocale, registerComponent, interceptValueProperty } 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
+ // ─── Component ─────────────────────────────────────────────
44
+
45
+ function _component(dom) {
46
+ if (dom[DOM_ATTRIBUTE]) return dom[DOM_ATTRIBUTE];
47
+ dom[DOM_ATTRIBUTE] = this;
48
+ this.dom = dom;
49
+
50
+ if (dom.tagName !== 'INPUT') {
51
+ this.isTextElement = true;
52
+ this._initTextElement();
53
+ return this;
54
+ }
55
+
56
+ // ── Create hidden input ─────────────────────────────
57
+ const hidden = document.createElement('input');
58
+ hidden.type = 'hidden';
59
+ hidden.name = dom.name;
60
+ dom.removeAttribute('name');
61
+ if (dom.hasAttribute('data-ln-fill-as')) {
62
+ hidden.setAttribute('data-ln-fill-as', dom.getAttribute('data-ln-fill-as'));
63
+ }
64
+ dom.type = 'text';
65
+ dom.setAttribute('inputmode', 'decimal');
66
+ dom.insertAdjacentElement('afterend', hidden);
67
+ this._hidden = hidden;
68
+
69
+ // ── Intercept programmatic value sets on hidden input ──
70
+ const self = this;
71
+ Object.defineProperty(hidden, 'value', {
72
+ get: function () {
73
+ return _inputValueDesc.get.call(hidden);
74
+ },
75
+ set: function (val) {
76
+ _inputValueDesc.set.call(hidden, val); // store raw natively
77
+ if (val !== '' && !isNaN(parseFloat(val))) {
78
+ self._setDisplayRaw(_formatNum(getLocale(self.dom), parseFloat(val), self.dom.getAttribute('data-ln-number-decimals')));
79
+ } else {
80
+ self._setDisplayRaw('');
81
+ }
82
+ self.dom.dispatchEvent(new Event('input', { bubbles: true })); // single ecosystem signal → _handleInput emits ln-number:input
83
+ }
84
+ });
85
+
86
+ // ── Intercept programmatic value sets on visible input (2-way binding) ──
87
+ interceptValueProperty(dom, _inputValueDesc, {
88
+ get: function () {
89
+ return _inputValueDesc.get.call(dom);
90
+ },
91
+ set: function (val) {
92
+ if (val === '') {
93
+ self._setDisplayRaw('');
94
+ self._setHiddenRaw('');
95
+ dom.dispatchEvent(new Event('input', { bubbles: true }));
96
+ return;
97
+ }
98
+ const num = typeof val === 'number' ? val : parseFloat(String(val).replace(/[^\d.-]/g, ''));
99
+ if (isNaN(num)) {
100
+ self._setDisplayRaw(String(val));
101
+ self._setHiddenRaw('');
102
+ } else {
103
+ self._setHiddenRaw(num);
104
+ self._setDisplayRaw(_formatNum(getLocale(dom), num, dom.getAttribute('data-ln-number-decimals')));
105
+ }
106
+ dom.dispatchEvent(new Event('input', { bubbles: true }));
107
+ }
108
+ });
109
+
110
+ // ── Bind input event ────────────────────────────────
111
+ this._onInput = function () {
112
+ self._handleInput();
113
+ };
114
+ dom.addEventListener('input', this._onInput);
115
+
116
+ // ── Bind paste event ────────────────────────────────
117
+ this._onPaste = function (e) {
118
+ e.preventDefault();
119
+ const pasted = (e.clipboardData || window.clipboardData).getData('text');
120
+ const info = _getFormatter(getLocale(dom));
121
+ const decSepEscaped = info.decimalSep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
122
+ // Strip everything except digits, minus, and decimal separators
123
+ let cleaned = pasted.replace(new RegExp('[^0-9\\-' + decSepEscaped + '.]', 'g'), '');
124
+ // Strip group separators before normalizing decimal
125
+ if (info.groupSep) {
126
+ cleaned = cleaned.split(info.groupSep).join('');
127
+ }
128
+ // Normalize: if locale decimal is not '.', replace it
129
+ if (info.decimalSep !== '.') {
130
+ cleaned = cleaned.replace(info.decimalSep, '.');
131
+ }
132
+ const num = parseFloat(cleaned);
133
+ self.value = isNaN(num) ? NaN : num;
134
+ };
135
+ dom.addEventListener('paste', this._onPaste);
136
+
137
+ // ── Handle pre-filled value ─────────────────────────
138
+ const initial = dom.value;
139
+ if (initial !== '') {
140
+ const num = parseFloat(initial);
141
+ if (!isNaN(num)) {
142
+ this._setHiddenRaw(num);
143
+ this._setDisplayRaw(_formatNum(getLocale(dom), num, dom.getAttribute('data-ln-number-decimals')));
144
+ dom.dispatchEvent(new Event('input', { bubbles: true }));
145
+ }
146
+ }
147
+
148
+ return this;
149
+ }
150
+
151
+ function _parseRawNumber(val) {
152
+ if (typeof val === 'number') return isNaN(val) ? null : val;
153
+ if (!val || typeof val !== 'string') return null;
154
+ let str = val.trim();
155
+ if (str === '') return null;
156
+ // Remove spaces/non-breaking spaces/currency symbols
157
+ str = str.replace(/[\s\u00A0$€£]/g, '');
158
+
159
+ // Handle "1.234,56" (DE/MK) vs "1,234.56" (EN)
160
+ if (str.indexOf(',') !== -1 && str.indexOf('.') !== -1) {
161
+ if (str.indexOf('.') < str.indexOf(',')) {
162
+ // DE/MK style: 1.234,56 -> 1234.56
163
+ str = str.replace(/\./g, '').replace(',', '.');
164
+ } else {
165
+ // EN style: 1,234.56 -> 1234.56
166
+ str = str.replace(/,/g, '');
167
+ }
168
+ } else if (str.indexOf(',') !== -1) {
169
+ // Only comma e.g. "1499,50" -> 1499.50
170
+ str = str.replace(',', '.');
171
+ }
172
+ // Strip any remaining invalid chars
173
+ str = str.replace(/[^\d.-]/g, '');
174
+ const num = parseFloat(str);
175
+ return isNaN(num) ? null : num;
176
+ }
177
+
178
+ _component.prototype._initTextElement = function () {
179
+ const dom = this.dom;
180
+ let valAttr = dom.getAttribute('data-ln-value');
181
+ let numAttr = dom.getAttribute('data-ln-number');
182
+
183
+ let candidate = null;
184
+ if (valAttr !== null && valAttr !== '') {
185
+ candidate = valAttr;
186
+ } else if (numAttr !== null && numAttr !== '' && numAttr !== 'true') {
187
+ candidate = numAttr;
188
+ } else {
189
+ candidate = dom.textContent.trim();
190
+ }
191
+
192
+ const num = _parseRawNumber(candidate);
193
+ if (num !== null) {
194
+ this._rawValue = num;
195
+ if (!dom.hasAttribute('data-ln-value')) {
196
+ dom.setAttribute('data-ln-value', String(num));
197
+ }
198
+ this._formatTextContent();
199
+ } else {
200
+ this._rawValue = null;
201
+ }
202
+ };
203
+
204
+ _component.prototype._formatTextContent = function () {
205
+ if (this._rawValue !== null && !isNaN(this._rawValue)) {
206
+ const decimals = this.dom.getAttribute('data-ln-number-decimals');
207
+ this.dom.textContent = _formatNum(getLocale(this.dom), this._rawValue, decimals);
208
+ }
209
+ };
210
+
211
+ _component.prototype._handleInput = function () {
212
+ const dom = this.dom;
213
+ const info = _getFormatter(getLocale(dom));
214
+ const raw = _inputValueDesc.get.call(dom);
215
+
216
+ // Edge case: empty
217
+ if (raw === '') {
218
+ this._setHiddenRaw('');
219
+ dispatch(dom, 'ln-number:input', { value: NaN, formatted: '' });
220
+ return;
221
+ }
222
+
223
+ // Edge case: just minus sign
224
+ if (raw === '-') {
225
+ this._setHiddenRaw('');
226
+ return;
227
+ }
228
+
229
+ // Save cursor context: count digits to the left of cursor
230
+ const cursorPos = dom.selectionStart;
231
+ let digitsBeforeCursor = 0;
232
+ for (let i = 0; i < cursorPos; i++) {
233
+ if (/[0-9]/.test(raw[i])) digitsBeforeCursor++;
234
+ }
235
+
236
+ // Parse: strip group separators, normalize decimal
237
+ let cleaned = raw;
238
+ if (info.groupSep) {
239
+ cleaned = cleaned.split(info.groupSep).join('');
240
+ }
241
+ cleaned = cleaned.replace(info.decimalSep, '.');
242
+
243
+ // Edge case: trailing decimal separator (user about to type decimals)
244
+ if (raw.endsWith(info.decimalSep) || raw.endsWith('.')) {
245
+ const beforeDecimal = cleaned.replace(/\.$/, '');
246
+ const num = parseFloat(beforeDecimal);
247
+ if (!isNaN(num)) {
248
+ this._setHiddenRaw(num);
249
+ }
250
+ return;
251
+ }
252
+
253
+ // Edge case: trailing zeros after decimal (user still typing)
254
+ const decimalIndex = cleaned.indexOf('.');
255
+ if (decimalIndex !== -1) {
256
+ const afterDecimal = cleaned.slice(decimalIndex + 1);
257
+ if (afterDecimal.endsWith('0')) {
258
+ const num = parseFloat(cleaned);
259
+ if (!isNaN(num)) {
260
+ this._setHiddenRaw(num);
261
+ }
262
+ return;
263
+ }
264
+ }
265
+
266
+ // Enforce decimal limit
267
+ const maxDecimals = dom.getAttribute('data-ln-number-decimals');
268
+ if (maxDecimals !== null && decimalIndex !== -1) {
269
+ const allowed = parseInt(maxDecimals, 10);
270
+ const afterDec = cleaned.slice(decimalIndex + 1);
271
+ if (afterDec.length > allowed) {
272
+ cleaned = cleaned.slice(0, decimalIndex + 1 + allowed);
273
+ }
274
+ }
275
+
276
+ const num = parseFloat(cleaned);
277
+ if (isNaN(num)) return;
278
+
279
+ // Enforce min/max
280
+ const minAttr = dom.getAttribute('data-ln-number-min');
281
+ const maxAttr = dom.getAttribute('data-ln-number-max');
282
+ if (minAttr !== null && num < parseFloat(minAttr)) return;
283
+ if (maxAttr !== null && num > parseFloat(maxAttr)) return;
284
+
285
+ // Format
286
+ let formatted;
287
+ if (maxDecimals !== null) {
288
+ formatted = _formatNum(getLocale(dom), num, maxDecimals);
289
+ } else {
290
+ // Preserve the user's decimal places
291
+ const userDecimals = decimalIndex !== -1 ? cleaned.slice(decimalIndex + 1).length : 0;
292
+ if (userDecimals > 0) {
293
+ const key = getLocale(dom) + '|u' + userDecimals;
294
+ if (!_formatters[key]) {
295
+ _formatters[key] = new Intl.NumberFormat(getLocale(dom), { useGrouping: true, minimumFractionDigits: userDecimals, maximumFractionDigits: userDecimals });
296
+ }
297
+ formatted = _formatters[key].format(num);
298
+ } else {
299
+ formatted = info.fmt.format(num);
300
+ }
301
+ }
302
+
303
+ this._setDisplayRaw(formatted);
304
+
305
+ // Restore cursor position
306
+ let targetDigits = digitsBeforeCursor;
307
+ let newPos = 0;
308
+ for (let i = 0; i < formatted.length && targetDigits > 0; i++) {
309
+ newPos = i + 1;
310
+ if (/[0-9]/.test(formatted[i])) targetDigits--;
311
+ }
312
+ // If we didn't consume all digits, put cursor at end
313
+ if (targetDigits > 0) newPos = formatted.length;
314
+ dom.setSelectionRange(newPos, newPos);
315
+
316
+ // Update hidden input (bypass our setter to avoid feedback loop)
317
+ this._setHiddenRaw(num);
318
+
319
+ dispatch(dom, 'ln-number:input', { value: num, formatted: formatted });
320
+ };
321
+
322
+ _component.prototype._setHiddenRaw = function (val) {
323
+ if (this._hidden) {
324
+ _inputValueDesc.set.call(this._hidden, String(val));
325
+ }
326
+ };
327
+
328
+ _component.prototype._setDisplayRaw = function (str) {
329
+ if (this.isTextElement) {
330
+ this.dom.textContent = String(str);
331
+ } else {
332
+ _inputValueDesc.set.call(this.dom, String(str));
333
+ }
334
+ };
335
+
336
+ _component.prototype._displayFormatted = function (num) {
337
+ if (this.isTextElement) {
338
+ this._formatTextContent();
339
+ } else {
340
+ this._setDisplayRaw(_formatNum(getLocale(this.dom), num, this.dom.getAttribute('data-ln-number-decimals')));
341
+ }
342
+ };
343
+
344
+ // ─── Public API ───────────────────────────────────────────
345
+
346
+ Object.defineProperty(_component.prototype, 'value', {
347
+ get: function () {
348
+ if (this.isTextElement) {
349
+ return this._rawValue;
350
+ }
351
+ const raw = _inputValueDesc.get.call(this._hidden);
352
+ return raw === '' ? NaN : parseFloat(raw);
353
+ },
354
+ set: function (num) {
355
+ if (this.isTextElement) {
356
+ if (typeof num !== 'number' || isNaN(num)) {
357
+ this._rawValue = null;
358
+ this.dom.textContent = '';
359
+ } else {
360
+ this._rawValue = num;
361
+ this.dom.setAttribute('data-ln-value', String(num));
362
+ this._formatTextContent();
363
+ }
364
+ return;
365
+ }
366
+
367
+ if (typeof num !== 'number' || isNaN(num)) {
368
+ this._setDisplayRaw('');
369
+ this._setHiddenRaw('');
370
+ this.dom.dispatchEvent(new Event('input', { bubbles: true }));
371
+ return;
372
+ }
373
+ this._setHiddenRaw(num);
374
+ this._setDisplayRaw(_formatNum(getLocale(this.dom), num, this.dom.getAttribute('data-ln-number-decimals')));
375
+ this.dom.dispatchEvent(new Event('input', { bubbles: true }));
376
+ }
377
+ });
378
+
379
+ Object.defineProperty(_component.prototype, 'formatted', {
380
+ get: function () {
381
+ if (this.isTextElement) {
382
+ return this.dom.textContent;
383
+ }
384
+ return _inputValueDesc.get.call(this.dom);
385
+ }
386
+ });
387
+
388
+ _component.prototype.destroy = function () {
389
+ if (!this.dom[DOM_ATTRIBUTE]) return;
390
+ if (!this.isTextElement) {
391
+ this.dom.removeEventListener('input', this._onInput);
392
+ this.dom.removeEventListener('paste', this._onPaste);
393
+ if (this._hidden) {
394
+ this.dom.name = this._hidden.name;
395
+ this._hidden.remove();
396
+ }
397
+ this.dom.type = 'number';
398
+ this.dom.removeAttribute('inputmode');
399
+ }
400
+ dispatch(this.dom, 'ln-number:destroyed', { target: this.dom });
401
+ delete this.dom[DOM_ATTRIBUTE];
402
+ };
403
+
404
+ // ─── Locale Observer ──────────────────────────────────────
405
+
406
+ function _localeObserver() {
407
+ new MutationObserver(function () {
408
+ const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
409
+ for (let i = 0; i < els.length; i++) {
410
+ const inst = els[i][DOM_ATTRIBUTE];
411
+ if (inst) {
412
+ if (inst.isTextElement) {
413
+ inst._formatTextContent();
414
+ } else if (!isNaN(inst.value)) {
415
+ inst._displayFormatted(inst.value);
416
+ }
417
+ }
418
+ }
419
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'], subtree: true });
420
+ }
421
+
422
+ // ─── Init ──────────────────────────────────────────────────
423
+
424
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-number', {
425
+ extraAttributes: [
426
+ 'data-ln-value',
427
+ 'data-ln-number-decimals',
428
+ 'data-ln-number-min',
429
+ 'data-ln-number-max',
430
+ 'lang'
431
+ ],
432
+ onAttributeChange: function (el) {
433
+ const inst = el[DOM_ATTRIBUTE];
434
+ if (!inst) return;
435
+ if (inst.isTextElement) {
436
+ inst._initTextElement();
437
+ } else if (!isNaN(inst.value)) {
438
+ inst._displayFormatted(inst.value);
439
+ }
440
+ }
441
+ });
442
+ _localeObserver();
443
+ })();