UNPKG

naive-ui

Version:

A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast

456 lines 14.4 kB
import { createTreeMate } from 'treemate'; import { useIsMounted, useMergedState } from 'vooks'; import { computed, defineComponent, h, nextTick, ref, toRef, Transition } from 'vue'; import { VBinder, VFollower, VTarget } from 'vueuc'; import { NInternalSelectMenu } from "../../_internal/index.mjs"; import { useConfig, useFormItem, useTheme, useThemeClass } from "../../_mixins/index.mjs"; import { call, useAdjustedTo, warn } from "../../_utils/index.mjs"; import { NInput } from "../../input/index.mjs"; import { mentionLight } from "../styles/index.mjs"; import style from "./styles/index.cssr.mjs"; import { getRelativePosition } from "./utils.mjs"; export const mentionProps = Object.assign(Object.assign({}, useTheme.props), { to: useAdjustedTo.propTo, autosize: [Boolean, Object], options: { type: Array, default: [] }, filter: { type: Function, default: (pattern, option) => { if (!pattern) return true; if (typeof option.label === 'string') { return option.label.startsWith(pattern); } if (typeof option.value === 'string') { return option.value.startsWith(pattern); } return false; } }, type: { type: String, default: 'text' }, separator: { type: String, validator: separator => { if (separator.length !== 1) { warn('mention', '`separator`\'s length must be 1.'); return false; } return true; }, default: ' ' }, bordered: { type: Boolean, default: undefined }, disabled: Boolean, value: String, defaultValue: { type: String, default: '' }, loading: Boolean, prefix: { type: [String, Array], default: '@' }, placeholder: { type: String, default: '' }, placement: { type: String, default: 'bottom-start' }, size: String, renderLabel: Function, status: String, 'onUpdate:show': [Array, Function], onUpdateShow: [Array, Function], 'onUpdate:value': [Array, Function], onUpdateValue: [Array, Function], onSearch: Function, onSelect: Function, onFocus: Function, onBlur: Function, // private internalDebug: Boolean }); export default defineComponent({ name: 'Mention', props: mentionProps, slots: Object, setup(props) { const { namespaceRef, mergedClsPrefixRef, mergedBorderedRef, inlineThemeDisabled } = useConfig(props); const themeRef = useTheme('Mention', '-mention', style, mentionLight, props, mergedClsPrefixRef); const formItem = useFormItem(props); const inputInstRef = ref(null); const cursorRef = ref(null); const followerRef = ref(null); const wrapperElRef = ref(null); const partialPatternRef = ref(''); let cachedPrefix = null; // cached pattern end is for partial pattern // for example @abc|def // end is after `c` let cachedPartialPatternStart = null; let cachedPartialPatternEnd = null; const filteredOptionsRef = computed(() => { const { value: pattern } = partialPatternRef; return props.options.filter(option => props.filter(pattern, option)); }); const treeMateRef = computed(() => { return createTreeMate(filteredOptionsRef.value, { getKey: v => { return v.value; } }); }); const selectMenuInstRef = ref(null); const showMenuRef = ref(false); const uncontrolledValueRef = ref(props.defaultValue); const controlledValueRef = toRef(props, 'value'); const mergedValueRef = useMergedState(controlledValueRef, uncontrolledValueRef); const cssVarsRef = computed(() => { const { self: { menuBoxShadow } } = themeRef.value; return { '--n-menu-box-shadow': menuBoxShadow }; }); const themeClassHandle = inlineThemeDisabled ? useThemeClass('mention', undefined, cssVarsRef, props) : undefined; function doUpdateShowMenu(show) { if (props.disabled) return; const { onUpdateShow, 'onUpdate:show': _onUpdateShow } = props; if (onUpdateShow) call(onUpdateShow, show); if (_onUpdateShow) call(_onUpdateShow, show); if (!show) { cachedPrefix = null; cachedPartialPatternStart = null; cachedPartialPatternEnd = null; } showMenuRef.value = show; } function doUpdateValue(value) { const { onUpdateValue, 'onUpdate:value': _onUpdateValue } = props; const { nTriggerFormChange, nTriggerFormInput } = formItem; if (_onUpdateValue) { call(_onUpdateValue, value); } if (onUpdateValue) { call(onUpdateValue, value); } nTriggerFormInput(); nTriggerFormChange(); uncontrolledValueRef.value = value; } function getInputEl() { return props.type === 'text' ? inputInstRef.value.inputElRef : inputInstRef.value.textareaElRef; } function deriveShowMenu() { var _a; const inputEl = getInputEl(); if (document.activeElement !== inputEl) { doUpdateShowMenu(false); return; } const { selectionEnd } = inputEl; if (selectionEnd === null) { doUpdateShowMenu(false); return; } const inputValue = inputEl.value; const { separator } = props; const { prefix } = props; const prefixArray = typeof prefix === 'string' ? [prefix] : prefix; for (let i = selectionEnd - 1; i >= 0; --i) { const char = inputValue[i]; if (char === separator || char === '\n' || char === '\r') { doUpdateShowMenu(false); return; } if (prefixArray.includes(char)) { const partialPattern = inputValue.slice(i + 1, selectionEnd); doUpdateShowMenu(true); (_a = props.onSearch) === null || _a === void 0 ? void 0 : _a.call(props, partialPattern, char); partialPatternRef.value = partialPattern; cachedPrefix = char; cachedPartialPatternStart = i + 1; cachedPartialPatternEnd = selectionEnd; return; } } doUpdateShowMenu(false); } function syncCursor() { const { value: cursorAnchor } = cursorRef; if (!cursorAnchor) return; const inputEl = getInputEl(); const cursorPos = getRelativePosition(inputEl); const inputRect = inputEl.getBoundingClientRect(); const wrapperRect = wrapperElRef.value.getBoundingClientRect(); cursorAnchor.style.left = `${cursorPos.left + inputRect.left - wrapperRect.left}px`; cursorAnchor.style.top = `${cursorPos.top + inputRect.top - wrapperRect.top}px`; cursorAnchor.style.height = `${cursorPos.height}px`; } function syncPosition() { var _a; if (!showMenuRef.value) return; (_a = followerRef.value) === null || _a === void 0 ? void 0 : _a.syncPosition(); } function handleInputUpdateValue(value) { doUpdateValue(value); // Vue update is mirco task. // So DOM must have been done when sync start in marco task. // I can't use nextTick(), Chrome doesn't update scrollLeft of INPUT // element is immediatelly updated. The behavior is wired but that's what // happens. syncAfterCursorMove(); } function syncAfterCursorMove() { setTimeout(() => { syncCursor(); deriveShowMenu(); void nextTick().then(syncPosition); }, 0); } function handleInputKeyDown(e) { var _a, _b; if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { if ((_a = inputInstRef.value) === null || _a === void 0 ? void 0 : _a.isCompositing) return; syncAfterCursorMove(); } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter') { if ((_b = inputInstRef.value) === null || _b === void 0 ? void 0 : _b.isCompositing) return; const { value: selectMenuInst } = selectMenuInstRef; if (showMenuRef.value) { if (selectMenuInst) { e.preventDefault(); if (e.key === 'ArrowUp') { selectMenuInst.prev(); } else if (e.key === 'ArrowDown') { selectMenuInst.next(); } else { // Enter const pendingOptionTmNode = selectMenuInst.getPendingTmNode(); if (pendingOptionTmNode) { handleSelect(pendingOptionTmNode); } else { doUpdateShowMenu(false); } } } } else { syncAfterCursorMove(); } } } function handleInputFocus(e) { const { onFocus } = props; onFocus === null || onFocus === void 0 ? void 0 : onFocus(e); const { nTriggerFormFocus } = formItem; nTriggerFormFocus(); syncAfterCursorMove(); } function focus() { var _a; (_a = inputInstRef.value) === null || _a === void 0 ? void 0 : _a.focus(); } function blur() { var _a; (_a = inputInstRef.value) === null || _a === void 0 ? void 0 : _a.blur(); } function handleInputBlur(e) { const { onBlur } = props; onBlur === null || onBlur === void 0 ? void 0 : onBlur(e); const { nTriggerFormBlur } = formItem; nTriggerFormBlur(); doUpdateShowMenu(false); } function handleSelect(tmNode) { var _a; if (cachedPrefix === null || cachedPartialPatternStart === null || cachedPartialPatternEnd === null) { if (process.env.NODE_ENV !== 'production') { warn('mention', 'Cache works unexpectly, this is probably a bug. Please create an issue.'); } return; } const { rawNode: { value = '' } } = tmNode; const inputEl = getInputEl(); const inputValue = inputEl.value; const { separator } = props; const nextEndPart = inputValue.slice(cachedPartialPatternEnd); const alreadySeparated = nextEndPart.startsWith(separator); const nextMiddlePart = `${value}${alreadySeparated ? '' : separator}`; doUpdateValue(inputValue.slice(0, cachedPartialPatternStart) + nextMiddlePart + nextEndPart); (_a = props.onSelect) === null || _a === void 0 ? void 0 : _a.call(props, tmNode.rawNode, cachedPrefix); const nextSelectionEnd = cachedPartialPatternStart + nextMiddlePart.length + (alreadySeparated ? 1 : 0); void nextTick().then(() => { // input value is updated inputEl.selectionStart = nextSelectionEnd; inputEl.selectionEnd = nextSelectionEnd; deriveShowMenu(); }); } function handleInputMouseDown() { if (!props.disabled) { syncAfterCursorMove(); } } return { namespace: namespaceRef, mergedClsPrefix: mergedClsPrefixRef, mergedBordered: mergedBorderedRef, mergedSize: formItem.mergedSizeRef, mergedStatus: formItem.mergedStatusRef, mergedTheme: themeRef, treeMate: treeMateRef, selectMenuInstRef, inputInstRef, cursorRef, followerRef, wrapperElRef, showMenu: showMenuRef, adjustedTo: useAdjustedTo(props), isMounted: useIsMounted(), mergedValue: mergedValueRef, handleInputFocus, handleInputBlur, handleInputUpdateValue, handleInputKeyDown, handleSelect, handleInputMouseDown, focus, blur, cssVars: inlineThemeDisabled ? undefined : cssVarsRef, themeClass: themeClassHandle === null || themeClassHandle === void 0 ? void 0 : themeClassHandle.themeClass, onRender: themeClassHandle === null || themeClassHandle === void 0 ? void 0 : themeClassHandle.onRender }; }, render() { const { mergedTheme, mergedClsPrefix, $slots } = this; return h("div", { class: `${mergedClsPrefix}-mention`, ref: "wrapperElRef" }, h(NInput, { status: this.mergedStatus, themeOverrides: mergedTheme.peerOverrides.Input, theme: mergedTheme.peers.Input, size: this.mergedSize, autosize: this.autosize, type: this.type, ref: "inputInstRef", placeholder: this.placeholder, onMousedown: this.handleInputMouseDown, onUpdateValue: this.handleInputUpdateValue, onKeydown: this.handleInputKeyDown, onFocus: this.handleInputFocus, onBlur: this.handleInputBlur, bordered: this.mergedBordered, disabled: this.disabled, value: this.mergedValue }), h(VBinder, null, { default: () => [h(VTarget, null, { default: () => { const style = { position: 'absolute', width: 0 }; if (process.env.NODE_ENV !== 'production' && this.internalDebug) { style.width = '1px'; style.background = 'red'; } return h("div", { style: style, ref: "cursorRef" }); } }), h(VFollower, { ref: "followerRef", placement: this.placement, show: this.showMenu, containerClass: this.namespace, to: this.adjustedTo, teleportDisabled: this.adjustedTo === useAdjustedTo.tdkey }, { default: () => h(Transition, { name: "fade-in-scale-up-transition", appear: this.isMounted }, { default: () => { const { mergedTheme, onRender } = this; onRender === null || onRender === void 0 ? void 0 : onRender(); return this.showMenu ? h(NInternalSelectMenu, { clsPrefix: mergedClsPrefix, theme: mergedTheme.peers.InternalSelectMenu, themeOverrides: mergedTheme.peerOverrides.InternalSelectMenu, autoPending: true, ref: "selectMenuInstRef", class: [`${mergedClsPrefix}-mention-menu`, this.themeClass], loading: this.loading, treeMate: this.treeMate, virtualScroll: false, style: this.cssVars, onToggle: this.handleSelect, renderLabel: this.renderLabel }, $slots) : null; } }) })] })); } });