import {ContentMenu} from "fwtoolkit" import type {ContentMenuInit} from "fwtoolkit/content_menu" import {DOMSerializer, type Node} from "prosemirror-model" import {NodeSelection, Plugin, PluginKey} from "prosemirror-state" import type {EditorView, NodeView} from "prosemirror-view" import type {Editor} from "../types.js" const key = new PluginKey("figureMenu") class FigureView implements NodeView { node: Node view: EditorView getPos: () => number options: {editor: Editor} dom: HTMLElement serializer: DOMSerializer contentDOM: HTMLElement menuButton: HTMLButtonElement constructor( node: Node, view: EditorView, getPos: () => number, options: {editor: Editor} ) { this.node = node this.view = view this.getPos = getPos this.options = options this.dom = document.createElement("div") this.dom.classList.add("figure") this.serializer = DOMSerializer.fromSchema(node.type.schema) const contentDOM = this.serializer.serializeNode(this.node) as HTMLElement contentDOM.classList.forEach((className: string) => this.dom.classList.add(className) ) contentDOM.classList.value = "" this.dom.appendChild(contentDOM) this.contentDOM = contentDOM this.menuButton = document.createElement("button") this.menuButton.classList.add("figure-menu-btn") this.menuButton.innerHTML = '' this.dom.insertBefore(this.menuButton, this.dom.firstChild) } stopEvent(event: Event) { let stopped = false if (event.type === "mousedown") { const mouseEvent = event as MouseEvent const composedPath = mouseEvent.composedPath() if (composedPath.includes(this.menuButton)) { stopped = true const tr = this.view.state.tr const $pos = this.view.state.doc.resolve(this.getPos()) tr.setSelection(new NodeSelection($pos)) this.view.dispatch(tr) const contentMenu = new ContentMenu({ menu: this.options.editor.menu.figureMenuModel as ContentMenuInit, width: 280, page: this.options.editor, menuPos: { X: Number.parseInt(mouseEvent.pageX as unknown as string) + 20, Y: Number.parseInt(mouseEvent.pageY as unknown as string) - 100 }, onClose: () => { this.view.focus() } }) contentMenu.open() } else if ( composedPath.includes(this.dom) && !composedPath.find( el => el instanceof Element && el.matches && el.matches("figcaption") ) ) { stopped = true const tr = this.view.state.tr const $pos = this.view.state.doc.resolve(this.getPos()) tr.setSelection(new NodeSelection($pos)) this.view.dispatch(tr) } } return stopped } } class FigureCaptionView implements NodeView { node: Node view: EditorView getPos: () => number options: {editor: Editor} dom: HTMLElement contentDOM: HTMLElement constructor( node: Node, view: EditorView, getPos: () => number, options: {editor: Editor} ) { this.node = node this.view = view this.getPos = getPos this.options = options this.dom = document.createElement("figcaption") this.dom.innerHTML = '' this.contentDOM = this.dom.lastElementChild as HTMLElement } } export const figurePlugin = (options: {editor: Editor}) => new Plugin({ key, state: { init(_config, _state) { const self = this as unknown as { spec: {props: {nodeViews: Record}} } if (options.editor.docInfo.access_rights === "write") { self.spec.props.nodeViews["figure"] = ( node: Node, view: EditorView, getPos: () => number ) => new FigureView(node, view, getPos, options) } self.spec.props.nodeViews["figure_caption"] = ( node: Node, view: EditorView, getPos: () => number ) => new FigureCaptionView(node, view, getPos, options) return {} }, apply(_tr, prev) { return prev } }, props: { nodeViews: {} } })