Moved ccm-cms-editor to separate module to make it usable in other projects (for example ScientificCMS)
parent
f73f9084cf
commit
0cceec93e8
|
|
@ -0,0 +1 @@
|
||||||
|
target
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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/ }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@librecms/ccm-cms",
|
"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",
|
"description": "JavaScript stuff for ccm-cms",
|
||||||
"main": "target/generated-resources/assets/@content-sections/cms-admin.js",
|
"main": "target/generated-resources/assets/@content-sections/cms-admin.js",
|
||||||
"types": "target/generated-resources/assets/@content-sections/cms-admin.d.ts",
|
"types": "target/generated-resources/assets/@content-sections/cms-admin.d.ts",
|
||||||
|
|
@ -23,14 +23,7 @@
|
||||||
"webpack-cli": "^4.8.0"
|
"webpack-cli": "^4.8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.127",
|
"@librecms/ccm-cms-editor": "../ccm-cms-editor",
|
||||||
"@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",
|
"acorn": "^8.5.0",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-icons": "^1.5.0",
|
"bootstrap-icons": "^1.5.0",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.librecms</groupId>
|
||||||
|
<artifactId>ccm-cms-editor</artifactId>
|
||||||
|
<version>${project.parent.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate</groupId>
|
<groupId>org.hibernate</groupId>
|
||||||
<artifactId>hibernate-core</artifactId>
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
|
@ -237,6 +243,15 @@
|
||||||
<arguments>pkg set version=${project.version}.${timestamp}</arguments>
|
<arguments>pkg set version=${project.version}.${timestamp}</arguments>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</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>
|
<execution>
|
||||||
<id>npm install</id>
|
<id>npm install</id>
|
||||||
<goals>
|
<goals>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-article-text-editor");
|
const editorElem = document.querySelector("#cms-article-text-editor");
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import "./cms-attachment-lists";
|
||||||
|
|
||||||
import "./cms-contentitempicker";
|
import "./cms-contentitempicker";
|
||||||
|
|
||||||
import "./cms-editor";
|
|
||||||
|
|
||||||
import "./cms-media-lists";
|
import "./cms-media-lists";
|
||||||
|
|
||||||
import "./cms-related-link";
|
import "./cms-related-link";
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AudioNode } from "./audio-node";
|
||||||
|
|
||||||
|
export * from "./audio-node";
|
||||||
|
|
||||||
|
export default AudioNode;
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ImageNode } from "./image-node";
|
||||||
|
|
||||||
|
export * from "./image-node";
|
||||||
|
|
||||||
|
export default ImageNode;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { VideoNode } from "./video-node";
|
||||||
|
|
||||||
|
export * from "./video-node";
|
||||||
|
|
||||||
|
export default VideoNode;
|
||||||
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-event-eventdate-editor");
|
const editorElem = document.querySelector("#cms-event-eventdate-editor");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-event-eventtype-editor");
|
const editorElem = document.querySelector("#cms-event-eventtype-editor");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-event-location-editor");
|
const editorElem = document.querySelector("#cms-event-location-editor");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-event-maincontributor-editor");
|
const editorElem = document.querySelector("#cms-event-maincontributor-editor");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-mpa-section-text-editor");
|
const editorElem = document.querySelector("#cms-mpa-section-text-editor");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CmsEditorBuilder, CmsEditor } from "./cms-editor";
|
import { CmsEditorBuilder, CmsEditor } from "@librecms/ccm-cms-editor";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const editorElem = document.querySelector("#cms-news-text-editor");
|
const editorElem = document.querySelector("#cms-news-text-editor");
|
||||||
|
|
|
||||||
18
pom.xml
18
pom.xml
|
|
@ -6,6 +6,23 @@
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<!--<ccm.version>7.0.0-SNAPSHOT</ccm.version>-->
|
<!--<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>
|
</properties>
|
||||||
|
|
||||||
<groupId>org.libreccm</groupId>
|
<groupId>org.libreccm</groupId>
|
||||||
|
|
@ -69,6 +86,7 @@
|
||||||
|
|
||||||
<!--<module>ccm-cms-pagemodelseditor</module>-->
|
<!--<module>ccm-cms-pagemodelseditor</module>-->
|
||||||
<!--<module>ccm-cms-tinymce</module>-->
|
<!--<module>ccm-cms-tinymce</module>-->
|
||||||
|
<module>ccm-cms-editor</module>
|
||||||
|
|
||||||
<module>ccm-cms-types-agenda</module>
|
<module>ccm-cms-types-agenda</module>
|
||||||
<module>ccm-cms-types-bookmark</module>
|
<module>ccm-cms-types-bookmark</module>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue