From 5083188ed82b9aa2e5df976f592d1baa67865c39 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 10 Sep 2024 15:55:46 +0100 Subject: [PATCH] Lexical: Added block indenting capability Needed a custom implementation due to hardcoded defaults for Lexical default indenting. --- resources/js/wysiwyg/nodes/_common.ts | 29 +++++++++- resources/js/wysiwyg/nodes/callout.ts | 18 +++++- resources/js/wysiwyg/nodes/custom-heading.ts | 18 +++++- .../js/wysiwyg/nodes/custom-paragraph.ts | 18 +++++- resources/js/wysiwyg/nodes/custom-quote.ts | 18 +++++- resources/js/wysiwyg/nodes/custom-table.ts | 18 +++++- resources/js/wysiwyg/nodes/media.ts | 22 ++++++- resources/js/wysiwyg/todo.md | 1 + .../js/wysiwyg/ui/defaults/buttons/lists.ts | 58 ++++++++++++++++++- resources/js/wysiwyg/ui/toolbars.ts | 12 +++- resources/js/wysiwyg/utils/nodes.ts | 6 +- 11 files changed, 193 insertions(+), 25 deletions(-) diff --git a/resources/js/wysiwyg/nodes/_common.ts b/resources/js/wysiwyg/nodes/_common.ts index ff957f953..8a0475c7b 100644 --- a/resources/js/wysiwyg/nodes/_common.ts +++ b/resources/js/wysiwyg/nodes/_common.ts @@ -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 { diff --git a/resources/js/wysiwyg/nodes/callout.ts b/resources/js/wysiwyg/nodes/callout.ts index ededc0f29..cfe32ec85 100644 --- a/resources/js/wysiwyg/nodes/callout.ts +++ b/resources/js/wysiwyg/nodes/callout.ts @@ -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; } diff --git a/resources/js/wysiwyg/nodes/custom-heading.ts b/resources/js/wysiwyg/nodes/custom-heading.ts index 885622ad3..5df6245f5 100644 --- a/resources/js/wysiwyg/nodes/custom-heading.ts +++ b/resources/js/wysiwyg/nodes/custom-heading.ts @@ -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 = {}; __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; } diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 498d286fd..34367a36b 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -6,6 +6,7 @@ ## Main Todo +- Align list nesting with old editor - Mac: Shortcut support via command. ## Secondary Todo diff --git a/resources/js/wysiwyg/ui/defaults/buttons/lists.ts b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts index edec3ea00..0857fb70a 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/lists.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index e7d486cd5..0ad638410 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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 diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index b8bb8de9a..48fbe043f 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -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