import { ObjectDirective, VNode, DirectiveHook, DirectiveBinding, warn } from '@vue/runtime-core' import { addEventListener } from '../modules/events' import { isArray, looseEqual, looseIndexOf, invokeArrayFns, toNumber, isSet } from '@vue/shared' type AssignerFn = (value: any) => void const getModelAssigner = (vnode: VNode): AssignerFn => { const fn = vnode.props!['onUpdate:modelValue'] return isArray(fn) ? value => invokeArrayFns(fn, value) : fn } function onCompositionStart(e: Event) { ;(e.target as any).composing = true } function onCompositionEnd(e: Event) { 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) } type ModelDirective = ObjectDirective // 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: ModelDirective< HTMLInputElement | HTMLTextAreaElement > = { created(el, { modifiers: { lazy, trim, number } }, vnode) { el._assign = getModelAssigner(vnode) const castToNumber = number || el.type === 'number' addEventListener(el, lazy ? 'change' : 'input', e => { if ((e.target as any).composing) return let domValue: string | number = el.value if (trim) { domValue = domValue.trim() } else if (castToNumber) { domValue = toNumber(domValue) } el._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) } }, // set value on mounted so it's after min/max for type="range" mounted(el, { value }) { el.value = value == null ? '' : value }, beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) { el._assign = getModelAssigner(vnode) // avoid clearing unresolved text. #2302 if ((el as any).composing) return if (document.activeElement === el) { if (trim && el.value.trim() === value) { return } if ((number || el.type === 'number') && toNumber(el.value) === value) { return } } const newValue = value == null ? '' : value if (el.value !== newValue) { el.value = newValue } } } export const vModelCheckbox: ModelDirective = { // #4096 array checkboxes need to be deep traversed deep: true, created(el, _, vnode) { el._assign = getModelAssigner(vnode) addEventListener(el, 'change', () => { const modelValue = (el as any)._modelValue const elementValue = getValue(el) const checked = el.checked const assign = el._assign if (isArray(modelValue)) { const index = looseIndexOf(modelValue, elementValue) const found = index !== -1 if (checked && !found) { assign(modelValue.concat(elementValue)) } else if (!checked && found) { const filtered = [...modelValue] filtered.splice(index, 1) assign(filtered) } } else if (isSet(modelValue)) { const cloned = new Set(modelValue) if (checked) { cloned.add(elementValue) } else { cloned.delete(elementValue) } assign(cloned) } else { assign(getCheckboxValue(el, checked)) } }) }, // set initial checked on mount to wait for true-value/false-value mounted: setChecked, beforeUpdate(el, binding, vnode) { el._assign = getModelAssigner(vnode) setChecked(el, binding, vnode) } } function setChecked( el: HTMLInputElement, { value, oldValue }: 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 if (isArray(value)) { el.checked = looseIndexOf(value, vnode.props!.value) > -1 } else if (isSet(value)) { el.checked = value.has(vnode.props!.value) } else if (value !== oldValue) { el.checked = looseEqual(value, getCheckboxValue(el, true)) } } export const vModelRadio: ModelDirective = { created(el, { value }, vnode) { el.checked = looseEqual(value, vnode.props!.value) el._assign = getModelAssigner(vnode) addEventListener(el, 'change', () => { el._assign(getValue(el)) }) }, beforeUpdate(el, { value, oldValue }, vnode) { el._assign = getModelAssigner(vnode) if (value !== oldValue) { el.checked = looseEqual(value, vnode.props!.value) } } } export const vModelSelect: ModelDirective = { // relies on its children //