From def28bfe1fbeed3162eab8d28f111de9e4a9234b Mon Sep 17 00:00:00 2001 From: Aliaksei Karzhou Date: Wed, 3 Jul 2024 17:44:28 +0300 Subject: [PATCH] feat: added callback click listeners --- assets/css/index.css | 130 +- assets/css/modals.css | 0 assets/img/icons/thanks.svg | 6 + assets/js/imask.js | 3641 +++++++++++++++++++++++++++++++++++ assets/js/main.js | 13 + assets/js/modals.js | 36 + assets/scss/_l-modal.scss | 130 ++ assets/scss/_l-team.scss | 1 - assets/scss/_m-input.scss | 4 + assets/scss/index.scss | 1 + index.html | 124 +- 11 files changed, 4065 insertions(+), 21 deletions(-) create mode 100644 assets/css/modals.css create mode 100644 assets/img/icons/thanks.svg create mode 100644 assets/js/imask.js create mode 100644 assets/js/modals.js create mode 100644 assets/scss/_l-modal.scss diff --git a/assets/css/index.css b/assets/css/index.css index 75327c8..8d89023 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -462,6 +462,9 @@ h3 { line-height: 140%; } } +.input:focus { + background-color: #609eff; +} .input::-moz-placeholder { color: #ffffff; } @@ -2692,7 +2695,6 @@ h3 { max-height: calc(100vh - 180px); padding-right: 4px; overflow: auto; - /* Handle */ } @media (max-width: 768px) { .team__modal-content { @@ -2935,4 +2937,130 @@ h3 { letter-spacing: 0.01em; text-align: center; color: rgba(255, 255, 255, 0.4); +} + +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9000; + display: none; + align-items: center; + justify-content: center; + background-color: rgba(45, 45, 45, 0.25); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} +.modal.active { + display: flex; +} +.modal__body { + position: relative; + max-width: calc(100% - 30px); + width: 550px; + padding: 40px 60px; + box-sizing: border-box; + border-radius: 60px 10px; + background-color: #609eff; +} +@media (max-width: 768px) { + .modal__body { + padding: 24px 20px; + box-sizing: border-box; + border-radius: 30px 8px; + background-color: #609eff; + } +} +.modal__body::before { + content: ""; + position: absolute; + top: -26px; + right: -26px; + z-index: 1001; + display: block; + width: 28px; + height: 28px; + background: url(../img/icons/close.svg) center no-repeat; + cursor: pointer; +} +@media (max-width: 768px) { + .modal__body::before { + top: -34px; + left: 50%; + right: auto; + transform: translateX(-50%); + } +} +.modal__content { + overflow: auto; +} +.modal__content--thanks { + padding-top: 164px; + background: url(../img/icons/thanks.svg) center top no-repeat; +} +.modal__title { + margin: 0 0 25px; + font-weight: 500; + font-size: 37px; + line-height: 122%; + letter-spacing: 0.01em; + text-align: center; + color: #ffffff; +} +@media (max-width: 768px) { + .modal__title { + font-size: 30px; + } +} +.modal__title--thanks { + margin: 0 0 8px; + font-weight: 700; + font-size: 56px; +} +@media (max-width: 768px) { + .modal__title--thanks { + font-size: 42px; + } +} +.modal__title span { + font-weight: 700; +} +.modal__description { + font-weight: 400; + font-size: 28px; + line-height: 122%; + letter-spacing: 0.01em; + text-align: center; + color: #ffffff; +} +.modal__form { + display: flex; + flex-direction: column; + gap: 25px; +} +@media (max-width: 768px) { + .modal__form { + gap: 12px; + } +} +.modal__form > * { + display: block; + width: 100%; + box-sizing: border-box; +} +.modal__disclaimer { + margin: 27px 0 0; + font-weight: 300; + font-size: 14px; + line-height: 138%; + letter-spacing: 0.01em; + text-align: center; + color: rgba(255, 255, 255, 0.5); +} +@media (max-width: 768px) { + .modal__disclaimer { + font-size: 10px; + } } \ No newline at end of file diff --git a/assets/css/modals.css b/assets/css/modals.css new file mode 100644 index 0000000..e69de29 diff --git a/assets/img/icons/thanks.svg b/assets/img/icons/thanks.svg new file mode 100644 index 0000000..1d17b54 --- /dev/null +++ b/assets/img/icons/thanks.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/js/imask.js b/assets/js/imask.js new file mode 100644 index 0000000..2e8d78c --- /dev/null +++ b/assets/js/imask.js @@ -0,0 +1,3641 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IMask = {})); +})(this, (function (exports) { 'use strict'; + + /** Checks if value is string */ + function isString(str) { + return typeof str === 'string' || str instanceof String; + } + + /** Checks if value is object */ + function isObject(obj) { + var _obj$constructor; + return typeof obj === 'object' && obj != null && (obj == null || (_obj$constructor = obj.constructor) == null ? void 0 : _obj$constructor.name) === 'Object'; + } + function pick(obj, keys) { + if (Array.isArray(keys)) return pick(obj, (_, k) => keys.includes(k)); + return Object.entries(obj).reduce((acc, _ref) => { + let [k, v] = _ref; + if (keys(v, k)) acc[k] = v; + return acc; + }, {}); + } + + /** Direction */ + const DIRECTION = { + NONE: 'NONE', + LEFT: 'LEFT', + FORCE_LEFT: 'FORCE_LEFT', + RIGHT: 'RIGHT', + FORCE_RIGHT: 'FORCE_RIGHT' + }; + + /** Direction */ + + function forceDirection(direction) { + switch (direction) { + case DIRECTION.LEFT: + return DIRECTION.FORCE_LEFT; + case DIRECTION.RIGHT: + return DIRECTION.FORCE_RIGHT; + default: + return direction; + } + } + + /** Escapes regular expression control chars */ + function escapeRegExp(str) { + return str.replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'); + } + + // cloned from https://github.com/epoberezkin/fast-deep-equal with small changes + function objectIncludes(b, a) { + if (a === b) return true; + const arrA = Array.isArray(a), + arrB = Array.isArray(b); + let i; + if (arrA && arrB) { + if (a.length != b.length) return false; + for (i = 0; i < a.length; i++) if (!objectIncludes(a[i], b[i])) return false; + return true; + } + if (arrA != arrB) return false; + if (a && b && typeof a === 'object' && typeof b === 'object') { + const dateA = a instanceof Date, + dateB = b instanceof Date; + if (dateA && dateB) return a.getTime() == b.getTime(); + if (dateA != dateB) return false; + const regexpA = a instanceof RegExp, + regexpB = b instanceof RegExp; + if (regexpA && regexpB) return a.toString() == b.toString(); + if (regexpA != regexpB) return false; + const keys = Object.keys(a); + // if (keys.length !== Object.keys(b).length) return false; + + for (i = 0; i < keys.length; i++) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + for (i = 0; i < keys.length; i++) if (!objectIncludes(b[keys[i]], a[keys[i]])) return false; + return true; + } else if (a && b && typeof a === 'function' && typeof b === 'function') { + return a.toString() === b.toString(); + } + return false; + } + + /** Selection range */ + + /** Provides details of changing input */ + class ActionDetails { + /** Current input value */ + + /** Current cursor position */ + + /** Old input value */ + + /** Old selection */ + + constructor(opts) { + Object.assign(this, opts); + + // double check if left part was changed (autofilling, other non-standard input triggers) + while (this.value.slice(0, this.startChangePos) !== this.oldValue.slice(0, this.startChangePos)) { + --this.oldSelection.start; + } + if (this.insertedCount) { + // double check right part + while (this.value.slice(this.cursorPos) !== this.oldValue.slice(this.oldSelection.end)) { + if (this.value.length - this.cursorPos < this.oldValue.length - this.oldSelection.end) ++this.oldSelection.end;else ++this.cursorPos; + } + } + } + + /** Start changing position */ + get startChangePos() { + return Math.min(this.cursorPos, this.oldSelection.start); + } + + /** Inserted symbols count */ + get insertedCount() { + return this.cursorPos - this.startChangePos; + } + + /** Inserted symbols */ + get inserted() { + return this.value.substr(this.startChangePos, this.insertedCount); + } + + /** Removed symbols count */ + get removedCount() { + // Math.max for opposite operation + return Math.max(this.oldSelection.end - this.startChangePos || + // for Delete + this.oldValue.length - this.value.length, 0); + } + + /** Removed symbols */ + get removed() { + return this.oldValue.substr(this.startChangePos, this.removedCount); + } + + /** Unchanged head symbols */ + get head() { + return this.value.substring(0, this.startChangePos); + } + + /** Unchanged tail symbols */ + get tail() { + return this.value.substring(this.startChangePos + this.insertedCount); + } + + /** Remove direction */ + get removeDirection() { + if (!this.removedCount || this.insertedCount) return DIRECTION.NONE; + + // align right if delete at right + return (this.oldSelection.end === this.cursorPos || this.oldSelection.start === this.cursorPos) && + // if not range removed (event with backspace) + this.oldSelection.end === this.oldSelection.start ? DIRECTION.RIGHT : DIRECTION.LEFT; + } + } + + /** Applies mask on element */ + function IMask(el, opts) { + // currently available only for input-like elements + return new IMask.InputMask(el, opts); + } + + // TODO can't use overloads here because of https://github.com/microsoft/TypeScript/issues/50754 + // export function maskedClass(mask: string): typeof MaskedPattern; + // export function maskedClass(mask: DateConstructor): typeof MaskedDate; + // export function maskedClass(mask: NumberConstructor): typeof MaskedNumber; + // export function maskedClass(mask: Array | ArrayConstructor): typeof MaskedDynamic; + // export function maskedClass(mask: MaskedDate): typeof MaskedDate; + // export function maskedClass(mask: MaskedNumber): typeof MaskedNumber; + // export function maskedClass(mask: MaskedEnum): typeof MaskedEnum; + // export function maskedClass(mask: MaskedRange): typeof MaskedRange; + // export function maskedClass(mask: MaskedRegExp): typeof MaskedRegExp; + // export function maskedClass(mask: MaskedFunction): typeof MaskedFunction; + // export function maskedClass(mask: MaskedPattern): typeof MaskedPattern; + // export function maskedClass(mask: MaskedDynamic): typeof MaskedDynamic; + // export function maskedClass(mask: Masked): typeof Masked; + // export function maskedClass(mask: typeof Masked): typeof Masked; + // export function maskedClass(mask: typeof MaskedDate): typeof MaskedDate; + // export function maskedClass(mask: typeof MaskedNumber): typeof MaskedNumber; + // export function maskedClass(mask: typeof MaskedEnum): typeof MaskedEnum; + // export function maskedClass(mask: typeof MaskedRange): typeof MaskedRange; + // export function maskedClass(mask: typeof MaskedRegExp): typeof MaskedRegExp; + // export function maskedClass(mask: typeof MaskedFunction): typeof MaskedFunction; + // export function maskedClass(mask: typeof MaskedPattern): typeof MaskedPattern; + // export function maskedClass(mask: typeof MaskedDynamic): typeof MaskedDynamic; + // export function maskedClass (mask: Mask): Mask; + // export function maskedClass(mask: RegExp): typeof MaskedRegExp; + // export function maskedClass(mask: (value: string, ...args: any[]) => boolean): typeof MaskedFunction; + + /** Get Masked class by mask type */ + function maskedClass(mask) /* TODO */{ + if (mask == null) throw new Error('mask property should be defined'); + if (mask instanceof RegExp) return IMask.MaskedRegExp; + if (isString(mask)) return IMask.MaskedPattern; + if (mask === Date) return IMask.MaskedDate; + if (mask === Number) return IMask.MaskedNumber; + if (Array.isArray(mask) || mask === Array) return IMask.MaskedDynamic; + if (IMask.Masked && mask.prototype instanceof IMask.Masked) return mask; + if (IMask.Masked && mask instanceof IMask.Masked) return mask.constructor; + if (mask instanceof Function) return IMask.MaskedFunction; + console.warn('Mask not found for mask', mask); // eslint-disable-line no-console + return IMask.Masked; + } + function normalizeOpts(opts) { + if (!opts) throw new Error('Options in not defined'); + if (IMask.Masked) { + if (opts.prototype instanceof IMask.Masked) return { + mask: opts + }; + + /* + handle cases like: + 1) opts = Masked + 2) opts = { mask: Masked, ...instanceOpts } + */ + const { + mask = undefined, + ...instanceOpts + } = opts instanceof IMask.Masked ? { + mask: opts + } : isObject(opts) && opts.mask instanceof IMask.Masked ? opts : {}; + if (mask) { + const _mask = mask.mask; + return { + ...pick(mask, (_, k) => !k.startsWith('_')), + mask: mask.constructor, + _mask, + ...instanceOpts + }; + } + } + if (!isObject(opts)) return { + mask: opts + }; + return { + ...opts + }; + } + + // TODO can't use overloads here because of https://github.com/microsoft/TypeScript/issues/50754 + + // From masked + // export default function createMask (opts: Opts): ReturnMasked; + // // From masked class + // export default function createMask, ReturnMasked extends Masked=InstanceType> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedDate=MaskedDate> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedNumber=MaskedNumber> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedEnum=MaskedEnum> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedRange=MaskedRange> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedRegExp=MaskedRegExp> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedFunction=MaskedFunction> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedPattern=MaskedPattern> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedDynamic=MaskedDynamic> (opts: Opts): ReturnMasked; + // // From mask opts + // export default function createMask, ReturnMasked=Opts extends MaskedOptions ? M : never> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedRegExp=MaskedRegExp> (opts: Opts): ReturnMasked; + // export default function createMask, ReturnMasked extends MaskedFunction=MaskedFunction> (opts: Opts): ReturnMasked; + + /** Creates new {@link Masked} depending on mask type */ + function createMask(opts) { + if (IMask.Masked && opts instanceof IMask.Masked) return opts; + const nOpts = normalizeOpts(opts); + const MaskedClass = maskedClass(nOpts.mask); + if (!MaskedClass) throw new Error("Masked class is not found for provided mask " + nOpts.mask + ", appropriate module needs to be imported manually before creating mask."); + if (nOpts.mask === MaskedClass) delete nOpts.mask; + if (nOpts._mask) { + nOpts.mask = nOpts._mask; + delete nOpts._mask; + } + return new MaskedClass(nOpts); + } + IMask.createMask = createMask; + + /** Generic element API to use with mask */ + class MaskElement { + /** */ + + /** */ + + /** */ + + /** Safely returns selection start */ + get selectionStart() { + let start; + try { + start = this._unsafeSelectionStart; + } catch {} + return start != null ? start : this.value.length; + } + + /** Safely returns selection end */ + get selectionEnd() { + let end; + try { + end = this._unsafeSelectionEnd; + } catch {} + return end != null ? end : this.value.length; + } + + /** Safely sets element selection */ + select(start, end) { + if (start == null || end == null || start === this.selectionStart && end === this.selectionEnd) return; + try { + this._unsafeSelect(start, end); + } catch {} + } + + /** */ + get isActive() { + return false; + } + /** */ + + /** */ + + /** */ + } + IMask.MaskElement = MaskElement; + + const KEY_Z = 90; + const KEY_Y = 89; + + /** Bridge between HTMLElement and {@link Masked} */ + class HTMLMaskElement extends MaskElement { + /** HTMLElement to use mask on */ + + constructor(input) { + super(); + this.input = input; + this._onKeydown = this._onKeydown.bind(this); + this._onInput = this._onInput.bind(this); + this._onBeforeinput = this._onBeforeinput.bind(this); + this._onCompositionEnd = this._onCompositionEnd.bind(this); + } + get rootElement() { + var _this$input$getRootNo, _this$input$getRootNo2, _this$input; + return (_this$input$getRootNo = (_this$input$getRootNo2 = (_this$input = this.input).getRootNode) == null ? void 0 : _this$input$getRootNo2.call(_this$input)) != null ? _this$input$getRootNo : document; + } + + /** Is element in focus */ + get isActive() { + return this.input === this.rootElement.activeElement; + } + + /** Binds HTMLElement events to mask internal events */ + bindEvents(handlers) { + this.input.addEventListener('keydown', this._onKeydown); + this.input.addEventListener('input', this._onInput); + this.input.addEventListener('beforeinput', this._onBeforeinput); + this.input.addEventListener('compositionend', this._onCompositionEnd); + this.input.addEventListener('drop', handlers.drop); + this.input.addEventListener('click', handlers.click); + this.input.addEventListener('focus', handlers.focus); + this.input.addEventListener('blur', handlers.commit); + this._handlers = handlers; + } + _onKeydown(e) { + if (this._handlers.redo && (e.keyCode === KEY_Z && e.shiftKey && (e.metaKey || e.ctrlKey) || e.keyCode === KEY_Y && e.ctrlKey)) { + e.preventDefault(); + return this._handlers.redo(e); + } + if (this._handlers.undo && e.keyCode === KEY_Z && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + return this._handlers.undo(e); + } + if (!e.isComposing) this._handlers.selectionChange(e); + } + _onBeforeinput(e) { + if (e.inputType === 'historyUndo' && this._handlers.undo) { + e.preventDefault(); + return this._handlers.undo(e); + } + if (e.inputType === 'historyRedo' && this._handlers.redo) { + e.preventDefault(); + return this._handlers.redo(e); + } + } + _onCompositionEnd(e) { + this._handlers.input(e); + } + _onInput(e) { + if (!e.isComposing) this._handlers.input(e); + } + + /** Unbinds HTMLElement events to mask internal events */ + unbindEvents() { + this.input.removeEventListener('keydown', this._onKeydown); + this.input.removeEventListener('input', this._onInput); + this.input.removeEventListener('beforeinput', this._onBeforeinput); + this.input.removeEventListener('compositionend', this._onCompositionEnd); + this.input.removeEventListener('drop', this._handlers.drop); + this.input.removeEventListener('click', this._handlers.click); + this.input.removeEventListener('focus', this._handlers.focus); + this.input.removeEventListener('blur', this._handlers.commit); + this._handlers = {}; + } + } + IMask.HTMLMaskElement = HTMLMaskElement; + + /** Bridge between InputElement and {@link Masked} */ + class HTMLInputMaskElement extends HTMLMaskElement { + /** InputElement to use mask on */ + + constructor(input) { + super(input); + this.input = input; + } + + /** Returns InputElement selection start */ + get _unsafeSelectionStart() { + return this.input.selectionStart != null ? this.input.selectionStart : this.value.length; + } + + /** Returns InputElement selection end */ + get _unsafeSelectionEnd() { + return this.input.selectionEnd; + } + + /** Sets InputElement selection */ + _unsafeSelect(start, end) { + this.input.setSelectionRange(start, end); + } + get value() { + return this.input.value; + } + set value(value) { + this.input.value = value; + } + } + IMask.HTMLMaskElement = HTMLMaskElement; + + class HTMLContenteditableMaskElement extends HTMLMaskElement { + /** Returns HTMLElement selection start */ + get _unsafeSelectionStart() { + const root = this.rootElement; + const selection = root.getSelection && root.getSelection(); + const anchorOffset = selection && selection.anchorOffset; + const focusOffset = selection && selection.focusOffset; + if (focusOffset == null || anchorOffset == null || anchorOffset < focusOffset) { + return anchorOffset; + } + return focusOffset; + } + + /** Returns HTMLElement selection end */ + get _unsafeSelectionEnd() { + const root = this.rootElement; + const selection = root.getSelection && root.getSelection(); + const anchorOffset = selection && selection.anchorOffset; + const focusOffset = selection && selection.focusOffset; + if (focusOffset == null || anchorOffset == null || anchorOffset > focusOffset) { + return anchorOffset; + } + return focusOffset; + } + + /** Sets HTMLElement selection */ + _unsafeSelect(start, end) { + if (!this.rootElement.createRange) return; + const range = this.rootElement.createRange(); + range.setStart(this.input.firstChild || this.input, start); + range.setEnd(this.input.lastChild || this.input, end); + const root = this.rootElement; + const selection = root.getSelection && root.getSelection(); + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + } + + /** HTMLElement value */ + get value() { + return this.input.textContent || ''; + } + set value(value) { + this.input.textContent = value; + } + } + IMask.HTMLContenteditableMaskElement = HTMLContenteditableMaskElement; + + class InputHistory { + constructor() { + this.states = []; + this.currentIndex = 0; + } + get currentState() { + return this.states[this.currentIndex]; + } + get isEmpty() { + return this.states.length === 0; + } + push(state) { + // if current index points before the last element then remove the future + if (this.currentIndex < this.states.length - 1) this.states.length = this.currentIndex + 1; + this.states.push(state); + if (this.states.length > InputHistory.MAX_LENGTH) this.states.shift(); + this.currentIndex = this.states.length - 1; + } + go(steps) { + this.currentIndex = Math.min(Math.max(this.currentIndex + steps, 0), this.states.length - 1); + return this.currentState; + } + undo() { + return this.go(-1); + } + redo() { + return this.go(+1); + } + clear() { + this.states.length = 0; + this.currentIndex = 0; + } + } + InputHistory.MAX_LENGTH = 100; + + /** Listens to element events and controls changes between element and {@link Masked} */ + class InputMask { + /** + View element + */ + + /** Internal {@link Masked} model */ + + constructor(el, opts) { + this.el = el instanceof MaskElement ? el : el.isContentEditable && el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA' ? new HTMLContenteditableMaskElement(el) : new HTMLInputMaskElement(el); + this.masked = createMask(opts); + this._listeners = {}; + this._value = ''; + this._unmaskedValue = ''; + this._rawInputValue = ''; + this.history = new InputHistory(); + this._saveSelection = this._saveSelection.bind(this); + this._onInput = this._onInput.bind(this); + this._onChange = this._onChange.bind(this); + this._onDrop = this._onDrop.bind(this); + this._onFocus = this._onFocus.bind(this); + this._onClick = this._onClick.bind(this); + this._onUndo = this._onUndo.bind(this); + this._onRedo = this._onRedo.bind(this); + this.alignCursor = this.alignCursor.bind(this); + this.alignCursorFriendly = this.alignCursorFriendly.bind(this); + this._bindEvents(); + + // refresh + this.updateValue(); + this._onChange(); + } + maskEquals(mask) { + var _this$masked; + return mask == null || ((_this$masked = this.masked) == null ? void 0 : _this$masked.maskEquals(mask)); + } + + /** Masked */ + get mask() { + return this.masked.mask; + } + set mask(mask) { + if (this.maskEquals(mask)) return; + if (!(mask instanceof IMask.Masked) && this.masked.constructor === maskedClass(mask)) { + // TODO "any" no idea + this.masked.updateOptions({ + mask + }); + return; + } + const masked = mask instanceof IMask.Masked ? mask : createMask({ + mask + }); + masked.unmaskedValue = this.masked.unmaskedValue; + this.masked = masked; + } + + /** Raw value */ + get value() { + return this._value; + } + set value(str) { + if (this.value === str) return; + this.masked.value = str; + this.updateControl('auto'); + } + + /** Unmasked value */ + get unmaskedValue() { + return this._unmaskedValue; + } + set unmaskedValue(str) { + if (this.unmaskedValue === str) return; + this.masked.unmaskedValue = str; + this.updateControl('auto'); + } + + /** Raw input value */ + get rawInputValue() { + return this._rawInputValue; + } + set rawInputValue(str) { + if (this.rawInputValue === str) return; + this.masked.rawInputValue = str; + this.updateControl(); + this.alignCursor(); + } + + /** Typed unmasked value */ + get typedValue() { + return this.masked.typedValue; + } + set typedValue(val) { + if (this.masked.typedValueEquals(val)) return; + this.masked.typedValue = val; + this.updateControl('auto'); + } + + /** Display value */ + get displayValue() { + return this.masked.displayValue; + } + + /** Starts listening to element events */ + _bindEvents() { + this.el.bindEvents({ + selectionChange: this._saveSelection, + input: this._onInput, + drop: this._onDrop, + click: this._onClick, + focus: this._onFocus, + commit: this._onChange, + undo: this._onUndo, + redo: this._onRedo + }); + } + + /** Stops listening to element events */ + _unbindEvents() { + if (this.el) this.el.unbindEvents(); + } + + /** Fires custom event */ + _fireEvent(ev, e) { + const listeners = this._listeners[ev]; + if (!listeners) return; + listeners.forEach(l => l(e)); + } + + /** Current selection start */ + get selectionStart() { + return this._cursorChanging ? this._changingCursorPos : this.el.selectionStart; + } + + /** Current cursor position */ + get cursorPos() { + return this._cursorChanging ? this._changingCursorPos : this.el.selectionEnd; + } + set cursorPos(pos) { + if (!this.el || !this.el.isActive) return; + this.el.select(pos, pos); + this._saveSelection(); + } + + /** Stores current selection */ + _saveSelection( /* ev */ + ) { + if (this.displayValue !== this.el.value) { + console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console + } + this._selection = { + start: this.selectionStart, + end: this.cursorPos + }; + } + + /** Syncronizes model value from view */ + updateValue() { + this.masked.value = this.el.value; + this._value = this.masked.value; + this._unmaskedValue = this.masked.unmaskedValue; + this._rawInputValue = this.masked.rawInputValue; + } + + /** Syncronizes view from model value, fires change events */ + updateControl(cursorPos) { + const newUnmaskedValue = this.masked.unmaskedValue; + const newValue = this.masked.value; + const newRawInputValue = this.masked.rawInputValue; + const newDisplayValue = this.displayValue; + const isChanged = this.unmaskedValue !== newUnmaskedValue || this.value !== newValue || this._rawInputValue !== newRawInputValue; + this._unmaskedValue = newUnmaskedValue; + this._value = newValue; + this._rawInputValue = newRawInputValue; + if (this.el.value !== newDisplayValue) this.el.value = newDisplayValue; + if (cursorPos === 'auto') this.alignCursor();else if (cursorPos != null) this.cursorPos = cursorPos; + if (isChanged) this._fireChangeEvents(); + if (!this._historyChanging && (isChanged || this.history.isEmpty)) this.history.push({ + unmaskedValue: newUnmaskedValue, + selection: { + start: this.selectionStart, + end: this.cursorPos + } + }); + } + + /** Updates options with deep equal check, recreates {@link Masked} model if mask type changes */ + updateOptions(opts) { + const { + mask, + ...restOpts + } = opts; // TODO types, yes, mask is optional + + const updateMask = !this.maskEquals(mask); + const updateOpts = this.masked.optionsIsChanged(restOpts); + if (updateMask) this.mask = mask; + if (updateOpts) this.masked.updateOptions(restOpts); // TODO + + if (updateMask || updateOpts) this.updateControl(); + } + + /** Updates cursor */ + updateCursor(cursorPos) { + if (cursorPos == null) return; + this.cursorPos = cursorPos; + + // also queue change cursor for mobile browsers + this._delayUpdateCursor(cursorPos); + } + + /** Delays cursor update to support mobile browsers */ + _delayUpdateCursor(cursorPos) { + this._abortUpdateCursor(); + this._changingCursorPos = cursorPos; + this._cursorChanging = setTimeout(() => { + if (!this.el) return; // if was destroyed + this.cursorPos = this._changingCursorPos; + this._abortUpdateCursor(); + }, 10); + } + + /** Fires custom events */ + _fireChangeEvents() { + this._fireEvent('accept', this._inputEvent); + if (this.masked.isComplete) this._fireEvent('complete', this._inputEvent); + } + + /** Aborts delayed cursor update */ + _abortUpdateCursor() { + if (this._cursorChanging) { + clearTimeout(this._cursorChanging); + delete this._cursorChanging; + } + } + + /** Aligns cursor to nearest available position */ + alignCursor() { + this.cursorPos = this.masked.nearestInputPos(this.masked.nearestInputPos(this.cursorPos, DIRECTION.LEFT)); + } + + /** Aligns cursor only if selection is empty */ + alignCursorFriendly() { + if (this.selectionStart !== this.cursorPos) return; // skip if range is selected + this.alignCursor(); + } + + /** Adds listener on custom event */ + on(ev, handler) { + if (!this._listeners[ev]) this._listeners[ev] = []; + this._listeners[ev].push(handler); + return this; + } + + /** Removes custom event listener */ + off(ev, handler) { + if (!this._listeners[ev]) return this; + if (!handler) { + delete this._listeners[ev]; + return this; + } + const hIndex = this._listeners[ev].indexOf(handler); + if (hIndex >= 0) this._listeners[ev].splice(hIndex, 1); + return this; + } + + /** Handles view input event */ + _onInput(e) { + this._inputEvent = e; + this._abortUpdateCursor(); + const details = new ActionDetails({ + // new state + value: this.el.value, + cursorPos: this.cursorPos, + // old state + oldValue: this.displayValue, + oldSelection: this._selection + }); + const oldRawValue = this.masked.rawInputValue; + const offset = this.masked.splice(details.startChangePos, details.removed.length, details.inserted, details.removeDirection, { + input: true, + raw: true + }).offset; + + // force align in remove direction only if no input chars were removed + // otherwise we still need to align with NONE (to get out from fixed symbols for instance) + const removeDirection = oldRawValue === this.masked.rawInputValue ? details.removeDirection : DIRECTION.NONE; + let cursorPos = this.masked.nearestInputPos(details.startChangePos + offset, removeDirection); + if (removeDirection !== DIRECTION.NONE) cursorPos = this.masked.nearestInputPos(cursorPos, DIRECTION.NONE); + this.updateControl(cursorPos); + delete this._inputEvent; + } + + /** Handles view change event and commits model value */ + _onChange() { + if (this.displayValue !== this.el.value) this.updateValue(); + this.masked.doCommit(); + this.updateControl(); + this._saveSelection(); + } + + /** Handles view drop event, prevents by default */ + _onDrop(ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + + /** Restore last selection on focus */ + _onFocus(ev) { + this.alignCursorFriendly(); + } + + /** Restore last selection on focus */ + _onClick(ev) { + this.alignCursorFriendly(); + } + _onUndo() { + this._applyHistoryState(this.history.undo()); + } + _onRedo() { + this._applyHistoryState(this.history.redo()); + } + _applyHistoryState(state) { + if (!state) return; + this._historyChanging = true; + this.unmaskedValue = state.unmaskedValue; + this.el.select(state.selection.start, state.selection.end); + this._saveSelection(); + this._historyChanging = false; + } + + /** Unbind view events and removes element reference */ + destroy() { + this._unbindEvents(); + this._listeners.length = 0; + delete this.el; + } + } + IMask.InputMask = InputMask; + + /** Provides details of changing model value */ + class ChangeDetails { + /** Inserted symbols */ + + /** Additional offset if any changes occurred before tail */ + + /** Raw inserted is used by dynamic mask */ + + /** Can skip chars */ + + static normalize(prep) { + return Array.isArray(prep) ? prep : [prep, new ChangeDetails()]; + } + constructor(details) { + Object.assign(this, { + inserted: '', + rawInserted: '', + tailShift: 0, + skip: false + }, details); + } + + /** Aggregate changes */ + aggregate(details) { + this.inserted += details.inserted; + this.rawInserted += details.rawInserted; + this.tailShift += details.tailShift; + this.skip = this.skip || details.skip; + return this; + } + + /** Total offset considering all changes */ + get offset() { + return this.tailShift + this.inserted.length; + } + get consumed() { + return Boolean(this.rawInserted) || this.skip; + } + equals(details) { + return this.inserted === details.inserted && this.tailShift === details.tailShift && this.rawInserted === details.rawInserted && this.skip === details.skip; + } + } + IMask.ChangeDetails = ChangeDetails; + + /** Provides details of continuous extracted tail */ + class ContinuousTailDetails { + /** Tail value as string */ + + /** Tail start position */ + + /** Start position */ + + constructor(value, from, stop) { + if (value === void 0) { + value = ''; + } + if (from === void 0) { + from = 0; + } + this.value = value; + this.from = from; + this.stop = stop; + } + toString() { + return this.value; + } + extend(tail) { + this.value += String(tail); + } + appendTo(masked) { + return masked.append(this.toString(), { + tail: true + }).aggregate(masked._appendPlaceholder()); + } + get state() { + return { + value: this.value, + from: this.from, + stop: this.stop + }; + } + set state(state) { + Object.assign(this, state); + } + unshift(beforePos) { + if (!this.value.length || beforePos != null && this.from >= beforePos) return ''; + const shiftChar = this.value[0]; + this.value = this.value.slice(1); + return shiftChar; + } + shift() { + if (!this.value.length) return ''; + const shiftChar = this.value[this.value.length - 1]; + this.value = this.value.slice(0, -1); + return shiftChar; + } + } + + /** Append flags */ + + /** Extract flags */ + + // see https://github.com/microsoft/TypeScript/issues/6223 + + /** Provides common masking stuff */ + class Masked { + /** */ + + /** */ + + /** Transforms value before mask processing */ + + /** Transforms each char before mask processing */ + + /** Validates if value is acceptable */ + + /** Does additional processing at the end of editing */ + + /** Format typed value to string */ + + /** Parse string to get typed value */ + + /** Enable characters overwriting */ + + /** */ + + /** */ + + /** */ + + /** */ + + constructor(opts) { + this._value = ''; + this._update({ + ...Masked.DEFAULTS, + ...opts + }); + this._initialized = true; + } + + /** Sets and applies new options */ + updateOptions(opts) { + if (!this.optionsIsChanged(opts)) return; + this.withValueRefresh(this._update.bind(this, opts)); + } + + /** Sets new options */ + _update(opts) { + Object.assign(this, opts); + } + + /** Mask state */ + get state() { + return { + _value: this.value, + _rawInputValue: this.rawInputValue + }; + } + set state(state) { + this._value = state._value; + } + + /** Resets value */ + reset() { + this._value = ''; + } + get value() { + return this._value; + } + set value(value) { + this.resolve(value, { + input: true + }); + } + + /** Resolve new value */ + resolve(value, flags) { + if (flags === void 0) { + flags = { + input: true + }; + } + this.reset(); + this.append(value, flags, ''); + this.doCommit(); + } + get unmaskedValue() { + return this.value; + } + set unmaskedValue(value) { + this.resolve(value, {}); + } + get typedValue() { + return this.parse ? this.parse(this.value, this) : this.unmaskedValue; + } + set typedValue(value) { + if (this.format) { + this.value = this.format(value, this); + } else { + this.unmaskedValue = String(value); + } + } + + /** Value that includes raw user input */ + get rawInputValue() { + return this.extractInput(0, this.displayValue.length, { + raw: true + }); + } + set rawInputValue(value) { + this.resolve(value, { + raw: true + }); + } + get displayValue() { + return this.value; + } + get isComplete() { + return true; + } + get isFilled() { + return this.isComplete; + } + + /** Finds nearest input position in direction */ + nearestInputPos(cursorPos, direction) { + return cursorPos; + } + totalInputPositions(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + return Math.min(this.displayValue.length, toPos - fromPos); + } + + /** Extracts value in range considering flags */ + extractInput(fromPos, toPos, flags) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + return this.displayValue.slice(fromPos, toPos); + } + + /** Extracts tail in range */ + extractTail(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + return new ContinuousTailDetails(this.extractInput(fromPos, toPos), fromPos); + } + + /** Appends tail */ + appendTail(tail) { + if (isString(tail)) tail = new ContinuousTailDetails(String(tail)); + return tail.appendTo(this); + } + + /** Appends char */ + _appendCharRaw(ch, flags) { + if (!ch) return new ChangeDetails(); + this._value += ch; + return new ChangeDetails({ + inserted: ch, + rawInserted: ch + }); + } + + /** Appends char */ + _appendChar(ch, flags, checkTail) { + if (flags === void 0) { + flags = {}; + } + const consistentState = this.state; + let details; + [ch, details] = this.doPrepareChar(ch, flags); + if (ch) { + details = details.aggregate(this._appendCharRaw(ch, flags)); + + // TODO handle `skip`? + + // try `autofix` lookahead + if (!details.rawInserted && this.autofix === 'pad') { + const noFixState = this.state; + this.state = consistentState; + let fixDetails = this.pad(flags); + const chDetails = this._appendCharRaw(ch, flags); + fixDetails = fixDetails.aggregate(chDetails); + + // if fix was applied or + // if details are equal use skip restoring state optimization + if (chDetails.rawInserted || fixDetails.equals(details)) { + details = fixDetails; + } else { + this.state = noFixState; + } + } + } + if (details.inserted) { + let consistentTail; + let appended = this.doValidate(flags) !== false; + if (appended && checkTail != null) { + // validation ok, check tail + const beforeTailState = this.state; + if (this.overwrite === true) { + consistentTail = checkTail.state; + for (let i = 0; i < details.rawInserted.length; ++i) { + checkTail.unshift(this.displayValue.length - details.tailShift); + } + } + let tailDetails = this.appendTail(checkTail); + appended = tailDetails.rawInserted.length === checkTail.toString().length; + + // not ok, try shift + if (!(appended && tailDetails.inserted) && this.overwrite === 'shift') { + this.state = beforeTailState; + consistentTail = checkTail.state; + for (let i = 0; i < details.rawInserted.length; ++i) { + checkTail.shift(); + } + tailDetails = this.appendTail(checkTail); + appended = tailDetails.rawInserted.length === checkTail.toString().length; + } + + // if ok, rollback state after tail + if (appended && tailDetails.inserted) this.state = beforeTailState; + } + + // revert all if something went wrong + if (!appended) { + details = new ChangeDetails(); + this.state = consistentState; + if (checkTail && consistentTail) checkTail.state = consistentTail; + } + } + return details; + } + + /** Appends optional placeholder at the end */ + _appendPlaceholder() { + return new ChangeDetails(); + } + + /** Appends optional eager placeholder at the end */ + _appendEager() { + return new ChangeDetails(); + } + + /** Appends symbols considering flags */ + append(str, flags, tail) { + if (!isString(str)) throw new Error('value should be string'); + const checkTail = isString(tail) ? new ContinuousTailDetails(String(tail)) : tail; + if (flags != null && flags.tail) flags._beforeTailState = this.state; + let details; + [str, details] = this.doPrepare(str, flags); + for (let ci = 0; ci < str.length; ++ci) { + const d = this._appendChar(str[ci], flags, checkTail); + if (!d.rawInserted && !this.doSkipInvalid(str[ci], flags, checkTail)) break; + details.aggregate(d); + } + if ((this.eager === true || this.eager === 'append') && flags != null && flags.input && str) { + details.aggregate(this._appendEager()); + } + + // append tail but aggregate only tailShift + if (checkTail != null) { + details.tailShift += this.appendTail(checkTail).tailShift; + // TODO it's a good idea to clear state after appending ends + // but it causes bugs when one append calls another (when dynamic dispatch set rawInputValue) + // this._resetBeforeTailState(); + } + return details; + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + this._value = this.displayValue.slice(0, fromPos) + this.displayValue.slice(toPos); + return new ChangeDetails(); + } + + /** Calls function and reapplies current value */ + withValueRefresh(fn) { + if (this._refreshing || !this._initialized) return fn(); + this._refreshing = true; + const rawInput = this.rawInputValue; + const value = this.value; + const ret = fn(); + this.rawInputValue = rawInput; + // append lost trailing chars at the end + if (this.value && this.value !== value && value.indexOf(this.value) === 0) { + this.append(value.slice(this.displayValue.length), {}, ''); + this.doCommit(); + } + delete this._refreshing; + return ret; + } + runIsolated(fn) { + if (this._isolated || !this._initialized) return fn(this); + this._isolated = true; + const state = this.state; + const ret = fn(this); + this.state = state; + delete this._isolated; + return ret; + } + doSkipInvalid(ch, flags, checkTail) { + return Boolean(this.skipInvalid); + } + + /** Prepares string before mask processing */ + doPrepare(str, flags) { + if (flags === void 0) { + flags = {}; + } + return ChangeDetails.normalize(this.prepare ? this.prepare(str, this, flags) : str); + } + + /** Prepares each char before mask processing */ + doPrepareChar(str, flags) { + if (flags === void 0) { + flags = {}; + } + return ChangeDetails.normalize(this.prepareChar ? this.prepareChar(str, this, flags) : str); + } + + /** Validates if value is acceptable */ + doValidate(flags) { + return (!this.validate || this.validate(this.value, this, flags)) && (!this.parent || this.parent.doValidate(flags)); + } + + /** Does additional processing at the end of editing */ + doCommit() { + if (this.commit) this.commit(this.value, this); + } + splice(start, deleteCount, inserted, removeDirection, flags) { + if (inserted === void 0) { + inserted = ''; + } + if (removeDirection === void 0) { + removeDirection = DIRECTION.NONE; + } + if (flags === void 0) { + flags = { + input: true + }; + } + const tailPos = start + deleteCount; + const tail = this.extractTail(tailPos); + const eagerRemove = this.eager === true || this.eager === 'remove'; + let oldRawValue; + if (eagerRemove) { + removeDirection = forceDirection(removeDirection); + oldRawValue = this.extractInput(0, tailPos, { + raw: true + }); + } + let startChangePos = start; + const details = new ChangeDetails(); + + // if it is just deletion without insertion + if (removeDirection !== DIRECTION.NONE) { + startChangePos = this.nearestInputPos(start, deleteCount > 1 && start !== 0 && !eagerRemove ? DIRECTION.NONE : removeDirection); + + // adjust tailShift if start was aligned + details.tailShift = startChangePos - start; + } + details.aggregate(this.remove(startChangePos)); + if (eagerRemove && removeDirection !== DIRECTION.NONE && oldRawValue === this.rawInputValue) { + if (removeDirection === DIRECTION.FORCE_LEFT) { + let valLength; + while (oldRawValue === this.rawInputValue && (valLength = this.displayValue.length)) { + details.aggregate(new ChangeDetails({ + tailShift: -1 + })).aggregate(this.remove(valLength - 1)); + } + } else if (removeDirection === DIRECTION.FORCE_RIGHT) { + tail.unshift(); + } + } + return details.aggregate(this.append(inserted, flags, tail)); + } + maskEquals(mask) { + return this.mask === mask; + } + optionsIsChanged(opts) { + return !objectIncludes(this, opts); + } + typedValueEquals(value) { + const tval = this.typedValue; + return value === tval || Masked.EMPTY_VALUES.includes(value) && Masked.EMPTY_VALUES.includes(tval) || (this.format ? this.format(value, this) === this.format(this.typedValue, this) : false); + } + pad(flags) { + return new ChangeDetails(); + } + } + Masked.DEFAULTS = { + skipInvalid: true + }; + Masked.EMPTY_VALUES = [undefined, null, '']; + IMask.Masked = Masked; + + class ChunksTailDetails { + /** */ + + constructor(chunks, from) { + if (chunks === void 0) { + chunks = []; + } + if (from === void 0) { + from = 0; + } + this.chunks = chunks; + this.from = from; + } + toString() { + return this.chunks.map(String).join(''); + } + extend(tailChunk) { + if (!String(tailChunk)) return; + tailChunk = isString(tailChunk) ? new ContinuousTailDetails(String(tailChunk)) : tailChunk; + const lastChunk = this.chunks[this.chunks.length - 1]; + const extendLast = lastChunk && ( + // if stops are same or tail has no stop + lastChunk.stop === tailChunk.stop || tailChunk.stop == null) && + // if tail chunk goes just after last chunk + tailChunk.from === lastChunk.from + lastChunk.toString().length; + if (tailChunk instanceof ContinuousTailDetails) { + // check the ability to extend previous chunk + if (extendLast) { + // extend previous chunk + lastChunk.extend(tailChunk.toString()); + } else { + // append new chunk + this.chunks.push(tailChunk); + } + } else if (tailChunk instanceof ChunksTailDetails) { + if (tailChunk.stop == null) { + // unwrap floating chunks to parent, keeping `from` pos + let firstTailChunk; + while (tailChunk.chunks.length && tailChunk.chunks[0].stop == null) { + firstTailChunk = tailChunk.chunks.shift(); // not possible to be `undefined` because length was checked above + firstTailChunk.from += tailChunk.from; + this.extend(firstTailChunk); + } + } + + // if tail chunk still has value + if (tailChunk.toString()) { + // if chunks contains stops, then popup stop to container + tailChunk.stop = tailChunk.blockIndex; + this.chunks.push(tailChunk); + } + } + } + appendTo(masked) { + if (!(masked instanceof IMask.MaskedPattern)) { + const tail = new ContinuousTailDetails(this.toString()); + return tail.appendTo(masked); + } + const details = new ChangeDetails(); + for (let ci = 0; ci < this.chunks.length; ++ci) { + const chunk = this.chunks[ci]; + const lastBlockIter = masked._mapPosToBlock(masked.displayValue.length); + const stop = chunk.stop; + let chunkBlock; + if (stop != null && ( + // if block not found or stop is behind lastBlock + !lastBlockIter || lastBlockIter.index <= stop)) { + if (chunk instanceof ChunksTailDetails || + // for continuous block also check if stop is exist + masked._stops.indexOf(stop) >= 0) { + details.aggregate(masked._appendPlaceholder(stop)); + } + chunkBlock = chunk instanceof ChunksTailDetails && masked._blocks[stop]; + } + if (chunkBlock) { + const tailDetails = chunkBlock.appendTail(chunk); + details.aggregate(tailDetails); + + // get not inserted chars + const remainChars = chunk.toString().slice(tailDetails.rawInserted.length); + if (remainChars) details.aggregate(masked.append(remainChars, { + tail: true + })); + } else { + details.aggregate(masked.append(chunk.toString(), { + tail: true + })); + } + } + return details; + } + get state() { + return { + chunks: this.chunks.map(c => c.state), + from: this.from, + stop: this.stop, + blockIndex: this.blockIndex + }; + } + set state(state) { + const { + chunks, + ...props + } = state; + Object.assign(this, props); + this.chunks = chunks.map(cstate => { + const chunk = "chunks" in cstate ? new ChunksTailDetails() : new ContinuousTailDetails(); + chunk.state = cstate; + return chunk; + }); + } + unshift(beforePos) { + if (!this.chunks.length || beforePos != null && this.from >= beforePos) return ''; + const chunkShiftPos = beforePos != null ? beforePos - this.from : beforePos; + let ci = 0; + while (ci < this.chunks.length) { + const chunk = this.chunks[ci]; + const shiftChar = chunk.unshift(chunkShiftPos); + if (chunk.toString()) { + // chunk still contains value + // but not shifted - means no more available chars to shift + if (!shiftChar) break; + ++ci; + } else { + // clean if chunk has no value + this.chunks.splice(ci, 1); + } + if (shiftChar) return shiftChar; + } + return ''; + } + shift() { + if (!this.chunks.length) return ''; + let ci = this.chunks.length - 1; + while (0 <= ci) { + const chunk = this.chunks[ci]; + const shiftChar = chunk.shift(); + if (chunk.toString()) { + // chunk still contains value + // but not shifted - means no more available chars to shift + if (!shiftChar) break; + --ci; + } else { + // clean if chunk has no value + this.chunks.splice(ci, 1); + } + if (shiftChar) return shiftChar; + } + return ''; + } + } + + class PatternCursor { + constructor(masked, pos) { + this.masked = masked; + this._log = []; + const { + offset, + index + } = masked._mapPosToBlock(pos) || (pos < 0 ? + // first + { + index: 0, + offset: 0 + } : + // last + { + index: this.masked._blocks.length, + offset: 0 + }); + this.offset = offset; + this.index = index; + this.ok = false; + } + get block() { + return this.masked._blocks[this.index]; + } + get pos() { + return this.masked._blockStartPos(this.index) + this.offset; + } + get state() { + return { + index: this.index, + offset: this.offset, + ok: this.ok + }; + } + set state(s) { + Object.assign(this, s); + } + pushState() { + this._log.push(this.state); + } + popState() { + const s = this._log.pop(); + if (s) this.state = s; + return s; + } + bindBlock() { + if (this.block) return; + if (this.index < 0) { + this.index = 0; + this.offset = 0; + } + if (this.index >= this.masked._blocks.length) { + this.index = this.masked._blocks.length - 1; + this.offset = this.block.displayValue.length; // TODO this is stupid type error, `block` depends on index that was changed above + } + } + _pushLeft(fn) { + this.pushState(); + for (this.bindBlock(); 0 <= this.index; --this.index, this.offset = ((_this$block = this.block) == null ? void 0 : _this$block.displayValue.length) || 0) { + var _this$block; + if (fn()) return this.ok = true; + } + return this.ok = false; + } + _pushRight(fn) { + this.pushState(); + for (this.bindBlock(); this.index < this.masked._blocks.length; ++this.index, this.offset = 0) { + if (fn()) return this.ok = true; + } + return this.ok = false; + } + pushLeftBeforeFilled() { + return this._pushLeft(() => { + if (this.block.isFixed || !this.block.value) return; + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.FORCE_LEFT); + if (this.offset !== 0) return true; + }); + } + pushLeftBeforeInput() { + // cases: + // filled input: 00| + // optional empty input: 00[]| + // nested block: XX<[]>| + return this._pushLeft(() => { + if (this.block.isFixed) return; + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.LEFT); + return true; + }); + } + pushLeftBeforeRequired() { + return this._pushLeft(() => { + if (this.block.isFixed || this.block.isOptional && !this.block.value) return; + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.LEFT); + return true; + }); + } + pushRightBeforeFilled() { + return this._pushRight(() => { + if (this.block.isFixed || !this.block.value) return; + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.FORCE_RIGHT); + if (this.offset !== this.block.value.length) return true; + }); + } + pushRightBeforeInput() { + return this._pushRight(() => { + if (this.block.isFixed) return; + + // const o = this.offset; + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.NONE); + // HACK cases like (STILL DOES NOT WORK FOR NESTED) + // aa|X + // aaX_ - this will not work + // if (o && o === this.offset && this.block instanceof PatternInputDefinition) continue; + return true; + }); + } + pushRightBeforeRequired() { + return this._pushRight(() => { + if (this.block.isFixed || this.block.isOptional && !this.block.value) return; + + // TODO check |[*]XX_ + this.offset = this.block.nearestInputPos(this.offset, DIRECTION.NONE); + return true; + }); + } + } + + class PatternFixedDefinition { + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + constructor(opts) { + Object.assign(this, opts); + this._value = ''; + this.isFixed = true; + } + get value() { + return this._value; + } + get unmaskedValue() { + return this.isUnmasking ? this.value : ''; + } + get rawInputValue() { + return this._isRawInput ? this.value : ''; + } + get displayValue() { + return this.value; + } + reset() { + this._isRawInput = false; + this._value = ''; + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this._value.length; + } + this._value = this._value.slice(0, fromPos) + this._value.slice(toPos); + if (!this._value) this._isRawInput = false; + return new ChangeDetails(); + } + nearestInputPos(cursorPos, direction) { + if (direction === void 0) { + direction = DIRECTION.NONE; + } + const minPos = 0; + const maxPos = this._value.length; + switch (direction) { + case DIRECTION.LEFT: + case DIRECTION.FORCE_LEFT: + return minPos; + case DIRECTION.NONE: + case DIRECTION.RIGHT: + case DIRECTION.FORCE_RIGHT: + default: + return maxPos; + } + } + totalInputPositions(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this._value.length; + } + return this._isRawInput ? toPos - fromPos : 0; + } + extractInput(fromPos, toPos, flags) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this._value.length; + } + if (flags === void 0) { + flags = {}; + } + return flags.raw && this._isRawInput && this._value.slice(fromPos, toPos) || ''; + } + get isComplete() { + return true; + } + get isFilled() { + return Boolean(this._value); + } + _appendChar(ch, flags) { + if (flags === void 0) { + flags = {}; + } + if (this.isFilled) return new ChangeDetails(); + const appendEager = this.eager === true || this.eager === 'append'; + const appended = this.char === ch; + const isResolved = appended && (this.isUnmasking || flags.input || flags.raw) && (!flags.raw || !appendEager) && !flags.tail; + const details = new ChangeDetails({ + inserted: this.char, + rawInserted: isResolved ? this.char : '' + }); + this._value = this.char; + this._isRawInput = isResolved && (flags.raw || flags.input); + return details; + } + _appendEager() { + return this._appendChar(this.char, { + tail: true + }); + } + _appendPlaceholder() { + const details = new ChangeDetails(); + if (this.isFilled) return details; + this._value = details.inserted = this.char; + return details; + } + extractTail() { + return new ContinuousTailDetails(''); + } + appendTail(tail) { + if (isString(tail)) tail = new ContinuousTailDetails(String(tail)); + return tail.appendTo(this); + } + append(str, flags, tail) { + const details = this._appendChar(str[0], flags); + if (tail != null) { + details.tailShift += this.appendTail(tail).tailShift; + } + return details; + } + doCommit() {} + get state() { + return { + _value: this._value, + _rawInputValue: this.rawInputValue + }; + } + set state(state) { + this._value = state._value; + this._isRawInput = Boolean(state._rawInputValue); + } + pad(flags) { + return this._appendPlaceholder(); + } + } + + class PatternInputDefinition { + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + /** */ + + constructor(opts) { + const { + parent, + isOptional, + placeholderChar, + displayChar, + lazy, + eager, + ...maskOpts + } = opts; + this.masked = createMask(maskOpts); + Object.assign(this, { + parent, + isOptional, + placeholderChar, + displayChar, + lazy, + eager + }); + } + reset() { + this.isFilled = false; + this.masked.reset(); + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.value.length; + } + if (fromPos === 0 && toPos >= 1) { + this.isFilled = false; + return this.masked.remove(fromPos, toPos); + } + return new ChangeDetails(); + } + get value() { + return this.masked.value || (this.isFilled && !this.isOptional ? this.placeholderChar : ''); + } + get unmaskedValue() { + return this.masked.unmaskedValue; + } + get rawInputValue() { + return this.masked.rawInputValue; + } + get displayValue() { + return this.masked.value && this.displayChar || this.value; + } + get isComplete() { + return Boolean(this.masked.value) || this.isOptional; + } + _appendChar(ch, flags) { + if (flags === void 0) { + flags = {}; + } + if (this.isFilled) return new ChangeDetails(); + const state = this.masked.state; + // simulate input + let details = this.masked._appendChar(ch, this.currentMaskFlags(flags)); + if (details.inserted && this.doValidate(flags) === false) { + details = new ChangeDetails(); + this.masked.state = state; + } + if (!details.inserted && !this.isOptional && !this.lazy && !flags.input) { + details.inserted = this.placeholderChar; + } + details.skip = !details.inserted && !this.isOptional; + this.isFilled = Boolean(details.inserted); + return details; + } + append(str, flags, tail) { + // TODO probably should be done via _appendChar + return this.masked.append(str, this.currentMaskFlags(flags), tail); + } + _appendPlaceholder() { + if (this.isFilled || this.isOptional) return new ChangeDetails(); + this.isFilled = true; + return new ChangeDetails({ + inserted: this.placeholderChar + }); + } + _appendEager() { + return new ChangeDetails(); + } + extractTail(fromPos, toPos) { + return this.masked.extractTail(fromPos, toPos); + } + appendTail(tail) { + return this.masked.appendTail(tail); + } + extractInput(fromPos, toPos, flags) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.value.length; + } + return this.masked.extractInput(fromPos, toPos, flags); + } + nearestInputPos(cursorPos, direction) { + if (direction === void 0) { + direction = DIRECTION.NONE; + } + const minPos = 0; + const maxPos = this.value.length; + const boundPos = Math.min(Math.max(cursorPos, minPos), maxPos); + switch (direction) { + case DIRECTION.LEFT: + case DIRECTION.FORCE_LEFT: + return this.isComplete ? boundPos : minPos; + case DIRECTION.RIGHT: + case DIRECTION.FORCE_RIGHT: + return this.isComplete ? boundPos : maxPos; + case DIRECTION.NONE: + default: + return boundPos; + } + } + totalInputPositions(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.value.length; + } + return this.value.slice(fromPos, toPos).length; + } + doValidate(flags) { + return this.masked.doValidate(this.currentMaskFlags(flags)) && (!this.parent || this.parent.doValidate(this.currentMaskFlags(flags))); + } + doCommit() { + this.masked.doCommit(); + } + get state() { + return { + _value: this.value, + _rawInputValue: this.rawInputValue, + masked: this.masked.state, + isFilled: this.isFilled + }; + } + set state(state) { + this.masked.state = state.masked; + this.isFilled = state.isFilled; + } + currentMaskFlags(flags) { + var _flags$_beforeTailSta; + return { + ...flags, + _beforeTailState: (flags == null || (_flags$_beforeTailSta = flags._beforeTailState) == null ? void 0 : _flags$_beforeTailSta.masked) || (flags == null ? void 0 : flags._beforeTailState) + }; + } + pad(flags) { + return new ChangeDetails(); + } + } + PatternInputDefinition.DEFAULT_DEFINITIONS = { + '0': /\d/, + 'a': /[\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/, + // http://stackoverflow.com/a/22075070 + '*': /./ + }; + + /** Masking by RegExp */ + class MaskedRegExp extends Masked { + /** */ + + /** Enable characters overwriting */ + + /** */ + + /** */ + + /** */ + + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + const mask = opts.mask; + if (mask) opts.validate = value => value.search(mask) >= 0; + super._update(opts); + } + } + IMask.MaskedRegExp = MaskedRegExp; + + /** Pattern mask */ + class MaskedPattern extends Masked { + /** */ + + /** */ + + /** Single char for empty input */ + + /** Single char for filled input */ + + /** Show placeholder only when needed */ + + /** Enable characters overwriting */ + + /** */ + + /** */ + + /** */ + + constructor(opts) { + super({ + ...MaskedPattern.DEFAULTS, + ...opts, + definitions: Object.assign({}, PatternInputDefinition.DEFAULT_DEFINITIONS, opts == null ? void 0 : opts.definitions) + }); + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + opts.definitions = Object.assign({}, this.definitions, opts.definitions); + super._update(opts); + this._rebuildMask(); + } + _rebuildMask() { + const defs = this.definitions; + this._blocks = []; + this.exposeBlock = undefined; + this._stops = []; + this._maskedBlocks = {}; + const pattern = this.mask; + if (!pattern || !defs) return; + let unmaskingBlock = false; + let optionalBlock = false; + for (let i = 0; i < pattern.length; ++i) { + if (this.blocks) { + const p = pattern.slice(i); + const bNames = Object.keys(this.blocks).filter(bName => p.indexOf(bName) === 0); + // order by key length + bNames.sort((a, b) => b.length - a.length); + // use block name with max length + const bName = bNames[0]; + if (bName) { + const { + expose, + repeat, + ...bOpts + } = normalizeOpts(this.blocks[bName]); // TODO type Opts + const blockOpts = { + lazy: this.lazy, + eager: this.eager, + placeholderChar: this.placeholderChar, + displayChar: this.displayChar, + overwrite: this.overwrite, + autofix: this.autofix, + ...bOpts, + repeat, + parent: this + }; + const maskedBlock = repeat != null ? new IMask.RepeatBlock(blockOpts /* TODO */) : createMask(blockOpts); + if (maskedBlock) { + this._blocks.push(maskedBlock); + if (expose) this.exposeBlock = maskedBlock; + + // store block index + if (!this._maskedBlocks[bName]) this._maskedBlocks[bName] = []; + this._maskedBlocks[bName].push(this._blocks.length - 1); + } + i += bName.length - 1; + continue; + } + } + let char = pattern[i]; + let isInput = (char in defs); + if (char === MaskedPattern.STOP_CHAR) { + this._stops.push(this._blocks.length); + continue; + } + if (char === '{' || char === '}') { + unmaskingBlock = !unmaskingBlock; + continue; + } + if (char === '[' || char === ']') { + optionalBlock = !optionalBlock; + continue; + } + if (char === MaskedPattern.ESCAPE_CHAR) { + ++i; + char = pattern[i]; + if (!char) break; + isInput = false; + } + const def = isInput ? new PatternInputDefinition({ + isOptional: optionalBlock, + lazy: this.lazy, + eager: this.eager, + placeholderChar: this.placeholderChar, + displayChar: this.displayChar, + ...normalizeOpts(defs[char]), + parent: this + }) : new PatternFixedDefinition({ + char, + eager: this.eager, + isUnmasking: unmaskingBlock + }); + this._blocks.push(def); + } + } + get state() { + return { + ...super.state, + _blocks: this._blocks.map(b => b.state) + }; + } + set state(state) { + if (!state) { + this.reset(); + return; + } + const { + _blocks, + ...maskedState + } = state; + this._blocks.forEach((b, bi) => b.state = _blocks[bi]); + super.state = maskedState; + } + reset() { + super.reset(); + this._blocks.forEach(b => b.reset()); + } + get isComplete() { + return this.exposeBlock ? this.exposeBlock.isComplete : this._blocks.every(b => b.isComplete); + } + get isFilled() { + return this._blocks.every(b => b.isFilled); + } + get isFixed() { + return this._blocks.every(b => b.isFixed); + } + get isOptional() { + return this._blocks.every(b => b.isOptional); + } + doCommit() { + this._blocks.forEach(b => b.doCommit()); + super.doCommit(); + } + get unmaskedValue() { + return this.exposeBlock ? this.exposeBlock.unmaskedValue : this._blocks.reduce((str, b) => str += b.unmaskedValue, ''); + } + set unmaskedValue(unmaskedValue) { + if (this.exposeBlock) { + const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length); + this.exposeBlock.unmaskedValue = unmaskedValue; + this.appendTail(tail); + this.doCommit(); + } else super.unmaskedValue = unmaskedValue; + } + get value() { + return this.exposeBlock ? this.exposeBlock.value : + // TODO return _value when not in change? + this._blocks.reduce((str, b) => str += b.value, ''); + } + set value(value) { + if (this.exposeBlock) { + const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length); + this.exposeBlock.value = value; + this.appendTail(tail); + this.doCommit(); + } else super.value = value; + } + get typedValue() { + return this.exposeBlock ? this.exposeBlock.typedValue : super.typedValue; + } + set typedValue(value) { + if (this.exposeBlock) { + const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length); + this.exposeBlock.typedValue = value; + this.appendTail(tail); + this.doCommit(); + } else super.typedValue = value; + } + get displayValue() { + return this._blocks.reduce((str, b) => str += b.displayValue, ''); + } + appendTail(tail) { + return super.appendTail(tail).aggregate(this._appendPlaceholder()); + } + _appendEager() { + var _this$_mapPosToBlock; + const details = new ChangeDetails(); + let startBlockIndex = (_this$_mapPosToBlock = this._mapPosToBlock(this.displayValue.length)) == null ? void 0 : _this$_mapPosToBlock.index; + if (startBlockIndex == null) return details; + + // TODO test if it works for nested pattern masks + if (this._blocks[startBlockIndex].isFilled) ++startBlockIndex; + for (let bi = startBlockIndex; bi < this._blocks.length; ++bi) { + const d = this._blocks[bi]._appendEager(); + if (!d.inserted) break; + details.aggregate(d); + } + return details; + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const blockIter = this._mapPosToBlock(this.displayValue.length); + const details = new ChangeDetails(); + if (!blockIter) return details; + for (let bi = blockIter.index, block; block = this._blocks[bi]; ++bi) { + var _flags$_beforeTailSta; + const blockDetails = block._appendChar(ch, { + ...flags, + _beforeTailState: (_flags$_beforeTailSta = flags._beforeTailState) == null || (_flags$_beforeTailSta = _flags$_beforeTailSta._blocks) == null ? void 0 : _flags$_beforeTailSta[bi] + }); + details.aggregate(blockDetails); + if (blockDetails.consumed) break; // go next char + } + return details; + } + extractTail(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + const chunkTail = new ChunksTailDetails(); + if (fromPos === toPos) return chunkTail; + this._forEachBlocksInRange(fromPos, toPos, (b, bi, bFromPos, bToPos) => { + const blockChunk = b.extractTail(bFromPos, bToPos); + blockChunk.stop = this._findStopBefore(bi); + blockChunk.from = this._blockStartPos(bi); + if (blockChunk instanceof ChunksTailDetails) blockChunk.blockIndex = bi; + chunkTail.extend(blockChunk); + }); + return chunkTail; + } + extractInput(fromPos, toPos, flags) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + if (flags === void 0) { + flags = {}; + } + if (fromPos === toPos) return ''; + let input = ''; + this._forEachBlocksInRange(fromPos, toPos, (b, _, fromPos, toPos) => { + input += b.extractInput(fromPos, toPos, flags); + }); + return input; + } + _findStopBefore(blockIndex) { + let stopBefore; + for (let si = 0; si < this._stops.length; ++si) { + const stop = this._stops[si]; + if (stop <= blockIndex) stopBefore = stop;else break; + } + return stopBefore; + } + + /** Appends placeholder depending on laziness */ + _appendPlaceholder(toBlockIndex) { + const details = new ChangeDetails(); + if (this.lazy && toBlockIndex == null) return details; + const startBlockIter = this._mapPosToBlock(this.displayValue.length); + if (!startBlockIter) return details; + const startBlockIndex = startBlockIter.index; + const endBlockIndex = toBlockIndex != null ? toBlockIndex : this._blocks.length; + this._blocks.slice(startBlockIndex, endBlockIndex).forEach(b => { + if (!b.lazy || toBlockIndex != null) { + var _blocks2; + details.aggregate(b._appendPlaceholder((_blocks2 = b._blocks) == null ? void 0 : _blocks2.length)); + } + }); + return details; + } + + /** Finds block in pos */ + _mapPosToBlock(pos) { + let accVal = ''; + for (let bi = 0; bi < this._blocks.length; ++bi) { + const block = this._blocks[bi]; + const blockStartPos = accVal.length; + accVal += block.displayValue; + if (pos <= accVal.length) { + return { + index: bi, + offset: pos - blockStartPos + }; + } + } + } + _blockStartPos(blockIndex) { + return this._blocks.slice(0, blockIndex).reduce((pos, b) => pos += b.displayValue.length, 0); + } + _forEachBlocksInRange(fromPos, toPos, fn) { + if (toPos === void 0) { + toPos = this.displayValue.length; + } + const fromBlockIter = this._mapPosToBlock(fromPos); + if (fromBlockIter) { + const toBlockIter = this._mapPosToBlock(toPos); + // process first block + const isSameBlock = toBlockIter && fromBlockIter.index === toBlockIter.index; + const fromBlockStartPos = fromBlockIter.offset; + const fromBlockEndPos = toBlockIter && isSameBlock ? toBlockIter.offset : this._blocks[fromBlockIter.index].displayValue.length; + fn(this._blocks[fromBlockIter.index], fromBlockIter.index, fromBlockStartPos, fromBlockEndPos); + if (toBlockIter && !isSameBlock) { + // process intermediate blocks + for (let bi = fromBlockIter.index + 1; bi < toBlockIter.index; ++bi) { + fn(this._blocks[bi], bi, 0, this._blocks[bi].displayValue.length); + } + + // process last block + fn(this._blocks[toBlockIter.index], toBlockIter.index, 0, toBlockIter.offset); + } + } + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + const removeDetails = super.remove(fromPos, toPos); + this._forEachBlocksInRange(fromPos, toPos, (b, _, bFromPos, bToPos) => { + removeDetails.aggregate(b.remove(bFromPos, bToPos)); + }); + return removeDetails; + } + nearestInputPos(cursorPos, direction) { + if (direction === void 0) { + direction = DIRECTION.NONE; + } + if (!this._blocks.length) return 0; + const cursor = new PatternCursor(this, cursorPos); + if (direction === DIRECTION.NONE) { + // ------------------------------------------------- + // NONE should only go out from fixed to the right! + // ------------------------------------------------- + if (cursor.pushRightBeforeInput()) return cursor.pos; + cursor.popState(); + if (cursor.pushLeftBeforeInput()) return cursor.pos; + return this.displayValue.length; + } + + // FORCE is only about a|* otherwise is 0 + if (direction === DIRECTION.LEFT || direction === DIRECTION.FORCE_LEFT) { + // try to break fast when *|a + if (direction === DIRECTION.LEFT) { + cursor.pushRightBeforeFilled(); + if (cursor.ok && cursor.pos === cursorPos) return cursorPos; + cursor.popState(); + } + + // forward flow + cursor.pushLeftBeforeInput(); + cursor.pushLeftBeforeRequired(); + cursor.pushLeftBeforeFilled(); + + // backward flow + if (direction === DIRECTION.LEFT) { + cursor.pushRightBeforeInput(); + cursor.pushRightBeforeRequired(); + if (cursor.ok && cursor.pos <= cursorPos) return cursor.pos; + cursor.popState(); + if (cursor.ok && cursor.pos <= cursorPos) return cursor.pos; + cursor.popState(); + } + if (cursor.ok) return cursor.pos; + if (direction === DIRECTION.FORCE_LEFT) return 0; + cursor.popState(); + if (cursor.ok) return cursor.pos; + cursor.popState(); + if (cursor.ok) return cursor.pos; + return 0; + } + if (direction === DIRECTION.RIGHT || direction === DIRECTION.FORCE_RIGHT) { + // forward flow + cursor.pushRightBeforeInput(); + cursor.pushRightBeforeRequired(); + if (cursor.pushRightBeforeFilled()) return cursor.pos; + if (direction === DIRECTION.FORCE_RIGHT) return this.displayValue.length; + + // backward flow + cursor.popState(); + if (cursor.ok) return cursor.pos; + cursor.popState(); + if (cursor.ok) return cursor.pos; + return this.nearestInputPos(cursorPos, DIRECTION.LEFT); + } + return cursorPos; + } + totalInputPositions(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + let total = 0; + this._forEachBlocksInRange(fromPos, toPos, (b, _, bFromPos, bToPos) => { + total += b.totalInputPositions(bFromPos, bToPos); + }); + return total; + } + + /** Get block by name */ + maskedBlock(name) { + return this.maskedBlocks(name)[0]; + } + + /** Get all blocks by name */ + maskedBlocks(name) { + const indices = this._maskedBlocks[name]; + if (!indices) return []; + return indices.map(gi => this._blocks[gi]); + } + pad(flags) { + const details = new ChangeDetails(); + this._forEachBlocksInRange(0, this.displayValue.length, b => details.aggregate(b.pad(flags))); + return details; + } + } + MaskedPattern.DEFAULTS = { + ...Masked.DEFAULTS, + lazy: true, + placeholderChar: '_' + }; + MaskedPattern.STOP_CHAR = '`'; + MaskedPattern.ESCAPE_CHAR = '\\'; + MaskedPattern.InputDefinition = PatternInputDefinition; + MaskedPattern.FixedDefinition = PatternFixedDefinition; + IMask.MaskedPattern = MaskedPattern; + + /** Pattern which accepts ranges */ + class MaskedRange extends MaskedPattern { + /** + Optionally sets max length of pattern. + Used when pattern length is longer then `to` param length. Pads zeros at start in this case. + */ + + /** Min bound */ + + /** Max bound */ + + get _matchFrom() { + return this.maxLength - String(this.from).length; + } + constructor(opts) { + super(opts); // mask will be created in _update + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + const { + to = this.to || 0, + from = this.from || 0, + maxLength = this.maxLength || 0, + autofix = this.autofix, + ...patternOpts + } = opts; + this.to = to; + this.from = from; + this.maxLength = Math.max(String(to).length, maxLength); + this.autofix = autofix; + const fromStr = String(this.from).padStart(this.maxLength, '0'); + const toStr = String(this.to).padStart(this.maxLength, '0'); + let sameCharsCount = 0; + while (sameCharsCount < toStr.length && toStr[sameCharsCount] === fromStr[sameCharsCount]) ++sameCharsCount; + patternOpts.mask = toStr.slice(0, sameCharsCount).replace(/0/g, '\\0') + '0'.repeat(this.maxLength - sameCharsCount); + super._update(patternOpts); + } + get isComplete() { + return super.isComplete && Boolean(this.value); + } + boundaries(str) { + let minstr = ''; + let maxstr = ''; + const [, placeholder, num] = str.match(/^(\D*)(\d*)(\D*)/) || []; + if (num) { + minstr = '0'.repeat(placeholder.length) + num; + maxstr = '9'.repeat(placeholder.length) + num; + } + minstr = minstr.padEnd(this.maxLength, '0'); + maxstr = maxstr.padEnd(this.maxLength, '9'); + return [minstr, maxstr]; + } + doPrepareChar(ch, flags) { + if (flags === void 0) { + flags = {}; + } + let details; + [ch, details] = super.doPrepareChar(ch.replace(/\D/g, ''), flags); + if (!ch) details.skip = !this.isComplete; + return [ch, details]; + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + if (!this.autofix || this.value.length + 1 > this.maxLength) return super._appendCharRaw(ch, flags); + const fromStr = String(this.from).padStart(this.maxLength, '0'); + const toStr = String(this.to).padStart(this.maxLength, '0'); + const [minstr, maxstr] = this.boundaries(this.value + ch); + if (Number(maxstr) < this.from) return super._appendCharRaw(fromStr[this.value.length], flags); + if (Number(minstr) > this.to) { + if (!flags.tail && this.autofix === 'pad' && this.value.length + 1 < this.maxLength) { + return super._appendCharRaw(fromStr[this.value.length], flags).aggregate(this._appendCharRaw(ch, flags)); + } + return super._appendCharRaw(toStr[this.value.length], flags); + } + return super._appendCharRaw(ch, flags); + } + doValidate(flags) { + const str = this.value; + const firstNonZero = str.search(/[^0]/); + if (firstNonZero === -1 && str.length <= this._matchFrom) return true; + const [minstr, maxstr] = this.boundaries(str); + return this.from <= Number(maxstr) && Number(minstr) <= this.to && super.doValidate(flags); + } + pad(flags) { + const details = new ChangeDetails(); + if (this.value.length === this.maxLength) return details; + const value = this.value; + const padLength = this.maxLength - this.value.length; + if (padLength) { + this.reset(); + for (let i = 0; i < padLength; ++i) { + details.aggregate(super._appendCharRaw('0', flags)); + } + + // append tail + value.split('').forEach(ch => this._appendCharRaw(ch)); + } + return details; + } + } + IMask.MaskedRange = MaskedRange; + + const DefaultPattern = 'd{.}`m{.}`Y'; + + // Make format and parse required when pattern is provided + + /** Date mask */ + class MaskedDate extends MaskedPattern { + static extractPatternOptions(opts) { + const { + mask, + pattern, + ...patternOpts + } = opts; + return { + ...patternOpts, + mask: isString(mask) ? mask : pattern + }; + } + + /** Pattern mask for date according to {@link MaskedDate#format} */ + + /** Start date */ + + /** End date */ + + /** Format typed value to string */ + + /** Parse string to get typed value */ + + constructor(opts) { + super(MaskedDate.extractPatternOptions({ + ...MaskedDate.DEFAULTS, + ...opts + })); + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + const { + mask, + pattern, + blocks, + ...patternOpts + } = { + ...MaskedDate.DEFAULTS, + ...opts + }; + const patternBlocks = Object.assign({}, MaskedDate.GET_DEFAULT_BLOCKS()); + // adjust year block + if (opts.min) patternBlocks.Y.from = opts.min.getFullYear(); + if (opts.max) patternBlocks.Y.to = opts.max.getFullYear(); + if (opts.min && opts.max && patternBlocks.Y.from === patternBlocks.Y.to) { + patternBlocks.m.from = opts.min.getMonth() + 1; + patternBlocks.m.to = opts.max.getMonth() + 1; + if (patternBlocks.m.from === patternBlocks.m.to) { + patternBlocks.d.from = opts.min.getDate(); + patternBlocks.d.to = opts.max.getDate(); + } + } + Object.assign(patternBlocks, this.blocks, blocks); + super._update({ + ...patternOpts, + mask: isString(mask) ? mask : pattern, + blocks: patternBlocks + }); + } + doValidate(flags) { + const date = this.date; + return super.doValidate(flags) && (!this.isComplete || this.isDateExist(this.value) && date != null && (this.min == null || this.min <= date) && (this.max == null || date <= this.max)); + } + + /** Checks if date is exists */ + isDateExist(str) { + return this.format(this.parse(str, this), this).indexOf(str) >= 0; + } + + /** Parsed Date */ + get date() { + return this.typedValue; + } + set date(date) { + this.typedValue = date; + } + get typedValue() { + return this.isComplete ? super.typedValue : null; + } + set typedValue(value) { + super.typedValue = value; + } + maskEquals(mask) { + return mask === Date || super.maskEquals(mask); + } + optionsIsChanged(opts) { + return super.optionsIsChanged(MaskedDate.extractPatternOptions(opts)); + } + } + MaskedDate.GET_DEFAULT_BLOCKS = () => ({ + d: { + mask: MaskedRange, + from: 1, + to: 31, + maxLength: 2 + }, + m: { + mask: MaskedRange, + from: 1, + to: 12, + maxLength: 2 + }, + Y: { + mask: MaskedRange, + from: 1900, + to: 9999 + } + }); + MaskedDate.DEFAULTS = { + ...MaskedPattern.DEFAULTS, + mask: Date, + pattern: DefaultPattern, + format: (date, masked) => { + if (!date) return ''; + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + return [day, month, year].join('.'); + }, + parse: (str, masked) => { + const [day, month, year] = str.split('.').map(Number); + return new Date(year, month - 1, day); + } + }; + IMask.MaskedDate = MaskedDate; + + /** Dynamic mask for choosing appropriate mask in run-time */ + class MaskedDynamic extends Masked { + constructor(opts) { + super({ + ...MaskedDynamic.DEFAULTS, + ...opts + }); + this.currentMask = undefined; + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + super._update(opts); + if ('mask' in opts) { + this.exposeMask = undefined; + // mask could be totally dynamic with only `dispatch` option + this.compiledMasks = Array.isArray(opts.mask) ? opts.mask.map(m => { + const { + expose, + ...maskOpts + } = normalizeOpts(m); + const masked = createMask({ + overwrite: this._overwrite, + eager: this._eager, + skipInvalid: this._skipInvalid, + ...maskOpts + }); + if (expose) this.exposeMask = masked; + return masked; + }) : []; + + // this.currentMask = this.doDispatch(''); // probably not needed but lets see + } + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const details = this._applyDispatch(ch, flags); + if (this.currentMask) { + details.aggregate(this.currentMask._appendChar(ch, this.currentMaskFlags(flags))); + } + return details; + } + _applyDispatch(appended, flags, tail) { + if (appended === void 0) { + appended = ''; + } + if (flags === void 0) { + flags = {}; + } + if (tail === void 0) { + tail = ''; + } + const prevValueBeforeTail = flags.tail && flags._beforeTailState != null ? flags._beforeTailState._value : this.value; + const inputValue = this.rawInputValue; + const insertValue = flags.tail && flags._beforeTailState != null ? flags._beforeTailState._rawInputValue : inputValue; + const tailValue = inputValue.slice(insertValue.length); + const prevMask = this.currentMask; + const details = new ChangeDetails(); + const prevMaskState = prevMask == null ? void 0 : prevMask.state; + + // clone flags to prevent overwriting `_beforeTailState` + this.currentMask = this.doDispatch(appended, { + ...flags + }, tail); + + // restore state after dispatch + if (this.currentMask) { + if (this.currentMask !== prevMask) { + // if mask changed reapply input + this.currentMask.reset(); + if (insertValue) { + this.currentMask.append(insertValue, { + raw: true + }); + details.tailShift = this.currentMask.value.length - prevValueBeforeTail.length; + } + if (tailValue) { + details.tailShift += this.currentMask.append(tailValue, { + raw: true, + tail: true + }).tailShift; + } + } else if (prevMaskState) { + // Dispatch can do something bad with state, so + // restore prev mask state + this.currentMask.state = prevMaskState; + } + } + return details; + } + _appendPlaceholder() { + const details = this._applyDispatch(); + if (this.currentMask) { + details.aggregate(this.currentMask._appendPlaceholder()); + } + return details; + } + _appendEager() { + const details = this._applyDispatch(); + if (this.currentMask) { + details.aggregate(this.currentMask._appendEager()); + } + return details; + } + appendTail(tail) { + const details = new ChangeDetails(); + if (tail) details.aggregate(this._applyDispatch('', {}, tail)); + return details.aggregate(this.currentMask ? this.currentMask.appendTail(tail) : super.appendTail(tail)); + } + currentMaskFlags(flags) { + var _flags$_beforeTailSta, _flags$_beforeTailSta2; + return { + ...flags, + _beforeTailState: ((_flags$_beforeTailSta = flags._beforeTailState) == null ? void 0 : _flags$_beforeTailSta.currentMaskRef) === this.currentMask && ((_flags$_beforeTailSta2 = flags._beforeTailState) == null ? void 0 : _flags$_beforeTailSta2.currentMask) || flags._beforeTailState + }; + } + doDispatch(appended, flags, tail) { + if (flags === void 0) { + flags = {}; + } + if (tail === void 0) { + tail = ''; + } + return this.dispatch(appended, this, flags, tail); + } + doValidate(flags) { + return super.doValidate(flags) && (!this.currentMask || this.currentMask.doValidate(this.currentMaskFlags(flags))); + } + doPrepare(str, flags) { + if (flags === void 0) { + flags = {}; + } + let [s, details] = super.doPrepare(str, flags); + if (this.currentMask) { + let currentDetails; + [s, currentDetails] = super.doPrepare(s, this.currentMaskFlags(flags)); + details = details.aggregate(currentDetails); + } + return [s, details]; + } + doPrepareChar(str, flags) { + if (flags === void 0) { + flags = {}; + } + let [s, details] = super.doPrepareChar(str, flags); + if (this.currentMask) { + let currentDetails; + [s, currentDetails] = super.doPrepareChar(s, this.currentMaskFlags(flags)); + details = details.aggregate(currentDetails); + } + return [s, details]; + } + reset() { + var _this$currentMask; + (_this$currentMask = this.currentMask) == null || _this$currentMask.reset(); + this.compiledMasks.forEach(m => m.reset()); + } + get value() { + return this.exposeMask ? this.exposeMask.value : this.currentMask ? this.currentMask.value : ''; + } + set value(value) { + if (this.exposeMask) { + this.exposeMask.value = value; + this.currentMask = this.exposeMask; + this._applyDispatch(); + } else super.value = value; + } + get unmaskedValue() { + return this.exposeMask ? this.exposeMask.unmaskedValue : this.currentMask ? this.currentMask.unmaskedValue : ''; + } + set unmaskedValue(unmaskedValue) { + if (this.exposeMask) { + this.exposeMask.unmaskedValue = unmaskedValue; + this.currentMask = this.exposeMask; + this._applyDispatch(); + } else super.unmaskedValue = unmaskedValue; + } + get typedValue() { + return this.exposeMask ? this.exposeMask.typedValue : this.currentMask ? this.currentMask.typedValue : ''; + } + set typedValue(typedValue) { + if (this.exposeMask) { + this.exposeMask.typedValue = typedValue; + this.currentMask = this.exposeMask; + this._applyDispatch(); + return; + } + let unmaskedValue = String(typedValue); + + // double check it + if (this.currentMask) { + this.currentMask.typedValue = typedValue; + unmaskedValue = this.currentMask.unmaskedValue; + } + this.unmaskedValue = unmaskedValue; + } + get displayValue() { + return this.currentMask ? this.currentMask.displayValue : ''; + } + get isComplete() { + var _this$currentMask2; + return Boolean((_this$currentMask2 = this.currentMask) == null ? void 0 : _this$currentMask2.isComplete); + } + get isFilled() { + var _this$currentMask3; + return Boolean((_this$currentMask3 = this.currentMask) == null ? void 0 : _this$currentMask3.isFilled); + } + remove(fromPos, toPos) { + const details = new ChangeDetails(); + if (this.currentMask) { + details.aggregate(this.currentMask.remove(fromPos, toPos)) + // update with dispatch + .aggregate(this._applyDispatch()); + } + return details; + } + get state() { + var _this$currentMask4; + return { + ...super.state, + _rawInputValue: this.rawInputValue, + compiledMasks: this.compiledMasks.map(m => m.state), + currentMaskRef: this.currentMask, + currentMask: (_this$currentMask4 = this.currentMask) == null ? void 0 : _this$currentMask4.state + }; + } + set state(state) { + const { + compiledMasks, + currentMaskRef, + currentMask, + ...maskedState + } = state; + if (compiledMasks) this.compiledMasks.forEach((m, mi) => m.state = compiledMasks[mi]); + if (currentMaskRef != null) { + this.currentMask = currentMaskRef; + this.currentMask.state = currentMask; + } + super.state = maskedState; + } + extractInput(fromPos, toPos, flags) { + return this.currentMask ? this.currentMask.extractInput(fromPos, toPos, flags) : ''; + } + extractTail(fromPos, toPos) { + return this.currentMask ? this.currentMask.extractTail(fromPos, toPos) : super.extractTail(fromPos, toPos); + } + doCommit() { + if (this.currentMask) this.currentMask.doCommit(); + super.doCommit(); + } + nearestInputPos(cursorPos, direction) { + return this.currentMask ? this.currentMask.nearestInputPos(cursorPos, direction) : super.nearestInputPos(cursorPos, direction); + } + get overwrite() { + return this.currentMask ? this.currentMask.overwrite : this._overwrite; + } + set overwrite(overwrite) { + this._overwrite = overwrite; + } + get eager() { + return this.currentMask ? this.currentMask.eager : this._eager; + } + set eager(eager) { + this._eager = eager; + } + get skipInvalid() { + return this.currentMask ? this.currentMask.skipInvalid : this._skipInvalid; + } + set skipInvalid(skipInvalid) { + this._skipInvalid = skipInvalid; + } + get autofix() { + return this.currentMask ? this.currentMask.autofix : this._autofix; + } + set autofix(autofix) { + this._autofix = autofix; + } + maskEquals(mask) { + return Array.isArray(mask) ? this.compiledMasks.every((m, mi) => { + if (!mask[mi]) return; + const { + mask: oldMask, + ...restOpts + } = mask[mi]; + return objectIncludes(m, restOpts) && m.maskEquals(oldMask); + }) : super.maskEquals(mask); + } + typedValueEquals(value) { + var _this$currentMask5; + return Boolean((_this$currentMask5 = this.currentMask) == null ? void 0 : _this$currentMask5.typedValueEquals(value)); + } + } + /** Currently chosen mask */ + /** Currently chosen mask */ + /** Compliled {@link Masked} options */ + /** Chooses {@link Masked} depending on input value */ + MaskedDynamic.DEFAULTS = { + ...Masked.DEFAULTS, + dispatch: (appended, masked, flags, tail) => { + if (!masked.compiledMasks.length) return; + const inputValue = masked.rawInputValue; + + // simulate input + const inputs = masked.compiledMasks.map((m, index) => { + const isCurrent = masked.currentMask === m; + const startInputPos = isCurrent ? m.displayValue.length : m.nearestInputPos(m.displayValue.length, DIRECTION.FORCE_LEFT); + if (m.rawInputValue !== inputValue) { + m.reset(); + m.append(inputValue, { + raw: true + }); + } else if (!isCurrent) { + m.remove(startInputPos); + } + m.append(appended, masked.currentMaskFlags(flags)); + m.appendTail(tail); + return { + index, + weight: m.rawInputValue.length, + totalInputPositions: m.totalInputPositions(0, Math.max(startInputPos, m.nearestInputPos(m.displayValue.length, DIRECTION.FORCE_LEFT))) + }; + }); + + // pop masks with longer values first + inputs.sort((i1, i2) => i2.weight - i1.weight || i2.totalInputPositions - i1.totalInputPositions); + return masked.compiledMasks[inputs[0].index]; + } + }; + IMask.MaskedDynamic = MaskedDynamic; + + /** Pattern which validates enum values */ + class MaskedEnum extends MaskedPattern { + constructor(opts) { + super({ + ...MaskedEnum.DEFAULTS, + ...opts + }); // mask will be created in _update + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + const { + enum: enum_, + ...eopts + } = opts; + if (enum_) { + const lengths = enum_.map(e => e.length); + const requiredLength = Math.min(...lengths); + const optionalLength = Math.max(...lengths) - requiredLength; + eopts.mask = '*'.repeat(requiredLength); + if (optionalLength) eopts.mask += '[' + '*'.repeat(optionalLength) + ']'; + this.enum = enum_; + } + super._update(eopts); + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const matchFrom = Math.min(this.nearestInputPos(0, DIRECTION.FORCE_RIGHT), this.value.length); + const matches = this.enum.filter(e => this.matchValue(e, this.unmaskedValue + ch, matchFrom)); + if (matches.length) { + if (matches.length === 1) { + this._forEachBlocksInRange(0, this.value.length, (b, bi) => { + const mch = matches[0][bi]; + if (bi >= this.value.length || mch === b.value) return; + b.reset(); + b._appendChar(mch, flags); + }); + } + const d = super._appendCharRaw(matches[0][this.value.length], flags); + if (matches.length === 1) { + matches[0].slice(this.unmaskedValue.length).split('').forEach(mch => d.aggregate(super._appendCharRaw(mch))); + } + return d; + } + return new ChangeDetails({ + skip: !this.isComplete + }); + } + extractTail(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + // just drop tail + return new ContinuousTailDetails('', fromPos); + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + if (fromPos === toPos) return new ChangeDetails(); + const matchFrom = Math.min(super.nearestInputPos(0, DIRECTION.FORCE_RIGHT), this.value.length); + let pos; + for (pos = fromPos; pos >= 0; --pos) { + const matches = this.enum.filter(e => this.matchValue(e, this.value.slice(matchFrom, pos), matchFrom)); + if (matches.length > 1) break; + } + const details = super.remove(pos, toPos); + details.tailShift += pos - fromPos; + return details; + } + get isComplete() { + return this.enum.indexOf(this.value) >= 0; + } + } + /** Match enum value */ + MaskedEnum.DEFAULTS = { + ...MaskedPattern.DEFAULTS, + matchValue: (estr, istr, matchFrom) => estr.indexOf(istr, matchFrom) === matchFrom + }; + IMask.MaskedEnum = MaskedEnum; + + /** Masking by custom Function */ + class MaskedFunction extends Masked { + /** */ + + /** Enable characters overwriting */ + + /** */ + + /** */ + + /** */ + + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + super._update({ + ...opts, + validate: opts.mask + }); + } + } + IMask.MaskedFunction = MaskedFunction; + + var _MaskedNumber; + /** Number mask */ + class MaskedNumber extends Masked { + /** Single char */ + + /** Single char */ + + /** Array of single chars */ + + /** */ + + /** */ + + /** Digits after point */ + + /** Flag to remove leading and trailing zeros in the end of editing */ + + /** Flag to pad trailing zeros after point in the end of editing */ + + /** Enable characters overwriting */ + + /** */ + + /** */ + + /** */ + + /** Format typed value to string */ + + /** Parse string to get typed value */ + + constructor(opts) { + super({ + ...MaskedNumber.DEFAULTS, + ...opts + }); + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + super._update(opts); + this._updateRegExps(); + } + _updateRegExps() { + const start = '^' + (this.allowNegative ? '[+|\\-]?' : ''); + const mid = '\\d*'; + const end = (this.scale ? "(" + escapeRegExp(this.radix) + "\\d{0," + this.scale + "})?" : '') + '$'; + this._numberRegExp = new RegExp(start + mid + end); + this._mapToRadixRegExp = new RegExp("[" + this.mapToRadix.map(escapeRegExp).join('') + "]", 'g'); + this._thousandsSeparatorRegExp = new RegExp(escapeRegExp(this.thousandsSeparator), 'g'); + } + _removeThousandsSeparators(value) { + return value.replace(this._thousandsSeparatorRegExp, ''); + } + _insertThousandsSeparators(value) { + // https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript + const parts = value.split(this.radix); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandsSeparator); + return parts.join(this.radix); + } + doPrepareChar(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const [prepCh, details] = super.doPrepareChar(this._removeThousandsSeparators(this.scale && this.mapToRadix.length && ( + /* + radix should be mapped when + 1) input is done from keyboard = flags.input && flags.raw + 2) unmasked value is set = !flags.input && !flags.raw + and should not be mapped when + 1) value is set = flags.input && !flags.raw + 2) raw value is set = !flags.input && flags.raw + */ + flags.input && flags.raw || !flags.input && !flags.raw) ? ch.replace(this._mapToRadixRegExp, this.radix) : ch), flags); + if (ch && !prepCh) details.skip = true; + if (prepCh && !this.allowPositive && !this.value && prepCh !== '-') details.aggregate(this._appendChar('-')); + return [prepCh, details]; + } + _separatorsCount(to, extendOnSeparators) { + if (extendOnSeparators === void 0) { + extendOnSeparators = false; + } + let count = 0; + for (let pos = 0; pos < to; ++pos) { + if (this._value.indexOf(this.thousandsSeparator, pos) === pos) { + ++count; + if (extendOnSeparators) to += this.thousandsSeparator.length; + } + } + return count; + } + _separatorsCountFromSlice(slice) { + if (slice === void 0) { + slice = this._value; + } + return this._separatorsCount(this._removeThousandsSeparators(slice).length, true); + } + extractInput(fromPos, toPos, flags) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + [fromPos, toPos] = this._adjustRangeWithSeparators(fromPos, toPos); + return this._removeThousandsSeparators(super.extractInput(fromPos, toPos, flags)); + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const prevBeforeTailValue = flags.tail && flags._beforeTailState ? flags._beforeTailState._value : this._value; + const prevBeforeTailSeparatorsCount = this._separatorsCountFromSlice(prevBeforeTailValue); + this._value = this._removeThousandsSeparators(this.value); + const oldValue = this._value; + this._value += ch; + const num = this.number; + let accepted = !isNaN(num); + let skip = false; + if (accepted) { + let fixedNum; + if (this.min != null && this.min < 0 && this.number < this.min) fixedNum = this.min; + if (this.max != null && this.max > 0 && this.number > this.max) fixedNum = this.max; + if (fixedNum != null) { + if (this.autofix) { + this._value = this.format(fixedNum, this).replace(MaskedNumber.UNMASKED_RADIX, this.radix); + skip || (skip = oldValue === this._value && !flags.tail); // if not changed on tail it's still ok to proceed + } else { + accepted = false; + } + } + accepted && (accepted = Boolean(this._value.match(this._numberRegExp))); + } + let appendDetails; + if (!accepted) { + this._value = oldValue; + appendDetails = new ChangeDetails(); + } else { + appendDetails = new ChangeDetails({ + inserted: this._value.slice(oldValue.length), + rawInserted: skip ? '' : ch, + skip + }); + } + this._value = this._insertThousandsSeparators(this._value); + const beforeTailValue = flags.tail && flags._beforeTailState ? flags._beforeTailState._value : this._value; + const beforeTailSeparatorsCount = this._separatorsCountFromSlice(beforeTailValue); + appendDetails.tailShift += (beforeTailSeparatorsCount - prevBeforeTailSeparatorsCount) * this.thousandsSeparator.length; + return appendDetails; + } + _findSeparatorAround(pos) { + if (this.thousandsSeparator) { + const searchFrom = pos - this.thousandsSeparator.length + 1; + const separatorPos = this.value.indexOf(this.thousandsSeparator, searchFrom); + if (separatorPos <= pos) return separatorPos; + } + return -1; + } + _adjustRangeWithSeparators(from, to) { + const separatorAroundFromPos = this._findSeparatorAround(from); + if (separatorAroundFromPos >= 0) from = separatorAroundFromPos; + const separatorAroundToPos = this._findSeparatorAround(to); + if (separatorAroundToPos >= 0) to = separatorAroundToPos + this.thousandsSeparator.length; + return [from, to]; + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + [fromPos, toPos] = this._adjustRangeWithSeparators(fromPos, toPos); + const valueBeforePos = this.value.slice(0, fromPos); + const valueAfterPos = this.value.slice(toPos); + const prevBeforeTailSeparatorsCount = this._separatorsCount(valueBeforePos.length); + this._value = this._insertThousandsSeparators(this._removeThousandsSeparators(valueBeforePos + valueAfterPos)); + const beforeTailSeparatorsCount = this._separatorsCountFromSlice(valueBeforePos); + return new ChangeDetails({ + tailShift: (beforeTailSeparatorsCount - prevBeforeTailSeparatorsCount) * this.thousandsSeparator.length + }); + } + nearestInputPos(cursorPos, direction) { + if (!this.thousandsSeparator) return cursorPos; + switch (direction) { + case DIRECTION.NONE: + case DIRECTION.LEFT: + case DIRECTION.FORCE_LEFT: + { + const separatorAtLeftPos = this._findSeparatorAround(cursorPos - 1); + if (separatorAtLeftPos >= 0) { + const separatorAtLeftEndPos = separatorAtLeftPos + this.thousandsSeparator.length; + if (cursorPos < separatorAtLeftEndPos || this.value.length <= separatorAtLeftEndPos || direction === DIRECTION.FORCE_LEFT) { + return separatorAtLeftPos; + } + } + break; + } + case DIRECTION.RIGHT: + case DIRECTION.FORCE_RIGHT: + { + const separatorAtRightPos = this._findSeparatorAround(cursorPos); + if (separatorAtRightPos >= 0) { + return separatorAtRightPos + this.thousandsSeparator.length; + } + } + } + return cursorPos; + } + doCommit() { + if (this.value) { + const number = this.number; + let validnum = number; + + // check bounds + if (this.min != null) validnum = Math.max(validnum, this.min); + if (this.max != null) validnum = Math.min(validnum, this.max); + if (validnum !== number) this.unmaskedValue = this.format(validnum, this); + let formatted = this.value; + if (this.normalizeZeros) formatted = this._normalizeZeros(formatted); + if (this.padFractionalZeros && this.scale > 0) formatted = this._padFractionalZeros(formatted); + this._value = formatted; + } + super.doCommit(); + } + _normalizeZeros(value) { + const parts = this._removeThousandsSeparators(value).split(this.radix); + + // remove leading zeros + parts[0] = parts[0].replace(/^(\D*)(0*)(\d*)/, (match, sign, zeros, num) => sign + num); + // add leading zero + if (value.length && !/\d$/.test(parts[0])) parts[0] = parts[0] + '0'; + if (parts.length > 1) { + parts[1] = parts[1].replace(/0*$/, ''); // remove trailing zeros + if (!parts[1].length) parts.length = 1; // remove fractional + } + return this._insertThousandsSeparators(parts.join(this.radix)); + } + _padFractionalZeros(value) { + if (!value) return value; + const parts = value.split(this.radix); + if (parts.length < 2) parts.push(''); + parts[1] = parts[1].padEnd(this.scale, '0'); + return parts.join(this.radix); + } + doSkipInvalid(ch, flags, checkTail) { + if (flags === void 0) { + flags = {}; + } + const dropFractional = this.scale === 0 && ch !== this.thousandsSeparator && (ch === this.radix || ch === MaskedNumber.UNMASKED_RADIX || this.mapToRadix.includes(ch)); + return super.doSkipInvalid(ch, flags, checkTail) && !dropFractional; + } + get unmaskedValue() { + return this._removeThousandsSeparators(this._normalizeZeros(this.value)).replace(this.radix, MaskedNumber.UNMASKED_RADIX); + } + set unmaskedValue(unmaskedValue) { + super.unmaskedValue = unmaskedValue; + } + get typedValue() { + return this.parse(this.unmaskedValue, this); + } + set typedValue(n) { + this.rawInputValue = this.format(n, this).replace(MaskedNumber.UNMASKED_RADIX, this.radix); + } + + /** Parsed Number */ + get number() { + return this.typedValue; + } + set number(number) { + this.typedValue = number; + } + get allowNegative() { + return this.min != null && this.min < 0 || this.max != null && this.max < 0; + } + get allowPositive() { + return this.min != null && this.min > 0 || this.max != null && this.max > 0; + } + typedValueEquals(value) { + // handle 0 -> '' case (typed = 0 even if value = '') + // for details see https://github.com/uNmAnNeR/imaskjs/issues/134 + return (super.typedValueEquals(value) || MaskedNumber.EMPTY_VALUES.includes(value) && MaskedNumber.EMPTY_VALUES.includes(this.typedValue)) && !(value === 0 && this.value === ''); + } + } + _MaskedNumber = MaskedNumber; + MaskedNumber.UNMASKED_RADIX = '.'; + MaskedNumber.EMPTY_VALUES = [...Masked.EMPTY_VALUES, 0]; + MaskedNumber.DEFAULTS = { + ...Masked.DEFAULTS, + mask: Number, + radix: ',', + thousandsSeparator: '', + mapToRadix: [_MaskedNumber.UNMASKED_RADIX], + min: Number.MIN_SAFE_INTEGER, + max: Number.MAX_SAFE_INTEGER, + scale: 2, + normalizeZeros: true, + padFractionalZeros: false, + parse: Number, + format: n => n.toLocaleString('en-US', { + useGrouping: false, + maximumFractionDigits: 20 + }) + }; + IMask.MaskedNumber = MaskedNumber; + + /** Mask pipe source and destination types */ + const PIPE_TYPE = { + MASKED: 'value', + UNMASKED: 'unmaskedValue', + TYPED: 'typedValue' + }; + /** Creates new pipe function depending on mask type, source and destination options */ + function createPipe(arg, from, to) { + if (from === void 0) { + from = PIPE_TYPE.MASKED; + } + if (to === void 0) { + to = PIPE_TYPE.MASKED; + } + const masked = createMask(arg); + return value => masked.runIsolated(m => { + m[from] = value; + return m[to]; + }); + } + + /** Pipes value through mask depending on mask type, source and destination options */ + function pipe(value, mask, from, to) { + return createPipe(mask, from, to)(value); + } + IMask.PIPE_TYPE = PIPE_TYPE; + IMask.createPipe = createPipe; + IMask.pipe = pipe; + + /** Pattern mask */ + class RepeatBlock extends MaskedPattern { + get repeatFrom() { + var _ref; + return (_ref = Array.isArray(this.repeat) ? this.repeat[0] : this.repeat === Infinity ? 0 : this.repeat) != null ? _ref : 0; + } + get repeatTo() { + var _ref2; + return (_ref2 = Array.isArray(this.repeat) ? this.repeat[1] : this.repeat) != null ? _ref2 : Infinity; + } + constructor(opts) { + super(opts); + } + updateOptions(opts) { + super.updateOptions(opts); + } + _update(opts) { + var _ref3, _ref4, _this$_blocks; + const { + repeat, + ...blockOpts + } = normalizeOpts(opts); // TODO type + this._blockOpts = Object.assign({}, this._blockOpts, blockOpts); + const block = createMask(this._blockOpts); + this.repeat = (_ref3 = (_ref4 = repeat != null ? repeat : block.repeat) != null ? _ref4 : this.repeat) != null ? _ref3 : Infinity; // TODO type + + super._update({ + mask: 'm'.repeat(Math.max(this.repeatTo === Infinity && ((_this$_blocks = this._blocks) == null ? void 0 : _this$_blocks.length) || 0, this.repeatFrom)), + blocks: { + m: block + }, + eager: block.eager, + overwrite: block.overwrite, + skipInvalid: block.skipInvalid, + lazy: block.lazy, + placeholderChar: block.placeholderChar, + displayChar: block.displayChar + }); + } + _allocateBlock(bi) { + if (bi < this._blocks.length) return this._blocks[bi]; + if (this.repeatTo === Infinity || this._blocks.length < this.repeatTo) { + this._blocks.push(createMask(this._blockOpts)); + this.mask += 'm'; + return this._blocks[this._blocks.length - 1]; + } + } + _appendCharRaw(ch, flags) { + if (flags === void 0) { + flags = {}; + } + const details = new ChangeDetails(); + for (let bi = (_this$_mapPosToBlock$ = (_this$_mapPosToBlock = this._mapPosToBlock(this.displayValue.length)) == null ? void 0 : _this$_mapPosToBlock.index) != null ? _this$_mapPosToBlock$ : Math.max(this._blocks.length - 1, 0), block, allocated; + // try to get a block or + // try to allocate a new block if not allocated already + block = (_this$_blocks$bi = this._blocks[bi]) != null ? _this$_blocks$bi : allocated = !allocated && this._allocateBlock(bi); ++bi) { + var _this$_mapPosToBlock$, _this$_mapPosToBlock, _this$_blocks$bi, _flags$_beforeTailSta; + const blockDetails = block._appendChar(ch, { + ...flags, + _beforeTailState: (_flags$_beforeTailSta = flags._beforeTailState) == null || (_flags$_beforeTailSta = _flags$_beforeTailSta._blocks) == null ? void 0 : _flags$_beforeTailSta[bi] + }); + if (blockDetails.skip && allocated) { + // remove the last allocated block and break + this._blocks.pop(); + this.mask = this.mask.slice(1); + break; + } + details.aggregate(blockDetails); + if (blockDetails.consumed) break; // go next char + } + return details; + } + _trimEmptyTail(fromPos, toPos) { + var _this$_mapPosToBlock2, _this$_mapPosToBlock3; + if (fromPos === void 0) { + fromPos = 0; + } + const firstBlockIndex = Math.max(((_this$_mapPosToBlock2 = this._mapPosToBlock(fromPos)) == null ? void 0 : _this$_mapPosToBlock2.index) || 0, this.repeatFrom, 0); + let lastBlockIndex; + if (toPos != null) lastBlockIndex = (_this$_mapPosToBlock3 = this._mapPosToBlock(toPos)) == null ? void 0 : _this$_mapPosToBlock3.index; + if (lastBlockIndex == null) lastBlockIndex = this._blocks.length - 1; + let removeCount = 0; + for (let blockIndex = lastBlockIndex; firstBlockIndex <= blockIndex; --blockIndex, ++removeCount) { + if (this._blocks[blockIndex].unmaskedValue) break; + } + if (removeCount) { + this._blocks.splice(lastBlockIndex - removeCount + 1, removeCount); + this.mask = this.mask.slice(removeCount); + } + } + reset() { + super.reset(); + this._trimEmptyTail(); + } + remove(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos === void 0) { + toPos = this.displayValue.length; + } + const removeDetails = super.remove(fromPos, toPos); + this._trimEmptyTail(fromPos, toPos); + return removeDetails; + } + totalInputPositions(fromPos, toPos) { + if (fromPos === void 0) { + fromPos = 0; + } + if (toPos == null && this.repeatTo === Infinity) return Infinity; + return super.totalInputPositions(fromPos, toPos); + } + get state() { + return super.state; + } + set state(state) { + this._blocks.length = state._blocks.length; + this.mask = this.mask.slice(0, this._blocks.length); + super.state = state; + } + } + IMask.RepeatBlock = RepeatBlock; + + try { + globalThis.IMask = IMask; + } catch {} + + exports.ChangeDetails = ChangeDetails; + exports.ChunksTailDetails = ChunksTailDetails; + exports.DIRECTION = DIRECTION; + exports.HTMLContenteditableMaskElement = HTMLContenteditableMaskElement; + exports.HTMLInputMaskElement = HTMLInputMaskElement; + exports.HTMLMaskElement = HTMLMaskElement; + exports.InputMask = InputMask; + exports.MaskElement = MaskElement; + exports.Masked = Masked; + exports.MaskedDate = MaskedDate; + exports.MaskedDynamic = MaskedDynamic; + exports.MaskedEnum = MaskedEnum; + exports.MaskedFunction = MaskedFunction; + exports.MaskedNumber = MaskedNumber; + exports.MaskedPattern = MaskedPattern; + exports.MaskedRange = MaskedRange; + exports.MaskedRegExp = MaskedRegExp; + exports.PIPE_TYPE = PIPE_TYPE; + exports.PatternFixedDefinition = PatternFixedDefinition; + exports.PatternInputDefinition = PatternInputDefinition; + exports.RepeatBlock = RepeatBlock; + exports.createMask = createMask; + exports.createPipe = createPipe; + exports.default = IMask; + exports.forceDirection = forceDirection; + exports.normalizeOpts = normalizeOpts; + exports.pipe = pipe; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); +//# sourceMappingURL=imask.js.map diff --git a/assets/js/main.js b/assets/js/main.js index c5d5f7f..d8e0731 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -3,3 +3,16 @@ import './step-by-step.js'; import './team.js'; import './faq.js'; import './nav.js'; +import './modals.js'; + +const maskOptions = { + mask: '+{7} (000) 000 00 00', +}; + +const phoneInputClassNames = ['input--phone']; + +const phoneInputs = phoneInputClassNames + .map((name) => document.querySelectorAll(`.${name}`)) + .reduce((acc, array) => [...acc, ...array], []); + +phoneInputs.forEach((inputElement) => IMask(inputElement, maskOptions)); diff --git a/assets/js/modals.js b/assets/js/modals.js new file mode 100644 index 0000000..c58a0d6 --- /dev/null +++ b/assets/js/modals.js @@ -0,0 +1,36 @@ +const callbackButtons = document.querySelectorAll('.button--callback'); +const callbackModal = document.querySelector('.modal--callback'); +const thanksModal = document.querySelector('.modal--thanks'); + +callbackButtons.forEach((button) => { + button.addEventListener('click', () => { + if (callbackModal) { + callbackModal.classList.add('active'); + thanksModal.classList.remove('active'); + } + }); +}); + +[callbackModal, thanksModal].forEach((modal) => { + modal.addEventListener('click', (event) => { + const isModal = event.target.classList.contains('modal'); + const isModalBody = event.target.classList.contains('modal__body'); + if (isModal || isModalBody) event.currentTarget.classList.remove('active'); + }); +}); + +const modalForms = document.querySelectorAll('.modal__form'); +const callbackForms = document.querySelectorAll('.callback__form'); + +[...modalForms, ...callbackForms].forEach((form) => { + form.addEventListener('submit', (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const name = formData.get('name'); + const phone = formData.get('phone'); + console.log(name, phone) + event.currentTarget.reset(); + if (callbackModal) callbackModal.classList.remove('active'); + if (thanksModal) thanksModal.classList.add('active'); + }); +}); diff --git a/assets/scss/_l-modal.scss b/assets/scss/_l-modal.scss new file mode 100644 index 0000000..dafdb37 --- /dev/null +++ b/assets/scss/_l-modal.scss @@ -0,0 +1,130 @@ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9000; + display: none; + align-items: center; + justify-content: center; + background-color: rgba($color: $black, $alpha: 0.25); + backdrop-filter: blur(10px); + + &.active { + display: flex; + } + + &__body { + position: relative; + max-width: calc(100% - 30px); + width: 550px; + padding: 40px 60px; + box-sizing: border-box; + border-radius: 60px 10px; + background-color: $blue; + + @include tablet { + padding: 24px 20px; + box-sizing: border-box; + border-radius: 30px 8px; + background-color: $blue; + } + + &::before { + content: ''; + position: absolute; + top: -26px; + right: -26px; + z-index: 1001; + display: block; + width: 28px; + height: 28px; + background: url(../img/icons/close.svg) center no-repeat; + cursor: pointer; + + @include tablet { + top: -34px; + left: 50%; + right: auto; + transform: translateX(-50%); + } + } + } + + &__content { + overflow: auto; + + &--thanks { + padding-top: 164px; + background: url(../img/icons/thanks.svg) center top no-repeat; + } + } + + &__title { + margin: 0 0 25px; + font-weight: 500; + font-size: 37px; + line-height: 122%; + letter-spacing: 0.01em; + text-align: center; + color: $white; + + @include tablet { + font-size: 30px; + } + + &--thanks { + margin: 0 0 8px; + font-weight: 700; + font-size: 56px; + + @include tablet { + font-size: 42px; + } + } + + & span { + font-weight: 700; + } + } + + &__description { + font-weight: 400; + font-size: 28px; + line-height: 122%; + letter-spacing: 0.01em; + text-align: center; + color: $white; + } + + &__form { + display: flex; + flex-direction: column; + gap: 25px; + + @include tablet { + gap: 12px; + } + + & > * { + display: block; + width: 100%; + box-sizing: border-box; + } + } + + &__disclaimer { + margin: 27px 0 0; + font-weight: 300; + font-size: 14px; + line-height: 138%; + letter-spacing: 0.01em; + text-align: center; + color: rgba($color: $white, $alpha: 0.5); + + @include tablet { + font-size: 10px; + } + } +} diff --git a/assets/scss/_l-team.scss b/assets/scss/_l-team.scss index 0f9eeb2..8381815 100644 --- a/assets/scss/_l-team.scss +++ b/assets/scss/_l-team.scss @@ -190,7 +190,6 @@ background: rgba($color: $black, $alpha: 0.05); } - /* Handle */ &::-webkit-scrollbar-thumb { background: rgba($color: $black, $alpha: 0.1); } diff --git a/assets/scss/_m-input.scss b/assets/scss/_m-input.scss index 2f270f3..4cccf5b 100644 --- a/assets/scss/_m-input.scss +++ b/assets/scss/_m-input.scss @@ -17,6 +17,10 @@ line-height: 140%; } + &:focus { + background-color: $blue; + } + &::placeholder { color: $white; } diff --git a/assets/scss/index.scss b/assets/scss/index.scss index 838e718..933b125 100644 --- a/assets/scss/index.scss +++ b/assets/scss/index.scss @@ -32,3 +32,4 @@ @import './l-team'; @import './l-faq'; @import './l-callback'; +@import './l-modal'; diff --git a/index.html b/index.html index bfc9f63..0760910 100644 --- a/index.html +++ b/index.html @@ -34,7 +34,9 @@ 8 (800) 101-21-27 - +
- +
@@ -101,7 +103,7 @@
- +
@@ -582,7 +590,9 @@ реабилитации и назначит дату прибытия в центр
- +
@@ -595,7 +605,9 @@ состоянии, то самостоятельно заберем его и привезем на реабилитацию
- +
@@ -608,7 +620,9 @@ облегчить состояние
- +
@@ -621,7 +635,9 @@ согласия и подтверждения этого в договорах
- +
@@ -707,7 +723,9 @@
- +
@@ -722,7 +740,9 @@ будет напоминать о пройденном пути
- +
@@ -737,7 +757,9 @@ плавную адаптацию
- +
@@ -1368,7 +1390,9 @@
Для получения стабильного результата
от 25 000 ₽
- +
@@ -1381,7 +1405,9 @@
от 35 000 ₽
- +
@@ -1394,7 +1420,9 @@
от 45 000 ₽
- +
@@ -1407,7 +1435,9 @@
от 15 000 ₽
- +
@@ -1881,14 +1911,20 @@ name="name" type="text" placeholder="Введите ваше имя" + autocomplete="off" + required /> - +
@@ -1943,7 +1979,9 @@
@@ -1955,7 +1993,55 @@ + + + + +