mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-22 03:51:06 +00:00
Lexical: Played with commands, extracted & improved callout node
This commit is contained in:
parent
5a4f595341
commit
6e852d2e65
7 changed files with 156 additions and 76 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -21,6 +21,7 @@
|
||||||
"@lexical/history": "^0.15.0",
|
"@lexical/history": "^0.15.0",
|
||||||
"@lexical/html": "^0.15.0",
|
"@lexical/html": "^0.15.0",
|
||||||
"@lexical/rich-text": "^0.15.0",
|
"@lexical/rich-text": "^0.15.0",
|
||||||
|
"@lexical/selection": "^0.15.0",
|
||||||
"@lexical/utils": "^0.15.0",
|
"@lexical/utils": "^0.15.0",
|
||||||
"@lezer/highlight": "^1.2.0",
|
"@lezer/highlight": "^1.2.0",
|
||||||
"@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
|
"@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"@lexical/history": "^0.15.0",
|
"@lexical/history": "^0.15.0",
|
||||||
"@lexical/html": "^0.15.0",
|
"@lexical/html": "^0.15.0",
|
||||||
"@lexical/rich-text": "^0.15.0",
|
"@lexical/rich-text": "^0.15.0",
|
||||||
|
"@lexical/selection": "^0.15.0",
|
||||||
"@lexical/utils": "^0.15.0",
|
"@lexical/utils": "^0.15.0",
|
||||||
"@lezer/highlight": "^1.2.0",
|
"@lezer/highlight": "^1.2.0",
|
||||||
"@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
|
"@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
|
||||||
|
|
|
@ -25,6 +25,7 @@ export class WysiwygEditor extends Component {
|
||||||
* @return {{html: String}}
|
* @return {{html: String}}
|
||||||
*/
|
*/
|
||||||
getContent() {
|
getContent() {
|
||||||
|
// TODO - Update
|
||||||
return {
|
return {
|
||||||
html: this.editor.getContent(),
|
html: this.editor.getContent(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {createEmptyHistoryState, registerHistory} from '@lexical/history';
|
||||||
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
|
import {registerRichText} from '@lexical/rich-text';
|
||||||
import {mergeRegister} from '@lexical/utils';
|
import {$getNearestBlockElementAncestorOrThrow, mergeRegister} from '@lexical/utils';
|
||||||
import {$generateNodesFromDOM} from '@lexical/html';
|
import {$generateNodesFromDOM} from '@lexical/html';
|
||||||
|
import {getNodesForPageEditor} from "./nodes/index.js";
|
||||||
class CalloutParagraph extends ElementNode {
|
import {$createCalloutNode, $isCalloutNode} from "./nodes/callout.js";
|
||||||
__category = 'info';
|
import {$setBlocksType} from "@lexical/selection";
|
||||||
|
|
||||||
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) {
|
export function createPageEditorInstance(editArea) {
|
||||||
console.log('creating editor', editArea);
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
namespace: 'BookStackPageEditor',
|
namespace: 'BookStackPageEditor',
|
||||||
nodes: [HeadingNode, QuoteNode, CalloutParagraph],
|
nodes: getNodesForPageEditor(),
|
||||||
onError: console.error,
|
onError: console.error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,4 +44,27 @@ export function createPageEditorInstance(editArea) {
|
||||||
console.log('editorState', editorState.toJSON());
|
console.log('editorState', editorState.toJSON());
|
||||||
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
|
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');
|
||||||
|
});
|
||||||
}
|
}
|
98
resources/js/wysiwyg/nodes/callout.js
Normal file
98
resources/js/wysiwyg/nodes/callout.js
Normal 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;
|
||||||
|
}
|
14
resources/js/wysiwyg/nodes/index.js
Normal file
14
resources/js/wysiwyg/nodes/index.js
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
|
@ -6,6 +6,10 @@
|
||||||
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
|
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
|
||||||
class="">
|
class="">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="button" id="lexical-button">Callout</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
|
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
|
||||||
<p>Some content here</p>
|
<p>Some content here</p>
|
||||||
<h2>List below this h2 header</h2>
|
<h2>List below this h2 header</h2>
|
||||||
|
|
Loading…
Add table
Reference in a new issue