vuecssuiant-designantdreactantantd-vueenterprisefrontendui-designvue-antdvue-antd-uivue3vuecomponent
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
10 KiB
317 lines
10 KiB
import type { CSSProperties } from 'vue'; |
|
import { |
|
computed, |
|
defineComponent, |
|
getCurrentInstance, |
|
nextTick, |
|
shallowRef, |
|
watch, |
|
watchEffect, |
|
} from 'vue'; |
|
import ClearableLabeledInput from './ClearableLabeledInput'; |
|
import ResizableTextArea from './ResizableTextArea'; |
|
import { textAreaProps } from './inputProps'; |
|
import type { InputFocusOptions } from '../vc-input/utils/commonUtils'; |
|
import { fixControlledValue, resolveOnChange, triggerFocus } from '../vc-input/utils/commonUtils'; |
|
import classNames from '../_util/classNames'; |
|
import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; |
|
import type { FocusEventHandler } from '../_util/EventInterface'; |
|
import useConfigInject from '../config-provider/hooks/useConfigInject'; |
|
import omit from '../_util/omit'; |
|
import type { VueNode } from '../_util/type'; |
|
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; |
|
|
|
// CSSINJS |
|
import useStyle from './style'; |
|
import { useInjectDisabled } from '../config-provider/DisabledContext'; |
|
|
|
function fixEmojiLength(value: string, maxLength: number) { |
|
return [...(value || '')].slice(0, maxLength).join(''); |
|
} |
|
|
|
function setTriggerValue( |
|
isCursorInEnd: boolean, |
|
preValue: string, |
|
triggerValue: string, |
|
maxLength: number, |
|
) { |
|
let newTriggerValue = triggerValue; |
|
if (isCursorInEnd) { |
|
// 光标在尾部,直接截断 |
|
newTriggerValue = fixEmojiLength(triggerValue, maxLength); |
|
} else if ( |
|
[...(preValue || '')].length < triggerValue.length && |
|
[...(triggerValue || '')].length > maxLength |
|
) { |
|
// 光标在中间,如果最后的值超过最大值,则采用原先的值 |
|
newTriggerValue = preValue; |
|
} |
|
return newTriggerValue; |
|
} |
|
|
|
export default defineComponent({ |
|
compatConfig: { MODE: 3 }, |
|
name: 'ATextarea', |
|
inheritAttrs: false, |
|
props: textAreaProps(), |
|
setup(props, { attrs, expose, emit }) { |
|
const formItemContext = useInjectFormItemContext(); |
|
const formItemInputContext = FormItemInputContext.useInject(); |
|
const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); |
|
const stateValue = shallowRef(props.value ?? props.defaultValue); |
|
const resizableTextArea = shallowRef(); |
|
const mergedValue = shallowRef(''); |
|
const { prefixCls, size, direction } = useConfigInject('input', props); |
|
|
|
// Style |
|
const [wrapSSR, hashId] = useStyle(prefixCls); |
|
const disabled = useInjectDisabled(); |
|
|
|
const showCount = computed(() => { |
|
return (props.showCount as any) === '' || props.showCount || false; |
|
}); |
|
// Max length value |
|
const hasMaxLength = computed(() => Number(props.maxlength) > 0); |
|
const compositing = shallowRef(false); |
|
|
|
const oldCompositionValueRef = shallowRef<string>(); |
|
const oldSelectionStartRef = shallowRef<number>(0); |
|
const onInternalCompositionStart = (e: CompositionEvent) => { |
|
compositing.value = true; |
|
// 拼音输入前保存一份旧值 |
|
oldCompositionValueRef.value = mergedValue.value; |
|
// 保存旧的光标位置 |
|
oldSelectionStartRef.value = (e.currentTarget as any).selectionStart; |
|
emit('compositionstart', e); |
|
}; |
|
|
|
const onInternalCompositionEnd = (e: CompositionEvent) => { |
|
compositing.value = false; |
|
let triggerValue = (e.currentTarget as any).value; |
|
if (hasMaxLength.value) { |
|
const isCursorInEnd = |
|
oldSelectionStartRef.value >= props.maxlength + 1 || |
|
oldSelectionStartRef.value === oldCompositionValueRef.value?.length; |
|
triggerValue = setTriggerValue( |
|
isCursorInEnd, |
|
oldCompositionValueRef.value, |
|
triggerValue, |
|
props.maxlength, |
|
); |
|
} |
|
// Patch composition onChange when value changed |
|
if (triggerValue !== mergedValue.value) { |
|
setValue(triggerValue); |
|
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue); |
|
} |
|
|
|
emit('compositionend', e); |
|
}; |
|
const instance = getCurrentInstance(); |
|
watch( |
|
() => props.value, |
|
() => { |
|
if ('value' in instance.vnode.props || {}) { |
|
stateValue.value = props.value ?? ''; |
|
} |
|
}, |
|
); |
|
|
|
const focus = (option?: InputFocusOptions) => { |
|
triggerFocus(resizableTextArea.value?.textArea, option); |
|
}; |
|
|
|
const blur = () => { |
|
resizableTextArea.value?.textArea?.blur(); |
|
}; |
|
|
|
const setValue = (value: string | number, callback?: Function) => { |
|
if (stateValue.value === value) { |
|
return; |
|
} |
|
if (props.value === undefined) { |
|
stateValue.value = value; |
|
} else { |
|
nextTick(() => { |
|
if (resizableTextArea.value.textArea.value !== mergedValue.value) { |
|
resizableTextArea.value?.instance.update?.(); |
|
} |
|
}); |
|
} |
|
nextTick(() => { |
|
callback && callback(); |
|
}); |
|
}; |
|
|
|
const handleKeyDown = (e: KeyboardEvent) => { |
|
if (e.keyCode === 13) { |
|
emit('pressEnter', e); |
|
} |
|
emit('keydown', e); |
|
}; |
|
|
|
const onBlur: FocusEventHandler = e => { |
|
const { onBlur } = props; |
|
onBlur?.(e); |
|
formItemContext.onFieldBlur(); |
|
}; |
|
const triggerChange = (e: Event) => { |
|
emit('update:value', (e.target as HTMLInputElement).value); |
|
emit('change', e); |
|
emit('input', e); |
|
formItemContext.onFieldChange(); |
|
}; |
|
|
|
const handleReset = (e: MouseEvent) => { |
|
resolveOnChange(resizableTextArea.value.textArea, e, triggerChange); |
|
setValue('', () => { |
|
focus(); |
|
}); |
|
}; |
|
|
|
const handleChange = (e: Event) => { |
|
let triggerValue = (e.target as any).value; |
|
if (stateValue.value === triggerValue) return; |
|
|
|
if (hasMaxLength.value) { |
|
// 1. 复制粘贴超过maxlength的情况 2.未超过maxlength的情况 |
|
const target = e.target as any; |
|
const isCursorInEnd = |
|
target.selectionStart >= props.maxlength + 1 || |
|
target.selectionStart === triggerValue.length || |
|
!target.selectionStart; |
|
triggerValue = setTriggerValue( |
|
isCursorInEnd, |
|
mergedValue.value, |
|
triggerValue, |
|
props.maxlength, |
|
); |
|
} |
|
resolveOnChange(e.currentTarget as any, e, triggerChange, triggerValue); |
|
setValue(triggerValue); |
|
}; |
|
const renderTextArea = () => { |
|
const { class: customClass } = attrs; |
|
const { bordered = true } = props; |
|
const resizeProps = { |
|
...omit(props, ['allowClear']), |
|
...attrs, |
|
class: [ |
|
{ |
|
[`${prefixCls.value}-borderless`]: !bordered, |
|
[`${customClass}`]: customClass && !showCount.value, |
|
[`${prefixCls.value}-sm`]: size.value === 'small', |
|
[`${prefixCls.value}-lg`]: size.value === 'large', |
|
}, |
|
getStatusClassNames(prefixCls.value, mergedStatus.value), |
|
hashId.value, |
|
], |
|
disabled: disabled.value, |
|
showCount: null, |
|
prefixCls: prefixCls.value, |
|
onInput: handleChange, |
|
onChange: handleChange, |
|
onBlur, |
|
onKeydown: handleKeyDown, |
|
onCompositionstart: onInternalCompositionStart, |
|
onCompositionend: onInternalCompositionEnd, |
|
}; |
|
if (props.valueModifiers?.lazy) { |
|
delete resizeProps.onInput; |
|
} |
|
return ( |
|
<ResizableTextArea |
|
{...resizeProps} |
|
id={resizeProps?.id ?? formItemContext.id.value} |
|
ref={resizableTextArea} |
|
maxlength={props.maxlength} |
|
lazy={props.lazy} |
|
/> |
|
); |
|
}; |
|
|
|
expose({ |
|
focus, |
|
blur, |
|
resizableTextArea, |
|
}); |
|
|
|
watchEffect(() => { |
|
let val = fixControlledValue(stateValue.value); |
|
if ( |
|
!compositing.value && |
|
hasMaxLength.value && |
|
(props.value === null || props.value === undefined) |
|
) { |
|
// fix #27612 将value转为数组进行截取,解决 '😂'.length === 2 等emoji表情导致的截取乱码的问题 |
|
val = fixEmojiLength(val, props.maxlength); |
|
} |
|
mergedValue.value = val; |
|
}); |
|
return () => { |
|
const { maxlength, bordered = true, hidden } = props; |
|
const { style, class: customClass } = attrs; |
|
const inputProps: any = { |
|
...props, |
|
...attrs, |
|
prefixCls: prefixCls.value, |
|
inputType: 'text', |
|
handleReset, |
|
direction: direction.value, |
|
bordered, |
|
style: showCount.value ? undefined : style, |
|
hashId: hashId.value, |
|
disabled: props.disabled ?? disabled.value, |
|
}; |
|
|
|
let textareaNode = ( |
|
<ClearableLabeledInput |
|
{...inputProps} |
|
value={mergedValue.value} |
|
v-slots={{ element: renderTextArea }} |
|
status={props.status} |
|
/> |
|
); |
|
|
|
if (showCount.value || formItemInputContext.hasFeedback) { |
|
const valueLength = [...mergedValue.value].length; |
|
let dataCount: VueNode = ''; |
|
if (typeof showCount.value === 'object') { |
|
dataCount = showCount.value.formatter({ |
|
value: mergedValue.value, |
|
count: valueLength, |
|
maxlength, |
|
}); |
|
} else { |
|
dataCount = `${valueLength}${hasMaxLength.value ? ` / ${maxlength}` : ''}`; |
|
} |
|
textareaNode = ( |
|
<div |
|
hidden={hidden} |
|
class={classNames( |
|
`${prefixCls.value}-textarea`, |
|
{ |
|
[`${prefixCls.value}-textarea-rtl`]: direction.value === 'rtl', |
|
[`${prefixCls.value}-textarea-show-count`]: showCount.value, |
|
[`${prefixCls.value}-textarea-in-form-item`]: formItemInputContext.isFormItemInput, |
|
}, |
|
`${prefixCls.value}-textarea-show-count`, |
|
customClass, |
|
hashId.value, |
|
)} |
|
style={style as CSSProperties} |
|
data-count={typeof dataCount !== 'object' ? dataCount : undefined} |
|
> |
|
{textareaNode} |
|
{formItemInputContext.hasFeedback && ( |
|
<span class={`${prefixCls.value}-textarea-suffix`}> |
|
{formItemInputContext.feedbackIcon} |
|
</span> |
|
)} |
|
</div> |
|
); |
|
} |
|
return wrapSSR(textareaNode); |
|
}; |
|
}, |
|
});
|
|
|