mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
Lexical: Added id support for all main block types
This commit is contained in:
parent
ebf95f637a
commit
ec965f28c0
@ -82,6 +82,11 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.debugEditorState = () => {
|
||||
console.log(editor.getEditorState().toJSON());
|
||||
};
|
||||
|
||||
const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
|
||||
registerCommonNodeMutationListeners(context);
|
||||
|
||||
|
@ -9,15 +9,17 @@ import {
|
||||
} from 'lexical';
|
||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||
import type {RangeSelection} from "lexical/LexicalSelection";
|
||||
import {el} from "../utils/dom";
|
||||
|
||||
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
|
||||
|
||||
export type SerializedCalloutNode = Spread<{
|
||||
category: CalloutCategory;
|
||||
id: string;
|
||||
}, SerializedElementNode>
|
||||
|
||||
export class CalloutNode extends ElementNode {
|
||||
|
||||
__id: string = '';
|
||||
__category: CalloutCategory = 'info';
|
||||
|
||||
static getType() {
|
||||
@ -25,7 +27,9 @@ export class CalloutNode extends ElementNode {
|
||||
}
|
||||
|
||||
static clone(node: CalloutNode) {
|
||||
return new CalloutNode(node.__category, node.__key);
|
||||
const newNode = new CalloutNode(node.__category, node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
constructor(category: CalloutCategory, key?: string) {
|
||||
@ -43,9 +47,22 @@ export class CalloutNode extends ElementNode {
|
||||
return self.__category;
|
||||
}
|
||||
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||
const element = document.createElement('p');
|
||||
element.classList.add('callout', this.__category || '');
|
||||
if (this.__id) {
|
||||
element.setAttribute('id', this.__id);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
@ -88,8 +105,13 @@ export class CalloutNode extends ElementNode {
|
||||
}
|
||||
}
|
||||
|
||||
const node = new CalloutNode(category);
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
|
||||
return {
|
||||
node: new CalloutNode(category),
|
||||
node,
|
||||
};
|
||||
},
|
||||
priority: 3,
|
||||
@ -106,11 +128,14 @@ export class CalloutNode extends ElementNode {
|
||||
type: 'callout',
|
||||
version: 1,
|
||||
category: this.__category,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
|
||||
return $createCalloutNode(serializedNode.category);
|
||||
const node = $createCalloutNode(serializedNode.category);
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
}
|
||||
@ -119,7 +144,7 @@ export function $createCalloutNode(category: CalloutCategory = 'info') {
|
||||
return new CalloutNode(category);
|
||||
}
|
||||
|
||||
export function $isCalloutNode(node: LexicalNode | null | undefined) {
|
||||
export function $isCalloutNode(node: LexicalNode | null | undefined): node is CalloutNode {
|
||||
return node instanceof CalloutNode;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
DecoratorNode,
|
||||
DOMConversion,
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMConversionOutput, DOMExportOutput,
|
||||
LexicalEditor, LexicalNode,
|
||||
SerializedLexicalNode,
|
||||
Spread
|
||||
@ -33,7 +33,9 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
}
|
||||
|
||||
static clone(node: CodeBlockNode): CodeBlockNode {
|
||||
return new CodeBlockNode(node.__language, node.__code);
|
||||
const newNode = new CodeBlockNode(node.__language, node.__code);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
constructor(language: string = '', code: string = '', key?: string) {
|
||||
@ -118,6 +120,13 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
return false;
|
||||
}
|
||||
|
||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||
const dom = this.createDOM(editor._config, editor);
|
||||
return {
|
||||
element: dom.querySelector('pre') as HTMLElement,
|
||||
};
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
return {
|
||||
pre(node: HTMLElement): DOMConversion|null {
|
||||
@ -130,10 +139,13 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
|| '';
|
||||
|
||||
const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
|
||||
const node = $createCodeBlockNode(language, code);
|
||||
|
||||
return {
|
||||
node: $createCodeBlockNode(language, code),
|
||||
};
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
|
||||
return { node };
|
||||
},
|
||||
priority: 3,
|
||||
};
|
||||
|
120
resources/js/wysiwyg/nodes/custom-heading.ts
Normal file
120
resources/js/wysiwyg/nodes/custom-heading.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput, ElementFormatType,
|
||||
LexicalNode,
|
||||
Spread
|
||||
} from "lexical";
|
||||
import {EditorConfig} from "lexical/LexicalEditor";
|
||||
import {HeadingNode, HeadingTagType, SerializedHeadingNode} from "@lexical/rich-text";
|
||||
|
||||
|
||||
export type SerializedCustomHeadingNode = Spread<{
|
||||
id: string;
|
||||
}, SerializedHeadingNode>
|
||||
|
||||
export class CustomHeadingNode extends HeadingNode {
|
||||
__id: string = '';
|
||||
|
||||
static getType() {
|
||||
return 'custom-heading';
|
||||
}
|
||||
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: CustomHeadingNode) {
|
||||
const newNode = new CustomHeadingNode(node.__tag, node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
if (this.__id) {
|
||||
dom.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCustomHeadingNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'custom-heading',
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedCustomHeadingNode): CustomHeadingNode {
|
||||
const node = $createCustomHeadingNode(serializedNode.tag);
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
h1: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
h2: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
h3: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
h4: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
h5: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
h6: (node: Node) => ({
|
||||
conversion: $convertHeadingElement,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
|
||||
const nodeName = element.nodeName.toLowerCase();
|
||||
let node = null;
|
||||
if (
|
||||
nodeName === 'h1' ||
|
||||
nodeName === 'h2' ||
|
||||
nodeName === 'h3' ||
|
||||
nodeName === 'h4' ||
|
||||
nodeName === 'h5' ||
|
||||
nodeName === 'h6'
|
||||
) {
|
||||
node = $createCustomHeadingNode(nodeName);
|
||||
if (element.style !== null) {
|
||||
node.setFormat(element.style.textAlign as ElementFormatType);
|
||||
}
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
}
|
||||
return {node};
|
||||
}
|
||||
|
||||
export function $createCustomHeadingNode(tag: HeadingTagType) {
|
||||
return new CustomHeadingNode(tag);
|
||||
}
|
||||
|
||||
export function $isCustomHeadingNode(node: LexicalNode | null | undefined): node is CustomHeadingNode {
|
||||
return node instanceof CustomHeadingNode;
|
||||
}
|
92
resources/js/wysiwyg/nodes/custom-list.ts
Normal file
92
resources/js/wysiwyg/nodes/custom-list.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import {
|
||||
DOMConversionFn,
|
||||
DOMConversionMap,
|
||||
LexicalNode,
|
||||
Spread
|
||||
} from "lexical";
|
||||
import {EditorConfig} from "lexical/LexicalEditor";
|
||||
import {ListNode, ListType, SerializedListNode} from "@lexical/list";
|
||||
|
||||
|
||||
export type SerializedCustomListNode = Spread<{
|
||||
id: string;
|
||||
}, SerializedListNode>
|
||||
|
||||
export class CustomListNode extends ListNode {
|
||||
__id: string = '';
|
||||
|
||||
static getType() {
|
||||
return 'custom-list';
|
||||
}
|
||||
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: CustomListNode) {
|
||||
const newNode = new CustomListNode(node.__listType, 0, node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
if (this.__id) {
|
||||
dom.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCustomListNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'custom-list',
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedCustomListNode): CustomListNode {
|
||||
const node = $createCustomListNode(serializedNode.listType);
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
// @ts-ignore
|
||||
const converter = super.importDOM().ol().conversion as DOMConversionFn<HTMLElement>;
|
||||
const customConvertFunction = (element: HTMLElement) => {
|
||||
const baseResult = converter(element);
|
||||
if (element.id && baseResult?.node) {
|
||||
(baseResult.node as CustomListNode).setId(element.id);
|
||||
}
|
||||
return baseResult;
|
||||
};
|
||||
|
||||
return {
|
||||
ol: () => ({
|
||||
conversion: customConvertFunction,
|
||||
priority: 0,
|
||||
}),
|
||||
ul: () => ({
|
||||
conversion: customConvertFunction,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function $createCustomListNode(type: ListType): CustomListNode {
|
||||
return new CustomListNode(type, 0);
|
||||
}
|
||||
|
||||
export function $isCustomListNode(node: LexicalNode | null | undefined): node is CustomListNode {
|
||||
return node instanceof CustomListNode;
|
||||
}
|
@ -31,7 +31,7 @@ export class CustomParagraphNode extends ParagraphNode {
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: CustomParagraphNode) {
|
||||
static clone(node: CustomParagraphNode): CustomParagraphNode {
|
||||
const newNode = new CustomParagraphNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
@ -39,9 +39,8 @@ export class CustomParagraphNode extends ParagraphNode {
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
const id = this.getId();
|
||||
if (id) {
|
||||
dom.setAttribute('id', id);
|
||||
if (this.__id) {
|
||||
dom.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return dom;
|
||||
@ -89,7 +88,7 @@ export class CustomParagraphNode extends ParagraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
export function $createCustomParagraphNode() {
|
||||
export function $createCustomParagraphNode(): CustomParagraphNode {
|
||||
return new CustomParagraphNode();
|
||||
}
|
||||
|
||||
|
89
resources/js/wysiwyg/nodes/custom-quote.ts
Normal file
89
resources/js/wysiwyg/nodes/custom-quote.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput, ElementFormatType,
|
||||
LexicalNode,
|
||||
Spread
|
||||
} from "lexical";
|
||||
import {EditorConfig} from "lexical/LexicalEditor";
|
||||
import {QuoteNode, SerializedQuoteNode} from "@lexical/rich-text";
|
||||
|
||||
|
||||
export type SerializedCustomQuoteNode = Spread<{
|
||||
id: string;
|
||||
}, SerializedQuoteNode>
|
||||
|
||||
export class CustomQuoteNode extends QuoteNode {
|
||||
__id: string = '';
|
||||
|
||||
static getType() {
|
||||
return 'custom-quote';
|
||||
}
|
||||
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: CustomQuoteNode) {
|
||||
const newNode = new CustomQuoteNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = super.createDOM(config);
|
||||
if (this.__id) {
|
||||
dom.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCustomQuoteNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'custom-quote',
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedCustomQuoteNode): CustomQuoteNode {
|
||||
const node = $createCustomQuoteNode();
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
blockquote: (node: Node) => ({
|
||||
conversion: $convertBlockquoteElement,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
|
||||
const node = $createCustomQuoteNode();
|
||||
if (element.style !== null) {
|
||||
node.setFormat(element.style.textAlign as ElementFormatType);
|
||||
}
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
return {node};
|
||||
}
|
||||
|
||||
export function $createCustomQuoteNode() {
|
||||
return new CustomQuoteNode();
|
||||
}
|
||||
|
||||
export function $isCustomQuoteNode(node: LexicalNode | null | undefined): node is CustomQuoteNode {
|
||||
return node instanceof CustomQuoteNode;
|
||||
}
|
@ -4,28 +4,50 @@ import {
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
SerializedElementNode,
|
||||
SerializedElementNode, Spread,
|
||||
} from 'lexical';
|
||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||
|
||||
import {el} from "../utils/dom";
|
||||
|
||||
export type SerializedDetailsNode = Spread<{
|
||||
id: string;
|
||||
}, SerializedElementNode>
|
||||
|
||||
export class DetailsNode extends ElementNode {
|
||||
__id: string = '';
|
||||
|
||||
static getType() {
|
||||
return 'details';
|
||||
}
|
||||
|
||||
static clone(node: DetailsNode) {
|
||||
return new DetailsNode(node.__key);
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: DetailsNode): DetailsNode {
|
||||
const newNode = new DetailsNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||
return el('details');
|
||||
const el = document.createElement('details');
|
||||
if (this.__id) {
|
||||
el.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
|
||||
return false;
|
||||
return prevNode.__id !== this.__id;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
@ -33,9 +55,12 @@ export class DetailsNode extends ElementNode {
|
||||
details(node: HTMLElement): DOMConversion|null {
|
||||
return {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||
return {
|
||||
node: new DetailsNode(),
|
||||
};
|
||||
const node = new DetailsNode();
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
|
||||
return {node};
|
||||
},
|
||||
priority: 3,
|
||||
};
|
||||
@ -43,16 +68,19 @@ export class DetailsNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
exportJSON(): SerializedDetailsNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'details',
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedElementNode): DetailsNode {
|
||||
return $createDetailsNode();
|
||||
static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {
|
||||
const node = $createDetailsNode();
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
}
|
||||
@ -61,7 +89,7 @@ export function $createDetailsNode() {
|
||||
return new DetailsNode();
|
||||
}
|
||||
|
||||
export function $isDetailsNode(node: LexicalNode | null | undefined) {
|
||||
export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
|
||||
return node instanceof DetailsNode;
|
||||
}
|
||||
|
||||
@ -106,16 +134,16 @@ export class SummaryNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedElementNode): DetailsNode {
|
||||
static importJSON(serializedNode: SerializedElementNode): SummaryNode {
|
||||
return $createSummaryNode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function $createSummaryNode() {
|
||||
export function $createSummaryNode(): SummaryNode {
|
||||
return new SummaryNode();
|
||||
}
|
||||
|
||||
export function $isSummaryNode(node: LexicalNode | null | undefined) {
|
||||
export function $isSummaryNode(node: LexicalNode | null | undefined): node is SummaryNode {
|
||||
return node instanceof SummaryNode;
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
}
|
||||
|
||||
static clone(node: DiagramNode): DiagramNode {
|
||||
return new DiagramNode(node.__drawingId, node.__drawingUrl);
|
||||
const newNode = new DiagramNode(node.__drawingId, node.__drawingUrl);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
constructor(drawingId: string, drawingUrl: string, key?: string) {
|
||||
@ -120,10 +122,13 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
const img = element.querySelector('img');
|
||||
const drawingUrl = img?.getAttribute('src') || '';
|
||||
const drawingId = element.getAttribute('drawio-diagram') || '';
|
||||
const node = $createDiagramNode(drawingId, drawingUrl);
|
||||
|
||||
return {
|
||||
node: $createDiagramNode(drawingId, drawingUrl),
|
||||
};
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
|
||||
return { node };
|
||||
},
|
||||
priority: 3,
|
||||
};
|
||||
@ -152,7 +157,7 @@ export function $createDiagramNode(drawingId: string = '', drawingUrl: string =
|
||||
return new DiagramNode(drawingId, drawingUrl);
|
||||
}
|
||||
|
||||
export function $isDiagramNode(node: LexicalNode | null | undefined) {
|
||||
export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
|
||||
return node instanceof DiagramNode;
|
||||
}
|
||||
|
||||
|
@ -4,26 +4,48 @@ import {
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
SerializedElementNode,
|
||||
SerializedElementNode, Spread,
|
||||
} from 'lexical';
|
||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||
|
||||
export type SerializedHorizontalRuleNode = Spread<{
|
||||
id: string;
|
||||
}, SerializedElementNode>
|
||||
|
||||
export class HorizontalRuleNode extends ElementNode {
|
||||
__id: string = '';
|
||||
|
||||
static getType() {
|
||||
return 'horizontal-rule';
|
||||
}
|
||||
|
||||
setId(id: string) {
|
||||
const self = this.getWritable();
|
||||
self.__id = id;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const self = this.getLatest();
|
||||
return self.__id;
|
||||
}
|
||||
|
||||
static clone(node: HorizontalRuleNode): HorizontalRuleNode {
|
||||
return new HorizontalRuleNode(node.__key);
|
||||
const newNode = new HorizontalRuleNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||
return document.createElement('hr');
|
||||
createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
|
||||
const el = document.createElement('hr');
|
||||
if (this.__id) {
|
||||
el.setAttribute('id', this.__id);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: unknown, dom: HTMLElement) {
|
||||
return false;
|
||||
updateDOM(prevNode: HorizontalRuleNode, dom: HTMLElement) {
|
||||
return prevNode.__id !== this.__id;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
@ -31,9 +53,12 @@ export class HorizontalRuleNode extends ElementNode {
|
||||
hr(node: HTMLElement): DOMConversion|null {
|
||||
return {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||
return {
|
||||
node: new HorizontalRuleNode(),
|
||||
};
|
||||
const node = new HorizontalRuleNode();
|
||||
if (element.id) {
|
||||
node.setId(element.id);
|
||||
}
|
||||
|
||||
return {node};
|
||||
},
|
||||
priority: 3,
|
||||
};
|
||||
@ -41,24 +66,27 @@ export class HorizontalRuleNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
exportJSON(): SerializedElementNode {
|
||||
exportJSON(): SerializedHorizontalRuleNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'horizontal-rule',
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedElementNode): HorizontalRuleNode {
|
||||
return $createHorizontalRuleNode();
|
||||
static importJSON(serializedNode: SerializedHorizontalRuleNode): HorizontalRuleNode {
|
||||
const node = $createHorizontalRuleNode();
|
||||
node.setId(serializedNode.id);
|
||||
return node;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function $createHorizontalRuleNode() {
|
||||
export function $createHorizontalRuleNode(): HorizontalRuleNode {
|
||||
return new HorizontalRuleNode();
|
||||
}
|
||||
|
||||
export function $isHorizontalRuleNode(node: LexicalNode | null | undefined) {
|
||||
export function $isHorizontalRuleNode(node: LexicalNode | null | undefined): node is HorizontalRuleNode {
|
||||
return node instanceof HorizontalRuleNode;
|
||||
}
|
@ -22,16 +22,19 @@ import {MediaNode} from "./media";
|
||||
import {CustomListItemNode} from "./custom-list-item";
|
||||
import {CustomTableCellNode} from "./custom-table-cell";
|
||||
import {CustomTableRowNode} from "./custom-table-row";
|
||||
import {CustomHeadingNode} from "./custom-heading";
|
||||
import {CustomQuoteNode} from "./custom-quote";
|
||||
import {CustomListNode} from "./custom-list";
|
||||
|
||||
/**
|
||||
* Load the nodes for lexical.
|
||||
*/
|
||||
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||
return [
|
||||
CalloutNode, // Todo - Create custom
|
||||
HeadingNode, // Todo - Create custom
|
||||
QuoteNode, // Todo - Create custom
|
||||
ListNode, // Todo - Create custom
|
||||
CalloutNode,
|
||||
CustomHeadingNode,
|
||||
CustomQuoteNode,
|
||||
CustomListNode,
|
||||
CustomListItemNode,
|
||||
CustomTableNode,
|
||||
CustomTableRowNode,
|
||||
@ -42,7 +45,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||
CodeBlockNode,
|
||||
DiagramNode,
|
||||
MediaNode,
|
||||
CustomParagraphNode,
|
||||
CustomParagraphNode, // TODO - ID
|
||||
LinkNode,
|
||||
{
|
||||
replace: ParagraphNode,
|
||||
@ -50,6 +53,24 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||
return new CustomParagraphNode();
|
||||
}
|
||||
},
|
||||
{
|
||||
replace: HeadingNode,
|
||||
with: (node: HeadingNode) => {
|
||||
return new CustomHeadingNode(node.__tag);
|
||||
}
|
||||
},
|
||||
{
|
||||
replace: QuoteNode,
|
||||
with: (node: QuoteNode) => {
|
||||
return new CustomQuoteNode();
|
||||
}
|
||||
},
|
||||
{
|
||||
replace: ListNode,
|
||||
with: (node: ListNode) => {
|
||||
return new CustomListNode(node.getListType(), node.getStart());
|
||||
}
|
||||
},
|
||||
{
|
||||
replace: ListItemNode,
|
||||
with: (node: ListItemNode) => {
|
||||
|
@ -66,7 +66,6 @@ function domElementToNode(tag: MediaNodeTag, element: Element): MediaNode {
|
||||
}
|
||||
|
||||
export class MediaNode extends ElementNode {
|
||||
|
||||
__tag: MediaNodeTag;
|
||||
__attributes: Record<string, string> = {};
|
||||
__sources: MediaNodeSource[] = [];
|
||||
@ -76,7 +75,10 @@ export class MediaNode extends ElementNode {
|
||||
}
|
||||
|
||||
static clone(node: MediaNode) {
|
||||
return new MediaNode(node.__tag, node.__key);
|
||||
const newNode = new MediaNode(node.__tag, node.__key);
|
||||
newNode.__attributes = Object.assign({}, node.__attributes);
|
||||
newNode.__sources = node.__sources.map(s => Object.assign({}, s));
|
||||
return newNode;
|
||||
}
|
||||
|
||||
constructor(tag: MediaNodeTag, key?: string) {
|
||||
@ -226,10 +228,10 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
|
||||
return new MediaNode(nodeTag);
|
||||
}
|
||||
|
||||
export function $isMediaNode(node: LexicalNode | null | undefined) {
|
||||
export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {
|
||||
return node instanceof MediaNode;
|
||||
}
|
||||
|
||||
export function $isMediaNodeOfTag(node: LexicalNode | null | undefined, tag: MediaNodeTag) {
|
||||
export function $isMediaNodeOfTag(node: LexicalNode | null | undefined, tag: MediaNodeTag): boolean {
|
||||
return node instanceof MediaNode && (node as MediaNode).getTag() === tag;
|
||||
}
|
@ -2,13 +2,14 @@
|
||||
|
||||
## In progress
|
||||
|
||||
//
|
||||
|
||||
## Main Todo
|
||||
|
||||
- Alignments: Use existing classes for blocks (including table cells)
|
||||
- Alignments: Handle inline block content (image, video)
|
||||
- Image paste upload
|
||||
- Keyboard shortcuts support
|
||||
- Add ID support to all block types
|
||||
- Link popup menu for cross-content reference
|
||||
- Link heading-based ID reference menu
|
||||
- Image gallery integration for insert
|
||||
|
Loading…
Reference in New Issue
Block a user