import { Directive, VNode, DirectiveBinding, warn } from '@vue/runtime-core' import { addEventListener } from '../modules/events' import { looseEqual, isArray } from '@vue/shared' const getModelAssigner = (vnode: VNode): ((value: any) => void) => vnode.props!['onUpdate:modelValue'] function onCompositionStart(e: CompositionEvent) { ;(e.target as any).composing = true } function onCompositionEnd(e: CompositionEvent) { const target = e.target as any if (target.composing) { target.composing = false trigger(target, 'input') } } function trigger(el: HTMLElement, type: string) { const e = document.createEvent('HTMLEvents') e.initEvent(type, true, true) el.dispatchEvent(e) } function toNumber(val: string): number | string { const n = parseFloat(val) return isNaN(n) ? val : n } // We are exporting the v-model runtime directly as vnode hooks so that it can // be tree-shaken in case v-model is never used. export const vModelText: Directive = { beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) { el.value = value const assign = getModelAssigner(vnode) const castToNumber = number || el.type === 'number' addEventListener(el, lazy ? 'change' : 'input', () => { let domValue: string | number = el.value if (trim) { domValue = domValue.trim() } else if (castToNumber) { domValue = toNumber(domValue) } assign(domValue) }) if (trim) { addEventListener(el, 'change', () => { el.value = el.value.trim() }) } if (!lazy) { addEventListener(el, 'compositionstart', onCompositionStart) addEventListener(el, 'compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. addEventListener(el, 'change', onCompositionEnd) } }, beforeUpdate(el, { value, modifiers: { trim, number } }) { if (document.activeElement === el) { if (trim && el.value.trim() === value) { return } if ((number || el.type === 'number') && toNumber(el.value) === value) { return } } el.value = value } } export const vModelCheckbox: Directive = { beforeMount(el, binding, vnode) { setChecked(el, binding, vnode) const assign = getModelAssigner(vnode) addEventListener(el, 'change', () => { const modelValue = (el as any)._modelValue const elementValue = getValue(el) if (isArray(modelValue)) { const i = looseIndexOf(modelValue, elementValue) if (i > -1) { assign([...modelValue.slice(0, i), ...modelValue.slice(i + 1)]) } else { assign(modelValue.concat(elementValue)) } } else { assign(el.checked) } }) }, beforeUpdate: setChecked } function setChecked( el: HTMLInputElement, { value }: DirectiveBinding, vnode: VNode ) { // store the v-model value on the element so it can be accessed by the // change listener. ;(el as any)._modelValue = value el.checked = isArray(value) ? looseIndexOf(value, vnode.props!.value) > -1 : !!value } export const vModelRadio: Directive = { beforeMount(el, { value }, vnode) { el.checked = looseEqual(value, vnode.props!.value) const assign = getModelAssigner(vnode) addEventListener(el, 'change', () => { assign(getValue(el)) }) }, beforeUpdate(el, { value }, vnode) { el.checked = looseEqual(value, vnode.props!.value) } } export const vModelSelect: Directive = { // use mounted & updated because expects an Array value for its binding, ` + `but got ${Object.prototype.toString.call(value).slice(8, -1)}.` ) return } for (let i = 0, l = el.options.length; i < l; i++) { const option = el.options[i] const optionValue = getValue(option) if (isMultiple) { option.selected = looseIndexOf(value, optionValue) > -1 } else { if (looseEqual(getValue(option), value)) { el.selectedIndex = i return } } } if (!isMultiple) { el.selectedIndex = -1 } } function looseIndexOf(arr: any[], val: any): number { for (let i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) return i } return -1 } // retrieve raw value set via :value bindings function getValue(el: HTMLOptionElement | HTMLInputElement) { return '_value' in el ? (el as any)._value : el.value } export const vModelDynamic: Directive< HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > = { beforeMount(el, binding, vnode) { callModelHook(el, binding, vnode, null, 'beforeMount') }, mounted(el, binding, vnode) { callModelHook(el, binding, vnode, null, 'mounted') }, beforeUpdate(el, binding, vnode, prevVNode) { callModelHook(el, binding, vnode, prevVNode, 'beforeUpdate') }, updated(el, binding, vnode, prevVNode) { callModelHook(el, binding, vnode, prevVNode, 'updated') } } function callModelHook( el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, binding: DirectiveBinding, vnode: VNode, prevVNode: VNode | null, hook: keyof Directive ) { let modelToUse: Directive switch (el.tagName) { case 'SELECT': modelToUse = vModelSelect break case 'TEXTAREA': modelToUse = vModelText break default: switch (el.type) { case 'checkbox': modelToUse = vModelCheckbox break case 'radio': modelToUse = vModelRadio break default: modelToUse = vModelText } } const fn = modelToUse[hook] fn && fn(el, binding, vnode, prevVNode) }