105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import type {SvelteComponent, ComponentType} from "svelte"
|
|
import type {Readable} from "svelte/store"
|
|
import tippy, {type Instance} from "tippy.js"
|
|
import type {Editor} from "@tiptap/core"
|
|
import {PluginKey} from "@tiptap/pm/state"
|
|
import Suggestion from "@tiptap/suggestion"
|
|
import type {Search} from "@welshman/app"
|
|
|
|
export type SuggestionsOptions = {
|
|
char: string
|
|
name: string
|
|
editor: Editor
|
|
search: Readable<Search<any, any>>
|
|
select: (value: any, props: any) => void
|
|
allowCreate?: boolean
|
|
suggestionComponent: ComponentType
|
|
suggestionsComponent: ComponentType
|
|
}
|
|
|
|
export const createSuggestions = (options: SuggestionsOptions) =>
|
|
Suggestion({
|
|
char: options.char,
|
|
editor: options.editor,
|
|
pluginKey: new PluginKey(`suggest-${options.name}`),
|
|
command: ({editor, range, props}) => {
|
|
// increase range.to by one when the next node is of type "text"
|
|
// and starts with a space character
|
|
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
|
const overrideSpace = nodeAfter?.text?.startsWith(" ")
|
|
|
|
if (overrideSpace) {
|
|
range.to += 1
|
|
}
|
|
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.insertContentAt(range, [
|
|
{type: options.name, attrs: props},
|
|
{type: "text", text: " "},
|
|
])
|
|
.run()
|
|
|
|
window.getSelection()?.collapseToEnd()
|
|
},
|
|
allow: ({state, range}) => {
|
|
const $from = state.doc.resolve(range.from)
|
|
const type = state.schema.nodes[options.name]
|
|
|
|
return !!$from.parent.type.contentMatch.matchType(type)
|
|
},
|
|
render: () => {
|
|
let popover: Instance[]
|
|
let suggestions: SvelteComponent
|
|
|
|
const mapProps = (props: any) => ({
|
|
term: props.query,
|
|
search: options.search,
|
|
allowCreate: options.allowCreate,
|
|
component: options.suggestionComponent,
|
|
select: (value: string) => options.select(value, props),
|
|
})
|
|
|
|
return {
|
|
onStart: props => {
|
|
const target = document.createElement("div")
|
|
|
|
popover = tippy("body", {
|
|
getReferenceClientRect: props.clientRect as any,
|
|
appendTo: document.querySelector("dialog[open]") || document.body,
|
|
content: target,
|
|
showOnCreate: true,
|
|
interactive: true,
|
|
trigger: "manual",
|
|
placement: "bottom-start",
|
|
})
|
|
|
|
suggestions = new options.suggestionsComponent({target, props: mapProps(props)})
|
|
},
|
|
onUpdate: props => {
|
|
suggestions.$set(mapProps(props))
|
|
|
|
if (props.clientRect) {
|
|
popover[0].setProps({
|
|
getReferenceClientRect: props.clientRect as any,
|
|
})
|
|
}
|
|
},
|
|
onKeyDown: props => {
|
|
if (props.event.key === "Escape") {
|
|
popover[0].hide()
|
|
|
|
return true
|
|
}
|
|
|
|
return Boolean(suggestions.onKeyDown?.(props.event))
|
|
},
|
|
onExit: () => {
|
|
popover[0].destroy()
|
|
suggestions.$destroy()
|
|
},
|
|
}
|
|
},
|
|
})
|