]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added block indenting capability
authorDan Brown <redacted>
Tue, 10 Sep 2024 14:55:46 +0000 (15:55 +0100)
committerDan Brown <redacted>
Tue, 10 Sep 2024 14:55:46 +0000 (15:55 +0100)
Needed a custom implementation due to hardcoded defaults for Lexical
default indenting.

resources/js/wysiwyg/nodes/_common.ts
resources/js/wysiwyg/nodes/callout.ts
resources/js/wysiwyg/nodes/custom-heading.ts
resources/js/wysiwyg/nodes/custom-paragraph.ts
resources/js/wysiwyg/nodes/custom-quote.ts
resources/js/wysiwyg/nodes/custom-table.ts
resources/js/wysiwyg/nodes/media.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/buttons/lists.ts
resources/js/wysiwyg/ui/toolbars.ts
resources/js/wysiwyg/utils/nodes.ts

index ff957f95300f2ef25de83f5a9ae904f0147df21d..8a0475c7bdaa2d5b5df0f4f4ea7fd5002011a6c1 100644 (file)
@@ -1,5 +1,6 @@
 import {LexicalNode, Spread} from "lexical";
 import type {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
+import {sizeToPixels} from "../utils/dom";
 
 export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
 const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
@@ -7,6 +8,7 @@ const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'jus
 export type SerializedCommonBlockNode = Spread<{
     id: string;
     alignment: CommonBlockAlignment;
+    inset: number;
 }, SerializedElementNode>
 
 export interface NodeHasAlignment {
@@ -21,7 +23,13 @@ export interface NodeHasId {
     getId(): string;
 }
 
-interface CommonBlockInterface extends NodeHasId, NodeHasAlignment {}
+export interface NodeHasInset {
+    readonly __inset: number;
+    setInset(inset: number): void;
+    getInset(): number;
+}
+
+interface CommonBlockInterface extends NodeHasId, NodeHasAlignment, NodeHasInset {}
 
 export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAlignment {
     const textAlignStyle: string = element.style.textAlign || '';
@@ -42,17 +50,24 @@ export function extractAlignmentFromElement(element: HTMLElement): CommonBlockAl
     return '';
 }
 
+export function extractInsetFromElement(element: HTMLElement): number {
+    const elemPadding: string = element.style.paddingLeft || '0';
+    return sizeToPixels(elemPadding);
+}
+
 export function setCommonBlockPropsFromElement(element: HTMLElement, node: CommonBlockInterface): void {
     if (element.id) {
         node.setId(element.id);
     }
 
     node.setAlignment(extractAlignmentFromElement(element));
+    node.setInset(extractInsetFromElement(element));
 }
 
 export function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: CommonBlockInterface): boolean {
     return nodeA.__id !== nodeB.__id ||
-        nodeA.__alignment !== nodeB.__alignment;
+        nodeA.__alignment !== nodeB.__alignment ||
+        nodeA.__inset !== nodeB.__inset;
 }
 
 export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
@@ -63,6 +78,16 @@ export function updateElementWithCommonBlockProps(element: HTMLElement, node: Co
     if (node.__alignment) {
         element.classList.add('align-' + node.__alignment);
     }
+
+    if (node.__inset) {
+        element.style.paddingLeft = `${node.__inset}px`;
+    }
+}
+
+export function deserializeCommonBlockNode(serializedNode: SerializedCommonBlockNode, node: CommonBlockInterface): void {
+    node.setId(serializedNode.id);
+    node.setAlignment(serializedNode.alignment);
+    node.setInset(serializedNode.inset);
 }
 
 export interface NodeHasSize {
index ededc0f290935b7a791c9f368b532eef999efa49..cfe32ec854cad1e13caf9c6a531b9da701422e10 100644 (file)
@@ -10,7 +10,7 @@ import {
 import type {EditorConfig} from "lexical/LexicalEditor";
 import type {RangeSelection} from "lexical/LexicalSelection";
 import {
-    CommonBlockAlignment, commonPropertiesDifferent,
+    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -26,6 +26,7 @@ export class CalloutNode extends ElementNode {
     __id: string = '';
     __category: CalloutCategory = 'info';
     __alignment: CommonBlockAlignment = '';
+    __inset: number = 0;
 
     static getType() {
         return 'callout';
@@ -35,6 +36,7 @@ export class CalloutNode extends ElementNode {
         const newNode = new CalloutNode(node.__category, node.__key);
         newNode.__id = node.__id;
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -73,6 +75,16 @@ export class CalloutNode extends ElementNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
         const element = document.createElement('p');
         element.classList.add('callout', this.__category || '');
@@ -141,13 +153,13 @@ export class CalloutNode extends ElementNode {
             category: this.__category,
             id: this.__id,
             alignment: this.__alignment,
+            inset: this.__inset,
         };
     }
 
     static importJSON(serializedNode: SerializedCalloutNode): CalloutNode {
         const node = $createCalloutNode(serializedNode.category);
-        node.setId(serializedNode.id);
-        node.setAlignment(serializedNode.alignment);
+        deserializeCommonBlockNode(serializedNode, node);
         return node;
     }
 
index 885622ad338b2a71fe092865533a6b2fbee27a60..5df6245f5a5ffca9fed2f86073814f5dadb53693 100644 (file)
@@ -7,7 +7,7 @@ import {
 import {EditorConfig} from "lexical/LexicalEditor";
 import {HeadingNode, HeadingTagType, SerializedHeadingNode} from "@lexical/rich-text";
 import {
-    CommonBlockAlignment, commonPropertiesDifferent,
+    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -19,6 +19,7 @@ export type SerializedCustomHeadingNode = Spread<SerializedCommonBlockNode, Seri
 export class CustomHeadingNode extends HeadingNode {
     __id: string = '';
     __alignment: CommonBlockAlignment = '';
+    __inset: number = 0;
 
     static getType() {
         return 'custom-heading';
@@ -44,9 +45,20 @@ export class CustomHeadingNode extends HeadingNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     static clone(node: CustomHeadingNode) {
         const newNode = new CustomHeadingNode(node.__tag, node.__key);
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -68,13 +80,13 @@ export class CustomHeadingNode extends HeadingNode {
             version: 1,
             id: this.__id,
             alignment: this.__alignment,
+            inset: this.__inset,
         };
     }
 
     static importJSON(serializedNode: SerializedCustomHeadingNode): CustomHeadingNode {
         const node = $createCustomHeadingNode(serializedNode.tag);
-        node.setId(serializedNode.id);
-        node.setAlignment(serializedNode.alignment);
+        deserializeCommonBlockNode(serializedNode, node);
         return node;
     }
 
index 663f32dfc687234537d7140019e9bd7dc537cc22..3adc10d0e9c2d6cf85afee72596e1474d9261fba 100644 (file)
@@ -7,7 +7,7 @@ import {
 } from "lexical";
 import {EditorConfig} from "lexical/LexicalEditor";
 import {
-    CommonBlockAlignment, commonPropertiesDifferent,
+    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -18,6 +18,7 @@ export type SerializedCustomParagraphNode = Spread<SerializedCommonBlockNode, Se
 export class CustomParagraphNode extends ParagraphNode {
     __id: string = '';
     __alignment: CommonBlockAlignment = '';
+    __inset: number = 0;
 
     static getType() {
         return 'custom-paragraph';
@@ -43,10 +44,21 @@ export class CustomParagraphNode extends ParagraphNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     static clone(node: CustomParagraphNode): CustomParagraphNode {
         const newNode = new CustomParagraphNode(node.__key);
         newNode.__id = node.__id;
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -68,13 +80,13 @@ export class CustomParagraphNode extends ParagraphNode {
             version: 1,
             id: this.__id,
             alignment: this.__alignment,
+            inset: this.__inset,
         };
     }
 
     static importJSON(serializedNode: SerializedCustomParagraphNode): CustomParagraphNode {
         const node = $createCustomParagraphNode();
-        node.setId(serializedNode.id);
-        node.setAlignment(serializedNode.alignment);
+        deserializeCommonBlockNode(serializedNode, node);
         return node;
     }
 
index cee289dbe72e806c73778c4ab44ceb200b60cf3d..39ae7bf8af3b6f3f1728c016db4b8766d9f44bcd 100644 (file)
@@ -7,7 +7,7 @@ import {
 import {EditorConfig} from "lexical/LexicalEditor";
 import {QuoteNode, SerializedQuoteNode} from "@lexical/rich-text";
 import {
-    CommonBlockAlignment, commonPropertiesDifferent,
+    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -19,6 +19,7 @@ export type SerializedCustomQuoteNode = Spread<SerializedCommonBlockNode, Serial
 export class CustomQuoteNode extends QuoteNode {
     __id: string = '';
     __alignment: CommonBlockAlignment = '';
+    __inset: number = 0;
 
     static getType() {
         return 'custom-quote';
@@ -44,10 +45,21 @@ export class CustomQuoteNode extends QuoteNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     static clone(node: CustomQuoteNode) {
         const newNode = new CustomQuoteNode(node.__key);
         newNode.__id = node.__id;
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -68,13 +80,13 @@ export class CustomQuoteNode extends QuoteNode {
             version: 1,
             id: this.__id,
             alignment: this.__alignment,
+            inset: this.__inset,
         };
     }
 
     static importJSON(serializedNode: SerializedCustomQuoteNode): CustomQuoteNode {
         const node = $createCustomQuoteNode();
-        node.setId(serializedNode.id);
-        node.setAlignment(serializedNode.alignment);
+        deserializeCommonBlockNode(serializedNode, node);
         return node;
     }
 
index b699763d97689351a463cdceef39ccf007ef9a83..c25c06c65151012f338d4c8cfb493f82856c5afa 100644 (file)
@@ -5,7 +5,7 @@ import {EditorConfig} from "lexical/LexicalEditor";
 import {el, extractStyleMapFromElement, StyleMap} from "../utils/dom";
 import {getTableColumnWidths} from "../utils/tables";
 import {
-    CommonBlockAlignment,
+    CommonBlockAlignment, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -21,6 +21,7 @@ export class CustomTableNode extends TableNode {
     __colWidths: string[] = [];
     __styles: StyleMap = new Map;
     __alignment: CommonBlockAlignment = '';
+    __inset: number = 0;
 
     static getType() {
         return 'custom-table';
@@ -46,6 +47,16 @@ export class CustomTableNode extends TableNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     setColWidths(widths: string[]) {
         const self = this.getWritable();
         self.__colWidths = widths;
@@ -72,6 +83,7 @@ export class CustomTableNode extends TableNode {
         newNode.__colWidths = node.__colWidths;
         newNode.__styles = new Map(node.__styles);
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -112,15 +124,15 @@ export class CustomTableNode extends TableNode {
             colWidths: this.__colWidths,
             styles: Object.fromEntries(this.__styles),
             alignment: this.__alignment,
+            inset: this.__inset,
         };
     }
 
     static importJSON(serializedNode: SerializedCustomTableNode): CustomTableNode {
         const node = $createCustomTableNode();
-        node.setId(serializedNode.id);
+        deserializeCommonBlockNode(serializedNode, node);
         node.setColWidths(serializedNode.colWidths);
         node.setStyles(new Map(Object.entries(serializedNode.styles)));
-        node.setAlignment(serializedNode.alignment);
         return node;
     }
 
index 8658a12167bada4d3a754680abba3102d3e99db8..64fe8f77b4d2fed2dc664bf22366d8c594844074 100644 (file)
@@ -10,7 +10,7 @@ import type {EditorConfig} from "lexical/LexicalEditor";
 
 import {el, setOrRemoveAttribute, sizeToPixels} from "../utils/dom";
 import {
-    CommonBlockAlignment,
+    CommonBlockAlignment, deserializeCommonBlockNode,
     SerializedCommonBlockNode,
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
@@ -80,6 +80,7 @@ export class MediaNode extends ElementNode {
     __tag: MediaNodeTag;
     __attributes: Record<string, string> = {};
     __sources: MediaNodeSource[] = [];
+    __inset: number = 0;
 
     static getType() {
         return 'media';
@@ -91,6 +92,7 @@ export class MediaNode extends ElementNode {
         newNode.__sources = node.__sources.map(s => Object.assign({}, s));
         newNode.__id = node.__id;
         newNode.__alignment = node.__alignment;
+        newNode.__inset = node.__inset;
         return newNode;
     }
 
@@ -168,6 +170,16 @@ export class MediaNode extends ElementNode {
         return self.__alignment;
     }
 
+    setInset(size: number) {
+        const self = this.getWritable();
+        self.__inset = size;
+    }
+
+    getInset(): number {
+        const self = this.getLatest();
+        return self.__inset;
+    }
+
     setHeight(height: number): void {
         if (!height) {
             return;
@@ -251,6 +263,10 @@ export class MediaNode extends ElementNode {
             }
         }
 
+        if (prevNode.__inset !== this.__inset) {
+            dom.style.paddingLeft = `${this.__inset}px`;
+        }
+
         return false;
     }
 
@@ -290,6 +306,7 @@ export class MediaNode extends ElementNode {
             version: 1,
             id: this.__id,
             alignment: this.__alignment,
+            inset: this.__inset,
             tag: this.__tag,
             attributes: this.__attributes,
             sources: this.__sources,
@@ -298,8 +315,7 @@ export class MediaNode extends ElementNode {
 
     static importJSON(serializedNode: SerializedMediaNode): MediaNode {
         const node = $createMediaNode(serializedNode.tag);
-        node.setId(serializedNode.id);
-        node.setAlignment(serializedNode.alignment);
+        deserializeCommonBlockNode(serializedNode, node);
         return node;
     }
 
index 498d286fd45b75040c08083753ad9b27dc7e27a1..34367a36b9c1a3133d6f66d517dfe0ed1ce44da1 100644 (file)
@@ -6,6 +6,7 @@
 
 ## Main Todo
 
+- Align list nesting with old editor
 - Mac: Shortcut support via command.
 
 ## Secondary Todo
index edec3ea0076fdd806c7254be5dffbd519cdeb6c3..0857fb70a4958304bf44530db502cc8d15001aee 100644 (file)
@@ -1,12 +1,24 @@
 import {$isListNode, ListNode, ListType} from "@lexical/list";
 import {EditorButtonDefinition} from "../../framework/buttons";
 import {EditorUiContext} from "../../framework/core";
-import {BaseSelection, LexicalNode} from "lexical";
+import {
+    BaseSelection,
+    LexicalEditor,
+    LexicalNode,
+} from "lexical";
 import listBulletIcon from "@icons/editor/list-bullet.svg";
 import listNumberedIcon from "@icons/editor/list-numbered.svg";
 import listCheckIcon from "@icons/editor/list-check.svg";
-import {$selectionContainsNodeType} from "../../../utils/selection";
+import indentIncreaseIcon from "@icons/editor/indent-increase.svg";
+import indentDecreaseIcon from "@icons/editor/indent-decrease.svg";
+import {
+    $getBlockElementNodesInSelection,
+    $selectionContainsNodeType,
+    $toggleSelection,
+    getLastSelection
+} from "../../../utils/selection";
 import {toggleSelectionAsList} from "../../../utils/formats";
+import {nodeHasInset} from "../../../utils/nodes";
 
 
 function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
@@ -27,3 +39,45 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
 export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
 export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
 export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
+
+
+function setInsetForSelection(editor: LexicalEditor, change: number): void {
+    const selection = getLastSelection(editor);
+
+    const elements = $getBlockElementNodesInSelection(selection);
+    for (const node of elements) {
+        if (nodeHasInset(node)) {
+            const currentInset = node.getInset();
+            const newInset = Math.min(Math.max(currentInset + change, 0), 500);
+            node.setInset(newInset)
+        }
+    }
+
+    $toggleSelection(editor);
+}
+
+export const indentIncrease: EditorButtonDefinition = {
+    label: 'Increase indent',
+    icon: indentIncreaseIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            setInsetForSelection(context.editor, 40);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const indentDecrease: EditorButtonDefinition = {
+    label: 'Decrease indent',
+    icon: indentDecreaseIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            setInsetForSelection(context.editor, -40);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
\ No newline at end of file
index e7d486cd5303ef2ce8a233ae5f50e35206bf377b..0ad638410add5cb47788df92d241cfd399b41127 100644 (file)
@@ -52,7 +52,13 @@ import {
     underline
 } from "./defaults/buttons/inline-formats";
 import {alignCenter, alignJustify, alignLeft, alignRight} from "./defaults/buttons/alignments";
-import {bulletList, numberList, taskList} from "./defaults/buttons/lists";
+import {
+    bulletList,
+    indentDecrease,
+    indentIncrease,
+    numberList,
+    taskList
+} from "./defaults/buttons/lists";
 import {
     codeBlock,
     details,
@@ -119,10 +125,12 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
         ]),
 
         // Lists
-        new EditorOverflowContainer(3, [
+        new EditorOverflowContainer(5, [
             new EditorButton(bulletList),
             new EditorButton(numberList),
             new EditorButton(taskList),
+            new EditorButton(indentDecrease),
+            new EditorButton(indentIncrease),
         ]),
 
         // Insert types
index b8bb8de9a2de6ee88bc1fe2625de7811472aae0e..48fbe043f390e7e08a259ae74090d1b0cb7623f8 100644 (file)
@@ -11,7 +11,7 @@ import {LexicalNodeMatcher} from "../nodes";
 import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
 import {$generateNodesFromDOM} from "@lexical/html";
 import {htmlToDom} from "./dom";
-import {NodeHasAlignment} from "../nodes/_common";
+import {NodeHasAlignment, NodeHasInset} from "../nodes/_common";
 import {$findMatchingParent} from "@lexical/utils";
 
 function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
@@ -96,4 +96,8 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null
 
 export function nodeHasAlignment(node: object): node is NodeHasAlignment {
     return '__alignment' in node;
+}
+
+export function nodeHasInset(node: object): node is NodeHasInset {
+    return '__inset' in node;
 }
\ No newline at end of file