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> 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() }, } }, })