Moved ccm-cms-editor to separate module to make it usable in other projects (for example ScientificCMS)

deploy_packages_to_gitea
Jens Pelzetter 2022-04-27 20:57:47 +02:00
parent f73f9084cf
commit 0cceec93e8
32 changed files with 8361 additions and 1994 deletions

1
ccm-cms-editor/.gitignore vendored 100644
View File

@ -0,0 +1 @@
target

5348
ccm-cms-editor/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "@librecms/ccm-cms-editor",
"version": "7.0.0-SNAPSHOT.2022-04-27T185311",
"description": "HTML WYSIWYG editor for LibreCMS based on TipTap",
"main": "target/generated-resources/assets/ccm-cms-editor.js",
"types": "target/generated-resources/assets/ccm-cms-editor.d.ts",
"scripts": {
"build": "npm-run-all build:*",
"build:js": "webpack"
},
"author": "Jens Pelzetter",
"license": "LGPL-3.0-or-later",
"devDependencies": {
"@types/jquery": "^3.5.6",
"npm-run-all": "^4.1.5",
"ts-loader": "^9.2.6",
"typescript": "^4.4.3",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0"
},
"dependencies": {
"@tiptap/core": "^2.0.0-beta.127",
"@tiptap/extension-subscript": "^2.0.0-beta.4",
"@tiptap/extension-superscript": "^2.0.0-beta.4",
"@tiptap/extension-table": "^2.0.0-beta.35",
"@tiptap/extension-table-cell": "^2.0.0-beta.15",
"@tiptap/extension-table-header": "^2.0.0-beta.17",
"@tiptap/extension-table-row": "^2.0.0-beta.14",
"@tiptap/starter-kit": "^2.0.0-beta.129",
"acorn": "^8.5.0",
"bootstrap": "^4.6.0",
"bootstrap-icons": "^1.5.0",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
"remixicon": "^2.5.0",
"sortablejs": "^1.14.0"
}
}

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>libreccm-parent</artifactId>
<groupId>org.libreccm</groupId>
<version>7.0.0-SNAPSHOT</version>
</parent>
<groupId>org.librecms</groupId>
<artifactId>ccm-cms-editor</artifactId>
<name>LibreCMS Editor</name>
<licenses>
<license>
<name>Lesser GPL 2.1</name>
<url>http://www.gnu.org/licenses/old-licenses/lgpl-2.1</url>
</license>
</licenses>
<build>
<finalName>ccm-cms-editor</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>./target/generated-resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<configuration>
<installDirectory>../node</installDirectory>
</configuration>
<executions>
<execution>
<id>Install node.js and NPM</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>${nodeJsVersion}</nodeVersion>
</configuration>
</execution>
<execution>
<id>set package version</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>pkg set version=${npmPackageVersion}</arguments>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
<execution>
<id>npm link</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>link</arguments>
</configuration>
</execution>
<execution>
<id>npm publish</id>
<goals>
<goal>npm</goal>
</goals>
<phase>deploy</phase>
<configuration>
<arguments>publish --userconfig ../libreccm.npmrc</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"lib": [
"DOM",
"ES2016"
],
"module": "commonjs",
"moduleResolution": "node",
"outDir": "target/generated-resources/assets",
"sourceMap": true,
"strict": true,
"target": "ES5"
},
"include": [
"src/main/typescript/**/*"
]
}

View File

@ -0,0 +1,23 @@
module.exports = {
mode: "development",
devtool: "source-map",
optimization: {
chunkIds: false
},
entry: {
"ccm-cms-editor": "./src/main/typescript/ccm-cms-editor.ts"
},
output: {
filename: "[name].js",
path: __dirname + "/target/generated-resources/assets"
},
resolve: {
extensions: [".tsx", ".ts", ".js", ".json"]
},
module: {
rules: [
// all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'
{ test: /\.tsx?$/, use: ["ts-loader"], exclude: /node_modules/ }
]
}
}

2301
ccm-cms/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@librecms/ccm-cms",
"version": "7.0.0-SNAPSHOT.2022-04-26T185027",
"version": "7.0.0-SNAPSHOT.2022-04-27T185311",
"description": "JavaScript stuff for ccm-cms",
"main": "target/generated-resources/assets/@content-sections/cms-admin.js",
"types": "target/generated-resources/assets/@content-sections/cms-admin.d.ts",
@ -23,14 +23,7 @@
"webpack-cli": "^4.8.0"
},
"dependencies": {
"@tiptap/core": "^2.0.0-beta.127",
"@tiptap/extension-subscript": "^2.0.0-beta.4",
"@tiptap/extension-superscript": "^2.0.0-beta.4",
"@tiptap/extension-table": "^2.0.0-beta.35",
"@tiptap/extension-table-cell": "^2.0.0-beta.15",
"@tiptap/extension-table-header": "^2.0.0-beta.17",
"@tiptap/extension-table-row": "^2.0.0-beta.14",
"@tiptap/starter-kit": "^2.0.0-beta.129",
"@librecms/ccm-cms-editor": "../ccm-cms-editor",
"acorn": "^8.5.0",
"bootstrap": "^4.6.0",
"bootstrap-icons": "^1.5.0",

View File

@ -42,6 +42,12 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.librecms</groupId>
<artifactId>ccm-cms-editor</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
@ -237,6 +243,15 @@
<arguments>pkg set version=${project.version}.${timestamp}</arguments>
</configuration>
</execution>
<!-- <execution>
<id>npm link @librecms/ccm-cms-editor</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>link @librecms/ccm-cms-editor</arguments>
</configuration>
</execution> -->
<execution>
<id>npm install</id>
<goals>

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-article-text-editor");

View File

@ -6,8 +6,6 @@ import "./cms-attachment-lists";
import "./cms-contentitempicker";
import "./cms-editor";
import "./cms-media-lists";
import "./cms-related-link";

View File

@ -0,0 +1,880 @@
import "bootstrap";
import * as $ from "jquery";
import { ChainedCommands, Editor, Node, mergeAttributes } from "@tiptap/core";
// import Gapcursor from "@tiptap/extension-gapcursor";
import StarterKit from "@tiptap/starter-kit";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import AudioNode from "./cms-editor/audio-node";
import ImageNode from "./cms-editor/image-node";
import VideoNode from "./cms-editor/video-node";
const BUTTONS: CmsEditorButton[] = [
{
selector: ".tiptap-emph",
command: (cmsEditor) => {
return cmsEditor.getEditor().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-code",
command: (cmsEditor) => {
return cmsEditor.getEditor().chain().focus().toggleCode().run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleCode()
.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-subscript",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleSubscript()
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleSubscript()
.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-h1",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleHeading({ level: 1 })
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleHeading({ level: 1 })
.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-h3",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleHeading({ level: 3 })
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleHeading({ level: 3 })
.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-h6",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleHeading({ level: 6 })
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleHeading({ level: 6 })
.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-blockquote",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleBlockquote()
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleBlockquote()
.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-ul",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.toggleBulletList()
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.toggleBulletList()
.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-audio",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.setLibreCmsAudio()
.insertContent("<p></p>")
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.setLibreCmsAudio()
.run();
},
},
{
selector: ".tiptap-insert-image",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.setLibreCmsImage()
.insertContent("<p></p>")
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.setLibreCmsImage()
.run();
},
},
{
selector: ".tiptap-insert-video",
command: (cmsEditor) => {
return cmsEditor
.getEditor()
.chain()
.focus()
.setLibreCmsVideo()
.insertContent("<p></p>")
.run();
},
can: (cmsEditor) => {
return cmsEditor
.getEditor()
.can()
.chain()
.focus()
.setLibreCmsVideo()
.run();
},
},
// {
// selector: "",
// command: cmsEditor => {},
// can: cmsEditor => {}
// },
// {
// selector: "",
// command: cmsEditor => {},
// can: cmsEditor => {}
// },
// {
// selector: "",
// command: cmsEditor => {},
// can: cmsEditor => {}
// }
];
class CmsEditor {
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;
// 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 {
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));
}
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) {
console.log(`Save response status: ${response.status}`);
window.location.href = response.url;
// const location = response.headers.get("Location");
// if (location) {
// window.location.href = location;
// } else {
// this.showMessage("#cms-editor-msg-save-successful");
// }
} else {
this.showSaveFailedMessage(
response.status,
response.statusText
);
}
} catch (error) {
console.error(error);
console.trace(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 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);
}
public getEditor(): Editor {
return this.editor;
}
public getEditorElem(): HTMLElement {
return this.editorElem;
}
}
class CmsEditorBuilder {
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<CmsEditor> {
// 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 variant = await this.fetchVariant(this.variantUrl);
console.log(`Got variant: ${variant}`);
try {
const editor: Editor = new Editor({
element: canvasElement,
extensions: [
AudioNode,
// Gapcursor,
ImageNode,
StarterKit,
Subscript,
Superscript,
Table.configure({
allowTableNodeSelection: true,
cellMinWidth: 100,
handleWidth: 25,
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
VideoNode,
],
content: variant,
});
return new CmsEditor(editor, this.editorElem, this.saveUrl);
} catch (error) {
console.error("Failed to create editor:");
console.trace(error);
throw error;
}
}
protected async fetchVariant(variantUrl: string): Promise<string> {
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) {
console.error(error);
console.trace(error);
this.showLoadVariantFailedErrorMessage(error as string);
throw error;
}
}
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;
}
interface CmsEditorButton {
selector: string;
command: (cmsEditor: CmsEditor) => boolean;
can: (cmsEditor: CmsEditor) => boolean;
}
interface EditorParam {
editor: Editor;
}
export { CmsEditor, CmsEditorBuilder, CmsEditorParameters };

View File

@ -0,0 +1,472 @@
import { Node, nodeInputRule, mergeAttributes, Range } from "@tiptap/core";
import { merge } from "jquery";
import { Node as ProsemirrorNode } from "prosemirror-model";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
libreCmsAudioNode: {
setLibreCmsAudio: (attributes?: { language: string }) => ReturnType;
};
}
}
export const AudioNode = Node.create({
name: "libreCmsAudioNode",
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 audioElem = element.querySelector("audio");
if (audioElem) {
return audioElem.textContent;
} else {
return "";
}
},
},
figCaption: {
parseHTML: (element) => {
const figCaptionElem = element.querySelector("figcaption");
if (figCaptionElem) {
return figCaptionElem.innerHTML;
} else {
return "";
}
},
},
audioSrc: {
parseHTML: (element) => {
const audioElem = element.querySelector("audio");
if (audioElem) {
return audioElem.src;
} else {
return "";
}
},
},
};
},
addCommands() {
return {
setLibreCmsAudio:
(attributes) =>
({ commands }) => {
return commands.setNode("libreCmsAudioNode", attributes);
},
};
},
addNodeView() {
return ({
editor,
node,
getPos,
HTMLAttributes,
decorations,
extension,
}) => {
const templateNode = document.querySelector(
"#librecms-audio-node-view"
);
if (!templateNode) {
const errorMsg = document.createElement("div");
errorMsg.classList.add("alert", "alert-danger");
errorMsg.textContent = "Failed to create audio node view.";
return errorMsg;
}
const dom = document.createElement("div");
dom.classList.add("librecms-audio-node-view", "p-2");
if (!node.attrs.align) {
node.attrs.align = "center";
}
dom.classList.add(`librecms-audio-node-align-${node.attrs.align}`);
const template = templateNode as HTMLTemplateElement;
const nodeView = template.content.cloneNode(true) as HTMLElement;
const audioElem = nodeView.querySelector("audio");
const dialogIdNr = Math.floor(Math.random() * 1000000000);
const selectDialogId = `librecms-image-audio-select-audio-dialog-${dialogIdNr}`;
const selectDialogTitleId = `librecms-audio-node-select-audio-dialog-${dialogIdNr}-title`;
const selectButtonElem = nodeView.querySelector(
".select-audio-button"
);
if (selectButtonElem) {
selectButtonElem.setAttribute(
"data-target",
`#${selectDialogId}`
);
}
const selectDialogElem = nodeView.querySelector(
".modal.select-audio-dialog"
);
if (selectDialogElem) {
selectDialogElem.id = selectDialogId;
selectDialogElem.setAttribute(
"aria-labelledby",
selectDialogTitleId
);
const selectDialogTitleElem =
selectDialogElem.querySelector(".modal-title");
if (selectDialogTitleElem) {
selectDialogTitleElem.id = selectDialogTitleId;
}
const selectDialogIds =
selectDialogElem.querySelectorAll("*[id]");
selectDialogIds.forEach((elemWithId) => {
elemWithId.id = `${elemWithId.id}-${dialogIdNr}`;
});
const selectDialogLabels =
selectDialogElem.querySelectorAll("*[for]");
selectDialogLabels.forEach((label) => {
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
});
const describedElems = selectDialogElem.querySelectorAll(
"*[aria-describedby]"
);
describedElems.forEach((describedElem) => {
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}`
);
});
}
const settingsDialogId = `librecms-audio-node-view-settings-dialog-${dialogIdNr}`;
const settingsDialogTitleId = `${settingsDialogId}-title`;
const settingsButtonElem = nodeView.querySelector(
".audio-settings-button"
);
if (settingsButtonElem) {
settingsButtonElem.setAttribute(
"data-target",
`#${settingsDialogId}`
);
}
const settingsDialogElem = nodeView.querySelector(
".modal.audio-settings-dialog"
);
if (settingsDialogElem) {
settingsDialogElem.id = settingsDialogId;
settingsDialogElem.setAttribute(
"aria-labelledby",
settingsDialogTitleId
);
const settingsDialogTitleElem =
settingsDialogElem.querySelector(".modal-title");
if (settingsDialogTitleElem) {
settingsDialogTitleElem.id = settingsDialogTitleId;
}
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
) as HTMLLabelElement;
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
}
const describedElems = settingsDialogElem.querySelectorAll(
"*[aria-describedby]"
);
for (let i = 0; i < describedElems.length; i++) {
const describedElem = describedElems.item(i);
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}`
);
}
const submitButton = settingsDialogElem.querySelector(
"button.audio-settings-dialog-save"
);
const altTextInput = settingsDialogElem.querySelector(
`input#alttext-${dialogIdNr}`
);
const captionInput = settingsDialogElem.querySelector(
`input#caption-${dialogIdNr}`
);
const alignSelect = settingsDialogElem.querySelector(
`select#align-${dialogIdNr}`
);
if (altTextInput) {
(altTextInput as HTMLInputElement).value =
node.attrs.altText;
} else {
console.warn("Input for alt text not found.");
}
if (captionInput) {
(captionInput as HTMLInputElement).value = node.attrs.figCaption;
} else {
console.warn("Input for caption not found.");
}
if (alignSelect) {
const optionElems = alignSelect.querySelectorAll("option");
for (let i = 0; i < optionElems.length; i++) {
const optionElem = optionElems.item(
i
) as HTMLOptionElement;
if (optionElem.value === node.attrs.align) {
optionElem.selected = true;
}
}
} else {
console.warn("Select for image alignment not found.");
}
if (submitButton) {
submitButton.addEventListener("click", (event) => {
const altInputElem = altTextInput as HTMLInputElement;
node.attrs.altText = altInputElem.value;
if (audioElem) {
audioElem.textContent = altInputElem.value;
}
const captionInputElem =
captionInput as HTMLInputElement;
node.attrs.figCaption = captionInputElem.value;
if (figCaptionElem) {
figCaptionElem.textContent = captionInputElem.value;
}
if (alignSelect) {
const selectElem = alignSelect as HTMLSelectElement;
node.attrs.align =
selectElem.selectedOptions.item(0)?.value;
["floatleft", "center", "floatright"].forEach(
(align) =>
dom.classList.remove(
`librecms-audio-node-align-${align}`
)
);
dom.classList.add(
`librecms-audio-node-align-${node.attrs.align}`
);
} else {
console.warn(
"Select for audio alignment not found."
);
}
});
} else {
console.warn(
"Submit button for image settings dialog not found."
);
}
}
const figCaptionElem = nodeView.querySelector("figcaption");
if (node.attrs.figCaption !== "" && figCaptionElem) {
figCaptionElem.innerHTML = node.attrs.figCaption;
}
dom.appendChild(nodeView);
if (selectButtonElem) {
if (audioElem) {
selectButtonElem.addEventListener("click", (event) =>
loadAudioAssets(event, node, audioElem)
);
} else {
console.error("audio elem not found.");
}
}
return {
dom,
};
};
},
parseHTML() {
return [
{
tag: "figure[data-type=librecms-audio-node]",
},
];
},
renderHTML({ node, HTMLAttributes }) {
return [
"figure",
mergeAttributes({
"data-align": node.attrs.align,
"data-type": "librecms-image-node",
}),
[
"audio",
{
src: node.attrs.audioSrc,
textContent: node.attrs.altText,
},
],
["figcaption", node.attrs.figCaption],
];
},
});
function loadAudioAssets(
event: Event,
node: ProsemirrorNode<any>,
audioElem: HTMLAudioElement
) {
const eventTarget = event.currentTarget as HTMLElement;
const editorElem = document.querySelector(".cms-editor");
if (!editorElem) {
return;
}
const baseUrl = editorElem.getAttribute("data-baseUrl");
const contentSection = editorElem.getAttribute("data-contentsection");
const dialogId = eventTarget.getAttribute("data-target");
if (!dialogId) {
console.error("data-target attribute is missing.");
return;
}
const dialog = document.querySelector(dialogId);
if (!dialog) {
console.error("dialog element not found is missing.");
return;
}
const rowTemplateResult = document.querySelector(
"#librecms-audio-node-view-row"
);
if (!rowTemplateResult) {
console.error("template for result row not found.");
return;
}
const rowTemplate = rowTemplateResult as HTMLTemplateElement;
const table = dialog.querySelector("table");
if (!table) {
console.error("result table not found.");
return;
}
const tableBody = table.querySelector("tbody");
if (!tableBody) {
console.error("table body not found.");
return;
}
tableBody.innerHTML = "";
const fetchUrl = `/content-sections/${contentSection}/assets?type=org.librecms.assets.AudioAsset,org.librecms.assets.ExternalAudioAsset`;
fetch(fetchUrl)
.then((response) => {
if (response.ok) {
response
.json()
.then((data) => {
const audioAssets = data as [];
for (const audioAsset of audioAssets) {
const row = rowTemplate.content.cloneNode(
true
) as Element;
const colName = row.querySelector(".col-name");
const colType = row.querySelector(".col-type");
const selectButton =
row.querySelector(".col-action button");
if (colName) {
colName.textContent = audioAsset["name"];
}
if (colType) {
colType.textContent = audioAsset["type"];
}
if (selectButton) {
selectButton.setAttribute(
"data-audiouuid",
audioAsset["uuid"]
);
selectButton.addEventListener(
"click",
(event) => {
const audioUrl = buildAudioMediaUrl(audioAsset);
node.attrs.audioSrc = audioUrl;
if (audioElem) {
audioElem.src = audioUrl;
} else {
console.error(
"audio element not found."
);
}
}
);
}
tableBody.appendChild(row);
}
})
.catch((error) => {
console.error(error);
});
} else {
console.error(
`Error. Status: ${response.status}. Status Text: ${response.statusText}`
);
}
})
.catch((error) => {
console.error(error);
});
}
function buildAudioMediaUrl(audioAsset: any) {
switch (audioAsset["type"]) {
case "org.librecms.assets.ExternalAudioAsset":
return audioAsset["properties"]["targetUrl"];
case "org.librecms.assets.AudioAsset":
return `/content-sections/info/audiomedia/uuid-${audioAsset["uuid"]}`;
default:
console.error(`Unknown audio asset type.`);
return "";
}
}

View File

@ -0,0 +1,5 @@
import { AudioNode } from "./audio-node";
export * from "./audio-node";
export default AudioNode;

View File

@ -0,0 +1,570 @@
import { Node, nodeInputRule, mergeAttributes, Range } from "@tiptap/core";
import { Node as ProsemirrorNode } from "prosemirror-model";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
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;
}
},
},
imgSrc: {
parseHTML: (element) => {
const imgElem = element.querySelector("img");
if (imgElem) {
return imgElem.src;
} else {
return "";
}
},
},
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", "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 dom = document.createElement("div");
dom.classList.add("librecms-image-node-view", "p-2");
if (!node.attrs.size) {
node.attrs.size = "50";
}
dom.classList.add(`librecms-image-node-width-${node.attrs.size}`);
if (!node.attrs.align) {
node.attrs.align = "center";
}
dom.classList.add(`librecms-image-node-align-${node.attrs.align}`);
const template = templateNode as HTMLTemplateElement;
const nodeView = template.content.cloneNode(true) as HTMLElement;
const imgElem = nodeView.querySelector("img");
if (imgElem) {
imgElem.src = node.attrs.imgSrc;
imgElem.alt = node.attrs.altText;
} else {
console.error("No img element.");
}
const dialogIdNr = Math.floor(Math.random() * 1000000000);
const selectDialogId = `librecms-image-node-select-image-dialog-${dialogIdNr}`;
const selectDialogTitleId = `librecms-image-node-select-image-dialog-${dialogIdNr}-title`;
const selectButtonElem = nodeView.querySelector(
".select-image-button"
);
if (selectButtonElem) {
selectButtonElem.setAttribute(
"data-target",
`#${selectDialogId}`
);
}
const selectDialogElem = nodeView.querySelector(
".modal.select-image-dialog"
);
if (selectDialogElem) {
selectDialogElem.id = selectDialogId;
selectDialogElem.setAttribute(
"aria-labelledby",
selectDialogTitleId
);
const selectDialogTitleElem =
selectDialogElem.querySelector(".modal-title");
if (selectDialogTitleElem) {
selectDialogTitleElem.id = selectDialogTitleId;
}
const selectDialogIds =
selectDialogElem.querySelectorAll("*[id]");
selectDialogIds.forEach((elemWithId) => {
elemWithId.id = `${elemWithId.id}-${dialogIdNr}`;
});
const selectDialogLabels =
selectDialogElem.querySelectorAll("*[for]");
selectDialogLabels.forEach((label) => {
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
});
const describedElems = selectDialogElem.querySelectorAll(
"*[aria-describedby]"
);
describedElems.forEach((describedElem) => {
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}`
);
});
}
const settingsDialogId = `librecms-image-node-view-settings-dialog-${dialogIdNr}`;
const settingsDialogTitleId = `${settingsDialogId}-title`;
const settingsButtonElem = nodeView.querySelector(
".image-settings-button"
);
if (settingsButtonElem) {
settingsButtonElem.setAttribute(
"data-target",
`#${settingsDialogId}`
);
}
const settingsDialogElem = nodeView.querySelector(
".modal.image-settings-dialog"
);
if (settingsDialogElem) {
settingsDialogElem.id = settingsDialogId;
settingsDialogElem.setAttribute(
"aria-labelledby",
settingsDialogTitleId
);
const settingsDialogTitleElem =
settingsDialogElem.querySelector(".modal-title");
if (settingsDialogTitleElem) {
settingsDialogTitleElem.id = settingsDialogTitleId;
}
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
) as HTMLLabelElement;
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
}
const describedElems = settingsDialogElem.querySelectorAll(
"*[aria-describedby]"
);
for (let i = 0; i < describedElems.length; i++) {
const describedElem = describedElems.item(i);
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}`
);
}
const submitButton = settingsDialogElem.querySelector(
"button.image-settings-dialog-save"
);
const altTextInput = settingsDialogElem.querySelector(
`input#alttext-${dialogIdNr}`
);
const captionInput = settingsDialogElem.querySelector(
`input#caption-${dialogIdNr}`
);
const alignSelect = settingsDialogElem.querySelector(
`select#align-${dialogIdNr}`
);
const sizeSelect = settingsDialogElem.querySelector(
`select#size-${dialogIdNr}`
);
const fullSizeOverlayInput = settingsDialogElem.querySelector(
`input#fullsizeoverlay-${dialogIdNr}`
);
if (altTextInput) {
(altTextInput as HTMLInputElement).value =
node.attrs.altText;
} else {
console.warn("Input for alt text not found.");
}
if (captionInput) {
(captionInput as HTMLInputElement).value =
node.attrs.figCaption;
} else {
console.warn("Input for caption not found.");
}
if (alignSelect) {
const optionElems = alignSelect.querySelectorAll("option");
for (let i = 0; i < optionElems.length; i++) {
const optionElem = optionElems.item(
i
) as HTMLOptionElement;
if (optionElem.value === node.attrs.align) {
optionElem.selected = true;
}
}
} else {
console.warn("Select for image alignment not found.");
}
if (sizeSelect) {
const optionElems = sizeSelect.querySelectorAll("option");
for (let i = 0; i < optionElems.length; i++) {
const optionElem = optionElems.item(
i
) as HTMLOptionElement;
if (optionElem.value === node.attrs.size) {
optionElem.selected = true;
}
}
} else {
console.warn("Select for image size not found.");
}
if (fullSizeOverlayInput) {
(fullSizeOverlayInput as HTMLInputElement).checked =
node.attrs.fullSizeOverlay;
} else {
console.warn("Input for fullSizeOverlay not found.");
}
if (submitButton) {
submitButton.addEventListener("click", (event) => {
const altInputElem = altTextInput as HTMLInputElement;
node.attrs.altText = altInputElem.value;
if (imgElem) {
imgElem.alt = altInputElem.value;
}
const captionInputElem =
captionInput as HTMLInputElement;
node.attrs.figCaption = captionInputElem.value;
if (figCaptionElem) {
figCaptionElem.textContent = captionInputElem.value;
}
if (alignSelect) {
const selectElem = alignSelect as HTMLSelectElement;
node.attrs.align =
selectElem.selectedOptions.item(0)?.value;
["floatleft", "center", "floatright"].forEach(
(align) =>
dom.classList.remove(
`librecms-image-node-align-${align}`
)
);
dom.classList.add(
`librecms-image-node-align-${node.attrs.align}`
);
} else {
console.warn(
"Select for image alignment not found."
);
}
if (sizeSelect) {
const selectElem = sizeSelect as HTMLSelectElement;
node.attrs.size =
selectElem.selectedOptions.item(0)?.value;
["25", "33", "50", "66", "75", "100"].forEach(
(size) =>
dom.classList.remove(
`librecms-image-node-width-${size}`
)
);
dom.classList.add(
`librecms-image-node-width-${node.attrs.size}`
);
} else {
console.warn("Select for image size not found.");
}
if (fullSizeOverlayInput) {
const inputElem =
fullSizeOverlayInput as HTMLInputElement;
node.attrs.fullSizeOverlay = inputElem.checked;
} else {
console.warn(
"Input for fullSizeOverlay not found."
);
}
});
} else {
console.warn(
"Submit button for image settings dialog not found."
);
}
}
const figCaptionElem = nodeView.querySelector("figcaption");
if (node.attrs.figCaption !== "" && figCaptionElem) {
figCaptionElem.innerHTML = node.attrs.figCaption;
}
dom.appendChild(nodeView);
if (selectButtonElem) {
if (imgElem) {
selectButtonElem.addEventListener("click", (event) =>
loadImages(event, node, imgElem)
);
} else {
console.error("img elem not found.");
}
}
return {
dom,
};
};
},
parseHTML() {
return [
{
tag: "figure[data-type=librecms-image-node]",
},
];
},
renderHTML({ node, HTMLAttributes }) {
// console.log("node = ");
// console.dir(node);
return [
"figure",
mergeAttributes({
"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: node.attrs.imgSrc,
},
],
["figcaption", node.attrs.figCaption],
];
},
});
function loadImages(
event: Event,
node: ProsemirrorNode<any>,
imgElem: HTMLImageElement
) {
// console.log("Loading images...");
const eventTarget = event.currentTarget as HTMLElement;
const editorElem = document.querySelector(".cms-editor");
if (!editorElem) {
return;
}
const baseUrl = editorElem.getAttribute("data-baseUrl");
const contentSection = editorElem.getAttribute("data-contentsection");
const dialogId = eventTarget.getAttribute("data-target");
if (!dialogId) {
console.error("data-target attribute is missing.");
return;
}
const dialog = document.querySelector(dialogId);
if (!dialog) {
console.error("dialog element not found is missing.");
return;
}
const rowTemplateResult = document.querySelector(
"#librecms-image-node-view-row"
);
if (!rowTemplateResult) {
console.error("template for result row not found.");
return;
}
const rowTemplate = rowTemplateResult as HTMLTemplateElement;
const table = dialog.querySelector("table");
if (!table) {
console.error("result table not found.");
return;
}
const tableBody = table.querySelector("tbody");
if (!tableBody) {
console.error("table body not found.");
return;
}
tableBody.innerHTML = "";
const fetchUrl = `/content-sections/${contentSection}/assets?type=org.librecms.assets.Image`;
fetch(fetchUrl)
.then((response) => {
if (response.ok) {
response
.json()
.then((data) => {
const images = data as [];
for (const image of images) {
const row = rowTemplate.content.cloneNode(
true
) as Element;
const colName = row.querySelector(".col-name");
const colType = row.querySelector(".col-type");
const selectButton =
row.querySelector(".col-action button");
if (colName) {
colName.textContent = image["name"];
}
if (colType) {
colType.textContent = image["type"];
}
if (selectButton) {
selectButton.setAttribute(
"data-imageuuid",
image["uuid"]
);
selectButton.addEventListener(
"click",
(event) => {
const imgUrl = `/content-sections/info/images/uuid-${image["uuid"]}`;
node.attrs.imgSrc = imgUrl;
if (imgElem) {
imgElem.src = imgUrl;
} else {
console.error(
"img element not found."
);
}
}
);
}
tableBody.appendChild(row);
}
})
.catch((error) => {
console.error(error);
console.trace(error);
});
} else {
console.error(
`Error. Status: ${response.status}. Status Text: ${response.statusText}`
);
}
})
.catch((error) => {
console.error(error);
console.trace(error);
});
}

View File

@ -0,0 +1,5 @@
import { ImageNode } from "./image-node";
export * from "./image-node";
export default ImageNode;

View File

@ -0,0 +1,5 @@
import { VideoNode } from "./video-node";
export * from "./video-node";
export default VideoNode;

View File

@ -0,0 +1,520 @@
import { Node, nodeInputRule, mergeAttributes, Range } from "@tiptap/core";
import { merge } from "jquery";
import { Node as ProsemirrorNode } from "prosemirror-model";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
libreCmsVideoNode: {
setLibreCmsVideo: (attributes?: { language: string }) => ReturnType;
};
}
}
export const VideoNode = Node.create({
name: "libreCmsVideoNode",
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 videoElem = element.querySelector("video");
if (videoElem) {
return videoElem.textContent;
} else {
return "";
}
},
},
figCaption: {
parseHTML: (element) => {
const figCaptionElem = element.querySelector("figcaption");
if (figCaptionElem) {
return figCaptionElem.innerHTML;
} else {
return "";
}
},
},
videoSrc: {
parseHTML: (element) => {
const videoElem = element.querySelector("video");
if (videoElem) {
return videoElem.src;
} else {
return "";
}
},
},
size: {
parseHTML: (element) => {
if (element.hasAttribute("data-size")) {
return element.getAttribute("data-size");
} else {
return "50";
}
},
},
};
},
addCommands() {
return {
setLibreCmsVideo:
(attributes) =>
({ commands }) => {
return commands.setNode("libreCmsVideoNode", attributes);
},
};
},
addNodeView() {
return ({
editor,
node,
getPos,
HTMLAttributes,
decorations,
extension,
}) => {
const templateNode = document.querySelector(
"#librecms-video-node-view"
);
if (!templateNode) {
const errorMsg = document.createElement("div");
errorMsg.classList.add("alert alert-danger");
errorMsg.textContent = "Failed to create video node view.";
}
const dom = document.createElement("div");
dom.classList.add("librecms-video-node-view", "p-2");
if (!node.attrs.size) {
node.attrs.size = "50";
}
dom.classList.add(`librecms-video-node-width-${node.attrs.size}`);
if (!node.attrs.align) {
node.attrs.align = "center";
}
dom.classList.add(`librecms-video-node-align-${node.attrs.align}`);
const template = templateNode as HTMLTemplateElement;
const nodeView = template.content.cloneNode(true) as HTMLElement;
const videoElem = nodeView.querySelector("video");
const dialogIdNr = Math.floor(Math.random() * 1000000000);
const selectDialogId = `librecms-video-node-select-video-dialog-title-${dialogIdNr}`;
const selectDialogTitleId = `librecms-video-node-select-video-dialog-${dialogIdNr}-title`;
const selectButtonElem = nodeView.querySelector(
".select-video-button"
);
if (selectButtonElem) {
selectButtonElem.setAttribute(
"data-target",
`#${selectDialogId}`
);
}
const selectDialogElem = nodeView.querySelector(
".modal.select-video-dialog"
);
if (selectDialogElem) {
selectDialogElem.id = selectDialogId;
selectDialogElem.setAttribute(
"aria-labelledby",
selectDialogTitleId
);
const selectDialogTitleElem =
selectDialogElem.querySelector(".modal-title");
if (selectDialogTitleElem) {
selectDialogTitleElem.id = selectDialogTitleId;
}
const selectDialogIds =
selectDialogElem.querySelectorAll("*[id]");
selectDialogIds.forEach((elemWithId) => {
elemWithId.id = `${elemWithId.id}-${dialogIdNr}`;
});
const selectDialogLabels =
selectDialogElem.querySelectorAll("*[for]");
selectDialogLabels.forEach((label) => {
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
});
const describedElems = selectDialogElem.querySelectorAll(
"*[aria-describedby]"
);
describedElems.forEach((describedElem) => {
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}}`
);
});
}
const settingsDialogId = `librecms-video-node-view-settings-dialog-${dialogIdNr}`;
const settingsDialogTitleId = `${settingsDialogId}-title`;
const settingsButtonElem = nodeView.querySelector(
".video-settings-button"
);
if (settingsButtonElem) {
settingsButtonElem.setAttribute(
"data-target",
`#${settingsDialogId}`
);
}
const settingsDialogElem = nodeView.querySelector(
".modal.video-settings-dialog"
);
if (settingsDialogElem) {
settingsDialogElem.id = settingsDialogId;
settingsDialogElem.setAttribute(
"aria-labelledby",
settingsDialogTitleId
);
const settingsDialogTitleElem =
settingsDialogElem.querySelector(".modal-title");
if (settingsDialogTitleElem) {
settingsDialogTitleElem.id = settingsDialogTitleId;
}
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
) as HTMLLabelElement;
label.setAttribute(
"for",
`${label.getAttribute("for")}-${dialogIdNr}`
);
}
const describedElems = settingsDialogElem.querySelectorAll(
"*[aria-describedby]"
);
for (let i = 0; i < describedElems.length; i++) {
const describedElem = describedElems.item(i);
describedElem.setAttribute(
"aria-describedby",
`${describedElem.getAttribute(
"aria-describedby"
)}-${dialogIdNr}`
);
}
const submitButton = settingsDialogElem.querySelector(
"button.video-settings-dialog-save"
);
const altTextInput = settingsDialogElem.querySelector(
`input#alttext-${dialogIdNr}`
);
const captionInput = settingsDialogElem.querySelector(
`input#caption-${dialogIdNr}`
);
const alignSelect = settingsDialogElem.querySelector(
`select#align-${dialogIdNr}`
);
const sizeSelect = settingsDialogElem.querySelector(
`select#size-${dialogIdNr}`
);
if (altTextInput) {
(altTextInput as HTMLInputElement).value =
node.attrs.altText;
} else {
console.warn("Input for alt text not found.");
}
if (captionInput) {
(captionInput as HTMLInputElement).value =
node.attrs.figCaption;
} else {
console.warn("Input for caption not found.");
}
if (alignSelect) {
const optionElems = alignSelect.querySelectorAll("option");
for (let i = 0; i < optionElems.length; i++) {
const optionElem = optionElems.item(
i
) as HTMLOptionElement;
if (optionElem.value === node.attrs.align) {
optionElem.selected = true;
}
}
} else {
console.warn("Select for video alignment not found.");
}
if (sizeSelect) {
const optionElems = sizeSelect.querySelectorAll("option");
for (let i = 0; i < optionElems.length; i++) {
const optionElem = optionElems.item(
i
) as HTMLOptionElement;
if (optionElem.value === node.attrs.size) {
optionElem.selected = true;
}
}
} else {
console.warn("Select for video size not found.");
}
if (submitButton) {
submitButton.addEventListener("click", (event) => {
const altInputElem = altTextInput as HTMLInputElement;
node.attrs.altText = altInputElem.value;
if (videoElem) {
videoElem.textContent = altInputElem.value;
}
const captionInputElem =
captionInput as HTMLInputElement;
node.attrs.figCaption = captionInputElem.value;
if (figCaptionElem) {
figCaptionElem.textContent = captionInputElem.value;
}
if (alignSelect) {
const selectElem = alignSelect as HTMLSelectElement;
node.attrs.align =
selectElem.selectedOptions.item(0)?.value;
["floatleft", "center", "floatright"].forEach(
(align) =>
dom.classList.remove(
`librecms-video-node-align-${align}`
)
);
dom.classList.add(
`librecms-video-node-align-${node.attrs.align}`
);
} else {
console.warn(
"Select for video alignment not found."
);
}
if (sizeSelect) {
const selectElem = sizeSelect as HTMLSelectElement;
node.attrs.size =
selectElem.selectedOptions.item(0)?.value;
["25", "33", "50", "66", "75", "100"].forEach(
(size) =>
dom.classList.remove(
`librecms-video-node-width-${size}`
)
);
dom.classList.add(
`librecms-video-node-width-${node.attrs.size}`
);
} else {
console.warn("Select for video size not found.");
}
});
} else {
console.warn(
"Submit button for video settings dialog not found."
);
}
}
const figCaptionElem = nodeView.querySelector("figcaption");
if (node.attrs.figCaption !== "" && figCaptionElem) {
figCaptionElem.innerHTML = node.attrs.figCaption;
}
dom.appendChild(nodeView);
if (selectButtonElem) {
if (videoElem) {
selectButtonElem.addEventListener("click", (event) =>
loadVideos(event, node, videoElem)
);
} else {
console.error("video elem not found.");
}
}
return {
dom,
};
};
},
parseHTML() {
return [
{
tag: "figure[data-type=librecms-video-node]",
},
];
},
renderHTML({ node, HTMLAttributes }) {
return [
"figure",
mergeAttributes({
"data-align": node.attrs.align,
"data-size": node.attrs.size,
"data-type": "librecms-video-node",
}),
[
"video",
{
src: node.attrs.videoSrc,
textContent: node.attrs.altText,
},
],
["figcaption", node.attrs.figCaption],
];
},
});
function loadVideos(
event: Event,
node: ProsemirrorNode<any>,
videoElem: HTMLVideoElement
) {
const eventTarget = event.currentTarget as HTMLElement;
const editorElem = document.querySelector(".cms-editor");
if (!editorElem) {
return;
}
const baseUrl = editorElem.getAttribute("data-baseUrl");
const contentSection = editorElem.getAttribute("data-contentsection");
const dialogId = eventTarget.getAttribute("data-target");
if (!dialogId) {
console.error("data-target attribute is missing.");
return;
}
const dialog = document.querySelector(dialogId);
if (!dialog) {
console.error("dialog element not found is missing.");
return;
}
const rowTemplateResult = document.querySelector(
"#librecms-video-node-view-row"
);
if (!rowTemplateResult) {
console.error("template for result row not found.");
return;
}
const rowTemplate = rowTemplateResult as HTMLTemplateElement;
const table = dialog.querySelector("table");
if (!table) {
console.error("result table not found.");
return;
}
const tableBody = table.querySelector("tbody");
if (!tableBody) {
console.error("table body not found.");
return;
}
tableBody.innerHTML = "";
const fetchUrl = `/content-sections/${contentSection}/assets?type=org.librecms.assets.VideoAsset,org.librecms.assets.ExternalVideoAsset`;
fetch(fetchUrl)
.then((response) => {
if (response.ok) {
response
.json()
.then((data) => {
const videos = data as [];
for (const video of videos) {
const row = rowTemplate.content.cloneNode(
true
) as Element;
const colName = row.querySelector(".col-name");
const colType = row.querySelector(".col-type");
const selectButton =
row.querySelector(".col-action button");
if (colName) {
colName.textContent = video["name"];
}
if (colType) {
colType.textContent = video["type"];
}
if (selectButton) {
selectButton.setAttribute(
"data-videouuid",
video["uuid"]
);
selectButton.addEventListener(
"click",
(event) => {
const videoUrl = buildVideoUrl(video);
node.attrs.videoSrc = videoUrl;
if (videoElem) {
videoElem.src = videoUrl;
} else {
console.error(
"video element not found."
);
}
}
);
}
tableBody.appendChild(row);
}
})
.catch((error) => {
console.error(error);
});
} else {
console.error(
`Error. Status: ${response.status}. Status Text: ${response.statusText}`
);
}
})
.catch((error) => {
console.error(error);
});
}
function buildVideoUrl(video: any) {
switch (video["type"]) {
case "org.librecms.assets.ExternalVideoAsset":
return video["properties"]["targetUrl"];
case "org.librecms.assets.VideoAsset":
return `/content-sections/info/videos/uuid-${video["uuid"]}`;
default:
console.error(`Unknown video asset type.`);
return "";
}
}

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-event-eventdate-editor");

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-event-eventtype-editor");

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-event-location-editor");

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-event-maincontributor-editor");

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-mpa-section-text-editor");

View File

@ -1,4 +1,4 @@
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
document.addEventListener("DOMContentLoaded", (event) => {
const editorElem = document.querySelector("#cms-news-text-editor");

18
pom.xml
View File

@ -6,6 +6,23 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--<ccm.version>7.0.0-SNAPSHOT</ccm.version>-->
<maven.build.timestamp.format>yyyy-MM-dd'T'HHmmss</maven.build.timestamp.format>
<!-- Node JS version to use by frontend-maven-plugin -->
<nodeJsVersion>v16.14.2</nodeJsVersion>
<!--
Version for NPM packages that are deployed to our package
repository. NPM does not support SNAPSHOTS like Maven, therefore
we add the build timestamp to simulate SNAPSHOT behaviour.
This has to be changed before a release.
-->
<npmPackageVersion>${project.version}.${maven.build.timestamp}</npmPackageVersion>
<!--
This is the NPM package version for a release, without a timestamp.
-->
<!--
<npmPackageVersion>${project.version}</npmPackageVersion>
-->
</properties>
<groupId>org.libreccm</groupId>
@ -69,6 +86,7 @@
<!--<module>ccm-cms-pagemodelseditor</module>-->
<!--<module>ccm-cms-tinymce</module>-->
<module>ccm-cms-editor</module>
<module>ccm-cms-types-agenda</module>
<module>ccm-cms-types-bookmark</module>