|
@@ -4,7 +4,13 @@ import { MenuOption } from 'lexical-vue'
|
|
|
import { useLexicalComposer } from 'lexical-vue/LexicalComposer'
|
|
import { useLexicalComposer } from 'lexical-vue/LexicalComposer'
|
|
|
import { TypeaheadMenuPlugin } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
|
|
import { TypeaheadMenuPlugin } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
|
|
|
import type { TriggerFn } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
|
|
import type { TriggerFn } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
|
|
|
-import { $insertNodes, type LexicalEditor, type TextNode } from 'lexical'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ $getSelection,
|
|
|
|
|
+ $insertNodes,
|
|
|
|
|
+ $isRangeSelection,
|
|
|
|
|
+ type LexicalEditor,
|
|
|
|
|
+ type TextNode
|
|
|
|
|
+} from 'lexical'
|
|
|
import { Icon } from '@repo/ui'
|
|
import { Icon } from '@repo/ui'
|
|
|
import { VARIABLE_TYPE_OPTIONS } from '@/constant'
|
|
import { VARIABLE_TYPE_OPTIONS } from '@/constant'
|
|
|
import { nodeMap } from '@/nodes'
|
|
import { nodeMap } from '@/nodes'
|
|
@@ -75,9 +81,10 @@ const props = withDefaults(
|
|
|
|
|
|
|
|
const editor = useLexicalComposer()
|
|
const editor = useLexicalComposer()
|
|
|
const { t } = useI18n()
|
|
const { t } = useI18n()
|
|
|
-const keyword = ref('')
|
|
|
|
|
|
|
+const queryKeyword = ref('')
|
|
|
|
|
+const panelKeyword = ref('')
|
|
|
const isMenuOpen = ref(false)
|
|
const isMenuOpen = ref(false)
|
|
|
-const suppressMenu = ref(false)
|
|
|
|
|
|
|
+const suppressedText = ref<string | null>(null)
|
|
|
const menuPanelRef = ref<HTMLElement | null>(null)
|
|
const menuPanelRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
const normalizedOptions = computed(() => {
|
|
const normalizedOptions = computed(() => {
|
|
@@ -85,14 +92,19 @@ const normalizedOptions = computed(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const filteredOptions = computed(() => {
|
|
const filteredOptions = computed(() => {
|
|
|
- const kw = keyword.value.trim().toLowerCase()
|
|
|
|
|
- if (!kw) return normalizedOptions.value
|
|
|
|
|
|
|
+ const query = queryKeyword.value.trim().toLowerCase()
|
|
|
|
|
+ const panel = panelKeyword.value.trim().toLowerCase()
|
|
|
|
|
+ if (!query && !panel) return normalizedOptions.value
|
|
|
|
|
|
|
|
return normalizedOptions.value.filter((option) => {
|
|
return normalizedOptions.value.filter((option) => {
|
|
|
const label = option.label.toLowerCase()
|
|
const label = option.label.toLowerCase()
|
|
|
const value = option.value.toLowerCase()
|
|
const value = option.value.toLowerCase()
|
|
|
const groupName = (option.groupName || '').toLowerCase()
|
|
const groupName = (option.groupName || '').toLowerCase()
|
|
|
- return label.includes(kw) || value.includes(kw) || groupName.includes(kw)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return [query, panel].every((keyword) => {
|
|
|
|
|
+ if (!keyword) return true
|
|
|
|
|
+ return label.includes(keyword) || value.includes(keyword) || groupName.includes(keyword)
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -119,16 +131,31 @@ const normalizedTriggers = computed(() => {
|
|
|
.filter((trigger) => trigger.length > 0)
|
|
.filter((trigger) => trigger.length > 0)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const hasFreshTriggerAtTail = (text: string) => {
|
|
|
|
|
- return normalizedTriggers.value.some((trigger) => text.endsWith(trigger))
|
|
|
|
|
|
|
+const getTextUpToAnchor = () => {
|
|
|
|
|
+ let text: string | null = null
|
|
|
|
|
+
|
|
|
|
|
+ editor.getEditorState().read(() => {
|
|
|
|
|
+ const selection = $getSelection()
|
|
|
|
|
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) return
|
|
|
|
|
+
|
|
|
|
|
+ const anchor = selection.anchor
|
|
|
|
|
+ if (anchor.type !== 'text') return
|
|
|
|
|
+
|
|
|
|
|
+ const anchorNode = anchor.getNode()
|
|
|
|
|
+ if (!anchorNode.isSimpleText()) return
|
|
|
|
|
+
|
|
|
|
|
+ text = anchorNode.getTextContent().slice(0, anchor.offset)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return text
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const triggerMatch: TriggerFn = (text, _lexicalEditor: LexicalEditor) => {
|
|
const triggerMatch: TriggerFn = (text, _lexicalEditor: LexicalEditor) => {
|
|
|
- if (suppressMenu.value) {
|
|
|
|
|
- if (!hasFreshTriggerAtTail(text)) {
|
|
|
|
|
|
|
+ if (suppressedText.value !== null) {
|
|
|
|
|
+ if (suppressedText.value === text) {
|
|
|
return null
|
|
return null
|
|
|
}
|
|
}
|
|
|
- suppressMenu.value = false
|
|
|
|
|
|
|
+ suppressedText.value = null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let latestMatch: ReturnType<TriggerFn> = null
|
|
let latestMatch: ReturnType<TriggerFn> = null
|
|
@@ -172,13 +199,7 @@ const normalizeTypeLabel = (type?: string) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const onQueryChange = (payload: string | null) => {
|
|
const onQueryChange = (payload: string | null) => {
|
|
|
- const nextKeyword = payload || ''
|
|
|
|
|
- if (nextKeyword.length > 0) {
|
|
|
|
|
- requestCloseMenu()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- keyword.value = nextKeyword
|
|
|
|
|
|
|
+ queryKeyword.value = payload || ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const onOpen = () => {
|
|
const onOpen = () => {
|
|
@@ -187,12 +208,14 @@ const onOpen = () => {
|
|
|
|
|
|
|
|
const onClose = () => {
|
|
const onClose = () => {
|
|
|
isMenuOpen.value = false
|
|
isMenuOpen.value = false
|
|
|
- keyword.value = ''
|
|
|
|
|
|
|
+ queryKeyword.value = ''
|
|
|
|
|
+ panelKeyword.value = ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const requestCloseMenu = () => {
|
|
const requestCloseMenu = () => {
|
|
|
- suppressMenu.value = true
|
|
|
|
|
- keyword.value = ''
|
|
|
|
|
|
|
+ suppressedText.value = getTextUpToAnchor()
|
|
|
|
|
+ queryKeyword.value = ''
|
|
|
|
|
+ panelKeyword.value = ''
|
|
|
editor.update(() => {
|
|
editor.update(() => {
|
|
|
// Trigger plugin update cycle so menu closes immediately.
|
|
// Trigger plugin update cycle so menu closes immediately.
|
|
|
})
|
|
})
|
|
@@ -239,11 +262,11 @@ const onDocumentMouseDown = (event: MouseEvent) => {
|
|
|
if (clickedInsideMenu) return
|
|
if (clickedInsideMenu) return
|
|
|
|
|
|
|
|
const rootElement = editor.getRootElement()
|
|
const rootElement = editor.getRootElement()
|
|
|
- const clickedInsideEditor = rootElement?.contains(target) ?? false
|
|
|
|
|
- if (clickedInsideEditor) return
|
|
|
|
|
-
|
|
|
|
|
requestCloseMenu()
|
|
requestCloseMenu()
|
|
|
- rootElement?.blur()
|
|
|
|
|
|
|
+ const clickedInsideEditor = rootElement?.contains(target) ?? false
|
|
|
|
|
+ if (!clickedInsideEditor) {
|
|
|
|
|
+ rootElement?.blur()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const onEditorKeyDown = (event: KeyboardEvent) => {
|
|
const onEditorKeyDown = (event: KeyboardEvent) => {
|
|
@@ -288,13 +311,14 @@ onUnmounted(() => {
|
|
|
<div ref="menuPanelRef" class="var-select__popover">
|
|
<div ref="menuPanelRef" class="var-select__popover">
|
|
|
<div class="var-select__panel">
|
|
<div class="var-select__panel">
|
|
|
<el-input
|
|
<el-input
|
|
|
- :model-value="keyword"
|
|
|
|
|
|
|
+ v-model="panelKeyword"
|
|
|
|
|
+ clearable
|
|
|
size="small"
|
|
size="small"
|
|
|
:placeholder="t('common.nodeBase.promptEditor.searchVariable')"
|
|
:placeholder="t('common.nodeBase.promptEditor.searchVariable')"
|
|
|
class="var-select__search"
|
|
class="var-select__search"
|
|
|
- readonly
|
|
|
|
|
- tabindex="-1"
|
|
|
|
|
- @mousedown.prevent
|
|
|
|
|
|
|
+ @mousedown.stop
|
|
|
|
|
+ @click.stop
|
|
|
|
|
+ @keydown.stop
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<div class="var-select__list">
|
|
<div class="var-select__list">
|