|
@@ -1,27 +1,108 @@
|
|
|
-import { createApp, h, reactive, shallowRef } from 'vue'
|
|
|
|
|
-import { TextNode } from 'lexical'
|
|
|
|
|
-import type { App, ShallowRef } from 'vue'
|
|
|
|
|
-import type { EditorConfig, SerializedTextNode } from 'lexical'
|
|
|
|
|
|
|
+import { computed, defineComponent, h, onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
|
|
|
|
+import { $applyNodeReplacement } from 'lexical'
|
|
|
import VarLabel from '@/components/VarLabel/index.vue'
|
|
import VarLabel from '@/components/VarLabel/index.vue'
|
|
|
import type { NodeVar } from '@/types/var'
|
|
import type { NodeVar } from '@/types/var'
|
|
|
-
|
|
|
|
|
-export type SerializedVarLabelNode = SerializedTextNode & {
|
|
|
|
|
- type: 'var-label'
|
|
|
|
|
- className: string
|
|
|
|
|
- key?: string
|
|
|
|
|
|
|
+import { DecoratorBlockNode } from 'lexical-vue/LexicalDecoratorBlockNode'
|
|
|
|
|
+import type { Component, PropType, ShallowRef } from 'vue'
|
|
|
|
|
+import type {
|
|
|
|
|
+ DOMConversionMap,
|
|
|
|
|
+ DOMConversionOutput,
|
|
|
|
|
+ DOMExportOutput,
|
|
|
|
|
+ EditorConfig,
|
|
|
|
|
+ ElementFormatType,
|
|
|
|
|
+ LexicalEditor,
|
|
|
|
|
+ LexicalNode,
|
|
|
|
|
+ NodeKey,
|
|
|
|
|
+ Spread
|
|
|
|
|
+} from 'lexical'
|
|
|
|
|
+import type { SerializedDecoratorBlockNode } from 'lexical-vue'
|
|
|
|
|
+
|
|
|
|
|
+export type SerializedVarLabelNode = Spread<
|
|
|
|
|
+ {
|
|
|
|
|
+ className: string
|
|
|
|
|
+ text: string
|
|
|
|
|
+ },
|
|
|
|
|
+ SerializedDecoratorBlockNode
|
|
|
|
|
+>
|
|
|
|
|
+
|
|
|
|
|
+type LegacySerializedVarLabelNode = {
|
|
|
|
|
+ className?: unknown
|
|
|
|
|
+ text?: unknown
|
|
|
|
|
+ format?: unknown
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type MountedVarLabelState = {
|
|
|
|
|
- app: App<Element>
|
|
|
|
|
|
|
+const DATA_ATTR_CONTEXT_ID = 'data-prompt-editor-context-id'
|
|
|
|
|
+
|
|
|
|
|
+const emptyNodeVars: NodeVar[] = []
|
|
|
|
|
+const promptEditorNodeVarsRefs = new Map<string, ShallowRef<NodeVar[]>>()
|
|
|
|
|
+
|
|
|
|
|
+const VarLabelDecorator = defineComponent({
|
|
|
|
|
+ name: 'PromptEditorVarLabelDecorator',
|
|
|
props: {
|
|
props: {
|
|
|
- label: string
|
|
|
|
|
- contextId: string
|
|
|
|
|
|
|
+ label: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ required: true
|
|
|
|
|
+ },
|
|
|
|
|
+ editor: {
|
|
|
|
|
+ type: Object as PropType<LexicalEditor>,
|
|
|
|
|
+ required: true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ setup(props) {
|
|
|
|
|
+ const contextId = ref('')
|
|
|
|
|
+
|
|
|
|
|
+ const syncContextId = (attempt = 0) => {
|
|
|
|
|
+ const nextContextId =
|
|
|
|
|
+ props.editor
|
|
|
|
|
+ .getRootElement()
|
|
|
|
|
+ ?.closest<HTMLElement>(`[${DATA_ATTR_CONTEXT_ID}]`)
|
|
|
|
|
+ ?.getAttribute(DATA_ATTR_CONTEXT_ID) || ''
|
|
|
|
|
+
|
|
|
|
|
+ if (contextId.value !== nextContextId) {
|
|
|
|
|
+ contextId.value = nextContextId
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!nextContextId && attempt < 6) {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ syncContextId(attempt + 1)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let removeRootListener: (() => void) | null = null
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(() => {
|
|
|
|
|
+ syncContextId()
|
|
|
|
|
+ removeRootListener = props.editor.registerRootListener(() => {
|
|
|
|
|
+ syncContextId()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ onUnmounted(() => {
|
|
|
|
|
+ removeRootListener?.()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const nodeVars = computed(() => {
|
|
|
|
|
+ if (!contextId.value) return emptyNodeVars
|
|
|
|
|
+ return getNodeVarsRef(contextId.value).value
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return () =>
|
|
|
|
|
+ h(VarLabel, {
|
|
|
|
|
+ label: props.label,
|
|
|
|
|
+ nodeVars: nodeVars.value
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
-const mountedVarLabels = new WeakMap<HTMLElement, MountedVarLabelState>()
|
|
|
|
|
-const promptEditorNodeVarsRefs = new Map<string, ShallowRef<NodeVar[]>>()
|
|
|
|
|
-const emptyNodeVars: NodeVar[] = []
|
|
|
|
|
|
|
+function convertVarLabelElement(domNode: HTMLElement): null | DOMConversionOutput {
|
|
|
|
|
+ const text = domNode.getAttribute('data-lexical-var-label')
|
|
|
|
|
+ if (!text) return null
|
|
|
|
|
+
|
|
|
|
|
+ const className = domNode.getAttribute('data-lexical-var-label-class') || 'custom-label'
|
|
|
|
|
+ const node = $createVarLabelNode(className, text)
|
|
|
|
|
+ return { node }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
function getNodeVarsRef(contextId: string) {
|
|
function getNodeVarsRef(contextId: string) {
|
|
|
const existingRef = promptEditorNodeVarsRefs.get(contextId)
|
|
const existingRef = promptEditorNodeVarsRefs.get(contextId)
|
|
@@ -32,28 +113,6 @@ function getNodeVarsRef(contextId: string) {
|
|
|
return nextRef
|
|
return nextRef
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function syncMountedContextId(dom: HTMLElement, props: MountedVarLabelState['props'], attempt = 0) {
|
|
|
|
|
- queueMicrotask(() => {
|
|
|
|
|
- if (!dom.isConnected) {
|
|
|
|
|
- if (attempt < 10) {
|
|
|
|
|
- requestAnimationFrame(() => {
|
|
|
|
|
- syncMountedContextId(dom, props, attempt + 1)
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const nextContextId =
|
|
|
|
|
- dom
|
|
|
|
|
- .closest<HTMLElement>('[data-prompt-editor-context-id]')
|
|
|
|
|
- ?.getAttribute('data-prompt-editor-context-id') || ''
|
|
|
|
|
-
|
|
|
|
|
- if (props.contextId !== nextContextId) {
|
|
|
|
|
- props.contextId = nextContextId
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
export function syncPromptEditorVarLabelNodeVars(
|
|
export function syncPromptEditorVarLabelNodeVars(
|
|
|
contextId: string,
|
|
contextId: string,
|
|
|
nodeVars: NodeVar[] | undefined
|
|
nodeVars: NodeVar[] | undefined
|
|
@@ -69,70 +128,37 @@ export function syncPromptEditorVarLabelNodeVars(
|
|
|
getNodeVarsRef(contextId).value = nextNodeVars
|
|
getNodeVarsRef(contextId).value = nextNodeVars
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function renderLabel(dom: HTMLElement, className: string, rawText: string) {
|
|
|
|
|
- dom.className = `${className} var-label-token`
|
|
|
|
|
- dom.contentEditable = 'false'
|
|
|
|
|
- dom.removeAttribute('title')
|
|
|
|
|
-
|
|
|
|
|
- const mountedState = mountedVarLabels.get(dom)
|
|
|
|
|
- if (mountedState) {
|
|
|
|
|
- mountedState.props.label = rawText
|
|
|
|
|
- syncMountedContextId(dom, mountedState.props)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const mountPoint = document.createElement('span')
|
|
|
|
|
- dom.replaceChildren(mountPoint)
|
|
|
|
|
-
|
|
|
|
|
- const props = reactive({
|
|
|
|
|
- label: rawText,
|
|
|
|
|
- contextId: ''
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- const app = createApp({
|
|
|
|
|
- render() {
|
|
|
|
|
- const nodeVars = props.contextId ? getNodeVarsRef(props.contextId).value : emptyNodeVars
|
|
|
|
|
-
|
|
|
|
|
- return h(VarLabel, {
|
|
|
|
|
- label: props.label,
|
|
|
|
|
- nodeVars
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- app.mount(mountPoint)
|
|
|
|
|
- syncMountedContextId(dom, props)
|
|
|
|
|
-
|
|
|
|
|
- mountedVarLabels.set(dom, {
|
|
|
|
|
- app,
|
|
|
|
|
- props
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export class VarLabelNode extends TextNode {
|
|
|
|
|
|
|
+export class VarLabelNode extends DecoratorBlockNode {
|
|
|
__className: string
|
|
__className: string
|
|
|
|
|
+ __text: string
|
|
|
|
|
|
|
|
static getType() {
|
|
static getType() {
|
|
|
return 'var-label'
|
|
return 'var-label'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
static clone(node: VarLabelNode) {
|
|
static clone(node: VarLabelNode) {
|
|
|
- return new VarLabelNode(node.__className, node.__text, node.__key)
|
|
|
|
|
|
|
+ return new VarLabelNode(node.__className, node.__text, node.__format, node.__key)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- static importJSON(serializedNode: SerializedTextNode): VarLabelNode {
|
|
|
|
|
- const { className, text, key } = serializedNode as SerializedVarLabelNode
|
|
|
|
|
- const node = new VarLabelNode(className, text, key)
|
|
|
|
|
- node.setMode('token')
|
|
|
|
|
- return node
|
|
|
|
|
|
|
+ static importJSON(serializedNode: SerializedVarLabelNode): VarLabelNode {
|
|
|
|
|
+ const legacyNode = serializedNode as SerializedVarLabelNode & LegacySerializedVarLabelNode
|
|
|
|
|
+ const className =
|
|
|
|
|
+ typeof legacyNode.className === 'string' && legacyNode.className.length
|
|
|
|
|
+ ? legacyNode.className
|
|
|
|
|
+ : 'custom-label'
|
|
|
|
|
+ const text = typeof legacyNode.text === 'string' ? legacyNode.text : ''
|
|
|
|
|
+ const format: ElementFormatType = typeof legacyNode.format === 'string' ? legacyNode.format : ''
|
|
|
|
|
+
|
|
|
|
|
+ return new VarLabelNode(className, text, format)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- constructor(className: string, text: string, key?: string) {
|
|
|
|
|
- super(text, key)
|
|
|
|
|
|
|
+ constructor(className: string, text: string, format?: ElementFormatType, key?: NodeKey) {
|
|
|
|
|
+ super(format, key)
|
|
|
this.__className = className
|
|
this.__className = className
|
|
|
|
|
+ this.__text = text
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- isTextEntity() {
|
|
|
|
|
|
|
+ isInline() {
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -144,36 +170,73 @@ export class VarLabelNode extends TextNode {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ isKeyboardSelectable() {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
createDOM(_config: EditorConfig) {
|
|
createDOM(_config: EditorConfig) {
|
|
|
const dom = document.createElement('span')
|
|
const dom = document.createElement('span')
|
|
|
- renderLabel(dom, this.__className, this.__text)
|
|
|
|
|
|
|
+ dom.className = `${this.__className} var-label-token`
|
|
|
|
|
+ dom.contentEditable = 'false'
|
|
|
return dom
|
|
return dom
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- updateDOM(prevNode: VarLabelNode, dom: ChildNode, _config: EditorConfig) {
|
|
|
|
|
- if (!(dom instanceof HTMLElement)) return true
|
|
|
|
|
-
|
|
|
|
|
- if (prevNode.__className !== this.__className || prevNode.__text !== this.__text) {
|
|
|
|
|
- renderLabel(dom, this.__className, this.__text)
|
|
|
|
|
|
|
+ updateDOM(prevNode: VarLabelNode, dom: HTMLElement, _config: EditorConfig) {
|
|
|
|
|
+ if (prevNode.__className !== this.__className) {
|
|
|
|
|
+ dom.className = `${this.__className} var-label-token`
|
|
|
}
|
|
}
|
|
|
|
|
+ dom.contentEditable = 'false'
|
|
|
|
|
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ static importDOM(): DOMConversionMap | null {
|
|
|
|
|
+ return {
|
|
|
|
|
+ span: (domNode: HTMLElement) => {
|
|
|
|
|
+ if (!domNode.hasAttribute('data-lexical-var-label')) return null
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ conversion: convertVarLabelElement,
|
|
|
|
|
+ priority: 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ exportDOM(): DOMExportOutput {
|
|
|
|
|
+ const element = document.createElement('span')
|
|
|
|
|
+ element.setAttribute('data-lexical-var-label', this.__text)
|
|
|
|
|
+ element.setAttribute('data-lexical-var-label-class', this.__className)
|
|
|
|
|
+ element.textContent = this.__text
|
|
|
|
|
+ return { element }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getTextContent() {
|
|
|
|
|
+ return this.__text
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ decorate(_editor: LexicalEditor, _config: EditorConfig): Component {
|
|
|
|
|
+ return h(VarLabelDecorator, {
|
|
|
|
|
+ label: this.__text,
|
|
|
|
|
+ editor: _editor
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
exportJSON(): SerializedVarLabelNode {
|
|
exportJSON(): SerializedVarLabelNode {
|
|
|
return {
|
|
return {
|
|
|
...super.exportJSON(),
|
|
...super.exportJSON(),
|
|
|
type: 'var-label',
|
|
type: 'var-label',
|
|
|
className: this.__className,
|
|
className: this.__className,
|
|
|
|
|
+ text: this.__text,
|
|
|
version: 1
|
|
version: 1
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export function $isVarLabelNode(node: unknown): node is VarLabelNode {
|
|
|
|
|
|
|
+export function $isVarLabelNode(node: LexicalNode | null | undefined): node is VarLabelNode {
|
|
|
return node instanceof VarLabelNode
|
|
return node instanceof VarLabelNode
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function $createVarLabelNode(className: string, text: string) {
|
|
export function $createVarLabelNode(className: string, text: string) {
|
|
|
- return new VarLabelNode(className, text).setMode('token')
|
|
|
|
|
|
|
+ return $applyNodeReplacement(new VarLabelNode(className, text))
|
|
|
}
|
|
}
|