From 6e852d2e652e881e5f0096efa2b35ae3d712b4a3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 May 2024 20:23:45 +0100 Subject: [PATCH] Lexical: Played with commands, extracted & improved callout node --- package-lock.json | 1 + package.json | 1 + resources/js/components/wysiwyg-editor.js | 1 + resources/js/wysiwyg/index.mjs | 113 ++++++------------ resources/js/wysiwyg/nodes/callout.js | 98 +++++++++++++++ resources/js/wysiwyg/nodes/index.js | 14 +++ .../pages/parts/wysiwyg-editor.blade.php | 4 + 7 files changed, 156 insertions(+), 76 deletions(-) create mode 100644 resources/js/wysiwyg/nodes/callout.js create mode 100644 resources/js/wysiwyg/nodes/index.js diff --git a/package-lock.json b/package-lock.json index 6a992f4d0..6cd32760d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@lexical/history": "^0.15.0", "@lexical/html": "^0.15.0", "@lexical/rich-text": "^0.15.0", + "@lexical/selection": "^0.15.0", "@lexical/utils": "^0.15.0", "@lezer/highlight": "^1.2.0", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", diff --git a/package.json b/package.json index 706c18738..42b86fdc7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@lexical/history": "^0.15.0", "@lexical/html": "^0.15.0", "@lexical/rich-text": "^0.15.0", + "@lexical/selection": "^0.15.0", "@lexical/utils": "^0.15.0", "@lezer/highlight": "^1.2.0", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index bcd480ce6..98732dab7 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -25,6 +25,7 @@ export class WysiwygEditor extends Component { * @return {{html: String}} */ getContent() { + // TODO - Update return { html: this.editor.getContent(), }; diff --git a/resources/js/wysiwyg/index.mjs b/resources/js/wysiwyg/index.mjs index 4c4f16ce3..decfa4f22 100644 --- a/resources/js/wysiwyg/index.mjs +++ b/resources/js/wysiwyg/index.mjs @@ -1,85 +1,23 @@ -import {$getRoot, createEditor, ElementNode} from 'lexical'; +import { + $createParagraphNode, + $getRoot, + $getSelection, + COMMAND_PRIORITY_LOW, + createCommand, + createEditor +} from 'lexical'; import {createEmptyHistoryState, registerHistory} from '@lexical/history'; -import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text'; -import {mergeRegister} from '@lexical/utils'; +import {registerRichText} from '@lexical/rich-text'; +import {$getNearestBlockElementAncestorOrThrow, 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 +import {getNodesForPageEditor} from "./nodes/index.js"; +import {$createCalloutNode, $isCalloutNode} from "./nodes/callout.js"; +import {$setBlocksType} from "@lexical/selection"; export function createPageEditorInstance(editArea) { - console.log('creating editor', editArea); - const config = { namespace: 'BookStackPageEditor', - nodes: [HeadingNode, QuoteNode, CalloutParagraph], + nodes: getNodesForPageEditor(), onError: console.error, }; @@ -106,4 +44,27 @@ export function createPageEditorInstance(editArea) { console.log('editorState', editorState.toJSON()); debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2); }); + + // Todo - How can we store things like IDs and alignment? + // Node overrides? + // https://lexical.dev/docs/concepts/node-replacement + + // Example of creating, registering and using a custom command + + const SET_BLOCK_CALLOUT_COMMAND = createCommand(); + editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category = 'info') => { + const selection = $getSelection(); + const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]); + if ($isCalloutNode(blockElement)) { + $setBlocksType(selection, $createParagraphNode); + } else { + $setBlocksType(selection, () => $createCalloutNode(category)); + } + return true; + }, COMMAND_PRIORITY_LOW); + + const button = document.getElementById('lexical-button'); + button.addEventListener('click', event => { + editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info'); + }); } \ No newline at end of file diff --git a/resources/js/wysiwyg/nodes/callout.js b/resources/js/wysiwyg/nodes/callout.js new file mode 100644 index 000000000..db90f22a9 --- /dev/null +++ b/resources/js/wysiwyg/nodes/callout.js @@ -0,0 +1,98 @@ +import {$createParagraphNode, ElementNode} from 'lexical'; + +export class Callout extends ElementNode { + + __category = 'info'; + + static getType() { + return 'callout'; + } + + static clone(node) { + return new Callout(node.__category, node.__key); + } + + constructor(category, key) { + super(key); + this.__category = category; + } + + createDOM(_config, _editor) { + const element = document.createElement('p'); + element.classList.add('callout', this.__category || ''); + return element; + } + + 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; + } + + insertNewAfter(selection, restoreSelection) { + const anchorOffset = selection ? selection.anchor.offset : 0; + const newElement = anchorOffset === this.getTextContentSize() || !selection + ? $createParagraphNode() : $createCalloutNode(this.__category); + + newElement.setDirection(this.getDirection()); + this.insertAfter(newElement, restoreSelection); + + if (anchorOffset === 0 && !this.isEmpty() && selection) { + const paragraph = $createParagraphNode(); + paragraph.select(); + this.replace(paragraph, true); + } + + return newElement; + } + + 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 Callout(category), + }; + }, + priority: 3, + }; + } + return null; + }, + }; + } + + exportJSON() { + return { + ...super.exportJSON(), + type: 'callout', + version: 1, + category: this.__category, + }; + } + + static importJSON(serializedNode) { + return $createCalloutNode(serializedNode.category); + } + +} + +export function $createCalloutNode(category = 'info') { + return new Callout(category); +} + +export function $isCalloutNode(node) { + return node instanceof Callout; +} diff --git a/resources/js/wysiwyg/nodes/index.js b/resources/js/wysiwyg/nodes/index.js new file mode 100644 index 000000000..ada229d9e --- /dev/null +++ b/resources/js/wysiwyg/nodes/index.js @@ -0,0 +1,14 @@ +import {HeadingNode, QuoteNode} from '@lexical/rich-text'; +import {Callout} from './callout'; + +/** + * Load the nodes for lexical. + * @returns {LexicalNode[]} + */ +export function getNodesForPageEditor() { + return [ + Callout, + HeadingNode, + QuoteNode, + ]; +} diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 7528b1e02..30be1a214 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -6,6 +6,10 @@ option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}" class=""> +
+ +
+

Some content here

List below this h2 header