kmaterialize 2.3.3-1.0.27 → 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.
- package/components/extensions/web/crazy-button.scss +3 -1
- package/components/extensions/web/crazy-button.ts +4 -0
- package/components/otp-input/_otp-input.scss +94 -0
- package/components/otp-input/otp-input.ts +223 -0
- package/dist/css/materialize.colors.min.css +1 -1
- package/dist/css/materialize.css +183 -1
- package/dist/css/materialize.min.css +2 -2
- package/dist/js/materialize.cjs.js +248 -4
- package/dist/js/materialize.d.ts +58 -3
- package/dist/js/materialize.js +248 -4
- package/dist/js/materialize.min.js +3 -3
- package/dist/js/materialize.mjs +248 -5
- package/package.json +1 -1
- package/sass/materialize.scss +2 -0
|
@@ -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,94 @@
|
|
|
1
|
+
.otp-input {
|
|
2
|
+
--otp-slot-size: 48px;
|
|
3
|
+
--otp-slot-gap: 8px;
|
|
4
|
+
--otp-slot-radius: 4px;
|
|
5
|
+
position: relative;
|
|
6
|
+
width: max-content;
|
|
7
|
+
max-width: 100%;
|
|
8
|
+
margin-block: 8px;
|
|
9
|
+
border-radius: var(--otp-slot-radius);
|
|
10
|
+
|
|
11
|
+
// One native control preserves selection, password semantics, autofill and form validation.
|
|
12
|
+
// Its accessible name comes from the original label; decorative slots are aria-hidden.
|
|
13
|
+
> input.otp-input-native {
|
|
14
|
+
position: absolute !important;
|
|
15
|
+
inset: 0 !important;
|
|
16
|
+
width: 100% !important;
|
|
17
|
+
height: 100% !important;
|
|
18
|
+
margin: 0 !important;
|
|
19
|
+
padding: 0 !important;
|
|
20
|
+
opacity: 0;
|
|
21
|
+
font-size: 16px;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
}
|
|
24
|
+
.otp-input-slots { display: flex; flex-wrap: wrap; align-items: center; gap: var(--otp-slot-gap); }
|
|
25
|
+
.otp-input-slot {
|
|
26
|
+
display: inline-grid;
|
|
27
|
+
place-items: center;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
flex: 0 0 var(--otp-slot-size);
|
|
30
|
+
width: var(--otp-slot-size);
|
|
31
|
+
height: calc(var(--otp-slot-size) + 8px);
|
|
32
|
+
border: 1px solid var(--md-sys-color-on-surface-variant);
|
|
33
|
+
border-radius: var(--otp-slot-radius);
|
|
34
|
+
background: transparent;
|
|
35
|
+
color: var(--md-sys-color-on-surface);
|
|
36
|
+
font-size: 24px;
|
|
37
|
+
font-weight: 500;
|
|
38
|
+
font-variant-numeric: tabular-nums;
|
|
39
|
+
line-height: 1;
|
|
40
|
+
transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
|
41
|
+
&.is-selected { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
|
|
42
|
+
&.is-active { border: 2px solid var(--input-color, var(--md-sys-color-primary)); }
|
|
43
|
+
}
|
|
44
|
+
.otp-input-group-start { margin-inline-start: var(--otp-slot-gap); }
|
|
45
|
+
.otp-input-separator { color: var(--md-sys-color-on-surface-variant); white-space: pre; }
|
|
46
|
+
&:hover:not(:has(> input:disabled)) .otp-input-slot:not(.is-active) { border-color: var(--md-sys-color-on-surface); }
|
|
47
|
+
&:has(> input:disabled) { opacity: .38; cursor: not-allowed; }
|
|
48
|
+
&[data-invalid], &:has(> input[aria-invalid='true']), &:has(> input:user-invalid) {
|
|
49
|
+
.otp-input-slot { border-color: var(--md-sys-color-error); }
|
|
50
|
+
.otp-input-slot.is-active { border-width: 2px; }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
@media (max-width: 600px) { .otp-input { --otp-slot-size: 36px; --otp-slot-gap: 4px; } }
|
|
54
|
+
@media (prefers-reduced-motion: reduce) { .otp-input .otp-input-slot { transition: none; } }
|
|
55
|
+
@media (forced-colors: active) {
|
|
56
|
+
.otp-input .otp-input-slot { border-color: ButtonText; }
|
|
57
|
+
.otp-input .otp-input-slot.is-active { outline: 2px solid Highlight; outline-offset: 2px; }
|
|
58
|
+
.otp-input:has(> input:disabled) .otp-input-slot { color: GrayText; border-color: GrayText; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Standard text-field markup: input, label, supporting text. Keep the label
|
|
62
|
+
// above the entire code so it never overlaps an individual slot or separator.
|
|
63
|
+
.input-field:has(> .otp-input) {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
margin-block: 24px;
|
|
68
|
+
> .otp-input { margin: 0; }
|
|
69
|
+
> label {
|
|
70
|
+
position: static;
|
|
71
|
+
order: -1;
|
|
72
|
+
transform: none;
|
|
73
|
+
margin: 0 0 8px;
|
|
74
|
+
padding: 0;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
line-height: 1.4;
|
|
77
|
+
background: transparent;
|
|
78
|
+
}
|
|
79
|
+
> .supporting-text { display: block; padding-inline: 0; }
|
|
80
|
+
&:focus-within > label { color: var(--input-color); }
|
|
81
|
+
&:has(input:disabled) > label { opacity: .38; }
|
|
82
|
+
&.error, &:has(input[aria-invalid='true']), &:has(.otp-input[data-invalid]), &:has(input:user-invalid) {
|
|
83
|
+
> label, > .supporting-text { color: var(--md-sys-color-error); }
|
|
84
|
+
.otp-input-slot { border-color: var(--md-sys-color-error); }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Match the standard text field: omit .outlined for bottom borders only.
|
|
89
|
+
// Set widths separately so focus, validation and forced-color tokens still apply.
|
|
90
|
+
.input-field:not(.outlined) > .otp-input .otp-input-slot {
|
|
91
|
+
border-width: 0 0 1px;
|
|
92
|
+
border-radius: var(--otp-slot-radius) var(--otp-slot-radius) 0 0;
|
|
93
|
+
&.is-active { border-width: 0 0 2px; }
|
|
94
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { MaskitoOptions } from '@maskito/core';
|
|
2
|
+
import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
|
|
3
|
+
import { MaskitoInput } from '../maskito-input/maskito-input';
|
|
4
|
+
|
|
5
|
+
export interface OtpInputOptions extends BaseOptions {
|
|
6
|
+
/** Number of editable slots (1–32); inferred when pattern is supplied. */
|
|
7
|
+
length?: number;
|
|
8
|
+
characters?: 'digits' | 'alphanumeric';
|
|
9
|
+
/** # = digit, A = ASCII letter, * = alphanumeric; backslash escapes literals. */
|
|
10
|
+
pattern?: string;
|
|
11
|
+
/** Maskito processing and overwrite behavior. The slot pattern owns the mask. */
|
|
12
|
+
maskOptions?: Partial<Omit<MaskitoOptions, 'mask'>>;
|
|
13
|
+
/** Visually group slots without adding separators to the submitted value. */
|
|
14
|
+
groupSize?: number;
|
|
15
|
+
/** Hide entered characters, using a native password input. */
|
|
16
|
+
masked?: boolean;
|
|
17
|
+
onComplete?: (value: string, instance: OtpInput) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Slot = { index: number; expression: string; literal?: string };
|
|
21
|
+
const tokenExpressions: Record<string, string> = { '#': '[0-9]', A: '[a-zA-Z]', '*': '[a-zA-Z0-9]' };
|
|
22
|
+
const escapePattern = (value: string) => value.replace(/[\\^$.*+?()[\]{}|/\-]/g, '\\$&');
|
|
23
|
+
|
|
24
|
+
/** A single accessible native input, rendered as Material OTP/PIN slots. Requires @maskito/core. */
|
|
25
|
+
export class OtpInput extends Component<OtpInputOptions> {
|
|
26
|
+
declare el: HTMLInputElement;
|
|
27
|
+
ready: Promise<void>;
|
|
28
|
+
private _mask?: MaskitoInput;
|
|
29
|
+
private _wrapper?: HTMLDivElement;
|
|
30
|
+
private _cells: Array<{ slot: Slot; el: HTMLSpanElement }> = [];
|
|
31
|
+
private _slots: Slot[];
|
|
32
|
+
private _attributes = new Map<string, string | null>();
|
|
33
|
+
private _observer?: MutationObserver;
|
|
34
|
+
private _form: HTMLFormElement | null;
|
|
35
|
+
private _resetTimer?: ReturnType<typeof setTimeout>;
|
|
36
|
+
private _destroyed = false;
|
|
37
|
+
private _lastComplete = '';
|
|
38
|
+
private _hadClass: boolean;
|
|
39
|
+
|
|
40
|
+
constructor(el: HTMLInputElement, options: Partial<OtpInputOptions> = {}) {
|
|
41
|
+
if (!['text', 'tel', 'password'].includes(el.type)) throw new TypeError('OtpInput requires a text, tel, or password input.');
|
|
42
|
+
const settings = { ...OtpInput.defaults, ...OtpInput._markup(el), ...options };
|
|
43
|
+
if (!['digits', 'alphanumeric'].includes(settings.characters!)) throw new TypeError('Unknown OTP character set.');
|
|
44
|
+
if (!Number.isInteger(settings.groupSize) || settings.groupSize! < 0 || settings.groupSize! > 32) throw new TypeError('OTP groupSize must be an integer between 0 and 32.');
|
|
45
|
+
if (settings.length !== undefined && (!Number.isInteger(settings.length) || settings.length < 1 || settings.length > 32)) throw new TypeError('OTP length must be an integer between 1 and 32.');
|
|
46
|
+
const pattern = settings.pattern ?? (settings.characters === 'alphanumeric' ? '*' : '#').repeat(settings.length ?? 6);
|
|
47
|
+
const slots: Slot[] = [];
|
|
48
|
+
let escaped = false;
|
|
49
|
+
for (const char of pattern) {
|
|
50
|
+
if (!escaped && char === '\\') { escaped = true; continue; }
|
|
51
|
+
const expression = !escaped && tokenExpressions[char];
|
|
52
|
+
if (char.length !== 1) throw new TypeError('OTP patterns support single UTF-16 code-unit characters.');
|
|
53
|
+
slots.push({ index: slots.length, expression: expression || escapePattern(char), ...(!expression ? { literal: char } : {}) });
|
|
54
|
+
escaped = false;
|
|
55
|
+
}
|
|
56
|
+
const length = slots.filter(slot => slot.literal === undefined).length;
|
|
57
|
+
if (escaped || length < 1 || length > 32 || slots.length > 64) throw new TypeError('OTP pattern must contain 1–32 editable slots and at most 64 characters, with paired escapes.');
|
|
58
|
+
if (settings.pattern && settings.length !== undefined && settings.length !== length) throw new TypeError('OTP length must match the editable slots in the pattern.');
|
|
59
|
+
// Validate before replacing an existing instance.
|
|
60
|
+
super(el, options, OtpInput);
|
|
61
|
+
this.el['M_OtpInput'] = this;
|
|
62
|
+
this.options = { ...settings, pattern, length };
|
|
63
|
+
this._slots = slots;
|
|
64
|
+
this._form = el.form;
|
|
65
|
+
this._hadClass = el.classList.contains('otp-input-native');
|
|
66
|
+
this.ready = this._setup().catch(error => { this.destroy(); throw error; });
|
|
67
|
+
}
|
|
68
|
+
static get defaults(): OtpInputOptions { return { characters: 'digits', groupSize: 0 }; }
|
|
69
|
+
static init(el: HTMLInputElement, options?: Partial<OtpInputOptions>): OtpInput;
|
|
70
|
+
static init(els: InitElements<HTMLInputElement | MElement>, options?: Partial<OtpInputOptions>): OtpInput[];
|
|
71
|
+
static init(els: HTMLInputElement | InitElements<HTMLInputElement | MElement>, options: Partial<OtpInputOptions> = {}): OtpInput | OtpInput[] {
|
|
72
|
+
return super.init(els, options, OtpInput);
|
|
73
|
+
}
|
|
74
|
+
static getInstance(el: HTMLInputElement): OtpInput { return el['M_OtpInput']; }
|
|
75
|
+
private static _markup(el: HTMLInputElement): Partial<OtpInputOptions> {
|
|
76
|
+
return {
|
|
77
|
+
...(el.dataset.otpLength !== undefined ? { length: Number(el.dataset.otpLength) } : {}),
|
|
78
|
+
...(el.dataset.otpPattern !== undefined ? { pattern: el.dataset.otpPattern } : {}),
|
|
79
|
+
...(el.dataset.otpGroupSize !== undefined ? { groupSize: Number(el.dataset.otpGroupSize) } : {}),
|
|
80
|
+
...(el.dataset.otpCharacters ? { characters: el.dataset.otpCharacters as OtpInputOptions['characters'] } : {}),
|
|
81
|
+
...(el.dataset.otpMasked !== undefined ? { masked: el.dataset.otpMasked !== 'false' } : {})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
private _setAttribute(name: string, value: string) {
|
|
85
|
+
this._attributes.set(name, this.el.getAttribute(name));
|
|
86
|
+
this.el.setAttribute(name, value);
|
|
87
|
+
}
|
|
88
|
+
private async _setup() {
|
|
89
|
+
this._mask = MaskitoInput.init(this.el, {
|
|
90
|
+
preset: 'pattern', pattern: this.options.pattern,
|
|
91
|
+
maskOptions: { overwriteMode: 'replace', ...this.options.maskOptions, mask: this._slots.map(slot => slot.literal ?? new RegExp(slot.expression)) }
|
|
92
|
+
});
|
|
93
|
+
await this._mask.ready;
|
|
94
|
+
if (this._destroyed) return;
|
|
95
|
+
this._setAttribute('maxlength', String(this._slots.length));
|
|
96
|
+
if (!this.el.hasAttribute('pattern')) this._setAttribute('pattern', this._slots.map(slot => slot.expression).join(''));
|
|
97
|
+
if (!this.el.hasAttribute('inputmode')) this._setAttribute('inputmode', this._slots.every(slot => slot.literal !== undefined || slot.expression === '[0-9]') ? 'numeric' : 'text');
|
|
98
|
+
if (this.options.masked !== undefined) this._setAttribute('type', this.options.masked ? 'password' : 'text');
|
|
99
|
+
if (!this.el.hasAttribute('autocomplete')) this._setAttribute('autocomplete', this.el.type === 'password' ? 'off' : 'one-time-code');
|
|
100
|
+
if (!this.el.hasAttribute('spellcheck')) this._setAttribute('spellcheck', 'false');
|
|
101
|
+
if (!this.el.hasAttribute('autocapitalize')) this._setAttribute('autocapitalize', 'off');
|
|
102
|
+
const wrapper = this._wrapper = document.createElement('div');
|
|
103
|
+
wrapper.className = 'otp-input';
|
|
104
|
+
wrapper.dir = 'ltr';
|
|
105
|
+
const cells = document.createElement('div');
|
|
106
|
+
cells.className = 'otp-input-slots';
|
|
107
|
+
cells.setAttribute('aria-hidden', 'true');
|
|
108
|
+
let editableIndex = 0;
|
|
109
|
+
for (const slot of this._slots) {
|
|
110
|
+
const cell = document.createElement('span');
|
|
111
|
+
cell.className = slot.literal !== undefined ? 'otp-input-separator' : 'otp-input-slot';
|
|
112
|
+
if (slot.literal !== undefined) cell.textContent = slot.literal;
|
|
113
|
+
else {
|
|
114
|
+
if (this.options.groupSize && editableIndex && editableIndex % this.options.groupSize === 0) cell.classList.add('otp-input-group-start');
|
|
115
|
+
editableIndex++;
|
|
116
|
+
}
|
|
117
|
+
cells.append(cell);
|
|
118
|
+
this._cells.push({ slot, el: cell });
|
|
119
|
+
}
|
|
120
|
+
this.el.before(wrapper);
|
|
121
|
+
wrapper.append(this.el, cells);
|
|
122
|
+
this.el.classList.add('otp-input-native');
|
|
123
|
+
// Register after Maskito so all displays and completion events see the formatted value.
|
|
124
|
+
this.el.addEventListener('input', this._onInput);
|
|
125
|
+
for (const event of ['focus', 'blur', 'keyup', 'click', 'select']) this.el.addEventListener(event, this._onSelection);
|
|
126
|
+
this.el.addEventListener('invalid', this._onInvalid);
|
|
127
|
+
document.addEventListener('selectionchange', this._onSelection);
|
|
128
|
+
wrapper.addEventListener('pointerdown', this._onPointerDown);
|
|
129
|
+
this._form?.addEventListener('reset', this._onReset);
|
|
130
|
+
this._observer = new MutationObserver(() => this._render(false));
|
|
131
|
+
this._observer.observe(this.el, { attributes: true, attributeFilter: ['type', 'disabled', 'readonly', 'aria-invalid'] });
|
|
132
|
+
this._render(false);
|
|
133
|
+
}
|
|
134
|
+
getValue(): string { return this.el.value; }
|
|
135
|
+
/** Editable characters only; pattern separators are omitted. */
|
|
136
|
+
getUnmaskedValue(): string { return this._slots.filter(slot => slot.literal === undefined).map(slot => this.el.value[slot.index] || '').join(''); }
|
|
137
|
+
isComplete(): boolean { return new RegExp(`^(?:${this._slots.map(slot => slot.expression).join('')})$`).test(this.el.value); }
|
|
138
|
+
async setValue(value: string, emit = true): Promise<void> {
|
|
139
|
+
await this.ready;
|
|
140
|
+
if (this._destroyed) return;
|
|
141
|
+
await this._mask!.setValue(value, emit);
|
|
142
|
+
this._render(false);
|
|
143
|
+
}
|
|
144
|
+
async clear(emit = true): Promise<void> { await this.setValue('', emit); }
|
|
145
|
+
async refresh(): Promise<void> {
|
|
146
|
+
await this.ready;
|
|
147
|
+
if (this._destroyed) return;
|
|
148
|
+
await this._mask!.refresh();
|
|
149
|
+
this._render(false);
|
|
150
|
+
}
|
|
151
|
+
focus(): void { if (!this._destroyed) this.el.focus(); }
|
|
152
|
+
private _onInput = () => {
|
|
153
|
+
if (this._wrapper) this._wrapper.removeAttribute('data-invalid');
|
|
154
|
+
this._render(true);
|
|
155
|
+
};
|
|
156
|
+
private _onSelection = () => { this._render(false); };
|
|
157
|
+
private _onInvalid = () => { this._wrapper?.setAttribute('data-invalid', 'true'); };
|
|
158
|
+
private _onReset = () => {
|
|
159
|
+
clearTimeout(this._resetTimer);
|
|
160
|
+
this._resetTimer = setTimeout(() => {
|
|
161
|
+
this._wrapper?.removeAttribute('data-invalid');
|
|
162
|
+
this._render(false);
|
|
163
|
+
}, 0);
|
|
164
|
+
};
|
|
165
|
+
private _onPointerDown = (event: PointerEvent) => {
|
|
166
|
+
if (event.button !== 0 || this.el.matches(':disabled')) return;
|
|
167
|
+
const cells = this._cells.filter(cell => cell.slot.literal === undefined);
|
|
168
|
+
let nearest = cells[0], distance = Infinity;
|
|
169
|
+
for (const cell of cells) {
|
|
170
|
+
const rect = cell.el.getBoundingClientRect();
|
|
171
|
+
const next = Math.abs(event.clientX - (rect.left + rect.width / 2)) + Math.abs(event.clientY - (rect.top + rect.height / 2));
|
|
172
|
+
if (next < distance) { nearest = cell; distance = next; }
|
|
173
|
+
}
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
this.el.focus();
|
|
176
|
+
const index = Math.min(nearest.slot.index, this.el.value.length);
|
|
177
|
+
this.el.setSelectionRange(index, Math.min(index + 1, this.el.value.length));
|
|
178
|
+
this._render(false);
|
|
179
|
+
};
|
|
180
|
+
private _render(emit: boolean) {
|
|
181
|
+
if (this._destroyed || !this._wrapper) return;
|
|
182
|
+
const focused = document.activeElement === this.el;
|
|
183
|
+
const start = this.el.selectionStart ?? 0, end = this.el.selectionEnd ?? start;
|
|
184
|
+
const editable = this._cells.filter(cell => cell.slot.literal === undefined);
|
|
185
|
+
const active = editable.find(cell => cell.slot.index >= start) ?? editable[editable.length - 1];
|
|
186
|
+
for (const cell of editable) {
|
|
187
|
+
const value = this.el.value[cell.slot.index] || '';
|
|
188
|
+
cell.el.textContent = value ? (this.el.type === 'password' ? '•' : value) : '';
|
|
189
|
+
cell.el.classList.toggle('is-active', focused && cell === active);
|
|
190
|
+
cell.el.classList.toggle('is-selected', focused && cell.slot.index >= start && cell.slot.index < end);
|
|
191
|
+
cell.el.classList.toggle('is-filled', !!value);
|
|
192
|
+
}
|
|
193
|
+
this._wrapper.classList.toggle('is-complete', this.isComplete());
|
|
194
|
+
const complete = this.isComplete() ? this.el.value : '';
|
|
195
|
+
const changed = complete !== this._lastComplete;
|
|
196
|
+
this._lastComplete = complete;
|
|
197
|
+
if (emit && complete && changed) {
|
|
198
|
+
this.el.dispatchEvent(new CustomEvent('otpcomplete', { bubbles: true, detail: { value: complete, unmaskedValue: this.getUnmaskedValue() } }));
|
|
199
|
+
this.options.onComplete?.(complete, this);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
destroy(): void {
|
|
203
|
+
if (this._destroyed) return;
|
|
204
|
+
this._destroyed = true;
|
|
205
|
+
clearTimeout(this._resetTimer);
|
|
206
|
+
this._observer?.disconnect();
|
|
207
|
+
this._mask?.destroy();
|
|
208
|
+
this.el.removeEventListener('input', this._onInput);
|
|
209
|
+
for (const event of ['focus', 'blur', 'keyup', 'click', 'select']) this.el.removeEventListener(event, this._onSelection);
|
|
210
|
+
this.el.removeEventListener('invalid', this._onInvalid);
|
|
211
|
+
document.removeEventListener('selectionchange', this._onSelection);
|
|
212
|
+
this._wrapper?.removeEventListener('pointerdown', this._onPointerDown);
|
|
213
|
+
this._form?.removeEventListener('reset', this._onReset);
|
|
214
|
+
if (this._wrapper?.contains(this.el)) this._wrapper.replaceWith(this.el);
|
|
215
|
+
else this._wrapper?.remove();
|
|
216
|
+
if (!this._hadClass) this.el.classList.remove('otp-input-native');
|
|
217
|
+
for (const [name, value] of this._attributes) {
|
|
218
|
+
if (value === null) this.el.removeAttribute(name);
|
|
219
|
+
else this.el.setAttribute(name, value);
|
|
220
|
+
}
|
|
221
|
+
if (OtpInput.getInstance(this.el) === this) this.el['M_OtpInput'] = undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/css/materialize.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Materialize v2.3.3-1.0.
|
|
2
|
+
* Materialize v2.3.3-1.0.28 (https://materializeweb.com)
|
|
3
3
|
* Copyright 2014-2026 Materialize
|
|
4
4
|
* MIT License (https://raw.githubusercontent.com/materializecss/materialize/master/LICENSE)
|
|
5
5
|
*/
|
|
@@ -21964,6 +21964,14 @@ div.suffix.suffix-tomselect {
|
|
|
21964
21964
|
color: var(--md-sys-color-inverse-surface);
|
|
21965
21965
|
}
|
|
21966
21966
|
|
|
21967
|
+
:where(crazy-button, regular-btn) {
|
|
21968
|
+
cursor: pointer;
|
|
21969
|
+
}
|
|
21970
|
+
|
|
21971
|
+
:where(crazy-button[disabled], regular-btn[disabled]) {
|
|
21972
|
+
cursor: default;
|
|
21973
|
+
}
|
|
21974
|
+
|
|
21967
21975
|
:is(crazy-button, regular-btn) {
|
|
21968
21976
|
display: -webkit-inline-box;
|
|
21969
21977
|
display: -webkit-inline-flex;
|
|
@@ -21974,6 +21982,7 @@ div.suffix.suffix-tomselect {
|
|
|
21974
21982
|
}
|
|
21975
21983
|
:is(crazy-button, regular-btn) > .btn, :is(crazy-button, regular-btn) > .btn-split > .btn {
|
|
21976
21984
|
position: relative;
|
|
21985
|
+
cursor: inherit;
|
|
21977
21986
|
}
|
|
21978
21987
|
:is(crazy-button, regular-btn) .btn > img {
|
|
21979
21988
|
width: var(--button-icon);
|
|
@@ -23333,4 +23342,177 @@ loading-screen-btn {
|
|
|
23333
23342
|
-webkit-transition: none;
|
|
23334
23343
|
transition: none;
|
|
23335
23344
|
}
|
|
23345
|
+
}
|
|
23346
|
+
.otp-input {
|
|
23347
|
+
--otp-slot-size: 48px;
|
|
23348
|
+
--otp-slot-gap: 8px;
|
|
23349
|
+
--otp-slot-radius: 4px;
|
|
23350
|
+
position: relative;
|
|
23351
|
+
width: -webkit-max-content;
|
|
23352
|
+
width: -moz-max-content;
|
|
23353
|
+
width: max-content;
|
|
23354
|
+
max-width: 100%;
|
|
23355
|
+
margin-block: 8px;
|
|
23356
|
+
border-radius: var(--otp-slot-radius);
|
|
23357
|
+
}
|
|
23358
|
+
.otp-input > input.otp-input-native {
|
|
23359
|
+
position: absolute !important;
|
|
23360
|
+
inset: 0 !important;
|
|
23361
|
+
width: 100% !important;
|
|
23362
|
+
height: 100% !important;
|
|
23363
|
+
margin: 0 !important;
|
|
23364
|
+
padding: 0 !important;
|
|
23365
|
+
opacity: 0;
|
|
23366
|
+
font-size: 16px;
|
|
23367
|
+
z-index: 1;
|
|
23368
|
+
}
|
|
23369
|
+
.otp-input .otp-input-slots {
|
|
23370
|
+
display: -webkit-box;
|
|
23371
|
+
display: -webkit-flex;
|
|
23372
|
+
display: -ms-flexbox;
|
|
23373
|
+
display: flex;
|
|
23374
|
+
-webkit-flex-wrap: wrap;
|
|
23375
|
+
-ms-flex-wrap: wrap;
|
|
23376
|
+
flex-wrap: wrap;
|
|
23377
|
+
-webkit-box-align: center;
|
|
23378
|
+
-webkit-align-items: center;
|
|
23379
|
+
-ms-flex-align: center;
|
|
23380
|
+
align-items: center;
|
|
23381
|
+
gap: var(--otp-slot-gap);
|
|
23382
|
+
}
|
|
23383
|
+
.otp-input .otp-input-slot {
|
|
23384
|
+
display: inline-grid;
|
|
23385
|
+
place-items: center;
|
|
23386
|
+
-webkit-box-sizing: border-box;
|
|
23387
|
+
box-sizing: border-box;
|
|
23388
|
+
-webkit-box-flex: 0;
|
|
23389
|
+
-webkit-flex: 0 0 var(--otp-slot-size);
|
|
23390
|
+
-ms-flex: 0 0 var(--otp-slot-size);
|
|
23391
|
+
flex: 0 0 var(--otp-slot-size);
|
|
23392
|
+
width: var(--otp-slot-size);
|
|
23393
|
+
height: calc(var(--otp-slot-size) + 8px);
|
|
23394
|
+
border: 1px solid var(--md-sys-color-on-surface-variant);
|
|
23395
|
+
border-radius: var(--otp-slot-radius);
|
|
23396
|
+
background: transparent;
|
|
23397
|
+
color: var(--md-sys-color-on-surface);
|
|
23398
|
+
font-size: 24px;
|
|
23399
|
+
font-weight: 500;
|
|
23400
|
+
font-variant-numeric: tabular-nums;
|
|
23401
|
+
line-height: 1;
|
|
23402
|
+
-webkit-transition: border-color 120ms ease, background-color 120ms ease, -webkit-box-shadow 120ms ease;
|
|
23403
|
+
transition: border-color 120ms ease, background-color 120ms ease, -webkit-box-shadow 120ms ease;
|
|
23404
|
+
transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
|
23405
|
+
transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease, -webkit-box-shadow 120ms ease;
|
|
23406
|
+
}
|
|
23407
|
+
.otp-input .otp-input-slot.is-selected {
|
|
23408
|
+
background: var(--md-sys-color-primary-container);
|
|
23409
|
+
color: var(--md-sys-color-on-primary-container);
|
|
23410
|
+
}
|
|
23411
|
+
.otp-input .otp-input-slot.is-active {
|
|
23412
|
+
border: 2px solid var(--input-color, var(--md-sys-color-primary));
|
|
23413
|
+
}
|
|
23414
|
+
.otp-input .otp-input-group-start {
|
|
23415
|
+
-webkit-margin-start: var(--otp-slot-gap);
|
|
23416
|
+
-moz-margin-start: var(--otp-slot-gap);
|
|
23417
|
+
margin-inline-start: var(--otp-slot-gap);
|
|
23418
|
+
}
|
|
23419
|
+
.otp-input .otp-input-separator {
|
|
23420
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
23421
|
+
white-space: pre;
|
|
23422
|
+
}
|
|
23423
|
+
.otp-input:hover:not(:has(> input:disabled)) .otp-input-slot:not(.is-active) {
|
|
23424
|
+
border-color: var(--md-sys-color-on-surface);
|
|
23425
|
+
}
|
|
23426
|
+
.otp-input:has(> input:disabled) {
|
|
23427
|
+
opacity: 0.38;
|
|
23428
|
+
cursor: not-allowed;
|
|
23429
|
+
}
|
|
23430
|
+
.otp-input[data-invalid] .otp-input-slot, .otp-input:has(> input[aria-invalid=true]) .otp-input-slot, .otp-input:has(> input:user-invalid) .otp-input-slot {
|
|
23431
|
+
border-color: var(--md-sys-color-error);
|
|
23432
|
+
}
|
|
23433
|
+
.otp-input[data-invalid] .otp-input-slot.is-active, .otp-input:has(> input[aria-invalid=true]) .otp-input-slot.is-active, .otp-input:has(> input:user-invalid) .otp-input-slot.is-active {
|
|
23434
|
+
border-width: 2px;
|
|
23435
|
+
}
|
|
23436
|
+
|
|
23437
|
+
@media (max-width: 600px) {
|
|
23438
|
+
.otp-input {
|
|
23439
|
+
--otp-slot-size: 36px;
|
|
23440
|
+
--otp-slot-gap: 4px;
|
|
23441
|
+
}
|
|
23442
|
+
}
|
|
23443
|
+
@media (prefers-reduced-motion: reduce) {
|
|
23444
|
+
.otp-input .otp-input-slot {
|
|
23445
|
+
-webkit-transition: none;
|
|
23446
|
+
transition: none;
|
|
23447
|
+
}
|
|
23448
|
+
}
|
|
23449
|
+
@media (forced-colors: active) {
|
|
23450
|
+
.otp-input .otp-input-slot {
|
|
23451
|
+
border-color: ButtonText;
|
|
23452
|
+
}
|
|
23453
|
+
.otp-input .otp-input-slot.is-active {
|
|
23454
|
+
outline: 2px solid Highlight;
|
|
23455
|
+
outline-offset: 2px;
|
|
23456
|
+
}
|
|
23457
|
+
.otp-input:has(> input:disabled) .otp-input-slot {
|
|
23458
|
+
color: GrayText;
|
|
23459
|
+
border-color: GrayText;
|
|
23460
|
+
}
|
|
23461
|
+
}
|
|
23462
|
+
.input-field:has(> .otp-input) {
|
|
23463
|
+
display: -webkit-box;
|
|
23464
|
+
display: -webkit-flex;
|
|
23465
|
+
display: -ms-flexbox;
|
|
23466
|
+
display: flex;
|
|
23467
|
+
-webkit-box-orient: vertical;
|
|
23468
|
+
-webkit-box-direction: normal;
|
|
23469
|
+
-webkit-flex-direction: column;
|
|
23470
|
+
-ms-flex-direction: column;
|
|
23471
|
+
flex-direction: column;
|
|
23472
|
+
-webkit-box-align: start;
|
|
23473
|
+
-webkit-align-items: flex-start;
|
|
23474
|
+
-ms-flex-align: start;
|
|
23475
|
+
align-items: flex-start;
|
|
23476
|
+
margin-block: 24px;
|
|
23477
|
+
}
|
|
23478
|
+
.input-field:has(> .otp-input) > .otp-input {
|
|
23479
|
+
margin: 0;
|
|
23480
|
+
}
|
|
23481
|
+
.input-field:has(> .otp-input) > label {
|
|
23482
|
+
position: static;
|
|
23483
|
+
-webkit-box-ordinal-group: 0;
|
|
23484
|
+
-webkit-order: -1;
|
|
23485
|
+
-ms-flex-order: -1;
|
|
23486
|
+
order: -1;
|
|
23487
|
+
-webkit-transform: none;
|
|
23488
|
+
transform: none;
|
|
23489
|
+
margin: 0 0 8px;
|
|
23490
|
+
padding: 0;
|
|
23491
|
+
font-size: 12px;
|
|
23492
|
+
line-height: 1.4;
|
|
23493
|
+
background: transparent;
|
|
23494
|
+
}
|
|
23495
|
+
.input-field:has(> .otp-input) > .supporting-text {
|
|
23496
|
+
display: block;
|
|
23497
|
+
padding-inline: 0;
|
|
23498
|
+
}
|
|
23499
|
+
.input-field:has(> .otp-input):focus-within > label {
|
|
23500
|
+
color: var(--input-color);
|
|
23501
|
+
}
|
|
23502
|
+
.input-field:has(> .otp-input):has(input:disabled) > label {
|
|
23503
|
+
opacity: 0.38;
|
|
23504
|
+
}
|
|
23505
|
+
.input-field:has(> .otp-input).error > label, .input-field:has(> .otp-input).error > .supporting-text, .input-field:has(> .otp-input):has(input[aria-invalid=true]) > label, .input-field:has(> .otp-input):has(input[aria-invalid=true]) > .supporting-text, .input-field:has(> .otp-input):has(.otp-input[data-invalid]) > label, .input-field:has(> .otp-input):has(.otp-input[data-invalid]) > .supporting-text, .input-field:has(> .otp-input):has(input:user-invalid) > label, .input-field:has(> .otp-input):has(input:user-invalid) > .supporting-text {
|
|
23506
|
+
color: var(--md-sys-color-error);
|
|
23507
|
+
}
|
|
23508
|
+
.input-field:has(> .otp-input).error .otp-input-slot, .input-field:has(> .otp-input):has(input[aria-invalid=true]) .otp-input-slot, .input-field:has(> .otp-input):has(.otp-input[data-invalid]) .otp-input-slot, .input-field:has(> .otp-input):has(input:user-invalid) .otp-input-slot {
|
|
23509
|
+
border-color: var(--md-sys-color-error);
|
|
23510
|
+
}
|
|
23511
|
+
|
|
23512
|
+
.input-field:not(.outlined) > .otp-input .otp-input-slot {
|
|
23513
|
+
border-width: 0 0 1px;
|
|
23514
|
+
border-radius: var(--otp-slot-radius) var(--otp-slot-radius) 0 0;
|
|
23515
|
+
}
|
|
23516
|
+
.input-field:not(.outlined) > .otp-input .otp-input-slot.is-active {
|
|
23517
|
+
border-width: 0 0 2px;
|
|
23336
23518
|
}
|