kmaterialize 2.3.3-1.0.26 → 2.3.3-1.0.28

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 (31) hide show
  1. package/components/appbar/navbar-content.scss +317 -0
  2. package/components/appbar/navbar-scroll.scss +101 -0
  3. package/components/appbar/navbar-scroll.ts +67 -0
  4. package/components/dropdown/_dropdown.scss +7 -1
  5. package/components/extensions/web/crazy-button.scss +3 -1
  6. package/components/extensions/web/crazy-button.ts +4 -0
  7. package/components/maskito-input/maskito-input.ts +139 -0
  8. package/components/navigation-drawer/navigation-rail.scss +30 -0
  9. package/components/otp-input/_otp-input.scss +94 -0
  10. package/components/otp-input/otp-input.ts +223 -0
  11. package/components/popup/popup-stepper.scss +79 -0
  12. package/components/popup/popup-stepper.ts +162 -0
  13. package/components/popup/popup.ts +7 -0
  14. package/components/radio-button/_radio-buttons.scss +18 -0
  15. package/components/rich-textarea/_rich-textarea.scss +104 -0
  16. package/components/rich-textarea/rich-textarea.ts +265 -0
  17. package/components/slider/_range.scss +130 -139
  18. package/components/slider/range.ts +162 -188
  19. package/components/switch/_switches.scss +30 -0
  20. package/components/tabs/_tabs.scss +16 -0
  21. package/dist/css/materialize.colors.min.css +1 -1
  22. package/dist/css/materialize.css +1534 -120
  23. package/dist/css/materialize.min.css +2 -2
  24. package/dist/css/materialize.min.css.map +1 -1
  25. package/dist/js/materialize.cjs.js +1427 -498
  26. package/dist/js/materialize.d.ts +218 -29
  27. package/dist/js/materialize.js +1427 -498
  28. package/dist/js/materialize.min.js +3 -3
  29. package/dist/js/materialize.mjs +1424 -499
  30. package/package.json +16 -1
  31. package/sass/materialize.scss +11 -0
@@ -0,0 +1,317 @@
1
+ /* App-bar content geometry and surface variants. */
2
+ .navbar .nav-icon-img {
3
+ display: block;
4
+ height: 24px;
5
+ width: 24px;
6
+ object-fit: contain;
7
+ }
8
+
9
+ .navbar .navbar-search-toolbar {
10
+ width: auto;
11
+ margin: 0 0 0 auto;
12
+ padding: 4px;
13
+ overflow: visible;
14
+ }
15
+
16
+ /* Inset surfaces retain their height at desktop and mobile sizes. */
17
+ .navbar.appbar-bordered.navbar-rounded:not(.nav-extended):not(.navbar-wrap) {
18
+ box-sizing: border-box;
19
+ height: 64px;
20
+ margin: 8px 0;
21
+ padding: 4px;
22
+ border-radius: 16px;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .navbar.appbar-bordered.navbar-rounded:not(.nav-extended):not(.navbar-wrap) > .nav-wrapper {
27
+ box-sizing: border-box;
28
+ flex: 0 0 56px;
29
+ height: 56px;
30
+ border-radius: 11px;
31
+ transform: none;
32
+ }
33
+
34
+ .navbar.appbar-bordered.navbar-full {
35
+ width: 100%;
36
+ margin: 0;
37
+ padding: 0;
38
+ border-radius: 0;
39
+ border-left: 0;
40
+ border-right: 0;
41
+ }
42
+
43
+ /* Dropdown triggers: keep the label and arrow in one predictable hit
44
+ target instead of inheriting the legacy floated-icon offsets. */
45
+ .navbar.appbar-bordered .dropdown-trigger {
46
+ display: inline-flex !important;
47
+ align-items: center;
48
+ justify-content: center;
49
+ gap: 8px;
50
+ width: max-content !important;
51
+ max-width: none;
52
+ height: 40px;
53
+ min-height: 40px;
54
+ padding: 0 14px 0 16px;
55
+ line-height: 1;
56
+ }
57
+
58
+ .navbar.appbar-bordered .dropdown-trigger > .material-icons {
59
+ display: inline-block !important;
60
+ position: static !important;
61
+ float: none !important;
62
+ width: 20px;
63
+ height: 20px;
64
+ margin: 0;
65
+ line-height: 20px;
66
+ transition: transform 160ms ease;
67
+ }
68
+
69
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> .dropdown-trigger) {
70
+ flex: 0 0 auto;
71
+ display: flex;
72
+ align-items: center;
73
+ height: 40px;
74
+ margin: 0;
75
+ padding: 0;
76
+ }
77
+
78
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> .dropdown-trigger) > .dropdown-trigger {
79
+ flex: 0 0 auto;
80
+ align-self: center;
81
+ margin: 0;
82
+ }
83
+
84
+ .navbar.appbar-bordered .dropdown-trigger[aria-expanded="true"] > .material-icons.right {
85
+ transform: rotate(180deg);
86
+ }
87
+
88
+ /* Icon links: center standalone icons inside their 40px hit targets. */
89
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons:only-child) {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ flex: 0 0 40px;
94
+ width: 40px;
95
+ height: 40px;
96
+ margin: 0;
97
+ padding: 0;
98
+ }
99
+
100
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger):has(> .material-icons:only-child) {
101
+ display: inline-grid !important;
102
+ place-items: center;
103
+ box-sizing: border-box;
104
+ width: 40px !important;
105
+ height: 40px;
106
+ min-height: 40px;
107
+ padding: 0 !important;
108
+ line-height: 1;
109
+ }
110
+
111
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons:only-child {
112
+ display: block !important;
113
+ position: static !important;
114
+ float: none !important;
115
+ width: 24px;
116
+ height: 24px;
117
+ margin: 0 !important;
118
+ line-height: 24px;
119
+ text-align: center;
120
+ }
121
+
122
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger):has(> .material-icons.left),
123
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger):has(> .material-icons.right) {
124
+ display: inline-flex !important;
125
+ align-items: center;
126
+ gap: 8px;
127
+ box-sizing: border-box;
128
+ width: max-content !important;
129
+ min-width: 0;
130
+ height: 40px;
131
+ min-height: 40px;
132
+ padding: 0 12px !important;
133
+ line-height: 1;
134
+ white-space: nowrap;
135
+ }
136
+
137
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons.left,
138
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons.right {
139
+ display: block !important;
140
+ position: static !important;
141
+ float: none !important;
142
+ flex: 0 0 24px;
143
+ width: 24px;
144
+ height: 24px;
145
+ margin: 0 !important;
146
+ line-height: 24px;
147
+ text-align: center;
148
+ }
149
+
150
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons.left),
151
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> a:not(.btn):not(.btn-large):not(.btn-floating):not(.dropdown-trigger) > .material-icons.right) {
152
+ flex: 0 0 auto;
153
+ width: max-content;
154
+ height: 40px;
155
+ }
156
+
157
+ /* Button rows: normalize the item row and prevent text-plus-icon buttons
158
+ from being mistaken for standalone icon buttons. */
159
+ .navbar.appbar-bordered .nav-wrapper > ul > li:has(> :is(.btn, .btn-large)) {
160
+ display: flex;
161
+ align-items: center;
162
+ flex: 0 0 auto;
163
+ height: 48px;
164
+ margin: 0;
165
+ padding: 0;
166
+ }
167
+
168
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn,
169
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn-large {
170
+ display: inline-flex !important;
171
+ align-items: center;
172
+ justify-content: center;
173
+ box-sizing: border-box;
174
+ width: max-content !important;
175
+ min-width: 104px;
176
+ height: 40px;
177
+ min-height: 40px;
178
+ margin: 0 !important;
179
+ padding: 0 20px !important;
180
+ line-height: 1;
181
+ white-space: nowrap;
182
+ }
183
+
184
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn-large {
185
+ min-width: 144px;
186
+ height: 48px;
187
+ min-height: 48px;
188
+ padding-inline: 24px !important;
189
+ }
190
+
191
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn:has(> .material-icons),
192
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn-large:has(> .material-icons) {
193
+ gap: 8px;
194
+ }
195
+
196
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn > .material-icons,
197
+ .navbar.appbar-bordered .nav-wrapper > ul > li > a.btn-large > .material-icons {
198
+ display: block !important;
199
+ position: static !important;
200
+ float: none !important;
201
+ flex: 0 0 20px;
202
+ width: 20px;
203
+ height: 20px;
204
+ margin: 0 !important;
205
+ line-height: 20px;
206
+ }
207
+
208
+ /* Extended navbar tabs: use the navbar's readable foreground instead of the
209
+ low-contrast primary token inherited by transparent tabs. */
210
+ .navbar.appbar-bordered .nav-content,
211
+ .navbar.appbar-bordered .nav-content .tabs {
212
+ background: var(--md-sys-color-surface-variant);
213
+ }
214
+
215
+ .navbar.appbar-bordered .nav-content .tabs > .tab > a {
216
+ color: var(--md-sys-color-on-surface) !important;
217
+ opacity: 0.92;
218
+ }
219
+
220
+ .navbar.appbar-bordered .nav-content .tabs > .tab > a.active {
221
+ color: var(--md-sys-color-on-surface) !important;
222
+ opacity: 1;
223
+ font-weight: 600;
224
+ }
225
+
226
+ .navbar.appbar-bordered .nav-content .tabs > .tab.disabled > a {
227
+ color: var(--md-sys-color-on-surface) !important;
228
+ opacity: 0.5;
229
+ }
230
+
231
+ .navbar.appbar-bordered .nav-content .tabs > .indicator {
232
+ background-color: var(--md-sys-color-on-surface) !important;
233
+ }
234
+
235
+ .navbar.appbar-bordered .sidenav-trigger {
236
+ display: inline-flex !important;
237
+ align-items: center;
238
+ justify-content: center;
239
+ flex: 0 0 40px;
240
+ width: 40px;
241
+ height: 40px;
242
+ min-height: 40px;
243
+ margin: 0;
244
+ padding: 0;
245
+ line-height: 1;
246
+ color: var(--md-sys-color-on-surface) !important;
247
+ }
248
+
249
+ .navbar.appbar-bordered .sidenav-trigger > .material-icons {
250
+ display: block !important;
251
+ position: static !important;
252
+ float: none !important;
253
+ width: 24px;
254
+ height: 24px;
255
+ margin: 0 !important;
256
+ line-height: 24px;
257
+ text-align: center;
258
+ }
259
+
260
+ .navbar.appbar-bordered.navbar-wrap > .nav-wrapper {
261
+ flex-wrap: wrap;
262
+ height: auto;
263
+ min-height: 56px;
264
+ padding-bottom: 8px;
265
+ overflow: visible;
266
+ row-gap: 8px;
267
+ }
268
+
269
+ .navbar.appbar-bordered.navbar-wrap {
270
+ height: auto;
271
+ min-height: 56px;
272
+ }
273
+
274
+ .navbar.appbar-bordered.navbar-wrap > .nav-wrapper > ul:not(.dropdown-content) {
275
+ box-sizing: border-box;
276
+ width: 100%;
277
+ max-width: 100%;
278
+ min-width: 0;
279
+ flex: 1 1 100%;
280
+ padding-left: 0;
281
+ height: auto !important;
282
+ flex-wrap: wrap;
283
+ white-space: normal;
284
+ }
285
+
286
+ .navbar.appbar-bordered.navbar-wrap > .nav-wrapper > ul:not(.dropdown-content) > li {
287
+ height: auto !important;
288
+ }
289
+
290
+ .navbar.appbar-bordered ul:not(.dropdown-content) > li > a {
291
+ white-space: nowrap;
292
+ }
293
+
294
+ .navbar.appbar-bordered:not(.navbar-wrap) .nav-wrapper > ul.ml-auto:not(.nav-actions):not(.dropdown-content) {
295
+ flex: 0 0 auto;
296
+ width: max-content;
297
+ max-width: none;
298
+ }
299
+
300
+ /* Separators used as list items need an explicit display/height; otherwise
301
+ navbar item rules can collapse the one-pixel divider. */
302
+ .navbar.appbar-bordered .nav-actions > li.nav-separator {
303
+ display: block !important;
304
+ flex: 0 0 1px;
305
+ width: 1px;
306
+ height: 24px;
307
+ margin: 0 4px;
308
+ padding: 0;
309
+ background: var(--md-sys-color-on-surface-variant);
310
+ opacity: 0.7;
311
+ border: 0;
312
+ visibility: visible !important;
313
+ }
314
+
315
+ @media (prefers-reduced-motion: reduce) {
316
+ .navbar.appbar-bordered .dropdown-trigger > .material-icons { transition: none; }
317
+ }
@@ -0,0 +1,101 @@
1
+ /* Crowded navigation options. Scroll keeps one compact row; wrap grows the
2
+ bar vertically and preserves every item without clipping. */
3
+ .navbar.appbar-bordered.navbar-scroll > .nav-wrapper {
4
+ position: relative;
5
+ flex-wrap: nowrap;
6
+ white-space: nowrap;
7
+ overflow-x: auto;
8
+ overflow-y: hidden;
9
+ }
10
+
11
+ .navbar.appbar-bordered.navbar-scroll > .nav-wrapper > ul:not(.dropdown-content) {
12
+ flex: 0 0 auto;
13
+ width: max-content;
14
+ min-width: max-content;
15
+ }
16
+
17
+ .navbar.appbar-bordered.navbar-scroll {
18
+ position: relative;
19
+ isolation: isolate;
20
+ }
21
+
22
+ .navbar.appbar-bordered.navbar-scroll > .nav-wrapper {
23
+ position: relative;
24
+ z-index: 0;
25
+ }
26
+
27
+ .navbar.appbar-bordered.navbar-scroll::after {
28
+ content: '';
29
+ position: absolute;
30
+ z-index: 20;
31
+ right: 0;
32
+ top: 0;
33
+ width: 15px;
34
+ height: 100%;
35
+ pointer-events: none;
36
+ opacity: var(--scroll-right-fade, 1) !important;
37
+ background: linear-gradient(
38
+ to right,
39
+ transparent,
40
+ var(--md-sys-color-surface-variant)
41
+ );
42
+ transition: opacity 160ms ease;
43
+ }
44
+
45
+ .navbar.appbar-bordered.navbar-scroll::before {
46
+ content: '';
47
+ position: absolute;
48
+ z-index: 20;
49
+ left: 0;
50
+ top: 0;
51
+ width: 15px;
52
+ height: 100%;
53
+ pointer-events: none;
54
+ opacity: var(--scroll-left-fade, 1) !important;
55
+ background: linear-gradient(
56
+ to left,
57
+ transparent,
58
+ var(--md-sys-color-surface-variant)
59
+ );
60
+ transition: opacity 160ms ease;
61
+ }
62
+
63
+ .navbar.appbar-bordered.navbar-scroll:not(.is-scroll-end)::after {
64
+ opacity: var(--scroll-right-fade, 0) !important;
65
+ }
66
+
67
+ .navbar.appbar-bordered.navbar-scroll.is-scroll-start::before {
68
+ opacity: 0;
69
+ }
70
+
71
+ .navbar.appbar-bordered.navbar-scroll:not(.is-scroll-start)::before {
72
+ opacity: var(--scroll-left-fade, 0) !important;
73
+ }
74
+
75
+ /* Fixed edge masks. These are real children of the navbar (not descendants
76
+ of the scrolling row), so they cannot move with the links. */
77
+ .navbar.appbar-bordered.navbar-scroll > .navbar-scroll-fade {
78
+ display: block;
79
+ position: absolute;
80
+ z-index: 1000;
81
+ top: 0;
82
+ width: 28px;
83
+ height: 100%;
84
+ pointer-events: none;
85
+ opacity: 1;
86
+ transition: opacity 160ms ease;
87
+ }
88
+
89
+ .navbar.appbar-bordered.navbar-scroll > .navbar-scroll-fade-left {
90
+ left: 0;
91
+ background: linear-gradient(to left, transparent, var(--md-sys-color-surface-variant));
92
+ }
93
+
94
+ .navbar.appbar-bordered.navbar-scroll > .navbar-scroll-fade-right {
95
+ right: 0;
96
+ background: linear-gradient(to right, transparent, var(--md-sys-color-surface-variant));
97
+ }
98
+
99
+ .navbar.appbar-bordered.navbar-scroll > .navbar-scroll-fade.is-hidden {
100
+ opacity: 0;
101
+ }
@@ -0,0 +1,67 @@
1
+ const instances = new WeakMap<HTMLElement, () => void>();
2
+
3
+ /** Initialize navbar overflow fades. Returns cleanup; safe to reinitialize. */
4
+ export function initNavbarScroll(navbar: HTMLElement): () => void {
5
+ instances.get(navbar)?.();
6
+ const row = navbar.querySelector<HTMLElement>(':scope > .nav-wrapper');
7
+ if (!row) return () => {};
8
+ const left = document.createElement('span');
9
+ const right = document.createElement('span');
10
+ left.className = 'navbar-scroll-fade navbar-scroll-fade-left';
11
+ right.className = 'navbar-scroll-fade navbar-scroll-fade-right';
12
+ left.setAttribute('aria-hidden', 'true');
13
+ right.setAttribute('aria-hidden', 'true');
14
+ navbar.append(left, right);
15
+ const previous = ['--scroll-left-fade', '--scroll-right-fade'].map(name => ({
16
+ name, value: navbar.style.getPropertyValue(name), priority: navbar.style.getPropertyPriority(name)
17
+ }));
18
+ const start = navbar.classList.contains('is-scroll-start');
19
+ const end = navbar.classList.contains('is-scroll-end');
20
+ let disposed = false;
21
+ const update = () => {
22
+ if (disposed) return;
23
+ const max = Math.max(0, row.scrollWidth - row.clientWidth);
24
+ const rtl = getComputedStyle(row).direction === 'rtl';
25
+ const offset = Math.min(max, Math.max(0, rtl ? -row.scrollLeft : row.scrollLeft));
26
+ const atLeft = max <= 1 || (rtl ? offset >= max - 1 : offset <= 1);
27
+ const atRight = max <= 1 || (rtl ? offset <= 1 : offset >= max - 1);
28
+ navbar.classList.toggle('is-scroll-start', atLeft);
29
+ navbar.classList.toggle('is-scroll-end', atRight);
30
+ navbar.style.setProperty('--scroll-left-fade', atLeft ? '0' : '1');
31
+ navbar.style.setProperty('--scroll-right-fade', atRight ? '0' : '1');
32
+ left.classList.toggle('is-hidden', atLeft);
33
+ right.classList.toggle('is-hidden', atRight);
34
+ };
35
+ const resize = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(update);
36
+ const observeSizes = () => {
37
+ resize?.disconnect();
38
+ resize?.observe(row);
39
+ Array.from(row.children).forEach(child => resize?.observe(child));
40
+ update();
41
+ };
42
+ const mutation = new MutationObserver(observeSizes);
43
+ mutation.observe(row, { childList: true, subtree: true, characterData: true });
44
+ row.addEventListener('scroll', update, { passive: true });
45
+ window.addEventListener('resize', update);
46
+ document.fonts?.ready.then(update);
47
+ observeSizes();
48
+ const dispose = () => {
49
+ if (disposed) return;
50
+ disposed = true;
51
+ row.removeEventListener('scroll', update);
52
+ window.removeEventListener('resize', update);
53
+ resize?.disconnect();
54
+ mutation.disconnect();
55
+ left.remove();
56
+ right.remove();
57
+ navbar.classList.toggle('is-scroll-start', start);
58
+ navbar.classList.toggle('is-scroll-end', end);
59
+ previous.forEach(({ name, value, priority }) => {
60
+ if (value) navbar.style.setProperty(name, value, priority);
61
+ else navbar.style.removeProperty(name);
62
+ });
63
+ if (instances.get(navbar) === dispose) instances.delete(navbar);
64
+ };
65
+ instances.set(navbar, dispose);
66
+ return dispose;
67
+ }
@@ -16,6 +16,7 @@
16
16
  display: none;
17
17
  min-width: 100px;
18
18
  border-radius: 12px;
19
+ border: 1px solid var(--md-sys-color-outline-variant);
19
20
  overflow-y: auto;
20
21
  overflow-x: hidden;
21
22
  opacity: 0;
@@ -42,7 +43,7 @@
42
43
  }
43
44
  & > a, & > span {
44
45
  font-size: 16px;
45
- color: var(--md-sys-color-primary);
46
+ color: var(--md-sys-color-on-surface);
46
47
  display: block;
47
48
  line-height: 22px;
48
49
  padding: (($dropdown-item-height - 22) * 0.5) 16px;
@@ -61,6 +62,11 @@
61
62
  width: 24px;
62
63
  }
63
64
  }
65
+ > li.disabled > a,
66
+ > li.disabled > span {
67
+ color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
68
+ }
69
+
64
70
  li:not(.disabled):hover, li.active {
65
71
  background-color: color-mix(in srgb, var(--md-sys-color-surface), var(--md-sys-color-on-surface) 8%);
66
72
  }
@@ -1,8 +1,10 @@
1
+ :where(crazy-button, regular-btn) { cursor: pointer; }
2
+ :where(crazy-button[disabled], regular-btn[disabled]) { cursor: default; }
1
3
  :is(crazy-button, regular-btn) {
2
4
  display: inline-flex;
3
5
  vertical-align: middle;
4
6
  max-width: 100%;
5
- > .btn, > .btn-split > .btn { position: relative; }
7
+ > .btn, > .btn-split > .btn { position: relative; cursor: inherit; }
6
8
  .btn > img { width: var(--button-icon); height: var(--button-icon); object-fit: contain; }
7
9
  // Image-only buttons fill the control and follow its round or square shape.
8
10
  .btn.btn-icon[part="button"] > img {
@@ -27,6 +27,7 @@ export default class CrazyButton extends Kmcomponent {
27
27
  wave: string('light', ['light', 'dark', 'false']),
28
28
  'tooltip-style': string('classic', ['classic', 'material']),
29
29
  'tooltip-position': string('top', ['top', 'right', 'bottom', 'left']),
30
+ cursor: string(),
30
31
  label: string(), 'icon-class': string('material-icons'), 'icon-text': string(),
31
32
  'icon-image': string(), 'icon-image-style': string(),
32
33
  'icon-position': string('right', ['left', 'right']),
@@ -93,6 +94,9 @@ export default class CrazyButton extends Kmcomponent {
93
94
  const a = this.prepareContext().attributes;
94
95
  const button = this.querySelector<HTMLElement>('[part="button"]')!;
95
96
  for (const el of this.querySelectorAll<HTMLElement>('.btn')) {
97
+ // CSSOM validates the value and supports keywords, var(), and image URLs.
98
+ // Invalid/empty values fall back to the host cursor; disabled controls keep their default.
99
+ if (!a.disabled && a.cursor) el.style.cursor = String(a.cursor);
96
100
  this.applyColor(el, String(a['color-primary']), false);
97
101
  this.applyColor(el, String(a['color-secondary']), true);
98
102
  }
@@ -0,0 +1,139 @@
1
+ import type { Maskito, MaskitoOptions } from '@maskito/core';
2
+ import type { MaskitoDateParams, MaskitoNumberParams, MaskitoTimeParams } from '@maskito/kit';
3
+ import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
4
+ import { loadPeer } from '../../src/peer-loader';
5
+ import { NumberInput } from '../number-input/number-input';
6
+
7
+ type MaskitoCore = typeof import('@maskito/core');
8
+ type MaskitoKit = typeof import('@maskito/kit');
9
+ export interface MaskitoInputOptions extends BaseOptions {
10
+ preset?: 'pattern' | 'number' | 'date' | 'time';
11
+ /** # = digit, A = ASCII letter, * = alphanumeric. Backslash escapes literals. */
12
+ pattern?: string;
13
+ number?: MaskitoNumberParams;
14
+ date?: MaskitoDateParams;
15
+ time?: MaskitoTimeParams;
16
+ /** Native Maskito options, including regex/dynamic masks, processors and plugins. */
17
+ maskOptions?: Partial<MaskitoOptions>;
18
+ }
19
+
20
+ /** Optional Maskito enhancement for native text inputs. */
21
+ export class MaskitoInput extends Component<MaskitoInputOptions> {
22
+ declare el: HTMLInputElement;
23
+ mask?: Maskito;
24
+ ready: Promise<void>;
25
+ private _core?: MaskitoCore;
26
+ private _maskOptions?: MaskitoOptions;
27
+ private _destroyed = false;
28
+ private _form: HTMLFormElement | null;
29
+ private _resetTimer?: ReturnType<typeof setTimeout>;
30
+
31
+ constructor(el: HTMLInputElement, options: Partial<MaskitoInputOptions>) {
32
+ if (!['text', 'tel', 'search', 'url', 'password'].includes(el.type)) {
33
+ throw new TypeError('MaskitoInput requires a text, tel, search, url, or password input. Use inputmode for a numeric keyboard.');
34
+ }
35
+ const markup = MaskitoInput._readMarkup(el);
36
+ super(el, options, MaskitoInput);
37
+ this.el['M_MaskitoInput'] = this;
38
+ this.options = { ...MaskitoInput.defaults, ...markup, ...options };
39
+ this._form = el.form;
40
+ this.ready = this._setup().catch(error => { this.destroy(); throw error; });
41
+ }
42
+ static get defaults(): MaskitoInputOptions { return { preset: 'pattern' }; }
43
+ static init(el: HTMLInputElement, options?: Partial<MaskitoInputOptions>): MaskitoInput;
44
+ static init(els: InitElements<HTMLInputElement | MElement>, options?: Partial<MaskitoInputOptions>): MaskitoInput[];
45
+ static init(els: HTMLInputElement | InitElements<HTMLInputElement | MElement>, options: Partial<MaskitoInputOptions> = {}): MaskitoInput | MaskitoInput[] {
46
+ return super.init(els, options, MaskitoInput);
47
+ }
48
+ static getInstance(el: HTMLInputElement): MaskitoInput { return el['M_MaskitoInput']; }
49
+ private static _readMarkup(el: HTMLInputElement): Partial<MaskitoInputOptions> {
50
+ let json: Partial<MaskitoInputOptions> = {};
51
+ if (el.dataset.maskitoOptions) {
52
+ const value = JSON.parse(el.dataset.maskitoOptions);
53
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('data-maskito-options must be a JSON object.');
54
+ json = value;
55
+ }
56
+ return {
57
+ preset: (el.dataset.maskito || 'pattern') as MaskitoInputOptions['preset'],
58
+ pattern: el.dataset.maskitoPattern,
59
+ ...json
60
+ };
61
+ }
62
+ private async _options(): Promise<MaskitoOptions> {
63
+ const custom = this.options.maskOptions;
64
+ let base: MaskitoOptions;
65
+ if (custom?.mask !== undefined) {
66
+ base = { mask: custom.mask };
67
+ } else if (this.options.preset === 'pattern') {
68
+ if (!this.options.pattern) throw new TypeError('Provide a pattern or maskOptions.mask for MaskitoInput.');
69
+ const tokens: Record<string, RegExp> = { '#': /\d/, A: /[a-zA-Z]/, '*': /[a-zA-Z0-9]/ };
70
+ const expression: Array<string | RegExp> = [];
71
+ let escaped = false;
72
+ for (const character of this.options.pattern) {
73
+ if (escaped) { expression.push(character); escaped = false; }
74
+ else if (character === '\\') escaped = true;
75
+ else expression.push(tokens[character] || character);
76
+ }
77
+ if (escaped) throw new TypeError('A Maskito pattern cannot end with an unpaired backslash.');
78
+ base = { mask: expression };
79
+ } else {
80
+ if (!['number', 'date', 'time'].includes(this.options.preset || '')) throw new TypeError(`Unknown MaskitoInput preset: ${this.options.preset}`);
81
+ const kit = await loadPeer<MaskitoKit>({
82
+ specifier: '@maskito/kit', globalName: 'MaskitoKit', feature: 'MaskitoInput number/date/time presets',
83
+ cdnHint: 'Bundle @maskito/kit with your application, or expose its exports as window.MaskitoKit.'
84
+ }, () => import('@maskito/kit'));
85
+ if (this.options.preset === 'number') base = kit.maskitoNumber(this.options.number ?? {});
86
+ else if (this.options.preset === 'date') base = kit.maskitoDate(this.options.date ?? { mode: 'dd/mm/yyyy', separator: '/' });
87
+ else base = kit.maskitoTime(this.options.time ?? { mode: 'HH:MM' });
88
+ }
89
+ return {
90
+ ...base, ...custom,
91
+ preprocessors: [...(custom?.preprocessors || []), ...(base.preprocessors || [])],
92
+ postprocessors: [...(base.postprocessors || []), ...(custom?.postprocessors || [])],
93
+ plugins: [...(base.plugins || []), ...(custom?.plugins || [])]
94
+ };
95
+ }
96
+ private async _setup() {
97
+ const core = await loadPeer<MaskitoCore>({
98
+ specifier: '@maskito/core', globalName: 'MaskitoCore', feature: 'MaskitoInput',
99
+ cdnHint: 'Bundle @maskito/core with your application, or expose its exports as window.MaskitoCore.'
100
+ }, () => import('@maskito/core'));
101
+ if (this._destroyed) return;
102
+ const options = await this._options();
103
+ if (this._destroyed) return;
104
+ // An explicitly selected Maskito input must have only one masking engine.
105
+ NumberInput.getInstance(this.el)?.destroy();
106
+ this._core = core;
107
+ this._maskOptions = options;
108
+ this.el.value = core.maskitoTransform(this.el.value, options);
109
+ this.mask = new core.Maskito(this.el, options);
110
+ this._form?.addEventListener('reset', this._onReset);
111
+ }
112
+ /** Set and format a value, emitting a normal input event by default. */
113
+ async setValue(value: string, emit = true): Promise<void> {
114
+ await this.ready;
115
+ if (this._destroyed) return;
116
+ const formatted = this._core!.maskitoTransform(value, this._maskOptions!);
117
+ if (emit) this._core!.maskitoUpdateElement(this.el, formatted);
118
+ else this.el.value = formatted;
119
+ }
120
+ /** Normalize a value assigned through input.value without emitting input. */
121
+ async refresh(): Promise<void> { await this.setValue(this.el.value, false); }
122
+ /** The native, formatted string. Use Maskito kit parsers for typed values. */
123
+ getValue(): string { return this.el.value; }
124
+ private _onReset = () => {
125
+ clearTimeout(this._resetTimer);
126
+ this._resetTimer = setTimeout(() => {
127
+ if (!this._destroyed) this.el.value = this._core!.maskitoTransform(this.el.value, this._maskOptions!);
128
+ }, 0);
129
+ };
130
+ destroy() {
131
+ if (this._destroyed) return;
132
+ this._destroyed = true;
133
+ clearTimeout(this._resetTimer);
134
+ this._form?.removeEventListener('reset', this._onReset);
135
+ this.mask?.destroy();
136
+ this.mask = undefined;
137
+ if (MaskitoInput.getInstance(this.el) === this) this.el['M_MaskitoInput'] = undefined;
138
+ }
139
+ }