Created
January 30, 2024 00:27
-
-
Save romansp/001601cfbb0b4ce29537949730f3faae to your computer and use it in GitHub Desktop.
Vue TipTap suggestion rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Node, | |
VueNodeViewRenderer, | |
mergeAttributes, | |
} from "@tiptap/vue-3"; | |
import { createSuggestionRenderer } from "./suggestionRenderer"; | |
export default Node.creat({ | |
// your mention node declarations here... | |
addProseMirrorPlugins() { | |
return [ | |
Suggestion({ | |
editor: this.editor, | |
...this.options.suggestion, | |
}), | |
]; | |
}, | |
}).configure({ | |
suggestion: createSuggestionRenderer(MentionList), | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script setup lang="ts"> | |
import type { SuggestionProps } from "@tiptap/suggestion"; | |
import SuggestionList from "./SuggestionList.vue"; | |
const props = defineProps<SuggestionProps>(); | |
function onKeyDown() {} | |
defineExpose({ | |
onKeyDown, | |
}); | |
</script> | |
<template> | |
<SuggestionList | |
v-bind="$props" | |
> | |
<div>Hello popup</div> | |
</SuggestionList> | |
</template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script setup lang="ts"> | |
import type { VirtualElement } from "@floating-ui/dom"; | |
import type { SuggestionProps } from "@tiptap/suggestion"; | |
import { | |
PopoverAnchor, | |
PopoverContent, | |
PopoverPortal, | |
PopoverRoot, | |
} from "radix-vue"; | |
import { ref, watch, watchEffect } from "vue"; | |
const props = defineProps<SuggestionProps>(); | |
const lastValidClientRect = ref<DOMRect>(); | |
watchEffect(() => { | |
// store valid client rect, and use it when suggestion list is about to be detached from the DOM | |
// otherwise list will jump to (0,0) | |
const clientRect = props.clientRect?.(); | |
if (clientRect) { | |
lastValidClientRect.value = clientRect; | |
} | |
}); | |
const anchor = ref<VirtualElement>({ | |
contextElement: props.editor.options.element, | |
getBoundingClientRect() { | |
const clientRect = props.clientRect?.(); | |
return clientRect ?? lastValidClientRect.value ?? { | |
bottom: 0, | |
height: 0, | |
left: 0, | |
right: 0, | |
top: 0, | |
width: 0, | |
x: 0, | |
y: 0, | |
}; | |
}, | |
}); | |
</script> | |
<template> | |
<!-- Wrap in div to workaround tiptap vue rendering single child in teleport --> | |
<div> | |
<Teleport | |
to="body" | |
> | |
<PopoverRoot default-open> | |
<PopoverAnchor :element="anchor as any" /> | |
<PopoverPortal> | |
<PopoverContent | |
update-position-strategy="always" | |
side="bottom" | |
align="start" | |
> | |
<slot /> | |
</PopoverContent> | |
</PopoverPortal> | |
</PopoverRoot> | |
</Teleport> | |
</div> | |
</template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { SuggestionKeyDownProps, SuggestionOptions } from "@tiptap/suggestion"; | |
import { VueRenderer } from "@tiptap/vue-3"; | |
import type { Component } from "vue"; | |
interface SuggestionComponent { | |
onKeyDown?: (props: SuggestionKeyDownProps) => boolean; | |
} | |
function isSuggestionComponent(componentRef: unknown): componentRef is SuggestionComponent { | |
if (typeof componentRef !== "object") return false; | |
if (componentRef === null) return false; | |
return true; | |
} | |
// tiptap doesn't export suggestion state type, so just declare it ourselves | |
export interface SuggestionPluginState { | |
active: boolean; | |
range: Range; | |
query: null | string; | |
text: null | string; | |
composing: boolean; | |
decorationId?: string | null; | |
} | |
export function createSuggestionRenderer(component: Component): Omit<SuggestionOptions, "editor"> { | |
return { | |
render: () => { | |
let renderer: VueRenderer | null; | |
function unmount() { | |
if (!renderer) return; | |
// capture into local variable, because renderer might be re-assigned while promise still settles | |
const thisRenderer = renderer; | |
function cleanup() { | |
thisRenderer.element.remove(); | |
thisRenderer.destroy(); | |
} | |
cleanup(); | |
renderer = null; | |
} | |
return { | |
onStart: props => { | |
renderer = new VueRenderer(component, { | |
props, | |
editor: props.editor, | |
}); | |
props.editor.options.element.appendChild(renderer.element); | |
}, | |
onUpdate(props) { | |
renderer?.updateProps(props); | |
}, | |
onKeyDown(props) { | |
let handled; | |
if (isSuggestionComponent(renderer?.ref)) { | |
handled = renderer?.ref.onKeyDown?.(props); | |
} | |
return handled ?? false; | |
}, | |
onExit() { | |
unmount(); | |
}, | |
}; | |
}, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment