Improve multi element indentation (#1982)

* Improve multi element indentation

* Remove bad UT

* Remove bad UT

* Add e2e test

* Address feedback
This commit is contained in:
Dominic Gannaway
2022-04-26 04:41:22 +01:00
committed by GitHub
parent 687c3a5c5c
commit c963cfa896
22 changed files with 1485 additions and 74 deletions

View File

@ -28,7 +28,7 @@ declare class CodeNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
): null | ParagraphNode | CodeHighlightNode; ): null | ParagraphNode | CodeHighlightNode;
canInsertTab(): true; canInsertTab(): boolean;
collapseAtStart(): true; collapseAtStart(): true;
setLanguage(language: string): void; setLanguage(language: string): void;
getLanguage(): string | void; getLanguage(): string | void;

View File

@ -28,7 +28,7 @@ declare export class CodeNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
): null | ParagraphNode | CodeHighlightNode; ): null | ParagraphNode | CodeHighlightNode;
canInsertTab(): true; canInsertTab(): boolean;
collapseAtStart(): true; collapseAtStart(): true;
setLanguage(language: string): void; setLanguage(language: string): void;
getLanguage(): string | void; getLanguage(): string | void;

View File

@ -92,14 +92,6 @@ describe('LexicalCodeNode tests', () => {
}); });
}); });
test('CodeNode.canInsertTab()', async () => {
const {editor} = testEnv;
await editor.update(() => {
const codeNode = $createCodeNode();
expect(codeNode.canInsertTab()).toBe(true);
});
});
test('$createCodeNode()', async () => { test('$createCodeNode()', async () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { await editor.update(() => {

View File

@ -313,10 +313,18 @@ export class CodeNode extends ElementNode {
return null; return null;
} }
canInsertTab(): true { canInsertTab(): boolean {
const selection = $getSelection();
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
return false;
}
return true; return true;
} }
canIndent(): false {
return false;
}
collapseAtStart(): true { collapseAtStart(): true {
const paragraph = $createParagraphNode(); const paragraph = $createParagraphNode();
const children = this.getChildren(); const children = this.getChildren();

View File

@ -22,7 +22,7 @@ export function $getListDepth(listNode: ListNode): number;
export function $handleListInsertParagraph(): boolean; export function $handleListInsertParagraph(): boolean;
export function $isListItemNode(node?: LexicalNode): node is ListItemNode; export function $isListItemNode(node?: LexicalNode): node is ListItemNode;
export function $isListNode(node?: LexicalNode): node is ListNode; export function $isListNode(node?: LexicalNode): node is ListNode;
export function indentList(): boolean; export function indentList(): void;
export function insertList(editor: LexicalEditor, listType: 'ul' | 'ol'): void; export function insertList(editor: LexicalEditor, listType: 'ul' | 'ol'): void;
export declare class ListItemNode extends ElementNode { export declare class ListItemNode extends ElementNode {
append(...nodes: LexicalNode[]): ListItemNode; append(...nodes: LexicalNode[]): ListItemNode;
@ -42,7 +42,7 @@ export declare class ListNode extends ElementNode {
append(...nodesToAppend: LexicalNode[]): ListNode; append(...nodesToAppend: LexicalNode[]): ListNode;
getTag(): ListNodeTagType; getTag(): ListNodeTagType;
} }
export function outdentList(): boolean; export function outdentList(): void;
export function removeList(editor: LexicalEditor): boolean; export function removeList(editor: LexicalEditor): boolean;
export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>; export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;

View File

@ -30,7 +30,7 @@ declare export function $isListItemNode(
declare export function $isListNode( declare export function $isListNode(
node: ?LexicalNode, node: ?LexicalNode,
): boolean %checks(node instanceof ListNode); ): boolean %checks(node instanceof ListNode);
declare export function indentList(): boolean; declare export function indentList(): void;
declare export function insertList( declare export function insertList(
editor: LexicalEditor, editor: LexicalEditor,
listType: 'ul' | 'ol', listType: 'ul' | 'ol',
@ -56,7 +56,7 @@ declare export class ListNode extends ElementNode {
getTag(): ListNodeTagType; getTag(): ListNodeTagType;
getStart(): number; getStart(): number;
} }
declare export function outdentList(): boolean; declare export function outdentList(): void;
declare export function removeList(editor: LexicalEditor): boolean; declare export function removeList(editor: LexicalEditor): boolean;
declare export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>; declare export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;

View File

@ -227,6 +227,11 @@ export class ListItemNode extends ElementNode {
return this; return this;
} }
canIndent(): false {
// Indent/outdent is handled specifically in the RichText logic.
return false;
}
insertBefore(nodeToInsert: LexicalNode): LexicalNode { insertBefore(nodeToInsert: LexicalNode): LexicalNode {
const siblings = this.getNextSiblings(); const siblings = this.getNextSiblings();
if ($isListItemNode(nodeToInsert)) { if ($isListItemNode(nodeToInsert)) {

View File

@ -94,6 +94,10 @@ export class ListNode extends ElementNode {
return false; return false;
} }
canIndent(): false {
return false;
}
append(...nodesToAppend: LexicalNode[]): ListNode { append(...nodesToAppend: LexicalNode[]): ListNode {
for (let i = 0; i < nodesToAppend.length; i++) { for (let i = 0; i < nodesToAppend.length; i++) {
const currentNode = nodesToAppend[i]; const currentNode = nodesToAppend[i];

View File

@ -300,10 +300,10 @@ export function $handleOutdent(listItemNodes: Array<ListItemNode>): void {
}); });
} }
function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): boolean { function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): void {
const selection = $getSelection(); const selection = $getSelection();
if (!$isRangeSelection(selection)) { if (!$isRangeSelection(selection)) {
return false; return;
} }
const selectedNodes = selection.getNodes(); const selectedNodes = selection.getNodes();
let listItemNodes = []; let listItemNodes = [];
@ -326,17 +326,15 @@ function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): boolean {
} else { } else {
$handleOutdent(listItemNodes); $handleOutdent(listItemNodes);
} }
return true;
} }
return false;
} }
export function indentList(): boolean { export function indentList(): void {
return maybeIndentOrOutdent('indent'); maybeIndentOrOutdent('indent');
} }
export function outdentList(): boolean { export function outdentList(): void {
return maybeIndentOrOutdent('outdent'); maybeIndentOrOutdent('outdent');
} }
export function $handleListInsertParagraph(): boolean { export function $handleListInsertParagraph(): boolean {

View File

@ -39,7 +39,7 @@ test.describe('Element format', () => {
html` html`
<p <p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
style="padding-inline-start: 80px; text-align: center;" style="padding-inline-start: 40px; text-align: center;"
dir="ltr"> dir="ltr">
<span data-lexical-text="true">Hello</span> <span data-lexical-text="true">Hello</span>
<a <a

File diff suppressed because it is too large Load Diff

View File

@ -34,10 +34,7 @@ export default function useList(editor: LexicalEditor): void {
editor.registerCommand( editor.registerCommand(
INDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND,
() => { () => {
const hasHandledIndention = indentList(); indentList();
if (hasHandledIndention) {
return true;
}
return false; return false;
}, },
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
@ -45,10 +42,7 @@ export default function useList(editor: LexicalEditor): void {
editor.registerCommand( editor.registerCommand(
OUTDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND,
() => { () => {
const hasHandledIndention = outdentList(); outdentList();
if (hasHandledIndention) {
return true;
}
return false; return false;
}, },
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,

View File

@ -41,6 +41,7 @@ import {
$isGridSelection, $isGridSelection,
$isNodeSelection, $isNodeSelection,
$isRangeSelection, $isRangeSelection,
$isTextNode,
CLICK_COMMAND, CLICK_COMMAND,
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
COPY_COMMAND, COPY_COMMAND,
@ -328,6 +329,33 @@ function onCutForRichText(event: ClipboardEvent, editor: LexicalEditor): void {
}); });
} }
function handleIndentAndOutdent(
insertTab: (node: LexicalNode) => void,
indentOrOutdent: (block: ElementNode) => void,
): void {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const alreadyHandled = new Set();
const nodes = selection.getNodes();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const key = node.getKey();
if (alreadyHandled.has(key)) {
continue;
}
alreadyHandled.add(key);
const parentBlock = $getNearestBlockElementAncestorOrThrow(node);
if (parentBlock.canInsertTab()) {
insertTab(node);
} else if (parentBlock.canIndent()) {
indentOrOutdent(parentBlock);
}
}
}
export function registerRichText( export function registerRichText(
editor: LexicalEditor, editor: LexicalEditor,
initialEditorState?: InitialEditorStateType, initialEditorState?: InitialEditorStateType,
@ -478,22 +506,17 @@ export function registerRichText(
editor.registerCommand( editor.registerCommand(
INDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND,
(payload) => { (payload) => {
const selection = $getSelection(); handleIndentAndOutdent(
if (!$isRangeSelection(selection)) { () => {
return false; editor.dispatchCommand(INSERT_TEXT_COMMAND, '\t');
} },
// Handle code blocks (block) => {
const anchor = selection.anchor; const indent = block.getIndent();
const parentBlock = $getNearestBlockElementAncestorOrThrow( if (indent !== 10) {
anchor.getNode(), block.setIndent(indent + 1);
}
},
); );
if (parentBlock.canInsertTab()) {
editor.dispatchCommand(INSERT_TEXT_COMMAND, '\t');
} else {
if (parentBlock.getIndent() !== 10) {
parentBlock.setIndent(parentBlock.getIndent() + 1);
}
}
return true; return true;
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
@ -501,27 +524,23 @@ export function registerRichText(
editor.registerCommand( editor.registerCommand(
OUTDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND,
(payload) => { (payload) => {
const selection = $getSelection(); handleIndentAndOutdent(
if (!$isRangeSelection(selection)) { (node) => {
return false; if ($isTextNode(node)) {
} const textContent = node.getTextContent();
// Handle code blocks const character = textContent[textContent.length - 1];
const anchor = selection.anchor; if (character === '\t') {
const anchorNode = anchor.getNode(); editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
const parentBlock = $getNearestBlockElementAncestorOrThrow( }
anchor.getNode(), }
},
(block) => {
const indent = block.getIndent();
if (indent !== 0) {
block.setIndent(indent - 1);
}
},
); );
if (parentBlock.canInsertTab()) {
const textContent = anchorNode.getTextContent();
const character = textContent[anchor.offset - 1];
if (character === '\t') {
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
}
} else {
if (parentBlock.getIndent() !== 0) {
parentBlock.setIndent(parentBlock.getIndent() - 1);
}
}
return true; return true;
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,

View File

@ -54,7 +54,6 @@ export declare class TableCellNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
): null | ParagraphNode | TableCellNode; ): null | ParagraphNode | TableCellNode;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
getTag(): string; getTag(): string;
setHeaderState(headerState: TableCellHeaderState): TableCellHeaderState; setHeaderState(headerState: TableCellHeaderState): TableCellHeaderState;
@ -83,7 +82,6 @@ export declare class TableNode extends ElementNode {
createDOM(config: EditorConfig): HTMLElement; createDOM(config: EditorConfig): HTMLElement;
updateDOM(prevNode: TableNode, dom: HTMLElement): boolean; updateDOM(prevNode: TableNode, dom: HTMLElement): boolean;
insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode; insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
getCordsFromCellNode(tableCellNode: TableCellNode): {x: number; y: number}; getCordsFromCellNode(tableCellNode: TableCellNode): {x: number; y: number};
getCellFromCords(x: number, y: number, grid: Grid): ?Cell; getCellFromCords(x: number, y: number, grid: Grid): ?Cell;
@ -112,7 +110,6 @@ declare class TableRowNode extends ElementNode {
): null | ParagraphNode | TableRowNode; ): null | ParagraphNode | TableRowNode;
setHeight(height: number): ?number; setHeight(height: number): ?number;
getHeight(): ?number; getHeight(): ?number;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
} }
declare function $createTableRowNode(): TableRowNode; declare function $createTableRowNode(): TableRowNode;

View File

@ -48,7 +48,6 @@ declare export class TableCellNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
): null | ParagraphNode | TableCellNode; ): null | ParagraphNode | TableCellNode;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
getTag(): string; getTag(): string;
setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState; setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState;
@ -81,7 +80,6 @@ declare export class TableNode extends ElementNode {
createDOM(config: EditorConfig): HTMLElement; createDOM(config: EditorConfig): HTMLElement;
updateDOM(prevNode: TableNode, dom: HTMLElement): boolean; updateDOM(prevNode: TableNode, dom: HTMLElement): boolean;
insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode; insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
getCordsFromCellNode( getCordsFromCellNode(
tableCellNode: TableCellNode, tableCellNode: TableCellNode,
@ -115,7 +113,6 @@ declare export class TableRowNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
): null | ParagraphNode | TableRowNode; ): null | ParagraphNode | TableRowNode;
canInsertTab(): true;
collapseAtStart(): true; collapseAtStart(): true;
} }
declare export function $createTableRowNode(): TableRowNode; declare export function $createTableRowNode(): TableRowNode;

View File

@ -175,6 +175,10 @@ export class TableCellNode extends GridCellNode {
canBeEmpty(): false { canBeEmpty(): false {
return false; return false;
} }
canIndent(): false {
return false;
}
} }
export function convertTableCellNodeElement( export function convertTableCellNodeElement(

View File

@ -183,6 +183,10 @@ export class TableNode extends GridNode {
canSelectBefore(): true { canSelectBefore(): true {
return true; return true;
} }
canIndent(): false {
return false;
}
} }
export function $getElementGridForTableNode( export function $getElementGridForTableNode(

View File

@ -72,6 +72,10 @@ export class TableRowNode extends GridRowNode {
canBeEmpty(): false { canBeEmpty(): false {
return false; return false;
} }
canIndent(): false {
return false;
}
} }
export function convertTableRowElement(domNode: Node): DOMConversionOutput { export function convertTableRowElement(domNode: Node): DOMConversionOutput {

View File

@ -696,6 +696,7 @@ export declare class ElementNode extends LexicalNode {
setIndent(indentLevel: number): ElementNode; setIndent(indentLevel: number): ElementNode;
insertNewAfter(selection: RangeSelection): null | LexicalNode; insertNewAfter(selection: RangeSelection): null | LexicalNode;
canInsertTab(): boolean; canInsertTab(): boolean;
canIndent(): boolean;
collapseAtStart(selection: RangeSelection): boolean; collapseAtStart(selection: RangeSelection): boolean;
excludeFromCopy(): boolean; excludeFromCopy(): boolean;
canExtractContents(): boolean; canExtractContents(): boolean;

View File

@ -724,6 +724,7 @@ declare export class ElementNode extends LexicalNode {
setIndent(indentLevel: number): this; setIndent(indentLevel: number): this;
insertNewAfter(selection: RangeSelection): null | LexicalNode; insertNewAfter(selection: RangeSelection): null | LexicalNode;
canInsertTab(): boolean; canInsertTab(): boolean;
canIndent(): boolean;
collapseAtStart(selection: RangeSelection): boolean; collapseAtStart(selection: RangeSelection): boolean;
excludeFromCopy(): boolean; excludeFromCopy(): boolean;
canExtractContents(): boolean; canExtractContents(): boolean;

View File

@ -118,7 +118,7 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
function setElementIndent(dom: HTMLElement, indent: number): void { function setElementIndent(dom: HTMLElement, indent: number): void {
dom.style.setProperty( dom.style.setProperty(
'padding-inline-start', 'padding-inline-start',
indent === 0 ? '' : indent * 40 + 'px', indent === 0 ? '' : indent * 20 + 'px',
); );
} }

View File

@ -410,6 +410,9 @@ export class ElementNode extends LexicalNode {
canInsertTab(): boolean { canInsertTab(): boolean {
return false; return false;
} }
canIndent(): boolean {
return true;
}
collapseAtStart(selection: RangeSelection): boolean { collapseAtStart(selection: RangeSelection): boolean {
return false; return false;
} }