Lexical: Played with commands, extracted & improved callout node

This commit is contained in:
Dan Brown 2024-05-27 20:23:45 +01:00
parent 5a4f595341
commit 6e852d2e65
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 156 additions and 76 deletions

1
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -25,6 +25,7 @@ export class WysiwygEditor extends Component {
* @return {{html: String}}
*/
getContent() {
// TODO - Update
return {
html: this.editor.getContent(),
};

View File

@ -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');
});
}

View File

@ -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;
}

View File

@ -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,
];
}

View File

@ -6,6 +6,10 @@
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
class="">
<div>
<button type="button" id="lexical-button">Callout</button>
</div>
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
<p>Some content here</p>
<h2>List below this h2 header</h2>