|
| 1 | +import NumberFormat from './number-format' |
| 2 | +// import options from './options' |
| 3 | + |
| 4 | +export const CONFIG_KEY = '__input-facade__' |
| 5 | + |
| 6 | +/** |
| 7 | + * Creates a CustomEvent('input') with detail = { facade: true } |
| 8 | + * used as a way to identify our own input event |
| 9 | + */ |
| 10 | +export function FacadeInputEvent() { |
| 11 | + return new CustomEvent('input', { |
| 12 | + bubbles: true, |
| 13 | + cancelable: true, |
| 14 | + detail: { facade: true } |
| 15 | + }) |
| 16 | +} |
| 17 | +/** |
| 18 | + * Transform an array or string config into an object |
| 19 | + * |
| 20 | + * @param {object} config The format config object |
| 21 | + * @param {object} modifiers An object of modifier flags that can influence the formating process |
| 22 | + */ |
| 23 | +export function normalizeConfig(defaults, extras) { |
| 24 | + defaults = defaults || {} |
| 25 | + extras = extras || {} |
| 26 | + return Object.keys(defaults).concat(Object.keys(extras)).reduce((acc, val) => { |
| 27 | + acc[val] = extras[val] === undefined ? defaults[val] : extras[val] |
| 28 | + return acc |
| 29 | + }, {}) |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * ensure that the element we're attaching to is an input element |
| 34 | + * if not try to find an input element in this elements childrens |
| 35 | + * |
| 36 | + * @param {HTMLInputElement} el |
| 37 | + */ |
| 38 | +export function getInputElement(el) { |
| 39 | + const inputElement = el instanceof HTMLInputElement ? el : el.querySelector('input') |
| 40 | + |
| 41 | + /* istanbul ignore next */ |
| 42 | + if (!inputElement) { |
| 43 | + throw new Error('facade directive requires an input element') |
| 44 | + } |
| 45 | + |
| 46 | + return inputElement |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Updates the cursor position to the right place after the masking rule was applied |
| 51 | + * @param {HTMLElement} el |
| 52 | + * @param {Number} position |
| 53 | + */ |
| 54 | +export function updateCursor(el, position) { |
| 55 | + const setSelectionRange = () => { el.setSelectionRange(position, position) } |
| 56 | + if (el === document.activeElement) { |
| 57 | + setSelectionRange() |
| 58 | + // Android Fix |
| 59 | + setTimeout(setSelectionRange, 1) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * Updates the element's value and unmasked value based on the masking config rules |
| 65 | + * |
| 66 | + * @param {HTMLInputElement} el The input element to update |
| 67 | + * @param {object} [options] |
| 68 | + * @param {Boolean} options.emit Wether to dispatch a new InputEvent or not |
| 69 | + * @param {Boolean} options.force Forces the update even if the old value and the new value are the same |
| 70 | + */ |
| 71 | +export function updateValue(el, vnode, { emit = true, force = false, clean = false } = {}) { |
| 72 | + const { config } = el[CONFIG_KEY] |
| 73 | + let { oldValue } = el[CONFIG_KEY] |
| 74 | + |
| 75 | + let currentValue = vnode && vnode.data.model ? vnode.data.model.value : el.value |
| 76 | + |
| 77 | + oldValue = oldValue || '' |
| 78 | + currentValue = currentValue || '' |
| 79 | + |
| 80 | + const number = new NumberFormat(config).clean(clean) |
| 81 | + let masked = number.format(currentValue) |
| 82 | + let unmasked = number.unformat(currentValue) |
| 83 | + |
| 84 | + // check value with in range max and min value |
| 85 | + if (clean) { |
| 86 | + if (config.max && unmasked > config.max) { |
| 87 | + masked = number.format(config.max) |
| 88 | + unmasked = number.unformat(config.max) |
| 89 | + } else if (config.min && unmasked < config.min) { |
| 90 | + masked = number.format(config.min) |
| 91 | + unmasked = number.unformat(config.min) |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + if (force || oldValue !== currentValue) { |
| 96 | + el[CONFIG_KEY].oldValue = masked |
| 97 | + el.unmaskedValue = unmasked |
| 98 | + // safari makes the cursor jump to the end if el.value gets assign even if to the same value |
| 99 | + if (el.value !== masked) { |
| 100 | + el.value = masked |
| 101 | + } |
| 102 | + |
| 103 | + // this part needs to be outside the above IF statement for vuetify in firefox |
| 104 | + // drawback is that we endup with two's input events in firefox |
| 105 | + return emit && el.dispatchEvent(FacadeInputEvent()) |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +/** |
| 110 | + * Input event handler |
| 111 | + * |
| 112 | + * @param {Event} event The event object |
| 113 | + */ |
| 114 | +export function inputHandler(event) { |
| 115 | + const { target, detail } = event |
| 116 | + // We dont need to run this method on the event we emit (prevent event loop) |
| 117 | + if (detail && detail.facade) { |
| 118 | + return false |
| 119 | + } |
| 120 | + |
| 121 | + // since we will be emitting our own custom input event |
| 122 | + // we can stop propagation of this native event |
| 123 | + event.stopPropagation() |
| 124 | + |
| 125 | + let positionFromEnd = target.value.length - target.selectionEnd |
| 126 | + const { oldValue, config } = target[CONFIG_KEY] |
| 127 | + |
| 128 | + updateValue(target, null, { emit: false }, event) |
| 129 | + // updated cursor position |
| 130 | + positionFromEnd = Math.max(positionFromEnd, config.suffix.length) |
| 131 | + positionFromEnd = target.value.length - positionFromEnd |
| 132 | + positionFromEnd = Math.max(positionFromEnd, config.prefix.length + 1) |
| 133 | + const decimalPosition = target.value.indexOf(config.decimal) |
| 134 | + const diff = positionFromEnd - decimalPosition |
| 135 | + const maxLength = target.value.length - config.suffix.length |
| 136 | + const positionAfterDecimal = positionFromEnd + 1 |
| 137 | + if (decimalPosition > 0 && diff > 0 && positionAfterDecimal <= maxLength) { |
| 138 | + positionFromEnd = positionAfterDecimal |
| 139 | + } |
| 140 | + updateCursor(target, positionFromEnd) |
| 141 | + |
| 142 | + if (oldValue !== target.value) { |
| 143 | + target.dispatchEvent(FacadeInputEvent()) |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +/** |
| 148 | + * Blur event handler |
| 149 | + * |
| 150 | + * @param {Event} event The event object |
| 151 | + */ |
| 152 | +export function blurHandler(event) { |
| 153 | + const { target, detail } = event |
| 154 | + // We dont need to run this method on the event we emit (prevent event loop) |
| 155 | + if (detail && detail.facade) { |
| 156 | + return false |
| 157 | + } |
| 158 | + |
| 159 | + const { oldValue } = target[CONFIG_KEY] |
| 160 | + |
| 161 | + updateValue(target, null, { force: true, clean: true }, event) |
| 162 | + |
| 163 | + if (oldValue !== target.value) { |
| 164 | + target.dispatchEvent(FacadeInputEvent()) |
| 165 | + } |
| 166 | +} |
0 commit comments