mirror of
https://github.com/facebook/lexical.git
synced 2025-08-06 08:30:33 +08:00
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:
2
packages/lexical-code/LexicalCode.d.ts
vendored
2
packages/lexical-code/LexicalCode.d.ts
vendored
@ -28,7 +28,7 @@ declare class CodeNode extends ElementNode {
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
): null | ParagraphNode | CodeHighlightNode;
|
||||
canInsertTab(): true;
|
||||
canInsertTab(): boolean;
|
||||
collapseAtStart(): true;
|
||||
setLanguage(language: string): void;
|
||||
getLanguage(): string | void;
|
||||
|
@ -28,7 +28,7 @@ declare export class CodeNode extends ElementNode {
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
): null | ParagraphNode | CodeHighlightNode;
|
||||
canInsertTab(): true;
|
||||
canInsertTab(): boolean;
|
||||
collapseAtStart(): true;
|
||||
setLanguage(language: string): void;
|
||||
getLanguage(): string | void;
|
||||
|
@ -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 () => {
|
||||
const {editor} = testEnv;
|
||||
await editor.update(() => {
|
||||
|
@ -313,10 +313,18 @@ export class CodeNode extends ElementNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
canInsertTab(): true {
|
||||
canInsertTab(): boolean {
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
collapseAtStart(): true {
|
||||
const paragraph = $createParagraphNode();
|
||||
const children = this.getChildren();
|
||||
|
4
packages/lexical-list/LexicalList.d.ts
vendored
4
packages/lexical-list/LexicalList.d.ts
vendored
@ -22,7 +22,7 @@ export function $getListDepth(listNode: ListNode): number;
|
||||
export function $handleListInsertParagraph(): boolean;
|
||||
export function $isListItemNode(node?: LexicalNode): node is ListItemNode;
|
||||
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 declare class ListItemNode extends ElementNode {
|
||||
append(...nodes: LexicalNode[]): ListItemNode;
|
||||
@ -42,7 +42,7 @@ export declare class ListNode extends ElementNode {
|
||||
append(...nodesToAppend: LexicalNode[]): ListNode;
|
||||
getTag(): ListNodeTagType;
|
||||
}
|
||||
export function outdentList(): boolean;
|
||||
export function outdentList(): void;
|
||||
export function removeList(editor: LexicalEditor): boolean;
|
||||
|
||||
export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
|
@ -30,7 +30,7 @@ declare export function $isListItemNode(
|
||||
declare export function $isListNode(
|
||||
node: ?LexicalNode,
|
||||
): boolean %checks(node instanceof ListNode);
|
||||
declare export function indentList(): boolean;
|
||||
declare export function indentList(): void;
|
||||
declare export function insertList(
|
||||
editor: LexicalEditor,
|
||||
listType: 'ul' | 'ol',
|
||||
@ -56,7 +56,7 @@ declare export class ListNode extends ElementNode {
|
||||
getTag(): ListNodeTagType;
|
||||
getStart(): number;
|
||||
}
|
||||
declare export function outdentList(): boolean;
|
||||
declare export function outdentList(): void;
|
||||
declare export function removeList(editor: LexicalEditor): boolean;
|
||||
|
||||
declare export var INSERT_UNORDERED_LIST_COMMAND: LexicalCommand<void>;
|
||||
|
@ -227,6 +227,11 @@ export class ListItemNode extends ElementNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
// Indent/outdent is handled specifically in the RichText logic.
|
||||
return false;
|
||||
}
|
||||
|
||||
insertBefore(nodeToInsert: LexicalNode): LexicalNode {
|
||||
const siblings = this.getNextSiblings();
|
||||
if ($isListItemNode(nodeToInsert)) {
|
||||
|
@ -94,6 +94,10 @@ export class ListNode extends ElementNode {
|
||||
return false;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
append(...nodesToAppend: LexicalNode[]): ListNode {
|
||||
for (let i = 0; i < nodesToAppend.length; i++) {
|
||||
const currentNode = nodesToAppend[i];
|
||||
|
@ -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();
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const selectedNodes = selection.getNodes();
|
||||
let listItemNodes = [];
|
||||
@ -326,17 +326,15 @@ function maybeIndentOrOutdent(direction: 'indent' | 'outdent'): boolean {
|
||||
} else {
|
||||
$handleOutdent(listItemNodes);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function indentList(): boolean {
|
||||
return maybeIndentOrOutdent('indent');
|
||||
export function indentList(): void {
|
||||
maybeIndentOrOutdent('indent');
|
||||
}
|
||||
|
||||
export function outdentList(): boolean {
|
||||
return maybeIndentOrOutdent('outdent');
|
||||
export function outdentList(): void {
|
||||
maybeIndentOrOutdent('outdent');
|
||||
}
|
||||
|
||||
export function $handleListInsertParagraph(): boolean {
|
||||
|
@ -39,7 +39,7 @@ test.describe('Element format', () => {
|
||||
html`
|
||||
<p
|
||||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
|
||||
style="padding-inline-start: 80px; text-align: center;"
|
||||
style="padding-inline-start: 40px; text-align: center;"
|
||||
dir="ltr">
|
||||
<span data-lexical-text="true">Hello</span>
|
||||
<a
|
||||
|
1380
packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs
Normal file
1380
packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@ -34,10 +34,7 @@ export default function useList(editor: LexicalEditor): void {
|
||||
editor.registerCommand(
|
||||
INDENT_CONTENT_COMMAND,
|
||||
() => {
|
||||
const hasHandledIndention = indentList();
|
||||
if (hasHandledIndention) {
|
||||
return true;
|
||||
}
|
||||
indentList();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
@ -45,10 +42,7 @@ export default function useList(editor: LexicalEditor): void {
|
||||
editor.registerCommand(
|
||||
OUTDENT_CONTENT_COMMAND,
|
||||
() => {
|
||||
const hasHandledIndention = outdentList();
|
||||
if (hasHandledIndention) {
|
||||
return true;
|
||||
}
|
||||
outdentList();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
|
@ -41,6 +41,7 @@ import {
|
||||
$isGridSelection,
|
||||
$isNodeSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
CLICK_COMMAND,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
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(
|
||||
editor: LexicalEditor,
|
||||
initialEditorState?: InitialEditorStateType,
|
||||
@ -478,22 +506,17 @@ export function registerRichText(
|
||||
editor.registerCommand(
|
||||
INDENT_CONTENT_COMMAND,
|
||||
(payload) => {
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
// Handle code blocks
|
||||
const anchor = selection.anchor;
|
||||
const parentBlock = $getNearestBlockElementAncestorOrThrow(
|
||||
anchor.getNode(),
|
||||
handleIndentAndOutdent(
|
||||
() => {
|
||||
editor.dispatchCommand(INSERT_TEXT_COMMAND, '\t');
|
||||
},
|
||||
(block) => {
|
||||
const indent = block.getIndent();
|
||||
if (indent !== 10) {
|
||||
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;
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
@ -501,27 +524,23 @@ export function registerRichText(
|
||||
editor.registerCommand(
|
||||
OUTDENT_CONTENT_COMMAND,
|
||||
(payload) => {
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
// Handle code blocks
|
||||
const anchor = selection.anchor;
|
||||
const anchorNode = anchor.getNode();
|
||||
const parentBlock = $getNearestBlockElementAncestorOrThrow(
|
||||
anchor.getNode(),
|
||||
handleIndentAndOutdent(
|
||||
(node) => {
|
||||
if ($isTextNode(node)) {
|
||||
const textContent = node.getTextContent();
|
||||
const character = textContent[textContent.length - 1];
|
||||
if (character === '\t') {
|
||||
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
(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;
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
|
3
packages/lexical-table/LexicalTable.d.ts
vendored
3
packages/lexical-table/LexicalTable.d.ts
vendored
@ -54,7 +54,6 @@ export declare class TableCellNode extends ElementNode {
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
): null | ParagraphNode | TableCellNode;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
getTag(): string;
|
||||
setHeaderState(headerState: TableCellHeaderState): TableCellHeaderState;
|
||||
@ -83,7 +82,6 @@ export declare class TableNode extends ElementNode {
|
||||
createDOM(config: EditorConfig): HTMLElement;
|
||||
updateDOM(prevNode: TableNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
getCordsFromCellNode(tableCellNode: TableCellNode): {x: number; y: number};
|
||||
getCellFromCords(x: number, y: number, grid: Grid): ?Cell;
|
||||
@ -112,7 +110,6 @@ declare class TableRowNode extends ElementNode {
|
||||
): null | ParagraphNode | TableRowNode;
|
||||
setHeight(height: number): ?number;
|
||||
getHeight(): ?number;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
}
|
||||
declare function $createTableRowNode(): TableRowNode;
|
||||
|
@ -48,7 +48,6 @@ declare export class TableCellNode extends ElementNode {
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
): null | ParagraphNode | TableCellNode;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
getTag(): string;
|
||||
setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState;
|
||||
@ -81,7 +80,6 @@ declare export class TableNode extends ElementNode {
|
||||
createDOM(config: EditorConfig): HTMLElement;
|
||||
updateDOM(prevNode: TableNode, dom: HTMLElement): boolean;
|
||||
insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
getCordsFromCellNode(
|
||||
tableCellNode: TableCellNode,
|
||||
@ -115,7 +113,6 @@ declare export class TableRowNode extends ElementNode {
|
||||
insertNewAfter(
|
||||
selection: RangeSelection,
|
||||
): null | ParagraphNode | TableRowNode;
|
||||
canInsertTab(): true;
|
||||
collapseAtStart(): true;
|
||||
}
|
||||
declare export function $createTableRowNode(): TableRowNode;
|
||||
|
@ -175,6 +175,10 @@ export class TableCellNode extends GridCellNode {
|
||||
canBeEmpty(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertTableCellNodeElement(
|
||||
|
@ -183,6 +183,10 @@ export class TableNode extends GridNode {
|
||||
canSelectBefore(): true {
|
||||
return true;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function $getElementGridForTableNode(
|
||||
|
@ -72,6 +72,10 @@ export class TableRowNode extends GridRowNode {
|
||||
canBeEmpty(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
canIndent(): false {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertTableRowElement(domNode: Node): DOMConversionOutput {
|
||||
|
1
packages/lexical/Lexical.d.ts
vendored
1
packages/lexical/Lexical.d.ts
vendored
@ -696,6 +696,7 @@ export declare class ElementNode extends LexicalNode {
|
||||
setIndent(indentLevel: number): ElementNode;
|
||||
insertNewAfter(selection: RangeSelection): null | LexicalNode;
|
||||
canInsertTab(): boolean;
|
||||
canIndent(): boolean;
|
||||
collapseAtStart(selection: RangeSelection): boolean;
|
||||
excludeFromCopy(): boolean;
|
||||
canExtractContents(): boolean;
|
||||
|
@ -724,6 +724,7 @@ declare export class ElementNode extends LexicalNode {
|
||||
setIndent(indentLevel: number): this;
|
||||
insertNewAfter(selection: RangeSelection): null | LexicalNode;
|
||||
canInsertTab(): boolean;
|
||||
canIndent(): boolean;
|
||||
collapseAtStart(selection: RangeSelection): boolean;
|
||||
excludeFromCopy(): boolean;
|
||||
canExtractContents(): boolean;
|
||||
|
@ -118,7 +118,7 @@ function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
|
||||
function setElementIndent(dom: HTMLElement, indent: number): void {
|
||||
dom.style.setProperty(
|
||||
'padding-inline-start',
|
||||
indent === 0 ? '' : indent * 40 + 'px',
|
||||
indent === 0 ? '' : indent * 20 + 'px',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -410,6 +410,9 @@ export class ElementNode extends LexicalNode {
|
||||
canInsertTab(): boolean {
|
||||
return false;
|
||||
}
|
||||
canIndent(): boolean {
|
||||
return true;
|
||||
}
|
||||
collapseAtStart(selection: RangeSelection): boolean {
|
||||
return false;
|
||||
}
|
||||
|
Reference in New Issue
Block a user