feat: input add clearIcon & status

pull/5820/head
tangjinzhou 2022-05-18 22:51:45 +08:00
parent e9d41efcec
commit 093fa555ba
27 changed files with 1088 additions and 595 deletions

View File

@ -1,9 +1,14 @@
import { inject, provide } from 'vue';
import { inject, provide, reactive, watchEffect } from 'vue';
function createContext<T>(defaultValue?: T) {
function createContext<T extends Record<string, any>>(defaultValue?: T) {
const contextKey = Symbol('contextKey');
const useProvide = (props: T) => {
provide(contextKey, props);
const useProvide = (props: T, newProps?: T) => {
const mergedProps = reactive<T>({} as T);
provide(contextKey, mergedProps);
watchEffect(() => {
Object.assign(mergedProps, props, newProps || {});
});
return mergedProps;
};
const useInject = () => {
return inject(contextKey, defaultValue as T) || ({} as T);

View File

@ -2,7 +2,7 @@ import Select from '../select';
import { Group, Button } from '../radio';
import type { CalendarMode } from './generateCalendar';
import type { Ref } from 'vue';
import { reactive, watchEffect, defineComponent, ref } from 'vue';
import { defineComponent, ref } from 'vue';
import type { Locale } from '../vc-picker/interface';
import type { GenerateConfig } from '../vc-picker/generate';
import { FormItemInputContext } from '../form/FormItemContext';
@ -170,13 +170,7 @@ export default defineComponent<CalendarHeaderProps<any>>({
setup(_props, { attrs }) {
const divRef = ref<HTMLDivElement>(null);
const formItemInputContext = FormItemInputContext.useInject();
const newFormItemInputContext = reactive({});
FormItemInputContext.useProvide(newFormItemInputContext);
watchEffect(() => {
Object.assign(newFormItemInputContext, formItemInputContext, {
isFormItemInput: false,
});
});
FormItemInputContext.useProvide(formItemInputContext, { isFormItemInput: false });
return () => {
const props = { ..._props, ...attrs };

View File

@ -1,4 +1,4 @@
import type { ComputedRef, InjectionKey, ConcreteComponent, FunctionalComponent } from 'vue';
import type { ComputedRef, InjectionKey, ConcreteComponent } from 'vue';
import {
watch,
computed,
@ -115,7 +115,12 @@ export interface FormItemStatusContextProps {
export const FormItemInputContext = createContext<FormItemStatusContextProps>({});
export const NoFormStatus: FunctionalComponent = (_, { slots }) => {
FormItemInputContext.useProvide({});
return slots.default?.();
};
export const NoFormStatus = defineComponent({
name: 'NoFormStatus',
setup(_, { slots }) {
FormItemInputContext.useProvide({});
return () => {
return slots.default?.();
};
},
});

View File

@ -3,11 +3,14 @@ import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import PropTypes from '../_util/vue-types';
import { cloneElement } from '../_util/vnode';
import type { PropType, VNode } from 'vue';
import { ref, defineComponent } from 'vue';
import { defineComponent } from 'vue';
import { tuple } from '../_util/type';
import type { Direction, SizeType } from '../config-provider';
import type { MouseEventHandler } from '../_util/EventInterface';
import { getInputClassName, hasAddon, hasPrefixSuffix } from './util';
import { hasAddon } from './util';
import { FormItemInputContext } from '../form/FormItemContext';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
const ClearableInputType = ['text', 'input'];
@ -34,20 +37,13 @@ export default defineComponent({
bordered: { type: Boolean, default: true },
triggerFocus: { type: Function as PropType<() => void> },
hidden: Boolean,
status: String as PropType<InputStatus>,
},
setup(props, { slots, attrs }) {
const containerRef = ref();
const onInputMouseUp: MouseEventHandler = e => {
if (containerRef.value?.contains(e.target as Element)) {
const { triggerFocus } = props;
triggerFocus?.();
}
};
const statusContext = FormItemInputContext.useInject();
const renderClearIcon = (prefixCls: string) => {
const { allowClear, value, disabled, readonly, handleReset, suffix = slots.suffix } = props;
if (!allowClear) {
return null;
}
const { value, disabled, readonly, handleReset, suffix = slots.suffix } = props;
const needClear = !disabled && !readonly && value;
const className = `${prefixCls}-clear-icon`;
return (
@ -66,123 +62,6 @@ export default defineComponent({
/>
);
};
const renderSuffix = (prefixCls: string) => {
const { suffix = slots.suffix?.(), allowClear } = props;
if (suffix || allowClear) {
return (
<span class={`${prefixCls}-suffix`}>
{renderClearIcon(prefixCls)}
{suffix}
</span>
);
}
return null;
};
const renderLabeledIcon = (prefixCls: string, element: VNode) => {
const {
focused,
value,
prefix = slots.prefix?.(),
size,
suffix = slots.suffix?.(),
disabled,
allowClear,
direction,
readonly,
bordered,
hidden,
addonAfter = slots.addonAfter,
addonBefore = slots.addonBefore,
} = props;
const suffixNode = renderSuffix(prefixCls);
if (!hasPrefixSuffix({ prefix, suffix, allowClear })) {
return cloneElement(element, {
value,
});
}
const prefixNode = prefix ? <span class={`${prefixCls}-prefix`}>{prefix}</span> : null;
const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, {
[`${prefixCls}-affix-wrapper-focused`]: focused,
[`${prefixCls}-affix-wrapper-disabled`]: disabled,
[`${prefixCls}-affix-wrapper-sm`]: size === 'small',
[`${prefixCls}-affix-wrapper-lg`]: size === 'large',
[`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-readonly`]: readonly,
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
// className will go to addon wrapper
[`${attrs.class}`]: !hasAddon({ addonAfter, addonBefore }) && attrs.class,
});
return (
<span
ref={containerRef}
class={affixWrapperCls}
style={attrs.style}
onMouseup={onInputMouseUp}
hidden={hidden}
>
{prefixNode}
{cloneElement(element, {
style: null,
value,
class: getInputClassName(prefixCls, bordered, size, disabled),
})}
{suffixNode}
</span>
);
};
const renderInputWithLabel = (prefixCls: string, labeledElement: VNode) => {
const {
addonBefore = slots.addonBefore?.(),
addonAfter = slots.addonAfter?.(),
size,
direction,
hidden,
} = props;
// Not wrap when there is not addons
if (!hasAddon({ addonBefore, addonAfter })) {
return labeledElement;
}
const wrapperClassName = `${prefixCls}-group`;
const addonClassName = `${wrapperClassName}-addon`;
const addonBeforeNode = addonBefore ? (
<span class={addonClassName}>{addonBefore}</span>
) : null;
const addonAfterNode = addonAfter ? <span class={addonClassName}>{addonAfter}</span> : null;
const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, wrapperClassName, {
[`${wrapperClassName}-rtl`]: direction === 'rtl',
});
const mergedGroupClassName = classNames(
`${prefixCls}-group-wrapper`,
{
[`${prefixCls}-group-wrapper-sm`]: size === 'small',
[`${prefixCls}-group-wrapper-lg`]: size === 'large',
[`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl',
},
attrs.class,
);
// Need another wrapper for changing display:table to display:inline-block
// and put style prop in wrapper
return (
<span class={mergedGroupClassName} style={attrs.style} hidden={hidden}>
<span class={mergedWrapperClassName}>
{addonBeforeNode}
{cloneElement(labeledElement, { style: null })}
{addonAfterNode}
</span>
</span>
);
};
const renderTextAreaWithClearIcon = (prefixCls: string, element: VNode) => {
const {
value,
@ -190,9 +69,13 @@ export default defineComponent({
direction,
bordered,
hidden,
status: customStatus,
addonAfter = slots.addonAfter,
addonBefore = slots.addonBefore,
} = props;
const { status: contextStatus, hasFeedback } = statusContext;
if (!allowClear) {
return cloneElement(element, {
value,
@ -201,6 +84,11 @@ export default defineComponent({
const affixWrapperCls = classNames(
`${prefixCls}-affix-wrapper`,
`${prefixCls}-affix-wrapper-textarea-with-clear-btn`,
getStatusClassNames(
`${prefixCls}-affix-wrapper`,
getMergedStatus(contextStatus, customStatus),
hasFeedback,
),
{
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
@ -209,7 +97,7 @@ export default defineComponent({
},
);
return (
<span class={affixWrapperCls} style={attrs.style} hidden={hidden}>
<span className={affixWrapperCls} style={attrs.style} hidden={hidden}>
{cloneElement(element, {
style: null,
value,
@ -224,7 +112,7 @@ export default defineComponent({
if (inputType === ClearableInputType[0]) {
return renderTextAreaWithClearIcon(prefixCls, element);
}
return renderInputWithLabel(prefixCls, renderLabeledIcon(prefixCls, element));
return null;
};
},
});

View File

@ -1,6 +1,7 @@
import type { PropType } from 'vue';
import { computed, defineComponent } from 'vue';
import type { SizeType } from '../config-provider';
import { FormItemInputContext } from '../form/FormItemContext';
import type { FocusEventHandler, MouseEventHandler } from '../_util/EventInterface';
import useConfigInject from '../_util/hooks/useConfigInject';
@ -17,6 +18,10 @@ export default defineComponent({
},
setup(props, { slots }) {
const { prefixCls, direction } = useConfigInject('input-group', props);
const formItemInputContext = FormItemInputContext.useInject();
FormItemInputContext.useProvide(formItemInputContext, {
isFormItemInput: false,
});
const cls = computed(() => {
const pre = prefixCls.value;
return {

View File

@ -1,108 +1,18 @@
import type { VNode } from 'vue';
import {
getCurrentInstance,
onBeforeUnmount,
onMounted,
watch,
ref,
defineComponent,
nextTick,
withDirectives,
} from 'vue';
import antInputDirective from '../_util/antInputDirective';
import { onBeforeUpdate, computed, onBeforeUnmount, onMounted, ref, defineComponent } from 'vue';
import classNames from '../_util/classNames';
import type { InputProps } from './inputProps';
import inputProps from './inputProps';
import { getInputClassName } from './util';
import ClearableLabeledInput from './ClearableLabeledInput';
import { useInjectFormItemContext } from '../form/FormItemContext';
import omit from '../_util/omit';
import {
FormItemInputContext,
NoFormStatus,
useInjectFormItemContext,
} from '../form/FormItemContext';
import useConfigInject from '../_util/hooks/useConfigInject';
import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface';
export function fixControlledValue(value: string | number) {
if (typeof value === 'undefined' || value === null) {
return '';
}
return String(value);
}
export function resolveOnChange(
target: HTMLInputElement,
e: Event,
onChange: Function,
targetValue?: string,
) {
if (!onChange) {
return;
}
const event: any = e;
if (e.type === 'click') {
Object.defineProperty(event, 'target', {
writable: true,
});
Object.defineProperty(event, 'currentTarget', {
writable: true,
});
// click clear icon
//event = Object.create(e);
const currentTarget = target.cloneNode(true);
event.target = currentTarget;
event.currentTarget = currentTarget;
// change target ref value cause e.target.value should be '' when clear input
(currentTarget as any).value = '';
onChange(event);
return;
}
// Trigger by composition event, this means we need force change the input value
if (targetValue !== undefined) {
Object.defineProperty(event, 'target', {
writable: true,
});
Object.defineProperty(event, 'currentTarget', {
writable: true,
});
event.target = target;
event.currentTarget = target;
target.value = targetValue;
onChange(event);
return;
}
onChange(event);
}
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
export function triggerFocus(
element?: HTMLInputElement | HTMLTextAreaElement,
option?: InputFocusOptions,
) {
if (!element) return;
element.focus(option);
// Selection content
const { cursor } = option || {};
if (cursor) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import type { InputFocusOptions } from '../vc-input/utils/commonUtils';
import { hasPrefixSuffix } from '../vc-input/utils/commonUtils';
import VcInput from '../vc-input/Input';
import inputProps from './inputProps';
import omit from '../_util/omit';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
export default defineComponent({
name: 'AInput',
@ -110,45 +20,13 @@ export default defineComponent({
props: inputProps(),
setup(props, { slots, attrs, expose, emit }) {
const inputRef = ref();
const clearableInputRef = ref();
let removePasswordTimeout: any;
const formItemContext = useInjectFormItemContext();
const formItemInputContext = FormItemInputContext.useInject();
const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status));
const { direction, prefixCls, size, autocomplete } = useConfigInject('input', props);
const stateValue = ref(props.value === undefined ? props.defaultValue : props.value);
const focused = ref(false);
watch(
() => props.value,
() => {
stateValue.value = props.value;
},
);
watch(
() => props.disabled,
() => {
if (props.value !== undefined) {
stateValue.value = props.value;
}
if (props.disabled) {
focused.value = false;
}
},
);
const clearPasswordValueAttribute = () => {
// https://github.com/ant-design/ant-design/issues/20541
removePasswordTimeout = setTimeout(() => {
if (
inputRef.value?.getAttribute('type') === 'password' &&
inputRef.value.hasAttribute('value')
) {
inputRef.value.removeAttribute('value');
}
});
};
const focus = (option?: InputFocusOptions) => {
triggerFocus(inputRef.value, option);
inputRef.value?.focus(option);
};
const blur = () => {
@ -171,28 +49,42 @@ export default defineComponent({
focus,
blur,
input: inputRef,
stateValue,
setSelectionRange,
select,
});
// ===================== Remove Password value =====================
const removePasswordTimeoutRef = ref<any[]>([]);
const removePasswordTimeout = () => {
removePasswordTimeoutRef.value.push(
setTimeout(() => {
if (
inputRef.value?.input &&
inputRef.value?.input.getAttribute('type') === 'password' &&
inputRef.value?.input.hasAttribute('value')
) {
inputRef.value?.input.removeAttribute('value');
}
}),
);
};
onMounted(() => {
removePasswordTimeout();
});
onBeforeUpdate(() => {
removePasswordTimeoutRef.value.forEach(item => clearTimeout(item));
});
onBeforeUnmount(() => {
removePasswordTimeoutRef.value.forEach(item => clearTimeout(item));
});
const onFocus: FocusEventHandler = e => {
const { onFocus } = props;
focused.value = true;
onFocus?.(e);
nextTick(() => {
clearPasswordValueAttribute();
});
const handleBlur = (e: FocusEvent) => {
removePasswordTimeout();
emit('blur', e);
};
const onBlur: FocusEventHandler = e => {
const { onBlur } = props;
focused.value = false;
onBlur?.(e);
formItemContext.onFieldBlur();
nextTick(() => {
clearPasswordValueAttribute();
});
const handleFocus = (e: FocusEvent) => {
removePasswordTimeout();
emit('focus', e);
};
const triggerChange = (e: Event) => {
@ -201,168 +93,74 @@ export default defineComponent({
emit('input', e);
formItemContext.onFieldChange();
};
const instance = getCurrentInstance();
const setValue = (value: string | number, callback?: Function) => {
if (stateValue.value === value) {
return;
}
if (props.value === undefined) {
stateValue.value = value;
} else {
nextTick(() => {
if (inputRef.value.value !== stateValue.value) {
instance.update();
}
});
}
nextTick(() => {
callback && callback();
});
};
const handleReset = (e: MouseEvent) => {
resolveOnChange(inputRef.value, e, triggerChange);
setValue('', () => {
focus();
});
};
const handleChange = (e: ChangeEvent) => {
const { value, composing } = e.target as any;
// https://github.com/vueComponent/ant-design-vue/issues/2203
if ((((e as any).isComposing || composing) && props.lazy) || stateValue.value === value)
return;
const newVal = e.target.value;
resolveOnChange(inputRef.value, e, triggerChange);
setValue(newVal, () => {
clearPasswordValueAttribute();
});
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
emit('pressEnter', e);
}
emit('keydown', e);
};
onMounted(() => {
if (process.env.NODE_ENV === 'test') {
if (props.autofocus) {
focus();
}
}
clearPasswordValueAttribute();
});
onBeforeUnmount(() => {
clearTimeout(removePasswordTimeout);
});
const renderInput = () => {
const {
addonBefore = slots.addonBefore,
addonAfter = slots.addonAfter,
disabled,
bordered = true,
valueModifiers = {},
htmlSize,
} = props;
const otherProps = omit(props as InputProps & { placeholder: string }, [
'prefixCls',
'onPressEnter',
'addonBefore',
'addonAfter',
'prefix',
'suffix',
'allowClear',
// Input elements must be either controlled or uncontrolled,
// specify either the value prop, or the defaultValue prop, but not both.
'defaultValue',
'size',
'bordered',
'htmlSize',
'lazy',
'showCount',
'valueModifiers',
]);
const inputProps = {
...otherProps,
...attrs,
autocomplete: autocomplete.value,
onChange: handleChange,
onInput: handleChange,
onFocus,
onBlur,
onKeydown: handleKeyDown,
class: classNames(
getInputClassName(prefixCls.value, bordered, size.value, disabled, direction.value),
{
[attrs.class as string]: attrs.class && !addonBefore && !addonAfter,
},
),
ref: inputRef,
key: 'ant-input',
size: htmlSize,
id: otherProps.id ?? formItemContext.id.value,
};
if (valueModifiers.lazy) {
delete inputProps.onInput;
}
if (!inputProps.autofocus) {
delete inputProps.autofocus;
}
const inputNode = <input {...omit(inputProps, ['size'])} />;
return withDirectives(inputNode as VNode, [[antInputDirective]]);
};
const renderShowCountSuffix = () => {
const value = stateValue.value;
const { maxlength, suffix = slots.suffix?.(), showCount } = props;
// Max length value
const hasMaxLength = Number(maxlength) > 0;
if (suffix || showCount) {
const valueLength = [...fixControlledValue(value)].length;
let dataCount = null;
if (typeof showCount === 'object') {
dataCount = showCount.formatter({ count: valueLength, maxlength });
} else {
dataCount = `${valueLength}${hasMaxLength ? ` / ${maxlength}` : ''}`;
}
return (
<>
{!!showCount && (
<span
class={classNames(`${prefixCls.value}-show-count-suffix`, {
[`${prefixCls.value}-show-count-has-suffix`]: !!suffix,
})}
>
{dataCount}
</span>
)}
{suffix}
</>
);
}
return null;
};
return () => {
const inputProps: any = {
...attrs,
...props,
prefixCls: prefixCls.value,
inputType: 'input',
value: fixControlledValue(stateValue.value),
handleReset,
focused: focused.value && !props.disabled,
};
const { hasFeedback, feedbackIcon } = formItemInputContext;
const {
allowClear,
bordered = true,
prefix = slots.prefix?.(),
suffix = slots.suffix?.(),
addonAfter = slots.addonAfter?.(),
addonBefore = slots.addonBefore?.(),
id = formItemContext.id?.value,
...rest
} = props;
const suffixNode = (hasFeedback || suffix) && (
<>
{suffix}
{hasFeedback && feedbackIcon}
</>
);
const prefixClsValue = prefixCls.value;
const inputHasPrefixSuffix = hasPrefixSuffix({ prefix, suffix }) || !!hasFeedback;
const clearIcon = slots.clearIcon || (() => <CloseCircleFilled />);
return (
<ClearableLabeledInput
{...omit(inputProps, ['element', 'valueModifiers', 'suffix', 'showCount'])}
ref={clearableInputRef}
v-slots={{ ...slots, element: renderInput, suffix: renderShowCountSuffix }}
/>
<VcInput
{...attrs}
{...omit(rest, ['onUpdate:value', 'onChange', 'onInput'])}
onChange={triggerChange}
id={id}
ref={inputRef}
prefixCls={prefixClsValue}
autocomplete={autocomplete.value}
onBlur={handleBlur}
onFocus={handleFocus}
suffix={suffixNode}
allowClear={allowClear}
addonAfter={addonAfter && <NoFormStatus>{addonAfter}</NoFormStatus>}
addonBefore={addonBefore && <NoFormStatus>{addonBefore}</NoFormStatus>}
inputClassName={classNames(
{
[`${prefixClsValue}-sm`]: size.value === 'small',
[`${prefixClsValue}-lg`]: size.value === 'large',
[`${prefixClsValue}-rtl`]: direction.value === 'rtl',
[`${prefixClsValue}-borderless`]: !bordered,
},
!inputHasPrefixSuffix && getStatusClassNames(prefixClsValue, mergedStatus.value),
)}
affixWrapperClassName={classNames(
{
[`${prefixClsValue}-affix-wrapper-sm`]: size.value === 'small',
[`${prefixClsValue}-affix-wrapper-lg`]: size.value === 'large',
[`${prefixClsValue}-affix-wrapper-rtl`]: direction.value === 'rtl',
[`${prefixClsValue}-affix-wrapper-borderless`]: !bordered,
},
getStatusClassNames(`${prefixClsValue}-affix-wrapper`, mergedStatus.value, hasFeedback),
)}
wrapperClassName={classNames({
[`${prefixClsValue}-group-rtl`]: direction.value === 'rtl',
})}
groupClassName={classNames(
{
[`${prefixClsValue}-group-wrapper-sm`]: size.value === 'small',
[`${prefixClsValue}-group-wrapper-lg`]: size.value === 'large',
[`${prefixClsValue}-group-wrapper-rtl`]: direction.value === 'rtl',
},
getStatusClassNames(`${prefixClsValue}-group-wrapper`, mergedStatus.value, hasFeedback),
)}
v-slots={{ ...slots, clearIcon }}
></VcInput>
);
};
},

View File

@ -3,15 +3,19 @@ import { computed, ref, defineComponent } from 'vue';
import classNames from '../_util/classNames';
import Input from './Input';
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';
import inputProps from './inputProps';
import Button from '../button';
import { cloneElement } from '../_util/vnode';
import PropTypes from '../_util/vue-types';
import isPlainObject from 'lodash-es/isPlainObject';
import type { ChangeEvent, MouseEventHandler } from '../_util/EventInterface';
import type {
ChangeEvent,
CompositionEventHandler,
MouseEventHandler,
} from '../_util/EventInterface';
import useConfigInject from '../_util/hooks/useConfigInject';
import omit from '../_util/omit';
import isMobile from '../_util/isMobile';
import inputProps from './inputProps';
export default defineComponent({
name: 'AInputSearch',
@ -29,6 +33,7 @@ export default defineComponent({
},
setup(props, { slots, attrs, expose, emit }) {
const inputRef = ref();
const composedRef = ref(false);
const focus = () => {
inputRef.value?.focus();
};
@ -55,12 +60,28 @@ export default defineComponent({
};
const onSearch = (e: MouseEvent | KeyboardEvent) => {
emit('search', inputRef.value?.stateValue, e);
emit('search', inputRef.value?.input?.stateValue, e);
if (!isMobile.tablet) {
inputRef.value.focus();
}
};
const onPressEnter = (e: KeyboardEvent) => {
if (composedRef.value) {
return;
}
onSearch(e);
};
const handleOnCompositionStart: CompositionEventHandler = e => {
composedRef.value = true;
emit('compositionstart', e);
};
const handleOnCompositionEnd: CompositionEventHandler = e => {
composedRef.value = false;
emit('compositionend', e);
};
const { prefixCls, getPrefixCls, direction, size } = useConfigInject('input-search', props);
const inputPrefixCls = computed(() => getPrefixCls('input', props.inputPrefixCls));
return () => {
@ -133,7 +154,9 @@ export default defineComponent({
ref={inputRef}
{...omit(restProps, ['onUpdate:value', 'onSearch', 'enterButton'])}
{...attrs}
onPressEnter={onSearch}
onPressEnter={onPressEnter}
onCompositionstart={handleOnCompositionStart}
onCompositionend={handleOnCompositionEnd}
size={size.value}
prefixCls={inputPrefixCls.value}
addonAfter={button}

View File

@ -11,14 +11,15 @@ import {
import ClearableLabeledInput from './ClearableLabeledInput';
import ResizableTextArea from './ResizableTextArea';
import { textAreaProps } from './inputProps';
import type { InputFocusOptions } from './Input';
import { fixControlledValue, resolveOnChange, triggerFocus } from './Input';
import type { InputFocusOptions } from '../vc-input/utils/commonUtils';
import { fixControlledValue, resolveOnChange, triggerFocus } from '../vc-input/utils/commonUtils';
import classNames from '../_util/classNames';
import { useInjectFormItemContext } from '../form/FormItemContext';
import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext';
import type { FocusEventHandler } from '../_util/EventInterface';
import useConfigInject from '../_util/hooks/useConfigInject';
import omit from '../_util/omit';
import type { VueNode } from '../_util/type';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
function fixEmojiLength(value: string, maxLength: number) {
return [...(value || '')].slice(0, maxLength).join('');
@ -50,6 +51,8 @@ export default defineComponent({
props: textAreaProps(),
setup(props, { attrs, expose, emit }) {
const formItemContext = useInjectFormItemContext();
const formItemInputContext = FormItemInputContext.useInject();
const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status));
const stateValue = ref(props.value === undefined ? props.defaultValue : props.value);
const resizableTextArea = ref();
const mergedValue = ref('');
@ -186,12 +189,15 @@ export default defineComponent({
...omit(props, ['allowClear']),
...attrs,
style: showCount.value ? {} : style,
class: {
[`${prefixCls.value}-borderless`]: !bordered,
[`${customClass}`]: customClass && !showCount.value,
[`${prefixCls.value}-sm`]: size.value === 'small',
[`${prefixCls.value}-lg`]: size.value === 'large',
},
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),
],
showCount: null,
prefixCls: prefixCls.value,
onInput: handleChange,
@ -259,10 +265,11 @@ export default defineComponent({
{...inputProps}
value={mergedValue.value}
v-slots={{ element: renderTextArea }}
status={props.status}
/>
);
if (showCount.value) {
if (showCount.value || formItemInputContext.hasFeedback) {
const valueLength = [...mergedValue.value].length;
let dataCount: VueNode = '';
if (typeof showCount.value === 'object') {
@ -277,6 +284,8 @@ export default defineComponent({
`${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,
@ -285,6 +294,11 @@ export default defineComponent({
data-count={typeof dataCount !== 'object' ? dataCount : undefined}
>
{textareaNode}
{formItemInputContext.hasFeedback && (
<span class={`${prefixCls.value}-textarea-suffix`}>
{formItemInputContext.feedbackIcon}
</span>
)}
</div>
);
}

View File

@ -16,15 +16,26 @@ Basic usage example.
</docs>
<template>
<a-input v-model:value="value" placeholder="Basic usage" />
<a-space direction="vertical">
<a-input v-model:value="value" placeholder="Basic usage" />
<a-input v-model:value.lazy="value1" autofocus placeholder="Lazy usage" />
</a-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { watch, defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const value = ref<string>('');
const value1 = ref<string>('');
watch(value, () => {
console.log(value.value);
});
watch(value1, () => {
console.log(value1.value);
});
return {
value,
value1,
};
},
});

View File

@ -18,7 +18,7 @@ Note: You don't need `Col` to control the width in the `compact` mode.
</docs>
<template>
<div>
<div class="site-input-group-wrapper">
<a-input-group size="large">
<a-row :gutter="8">
<a-col :span="5">
@ -79,13 +79,15 @@ Note: You don't need `Col` to control the width in the `compact` mode.
/>
<a-input
v-model:value="value13"
style="width: 30px; border-left: 0; pointer-events: none; background-color: #fff"
class="site-input-split"
style="width: 30px; border-left: 0; pointer-events: none"
placeholder="~"
disabled
/>
<a-input
v-model:value="value14"
style="width: 100px; text-align: center; border-left: 0"
class="site-input-right"
style="width: 100px; text-align: center"
placeholder="Maximum"
/>
</a-input-group>
@ -221,3 +223,29 @@ export default defineComponent({
},
});
</script>
<style scoped>
.site-input-group-wrapper .site-input-split {
background-color: #fff;
}
.site-input-group-wrapper .site-input-right {
border-left-width: 0;
}
.site-input-group-wrapper .site-input-right:hover,
.site-input-group-wrapper .site-input-right:focus {
border-left-width: 1px;
}
.site-input-group-wrapper .ant-input-rtl.site-input-right {
border-right-width: 0;
}
.site-input-group-wrapper .ant-input-rtl.site-input-right:hover,
.site-input-group-wrapper .ant-input-rtl.site-input-right:focus {
border-right-width: 1px;
}
[data-theme='dark'] .site-input-group-wrapper .site-input-split {
background-color: transparent;
}
</style>

View File

@ -14,6 +14,7 @@
<show-count />
<textarea-resize />
<borderlessVue />
<statusVue />
</demo-sort>
</template>
@ -32,6 +33,7 @@ import ShowCount from './show-count.vue';
import Addon from './addon.vue';
import Tooltip from './tooltip.vue';
import borderlessVue from './borderless.vue';
import statusVue from './status.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
@ -40,6 +42,7 @@ export default defineComponent({
CN,
US,
components: {
statusVue,
Basic,
AutosizeTextarea,
Presuffix,

View File

@ -0,0 +1,43 @@
<docs>
---
order: 19
version: 3.3.0
title:
zh-CN: 自定义状态
en-US: Status
---
## zh-CN
使用 `status` Input 添加状态可选 `error` 或者 `warning`
## en-US
Add status to Input with `status`, which could be `error` or `warning`.
</docs>
<template>
<a-space direction="vertical" style="width: 100%">
<a-input status="error" placeholder="Error" />
<a-input status="warning" placeholder="Warning" />
<a-input status="error" placeholder="Error with prefix">
<template #prefix><ClockCircleOutlined /></template>
</a-input>
<a-input status="warning" placeholder="Warning with prefix">
<template #prefix><ClockCircleOutlined /></template>
</a-input>
</a-space>
</template>
<script lang="ts">
import { ClockCircleOutlined } from '@ant-design/icons-vue';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
ClockCircleOutlined,
},
setup() {
return {};
},
});
</script>

View File

@ -22,13 +22,15 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca
| addonBefore | The label text displayed before (on the left side of) the input field. | string\|slot | | |
| allowClear | allow to remove input content with clear icon | boolean | | |
| bordered | Whether has border style | boolean | true | 4.5.0 |
| clearIcon | custom clear icon when allowClear | slot | `<CloseCircleFilled />` | 3.3.0 |
| defaultValue | The initial input content | string | | |
| disabled | Whether the input is disabled. | boolean | false | |
| id | The ID for input | string | | |
| maxlength | max length | number | | 1.5.0 |
| prefix | The prefix icon for the Input. | string\|slot | | |
| showCount | Whether show text count | boolean | false | 3.0 |
| size | The size of the input box. Note: in the context of a form, the `large` size is used. Available: `large` `default` `small` | string | `default` | |
| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 |
| size | The size of the input box. Note: in the context of a form, the `middle` size is used. Available: `large` `middle` `small` | string | - | |
| suffix | The suffix icon for the Input. | string\|slot | | |
| type | The type of input, see: [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)(use `<a-textarea />` instead of `type="textarea"`) | string | `text` | |
| value(v-model) | The input content value | string | | |

View File

@ -23,13 +23,15 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
| addonBefore | 带标签的 input设置前置标签 | string\|slot | | |
| allowClear | 可以点击清除图标删除内容 | boolean | | |
| bordered | 是否有边框 | boolean | true | 3.0 |
| clearIcon | 自定义清除图标 allowClear 为 true 时生效) | slot | `<CloseCircleFilled />` | 3.3.0 |
| defaultValue | 输入框默认内容 | string | | |
| disabled | 是否禁用状态,默认为 false | boolean | false | |
| id | 输入框的 id | string | | |
| maxlength | 最大长度 | number | | 1.5.0 |
| prefix | 带有前缀图标的 input | string\|slot | | |
| showCount | 是否展示字数 | boolean | false | 3.0 |
| size | 控件大小。注:标准表单内的输入框大小限制为 `large`。可选 `large` `default` `small` | string | `default` | |
| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 |
| size | 控件大小。注:标准表单内的输入框大小限制为 `middle`。可选 `large` `middle` `small` | string | - | |
| suffix | 带有后缀图标的 input | string\|slot | | |
| type | 声明 input 类型,同原生 input 标签的 type 属性,见:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#属性)(请直接使用 `<a-textarea />` 代替 `type="textarea"`)。 | string | `text` | |
| value(v-model) | 输入框内容 | string | | |

View File

@ -1,92 +1,25 @@
import type { ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
import type { SizeType } from '../config-provider';
import omit from '../_util/omit';
import type { LiteralUnion, VueNode } from '../_util/type';
import type {
ChangeEventHandler,
CompositionEventHandler,
FocusEventHandler,
KeyboardEventHandler,
} from '../_util/EventInterface';
import type { VueNode } from '../_util/type';
import type { CompositionEventHandler } from '../_util/EventInterface';
import { inputProps as vcInputProps } from '../vc-input/inputProps';
export const inputDefaultValue = Symbol() as unknown as string;
const inputProps = () => ({
id: String,
prefixCls: String,
inputPrefixCls: String,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: {
type: [String, Number, Symbol] as PropType<string | number>,
default: undefined,
},
placeholder: {
type: [String, Number] as PropType<string | number>,
},
autocomplete: String,
type: {
type: String as PropType<
LiteralUnion<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week',
string
>
>,
default: 'text',
},
name: String,
size: { type: String as PropType<SizeType> },
disabled: { type: Boolean, default: undefined },
readonly: { type: Boolean, default: undefined },
addonBefore: PropTypes.any,
addonAfter: PropTypes.any,
prefix: PropTypes.any,
suffix: PropTypes.any,
autofocus: { type: Boolean, default: undefined },
allowClear: { type: Boolean, default: undefined },
lazy: { type: Boolean, default: true },
maxlength: Number,
loading: { type: Boolean, default: undefined },
bordered: { type: Boolean, default: undefined },
showCount: { type: [Boolean, Object] as PropType<boolean | ShowCountProps> },
htmlSize: Number,
onPressEnter: Function as PropType<KeyboardEventHandler>,
onKeydown: Function as PropType<KeyboardEventHandler>,
onKeyup: Function as PropType<KeyboardEventHandler>,
onFocus: Function as PropType<FocusEventHandler>,
onBlur: Function as PropType<FocusEventHandler>,
onChange: Function as PropType<ChangeEventHandler>,
onInput: Function as PropType<ChangeEventHandler>,
'onUpdate:value': Function as PropType<(val: string) => void>,
valueModifiers: Object,
hidden: Boolean,
});
export default inputProps;
export type InputProps = Partial<ExtractPropTypes<ReturnType<typeof inputProps>>>;
export interface AutoSizeType {
minRows?: number;
maxRows?: number;
}
const inputProps = () => {
return omit(vcInputProps(), [
'wrapperClassName',
'groupClassName',
'inputClassName',
'affixWrapperClassName',
]);
};
export default inputProps;
export type InputProps = Partial<ExtractPropTypes<ReturnType<typeof inputProps>>>;
export interface ShowCountProps {
formatter: (args: { count: number; maxlength?: number }) => VueNode;
}

View File

@ -50,6 +50,10 @@
display: flex;
flex: none;
align-items: center;
> *:not(:last-child) {
margin-right: 8px;
}
}
&-show-count-suffix {

View File

@ -2,7 +2,8 @@
@input-prefix-cls: ~'@{ant-prefix}-input';
// ========================= Input =========================
.@{iconfont-css-prefix}.@{ant-prefix}-input-clear-icon {
.@{iconfont-css-prefix}.@{ant-prefix}-input-clear-icon,
.@{ant-prefix}-input-clear-icon {
margin: 0;
color: @disabled-color;
font-size: @font-size-sm;

View File

@ -3,6 +3,7 @@
@import './mixin';
@import './affix';
@import './allow-clear';
@import './status';
@input-prefix-cls: ~'@{ant-prefix}-input';
@ -24,7 +25,7 @@
}
}
&-password-icon {
&-password-icon.@{iconfont-css-prefix} {
color: @text-color-secondary;
cursor: pointer;
transition: all 0.3s;
@ -60,6 +61,23 @@
content: attr(data-count);
pointer-events: none;
}
&.@{input-prefix-cls}-textarea-in-form-item {
&::after {
margin-bottom: -22px;
}
}
}
&-textarea-suffix {
position: absolute;
top: 0;
right: @input-padding-horizontal-base;
bottom: 0;
z-index: 1;
display: inline-flex;
align-items: center;
margin: auto;
}
}

View File

@ -1,5 +1,6 @@
import '../../style/index.less';
import './index.less';
// deps-lint-skip: form
// style dependencies
import '../../button/style';

View File

@ -30,14 +30,14 @@
border-color: @hoverBorderColor;
box-shadow: @input-outline-offset @outline-blur-size @outline-width @outlineColor;
}
border-right-width: @border-width-base !important;
border-right-width: @border-width-base;
outline: 0;
}
// == when hover
.hover(@color: @input-hover-border-color) {
border-color: @color;
border-right-width: @border-width-base !important;
border-right-width: @border-width-base;
}
.disabled() {
@ -66,7 +66,7 @@
background-color: @input-bg;
background-image: none;
border: @border-width-base @border-style-base @input-border-color;
border-radius: @border-radius-base;
border-radius: @control-border-radius;
transition: all 0.3s;
.placeholder(); // Reset placeholder
@ -193,7 +193,7 @@
text-align: center;
background-color: @input-addon-bg;
border: @border-width-base @border-style-base @input-border-color;
border-radius: @border-radius-base;
border-radius: @control-border-radius;
transition: all 0.3s;
// Reset Select's style in addon
@ -297,8 +297,8 @@
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.@{ant-prefix}-input-search & {
border-top-left-radius: @border-radius-base;
border-bottom-left-radius: @border-radius-base;
border-top-left-radius: @control-border-radius;
border-bottom-left-radius: @control-border-radius;
}
}
@ -384,8 +384,8 @@
& > .@{ant-prefix}-select:first-child > .@{ant-prefix}-select-selector,
& > .@{ant-prefix}-select-auto-complete:first-child .@{ant-prefix}-input,
& > .@{ant-prefix}-cascader-picker:first-child .@{ant-prefix}-input {
border-top-left-radius: @border-radius-base;
border-bottom-left-radius: @border-radius-base;
border-top-left-radius: @control-border-radius;
border-bottom-left-radius: @control-border-radius;
}
& > *:last-child,
@ -393,8 +393,8 @@
& > .@{ant-prefix}-cascader-picker:last-child .@{ant-prefix}-input,
& > .@{ant-prefix}-cascader-picker-focused:last-child .@{ant-prefix}-input {
border-right-width: @border-width-base;
border-top-right-radius: @border-radius-base;
border-bottom-right-radius: @border-radius-base;
border-top-right-radius: @control-border-radius;
border-bottom-right-radius: @control-border-radius;
}
// https://github.com/ant-design/ant-design/issues/12493
@ -416,9 +416,55 @@
}
& > .@{ant-prefix}-input {
border-radius: @border-radius-base 0 0 @border-radius-base;
border-radius: @control-border-radius 0 0 @control-border-radius;
}
}
}
}
}
.status-color(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
@background-color: @input-bg;
@hoverBorderColor: @primary-color-hover;
@outlineColor: @primary-color-outline;
) {
&:not(.@{prefix-cls}-disabled):not(.@{prefix-cls}-borderless).@{prefix-cls} {
&,
&:hover {
background: @background-color;
border-color: @border-color;
}
&:focus,
&-focused {
.active(@text-color, @hoverBorderColor, @outlineColor);
}
}
}
.status-color-common(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
@background-color: @input-bg;
@hoverBorderColor: @primary-color-hover;
@outlineColor: @primary-color-outline;
) {
.@{prefix-cls}-prefix {
color: @text-color;
}
}
.group-status-color(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
) {
.@{prefix-cls}-group-addon {
color: @text-color;
border-color: @border-color;
}
}

View File

@ -0,0 +1,42 @@
@import './mixin';
@input-prefix-cls: ~'@{ant-prefix}-input';
@input-wrapper-cls: @input-prefix-cls, ~'@{input-prefix-cls}-affix-wrapper';
each(@input-wrapper-cls, {
.@{value} {
&-status-error {
.status-color(@value, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
.status-color-common(@input-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
}
&-status-warning {
.status-color(@value, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
.status-color-common(@input-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
}
}
});
.@{input-prefix-cls}-textarea {
&-status-error,
&-status-warning,
&-status-success,
&-status-validating {
&.@{input-prefix-cls}-textarea-has-feedback {
.@{input-prefix-cls} {
padding-right: 24px;
}
}
}
}
.@{input-prefix-cls}-group-wrapper {
&-status-error {
.group-status-color(@input-prefix-cls, @error-color, @error-color);
}
&-status-warning {
.group-status-color(@input-prefix-cls, @warning-color, @warning-color);
}
}

View File

@ -1,22 +1,4 @@
import type { Direction, SizeType } from '../config-provider';
import classNames from '../_util/classNames';
import { filterEmpty } from '../_util/props-util';
export function getInputClassName(
prefixCls: string,
bordered: boolean,
size?: SizeType,
disabled?: boolean,
direction?: Direction,
) {
return classNames(prefixCls, {
[`${prefixCls}-sm`]: size === 'small',
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-borderless`]: !bordered,
});
}
const isValid = (value: any) => {
return (
value !== undefined &&

View File

@ -0,0 +1,151 @@
import { defineComponent, ref } from 'vue';
import classNames from '../_util/classNames';
import type { MouseEventHandler } from '../_util/EventInterface';
import { cloneElement } from '../_util/vnode';
import { baseInputProps } from './inputProps';
import { hasAddon, hasPrefixSuffix } from './utils/commonUtils';
export default defineComponent({
name: 'BaseInput',
inheritAttrs: false,
props: baseInputProps(),
setup(props, { slots, attrs }) {
const containerRef = ref();
const onInputMouseDown: MouseEventHandler = e => {
if (containerRef.value?.contains(e.target as Element)) {
const { triggerFocus } = props;
triggerFocus?.();
}
};
const getClearIcon = () => {
const {
allowClear,
value,
disabled,
readonly,
handleReset,
suffix = slots.suffix,
prefixCls,
} = props;
if (!allowClear) {
return null;
}
const needClear = !disabled && !readonly && value;
const className = `${prefixCls}-clear-icon`;
const iconNode = slots.clearIcon?.() || '*';
return (
<span
onClick={handleReset}
// Do not trigger onBlur when clear input
onMousedown={e => e.preventDefault()}
class={classNames(
{
[`${className}-hidden`]: !needClear,
[`${className}-has-suffix`]: !!suffix,
},
className,
)}
role="button"
tabindex={-1}
>
{iconNode}
</span>
);
};
return () => {
const {
focused,
value,
disabled,
allowClear,
readonly,
hidden,
prefixCls,
prefix = slots.prefix?.(),
suffix = slots.suffix?.(),
addonAfter = slots.addonAfter,
addonBefore = slots.addonBefore,
inputElement,
affixWrapperClassName,
wrapperClassName,
groupClassName,
} = props;
let element = cloneElement(inputElement, {
value,
hidden,
});
// ================== Prefix & Suffix ================== //
if (hasPrefixSuffix({ prefix, suffix, allowClear })) {
const affixWrapperPrefixCls = `${prefixCls}-affix-wrapper`;
const affixWrapperCls = classNames(
affixWrapperPrefixCls,
{
[`${affixWrapperPrefixCls}-disabled`]: disabled,
[`${affixWrapperPrefixCls}-focused`]: focused,
[`${affixWrapperPrefixCls}-readonly`]: readonly,
[`${affixWrapperPrefixCls}-input-with-clear-btn`]: suffix && allowClear && value,
},
!hasAddon({ addonAfter, addonBefore }) && attrs.class,
affixWrapperClassName,
);
const suffixNode = (suffix || allowClear) && (
<span class={`${prefixCls}-suffix`}>
{getClearIcon()}
{suffix}
</span>
);
element = (
<span
class={affixWrapperCls}
style={attrs.style}
hidden={!hasAddon({ addonAfter, addonBefore }) && hidden}
onMousedown={onInputMouseDown}
ref={containerRef}
>
{prefix && <span class={`${prefixCls}-prefix`}>{prefix}</span>}
{cloneElement(inputElement, {
style: null,
value,
hidden: null,
})}
{suffixNode}
</span>
);
}
// ================== Addon ================== //
if (hasAddon({ addonAfter, addonBefore })) {
const wrapperCls = `${prefixCls}-group`;
const addonCls = `${wrapperCls}-addon`;
const mergedWrapperClassName = classNames(
`${prefixCls}-wrapper`,
wrapperCls,
wrapperClassName,
);
const mergedGroupClassName = classNames(
`${prefixCls}-group-wrapper`,
attrs.class,
groupClassName,
);
// Need another wrapper for changing display:table to display:inline-block
// and put style prop in wrapper
return (
<span class={mergedGroupClassName} style={attrs.style} hidden={hidden}>
<span class={mergedWrapperClassName}>
{addonBefore && <span class={addonCls}>{addonBefore}</span>}
{cloneElement(element, { style: null, hidden: null })}
{addonAfter && <span class={addonCls}>{addonAfter}</span>}
</span>
</span>
);
}
return element;
};
},
});

View File

@ -0,0 +1,261 @@
// base 0.0.1-alpha.7
import type { VNode } from 'vue';
import {
onMounted,
defineComponent,
getCurrentInstance,
nextTick,
ref,
watch,
withDirectives,
} from 'vue';
import classNames from '../_util/classNames';
import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface';
import omit from '../_util/omit';
import type { InputProps } from './inputProps';
import { inputProps } from './inputProps';
import type { InputFocusOptions } from './utils/commonUtils';
import {
fixControlledValue,
hasAddon,
hasPrefixSuffix,
resolveOnChange,
triggerFocus,
} from './utils/commonUtils';
import antInputDirective from '../_util/antInputDirective';
import BaseInput from './BaseInput';
export default defineComponent({
name: 'VCInput',
inheritAttrs: false,
props: inputProps(),
setup(props, { slots, attrs, expose, emit }) {
const stateValue = ref(props.value === undefined ? props.defaultValue : props.value);
const focused = ref(false);
const inputRef = ref<HTMLInputElement>();
watch(
() => props.value,
() => {
stateValue.value = props.value;
},
);
watch(
() => props.disabled,
() => {
if (props.disabled) {
focused.value = false;
}
},
);
const focus = (option?: InputFocusOptions) => {
if (inputRef.value) {
triggerFocus(inputRef.value, option);
}
};
const blur = () => {
inputRef.value?.blur();
};
const setSelectionRange = (
start: number,
end: number,
direction?: 'forward' | 'backward' | 'none',
) => {
inputRef.value?.setSelectionRange(start, end, direction);
};
const select = () => {
inputRef.value?.select();
};
expose({
focus,
blur,
input: inputRef,
stateValue,
setSelectionRange,
select,
});
const triggerChange = (e: Event) => {
emit('change', e);
};
const instance = getCurrentInstance();
const setValue = (value: string | number, callback?: Function) => {
if (stateValue.value === value) {
return;
}
if (props.value === undefined) {
stateValue.value = value;
} else {
nextTick(() => {
if (inputRef.value.value !== stateValue.value) {
instance.update();
}
});
}
nextTick(() => {
callback && callback();
});
};
const handleChange = (e: ChangeEvent) => {
const { value, composing } = e.target as any;
// https://github.com/vueComponent/ant-design-vue/issues/2203
if ((((e as any).isComposing || composing) && props.lazy) || stateValue.value === value)
return;
const newVal = e.target.value;
resolveOnChange(inputRef.value, e, triggerChange);
setValue(newVal);
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
emit('pressEnter', e);
}
emit('keydown', e);
};
const handleFocus: FocusEventHandler = e => {
focused.value = true;
emit('focus', e);
};
const handleBlur: FocusEventHandler = e => {
focused.value = false;
emit('blur', e);
};
const handleReset = (e: MouseEvent) => {
resolveOnChange(inputRef.value, e, triggerChange);
setValue('', () => {
focus();
});
};
const getInputElement = () => {
const {
addonBefore = slots.addonBefore,
addonAfter = slots.addonAfter,
disabled,
valueModifiers = {},
htmlSize,
autocomplete,
prefixCls,
inputClassName,
prefix = slots.prefix?.(),
suffix = slots.suffix?.(),
allowClear,
type = 'text',
} = props;
const otherProps = omit(props as InputProps & { placeholder: string }, [
'prefixCls',
'onPressEnter',
'addonBefore',
'addonAfter',
'prefix',
'suffix',
'allowClear',
// Input elements must be either controlled or uncontrolled,
// specify either the value prop, or the defaultValue prop, but not both.
'defaultValue',
'size',
'bordered',
'htmlSize',
'lazy',
'showCount',
'valueModifiers',
'showCount',
'affixWrapperClassName',
'groupClassName',
'inputClassName',
'wrapperClassName',
]);
const inputProps = {
...otherProps,
...attrs,
autocomplete,
onChange: handleChange,
onInput: handleChange,
onFocus: handleFocus,
onBlur: handleBlur,
onKeydown: handleKeyDown,
class: classNames(
prefixCls,
{
[`${prefixCls}-disabled`]: disabled,
},
inputClassName,
!hasAddon({ addonAfter, addonBefore }) &&
!hasPrefixSuffix({ prefix, suffix, allowClear }) &&
attrs.class,
),
ref: inputRef,
key: 'ant-input',
size: htmlSize,
type,
};
if (valueModifiers.lazy) {
delete inputProps.onInput;
}
if (!inputProps.autofocus) {
delete inputProps.autofocus;
}
const inputNode = <input {...omit(inputProps, ['size'])} />;
return withDirectives(inputNode as VNode, [[antInputDirective]]);
};
const getSuffix = () => {
const { maxlength, suffix = slots.suffix?.(), showCount, prefixCls } = props;
// Max length value
const hasMaxLength = Number(maxlength) > 0;
if (suffix || showCount) {
const valueLength = [...fixControlledValue(stateValue.value)].length;
const dataCount =
typeof showCount === 'object'
? showCount.formatter({ count: valueLength, maxlength })
: `${valueLength}${hasMaxLength ? ` / ${maxlength}` : ''}`;
return (
<>
{!!showCount && (
<span
class={classNames(`${prefixCls}-show-count-suffix`, {
[`${prefixCls}-show-count-has-suffix`]: !!suffix,
})}
>
{dataCount}
</span>
)}
{suffix}
</>
);
}
return null;
};
onMounted(() => {
if (process.env.NODE_ENV === 'test') {
if (props.autofocus) {
focus();
}
}
});
return () => {
const { prefixCls, disabled, ...rest } = props;
return (
<BaseInput
{...rest}
{...attrs}
prefixCls={prefixCls}
inputElement={getInputElement()}
handleReset={handleReset}
value={fixControlledValue(stateValue.value)}
focused={focused.value}
triggerFocus={focus}
suffix={getSuffix()}
disabled={disabled}
v-slots={slots}
/>
);
};
},
});

View File

@ -0,0 +1,126 @@
import type { ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
import type { SizeType } from '../config-provider';
import type { LiteralUnion, VueNode } from '../_util/type';
import type {
ChangeEventHandler,
CompositionEventHandler,
FocusEventHandler,
KeyboardEventHandler,
MouseEventHandler,
} from '../_util/EventInterface';
import type { InputStatus } from '../_util/statusUtils';
import type { InputFocusOptions } from './utils/commonUtils';
export const inputDefaultValue = Symbol() as unknown as string;
export const commonInputProps = () => {
return {
addonBefore: PropTypes.any,
addonAfter: PropTypes.any,
prefix: PropTypes.any,
suffix: PropTypes.any,
clearIcon: PropTypes.any,
affixWrapperClassName: String,
groupClassName: String,
wrapperClassName: String,
inputClassName: String,
allowClear: { type: Boolean, default: undefined },
};
};
export const baseInputProps = () => {
return {
...commonInputProps(),
value: {
type: [String, Number, Symbol] as PropType<string | number>,
default: undefined,
},
defaultValue: {
type: [String, Number, Symbol] as PropType<string | number>,
default: undefined,
},
inputElement: PropTypes.any,
prefixCls: String,
disabled: { type: Boolean, default: undefined },
focused: { type: Boolean, default: undefined },
triggerFocus: Function as PropType<() => void>,
readonly: { type: Boolean, default: undefined },
handleReset: Function as PropType<MouseEventHandler>,
hidden: { type: Boolean, default: undefined },
};
};
export const inputProps = () => ({
...baseInputProps(),
id: String,
placeholder: {
type: [String, Number] as PropType<string | number>,
},
autocomplete: String,
type: {
type: String as PropType<
LiteralUnion<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week',
string
>
>,
default: 'text',
},
name: String,
size: { type: String as PropType<SizeType> },
autofocus: { type: Boolean, default: undefined },
lazy: { type: Boolean, default: true },
maxlength: Number,
loading: { type: Boolean, default: undefined },
bordered: { type: Boolean, default: undefined },
showCount: { type: [Boolean, Object] as PropType<boolean | ShowCountProps> },
htmlSize: Number,
onPressEnter: Function as PropType<KeyboardEventHandler>,
onKeydown: Function as PropType<KeyboardEventHandler>,
onKeyup: Function as PropType<KeyboardEventHandler>,
onFocus: Function as PropType<FocusEventHandler>,
onBlur: Function as PropType<FocusEventHandler>,
onChange: Function as PropType<ChangeEventHandler>,
onInput: Function as PropType<ChangeEventHandler>,
'onUpdate:value': Function as PropType<(val: string) => void>,
onCompositionstart: Function as PropType<CompositionEventHandler>,
onCompositionend: Function as PropType<CompositionEventHandler>,
valueModifiers: Object,
hidden: { type: Boolean, default: undefined },
status: String as PropType<InputStatus>,
});
export type InputProps = Partial<ExtractPropTypes<ReturnType<typeof inputProps>>>;
export interface ShowCountProps {
formatter: (args: { count: number; maxlength?: number }) => VueNode;
}
export interface InputRef {
focus: (options?: InputFocusOptions) => void;
blur: () => void;
setSelectionRange: (
start: number,
end: number,
direction?: 'forward' | 'backward' | 'none',
) => void;
select: () => void;
input: HTMLInputElement | null;
}

View File

@ -0,0 +1,104 @@
import { filterEmpty } from '../../_util/props-util';
const isValid = (value: any) => {
return (
value !== undefined &&
value !== null &&
(Array.isArray(value) ? filterEmpty(value).length : true)
);
};
export function hasPrefixSuffix(propsAndSlots: any) {
return (
isValid(propsAndSlots.prefix) ||
isValid(propsAndSlots.suffix) ||
isValid(propsAndSlots.allowClear)
);
}
export function hasAddon(propsAndSlots: any) {
return isValid(propsAndSlots.addonBefore) || isValid(propsAndSlots.addonAfter);
}
export function fixControlledValue(value: string | number) {
if (typeof value === 'undefined' || value === null) {
return '';
}
return String(value);
}
export function resolveOnChange(
target: HTMLInputElement,
e: Event,
onChange: Function,
targetValue?: string,
) {
if (!onChange) {
return;
}
const event: any = e;
if (e.type === 'click') {
Object.defineProperty(event, 'target', {
writable: true,
});
Object.defineProperty(event, 'currentTarget', {
writable: true,
});
// click clear icon
//event = Object.create(e);
const currentTarget = target.cloneNode(true);
event.target = currentTarget;
event.currentTarget = currentTarget;
// change target ref value cause e.target.value should be '' when clear input
(currentTarget as any).value = '';
onChange(event);
return;
}
// Trigger by composition event, this means we need force change the input value
if (targetValue !== undefined) {
Object.defineProperty(event, 'target', {
writable: true,
});
Object.defineProperty(event, 'currentTarget', {
writable: true,
});
event.target = target;
event.currentTarget = target;
target.value = targetValue;
onChange(event);
return;
}
onChange(event);
}
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
export function triggerFocus(
element?: HTMLInputElement | HTMLTextAreaElement,
option?: InputFocusOptions,
) {
if (!element) return;
element.focus(option);
// Selection content
const { cursor } = option || {};
if (cursor) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}

View File

@ -0,0 +1,3 @@
/** https://github.com/Microsoft/TypeScript/issues/29729 */
// eslint-disable-next-line @typescript-eslint/ban-types
export type LiteralUnion<T extends U, U> = T | (U & {});