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);
|
const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
|
||||||
registerCommonNodeMutationListeners(context);
|
registerCommonNodeMutationListeners(context);
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,17 @@ import {
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import type {RangeSelection} from "lexical/LexicalSelection";
|
import type {RangeSelection} from "lexical/LexicalSelection";
|
||||||
|
import {el} from "../utils/dom";
|
||||||
|
|
||||||
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
|
export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success';
|
||||||
|
|
||||||
export type SerializedCalloutNode = Spread<{
|
export type SerializedCalloutNode = Spread<{
|
||||||
category: CalloutCategory;
|
category: CalloutCategory;
|
||||||
|
id: string;
|
||||||
}, SerializedElementNode>
|
}, SerializedElementNode>
|
||||||
|
|
||||||
export class CalloutNode extends ElementNode {
|
export class CalloutNode extends ElementNode {
|
||||||
|
__id: string = '';
|
||||||
__category: CalloutCategory = 'info';
|
__category: CalloutCategory = 'info';
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -25,7 +27,9 @@ export class CalloutNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: CalloutNode) {
|
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) {
|
constructor(category: CalloutCategory, key?: string) {
|
||||||
|
@ -43,9 +47,22 @@ export class CalloutNode extends ElementNode {
|
||||||
return self.__category;
|
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) {
|
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||||
const element = document.createElement('p');
|
const element = document.createElement('p');
|
||||||
element.classList.add('callout', this.__category || '');
|
element.classList.add('callout', this.__category || '');
|
||||||
|
if (this.__id) {
|
||||||
|
element.setAttribute('id', this.__id);
|
||||||
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +105,13 @@ export class CalloutNode extends ElementNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const node = new CalloutNode(category);
|
||||||
|
if (element.id) {
|
||||||
|
node.setId(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: new CalloutNode(category),
|
node,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
|
@ -106,11 +128,14 @@ export class CalloutNode extends ElementNode {
|
||||||
type: 'callout',
|
type: 'callout',
|
||||||
version: 1,
|
version: 1,
|
||||||
category: this.__category,
|
category: this.__category,
|
||||||
|
id: this.__id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
|
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);
|
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;
|
return node instanceof CalloutNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
DecoratorNode,
|
DecoratorNode,
|
||||||
DOMConversion,
|
DOMConversion,
|
||||||
DOMConversionMap,
|
DOMConversionMap,
|
||||||
DOMConversionOutput,
|
DOMConversionOutput, DOMExportOutput,
|
||||||
LexicalEditor, LexicalNode,
|
LexicalEditor, LexicalNode,
|
||||||
SerializedLexicalNode,
|
SerializedLexicalNode,
|
||||||
Spread
|
Spread
|
||||||
|
@ -33,7 +33,9 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: CodeBlockNode): CodeBlockNode {
|
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) {
|
constructor(language: string = '', code: string = '', key?: string) {
|
||||||
|
@ -118,6 +120,13 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||||
|
const dom = this.createDOM(editor._config, editor);
|
||||||
|
return {
|
||||||
|
element: dom.querySelector('pre') as HTMLElement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap|null {
|
static importDOM(): DOMConversionMap|null {
|
||||||
return {
|
return {
|
||||||
pre(node: HTMLElement): DOMConversion|null {
|
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 code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
|
||||||
|
const node = $createCodeBlockNode(language, code);
|
||||||
|
|
||||||
return {
|
if (element.id) {
|
||||||
node: $createCodeBlockNode(language, code),
|
node.setId(element.id);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return { node };
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
return self.__id;
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: CustomParagraphNode) {
|
static clone(node: CustomParagraphNode): CustomParagraphNode {
|
||||||
const newNode = new CustomParagraphNode(node.__key);
|
const newNode = new CustomParagraphNode(node.__key);
|
||||||
newNode.__id = node.__id;
|
newNode.__id = node.__id;
|
||||||
return newNode;
|
return newNode;
|
||||||
|
@ -39,9 +39,8 @@ export class CustomParagraphNode extends ParagraphNode {
|
||||||
|
|
||||||
createDOM(config: EditorConfig): HTMLElement {
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
const dom = super.createDOM(config);
|
const dom = super.createDOM(config);
|
||||||
const id = this.getId();
|
if (this.__id) {
|
||||||
if (id) {
|
dom.setAttribute('id', this.__id);
|
||||||
dom.setAttribute('id', id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dom;
|
return dom;
|
||||||
|
@ -89,7 +88,7 @@ export class CustomParagraphNode extends ParagraphNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createCustomParagraphNode() {
|
export function $createCustomParagraphNode(): CustomParagraphNode {
|
||||||
return new CustomParagraphNode();
|
return new CustomParagraphNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
ElementNode,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
SerializedElementNode,
|
SerializedElementNode, Spread,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
|
|
||||||
import {el} from "../utils/dom";
|
import {el} from "../utils/dom";
|
||||||
|
|
||||||
|
export type SerializedDetailsNode = Spread<{
|
||||||
|
id: string;
|
||||||
|
}, SerializedElementNode>
|
||||||
|
|
||||||
export class DetailsNode extends ElementNode {
|
export class DetailsNode extends ElementNode {
|
||||||
|
__id: string = '';
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
return 'details';
|
return 'details';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: DetailsNode) {
|
setId(id: string) {
|
||||||
return new DetailsNode(node.__key);
|
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) {
|
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) {
|
updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
|
||||||
return false;
|
return prevNode.__id !== this.__id;
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap|null {
|
static importDOM(): DOMConversionMap|null {
|
||||||
|
@ -33,9 +55,12 @@ export class DetailsNode extends ElementNode {
|
||||||
details(node: HTMLElement): DOMConversion|null {
|
details(node: HTMLElement): DOMConversion|null {
|
||||||
return {
|
return {
|
||||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||||
return {
|
const node = new DetailsNode();
|
||||||
node: new DetailsNode(),
|
if (element.id) {
|
||||||
};
|
node.setId(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {node};
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
};
|
};
|
||||||
|
@ -43,16 +68,19 @@ export class DetailsNode extends ElementNode {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedElementNode {
|
exportJSON(): SerializedDetailsNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: 'details',
|
type: 'details',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: this.__id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedElementNode): DetailsNode {
|
static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {
|
||||||
return $createDetailsNode();
|
const node = $createDetailsNode();
|
||||||
|
node.setId(serializedNode.id);
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -61,7 +89,7 @@ export function $createDetailsNode() {
|
||||||
return new DetailsNode();
|
return new DetailsNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isDetailsNode(node: LexicalNode | null | undefined) {
|
export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
|
||||||
return node instanceof 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();
|
return $createSummaryNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createSummaryNode() {
|
export function $createSummaryNode(): SummaryNode {
|
||||||
return new 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;
|
return node instanceof SummaryNode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,9 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: DiagramNode): DiagramNode {
|
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) {
|
constructor(drawingId: string, drawingUrl: string, key?: string) {
|
||||||
|
@ -120,10 +122,13 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||||
const img = element.querySelector('img');
|
const img = element.querySelector('img');
|
||||||
const drawingUrl = img?.getAttribute('src') || '';
|
const drawingUrl = img?.getAttribute('src') || '';
|
||||||
const drawingId = element.getAttribute('drawio-diagram') || '';
|
const drawingId = element.getAttribute('drawio-diagram') || '';
|
||||||
|
const node = $createDiagramNode(drawingId, drawingUrl);
|
||||||
|
|
||||||
return {
|
if (element.id) {
|
||||||
node: $createDiagramNode(drawingId, drawingUrl),
|
node.setId(element.id);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return { node };
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
};
|
};
|
||||||
|
@ -152,7 +157,7 @@ export function $createDiagramNode(drawingId: string = '', drawingUrl: string =
|
||||||
return new DiagramNode(drawingId, drawingUrl);
|
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;
|
return node instanceof DiagramNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,26 +4,48 @@ import {
|
||||||
ElementNode,
|
ElementNode,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
SerializedElementNode,
|
SerializedElementNode, Spread,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
|
|
||||||
|
export type SerializedHorizontalRuleNode = Spread<{
|
||||||
|
id: string;
|
||||||
|
}, SerializedElementNode>
|
||||||
|
|
||||||
export class HorizontalRuleNode extends ElementNode {
|
export class HorizontalRuleNode extends ElementNode {
|
||||||
|
__id: string = '';
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
return 'horizontal-rule';
|
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 {
|
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) {
|
createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
|
||||||
return document.createElement('hr');
|
const el = document.createElement('hr');
|
||||||
|
if (this.__id) {
|
||||||
|
el.setAttribute('id', this.__id);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(prevNode: unknown, dom: HTMLElement) {
|
return el;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
updateDOM(prevNode: HorizontalRuleNode, dom: HTMLElement) {
|
||||||
|
return prevNode.__id !== this.__id;
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap|null {
|
static importDOM(): DOMConversionMap|null {
|
||||||
|
@ -31,9 +53,12 @@ export class HorizontalRuleNode extends ElementNode {
|
||||||
hr(node: HTMLElement): DOMConversion|null {
|
hr(node: HTMLElement): DOMConversion|null {
|
||||||
return {
|
return {
|
||||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||||
return {
|
const node = new HorizontalRuleNode();
|
||||||
node: new HorizontalRuleNode(),
|
if (element.id) {
|
||||||
};
|
node.setId(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {node};
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
};
|
};
|
||||||
|
@ -41,24 +66,27 @@ export class HorizontalRuleNode extends ElementNode {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedElementNode {
|
exportJSON(): SerializedHorizontalRuleNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: 'horizontal-rule',
|
type: 'horizontal-rule',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: this.__id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedElementNode): HorizontalRuleNode {
|
static importJSON(serializedNode: SerializedHorizontalRuleNode): HorizontalRuleNode {
|
||||||
return $createHorizontalRuleNode();
|
const node = $createHorizontalRuleNode();
|
||||||
|
node.setId(serializedNode.id);
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createHorizontalRuleNode() {
|
export function $createHorizontalRuleNode(): HorizontalRuleNode {
|
||||||
return new 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;
|
return node instanceof HorizontalRuleNode;
|
||||||
}
|
}
|
|
@ -22,16 +22,19 @@ import {MediaNode} from "./media";
|
||||||
import {CustomListItemNode} from "./custom-list-item";
|
import {CustomListItemNode} from "./custom-list-item";
|
||||||
import {CustomTableCellNode} from "./custom-table-cell";
|
import {CustomTableCellNode} from "./custom-table-cell";
|
||||||
import {CustomTableRowNode} from "./custom-table-row";
|
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.
|
* Load the nodes for lexical.
|
||||||
*/
|
*/
|
||||||
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||||
return [
|
return [
|
||||||
CalloutNode, // Todo - Create custom
|
CalloutNode,
|
||||||
HeadingNode, // Todo - Create custom
|
CustomHeadingNode,
|
||||||
QuoteNode, // Todo - Create custom
|
CustomQuoteNode,
|
||||||
ListNode, // Todo - Create custom
|
CustomListNode,
|
||||||
CustomListItemNode,
|
CustomListItemNode,
|
||||||
CustomTableNode,
|
CustomTableNode,
|
||||||
CustomTableRowNode,
|
CustomTableRowNode,
|
||||||
|
@ -42,7 +45,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||||
CodeBlockNode,
|
CodeBlockNode,
|
||||||
DiagramNode,
|
DiagramNode,
|
||||||
MediaNode,
|
MediaNode,
|
||||||
CustomParagraphNode,
|
CustomParagraphNode, // TODO - ID
|
||||||
LinkNode,
|
LinkNode,
|
||||||
{
|
{
|
||||||
replace: ParagraphNode,
|
replace: ParagraphNode,
|
||||||
|
@ -50,6 +53,24 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||||
return new CustomParagraphNode();
|
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,
|
replace: ListItemNode,
|
||||||
with: (node: ListItemNode) => {
|
with: (node: ListItemNode) => {
|
||||||
|
|
|
@ -66,7 +66,6 @@ function domElementToNode(tag: MediaNodeTag, element: Element): MediaNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MediaNode extends ElementNode {
|
export class MediaNode extends ElementNode {
|
||||||
|
|
||||||
__tag: MediaNodeTag;
|
__tag: MediaNodeTag;
|
||||||
__attributes: Record<string, string> = {};
|
__attributes: Record<string, string> = {};
|
||||||
__sources: MediaNodeSource[] = [];
|
__sources: MediaNodeSource[] = [];
|
||||||
|
@ -76,7 +75,10 @@ export class MediaNode extends ElementNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: MediaNode) {
|
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) {
|
constructor(tag: MediaNodeTag, key?: string) {
|
||||||
|
@ -226,10 +228,10 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
|
||||||
return new MediaNode(nodeTag);
|
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;
|
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;
|
return node instanceof MediaNode && (node as MediaNode).getTag() === tag;
|
||||||
}
|
}
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
- Alignments: Use existing classes for blocks (including table cells)
|
- Alignments: Use existing classes for blocks (including table cells)
|
||||||
- Alignments: Handle inline block content (image, video)
|
- Alignments: Handle inline block content (image, video)
|
||||||
- Image paste upload
|
- Image paste upload
|
||||||
- Keyboard shortcuts support
|
- Keyboard shortcuts support
|
||||||
- Add ID support to all block types
|
|
||||||
- Link popup menu for cross-content reference
|
- Link popup menu for cross-content reference
|
||||||
- Link heading-based ID reference menu
|
- Link heading-based ID reference menu
|
||||||
- Image gallery integration for insert
|
- Image gallery integration for insert
|
||||||
|
|
Loading…
Reference in New Issue