2019-10-16 06:12:26 +00:00
|
|
|
import {
|
|
|
|
ObjectDirective,
|
|
|
|
VNode,
|
2020-03-18 16:30:20 +00:00
|
|
|
DirectiveHook,
|
2019-10-16 06:12:26 +00:00
|
|
|
DirectiveBinding,
|
|
|
|
warn
|
|
|
|
} from '@vue/runtime-core'
|
2019-10-11 21:55:20 +00:00
|
|
|
import { addEventListener } from '../modules/events'
|
2020-05-18 14:09:10 +00:00
|
|
|
import {
|
|
|
|
isArray,
|
|
|
|
looseEqual,
|
|
|
|
looseIndexOf,
|
|
|
|
invokeArrayFns,
|
2020-09-14 15:16:50 +00:00
|
|
|
toNumber,
|
|
|
|
isSet,
|
|
|
|
looseHas
|
2020-05-18 14:09:10 +00:00
|
|
|
} from '@vue/shared'
|
2019-10-11 21:55:20 +00:00
|
|
|
|
2020-04-05 00:51:42 +00:00
|
|
|
type AssignerFn = (value: any) => void
|
|
|
|
|
|
|
|
const getModelAssigner = (vnode: VNode): AssignerFn => {
|
|
|
|
const fn = vnode.props!['onUpdate:modelValue']
|
|
|
|
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
|
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
|
2020-03-23 15:08:22 +00:00
|
|
|
function onCompositionStart(e: Event) {
|
2019-10-11 21:55:20 +00:00
|
|
|
;(e.target as any).composing = true
|
|
|
|
}
|
|
|
|
|
2020-03-23 15:08:22 +00:00
|
|
|
function onCompositionEnd(e: Event) {
|
2019-10-11 21:55:20 +00:00
|
|
|
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)
|
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
|
2020-04-05 00:51:42 +00:00
|
|
|
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
|
|
|
|
|
2019-10-10 22:02:51 +00:00
|
|
|
// We are exporting the v-model runtime directly as vnode hooks so that it can
|
2020-06-10 18:57:21 +00:00
|
|
|
// be tree-shaken in case v-model is never used.
|
2020-04-05 00:51:42 +00:00
|
|
|
export const vModelText: ModelDirective<
|
2019-10-22 15:52:29 +00:00
|
|
|
HTMLInputElement | HTMLTextAreaElement
|
|
|
|
> = {
|
2020-08-24 21:12:16 +00:00
|
|
|
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
2020-07-06 23:02:33 +00:00
|
|
|
el.value = value == null ? '' : value
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
2019-10-12 00:35:25 +00:00
|
|
|
const castToNumber = number || el.type === 'number'
|
2020-05-18 14:23:55 +00:00
|
|
|
addEventListener(el, lazy ? 'change' : 'input', e => {
|
|
|
|
if ((e.target as any).composing) return
|
2019-10-12 00:35:25 +00:00
|
|
|
let domValue: string | number = el.value
|
|
|
|
if (trim) {
|
|
|
|
domValue = domValue.trim()
|
|
|
|
} else if (castToNumber) {
|
|
|
|
domValue = toNumber(domValue)
|
|
|
|
}
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign(domValue)
|
2019-10-11 21:55:20 +00:00
|
|
|
})
|
2019-10-12 00:35:25 +00:00
|
|
|
if (trim) {
|
|
|
|
addEventListener(el, 'change', () => {
|
|
|
|
el.value = el.value.trim()
|
|
|
|
})
|
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
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)
|
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
},
|
2020-06-30 15:23:09 +00:00
|
|
|
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
2019-10-12 00:35:25 +00:00
|
|
|
if (document.activeElement === el) {
|
|
|
|
if (trim && el.value.trim() === value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if ((number || el.type === 'number') && toNumber(el.value) === value) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2020-08-25 13:47:55 +00:00
|
|
|
const newValue = value == null ? '' : value
|
|
|
|
if (el.value !== newValue) {
|
|
|
|
el.value = newValue
|
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 00:51:42 +00:00
|
|
|
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
|
2020-08-24 21:12:16 +00:00
|
|
|
created(el, binding, vnode) {
|
2019-10-12 00:35:25 +00:00
|
|
|
setChecked(el, binding, vnode)
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
2019-10-11 21:55:20 +00:00
|
|
|
addEventListener(el, 'change', () => {
|
2019-10-12 00:35:25 +00:00
|
|
|
const modelValue = (el as any)._modelValue
|
|
|
|
const elementValue = getValue(el)
|
2019-10-14 21:07:34 +00:00
|
|
|
const checked = el.checked
|
2020-04-05 00:51:42 +00:00
|
|
|
const assign = el._assign
|
2019-10-12 00:35:25 +00:00
|
|
|
if (isArray(modelValue)) {
|
2019-10-14 21:07:34 +00:00
|
|
|
const index = looseIndexOf(modelValue, elementValue)
|
|
|
|
const found = index !== -1
|
|
|
|
if (checked && !found) {
|
2019-10-12 00:35:25 +00:00
|
|
|
assign(modelValue.concat(elementValue))
|
2019-10-14 21:07:34 +00:00
|
|
|
} else if (!checked && found) {
|
|
|
|
const filtered = [...modelValue]
|
|
|
|
filtered.splice(index, 1)
|
|
|
|
assign(filtered)
|
2019-10-12 00:35:25 +00:00
|
|
|
}
|
2020-09-14 15:16:50 +00:00
|
|
|
} else if (isSet(modelValue)) {
|
|
|
|
const found = modelValue.has(elementValue)
|
|
|
|
if (checked && !found) {
|
|
|
|
assign(modelValue.add(elementValue))
|
|
|
|
} else if (!checked && found) {
|
|
|
|
modelValue.delete(elementValue)
|
|
|
|
assign(modelValue)
|
|
|
|
}
|
2019-10-12 00:35:25 +00:00
|
|
|
} else {
|
2019-11-12 21:24:39 +00:00
|
|
|
assign(getCheckboxValue(el, checked))
|
2019-10-12 00:35:25 +00:00
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
})
|
|
|
|
},
|
2020-04-05 00:51:42 +00:00
|
|
|
beforeUpdate(el, binding, vnode) {
|
|
|
|
el._assign = getModelAssigner(vnode)
|
2020-04-06 13:55:19 +00:00
|
|
|
setChecked(el, binding, vnode)
|
2020-04-05 00:51:42 +00:00
|
|
|
}
|
2019-10-12 00:35:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function setChecked(
|
|
|
|
el: HTMLInputElement,
|
2019-10-26 20:00:07 +00:00
|
|
|
{ value, oldValue }: DirectiveBinding,
|
2019-10-12 00:35:25 +00:00
|
|
|
vnode: VNode
|
|
|
|
) {
|
|
|
|
// store the v-model value on the element so it can be accessed by the
|
|
|
|
// change listener.
|
|
|
|
;(el as any)._modelValue = value
|
2019-10-26 20:00:07 +00:00
|
|
|
if (isArray(value)) {
|
|
|
|
el.checked = looseIndexOf(value, vnode.props!.value) > -1
|
2020-09-14 15:16:50 +00:00
|
|
|
} else if (isSet(value)) {
|
|
|
|
el.checked = looseHas(value, vnode.props!.value)
|
2019-10-26 20:00:07 +00:00
|
|
|
} else if (value !== oldValue) {
|
2019-11-12 21:24:39 +00:00
|
|
|
el.checked = looseEqual(value, getCheckboxValue(el, true))
|
2019-10-26 20:00:07 +00:00
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 00:51:42 +00:00
|
|
|
export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
2020-08-24 21:12:16 +00:00
|
|
|
created(el, { value }, vnode) {
|
2019-10-11 21:55:20 +00:00
|
|
|
el.checked = looseEqual(value, vnode.props!.value)
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
2019-10-11 21:55:20 +00:00
|
|
|
addEventListener(el, 'change', () => {
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign(getValue(el))
|
2019-10-11 21:55:20 +00:00
|
|
|
})
|
|
|
|
},
|
2019-10-26 20:00:07 +00:00
|
|
|
beforeUpdate(el, { value, oldValue }, vnode) {
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
2019-10-26 20:00:07 +00:00
|
|
|
if (value !== oldValue) {
|
|
|
|
el.checked = looseEqual(value, vnode.props!.value)
|
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 00:51:42 +00:00
|
|
|
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
|
2020-10-07 19:06:41 +00:00
|
|
|
created(el, { modifiers: { number } }, vnode) {
|
2019-10-11 21:55:20 +00:00
|
|
|
addEventListener(el, 'change', () => {
|
|
|
|
const selectedVal = Array.prototype.filter
|
|
|
|
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
2020-10-07 19:06:41 +00:00
|
|
|
.map(
|
|
|
|
(o: HTMLOptionElement) =>
|
|
|
|
number ? toNumber(getValue(o)) : getValue(o)
|
|
|
|
)
|
2020-04-05 00:51:42 +00:00
|
|
|
el._assign(el.multiple ? selectedVal : selectedVal[0])
|
2019-10-11 21:55:20 +00:00
|
|
|
})
|
2020-08-24 21:12:16 +00:00
|
|
|
el._assign = getModelAssigner(vnode)
|
|
|
|
},
|
|
|
|
// set value in mounted & updated because <select> relies on its children
|
|
|
|
// <option>s.
|
|
|
|
mounted(el, { value }) {
|
|
|
|
setSelected(el, value)
|
2019-10-11 21:55:20 +00:00
|
|
|
},
|
2020-04-05 00:51:42 +00:00
|
|
|
beforeUpdate(el, _binding, vnode) {
|
|
|
|
el._assign = getModelAssigner(vnode)
|
|
|
|
},
|
2019-10-11 21:55:20 +00:00
|
|
|
updated(el, { value }) {
|
|
|
|
setSelected(el, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setSelected(el: HTMLSelectElement, value: any) {
|
|
|
|
const isMultiple = el.multiple
|
2020-09-14 15:16:50 +00:00
|
|
|
if (isMultiple && !isArray(value) && !isSet(value)) {
|
2019-10-11 21:55:20 +00:00
|
|
|
__DEV__ &&
|
|
|
|
warn(
|
2020-09-14 15:16:50 +00:00
|
|
|
`<select multiple v-model> expects an Array or Set value for its binding, ` +
|
2019-10-12 00:35:25 +00:00
|
|
|
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
|
2019-10-11 21:55:20 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for (let i = 0, l = el.options.length; i < l; i++) {
|
2019-10-12 00:35:25 +00:00
|
|
|
const option = el.options[i]
|
|
|
|
const optionValue = getValue(option)
|
2019-10-11 21:55:20 +00:00
|
|
|
if (isMultiple) {
|
2020-09-14 15:16:50 +00:00
|
|
|
if (isArray(value)) {
|
|
|
|
option.selected = looseIndexOf(value, optionValue) > -1
|
|
|
|
} else {
|
|
|
|
option.selected = looseHas(value, optionValue)
|
|
|
|
}
|
2019-10-11 21:55:20 +00:00
|
|
|
} else {
|
|
|
|
if (looseEqual(getValue(option), value)) {
|
2019-10-12 00:35:25 +00:00
|
|
|
el.selectedIndex = i
|
2019-10-11 21:55:20 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!isMultiple) {
|
|
|
|
el.selectedIndex = -1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// retrieve raw value set via :value bindings
|
|
|
|
function getValue(el: HTMLOptionElement | HTMLInputElement) {
|
|
|
|
return '_value' in el ? (el as any)._value : el.value
|
|
|
|
}
|
|
|
|
|
2019-11-12 21:24:39 +00:00
|
|
|
// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
|
|
|
|
function getCheckboxValue(
|
|
|
|
el: HTMLInputElement & { _trueValue?: any; _falseValue?: any },
|
|
|
|
checked: boolean
|
|
|
|
) {
|
|
|
|
const key = checked ? '_trueValue' : '_falseValue'
|
|
|
|
return key in el ? el[key] : checked
|
|
|
|
}
|
|
|
|
|
2019-10-16 06:12:26 +00:00
|
|
|
export const vModelDynamic: ObjectDirective<
|
2019-10-11 21:55:20 +00:00
|
|
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
|
|
> = {
|
2020-08-24 21:12:16 +00:00
|
|
|
created(el, binding, vnode) {
|
|
|
|
callModelHook(el, binding, vnode, null, 'created')
|
2019-10-11 21:55:20 +00:00
|
|
|
},
|
|
|
|
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')
|
|
|
|
}
|
2019-10-10 22:02:51 +00:00
|
|
|
}
|
|
|
|
|
2019-10-11 21:55:20 +00:00
|
|
|
function callModelHook(
|
|
|
|
el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
|
|
|
|
binding: DirectiveBinding,
|
|
|
|
vnode: VNode,
|
|
|
|
prevVNode: VNode | null,
|
2020-08-24 21:12:16 +00:00
|
|
|
hook: keyof ObjectDirective
|
2019-10-11 21:55:20 +00:00
|
|
|
) {
|
2019-10-16 06:12:26 +00:00
|
|
|
let modelToUse: ObjectDirective
|
2019-10-11 21:55:20 +00:00
|
|
|
switch (el.tagName) {
|
|
|
|
case 'SELECT':
|
|
|
|
modelToUse = vModelSelect
|
|
|
|
break
|
|
|
|
case 'TEXTAREA':
|
|
|
|
modelToUse = vModelText
|
|
|
|
break
|
|
|
|
default:
|
2020-08-24 21:12:16 +00:00
|
|
|
switch (vnode.props && vnode.props.type) {
|
2019-10-11 21:55:20 +00:00
|
|
|
case 'checkbox':
|
|
|
|
modelToUse = vModelCheckbox
|
|
|
|
break
|
|
|
|
case 'radio':
|
|
|
|
modelToUse = vModelRadio
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
modelToUse = vModelText
|
|
|
|
}
|
|
|
|
}
|
2020-03-18 16:30:20 +00:00
|
|
|
const fn = modelToUse[hook] as DirectiveHook
|
2019-10-11 21:55:20 +00:00
|
|
|
fn && fn(el, binding, vnode, prevVNode)
|
2019-10-10 22:02:51 +00:00
|
|
|
}
|
2020-03-16 22:36:19 +00:00
|
|
|
|
|
|
|
// SSR vnode transforms
|
|
|
|
if (__NODE_JS__) {
|
|
|
|
vModelText.getSSRProps = ({ value }) => ({ value })
|
|
|
|
|
|
|
|
vModelRadio.getSSRProps = ({ value }, vnode) => {
|
|
|
|
if (vnode.props && looseEqual(vnode.props.value, value)) {
|
|
|
|
return { checked: true }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
vModelCheckbox.getSSRProps = ({ value }, vnode) => {
|
|
|
|
if (isArray(value)) {
|
|
|
|
if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
|
|
|
|
return { checked: true }
|
|
|
|
}
|
2020-09-14 15:16:50 +00:00
|
|
|
} else if (isSet(value)) {
|
|
|
|
if (vnode.props && looseHas(value, vnode.props.value)) {
|
|
|
|
return { checked: true }
|
|
|
|
}
|
2020-03-16 22:36:19 +00:00
|
|
|
} else if (value) {
|
|
|
|
return { checked: true }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|