diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index c5b3c9ef3..20193db7f 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -14,6 +14,7 @@ const entryPoints = { code: path.join(__dirname, '../../resources/js/code/index.mjs'), 'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'), markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'), + wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.mjs'), }; // Locate our output directory diff --git a/package-lock.json b/package-lock.json index 63b0d2478..6a992f4d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,16 @@ "@codemirror/state": "^6.3.3", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.22.2", + "@lexical/history": "^0.15.0", + "@lexical/html": "^0.15.0", + "@lexical/rich-text": "^0.15.0", + "@lexical/utils": "^0.15.0", "@lezer/highlight": "^1.2.0", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "codemirror": "^6.0.1", "idb-keyval": "^6.2.1", + "lexical": "^0.15.0", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "snabbdom": "^3.5.1", @@ -691,6 +696,85 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@lexical/clipboard": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.15.0.tgz", + "integrity": "sha512-binCltK7KiURQJFogvueYfmDNEKynN/lmZrCLFp2xBjEIajqw4WtOVLJZ33engdqNlvj0JqrxrWxbKG+yvUwrg==", + "dependencies": { + "@lexical/html": "0.15.0", + "@lexical/list": "0.15.0", + "@lexical/selection": "0.15.0", + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/history": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.15.0.tgz", + "integrity": "sha512-r+pzR2k/51AL6l8UfXeVe/GWPIeWY1kEOuKx9nsYB9tmAkTF66tTFz33DJIMWBVtAHWN7Dcdv0/yy6q8R6CAUQ==", + "dependencies": { + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/html": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.15.0.tgz", + "integrity": "sha512-x/sfGvibwo8b5Vso4ppqNyS/fVve6Rn+TmvP/0eWOaa0I3aOQ57ulfcK6p/GTe+ZaEi8vW64oZPdi8XDgwSRaA==", + "dependencies": { + "@lexical/selection": "0.15.0", + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/list": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.15.0.tgz", + "integrity": "sha512-JuF4k7uo4rZFOSZGrmkxo1+sUrwTKNBhhJAiCgtM+6TO90jppxzCFNKur81yPzF1+g4GWLC9gbjzKb52QPb6cQ==", + "dependencies": { + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.15.0.tgz", + "integrity": "sha512-76tXh/eeEOHl91HpFEXCc/tUiLrsa9RcSyvCzRZahk5zqYvQPXma/AUfRzuSMf2kLwDEoauKAVqNFQcbPhqwpQ==", + "dependencies": { + "@lexical/clipboard": "0.15.0", + "@lexical/selection": "0.15.0", + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/selection": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.15.0.tgz", + "integrity": "sha512-S+AQC6eJiQYSa5zOPuecN85prCT0Bcb8miOdJaE17Zh+vgdUH5gk9I0tEBeG5T7tkSpq6lFiEqs2FZSfaHflbQ==", + "dependencies": { + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/table": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.15.0.tgz", + "integrity": "sha512-3IRBg8IoIHetqKozRQbJQ2aPyG0ziXZ+lc8TOIAGs6METW/wxntaV+rTNrODanKAgvk2iJTIyfFkYjsqS9+VFg==", + "dependencies": { + "@lexical/utils": "0.15.0", + "lexical": "0.15.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.15.0.tgz", + "integrity": "sha512-/6954LDmTcVFgexhy5WOZDa4TxNQOEZNrf8z7TRAFiAQkihcME/GRoq1en5cbXoVNF8jv5AvNyyc7x0MByRJ6A==", + "dependencies": { + "@lexical/list": "0.15.0", + "@lexical/selection": "0.15.0", + "@lexical/table": "0.15.0", + "lexical": "0.15.0" + } + }, "node_modules/@lezer/common": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", @@ -2709,6 +2793,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lexical": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.15.0.tgz", + "integrity": "sha512-/7HrPAmtgsc1F+qpv5bFwoQZ6CbH/w3mPPL2AW5P75/QYrqKz4bhvJrc2jozIX0GxtuT/YUYT7w+1sZMtUWbOg==" + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", diff --git a/package.json b/package.json index ba2de38ba..706c18738 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,16 @@ "@codemirror/state": "^6.3.3", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.22.2", + "@lexical/history": "^0.15.0", + "@lexical/html": "^0.15.0", + "@lexical/rich-text": "^0.15.0", + "@lexical/utils": "^0.15.0", "@lezer/highlight": "^1.2.0", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "codemirror": "^6.0.1", "idb-keyval": "^6.2.1", + "lexical": "^0.15.0", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "snabbdom": "^3.5.1", diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 3a66079d7..8ad5e14cb 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -58,4 +58,5 @@ export {TriLayout} from './tri-layout'; export {UserSelect} from './user-select'; export {WebhookEvents} from './webhook-events'; export {WysiwygEditor} from './wysiwyg-editor'; +export {WysiwygEditorTinymce} from './wysiwyg-editor-tinymce'; export {WysiwygInput} from './wysiwyg-input'; diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index 79c9d3c2c..cac20c9fb 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -1,6 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; -import {buildForInput} from '../wysiwyg/config'; +import {buildForInput} from '../wysiwyg-tinymce/config'; export class PageComment extends Component { diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index cfb0634a9..bd6dd3c82 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -1,6 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; -import {buildForInput} from '../wysiwyg/config'; +import {buildForInput} from '../wysiwyg-tinymce/config'; export class PageComments extends Component { diff --git a/resources/js/components/wysiwyg-editor-tinymce.js b/resources/js/components/wysiwyg-editor-tinymce.js new file mode 100644 index 000000000..093442ea2 --- /dev/null +++ b/resources/js/components/wysiwyg-editor-tinymce.js @@ -0,0 +1,48 @@ +import {buildForEditor as buildEditorConfig} from '../wysiwyg-tinymce/config'; +import {Component} from './component'; + +export class WysiwygEditorTinymce extends Component { + + setup() { + this.elem = this.$el; + + this.tinyMceConfig = buildEditorConfig({ + language: this.$opts.language, + containerElement: this.elem, + darkMode: document.documentElement.classList.contains('dark-mode'), + textDirection: this.$opts.textDirection, + drawioUrl: this.getDrawIoUrl(), + pageId: Number(this.$opts.pageId), + translations: { + imageUploadErrorText: this.$opts.imageUploadErrorText, + serverUploadLimitText: this.$opts.serverUploadLimitText, + }, + translationMap: window.editor_translations, + }); + + window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig}); + window.tinymce.init(this.tinyMceConfig).then(editors => { + this.editor = editors[0]; + }); + } + + getDrawIoUrl() { + const drawioUrlElem = document.querySelector('[drawio-url]'); + if (drawioUrlElem) { + return drawioUrlElem.getAttribute('drawio-url'); + } + return ''; + } + + /** + * Get the content of this editor. + * Used by the parent page editor component. + * @return {{html: String}} + */ + getContent() { + return { + html: this.editor.getContent(), + }; + } + +} diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 82f60827d..bcd480ce6 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -1,28 +1,13 @@ -import {buildForEditor as buildEditorConfig} from '../wysiwyg/config'; import {Component} from './component'; export class WysiwygEditor extends Component { setup() { this.elem = this.$el; + this.editArea = this.$refs.editArea; - this.tinyMceConfig = buildEditorConfig({ - language: this.$opts.language, - containerElement: this.elem, - darkMode: document.documentElement.classList.contains('dark-mode'), - textDirection: this.$opts.textDirection, - drawioUrl: this.getDrawIoUrl(), - pageId: Number(this.$opts.pageId), - translations: { - imageUploadErrorText: this.$opts.imageUploadErrorText, - serverUploadLimitText: this.$opts.serverUploadLimitText, - }, - translationMap: window.editor_translations, - }); - - window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig}); - window.tinymce.init(this.tinyMceConfig).then(editors => { - this.editor = editors[0]; + window.importVersioned('wysiwyg').then(wysiwyg => { + wysiwyg.createPageEditorInstance(this.editArea); }); } diff --git a/resources/js/components/wysiwyg-input.js b/resources/js/components/wysiwyg-input.js index ad964aed2..aa21a6371 100644 --- a/resources/js/components/wysiwyg-input.js +++ b/resources/js/components/wysiwyg-input.js @@ -1,5 +1,5 @@ import {Component} from './component'; -import {buildForInput} from '../wysiwyg/config'; +import {buildForInput} from '../wysiwyg-tinymce/config'; export class WysiwygInput extends Component { diff --git a/resources/js/wysiwyg/common-events.js b/resources/js/wysiwyg-tinymce/common-events.js similarity index 100% rename from resources/js/wysiwyg/common-events.js rename to resources/js/wysiwyg-tinymce/common-events.js diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg-tinymce/config.js similarity index 100% rename from resources/js/wysiwyg/config.js rename to resources/js/wysiwyg-tinymce/config.js diff --git a/resources/js/wysiwyg/drop-paste-handling.js b/resources/js/wysiwyg-tinymce/drop-paste-handling.js similarity index 100% rename from resources/js/wysiwyg/drop-paste-handling.js rename to resources/js/wysiwyg-tinymce/drop-paste-handling.js diff --git a/resources/js/wysiwyg/filters.js b/resources/js/wysiwyg-tinymce/filters.js similarity index 100% rename from resources/js/wysiwyg/filters.js rename to resources/js/wysiwyg-tinymce/filters.js diff --git a/resources/js/wysiwyg/fixes.js b/resources/js/wysiwyg-tinymce/fixes.js similarity index 100% rename from resources/js/wysiwyg/fixes.js rename to resources/js/wysiwyg-tinymce/fixes.js diff --git a/resources/js/wysiwyg/icons.js b/resources/js/wysiwyg-tinymce/icons.js similarity index 100% rename from resources/js/wysiwyg/icons.js rename to resources/js/wysiwyg-tinymce/icons.js diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg-tinymce/plugin-codeeditor.js similarity index 100% rename from resources/js/wysiwyg/plugin-codeeditor.js rename to resources/js/wysiwyg-tinymce/plugin-codeeditor.js diff --git a/resources/js/wysiwyg/plugin-drawio.js b/resources/js/wysiwyg-tinymce/plugin-drawio.js similarity index 100% rename from resources/js/wysiwyg/plugin-drawio.js rename to resources/js/wysiwyg-tinymce/plugin-drawio.js diff --git a/resources/js/wysiwyg/plugins-about.js b/resources/js/wysiwyg-tinymce/plugins-about.js similarity index 100% rename from resources/js/wysiwyg/plugins-about.js rename to resources/js/wysiwyg-tinymce/plugins-about.js diff --git a/resources/js/wysiwyg/plugins-customhr.js b/resources/js/wysiwyg-tinymce/plugins-customhr.js similarity index 100% rename from resources/js/wysiwyg/plugins-customhr.js rename to resources/js/wysiwyg-tinymce/plugins-customhr.js diff --git a/resources/js/wysiwyg/plugins-details.js b/resources/js/wysiwyg-tinymce/plugins-details.js similarity index 100% rename from resources/js/wysiwyg/plugins-details.js rename to resources/js/wysiwyg-tinymce/plugins-details.js diff --git a/resources/js/wysiwyg/plugins-imagemanager.js b/resources/js/wysiwyg-tinymce/plugins-imagemanager.js similarity index 100% rename from resources/js/wysiwyg/plugins-imagemanager.js rename to resources/js/wysiwyg-tinymce/plugins-imagemanager.js diff --git a/resources/js/wysiwyg/plugins-stub.js b/resources/js/wysiwyg-tinymce/plugins-stub.js similarity index 100% rename from resources/js/wysiwyg/plugins-stub.js rename to resources/js/wysiwyg-tinymce/plugins-stub.js diff --git a/resources/js/wysiwyg/plugins-table-additions.js b/resources/js/wysiwyg-tinymce/plugins-table-additions.js similarity index 100% rename from resources/js/wysiwyg/plugins-table-additions.js rename to resources/js/wysiwyg-tinymce/plugins-table-additions.js diff --git a/resources/js/wysiwyg/plugins-tasklist.js b/resources/js/wysiwyg-tinymce/plugins-tasklist.js similarity index 100% rename from resources/js/wysiwyg/plugins-tasklist.js rename to resources/js/wysiwyg-tinymce/plugins-tasklist.js diff --git a/resources/js/wysiwyg/scrolling.js b/resources/js/wysiwyg-tinymce/scrolling.js similarity index 100% rename from resources/js/wysiwyg/scrolling.js rename to resources/js/wysiwyg-tinymce/scrolling.js diff --git a/resources/js/wysiwyg/shortcuts.js b/resources/js/wysiwyg-tinymce/shortcuts.js similarity index 100% rename from resources/js/wysiwyg/shortcuts.js rename to resources/js/wysiwyg-tinymce/shortcuts.js diff --git a/resources/js/wysiwyg/toolbars.js b/resources/js/wysiwyg-tinymce/toolbars.js similarity index 100% rename from resources/js/wysiwyg/toolbars.js rename to resources/js/wysiwyg-tinymce/toolbars.js diff --git a/resources/js/wysiwyg/util.js b/resources/js/wysiwyg-tinymce/util.js similarity index 100% rename from resources/js/wysiwyg/util.js rename to resources/js/wysiwyg-tinymce/util.js diff --git a/resources/js/wysiwyg/index.mjs b/resources/js/wysiwyg/index.mjs new file mode 100644 index 000000000..4c4f16ce3 --- /dev/null +++ b/resources/js/wysiwyg/index.mjs @@ -0,0 +1,109 @@ +import {$getRoot, createEditor, ElementNode} from 'lexical'; +import {createEmptyHistoryState, registerHistory} from '@lexical/history'; +import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text'; +import {mergeRegister} from '@lexical/utils'; +import {$generateNodesFromDOM} from '@lexical/html'; + +class CalloutParagraph extends ElementNode { + __category = 'info'; + + static getType() { + return 'callout'; + } + + static clone(node) { + return new CalloutParagraph(node.__category, node.__key); + } + + constructor(category, key) { + super(key); + this.__category = category; + } + + createDOM(_config, _editor) { + const dom = document.createElement('p'); + dom.classList.add('callout', this.__category || ''); + return dom; + } + + updateDOM(prevNode, dom) { + // Returning false tells Lexical that this node does not need its + // DOM element replacing with a new copy from createDOM. + return false; + } + + static importDOM() { + return { + p: node => { + if (node.classList.contains('callout')) { + return { + conversion: element => { + let category = 'info'; + const categories = ['info', 'success', 'warning', 'danger']; + + for (const c of categories) { + if (element.classList.contains(c)) { + category = c; + break; + } + } + + return { + node: new CalloutParagraph(category), + }; + }, + priority: 3, + } + } + return null; + } + } + } + + exportJSON() { + return { + ...super.exportJSON(), + type: 'callout', + version: 1, + category: this.__category, + }; + } +} + +// TODO - Extract callout to own file +// TODO - Add helper functions +// https://lexical.dev/docs/concepts/nodes#creating-custom-nodes + +export function createPageEditorInstance(editArea) { + console.log('creating editor', editArea); + + const config = { + namespace: 'BookStackPageEditor', + nodes: [HeadingNode, QuoteNode, CalloutParagraph], + onError: console.error, + }; + + const startingHtml = editArea.innerHTML; + const parser = new DOMParser(); + const dom = parser.parseFromString(startingHtml, 'text/html'); + + const editor = createEditor(config); + editor.setRootElement(editArea); + + mergeRegister( + registerRichText(editor), + registerHistory(editor, createEmptyHistoryState(), 300), + ); + + editor.update(() => { + const startingNodes = $generateNodesFromDOM(editor, dom); + const root = $getRoot(); + root.append(...startingNodes); + }); + + const debugView = document.getElementById('lexical-debug'); + editor.registerUpdateListener(({editorState}) => { + console.log('editorState', editorState.toJSON()); + debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2); + }); +} \ No newline at end of file diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php index e2c839cd2..490374e40 100644 --- a/resources/views/pages/parts/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -33,11 +33,15 @@ {{--Editors--}}
Some content here
++ Hello there, this is an info callout +
+