mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Added block indenting capability
Needed a custom implementation due to hardcoded defaults for Lexical default indenting.
This commit is contained in:
parent
2036438203
commit
5083188ed8
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
## Main Todo
|
||||
|
||||
- Align list nesting with old editor
|
||||
- Mac: Shortcut support via command.
|
||||
|
||||
## Secondary Todo
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user