| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- import {
- type DirectiveBinding,
- type DirectiveHook,
- type ObjectDirective,
- type VNode,
- nextTick,
- warn,
- } from '@vue/runtime-core'
- import { addEventListener } from '../modules/events'
- import {
- invokeArrayFns,
- isArray,
- isSet,
- looseEqual,
- looseIndexOf,
- looseToNumber,
- } from '@vue/shared'
- type AssignerFn = (value: any) => void
- const getModelAssigner = (vnode: VNode): AssignerFn => {
- const fn =
- vnode.props!['onUpdate:modelValue'] ||
- (__COMPAT__ && vnode.props!['onModelCompat:input'])
- 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
- target.dispatchEvent(new Event('input'))
- }
- }
- const assignKey = Symbol('_assign')
- type ModelDirective<T> = ObjectDirective<
- T & { [assignKey]: AssignerFn; _assigning?: boolean }
- >
- // 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[assignKey] = getModelAssigner(vnode)
- const castToNumber =
- number || (vnode.props && vnode.props.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()
- }
- if (castToNumber) {
- domValue = looseToNumber(domValue)
- }
- el[assignKey](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: { lazy, trim, number } }, vnode) {
- el[assignKey] = getModelAssigner(vnode)
- // avoid clearing unresolved text. #2302
- if ((el as any).composing) return
- const elValue =
- number || el.type === 'number' ? looseToNumber(el.value) : el.value
- const newValue = value == null ? '' : value
- if (elValue === newValue) {
- return
- }
- if (document.activeElement === el && el.type !== 'range') {
- if (lazy) {
- return
- }
- if (trim && el.value.trim() === newValue) {
- return
- }
- }
- el.value = newValue
- },
- }
- export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
- // #4096 array checkboxes need to be deep traversed
- deep: true,
- created(el, _, vnode) {
- el[assignKey] = getModelAssigner(vnode)
- addEventListener(el, 'change', () => {
- const modelValue = (el as any)._modelValue
- const elementValue = getValue(el)
- const checked = el.checked
- const assign = el[assignKey]
- 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[assignKey] = 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<HTMLInputElement> = {
- created(el, { value }, vnode) {
- el.checked = looseEqual(value, vnode.props!.value)
- el[assignKey] = getModelAssigner(vnode)
- addEventListener(el, 'change', () => {
- el[assignKey](getValue(el))
- })
- },
- beforeUpdate(el, { value, oldValue }, vnode) {
- el[assignKey] = getModelAssigner(vnode)
- if (value !== oldValue) {
- el.checked = looseEqual(value, vnode.props!.value)
- }
- },
- }
- export const vModelSelect: ModelDirective<HTMLSelectElement> = {
- // <select multiple> value need to be deep traversed
- deep: true,
- created(el, { value, modifiers: { number } }, vnode) {
- const isSetModel = isSet(value)
- addEventListener(el, 'change', () => {
- const selectedVal = Array.prototype.filter
- .call(el.options, (o: HTMLOptionElement) => o.selected)
- .map((o: HTMLOptionElement) =>
- number ? looseToNumber(getValue(o)) : getValue(o),
- )
- el[assignKey](
- el.multiple
- ? isSetModel
- ? new Set(selectedVal)
- : selectedVal
- : selectedVal[0],
- )
- el._assigning = true
- nextTick(() => {
- el._assigning = false
- })
- })
- el[assignKey] = getModelAssigner(vnode)
- },
- // set value in mounted & updated because <select> relies on its children
- // <option>s.
- mounted(el, { value, modifiers: { number } }) {
- setSelected(el, value, number)
- },
- beforeUpdate(el, _binding, vnode) {
- el[assignKey] = getModelAssigner(vnode)
- },
- updated(el, { value, modifiers: { number } }) {
- if (!el._assigning) {
- setSelected(el, value, number)
- }
- },
- }
- function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
- const isMultiple = el.multiple
- const isArrayValue = isArray(value)
- if (isMultiple && !isArrayValue && !isSet(value)) {
- __DEV__ &&
- warn(
- `<select multiple v-model> expects an Array or Set 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) {
- if (isArrayValue) {
- const optionType = typeof optionValue
- // fast path for string / number values
- if (optionType === 'string' || optionType === 'number') {
- option.selected = value.includes(
- number ? looseToNumber(optionValue) : optionValue,
- )
- } else {
- option.selected = looseIndexOf(value, optionValue) > -1
- }
- } else {
- option.selected = value.has(optionValue)
- }
- } else if (looseEqual(getValue(option), value)) {
- if (el.selectedIndex !== i) el.selectedIndex = i
- return
- }
- }
- if (!isMultiple && el.selectedIndex !== -1) {
- 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
- }
- // 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
- }
- export const vModelDynamic: ObjectDirective<
- HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
- > = {
- created(el, binding, vnode) {
- callModelHook(el, binding, vnode, null, 'created')
- },
- 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 resolveDynamicModel(tagName: string, type: string | undefined) {
- switch (tagName) {
- case 'SELECT':
- return vModelSelect
- case 'TEXTAREA':
- return vModelText
- default:
- switch (type) {
- case 'checkbox':
- return vModelCheckbox
- case 'radio':
- return vModelRadio
- default:
- return vModelText
- }
- }
- }
- function callModelHook(
- el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
- binding: DirectiveBinding,
- vnode: VNode,
- prevVNode: VNode | null,
- hook: keyof ObjectDirective,
- ) {
- const modelToUse = resolveDynamicModel(
- el.tagName,
- vnode.props && vnode.props.type,
- )
- const fn = modelToUse[hook] as DirectiveHook
- fn && fn(el, binding, vnode, prevVNode)
- }
- // SSR vnode transforms, only used when user includes client-oriented render
- // function in SSR
- export function initVModelForSSR() {
- 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 }
- }
- } else if (isSet(value)) {
- if (vnode.props && value.has(vnode.props.value)) {
- return { checked: true }
- }
- } else if (value) {
- return { checked: true }
- }
- }
- vModelDynamic.getSSRProps = (binding, vnode) => {
- if (typeof vnode.type !== 'string') {
- return
- }
- const modelToUse = resolveDynamicModel(
- // resolveDynamicModel expects an uppercase tag name, but vnode.type is lowercase
- vnode.type.toUpperCase(),
- vnode.props && vnode.props.type,
- )
- if (modelToUse.getSSRProps) {
- return modelToUse.getSSRProps(binding, vnode)
- }
- }
- }
|