refactor: mentions
parent
ab5f9c9bf8
commit
10d0a91c55
|
@ -11,11 +11,12 @@ function $$(className) {
|
|||
}
|
||||
|
||||
function triggerInput(wrapper, text = '') {
|
||||
const lastChar = text[text.length - 1];
|
||||
wrapper.find('textarea').element.value = text;
|
||||
wrapper.find('textarea').element.selectionStart = text.length;
|
||||
wrapper.find('textarea').trigger('keydown');
|
||||
wrapper.find('textarea').trigger('change');
|
||||
wrapper.find('textarea').trigger('keyup');
|
||||
wrapper.find('textarea').trigger('keyup', { key: lastChar });
|
||||
}
|
||||
|
||||
describe('Mentions', () => {
|
||||
|
@ -69,9 +70,9 @@ describe('Mentions', () => {
|
|||
},
|
||||
{ sync: false, attachTo: 'body' },
|
||||
);
|
||||
await sleep(500);
|
||||
await sleep(100);
|
||||
triggerInput(wrapper, '@');
|
||||
await sleep(500);
|
||||
await sleep(100);
|
||||
expect($$('.ant-mentions-dropdown-menu-item').length).toBeTruthy();
|
||||
expect($$('.ant-spin')).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { App, PropType, VNodeTypes, Plugin, ExtractPropTypes } from 'vue';
|
||||
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
|
@ -6,9 +7,8 @@ import omit from 'omit.js';
|
|||
import PropTypes from '../_util/vue-types';
|
||||
import VcMentions from '../vc-mentions';
|
||||
import { mentionsProps as baseMentionsProps } from '../vc-mentions/src/mentionsProps';
|
||||
import Spin from '../spin';
|
||||
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import { flattenChildren, getOptionProps } from '../_util/props-util';
|
||||
|
||||
const { Option } = VcMentions;
|
||||
|
||||
|
@ -19,23 +19,26 @@ interface MentionsConfig {
|
|||
|
||||
export interface MentionsOptionProps {
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
children: VNodeTypes;
|
||||
disabled?: boolean;
|
||||
label?: string | number | ((o: MentionsOptionProps) => any);
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function loadingFilterOption() {
|
||||
return true;
|
||||
interface MentionsEntity {
|
||||
prefix: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function getMentions(value = '', config: MentionsConfig) {
|
||||
export type MentionPlacement = 'top' | 'bottom';
|
||||
|
||||
const getMentions = (value = '', config: MentionsConfig): MentionsEntity[] => {
|
||||
const { prefix = '@', split = ' ' } = config || {};
|
||||
const prefixList = Array.isArray(prefix) ? prefix : [prefix];
|
||||
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
|
||||
|
||||
return value
|
||||
.split(split)
|
||||
.map((str = '') => {
|
||||
let hitPrefix = null;
|
||||
.map((str = ''): MentionsEntity | null => {
|
||||
let hitPrefix: string | null = null;
|
||||
|
||||
prefixList.some(prefixStr => {
|
||||
const startStr = str.slice(0, prefixStr.length);
|
||||
|
@ -49,13 +52,13 @@ function getMentions(value = '', config: MentionsConfig) {
|
|||
if (hitPrefix !== null) {
|
||||
return {
|
||||
prefix: hitPrefix,
|
||||
value: str.slice(hitPrefix.length),
|
||||
value: str.slice((hitPrefix as string).length),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(entity => !!entity && !!entity.value);
|
||||
}
|
||||
.filter((entity): entity is MentionsEntity => !!entity && !!entity.value);
|
||||
};
|
||||
|
||||
const mentionsProps = {
|
||||
...baseMentionsProps,
|
||||
|
@ -72,6 +75,8 @@ const mentionsProps = {
|
|||
onChange: {
|
||||
type: Function as PropType<(text: string) => void>,
|
||||
},
|
||||
notFoundContent: PropTypes.any,
|
||||
defaultValue: String,
|
||||
};
|
||||
|
||||
export type MentionsProps = Partial<ExtractPropTypes<typeof mentionsProps>>;
|
||||
|
@ -81,12 +86,20 @@ const Mentions = defineComponent({
|
|||
inheritAttrs: false,
|
||||
props: mentionsProps,
|
||||
getMentions,
|
||||
emits: ['update:value', 'change', 'focus', 'blur', 'select'],
|
||||
Option,
|
||||
emits: ['update:value', 'change', 'focus', 'blur', 'select', 'pressenter'],
|
||||
slots: ['notFoundContent', 'option'],
|
||||
setup(props, { slots, emit, attrs, expose }) {
|
||||
const { prefixCls, renderEmpty, direction } = useConfigInject('mentions', props);
|
||||
const focused = ref(false);
|
||||
const vcMentions = ref(null);
|
||||
|
||||
const value = ref(props.value ?? props.defaultValue ?? '');
|
||||
watch(
|
||||
() => props.value,
|
||||
val => {
|
||||
value.value = val;
|
||||
},
|
||||
);
|
||||
const handleFocus = (e: FocusEvent) => {
|
||||
focused.value = true;
|
||||
emit('focus', e);
|
||||
|
@ -103,43 +116,34 @@ const Mentions = defineComponent({
|
|||
};
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
if (props.value === undefined) {
|
||||
value.value = val;
|
||||
}
|
||||
emit('update:value', val);
|
||||
emit('change', val);
|
||||
};
|
||||
|
||||
const getNotFoundContent = (renderEmpty: RenderEmptyHandler) => {
|
||||
const getNotFoundContent = () => {
|
||||
const notFoundContent = props.notFoundContent;
|
||||
if (notFoundContent !== undefined) {
|
||||
return notFoundContent;
|
||||
}
|
||||
|
||||
return renderEmpty('Select');
|
||||
if (slots.notFoundContent) {
|
||||
return slots.notFoundContent();
|
||||
}
|
||||
return renderEmpty.value('Select');
|
||||
};
|
||||
|
||||
const getOptions = () => {
|
||||
const { loading } = props;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Option value="ANTD_SEARCHING" disabled>
|
||||
<Spin size="small" />
|
||||
</Option>
|
||||
);
|
||||
}
|
||||
return slots.default?.();
|
||||
};
|
||||
|
||||
const getFilterOption = () => {
|
||||
const { filterOption, loading } = props;
|
||||
if (loading) {
|
||||
return loadingFilterOption;
|
||||
}
|
||||
return filterOption;
|
||||
return flattenChildren(slots.default?.() || []).map(item => {
|
||||
return { ...getOptionProps(item), label: (item.children as any)?.default?.() };
|
||||
});
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
(vcMentions.value as HTMLTextAreaElement).focus();
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
(vcMentions.value as HTMLTextAreaElement).blur();
|
||||
};
|
||||
|
@ -157,35 +161,40 @@ const Mentions = defineComponent({
|
|||
});
|
||||
|
||||
return () => {
|
||||
const { disabled, getPopupContainer, ...restProps } = props;
|
||||
const { disabled, getPopupContainer, rows = 1, ...restProps } = props;
|
||||
const { class: className, ...otherAttrs } = attrs;
|
||||
const otherProps = omit(restProps, ['loading', 'onUpdate:value', 'prefixCls']);
|
||||
const otherProps = omit(restProps, ['defaultValue', 'onUpdate:value', 'prefixCls']);
|
||||
|
||||
const mergedClassName = classNames(className, {
|
||||
[`${prefixCls.value}-disabled`]: disabled,
|
||||
[`${prefixCls.value}-focused`]: focused.value,
|
||||
[`${prefixCls}-rtl`]: direction.value === 'rtl',
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
});
|
||||
|
||||
const mentionsProps = {
|
||||
prefixCls: prefixCls.value,
|
||||
notFoundContent: getNotFoundContent(renderEmpty.value),
|
||||
...otherProps,
|
||||
disabled,
|
||||
direction: direction.value,
|
||||
filterOption: getFilterOption(),
|
||||
filterOption: props.filterOption,
|
||||
getPopupContainer,
|
||||
children: getOptions(),
|
||||
options: props.options || getOptions(),
|
||||
class: mergedClassName,
|
||||
rows: 1,
|
||||
...otherAttrs,
|
||||
rows,
|
||||
onChange: handleChange,
|
||||
onSelect: handleSelect,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
ref: vcMentions,
|
||||
value: value.value,
|
||||
};
|
||||
return <VcMentions {...mentionsProps} />;
|
||||
return (
|
||||
<VcMentions
|
||||
{...mentionsProps}
|
||||
v-slots={{ notFoundContent: getNotFoundContent, option: slots.option }}
|
||||
></VcMentions>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -198,11 +207,12 @@ export const MentionsOption = {
|
|||
/* istanbul ignore next */
|
||||
Mentions.install = function (app: App) {
|
||||
app.component(Mentions.name, Mentions);
|
||||
app.component(MentionsOption.name, MentionsOption);
|
||||
app.component('AMentionsOption', Option);
|
||||
return app;
|
||||
};
|
||||
|
||||
export default Mentions as typeof Mentions &
|
||||
Plugin & {
|
||||
getMentions: typeof getMentions;
|
||||
readonly Option: typeof Option;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// base rc-mentions .6.2
|
||||
import Mentions from './src/Mentions';
|
||||
import Option from './src/Option';
|
||||
|
||||
Mentions.Option = Option;
|
||||
|
||||
export { Option };
|
||||
export default Mentions;
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
import Menu, { Item as MenuItem } from '../../menu';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent, inject, ref } from 'vue';
|
||||
import type { OptionProps } from './Option';
|
||||
import MentionsContextKey from './MentionsContext';
|
||||
import Spin from '../../spin';
|
||||
|
||||
function noop() {}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownMenu',
|
||||
props: {
|
||||
prefixCls: PropTypes.string,
|
||||
options: PropTypes.any,
|
||||
prefixCls: String,
|
||||
options: {
|
||||
type: Array as PropType<OptionProps[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
slots: ['notFoundContent', 'option'],
|
||||
setup(props, { slots }) {
|
||||
const {
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
selectOption,
|
||||
onFocus = noop,
|
||||
onBlur = noop,
|
||||
loading,
|
||||
} = inject(MentionsContextKey, {
|
||||
activeIndex: ref(),
|
||||
loading: ref(false),
|
||||
});
|
||||
return () => {
|
||||
const {
|
||||
notFoundContent,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
selectOption,
|
||||
onFocus = noop,
|
||||
onBlur = noop,
|
||||
} = inject('mentionsContext');
|
||||
|
||||
const { prefixCls, options } = props;
|
||||
const activeOption = options[activeIndex.value] || {};
|
||||
|
||||
|
@ -35,9 +43,9 @@ export default defineComponent({
|
|||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
>
|
||||
{[
|
||||
...options.map((option, index) => {
|
||||
const { value, disabled, children } = option;
|
||||
{!loading.value &&
|
||||
options.map((option, index) => {
|
||||
const { value, disabled, label = option.value } = option;
|
||||
return (
|
||||
<MenuItem
|
||||
key={value}
|
||||
|
@ -46,16 +54,21 @@ export default defineComponent({
|
|||
setActiveIndex(index);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{slots.option?.(option) ??
|
||||
(typeof label === 'function' ? label({ value, disabled }) : label)}
|
||||
</MenuItem>
|
||||
);
|
||||
}),
|
||||
!options.length && (
|
||||
<MenuItem key="notFoundContent" disabled>
|
||||
{notFoundContent.value}
|
||||
</MenuItem>
|
||||
),
|
||||
].filter(Boolean)}
|
||||
})}
|
||||
{!loading.value && options.length === 0 ? (
|
||||
<MenuItem key="notFoundContent" disabled>
|
||||
{slots.notFoundContent?.()}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{loading.value && (
|
||||
<MenuItem key="loading" disabled>
|
||||
<Spin size="small" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import PropTypes from '../../_util/vue-types';
|
||||
import Trigger from '../../vc-trigger';
|
||||
import DropdownMenu from './DropdownMenu';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import type { OptionProps } from './Option';
|
||||
|
||||
const BUILT_IN_PLACEMENTS = {
|
||||
bottomRight: {
|
||||
|
@ -12,6 +14,14 @@ const BUILT_IN_PLACEMENTS = {
|
|||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
bottomLeft: {
|
||||
points: ['tr', 'bl'],
|
||||
offset: [0, 4],
|
||||
overflow: {
|
||||
adjustX: 0,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
topRight: {
|
||||
points: ['bl', 'tr'],
|
||||
offset: [0, -4],
|
||||
|
@ -20,50 +30,72 @@ const BUILT_IN_PLACEMENTS = {
|
|||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
topLeft: {
|
||||
points: ['br', 'tl'],
|
||||
offset: [0, -4],
|
||||
overflow: {
|
||||
adjustX: 0,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'KeywordTrigger',
|
||||
props: {
|
||||
loading: PropTypes.looseBool,
|
||||
options: PropTypes.array,
|
||||
options: {
|
||||
type: Array as PropType<OptionProps[]>,
|
||||
default: () => [],
|
||||
},
|
||||
prefixCls: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
visible: PropTypes.looseBool,
|
||||
transitionName: PropTypes.string,
|
||||
getPopupContainer: PropTypes.func,
|
||||
direction: PropTypes.string,
|
||||
},
|
||||
methods: {
|
||||
getDropdownPrefix() {
|
||||
return `${this.$props.prefixCls}-dropdown`;
|
||||
},
|
||||
getDropdownElement() {
|
||||
const { options } = this.$props;
|
||||
return <DropdownMenu prefixCls={this.getDropdownPrefix()} options={options} />;
|
||||
},
|
||||
},
|
||||
slots: ['notFoundContent', 'option'],
|
||||
setup(props, { slots }) {
|
||||
const getDropdownPrefix = () => {
|
||||
return `${props.prefixCls}-dropdown`;
|
||||
};
|
||||
const getDropdownElement = () => {
|
||||
const { options } = props;
|
||||
return (
|
||||
<DropdownMenu
|
||||
prefixCls={getDropdownPrefix()}
|
||||
options={options}
|
||||
v-slots={{ notFoundContent: slots.notFoundContent, option: slots.option }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible, placement, transitionName, getPopupContainer } = this.$props;
|
||||
|
||||
const { $slots } = this;
|
||||
|
||||
const children = $slots.default?.();
|
||||
|
||||
const popupElement = this.getDropdownElement();
|
||||
|
||||
return (
|
||||
<Trigger
|
||||
prefixCls={this.getDropdownPrefix()}
|
||||
popupVisible={visible}
|
||||
popup={popupElement}
|
||||
popupPlacement={placement === 'top' ? 'topRight' : 'bottomRight'}
|
||||
popupTransitionName={transitionName}
|
||||
builtinPlacements={BUILT_IN_PLACEMENTS}
|
||||
getPopupContainer={getPopupContainer}
|
||||
>
|
||||
{children}
|
||||
</Trigger>
|
||||
);
|
||||
const popupPlacement = computed(() => {
|
||||
const { placement, direction } = props;
|
||||
let popupPlacement = 'topRight';
|
||||
if (direction === 'rtl') {
|
||||
popupPlacement = placement === 'top' ? 'topLeft' : 'bottomLeft';
|
||||
} else {
|
||||
popupPlacement = placement === 'top' ? 'topRight' : 'bottomRight';
|
||||
}
|
||||
return popupPlacement;
|
||||
});
|
||||
return () => {
|
||||
const { visible, transitionName, getPopupContainer } = props;
|
||||
return (
|
||||
<Trigger
|
||||
prefixCls={getDropdownPrefix()}
|
||||
popupVisible={visible}
|
||||
popup={getDropdownElement()}
|
||||
popupPlacement={popupPlacement.value}
|
||||
popupTransitionName={transitionName}
|
||||
builtinPlacements={BUILT_IN_PLACEMENTS}
|
||||
getPopupContainer={getPopupContainer}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</Trigger>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { ExtractPropTypes } from 'vue';
|
||||
import { toRef, watchEffect } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
provide,
|
||||
withDirectives,
|
||||
onMounted,
|
||||
ref,
|
||||
reactive,
|
||||
onUpdated,
|
||||
|
@ -13,8 +13,7 @@ import {
|
|||
import classNames from '../../_util/classNames';
|
||||
import omit from 'omit.js';
|
||||
import KeyCode from '../../_util/KeyCode';
|
||||
import { getOptionProps, initDefaultProps } from '../../_util/props-util';
|
||||
import warning from 'warning';
|
||||
import { initDefaultProps } from '../../_util/props-util';
|
||||
import {
|
||||
getBeforeSelectionText,
|
||||
getLastMeasureIndex,
|
||||
|
@ -24,21 +23,25 @@ import {
|
|||
import KeywordTrigger from './KeywordTrigger';
|
||||
import { vcMentionsProps, defaultProps } from './mentionsProps';
|
||||
import antInput from '../../_util/antInputDirective';
|
||||
import type { OptionProps } from './Option';
|
||||
import MentionsContextKey from './MentionsContext';
|
||||
|
||||
export type MentionsProps = Partial<ExtractPropTypes<typeof vcMentionsProps>>;
|
||||
|
||||
function noop() {}
|
||||
|
||||
const Mentions = {
|
||||
export default defineComponent({
|
||||
name: 'Mentions',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(vcMentionsProps, defaultProps),
|
||||
setup(props: MentionsProps, { emit, attrs, expose }) {
|
||||
slots: ['notFoundContent', 'option'],
|
||||
emits: ['change', 'select', 'search', 'focus', 'blur', 'pressenter'],
|
||||
setup(props: MentionsProps, { emit, attrs, expose, slots }) {
|
||||
const measure = ref(null);
|
||||
const textarea = ref(null);
|
||||
const focusId = ref();
|
||||
const state = reactive({
|
||||
value: props.defaultValue || props.value || '',
|
||||
value: props.value || '',
|
||||
measuring: false,
|
||||
measureLocation: 0,
|
||||
measureText: null,
|
||||
|
@ -47,8 +50,11 @@ const Mentions = {
|
|||
isFocus: false,
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
state.value = props.value;
|
||||
});
|
||||
|
||||
const triggerChange = (val: string) => {
|
||||
state.value = val;
|
||||
emit('change', val);
|
||||
};
|
||||
|
||||
|
@ -57,6 +63,24 @@ const Mentions = {
|
|||
triggerChange(value);
|
||||
};
|
||||
|
||||
const startMeasure = (measureText: string, measurePrefix: string, measureLocation: number) => {
|
||||
Object.assign(state, {
|
||||
measuring: true,
|
||||
measureText,
|
||||
measurePrefix,
|
||||
measureLocation,
|
||||
activeIndex: 0,
|
||||
});
|
||||
};
|
||||
const stopMeasure = (callback?: () => void) => {
|
||||
Object.assign(state, {
|
||||
measuring: false,
|
||||
measureLocation: 0,
|
||||
measureText: null,
|
||||
});
|
||||
callback?.();
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const { which } = event;
|
||||
// Skip if not measuring
|
||||
|
@ -66,7 +90,7 @@ const Mentions = {
|
|||
|
||||
if (which === KeyCode.UP || which === KeyCode.DOWN) {
|
||||
// Control arrow function
|
||||
const optionLen = getOptions().length;
|
||||
const optionLen = options.value.length;
|
||||
const offset = which === KeyCode.UP ? -1 : 1;
|
||||
const newActiveIndex = (state.activeIndex + offset + optionLen) % optionLen;
|
||||
state.activeIndex = newActiveIndex;
|
||||
|
@ -76,12 +100,11 @@ const Mentions = {
|
|||
} else if (which === KeyCode.ENTER) {
|
||||
// Measure hit
|
||||
event.preventDefault();
|
||||
const options = getOptions();
|
||||
if (!options.length) {
|
||||
if (!options.value.length) {
|
||||
stopMeasure();
|
||||
return;
|
||||
}
|
||||
const option = options[state.activeIndex];
|
||||
const option = options.value[state.activeIndex];
|
||||
selectOption(option);
|
||||
}
|
||||
};
|
||||
|
@ -110,6 +133,7 @@ const Mentions = {
|
|||
if (validateMeasure) {
|
||||
if (
|
||||
key === measurePrefix ||
|
||||
key === 'Shift' ||
|
||||
measuring ||
|
||||
(measureText !== prevMeasureText && matchOption)
|
||||
) {
|
||||
|
@ -131,6 +155,12 @@ const Mentions = {
|
|||
stopMeasure();
|
||||
}
|
||||
};
|
||||
const onPressEnter = event => {
|
||||
if (!state.measuring) {
|
||||
emit('pressenter', event);
|
||||
}
|
||||
};
|
||||
|
||||
const onInputFocus = (event: Event) => {
|
||||
onFocus(event);
|
||||
};
|
||||
|
@ -152,7 +182,7 @@ const Mentions = {
|
|||
emit('blur', event);
|
||||
}, 100);
|
||||
};
|
||||
const selectOption = option => {
|
||||
const selectOption = (option: OptionProps) => {
|
||||
const { split } = props;
|
||||
const { value: mentionValue = '' } = option;
|
||||
const { text, selectionLocation } = replaceWithMeasure(state.value, {
|
||||
|
@ -170,38 +200,26 @@ const Mentions = {
|
|||
|
||||
emit('select', option, state.measurePrefix);
|
||||
};
|
||||
const setActiveIndex = activeIndex => {
|
||||
const setActiveIndex = (activeIndex: number) => {
|
||||
state.activeIndex = activeIndex;
|
||||
};
|
||||
|
||||
const getOptions = (measureText?: string) => {
|
||||
const targetMeasureText = measureText || state.measureText || '';
|
||||
const { filterOption, children = [] } = props;
|
||||
const list = (Array.isArray(children) ? children : [children])
|
||||
.map(item => {
|
||||
return { ...getOptionProps(item), children: item.children.default?.() };
|
||||
})
|
||||
.filter(option => {
|
||||
/** Return all result if `filterOption` is false. */
|
||||
if (!!filterOption === false) {
|
||||
return true;
|
||||
}
|
||||
return filterOption(targetMeasureText, option);
|
||||
});
|
||||
const { filterOption } = props;
|
||||
const list = props.options.filter((option: OptionProps) => {
|
||||
/** Return all result if `filterOption` is false. */
|
||||
if (!!filterOption === false) {
|
||||
return true;
|
||||
}
|
||||
return filterOption(targetMeasureText, option);
|
||||
});
|
||||
return list;
|
||||
};
|
||||
const startMeasure = (measureText, measurePrefix, measureLocation) => {
|
||||
state.measuring = true;
|
||||
state.measureText = measureText;
|
||||
state.measurePrefix = measurePrefix;
|
||||
state.measureLocation = measureLocation;
|
||||
state.activeIndex = 0;
|
||||
};
|
||||
const stopMeasure = (callback?: () => void) => {
|
||||
state.measuring = false;
|
||||
state.measureLocation = 0;
|
||||
state.measureText = null;
|
||||
callback?.();
|
||||
};
|
||||
const options = computed(() => {
|
||||
return getOptions();
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
textarea.value.focus();
|
||||
};
|
||||
|
@ -209,19 +227,13 @@ const Mentions = {
|
|||
textarea.value.blur();
|
||||
};
|
||||
expose({ blur, focus });
|
||||
onMounted(() => {
|
||||
warning(props.children, 'please children prop replace slots.default');
|
||||
|
||||
provide('mentionsContext', {
|
||||
notFoundContent: computed(() => {
|
||||
return props.notFoundContent;
|
||||
}),
|
||||
activeIndex: computed(() => {
|
||||
return state.activeIndex;
|
||||
}),
|
||||
setActiveIndex,
|
||||
selectOption,
|
||||
});
|
||||
provide(MentionsContextKey, {
|
||||
activeIndex: toRef(state, 'activeIndex'),
|
||||
setActiveIndex,
|
||||
selectOption,
|
||||
onFocus,
|
||||
onBlur,
|
||||
loading: toRef(props, 'loading'),
|
||||
});
|
||||
onUpdated(() => {
|
||||
nextTick(() => {
|
||||
|
@ -232,28 +244,21 @@ const Mentions = {
|
|||
});
|
||||
return () => {
|
||||
const { measureLocation, measurePrefix, measuring } = state;
|
||||
const {
|
||||
prefixCls,
|
||||
placement,
|
||||
transitionName,
|
||||
notFoundContent,
|
||||
getPopupContainer,
|
||||
...restProps
|
||||
} = props;
|
||||
const { prefixCls, placement, transitionName, getPopupContainer, direction, ...restProps } =
|
||||
props;
|
||||
|
||||
const { class: className, style, ...otherAttrs } = attrs;
|
||||
|
||||
const inputProps = omit(restProps, [
|
||||
'value',
|
||||
'defaultValue',
|
||||
'prefix',
|
||||
'split',
|
||||
'children',
|
||||
'validateSearch',
|
||||
'filterOption',
|
||||
'options',
|
||||
'loading',
|
||||
]);
|
||||
|
||||
const options = measuring ? getOptions() : [];
|
||||
const textareaProps = {
|
||||
...inputProps,
|
||||
...otherAttrs,
|
||||
|
@ -265,8 +270,8 @@ const Mentions = {
|
|||
onKeydown: onKeyDown,
|
||||
onKeyup: onKeyUp,
|
||||
onFocus: onInputFocus,
|
||||
onPressenter: onPressEnter,
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={classNames(prefixCls, className)} style={style}>
|
||||
{withDirectives(<textarea ref={textarea} {...textareaProps} />, [[antInput]])}
|
||||
|
@ -277,9 +282,11 @@ const Mentions = {
|
|||
prefixCls={prefixCls}
|
||||
transitionName={transitionName}
|
||||
placement={placement}
|
||||
options={options}
|
||||
options={measuring ? options.value : []}
|
||||
visible
|
||||
direction={direction}
|
||||
getPopupContainer={getPopupContainer}
|
||||
v-slots={{ notFoundContent: slots.notFoundContent, option: slots.option }}
|
||||
>
|
||||
<span>{measurePrefix}</span>
|
||||
</KeywordTrigger>
|
||||
|
@ -290,6 +297,4 @@ const Mentions = {
|
|||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent(Mentions);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import type { InjectionKey, Ref } from 'vue';
|
||||
import type { OptionProps } from './Option';
|
||||
|
||||
export interface MentionsContext {
|
||||
activeIndex: Ref<number>;
|
||||
setActiveIndex?: (index: number) => void;
|
||||
selectOption?: (option: OptionProps) => void;
|
||||
onFocus?: EventListener;
|
||||
onBlur?: EventListener;
|
||||
loading?: Ref<boolean>;
|
||||
}
|
||||
|
||||
const MentionsContextKey: InjectionKey<MentionsContext> = Symbol('MentionsContextKey');
|
||||
|
||||
export default MentionsContextKey;
|
|
@ -1,19 +1,18 @@
|
|||
import type { ExtractPropTypes } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
|
||||
export const optionProps = {
|
||||
value: PropTypes.string,
|
||||
disabled: PropTypes.looseBool,
|
||||
children: PropTypes.any,
|
||||
value: String,
|
||||
disabled: Boolean,
|
||||
label: [String, Number, Function],
|
||||
};
|
||||
|
||||
export type OptionProps = ExtractPropTypes<typeof optionProps>;
|
||||
export type OptionProps = Partial<ExtractPropTypes<typeof optionProps>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Option',
|
||||
props: optionProps,
|
||||
render() {
|
||||
return null;
|
||||
render(_props: any, { slots }: any) {
|
||||
return slots.default?.();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,17 +6,17 @@ import {
|
|||
validateSearch as defaultValidateSearch,
|
||||
} from './util';
|
||||
import { tuple } from '../../_util/type';
|
||||
import type { OptionProps } from './Option';
|
||||
|
||||
export const PlaceMent = tuple('top', 'bottom');
|
||||
export type Direction = 'ltr' | 'rtl';
|
||||
|
||||
export const mentionsProps = {
|
||||
autofocus: PropTypes.looseBool,
|
||||
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||
prefixCls: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
disabled: PropTypes.looseBool,
|
||||
notFoundContent: PropTypes.VNodeChild,
|
||||
split: PropTypes.string,
|
||||
transitionName: PropTypes.string,
|
||||
placement: PropTypes.oneOf(PlaceMent),
|
||||
|
@ -27,16 +27,23 @@ export const mentionsProps = {
|
|||
getPopupContainer: {
|
||||
type: Function as PropType<() => HTMLElement>,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<OptionProps>,
|
||||
default: () => undefined,
|
||||
},
|
||||
loading: PropTypes.looseBool,
|
||||
rows: [Number, String],
|
||||
direction: { type: String as PropType<Direction> },
|
||||
};
|
||||
|
||||
export const vcMentionsProps = {
|
||||
...mentionsProps,
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
export const defaultProps = {
|
||||
prefix: '@',
|
||||
split: ' ',
|
||||
rows: 1,
|
||||
validateSearch: defaultValidateSearch,
|
||||
filterOption: defaultFilterOption,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
import type { MentionsProps } from './Mentions';
|
||||
import type { OptionProps } from './Option';
|
||||
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
type OmitFunc = <T extends object, K extends [...(keyof T)[]]>(
|
||||
obj: T,
|
||||
...keys: K
|
||||
) => { [K2 in Exclude<keyof T, K[number]>]: T[K2] };
|
||||
|
||||
export const omit: OmitFunc = (obj, ...keys) => {
|
||||
const clone = {
|
||||
...obj,
|
||||
};
|
||||
keys.forEach(key => {
|
||||
delete clone[key];
|
||||
});
|
||||
|
||||
return clone;
|
||||
};
|
||||
|
||||
interface MeasureConfig {
|
||||
measureLocation: number;
|
||||
prefix: string;
|
||||
targetText: string;
|
||||
selectionStart: number;
|
||||
split: string;
|
||||
}
|
||||
/**
|
||||
* Cut input selection into 2 part and return text before selection start
|
||||
*/
|
||||
|
@ -15,17 +27,17 @@ export function getBeforeSelectionText(input: HTMLTextAreaElement) {
|
|||
return input.value.slice(0, selectionStart);
|
||||
}
|
||||
|
||||
function lower(char: string | undefined): string {
|
||||
return (char || '').toLowerCase();
|
||||
interface MeasureIndex {
|
||||
location: number;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last match prefix index
|
||||
*/
|
||||
export function getLastMeasureIndex(text: string, prefix: string | string[] = '') {
|
||||
export function getLastMeasureIndex(text: string, prefix: string | string[] = ''): MeasureIndex {
|
||||
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
|
||||
return prefixList.reduce(
|
||||
(lastMatch, prefixStr) => {
|
||||
(lastMatch: MeasureIndex, prefixStr): MeasureIndex => {
|
||||
const lastIndex = text.lastIndexOf(prefixStr);
|
||||
if (lastIndex > lastMatch.location) {
|
||||
return {
|
||||
|
@ -39,6 +51,18 @@ export function getLastMeasureIndex(text: string, prefix: string | string[] = ''
|
|||
);
|
||||
}
|
||||
|
||||
interface MeasureConfig {
|
||||
measureLocation: number;
|
||||
prefix: string;
|
||||
targetText: string;
|
||||
selectionStart: number;
|
||||
split: string;
|
||||
}
|
||||
|
||||
function lower(char: string | undefined): string {
|
||||
return (char || '').toLowerCase();
|
||||
}
|
||||
|
||||
function reduceText(text: string, targetText: string, split: string) {
|
||||
const firstChar = text[0];
|
||||
if (!firstChar || firstChar === split) {
|
||||
|
@ -112,7 +136,7 @@ export function validateSearch(text: string, props: MentionsProps) {
|
|||
return !split || text.indexOf(split) === -1;
|
||||
}
|
||||
|
||||
export function filterOption(input = '', { value = '' } = {}) {
|
||||
export function filterOption(input: string, { value = '' }: OptionProps): boolean {
|
||||
const lowerCase = input.toLowerCase();
|
||||
return value.toLowerCase().indexOf(lowerCase) !== -1;
|
||||
}
|
||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
|||
Subproject commit b6ab0fec2cfa378bab8dfe6c8ef6b6a8664b970e
|
||||
Subproject commit 89612874e476dc788711cdaedfd037b9497e5e78
|
Loading…
Reference in New Issue