"@codemirror/view": "^6.22.2",
"@lexical/history": "^0.15.0",
"@lexical/html": "^0.15.0",
+ "@lexical/link": "^0.15.0",
"@lexical/rich-text": "^0.15.0",
"@lexical/selection": "^0.15.0",
"@lexical/utils": "^0.15.0",
"lexical": "0.15.0"
}
},
+ "node_modules/@lexical/link": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.15.0.tgz",
+ "integrity": "sha512-KBV/zWk5FxqZGNcq3IKGBDCcS4t0uteU1osAIG+pefo4waTkOOgibxxEJDop2QR5wtjkYva3Qp0D8ZyJDMMMlw==",
+ "dependencies": {
+ "@lexical/utils": "0.15.0",
+ "lexical": "0.15.0"
+ }
+ },
"node_modules/@lexical/list": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.15.0.tgz",
"@codemirror/view": "^6.22.2",
"@lexical/history": "^0.15.0",
"@lexical/html": "^0.15.0",
+ "@lexical/link": "^0.15.0",
"@lexical/rich-text": "^0.15.0",
"@lexical/selection": "^0.15.0",
"@lexical/utils": "^0.15.0",
$getSelection,
$isTextNode,
BaseSelection,
- ElementFormatType,
LexicalEditor, TextFormatType
} from "lexical";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {$setBlocksType} from "@lexical/selection";
-import {TextNodeThemeClasses} from "lexical/LexicalEditor";
+
+export function el(tag: string, attrs: Record<string, string> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
+ const el = document.createElement(tag);
+ const attrKeys = Object.keys(attrs);
+ for (const attr of attrKeys) {
+ el.setAttribute(attr, attrs[attr]);
+ }
+
+ for (const child of children) {
+ if (typeof child === 'string') {
+ el.append(document.createTextNode(child));
+ } else {
+ el.append(child);
+ }
+ }
+
+ return el;
+}
export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
if (!selection) {
import {CalloutNode} from './callout';
import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
import {CustomParagraphNode} from "./custom-paragraph";
+import {LinkNode} from "@lexical/link";
/**
* Load the nodes for lexical.
with: (node: ParagraphNode) => {
return new CustomParagraphNode();
}
- }
+ },
+ LinkNode,
];
}
-import {EditorButtonDefinition} from "./editor-button";
+import {EditorButtonDefinition} from "../framework/buttons";
import {
$createParagraphNode,
$isParagraphNode,
REDO_COMMAND, TextFormatType,
UNDO_COMMAND
} from "lexical";
-import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../helpers";
-import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../nodes/callout";
+import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../../helpers";
+import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
import {
$createHeadingNode,
$createQuoteNode,
HeadingNode,
HeadingTagType
} from "@lexical/rich-text";
+import {$isLinkNode, $toggleLink} from "@lexical/link";
-export const undoButton: EditorButtonDefinition = {
+export const undo: EditorButtonDefinition = {
label: 'Undo',
action(editor: LexicalEditor) {
- editor.dispatchCommand(UNDO_COMMAND);
+ editor.dispatchCommand(UNDO_COMMAND, undefined);
},
isActive(selection: BaseSelection|null): boolean {
return false;
}
}
-export const redoButton: EditorButtonDefinition = {
+export const redo: EditorButtonDefinition = {
label: 'Redo',
action(editor: LexicalEditor) {
- editor.dispatchCommand(REDO_COMMAND);
+ editor.dispatchCommand(REDO_COMMAND, undefined);
},
isActive(selection: BaseSelection|null): boolean {
return false;
};
}
-export const infoCalloutButton: EditorButtonDefinition = buildCalloutButton('info', 'Info');
-export const dangerCalloutButton: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
-export const warningCalloutButton: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
-export const successCalloutButton: EditorButtonDefinition = buildCalloutButton('success', 'Success');
+export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
+export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
+export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
+export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
};
}
-export const h2Button: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
-export const h3Button: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
-export const h4Button: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
-export const h5Button: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
+export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
+export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
+export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
+export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
-export const blockquoteButton: EditorButtonDefinition = {
+export const blockquote: EditorButtonDefinition = {
label: 'Blockquote',
action(editor: LexicalEditor) {
toggleSelectionBlockNodeType(editor, $isQuoteNode, $createQuoteNode);
}
};
-export const paragraphButton: EditorButtonDefinition = {
+export const paragraph: EditorButtonDefinition = {
label: 'Paragraph',
action(editor: LexicalEditor) {
toggleSelectionBlockNodeType(editor, $isParagraphNode, $createParagraphNode);
};
}
-export const boldButton: EditorButtonDefinition = buildFormatButton('Bold', 'bold');
-export const italicButton: EditorButtonDefinition = buildFormatButton('Italic', 'italic');
-export const underlineButton: EditorButtonDefinition = buildFormatButton('Underline', 'underline');
+export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold');
+export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic');
+export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline');
// Todo - Text color
// Todo - Highlight color
-export const strikethroughButton: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough');
-export const superscriptButton: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript');
-export const subscriptButton: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript');
-export const codeButton: EditorButtonDefinition = buildFormatButton('Inline Code', 'code');
-// Todo - Clear formatting
\ No newline at end of file
+export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough');
+export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript');
+export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript');
+export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code');
+// Todo - Clear formatting
+
+
+export const link: EditorButtonDefinition = {
+ label: 'Insert/edit link',
+ action(editor: LexicalEditor) {
+ editor.update(() => {
+ $toggleLink('http://example.com');
+ })
+ },
+ isActive(selection: BaseSelection|null): boolean {
+ return selectionContainsNodeType(selection, $isLinkNode);
+ }
+};
+
+++ /dev/null
-import {BaseSelection, LexicalEditor} from "lexical";
-
-export interface EditorButtonDefinition {
- label: string;
- action: (editor: LexicalEditor) => void;
- isActive: (selection: BaseSelection|null) => boolean;
-}
-
-export class EditorButton {
- #definition: EditorButtonDefinition;
- #editor: LexicalEditor;
- #dom: HTMLButtonElement;
-
- constructor(definition: EditorButtonDefinition, editor: LexicalEditor) {
- this.#definition = definition;
- this.#editor = editor;
- this.#dom = this.buildDOM();
- }
-
- private buildDOM(): HTMLButtonElement {
- const button = document.createElement("button");
- button.setAttribute('type', 'button');
- button.textContent = this.#definition.label;
- button.classList.add('editor-toolbar-button');
-
- button.addEventListener('click', event => {
- this.runAction();
- });
-
- return button;
- }
-
- getDOMElement(): HTMLButtonElement {
- return this.#dom;
- }
-
- runAction() {
- this.#definition.action(this.#editor);
- }
-
- updateActiveState(selection: BaseSelection|null) {
- const isActive = this.#definition.isActive(selection);
- this.#dom.classList.toggle('editor-toolbar-button-active', isActive);
- }
-}
\ No newline at end of file
--- /dev/null
+import {BaseSelection, LexicalEditor} from "lexical";
+
+export type EditorUiStateUpdate = {
+ editor: LexicalEditor,
+ selection: BaseSelection|null,
+};
+
+export type EditorUiContext = {
+ editor: LexicalEditor,
+};
+
+export abstract class EditorUiElement {
+ protected dom: HTMLElement|null = null;
+ private context: EditorUiContext|null = null;
+
+ protected abstract buildDOM(): HTMLElement;
+
+ setContext(context: EditorUiContext): void {
+ this.context = context;
+ }
+
+ getContext(): EditorUiContext {
+ if (this.context === null) {
+ throw new Error('Attempted to use EditorUIContext before it has been set');
+ }
+
+ return this.context;
+ }
+
+ getDOMElement(): HTMLElement {
+ if (!this.dom) {
+ this.dom = this.buildDOM();
+ }
+
+ return this.dom;
+ }
+
+ abstract updateState(state: EditorUiStateUpdate): void;
+}
\ No newline at end of file
--- /dev/null
+import {BaseSelection, LexicalEditor} from "lexical";
+import {EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {el} from "../../helpers";
+
+export interface EditorButtonDefinition {
+ label: string;
+ action: (editor: LexicalEditor) => void;
+ isActive: (selection: BaseSelection|null) => boolean;
+}
+
+export class EditorButton extends EditorUiElement {
+ protected definition: EditorButtonDefinition;
+
+ constructor(definition: EditorButtonDefinition) {
+ super();
+ this.definition = definition;
+ }
+
+ protected buildDOM(): HTMLButtonElement {
+ const button = el('button', {
+ type: 'button',
+ class: 'editor-toolbar-button',
+ }, [this.definition.label]) as HTMLButtonElement;
+
+ button.addEventListener('click', event => {
+ this.definition.action(this.getContext().editor);
+ });
+
+ return button;
+ }
+
+ updateActiveState(selection: BaseSelection|null) {
+ const isActive = this.definition.isActive(selection);
+ this.dom?.classList.toggle('editor-toolbar-button-active', isActive);
+ }
+
+ updateState(state: EditorUiStateUpdate): void {
+ this.updateActiveState(state.selection);
+ }
+}
\ No newline at end of file
--- /dev/null
+import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {el} from "../../helpers";
+
+export class EditorContainerUiElement extends EditorUiElement {
+ protected children : EditorUiElement[];
+
+ constructor(children: EditorUiElement[]) {
+ super();
+ this.children = children;
+ }
+
+ protected buildDOM(): HTMLElement {
+ return el('div', {}, this.getChildren().map(child => child.getDOMElement()));
+ }
+
+ getChildren(): EditorUiElement[] {
+ return this.children;
+ }
+
+ updateState(state: EditorUiStateUpdate): void {
+ for (const child of this.children) {
+ child.updateState(state);
+ }
+ }
+
+ setContext(context: EditorUiContext) {
+ for (const child of this.getChildren()) {
+ child.setContext(context);
+ }
+ }
+}
+
+export class EditorFormatMenu extends EditorContainerUiElement {
+ buildDOM(): HTMLElement {
+ return el('div', {
+ class: 'editor-format-menu'
+ }, this.getChildren().map(child => child.getDOMElement()));
+ }
+
+}
\ No newline at end of file
LexicalEditor,
SELECTION_CHANGE_COMMAND
} from "lexical";
-import {EditorButton, EditorButtonDefinition} from "./editor-button";
-import {
- blockquoteButton, boldButton, codeButton,
- dangerCalloutButton,
- h2Button,
- h3Button, h4Button, h5Button,
- infoCalloutButton, italicButton, paragraphButton, redoButton, strikethroughButton, subscriptButton,
- successCalloutButton, superscriptButton, underlineButton, undoButton,
- warningCalloutButton
-} from "./buttons";
-
-
-
-const toolbarButtonDefinitions: EditorButtonDefinition[] = [
- undoButton, redoButton,
-
- infoCalloutButton, warningCalloutButton, dangerCalloutButton, successCalloutButton,
- h2Button, h3Button, h4Button, h5Button,
- blockquoteButton, paragraphButton,
-
- boldButton, italicButton, underlineButton, strikethroughButton,
- superscriptButton, subscriptButton, codeButton,
-];
+import {getMainEditorFullToolbar} from "./toolbars";
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
- const toolbarContainer = document.createElement('div');
- toolbarContainer.classList.add('editor-toolbar-container');
-
- const buttons = toolbarButtonDefinitions.map(definition => {
- return new EditorButton(definition, editor);
- });
-
- const buttonElements = buttons.map(button => button.getDOMElement());
-
- toolbarContainer.append(...buttonElements);
- element.before(toolbarContainer);
+ const toolbar = getMainEditorFullToolbar();
+ toolbar.setContext({editor});
+ element.before(toolbar.getDOMElement());
// Update button states on editor selection change
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
const selection = $getSelection();
- for (const button of buttons) {
- button.updateActiveState(selection);
- }
+ toolbar.updateState({editor, selection});
return false;
}, COMMAND_PRIORITY_LOW);
}
\ No newline at end of file
--- /dev/null
+import {EditorButton} from "./framework/buttons";
+import {
+ blockquote, bold, code,
+ dangerCallout,
+ h2, h3, h4, h5,
+ infoCallout, italic, link, paragraph,
+ redo, strikethrough, subscript,
+ successCallout, superscript, underline,
+ undo,
+ warningCallout
+} from "./defaults/button-definitions";
+import {EditorContainerUiElement, EditorFormatMenu} from "./framework/containers";
+
+
+export function getMainEditorFullToolbar(): EditorContainerUiElement {
+ return new EditorContainerUiElement([
+ new EditorButton(undo),
+ new EditorButton(redo),
+
+ new EditorFormatMenu([
+ new EditorButton(h2),
+ new EditorButton(h3),
+ new EditorButton(h4),
+ new EditorButton(h5),
+ new EditorButton(blockquote),
+ new EditorButton(paragraph),
+ new EditorButton(infoCallout),
+ new EditorButton(successCallout),
+ new EditorButton(warningCallout),
+ new EditorButton(dangerCallout),
+ ]),
+
+ new EditorButton(bold),
+ new EditorButton(italic),
+ new EditorButton(underline),
+ new EditorButton(strikethrough),
+ new EditorButton(superscript),
+ new EditorButton(subscript),
+ new EditorButton(code),
+
+ new EditorButton(link),
+ ]);
+}
\ No newline at end of file
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
<p id="Content!">Some <strong>content</strong> here</p>
+ <p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2>
<ul>
<li>Hello</li>