diff --git a/ccm-cms/src/main/resources/META-INF/resources/components/librecms/cmsEditor.xhtml b/ccm-cms/src/main/resources/META-INF/resources/components/librecms/cmsEditor.xhtml index 5c8ebb901..b0734028d 100644 --- a/ccm-cms/src/main/resources/META-INF/resources/components/librecms/cmsEditor.xhtml +++ b/ccm-cms/src/main/resources/META-INF/resources/components/librecms/cmsEditor.xhtml @@ -678,7 +678,7 @@ id="librecms-image-node-view-settings-dialog-" tabindex="-1"> diff --git a/ccm-cms/src/main/typescript/content-sections/cms-editor.ts b/ccm-cms/src/main/typescript/content-sections/cms-editor.ts index 02c893d24..030c7a52f 100644 --- a/ccm-cms/src/main/typescript/content-sections/cms-editor.ts +++ b/ccm-cms/src/main/typescript/content-sections/cms-editor.ts @@ -10,800 +10,804 @@ import TableRow from "@tiptap/extension-table-row"; import TableCell from "@tiptap/extension-table-cell"; import TableHeader from "@tiptap/extension-table-header"; +import ImageNode from "./cms-editor/image-node"; + const BUTTONS: CmsEditorButton[] = [ - { - selector: ".tiptap-emph", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleItalic().run(); + { + selector: ".tiptap-emph", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().toggleItalic().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleItalic() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().toggleItalic().run(); + { + selector: ".tiptap-strong-emph", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().toggleBold().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleBold() + .run(); + }, }, - }, - { - selector: ".tiptap-strong-emph", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleBold().run(); + { + selector: ".tiptap-code", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().toggleCode().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleCode() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().toggleBold().run(); + { + selector: ".tiptap-strikethrough", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().toggleStrike().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleStrike() + .run(); + }, }, - }, - { - selector: ".tiptap-code", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleCode().run(); + { + selector: ".tiptap-subscript", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleSubscript() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleSubscript() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().toggleCode().run(); + { + selector: ".tiptap-superscript", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleSuperscript() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleSuperscript() + .run(); + }, }, - }, - { - selector: ".tiptap-strikethrough", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleStrike().run(); + { + selector: ".tiptap-h1", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeading({ level: 1 }) + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeading({ level: 1 }) + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().toggleStrike().run(); + { + selector: ".tiptap-h2", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeading({ level: 2 }) + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeading({ level: 2 }) + .run(); + }, }, - }, - { - selector: ".tiptap-subscript", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleSubscript().run(); + { + selector: ".tiptap-h3", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeading({ level: 3 }) + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeading({ level: 3 }) + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleSubscript() - .run(); + { + selector: ".tiptap-h5", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeading({ level: 5 }) + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeading({ level: 5 }) + .run(); + }, }, - }, - { - selector: ".tiptap-superscript", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleSuperscript().run(); + { + selector: ".tiptap-h6", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeading({ level: 6 }) + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeading({ level: 6 }) + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleSuperscript() - .run(); + { + selector: ".tiptap-paragraph", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().clearNodes().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .clearNodes() + .run(); + }, }, - }, - { - selector: ".tiptap-h1", - command: (cmsEditor) => { - return cmsEditor - .getEditor() - .chain() - .focus() - .toggleHeading({ level: 1 }) - .run(); + { + selector: ".tiptap-blockquote", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleBlockquote() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleBlockquote() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeading({ level: 1 }) - .run(); + { + selector: ".tiptap-codeblock", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleCodeBlock() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleCodeBlock() + .run(); + }, }, - }, - { - selector: ".tiptap-h2", - command: (cmsEditor) => { - return cmsEditor - .getEditor() - .chain() - .focus() - .toggleHeading({ level: 2 }) - .run(); + { + selector: ".tiptap-ul", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleBulletList() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleBulletList() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeading({ level: 2 }) - .run(); + { + selector: ".tiptap-ol", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleOrderedList() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleOrderedList() + .run(); + }, }, - }, - { - selector: ".tiptap-h3", - command: (cmsEditor) => { - return cmsEditor - .getEditor() - .chain() - .focus() - .toggleHeading({ level: 3 }) - .run(); + { + selector: ".cms-editor-insert-table-dialog", + command: (cmsEditor) => { + return true; + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .insertTable() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeading({ level: 3 }) - .run(); + { + selector: ".cms-editor-insert-table-dialog .btn-success", + command: (cmsEditor) => { + const dialog = cmsEditor + .getEditorElem() + .querySelector(".cms-editor-insert-table-dialog"); + if (!dialog) { + return false; + } + const rowsInput = dialog.querySelector( + "input#rows" + ) as HTMLInputElement; + const colsInput = dialog.querySelector( + "input#cols" + ) as HTMLInputElement; + const headerRowInput = dialog.querySelector( + "input#headerRow" + ) as HTMLInputElement; + console.log(`rowsInput = ${rowsInput}`); + console.log(`colsInput = ${colsInput}`); + console.log(`headerRowInput = ${headerRowInput}`); + const rows = parseInt(rowsInput.value, 10); + const cols = parseInt(colsInput.value, 10); + const headerRow = JSON.parse(headerRowInput.value) as Boolean; + const insertTableDialog = $("#insert-table-dialog") as any; + insertTableDialog.modal("hide"); + return cmsEditor + .getEditor() + .chain() + .focus() + .insertTable({ + // allowTableNodeSelection: true, + // cellMinWidth: 150, + cols: cols, + // headerRow: headerRow, + // resizable: true, + rows: rows, + }) + .run(); + }, + can: (cmsEditor) => { + return true; + }, }, - }, - { - selector: ".tiptap-h5", - command: (cmsEditor) => { - return cmsEditor - .getEditor() - .chain() - .focus() - .toggleHeading({ level: 5 }) - .run(); + { + selector: ".tiptap-insert-table-row-before", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().addRowBefore().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .addRowBefore() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeading({ level: 5 }) - .run(); + { + selector: ".tiptap-insert-table-row-after", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().addRowAfter().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .addRowAfter() + .run(); + }, }, - }, - { - selector: ".tiptap-h6", - command: (cmsEditor) => { - return cmsEditor - .getEditor() - .chain() - .focus() - .toggleHeading({ level: 6 }) - .run(); + { + selector: ".tiptap-insert-table-column-before", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .addColumnBefore() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .addColumnBefore() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeading({ level: 6 }) - .run(); + { + selector: ".tiptap-insert-table-column-after", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().addColumnAfter().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .addColumnAfter() + .run(); + }, }, - }, - { - selector: ".tiptap-paragraph", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().clearNodes().run(); + { + selector: ".tiptap-remove-table-row", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().deleteRow().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .deleteRow() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().clearNodes().run(); + { + selector: ".tiptap-remove-table-column", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().deleteColumn().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .deleteColumn() + .run(); + }, }, - }, - { - selector: ".tiptap-blockquote", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleBlockquote().run(); + { + selector: ".tiptap-remove-table", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().deleteTable().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .deleteTable() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleBlockquote() - .run(); + { + selector: ".tiptap-toggle-table-header-row", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeaderRow() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeaderRow() + .run(); + }, }, - }, - { - selector: ".tiptap-codeblock", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleCodeBlock().run(); + { + selector: ".tiptap-toggle-table-header-column", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .toggleHeaderColumn() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .toggleHeaderColumn() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleCodeBlock() - .run(); + { + selector: ".tiptap-merge-table-cells", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().mergeCells().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .mergeCells() + .run(); + }, }, - }, - { - selector: ".tiptap-ul", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleBulletList().run(); + { + selector: ".tiptap-split-table-cell", + command: (cmsEditor) => { + return cmsEditor.getEditor().chain().focus().splitCell().run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .splitCell() + .run(); + }, }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleBulletList() - .run(); + { + selector: ".tiptap-insert-image", + command: (cmsEditor) => { + return cmsEditor + .getEditor() + .chain() + .focus() + .setLibreCmsImage() + .run(); + }, + can: (cmsEditor) => { + return cmsEditor + .getEditor() + .can() + .chain() + .focus() + .setLibreCmsImage() + .run(); + }, }, - }, - { - selector: ".tiptap-ol", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleOrderedList().run(); - }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleOrderedList() - .run(); - }, - }, - { - selector: ".cms-editor-insert-table-dialog", - command: (cmsEditor) => { - return true; - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().insertTable().run(); - }, - }, - { - selector: ".cms-editor-insert-table-dialog .btn-success", - command: (cmsEditor) => { - const dialog = cmsEditor - .getEditorElem() - .querySelector(".cms-editor-insert-table-dialog"); - if (!dialog) { - return false; - } - const rowsInput = dialog.querySelector("input#rows") as HTMLInputElement; - const colsInput = dialog.querySelector("input#cols") as HTMLInputElement; - const headerRowInput = dialog.querySelector( - "input#headerRow" - ) as HTMLInputElement; - console.log(`rowsInput = ${rowsInput}`); - console.log(`colsInput = ${colsInput}`); - console.log(`headerRowInput = ${headerRowInput}`); - const rows = parseInt(rowsInput.value, 10); - const cols = parseInt(colsInput.value, 10); - const headerRow = JSON.parse(headerRowInput.value) as Boolean; - const insertTableDialog = $("#insert-table-dialog") as any; - insertTableDialog.modal("hide"); - return cmsEditor - .getEditor() - .chain() - .focus() - .insertTable({ - // allowTableNodeSelection: true, - // cellMinWidth: 150, - cols: cols, - // headerRow: headerRow, - // resizable: true, - rows: rows, - }) - .run(); - }, - can: (cmsEditor) => { - return true; - }, - }, - { - selector: ".tiptap-insert-table-row-before", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().addRowBefore().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().addRowBefore().run(); - }, - }, - { - selector: ".tiptap-insert-table-row-after", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().addRowAfter().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().addRowAfter().run(); - }, - }, - { - selector: ".tiptap-insert-table-column-before", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().addColumnBefore().run(); - }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .addColumnBefore() - .run(); - }, - }, - { - selector: ".tiptap-insert-table-column-after", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().addColumnAfter().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().addColumnAfter().run(); - }, - }, - { - selector: ".tiptap-remove-table-row", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().deleteRow().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().deleteRow().run(); - }, - }, - { - selector: ".tiptap-remove-table-column", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().deleteColumn().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().deleteColumn().run(); - }, - }, - { - selector: ".tiptap-remove-table", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().deleteTable().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().deleteTable().run(); - }, - }, - { - selector: ".tiptap-toggle-table-header-row", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleHeaderRow().run(); - }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeaderRow() - .run(); - }, - }, - { - selector: ".tiptap-toggle-table-header-column", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().toggleHeaderColumn().run(); - }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .toggleHeaderColumn() - .run(); - }, - }, - { - selector: ".tiptap-merge-table-cells", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().mergeCells().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().mergeCells().run(); - }, - }, - { - selector: ".tiptap-split-table-cell", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().splitCell().run(); - }, - can: (cmsEditor) => { - return cmsEditor.getEditor().can().chain().focus().splitCell().run(); - }, - }, - { - selector: ".tiptap-insert-image", - command: (cmsEditor) => { - return cmsEditor.getEditor().chain().focus().setLibreCmsImage().run(); - }, - can: (cmsEditor) => { - return cmsEditor - .getEditor() - .can() - .chain() - .focus() - .setLibreCmsImage() - .run(); - }, - }, - // { - // selector: "", - // command: cmsEditor => {}, - // can: cmsEditor => {} - // }, - // { - // selector: "", - // command: cmsEditor => {}, - // can: cmsEditor => {} - // }, - // { - // selector: "", - // command: cmsEditor => {}, - // can: cmsEditor => {} - // } + // { + // selector: "", + // command: cmsEditor => {}, + // can: cmsEditor => {} + // }, + // { + // selector: "", + // command: cmsEditor => {}, + // can: cmsEditor => {} + // }, + // { + // selector: "", + // command: cmsEditor => {}, + // can: cmsEditor => {} + // } ]; -declare module "@tiptap/core" { - interface Commands { - libreCmsImageNode: { - setLibreCmsImage: (attributes?: { language: string }) => ReturnType; - }; - } -} - -const ImageNode = Node.create({ - name: "libreCmsImageNode", - - content: "inline*", - - marks: "", - - group: "block", - - code: false, - - defining: true, - - addAttributes() { - return { - altText: { - parseHTML: (element) => { - const imgElem = element.querySelector("img"); - if (imgElem) { - return imgElem.alt; - } else { - return ""; - } - }, - }, - figCaption: { - parseHTML: (element) => { - const figCaptionElem = element.querySelector("figcaption"); - if (figCaptionElem) { - return figCaptionElem.innerHTML; - } else { - return ""; - } - }, - }, - }; - }, - - addCommands() { - return { - setLibreCmsImage: - (attributes) => - ({ commands }) => { - return commands.setNode("libreCmsImageNode", attributes); - }, - }; - }, - - addNodeView() { - return ({ - editor, - node, - getPos, - HTMLAttributes, - decorations, - extension, - }) => { - const templateNode = document.querySelector("#librecms-image-node-view"); - if (!templateNode) { - const errorMsg = document.createElement("div"); - errorMsg.classList.add("alert"); - errorMsg.classList.add("alert-danger"); - errorMsg.textContent = "Failed to create image node view."; - - return errorMsg; - } - - console.log("Node: "); - console.dir(node); - - console.log(`getPos =`); - console.dir(getPos); - - console.log(`HTMLAttributes =`); - console.dir(HTMLAttributes); - - console.log(`decorations = ${decorations}`); - console.dir(decorations); - - console.log(`extension = ${extension}`); - console.dir(extension); - - const template = templateNode as HTMLTemplateElement; - const nodeView = template.content.cloneNode(true) as HTMLElement; - const dialogId = `librecms-image-node-view-settings-dialog-${Math.floor( - Math.random() * 1000000000 - )}`; - const dialogTitleId = `${dialogId}-title`; - const settingsButtonElem = nodeView.querySelector( - ".image-settings-button" - ); - if (settingsButtonElem) { - settingsButtonElem.setAttribute("data-target", `#${dialogId}`); - } - const settingsDialogElem = nodeView.querySelector( - ".modal.image-settings-dialog" - ); - if (settingsDialogElem) { - settingsDialogElem.id = dialogId; - settingsDialogElem.setAttribute("aria-labelledby", dialogTitleId); - - const settingsDialogTitleElem = - settingsDialogElem.querySelector(".modal-title"); - if (settingsDialogTitleElem) { - settingsDialogTitleElem.id = dialogTitleId; - } - } - - const figCaptionElem = nodeView.querySelector("figcaption"); - if (node.attrs.figCaption !== "" && figCaptionElem) { - figCaptionElem.innerHTML = node.attrs.figCaption; - } - const dom = document.createElement("div"); - dom.classList.add("librecms-image-node-view", "p-2"); - dom.appendChild(nodeView); - - return { - dom, - }; - }; - }, - - parseHTML() { - return [ - { - tag: "figure[data-type=librecms-image-node]", - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return [ - "figure", - mergeAttributes(HTMLAttributes, { "data-type": "librecms-image-node" }), - ["img", { src: "/assets/remixicon/image-line.svg" }], - ["figcaption"], - ]; - }, -}); - class CmsEditor { - private editor: Editor; - private editorElem: HTMLElement; - private saveUrl: string; + private editor: Editor; + private editorElem: HTMLElement; + private saveUrl: string; - public constructor(editor: Editor, editorElem: HTMLElement, saveUrl: string) { - this.editor = editor; - this.editorElem = editorElem; - this.saveUrl = saveUrl; + public constructor( + editor: Editor, + editorElem: HTMLElement, + saveUrl: string + ) { + this.editor = editor; + this.editorElem = editorElem; + this.saveUrl = saveUrl; - console.log("initializing editor buttons"); - const buttonsElem = editorElem.querySelector(".cms-tiptap-editor-buttons"); - if (buttonsElem) { - for (const button of BUTTONS) { - const buttonElem = buttonsElem.querySelector(button.selector); - if (buttonElem) { - buttonElem.addEventListener("click", (event) => { - event.preventDefault(); - button.command(this); - }); + console.log("initializing editor buttons"); + const buttonsElem = editorElem.querySelector( + ".cms-tiptap-editor-buttons" + ); + if (buttonsElem) { + for (const button of BUTTONS) { + const buttonElem = buttonsElem.querySelector(button.selector); + if (buttonElem) { + buttonElem.addEventListener("click", (event) => { + event.preventDefault(); + button.command(this); + }); + } else { + continue; + } + } } else { - continue; + console.error("editorButtonsElem not found."); + return; } - } - } else { - console.error("editorButtonsElem not found."); - return; + + editor.on("selectionUpdate", ({ editor }: { editor: Editor }) => { + console.log(`checkButton - this.editorElem = ${this.editorElem}`); + const buttonsElem = editorElem.querySelector( + ".cms-tiptap-editor-buttons" + ); + if (!buttonsElem) { + return; + } + for (const button of BUTTONS) { + const elem = buttonsElem.querySelector(button.selector); + if (elem) { + const buttonElem = elem as HTMLButtonElement; + if (button.can(this)) { + buttonElem.removeAttribute("disabled"); + } else { + buttonElem.setAttribute("disabled", "disabled"); + } + } else { + continue; + } + } + }); + + console.log(`editorElem = ${editorElem}`); + + const saveButton = editorElem.querySelector(".cms-editor-save-button"); + saveButton?.addEventListener("click", (event) => this.save(event)); } - editor.on("selectionUpdate", ({ editor }: { editor: Editor }) => { - console.log(`checkButton - this.editorElem = ${this.editorElem}`); - const buttonsElem = editorElem.querySelector( - ".cms-tiptap-editor-buttons" - ); - if (!buttonsElem) { - return; - } - for (const button of BUTTONS) { - const elem = buttonsElem.querySelector(button.selector); - if (elem) { - const buttonElem = elem as HTMLButtonElement; - if (button.can(this)) { - buttonElem.removeAttribute("disabled"); - } else { - buttonElem.setAttribute("disabled", "disabled"); - } - } else { - continue; + protected async save(event: Event) { + event.preventDefault(); + + const params = new URLSearchParams(); + params.append("value", this.editor.getHTML()); + + try { + const response = await fetch(this.saveUrl, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + if (response.ok) { + } else { + this.showSaveFailedMessage( + response.status, + response.statusText + ); + } + } catch (error) { + this.showSaveFailedErrMessage(error as string); } - } - }); - - console.log(`editorElem = ${editorElem}`); - - const saveButton = editorElem.querySelector(".cms-editor-save-button"); - saveButton?.addEventListener("click", (event) => this.save(event)); - } - - protected async save(event: Event) { - event.preventDefault(); - - const params = new URLSearchParams(); - params.append("value", this.editor.getHTML()); - - try { - const response = await fetch(this.saveUrl, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: params, - }); - if (response.ok) { - } else { - this.showSaveFailedMessage(response.status, response.statusText); - } - } catch (error) { - this.showSaveFailedErrMessage(error as string); } - } - protected showSaveFailedMessage(status: number, statusText: string) { - this.showMessage("#cms-editor-msg-save-failed"); - console.error( - `Failed to save text. Status: ${statusText}. Status Text: ${statusText}` - ); - } + protected showSaveFailedMessage(status: number, statusText: string) { + this.showMessage("#cms-editor-msg-save-failed"); + console.error( + `Failed to save text. Status: ${statusText}. Status Text: ${statusText}` + ); + } - protected showSaveFailedErrMessage(error: string) { - this.showMessage("#cms-editor-msg-save-failed"); - console.error(error); - } + protected showSaveFailedErrMessage(error: string) { + this.showMessage("#cms-editor-msg-save-failed"); + console.error(error); + } - protected showMessage(messageId: string) { - const template = this.editorElem.querySelector( - messageId - ) as HTMLTemplateElement; - const message = template.content.cloneNode(true); - this.editorElem.querySelector(".cms-editor-messages")?.append(message); - } + protected showMessage(messageId: string) { + const template = this.editorElem.querySelector( + messageId + ) as HTMLTemplateElement; + const message = template.content.cloneNode(true); + this.editorElem.querySelector(".cms-editor-messages")?.append(message); + } - public getEditor(): Editor { - return this.editor; - } + public getEditor(): Editor { + return this.editor; + } - public getEditorElem(): HTMLElement { - return this.editorElem; - } + public getEditorElem(): HTMLElement { + return this.editorElem; + } } class CmsEditorBuilder { - private editorElem: HTMLElement; - private saveUrl: string; - private variantUrl: string; + private editorElem: HTMLElement; + private saveUrl: string; + private variantUrl: string; - constructor(editorElem: HTMLElement, saveUrl: string, variantUrl: string) { - this.editorElem = editorElem; - this.saveUrl = saveUrl; - this.variantUrl = variantUrl; - } - - public async buildEditor(): Promise { - console.log("Build CMS Editor."); - const canvasElement = this.editorElem.querySelector( - ".cms-tiptap-editor-canvas" - ); - if (!canvasElement) { - this.showMessage("#cms-editor-msg-canvas-element-not-found"); - console.error("canvasElem not found."); - throw "canvasElem not found."; + constructor(editorElem: HTMLElement, saveUrl: string, variantUrl: string) { + this.editorElem = editorElem; + this.saveUrl = saveUrl; + this.variantUrl = variantUrl; } - const variant = await this.fetchVariant(this.variantUrl); + public async buildEditor(): Promise { + console.log("Build CMS Editor."); + const canvasElement = this.editorElem.querySelector( + ".cms-tiptap-editor-canvas" + ); + if (!canvasElement) { + this.showMessage("#cms-editor-msg-canvas-element-not-found"); + console.error("canvasElem not found."); + throw "canvasElem not found."; + } - const editor: Editor = new Editor({ - element: canvasElement, - extensions: [ - Gapcursor, - ImageNode, - StarterKit, - Subscript, - Superscript, - Table.configure({ - allowTableNodeSelection: true, - cellMinWidth: 100, - handleWidth: 25, - resizable: true, - }), - TableRow, - TableHeader, - TableCell, - ], - content: variant, - }); + const variant = await this.fetchVariant(this.variantUrl); - return new CmsEditor(editor, this.editorElem, this.saveUrl); - } + const editor: Editor = new Editor({ + element: canvasElement, + extensions: [ + Gapcursor, + ImageNode, + StarterKit, + Subscript, + Superscript, + Table.configure({ + allowTableNodeSelection: true, + cellMinWidth: 100, + handleWidth: 25, + resizable: true, + }), + TableRow, + TableHeader, + TableCell, + ], + content: variant, + }); - protected async fetchVariant(variantUrl: string): Promise { - try { - const response = await fetch(variantUrl, { - method: "GET", - credentials: "include", - }); - - if (response.ok) { - return await response.text(); - } else { - this.showLoadVariantFailedMessage(response.status, response.statusText); - throw `Failed to load variant. Status: ${response.status}, Status Text: ${response.statusText}`; - } - } catch (error) { - this.showLoadVariantFailedErrorMessage(error as string); - throw error; + return new CmsEditor(editor, this.editorElem, this.saveUrl); } - } - protected showLoadVariantFailedMessage(status: number, statusText: string) { - this.showMessage("#cms-editor-msg-variant-load-failed"); - console.error( - `Failed to load variant: HTTP Status: ${status}, statusText: ${statusText}` - ); - } + protected async fetchVariant(variantUrl: string): Promise { + try { + const response = await fetch(variantUrl, { + method: "GET", + credentials: "include", + }); - protected showLoadVariantFailedErrorMessage(error: string) { - this.showMessage("#cms-editor-msg-variant-load-failed"); - console.error(`Failed to load variant: ${error}`); - } + if (response.ok) { + return await response.text(); + } else { + this.showLoadVariantFailedMessage( + response.status, + response.statusText + ); + throw `Failed to load variant. Status: ${response.status}, Status Text: ${response.statusText}`; + } + } catch (error) { + this.showLoadVariantFailedErrorMessage(error as string); + throw error; + } + } - protected showMessage(messageId: string) { - const template = this.editorElem.querySelector( - messageId - ) as HTMLTemplateElement; - const message = template.content.cloneNode(true); - this.editorElem.querySelector(".cms-editor-messages")?.append(message); - } + protected showLoadVariantFailedMessage(status: number, statusText: string) { + this.showMessage("#cms-editor-msg-variant-load-failed"); + console.error( + `Failed to load variant: HTTP Status: ${status}, statusText: ${statusText}` + ); + } + + protected showLoadVariantFailedErrorMessage(error: string) { + this.showMessage("#cms-editor-msg-variant-load-failed"); + console.error(`Failed to load variant: ${error}`); + } + + protected showMessage(messageId: string) { + const template = this.editorElem.querySelector( + messageId + ) as HTMLTemplateElement; + const message = template.content.cloneNode(true); + this.editorElem.querySelector(".cms-editor-messages")?.append(message); + } } interface CmsEditorParameters { - editorElem: HTMLElement; - variantUrl: string; + editorElem: HTMLElement; + variantUrl: string; } interface CmsEditorButton { - selector: string; - command: (cmsEditor: CmsEditor) => boolean; - can: (cmsEditor: CmsEditor) => boolean; + selector: string; + command: (cmsEditor: CmsEditor) => boolean; + can: (cmsEditor: CmsEditor) => boolean; } interface EditorParam { - editor: Editor; + editor: Editor; } export { CmsEditor, CmsEditorBuilder, CmsEditorParameters }; diff --git a/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/image-node.ts b/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/image-node.ts new file mode 100644 index 000000000..fd5c2c9c9 --- /dev/null +++ b/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/image-node.ts @@ -0,0 +1,235 @@ +import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core"; + +declare module "@tiptap/core" { + interface Commands { + libreCmsImageNode: { + setLibreCmsImage: (attributes?: { language: string }) => ReturnType; + }; + } +} + +export const ImageNode = Node.create({ + name: "libreCmsImageNode", + + content: "inline*", + + marks: "", + + group: "block", + + code: false, + + defining: true, + + addAttributes() { + return { + align: { + parseHTML: (element) => { + if (element.hasAttribute("data-align")) { + return element.getAttribute("data-align"); + } else { + return "center"; + } + }, + }, + altText: { + parseHTML: (element) => { + const imgElem = element.querySelector("img"); + if (imgElem) { + return imgElem.alt; + } else { + return ""; + } + }, + }, + figCaption: { + parseHTML: (element) => { + const figCaptionElem = element.querySelector("figcaption"); + if (figCaptionElem) { + return figCaptionElem.innerHTML; + } else { + return ""; + } + }, + }, + fullSizeOverlay: { + parseHTML: (element) => { + if (element.hasAttribute("data-fullsizeoverlay")) { + return ( + element.getAttribute("data-fullsizeoverlay") === + "true" + ); + } else { + return false; + } + }, + }, + size: { + parseHTML: (element) => { + if (element.hasAttribute("data-size")) { + return element.getAttribute("data-size"); + } else { + return "50"; + } + }, + }, + }; + }, + + addCommands() { + return { + setLibreCmsImage: + (attributes) => + ({ commands }) => { + return commands.setNode("libreCmsImageNode", attributes); + }, + }; + }, + + addNodeView() { + return ({ + editor, + node, + getPos, + HTMLAttributes, + decorations, + extension, + }) => { + const templateNode = document.querySelector( + "#librecms-image-node-view" + ); + if (!templateNode) { + const errorMsg = document.createElement("div"); + errorMsg.classList.add("alert"); + errorMsg.classList.add("alert-danger"); + errorMsg.textContent = "Failed to create image node view."; + + return errorMsg; + } + + // console.log("Node: "); + // console.dir(node); + + // console.log(`getPos =`); + // console.dir(getPos); + + // console.log(`HTMLAttributes =`); + // console.dir(HTMLAttributes); + + // console.log(`decorations = ${decorations}`); + // console.dir(decorations); + + // console.log(`extension = ${extension}`); + // console.dir(extension); + + const template = templateNode as HTMLTemplateElement; + const nodeView = template.content.cloneNode(true) as HTMLElement; + const dialogIdNr = Math.floor(Math.random() * 1000000000); + const dialogId = `librecms-image-node-view-settings-dialog-${dialogIdNr}`; + const dialogTitleId = `${dialogId}-title`; + const settingsButtonElem = nodeView.querySelector( + ".image-settings-button" + ); + if (settingsButtonElem) { + settingsButtonElem.setAttribute("data-target", `#${dialogId}`); + } + const settingsDialogElem = nodeView.querySelector( + ".modal.image-settings-dialog" + ); + if (settingsDialogElem) { + settingsDialogElem.id = dialogId; + settingsDialogElem.setAttribute( + "aria-labelledby", + dialogTitleId + ); + + const settingsDialogTitleElem = + settingsDialogElem.querySelector(".modal-title"); + if (settingsDialogTitleElem) { + settingsDialogTitleElem.id = dialogTitleId; + } + + const settingDialogIds = + settingsDialogElem.querySelectorAll("*[id]"); + for (let i = 0; i < settingDialogIds.length; i++) { + const elemWithId = settingDialogIds.item(i); + elemWithId.id = `${elemWithId.id}-${dialogIdNr}`; + } + const settingDialogLabels = + settingsDialogElem.querySelectorAll("*[for]"); + for (let i = 0; i < settingDialogLabels.length; i++) { + const label = settingDialogLabels.item(i); + label.id = `${label.id}-${dialogIdNr}`; + } + const describedElems = settingsDialogElem.querySelectorAll( + "*[aria-describedby]" + ); + for (let i = 0; i < describedElems.length; i++) { + const describedElem = describedElems.item(i); + describedElem.id = `${describedElem.id}-${dialogIdNr}`; + } + + const submitButton = settingsDialogElem.querySelector( + "button.image-settings-dialog-save" + ); + + const altTextInput = settingsDialogElem.querySelector( + `input#alttext-${dialogIdNr}` + ); + const alignSelect = settingsDialogElem.querySelector( + `input#align-${dialogIdNr}` + ); + + // ToDo: Init inputs + + if (submitButton) { + submitButton.addEventListener("click", (event) => { + // ToDo: Read values from inputs and update node.attrs + }); + } + } + + const figCaptionElem = nodeView.querySelector("figcaption"); + if (node.attrs.figCaption !== "" && figCaptionElem) { + figCaptionElem.innerHTML = node.attrs.figCaption; + } + const dom = document.createElement("div"); + dom.classList.add("librecms-image-node-view", "p-2"); + dom.appendChild(nodeView); + + return { + dom, + }; + }; + }, + + parseHTML() { + return [ + { + tag: "figure[data-type=librecms-image-node]", + }, + ]; + }, + + renderHTML({ node, HTMLAttributes }) { + console.log("node = "); + console.dir(node); + return [ + "figure", + mergeAttributes(HTMLAttributes, { + "data-align": node.attrs.align, + "data-fullsizeoverlay": node.attrs.fullSizeOverlay, + "data-size": node.attrs.size, + "data-type": "librecms-image-node", + }), + [ + "img", + { + alt: node.attrs.altText, + src: "/assets/remixicon/image-line.svg", + }, + ], + ["figcaption"], + ]; + }, +}); diff --git a/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/index.ts b/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/index.ts new file mode 100644 index 000000000..213850915 --- /dev/null +++ b/ccm-cms/src/main/typescript/content-sections/cms-editor/image-node/index.ts @@ -0,0 +1,5 @@ +import { ImageNode } from "./image-node"; + +export * from "./image-node"; + +export default ImageNode; \ No newline at end of file