• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uNmAnNeR / imaskjs / 7192372037

13 Dec 2023 08:07AM UTC coverage: 30.682%. Remained the same
7192372037

push

github

uNmAnNeR
up deps

0 of 2 branches covered (0.0%)

Branch coverage included in aggregate %.

27 of 86 relevant lines covered (31.4%)

0.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/packages/imask/src/controls/input.ts
1
import { DIRECTION, type Selection } from '../core/utils';
1✔
2
import ActionDetails from '../core/action-details';
3
import createMask, { type UpdateOpts, maskedClass, type FactoryArg, type FactoryReturnMasked } from '../masked/factory';
4
import Masked from '../masked/base';
5
import MaskElement from './mask-element';
6
import HTMLInputMaskElement, { type InputElement } from './html-input-mask-element';
7
import HTMLContenteditableMaskElement from './html-contenteditable-mask-element';
8
import IMask from '../core/holder';
9

10

11
export
12
type InputMaskElement = MaskElement | InputElement | HTMLElement;
13

14
export
15
type InputMaskEventListener = (e?: InputEvent) => void;
16

17
/** Listens to element events and controls changes between element and {@link Masked} */
18
export default
19
class InputMask<Opts extends FactoryArg=Record<string, unknown>> {
20
  /**
21
    View element
22
  */
23
  declare el: MaskElement;
24

25
  /** Internal {@link Masked} model */
26
  declare masked: FactoryReturnMasked<Opts>;
27

28
  declare _listeners: Record<string, Array<InputMaskEventListener>>;
29
  declare _value: string;
30
  declare _changingCursorPos: number;
31
  declare _unmaskedValue: string;
32
  declare _selection: Selection;
33
  declare _cursorChanging?: ReturnType<typeof setTimeout>;
34
  declare _inputEvent?: InputEvent;
35

36
  constructor (el: InputMaskElement, opts: Opts) {
37
    this.el =
38
      (el instanceof MaskElement) ? el :
39
      (el.isContentEditable && el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') ? new HTMLContenteditableMaskElement(el) :
40
      new HTMLInputMaskElement(el as InputElement);
41

42
    this.masked = createMask(opts);
43

44
    this._listeners = {};
45
    this._value = '';
46
    this._unmaskedValue = '';
47

48
    this._saveSelection = this._saveSelection.bind(this);
49
    this._onInput = this._onInput.bind(this);
50
    this._onChange = this._onChange.bind(this);
51
    this._onDrop = this._onDrop.bind(this);
52
    this._onFocus = this._onFocus.bind(this);
53
    this._onClick = this._onClick.bind(this);
54
    this.alignCursor = this.alignCursor.bind(this);
55
    this.alignCursorFriendly = this.alignCursorFriendly.bind(this);
56

57
    this._bindEvents();
58

59
    // refresh
60
    this.updateValue();
61
    this._onChange();
62
  }
63

64
  maskEquals (mask: any): boolean {
65
    return mask == null || this.masked?.maskEquals(mask);
66
  }
67

68
  /** Masked */
69
  get mask (): FactoryReturnMasked<Opts>['mask'] {
70
    return this.masked.mask;
71
  }
72
  set mask (mask: any) {
73
    if (this.maskEquals(mask)) return;
74

75
    if (!((mask as Masked) instanceof IMask.Masked) && this.masked.constructor === maskedClass(mask as Masked)) {
76
      // TODO "any" no idea
77
      this.masked.updateOptions({ mask } as any);
78
      return;
79
    }
80

81
    const masked = (mask instanceof IMask.Masked ? mask : createMask({ mask } as Opts)) as FactoryReturnMasked<Opts>;
82
    masked.unmaskedValue = this.masked.unmaskedValue;
83
    this.masked = masked;
84
  }
85

86
  /** Raw value */
87
  get value (): string {
88
    return this._value;
89
  }
90

91
  set value (str: string) {
92
    if (this.value === str) return;
93

94
    this.masked.value = str;
95
    this.updateControl();
96
    this.alignCursor();
97
  }
98

99
  /** Unmasked value */
100
  get unmaskedValue (): string {
101
    return this._unmaskedValue;
102
  }
103

104
  set unmaskedValue (str: string) {
105
    if (this.unmaskedValue === str) return;
106

107
    this.masked.unmaskedValue = str;
108
    this.updateControl();
109
    this.alignCursor();
110
  }
111

112
  /** Typed unmasked value */
113
  get typedValue (): FactoryReturnMasked<Opts>['typedValue'] {
114
    return this.masked.typedValue;
115
  }
116

117
  set typedValue (val: FactoryReturnMasked<Opts>['typedValue']) {
118
    if (this.masked.typedValueEquals(val)) return;
119

120
    this.masked.typedValue = val;
121
    this.updateControl();
122
    this.alignCursor();
123
  }
124

125
  /** Display value */
126
  get displayValue (): string {
127
    return this.masked.displayValue;
128
  }
129

130
  /** Starts listening to element events */
131
  _bindEvents () {
132
    this.el.bindEvents({
133
      selectionChange: this._saveSelection,
134
      input: this._onInput,
135
      drop: this._onDrop,
136
      click: this._onClick,
137
      focus: this._onFocus,
138
      commit: this._onChange,
139
    });
140
  }
141

142
  /** Stops listening to element events */
143
  _unbindEvents () {
144
    if (this.el) this.el.unbindEvents();
145
  }
146

147
  /** Fires custom event */
148
  _fireEvent (ev: string, e?: InputEvent) {
149
    const listeners = this._listeners[ev];
150
    if (!listeners) return;
151

152
    listeners.forEach(l => l(e));
153
  }
154

155
  /** Current selection start */
156
  get selectionStart (): number {
157
    return this._cursorChanging ?
158
      this._changingCursorPos :
159

160
      this.el.selectionStart;
161
  }
162

163
  /** Current cursor position */
164
  get cursorPos (): number {
165
    return this._cursorChanging ?
166
      this._changingCursorPos :
167

168
      this.el.selectionEnd;
169
  }
170
  set cursorPos (pos: number) {
171
    if (!this.el || !this.el.isActive) return;
172

173
    this.el.select(pos, pos);
174
    this._saveSelection();
175
  }
176

177
  /** Stores current selection */
178
  _saveSelection (/* ev */) {
179
    if (this.displayValue !== this.el.value) {
180
      console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console
181
    }
182
    this._selection = {
183
      start: this.selectionStart,
184
      end: this.cursorPos,
185
    };
186
  }
187

188
  /** Syncronizes model value from view */
189
  updateValue () {
190
    this.masked.value = this.el.value;
191
    this._value = this.masked.value;
192
  }
193

194
  /** Syncronizes view from model value, fires change events */
195
  updateControl () {
196
    const newUnmaskedValue = this.masked.unmaskedValue;
197
    const newValue = this.masked.value;
198
    const newDisplayValue = this.displayValue;
199
    const isChanged = (this.unmaskedValue !== newUnmaskedValue ||
200
      this.value !== newValue);
201

202
    this._unmaskedValue = newUnmaskedValue;
203
    this._value = newValue;
204

205
    if (this.el.value !== newDisplayValue) this.el.value = newDisplayValue;
206
    if (isChanged) this._fireChangeEvents();
207
  }
208

209
  /** Updates options with deep equal check, recreates {@link Masked} model if mask type changes */
210
  updateOptions(opts: UpdateOpts<Opts>) {
211
    const { mask, ...restOpts } = opts;
212

213
    const updateMask = !this.maskEquals(mask);
214
    const updateOpts = this.masked.optionsIsChanged(restOpts);
215

216
    if (updateMask) this.mask = mask;
217
    if (updateOpts) this.masked.updateOptions(restOpts);  // TODO
218

219
    if (updateMask || updateOpts) this.updateControl();
220
  }
221

222
  /** Updates cursor */
223
  updateCursor (cursorPos: number) {
224
    if (cursorPos == null) return;
225
    this.cursorPos = cursorPos;
226

227
    // also queue change cursor for mobile browsers
228
    this._delayUpdateCursor(cursorPos);
229
  }
230

231
  /** Delays cursor update to support mobile browsers */
232
  _delayUpdateCursor (cursorPos: number) {
233
    this._abortUpdateCursor();
234
    this._changingCursorPos = cursorPos;
235
    this._cursorChanging = setTimeout(() => {
236
      if (!this.el) return; // if was destroyed
237
      this.cursorPos = this._changingCursorPos;
238
      this._abortUpdateCursor();
239
    }, 10);
240
  }
241

242
  /** Fires custom events */
243
  _fireChangeEvents () {
244
    this._fireEvent('accept', this._inputEvent);
245
    if (this.masked.isComplete) this._fireEvent('complete', this._inputEvent);
246
  }
247

248
  /** Aborts delayed cursor update */
249
  _abortUpdateCursor () {
250
    if (this._cursorChanging) {
251
      clearTimeout(this._cursorChanging);
252
      delete this._cursorChanging;
253
    }
254
  }
255

256
  /** Aligns cursor to nearest available position */
257
  alignCursor () {
258
    this.cursorPos = this.masked.nearestInputPos(this.masked.nearestInputPos(this.cursorPos, DIRECTION.LEFT));
259
  }
260

261
  /** Aligns cursor only if selection is empty */
262
  alignCursorFriendly () {
263
    if (this.selectionStart !== this.cursorPos) return;  // skip if range is selected
264
    this.alignCursor();
265
  }
266

267
  /** Adds listener on custom event */
268
  on (ev: string, handler: InputMaskEventListener): this {
269
    if (!this._listeners[ev]) this._listeners[ev] = [];
270
    this._listeners[ev].push(handler);
271
    return this;
272
  }
273

274
  /** Removes custom event listener */
275
  off (ev: string, handler: InputMaskEventListener): this {
276
    if (!this._listeners[ev]) return this;
277
    if (!handler) {
278
      delete this._listeners[ev];
279
      return this;
280
    }
281
    const hIndex = this._listeners[ev].indexOf(handler);
282
    if (hIndex >= 0) this._listeners[ev].splice(hIndex, 1);
283
    return this;
284
  }
285

286
  /** Handles view input event */
287
  _onInput (e: InputEvent): void {
288
    this._inputEvent = e;
289
    this._abortUpdateCursor();
290

291
    const details = new ActionDetails({
292
      // new state
293
      value: this.el.value,
294
      cursorPos: this.cursorPos,
295

296
      // old state
297
      oldValue: this.displayValue,
298
      oldSelection: this._selection,
299
    });
300

301
    const oldRawValue = this.masked.rawInputValue;
302

303
    const offset = this.masked.splice(
304
      details.startChangePos,
305
      details.removed.length,
306
      details.inserted,
307
      details.removeDirection,
308
      { input: true, raw: true },
309
    ).offset;
310

311
    // force align in remove direction only if no input chars were removed
312
    // otherwise we still need to align with NONE (to get out from fixed symbols for instance)
313
    const removeDirection = oldRawValue === this.masked.rawInputValue ?
314
      details.removeDirection :
315
      DIRECTION.NONE;
316

317
    let cursorPos = this.masked.nearestInputPos(
318
      details.startChangePos + offset,
319
      removeDirection,
320
    );
321
    if (removeDirection !== DIRECTION.NONE) cursorPos = this.masked.nearestInputPos(cursorPos, DIRECTION.NONE);
322

323
    this.updateControl();
324
    this.updateCursor(cursorPos);
325
    delete this._inputEvent;
326
  }
327

328
  /** Handles view change event and commits model value */
329
  _onChange () {
330
    if (this.displayValue !== this.el.value) {
331
      this.updateValue();
332
    }
333
    this.masked.doCommit();
334
    this.updateControl();
335
    this._saveSelection();
336
  }
337

338
  /** Handles view drop event, prevents by default */
339
  _onDrop (ev: Event) {
340
    ev.preventDefault();
341
    ev.stopPropagation();
342
  }
343

344
  /** Restore last selection on focus */
345
  _onFocus (ev: Event) {
346
    this.alignCursorFriendly();
347
  }
348

349
  /** Restore last selection on focus */
350
  _onClick (ev: Event) {
351
    this.alignCursorFriendly();
352
  }
353

354
  /** Unbind view events and removes element reference */
355
  destroy () {
356
    this._unbindEvents();
357
    (this._listeners as any).length = 0;
358
    delete (this as any).el;
359
  }
360
}
361

362

363
IMask.InputMask = InputMask;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc