PluginProbe
Booking Calendar / 11.8.4
Booking Calendar v11.8.4
11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 All 204 releases
booking / vendors / imask / esm / controls / input.js

input.js in Booking Calendar 11.8.4, at vendors/imask/esm/controls/input.js

351 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { DIRECTION } from '../core/utils.js';
2 import ActionDetails from '../core/action-details.js';
3 import createMask, { maskedClass } from '../masked/factory.js';
4 import MaskElement from './mask-element.js';
5 import HTMLInputMaskElement from './html-input-mask-element.js';
6 import HTMLContenteditableMaskElement from './html-contenteditable-mask-element.js';
7 import IMask from '../core/holder.js';
8 import InputHistory from './input-history.js';
9 import './html-mask-element.js';
10
11 /** Listens to element events and controls changes between element and {@link Masked} */
12 class InputMask {
13 /**
14 View element
15 */
16
17 /** Internal {@link Masked} model */
18
19 constructor(el, opts) {
20 this.el = el instanceof MaskElement ? el : el.isContentEditable && el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA' ? new HTMLContenteditableMaskElement(el) : new HTMLInputMaskElement(el);
21 this.masked = createMask(opts);
22 this._listeners = {};
23 this._value = '';
24 this._unmaskedValue = '';
25 this._rawInputValue = '';
26 this.history = new InputHistory();
27 this._saveSelection = this._saveSelection.bind(this);
28 this._onInput = this._onInput.bind(this);
29 this._onChange = this._onChange.bind(this);
30 this._onDrop = this._onDrop.bind(this);
31 this._onFocus = this._onFocus.bind(this);
32 this._onClick = this._onClick.bind(this);
33 this._onUndo = this._onUndo.bind(this);
34 this._onRedo = this._onRedo.bind(this);
35 this.alignCursor = this.alignCursor.bind(this);
36 this.alignCursorFriendly = this.alignCursorFriendly.bind(this);
37 this._bindEvents();
38
39 // refresh
40 this.updateValue();
41 this._onChange();
42 }
43 maskEquals(mask) {
44 var _this$masked;
45 return mask == null || ((_this$masked = this.masked) == null ? void 0 : _this$masked.maskEquals(mask));
46 }
47
48 /** Masked */
49 get mask() {
50 return this.masked.mask;
51 }
52 set mask(mask) {
53 if (this.maskEquals(mask)) return;
54 if (!(mask instanceof IMask.Masked) && this.masked.constructor === maskedClass(mask)) {
55 // TODO "any" no idea
56 this.masked.updateOptions({
57 mask
58 });
59 return;
60 }
61 const masked = mask instanceof IMask.Masked ? mask : createMask({
62 mask
63 });
64 masked.unmaskedValue = this.masked.unmaskedValue;
65 this.masked = masked;
66 }
67
68 /** Raw value */
69 get value() {
70 return this._value;
71 }
72 set value(str) {
73 if (this.value === str) return;
74 this.masked.value = str;
75 this.updateControl('auto');
76 }
77
78 /** Unmasked value */
79 get unmaskedValue() {
80 return this._unmaskedValue;
81 }
82 set unmaskedValue(str) {
83 if (this.unmaskedValue === str) return;
84 this.masked.unmaskedValue = str;
85 this.updateControl('auto');
86 }
87
88 /** Raw input value */
89 get rawInputValue() {
90 return this._rawInputValue;
91 }
92 set rawInputValue(str) {
93 if (this.rawInputValue === str) return;
94 this.masked.rawInputValue = str;
95 this.updateControl();
96 this.alignCursor();
97 }
98
99 /** Typed unmasked value */
100 get typedValue() {
101 return this.masked.typedValue;
102 }
103 set typedValue(val) {
104 if (this.masked.typedValueEquals(val)) return;
105 this.masked.typedValue = val;
106 this.updateControl('auto');
107 }
108
109 /** Display value */
110 get displayValue() {
111 return this.masked.displayValue;
112 }
113
114 /** Starts listening to element events */
115 _bindEvents() {
116 this.el.bindEvents({
117 selectionChange: this._saveSelection,
118 input: this._onInput,
119 drop: this._onDrop,
120 click: this._onClick,
121 focus: this._onFocus,
122 commit: this._onChange,
123 undo: this._onUndo,
124 redo: this._onRedo
125 });
126 }
127
128 /** Stops listening to element events */
129 _unbindEvents() {
130 if (this.el) this.el.unbindEvents();
131 }
132
133 /** Fires custom event */
134 _fireEvent(ev, e) {
135 const listeners = this._listeners[ev];
136 if (!listeners) return;
137 listeners.forEach(l => l(e));
138 }
139
140 /** Current selection start */
141 get selectionStart() {
142 return this._cursorChanging ? this._changingCursorPos : this.el.selectionStart;
143 }
144
145 /** Current cursor position */
146 get cursorPos() {
147 return this._cursorChanging ? this._changingCursorPos : this.el.selectionEnd;
148 }
149 set cursorPos(pos) {
150 if (!this.el || !this.el.isActive) return;
151 this.el.select(pos, pos);
152 this._saveSelection();
153 }
154
155 /** Stores current selection */
156 _saveSelection( /* ev */
157 ) {
158 if (this.displayValue !== this.el.value) {
159 console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console
160 }
161 this._selection = {
162 start: this.selectionStart,
163 end: this.cursorPos
164 };
165 }
166
167 /** Syncronizes model value from view */
168 updateValue() {
169 this.masked.value = this.el.value;
170 this._value = this.masked.value;
171 this._unmaskedValue = this.masked.unmaskedValue;
172 this._rawInputValue = this.masked.rawInputValue;
173 }
174
175 /** Syncronizes view from model value, fires change events */
176 updateControl(cursorPos) {
177 const newUnmaskedValue = this.masked.unmaskedValue;
178 const newValue = this.masked.value;
179 const newRawInputValue = this.masked.rawInputValue;
180 const newDisplayValue = this.displayValue;
181 const isChanged = this.unmaskedValue !== newUnmaskedValue || this.value !== newValue || this._rawInputValue !== newRawInputValue;
182 this._unmaskedValue = newUnmaskedValue;
183 this._value = newValue;
184 this._rawInputValue = newRawInputValue;
185 if (this.el.value !== newDisplayValue) this.el.value = newDisplayValue;
186 if (cursorPos === 'auto') this.alignCursor();else if (cursorPos != null) this.cursorPos = cursorPos;
187 if (isChanged) this._fireChangeEvents();
188 if (!this._historyChanging && (isChanged || this.history.isEmpty)) this.history.push({
189 unmaskedValue: newUnmaskedValue,
190 selection: {
191 start: this.selectionStart,
192 end: this.cursorPos
193 }
194 });
195 }
196
197 /** Updates options with deep equal check, recreates {@link Masked} model if mask type changes */
198 updateOptions(opts) {
199 const {
200 mask,
201 ...restOpts
202 } = opts; // TODO types, yes, mask is optional
203
204 const updateMask = !this.maskEquals(mask);
205 const updateOpts = this.masked.optionsIsChanged(restOpts);
206 if (updateMask) this.mask = mask;
207 if (updateOpts) this.masked.updateOptions(restOpts); // TODO
208
209 if (updateMask || updateOpts) this.updateControl();
210 }
211
212 /** Updates cursor */
213 updateCursor(cursorPos) {
214 if (cursorPos == null) return;
215 this.cursorPos = cursorPos;
216
217 // also queue change cursor for mobile browsers
218 this._delayUpdateCursor(cursorPos);
219 }
220
221 /** Delays cursor update to support mobile browsers */
222 _delayUpdateCursor(cursorPos) {
223 this._abortUpdateCursor();
224 this._changingCursorPos = cursorPos;
225 this._cursorChanging = setTimeout(() => {
226 if (!this.el) return; // if was destroyed
227 this.cursorPos = this._changingCursorPos;
228 this._abortUpdateCursor();
229 }, 10);
230 }
231
232 /** Fires custom events */
233 _fireChangeEvents() {
234 this._fireEvent('accept', this._inputEvent);
235 if (this.masked.isComplete) this._fireEvent('complete', this._inputEvent);
236 }
237
238 /** Aborts delayed cursor update */
239 _abortUpdateCursor() {
240 if (this._cursorChanging) {
241 clearTimeout(this._cursorChanging);
242 delete this._cursorChanging;
243 }
244 }
245
246 /** Aligns cursor to nearest available position */
247 alignCursor() {
248 this.cursorPos = this.masked.nearestInputPos(this.masked.nearestInputPos(this.cursorPos, DIRECTION.LEFT));
249 }
250
251 /** Aligns cursor only if selection is empty */
252 alignCursorFriendly() {
253 if (this.selectionStart !== this.cursorPos) return; // skip if range is selected
254 this.alignCursor();
255 }
256
257 /** Adds listener on custom event */
258 on(ev, handler) {
259 if (!this._listeners[ev]) this._listeners[ev] = [];
260 this._listeners[ev].push(handler);
261 return this;
262 }
263
264 /** Removes custom event listener */
265 off(ev, handler) {
266 if (!this._listeners[ev]) return this;
267 if (!handler) {
268 delete this._listeners[ev];
269 return this;
270 }
271 const hIndex = this._listeners[ev].indexOf(handler);
272 if (hIndex >= 0) this._listeners[ev].splice(hIndex, 1);
273 return this;
274 }
275
276 /** Handles view input event */
277 _onInput(e) {
278 this._inputEvent = e;
279 this._abortUpdateCursor();
280 const details = new ActionDetails({
281 // new state
282 value: this.el.value,
283 cursorPos: this.cursorPos,
284 // old state
285 oldValue: this.displayValue,
286 oldSelection: this._selection
287 });
288 const oldRawValue = this.masked.rawInputValue;
289 const offset = this.masked.splice(details.startChangePos, details.removed.length, details.inserted, details.removeDirection, {
290 input: true,
291 raw: true
292 }).offset;
293
294 // force align in remove direction only if no input chars were removed
295 // otherwise we still need to align with NONE (to get out from fixed symbols for instance)
296 const removeDirection = oldRawValue === this.masked.rawInputValue ? details.removeDirection : DIRECTION.NONE;
297 let cursorPos = this.masked.nearestInputPos(details.startChangePos + offset, removeDirection);
298 if (removeDirection !== DIRECTION.NONE) cursorPos = this.masked.nearestInputPos(cursorPos, DIRECTION.NONE);
299 this.updateControl(cursorPos);
300 delete this._inputEvent;
301 }
302
303 /** Handles view change event and commits model value */
304 _onChange() {
305 if (this.displayValue !== this.el.value) this.updateValue();
306 this.masked.doCommit();
307 this.updateControl();
308 this._saveSelection();
309 }
310
311 /** Handles view drop event, prevents by default */
312 _onDrop(ev) {
313 ev.preventDefault();
314 ev.stopPropagation();
315 }
316
317 /** Restore last selection on focus */
318 _onFocus(ev) {
319 this.alignCursorFriendly();
320 }
321
322 /** Restore last selection on focus */
323 _onClick(ev) {
324 this.alignCursorFriendly();
325 }
326 _onUndo() {
327 this._applyHistoryState(this.history.undo());
328 }
329 _onRedo() {
330 this._applyHistoryState(this.history.redo());
331 }
332 _applyHistoryState(state) {
333 if (!state) return;
334 this._historyChanging = true;
335 this.unmaskedValue = state.unmaskedValue;
336 this.el.select(state.selection.start, state.selection.end);
337 this._saveSelection();
338 this._historyChanging = false;
339 }
340
341 /** Unbind view events and removes element reference */
342 destroy() {
343 this._unbindEvents();
344 this._listeners.length = 0;
345 delete this.el;
346 }
347 }
348 IMask.InputMask = InputMask;
349
350 export { InputMask as default };
351