}
});
+ // @ts-ignore
+ window.debugEditorState = () => {
+ console.log(editor.getEditorState().toJSON());
+ };
+
const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
registerCommonNodeMutationListeners(context);
} 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() {
}
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) {
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;
}
}
}
+ const node = new CalloutNode(category);
+ if (element.id) {
+ node.setId(element.id);
+ }
+
return {
- node: new CalloutNode(category),
+ node,
};
},
priority: 3,
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;
}
}
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;
}
DecoratorNode,
DOMConversion,
DOMConversionMap,
- DOMConversionOutput,
+ DOMConversionOutput, DOMExportOutput,
LexicalEditor, LexicalNode,
SerializedLexicalNode,
Spread
}
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) {
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 {
|| '';
const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
+ const node = $createCodeBlockNode(language, code);
+
+ if (element.id) {
+ node.setId(element.id);
+ }
- return {
- node: $createCodeBlockNode(language, code),
- };
+ return { node };
},
priority: 3,
};
--- /dev/null
+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;
+}
\ No newline at end of file
--- /dev/null
+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;
+}
\ No newline at end of file
return self.__id;
}
- static clone(node: CustomParagraphNode) {
+ static clone(node: CustomParagraphNode): 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);
+ if (this.__id) {
+ dom.setAttribute('id', this.__id);
}
return dom;
}
}
-export function $createCustomParagraphNode() {
+export function $createCustomParagraphNode(): CustomParagraphNode {
return new CustomParagraphNode();
}
--- /dev/null
+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;
+}
\ No newline at end of file
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 {
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,
};
};
}
- 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;
}
}
return new DetailsNode();
}
-export function $isDetailsNode(node: LexicalNode | null | undefined) {
+export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
return node instanceof DetailsNode;
}
};
}
- 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;
}
}
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) {
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,
};
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;
}
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 {
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,
};
};
}
- 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;
}
\ No newline at end of file
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,
CodeBlockNode,
DiagramNode,
MediaNode,
- CustomParagraphNode,
+ CustomParagraphNode, // TODO - ID
LinkNode,
{
replace: ParagraphNode,
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) => {
}
export class MediaNode extends ElementNode {
-
__tag: MediaNodeTag;
__attributes: Record<string, string> = {};
__sources: MediaNodeSource[] = [];
}
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) {
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;
}
\ No newline at end of file
## 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