From 480b5c358144be556dbed731b82a0596d4789f18 Mon Sep 17 00:00:00 2001 From: ajuner <106791576@qq.com> Date: Fri, 2 Jul 2021 17:24:05 +0800 Subject: [PATCH] refactor(mentions): use compositionAPI --- components/mentions/index.tsx | 195 ++++++----- components/mentions/style/index.less | 10 +- components/mentions/style/rtl.less | 10 + components/vc-mentions/{index.js => index.ts} | 0 components/vc-mentions/src/DropdownMenu.jsx | 65 ---- components/vc-mentions/src/DropdownMenu.tsx | 63 ++++ ...{KeywordTrigger.jsx => KeywordTrigger.tsx} | 11 +- components/vc-mentions/src/Mentions.jsx | 312 ------------------ components/vc-mentions/src/Mentions.tsx | 295 +++++++++++++++++ .../src/{Option.jsx => Option.tsx} | 7 +- components/vc-mentions/src/mentionsProps.ts | 6 +- components/vc-mentions/src/placement.js | 3 - .../vc-mentions/src/{util.js => util.ts} | 25 +- 13 files changed, 496 insertions(+), 506 deletions(-) create mode 100644 components/mentions/style/rtl.less rename components/vc-mentions/{index.js => index.ts} (100%) delete mode 100644 components/vc-mentions/src/DropdownMenu.jsx create mode 100644 components/vc-mentions/src/DropdownMenu.tsx rename components/vc-mentions/src/{KeywordTrigger.jsx => KeywordTrigger.tsx} (88%) delete mode 100644 components/vc-mentions/src/Mentions.jsx create mode 100644 components/vc-mentions/src/Mentions.tsx rename components/vc-mentions/src/{Option.jsx => Option.tsx} (62%) delete mode 100644 components/vc-mentions/src/placement.js rename components/vc-mentions/src/{util.js => util.ts} (77%) diff --git a/components/mentions/index.tsx b/components/mentions/index.tsx index dfc62c38ef..6f9bfed3d1 100644 --- a/components/mentions/index.tsx +++ b/components/mentions/index.tsx @@ -1,15 +1,14 @@ import type { App, PropType, VNodeTypes, Plugin, ExtractPropTypes } from 'vue'; -import { defineComponent, inject, nextTick } from 'vue'; +import { ref, onMounted } from 'vue'; +import { defineComponent, nextTick } from 'vue'; import classNames from '../_util/classNames'; 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 BaseMixin from '../_util/BaseMixin'; -import { defaultConfigProvider } from '../config-provider'; -import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; import type { RenderEmptyHandler } from '../config-provider/renderEmpty'; +import useConfigInject from '../_util/hooks/useConfigInject'; const { Option } = VcMentions; @@ -79,65 +78,46 @@ export type MentionsProps = Partial>; const Mentions = defineComponent({ name: 'AMentions', - mixins: [BaseMixin], inheritAttrs: false, - Option: { ...Option, name: 'AMentionsOption' }, - getMentions, props: mentionsProps, + getMentions, emits: ['update:value', 'change', 'focus', 'blur', 'select'], - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), + setup(props, { slots, emit, attrs, expose }) { + const { prefixCls, renderEmpty, direction } = useConfigInject('mentions', props); + const focused = ref(false); + const vcMentions = ref(null); + + const handleFocus = (e: FocusEvent) => { + focused.value = true; + emit('focus', e); }; - }, - data() { - return { - focused: false, + + const handleBlur = (e: FocusEvent) => { + focused.value = false; + emit('blur', e); }; - }, - mounted() { - nextTick(() => { - if (process.env.NODE_ENV === 'test') { - if (this.autofocus) { - this.focus(); - } - } - }); - }, - methods: { - handleFocus(e: FocusEvent) { - this.$emit('focus', e); - this.setState({ - focused: true, - }); - }, - handleBlur(e: FocusEvent) { - this.$emit('blur', e); - this.setState({ - focused: false, - }); - }, - handleSelect(...args: [MentionsOptionProps, string]) { - this.$emit('select', ...args); - this.setState({ - focused: true, - }); - }, - handleChange(val: string) { - this.$emit('update:value', val); - this.$emit('change', val); - }, - getNotFoundContent(renderEmpty: RenderEmptyHandler) { - const notFoundContent = getComponent(this, 'notFoundContent'); + + const handleSelect = (...args: [MentionsOptionProps, string]) => { + emit('select', ...args); + focused.value = true; + }; + + const handleChange = (val: string) => { + emit('update:value', val); + emit('change', val); + }; + + const getNotFoundContent = (renderEmpty: RenderEmptyHandler) => { + const notFoundContent = props.notFoundContent; if (notFoundContent !== undefined) { return notFoundContent; } return renderEmpty('Select'); - }, - getOptions() { - const { loading } = this.$props; - const children = getSlot(this); + }; + + const getOptions = () => { + const { loading } = props; if (loading) { return ( @@ -146,71 +126,82 @@ const Mentions = defineComponent({ ); } - return children; - }, - getFilterOption() { - const { filterOption, loading } = this.$props; + return slots.default?.(); + }; + + const getFilterOption = () => { + const { filterOption, loading } = props; if (loading) { return loadingFilterOption; } return filterOption; - }, - focus() { - (this.$refs.vcMentions as HTMLTextAreaElement).focus(); - }, - blur() { - (this.$refs.vcMentions as HTMLTextAreaElement).blur(); - }, - }, - render() { - const { focused } = this.$data; - const { getPrefixCls, renderEmpty } = this.configProvider; - const { - prefixCls: customizePrefixCls, - disabled, - getPopupContainer, - ...restProps - } = getOptionProps(this) as any; - const { class: className, ...otherAttrs } = this.$attrs; - const prefixCls = getPrefixCls('mentions', customizePrefixCls); - const otherProps = omit(restProps, ['loading', 'onUpdate:value']); - - const mergedClassName = classNames(className, { - [`${prefixCls}-disabled`]: disabled, - [`${prefixCls}-focused`]: focused, - }); + }; - const mentionsProps = { - prefixCls, - notFoundContent: this.getNotFoundContent(renderEmpty), - ...otherProps, - disabled, - filterOption: this.getFilterOption(), - getPopupContainer, - children: this.getOptions(), - class: mergedClassName, - rows: 1, - ...otherAttrs, - onChange: this.handleChange, - onSelect: this.handleSelect, - onFocus: this.handleFocus, - onBlur: this.handleBlur, - ref: 'vcMentions', + const focus = () => { + (vcMentions.value as HTMLTextAreaElement).focus(); }; + const blur = () => { + (vcMentions.value as HTMLTextAreaElement).blur(); + }; + + expose({ focus, blur }); + + onMounted(() => { + nextTick(() => { + if (process.env.NODE_ENV === 'test') { + if (props.autofocus) { + focus(); + } + } + }); + }); + + return () => { + const { disabled, getPopupContainer, ...restProps } = props; + const { class: className, ...otherAttrs } = attrs; + const otherProps = omit(restProps, ['loading', 'onUpdate:value', 'prefixCls']); + + const mergedClassName = classNames(className, { + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-focused`]: focused.value, + [`${prefixCls}-rtl`]: direction.value === 'rtl', + }); - return ; + const mentionsProps = { + prefixCls: prefixCls.value, + notFoundContent: getNotFoundContent(renderEmpty.value), + ...otherProps, + disabled, + direction: direction.value, + filterOption: getFilterOption(), + getPopupContainer, + children: getOptions(), + class: mergedClassName, + rows: 1, + ...otherAttrs, + onChange: handleChange, + onSelect: handleSelect, + onFocus: handleFocus, + onBlur: handleBlur, + ref: vcMentions, + }; + return ; + }; }, }); +export const MentionsOption = { + ...Option, + name: 'AMentionsOption', +}; + /* istanbul ignore next */ Mentions.install = function (app: App) { app.component(Mentions.name, Mentions); - app.component(Mentions.Option.name, Mentions.Option); + app.component(MentionsOption.name, MentionsOption); return app; }; -export const MentionsOption = Mentions.Option; - export default Mentions as typeof Mentions & Plugin & { readonly Option: typeof Option; diff --git a/components/mentions/style/index.less b/components/mentions/style/index.less index 0432803fbf..d2285d943c 100644 --- a/components/mentions/style/index.less +++ b/components/mentions/style/index.less @@ -64,10 +64,6 @@ background-color: transparent; } .placeholder(); - - &:read-only { - cursor: default; - } } &-measure { @@ -123,7 +119,7 @@ overflow: hidden; color: @text-color; font-weight: normal; - line-height: 22px; + line-height: @line-height-base; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; @@ -159,9 +155,11 @@ } &-active { - background-color: @item-active-bg; + background-color: @item-hover-bg; } } } } } + +@import './rtl'; diff --git a/components/mentions/style/rtl.less b/components/mentions/style/rtl.less new file mode 100644 index 0000000000..7cd95832dc --- /dev/null +++ b/components/mentions/style/rtl.less @@ -0,0 +1,10 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@mention-prefix-cls: ~'@{ant-prefix}-mentions'; + +.@{mention-prefix-cls} { + &-rtl { + direction: rtl; + } +} diff --git a/components/vc-mentions/index.js b/components/vc-mentions/index.ts similarity index 100% rename from components/vc-mentions/index.js rename to components/vc-mentions/index.ts diff --git a/components/vc-mentions/src/DropdownMenu.jsx b/components/vc-mentions/src/DropdownMenu.jsx deleted file mode 100644 index d7934e202b..0000000000 --- a/components/vc-mentions/src/DropdownMenu.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import Menu, { Item as MenuItem } from '../../menu'; -import PropTypes from '../../_util/vue-types'; -import { OptionProps } from './Option'; -import { inject } from 'vue'; - -function noop() {} -export default { - name: 'DropdownMenu', - props: { - prefixCls: PropTypes.string, - options: PropTypes.arrayOf(OptionProps), - }, - setup() { - return { - mentionsContext: inject('mentionsContext'), - }; - }, - render() { - const { - notFoundContent, - activeIndex, - setActiveIndex, - selectOption, - onFocus = noop, - onBlur = noop, - } = this.mentionsContext; - const { prefixCls, options } = this.$props; - const activeOption = options[activeIndex] || {}; - - return ( - { - const option = options.find(({ value }) => value === key); - selectOption(option); - }} - onBlur={onBlur} - onFocus={onFocus} - > - {[ - ...options.map((option, index) => { - const { value, disabled, children } = option; - return ( - { - setActiveIndex(index); - }} - > - {children} - - ); - }), - !options.length && ( - - {notFoundContent} - - ), - ].filter(Boolean)} - - ); - }, -}; diff --git a/components/vc-mentions/src/DropdownMenu.tsx b/components/vc-mentions/src/DropdownMenu.tsx new file mode 100644 index 0000000000..453b9dc22f --- /dev/null +++ b/components/vc-mentions/src/DropdownMenu.tsx @@ -0,0 +1,63 @@ +import Menu, { Item as MenuItem } from '../../menu'; +import PropTypes from '../../_util/vue-types'; +import { defineComponent, inject } from 'vue'; + +function noop() {} + +export default defineComponent({ + name: 'DropdownMenu', + props: { + prefixCls: PropTypes.string, + options: PropTypes.any, + }, + setup(props) { + return () => { + const { + notFoundContent, + activeIndex, + setActiveIndex, + selectOption, + onFocus = noop, + onBlur = noop, + } = inject('mentionsContext'); + + const { prefixCls, options } = props; + const activeOption = options[activeIndex.value] || {}; + + return ( + { + const option = options.find(({ value }) => value === key); + selectOption(option); + }} + onBlur={onBlur} + onFocus={onFocus} + > + {[ + ...options.map((option, index) => { + const { value, disabled, children } = option; + return ( + { + setActiveIndex(index); + }} + > + {children} + + ); + }), + !options.length && ( + + {notFoundContent.value} + + ), + ].filter(Boolean)} + + ); + }; + }, +}); diff --git a/components/vc-mentions/src/KeywordTrigger.jsx b/components/vc-mentions/src/KeywordTrigger.tsx similarity index 88% rename from components/vc-mentions/src/KeywordTrigger.jsx rename to components/vc-mentions/src/KeywordTrigger.tsx index 2e5f01d295..7da20e4e88 100644 --- a/components/vc-mentions/src/KeywordTrigger.jsx +++ b/components/vc-mentions/src/KeywordTrigger.tsx @@ -1,8 +1,7 @@ import PropTypes from '../../_util/vue-types'; import Trigger from '../../vc-trigger'; import DropdownMenu from './DropdownMenu'; -import { OptionProps } from './Option'; -import { PlaceMent } from './placement'; +import { defineComponent } from 'vue'; const BUILT_IN_PLACEMENTS = { bottomRight: { @@ -23,13 +22,13 @@ const BUILT_IN_PLACEMENTS = { }, }; -export default { +export default defineComponent({ name: 'KeywordTrigger', props: { loading: PropTypes.looseBool, - options: PropTypes.arrayOf(OptionProps), + options: PropTypes.array, prefixCls: PropTypes.string, - placement: PropTypes.oneOf(PlaceMent), + placement: PropTypes.string, visible: PropTypes.looseBool, transitionName: PropTypes.string, getPopupContainer: PropTypes.func, @@ -67,4 +66,4 @@ export default { ); }, -}; +}); diff --git a/components/vc-mentions/src/Mentions.jsx b/components/vc-mentions/src/Mentions.jsx deleted file mode 100644 index 91a5d9a66a..0000000000 --- a/components/vc-mentions/src/Mentions.jsx +++ /dev/null @@ -1,312 +0,0 @@ -import { defineComponent, provide, withDirectives } from 'vue'; -import classNames from '../../_util/classNames'; -import omit from 'omit.js'; -import KeyCode from '../../_util/KeyCode'; -import BaseMixin from '../../_util/BaseMixin'; -import { hasProp, getOptionProps, initDefaultProps } from '../../_util/props-util'; -import warning from 'warning'; -import { - getBeforeSelectionText, - getLastMeasureIndex, - replaceWithMeasure, - setInputSelection, -} from './util'; -import KeywordTrigger from './KeywordTrigger'; -import { vcMentionsProps, defaultProps } from './mentionsProps'; -import antInput from '../../_util/antInputDirective'; - -function noop() {} - -const Mentions = { - name: 'Mentions', - mixins: [BaseMixin], - inheritAttrs: false, - props: initDefaultProps(vcMentionsProps, defaultProps), - created() { - this.mentionsContext = provide('mentionsContext', this); - }, - data() { - const { value = '', defaultValue = '' } = this.$props; - warning(this.$props.children, 'please children prop replace slots.default'); - return { - _value: !hasProp(this, 'value') ? defaultValue : value, - measuring: false, - measureLocation: 0, - measureText: null, - measurePrefix: '', - activeIndex: 0, - isFocus: false, - }; - }, - watch: { - value(val) { - this.$data._value = val; - }, - }, - updated() { - this.$nextTick(() => { - const { measuring } = this.$data; - - // Sync measure div top with textarea for rc-trigger usage - if (measuring) { - this.$refs.measure.scrollTop = this.$refs.textarea.scrollTop; - } - }); - }, - methods: { - triggerChange(value) { - const props = getOptionProps(this); - if (!('value' in props)) { - this.setState({ _value: value }); - } else { - this.$forceUpdate(); - } - this.__emit('change', value); - }, - onChange({ target: { value, composing }, isComposing }) { - if (isComposing || composing) return; - this.triggerChange(value); - }, - onKeyDown(event) { - const { which } = event; - const { activeIndex, measuring } = this.$data; - // Skip if not measuring - if (!measuring) { - return; - } - - if (which === KeyCode.UP || which === KeyCode.DOWN) { - // Control arrow function - const optionLen = this.getOptions().length; - const offset = which === KeyCode.UP ? -1 : 1; - const newActiveIndex = (activeIndex + offset + optionLen) % optionLen; - this.setState({ - activeIndex: newActiveIndex, - }); - event.preventDefault(); - } else if (which === KeyCode.ESC) { - this.stopMeasure(); - } else if (which === KeyCode.ENTER) { - // Measure hit - event.preventDefault(); - const options = this.getOptions(); - if (!options.length) { - this.stopMeasure(); - return; - } - const option = options[activeIndex]; - this.selectOption(option); - } - }, - /** - * When to start measure: - * 1. When user press `prefix` - * 2. When measureText !== prevMeasureText - * - If measure hit - * - If measuring - * - * When to stop measure: - * 1. Selection is out of range - * 2. Contains `space` - * 3. ESC or select one - */ - onKeyUp(event) { - const { key, which } = event; - const { measureText: prevMeasureText, measuring } = this.$data; - const { prefix = '', validateSearch } = this.$props; - const target = event.target; - const selectionStartText = getBeforeSelectionText(target); - const { location: measureIndex, prefix: measurePrefix } = getLastMeasureIndex( - selectionStartText, - prefix, - ); - - // Skip if match the white key list - if ([KeyCode.ESC, KeyCode.UP, KeyCode.DOWN, KeyCode.ENTER].indexOf(which) !== -1) { - return; - } - - if (measureIndex !== -1) { - const measureText = selectionStartText.slice(measureIndex + measurePrefix.length); - const validateMeasure = validateSearch(measureText, this.$props); - const matchOption = !!this.getOptions(measureText).length; - - if (validateMeasure) { - if ( - key === measurePrefix || - measuring || - (measureText !== prevMeasureText && matchOption) - ) { - this.startMeasure(measureText, measurePrefix, measureIndex); - } - } else if (measuring) { - // Stop if measureText is invalidate - this.stopMeasure(); - } - - /** - * We will trigger `onSearch` to developer since they may use for async update. - * If met `space` means user finished searching. - */ - if (validateMeasure) { - this.__emit('search', measureText, measurePrefix); - } - } else if (measuring) { - this.stopMeasure(); - } - }, - onInputFocus(event) { - this.onFocus(event); - }, - onInputBlur(event) { - this.onBlur(event); - }, - onDropdownFocus() { - this.onFocus(); - }, - onDropdownBlur() { - this.onBlur(); - }, - onFocus(event) { - window.clearTimeout(this.focusId); - const { isFocus } = this.$data; - if (!isFocus && event) { - this.__emit('focus', event); - } - this.setState({ isFocus: true }); - }, - onBlur(event) { - this.focusId = window.setTimeout(() => { - this.setState({ isFocus: false }); - this.stopMeasure(); - this.__emit('blur', event); - }, 100); - }, - selectOption(option) { - const { _value: value, measureLocation, measurePrefix } = this.$data; - const { split } = this.$props; - const { value: mentionValue = '' } = option; - const { text, selectionLocation } = replaceWithMeasure(value, { - measureLocation, - targetText: mentionValue, - prefix: measurePrefix, - selectionStart: this.$refs.textarea.selectionStart, - split, - }); - this.triggerChange(text); - this.stopMeasure(() => { - // We need restore the selection position - setInputSelection(this.$refs.textarea, selectionLocation); - }); - - this.__emit('select', option, measurePrefix); - }, - setActiveIndex(activeIndex) { - this.setState({ - activeIndex, - }); - }, - getOptions(measureText) { - const targetMeasureText = measureText || this.$data.measureText || ''; - const { filterOption, children = [] } = this.$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); - }); - return list; - }, - startMeasure(measureText, measurePrefix, measureLocation) { - this.setState({ - measuring: true, - measureText, - measurePrefix, - measureLocation, - activeIndex: 0, - }); - }, - stopMeasure(callback) { - this.setState( - { - measuring: false, - measureLocation: 0, - measureText: null, - }, - callback, - ); - }, - focus() { - this.$refs.textarea.focus(); - }, - blur() { - this.$refs.textarea.blur(); - }, - }, - - render() { - const { _value: value, measureLocation, measurePrefix, measuring } = this.$data; - const { - prefixCls, - placement, - transitionName, - notFoundContent, - getPopupContainer, - ...restProps - } = getOptionProps(this); - - const { class: className, style, ...otherAttrs } = this.$attrs; - - const inputProps = omit(restProps, [ - 'value', - 'defaultValue', - 'prefix', - 'split', - 'children', - 'validateSearch', - 'filterOption', - ]); - - const options = measuring ? this.getOptions() : []; - const textareaProps = { - ...inputProps, - ...otherAttrs, - onChange: noop, - onSelect: noop, - value, - onInput: this.onChange, - onBlur: this.onInputBlur, - onKeydown: this.onKeyDown, - onKeyup: this.onKeyUp, - onFocus: this.onInputFocus, - }; - return ( -
- {withDirectives(