mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-24 20:44:11 +00:00
Lexical: Added custom id-supporting paragraph blocks
This commit is contained in:
parent
49546cd627
commit
0f8bd869d8
6 changed files with 122 additions and 20 deletions
|
@ -5,7 +5,7 @@
|
||||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources",
|
"build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources",
|
||||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||||
"build:js:dev": "node dev/build/esbuild.js",
|
"build:js:dev": "node dev/build/esbuild.js",
|
||||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" -c \"npm run build:js:dev\"",
|
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" \"./resources/**/*.ts\" -c \"npm run build:js:dev\"",
|
||||||
"build:js:production": "node dev/build/esbuild.js production",
|
"build:js:production": "node dev/build/esbuild.js production",
|
||||||
"build": "npm-run-all --parallel build:*:dev",
|
"build": "npm-run-all --parallel build:*:dev",
|
||||||
"production": "npm-run-all --parallel build:*:production",
|
"production": "npm-run-all --parallel build:*:production",
|
||||||
|
|
|
@ -45,10 +45,6 @@ export function createPageEditorInstance(editArea: HTMLElement) {
|
||||||
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
|
// Example of creating, registering and using a custom command
|
||||||
|
|
||||||
const SET_BLOCK_CALLOUT_COMMAND = createCommand();
|
const SET_BLOCK_CALLOUT_COMMAND = createCommand();
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type SerializedCalloutNode = Spread<{
|
||||||
category: CalloutCategory;
|
category: CalloutCategory;
|
||||||
}, SerializedElementNode>
|
}, SerializedElementNode>
|
||||||
|
|
||||||
export class Callout extends ElementNode {
|
export class CalloutNode extends ElementNode {
|
||||||
|
|
||||||
__category: CalloutCategory = 'info';
|
__category: CalloutCategory = 'info';
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ export class Callout extends ElementNode {
|
||||||
return 'callout';
|
return 'callout';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: Callout) {
|
static clone(node: CalloutNode) {
|
||||||
return new Callout(node.__category, node.__key);
|
return new CalloutNode(node.__category, node.__key);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(category: CalloutCategory, key?: string) {
|
constructor(category: CalloutCategory, key?: string) {
|
||||||
|
@ -45,7 +45,7 @@ export class Callout extends ElementNode {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): Callout|ParagraphNode {
|
insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): CalloutNode|ParagraphNode {
|
||||||
const anchorOffset = selection ? selection.anchor.offset : 0;
|
const anchorOffset = selection ? selection.anchor.offset : 0;
|
||||||
const newElement = anchorOffset === this.getTextContentSize() || !selection
|
const newElement = anchorOffset === this.getTextContentSize() || !selection
|
||||||
? $createParagraphNode() : $createCalloutNode(this.__category);
|
? $createParagraphNode() : $createCalloutNode(this.__category);
|
||||||
|
@ -79,7 +79,7 @@ export class Callout extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: new Callout(category),
|
node: new CalloutNode(category),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
|
@ -99,16 +99,16 @@ export class Callout extends ElementNode {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedCalloutNode): Callout {
|
static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
|
||||||
return $createCalloutNode(serializedNode.category);
|
return $createCalloutNode(serializedNode.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createCalloutNode(category: CalloutCategory = 'info') {
|
export function $createCalloutNode(category: CalloutCategory = 'info') {
|
||||||
return new Callout(category);
|
return new CalloutNode(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isCalloutNode(node: LexicalNode | null | undefined) {
|
export function $isCalloutNode(node: LexicalNode | null | undefined) {
|
||||||
return node instanceof Callout;
|
return node instanceof CalloutNode;
|
||||||
}
|
}
|
||||||
|
|
98
resources/js/wysiwyg/nodes/custom-paragraph.ts
Normal file
98
resources/js/wysiwyg/nodes/custom-paragraph.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import {
|
||||||
|
DOMConversion,
|
||||||
|
DOMConversionMap,
|
||||||
|
DOMConversionOutput, ElementFormatType,
|
||||||
|
LexicalNode,
|
||||||
|
ParagraphNode,
|
||||||
|
SerializedParagraphNode,
|
||||||
|
Spread
|
||||||
|
} from "lexical";
|
||||||
|
import {EditorConfig} from "lexical/LexicalEditor";
|
||||||
|
|
||||||
|
|
||||||
|
export type SerializedCustomParagraphNode = Spread<{
|
||||||
|
id: string;
|
||||||
|
}, SerializedParagraphNode>
|
||||||
|
|
||||||
|
export class CustomParagraphNode extends ParagraphNode {
|
||||||
|
__id: string = '';
|
||||||
|
|
||||||
|
static getType() {
|
||||||
|
return 'custom-paragraph';
|
||||||
|
}
|
||||||
|
|
||||||
|
setId(id: string) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: CustomParagraphNode) {
|
||||||
|
const newNode = new CustomParagraphNode(node.__key);
|
||||||
|
newNode.__id = node.__id;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
|
const dom = super.createDOM(config);
|
||||||
|
const id = this.getId();
|
||||||
|
if (id) {
|
||||||
|
dom.setAttribute('id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON(): SerializedCustomParagraphNode {
|
||||||
|
return {
|
||||||
|
...super.exportJSON(),
|
||||||
|
type: 'custom-paragraph',
|
||||||
|
version: 1,
|
||||||
|
id: this.__id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedCustomParagraphNode): CustomParagraphNode {
|
||||||
|
const node = $createCustomParagraphNode();
|
||||||
|
node.setId(serializedNode.id);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM(): DOMConversionMap|null {
|
||||||
|
return {
|
||||||
|
p(node: HTMLElement): DOMConversion|null {
|
||||||
|
return {
|
||||||
|
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||||
|
const node = $createCustomParagraphNode();
|
||||||
|
if (element.style) {
|
||||||
|
node.setFormat(element.style.textAlign as ElementFormatType);
|
||||||
|
const indent = parseInt(element.style.textIndent, 10) / 20;
|
||||||
|
if (indent > 0) {
|
||||||
|
node.setIndent(indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.id) {
|
||||||
|
node.setId(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {node};
|
||||||
|
},
|
||||||
|
priority: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createCustomParagraphNode() {
|
||||||
|
return new CustomParagraphNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isCustomParagraphNode(node: LexicalNode | null | undefined) {
|
||||||
|
return node instanceof CustomParagraphNode;
|
||||||
|
}
|
|
@ -1,14 +1,22 @@
|
||||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
||||||
import {Callout} from './callout';
|
import {CalloutNode} from './callout';
|
||||||
import {KlassConstructor, LexicalNode} from "lexical";
|
import {KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
|
||||||
|
import {CustomParagraphNode} from "./custom-paragraph";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the nodes for lexical.
|
* Load the nodes for lexical.
|
||||||
*/
|
*/
|
||||||
export function getNodesForPageEditor(): KlassConstructor<typeof LexicalNode>[] {
|
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||||
return [
|
return [
|
||||||
Callout,
|
CalloutNode, // Todo - Create custom
|
||||||
HeadingNode,
|
HeadingNode, // Todo - Create custom
|
||||||
QuoteNode,
|
QuoteNode, // Todo - Create custom
|
||||||
|
CustomParagraphNode,
|
||||||
|
{
|
||||||
|
replace: ParagraphNode,
|
||||||
|
with: (node: ParagraphNode) => {
|
||||||
|
return new CustomParagraphNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
|
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
|
||||||
<p>Some content here</p>
|
<p id="Content!">Some <strong>content</strong> here</p>
|
||||||
<h2>List below this h2 header</h2>
|
<h2>List below this h2 header</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Hello</li>
|
<li>Hello</li>
|
||||||
|
|
Loading…
Add table
Reference in a new issue