Skip to content

Commit

Permalink
feat(list): enhance rich text list editing experience (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
giamir authored Mar 28, 2024
1 parent 1a289c0 commit 998c820
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 5 deletions.
5 changes: 4 additions & 1 deletion config/jest-unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ module.exports = {
},
rootDir: "../",
testPathIgnorePatterns: ["/node_modules/", String.raw`\.e2e\.test`],
setupFilesAfterEnv: ["<rootDir>/test/matchers.ts"],
setupFilesAfterEnv: [
"<rootDir>/test/setup.ts",
"<rootDir>/test/matchers.ts",
],
transform: {
"^.+\\.ts$": [
"ts-jest",
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"prosemirror-schema-list": "^1.3.0",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.8.0",
"prosemirror-utils": "^1.2.1-0",
"prosemirror-view": "^1.33.1"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/rich-text/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { insertParagraphIfAtDocEnd } from "./helpers";
import { inTable } from "./tables";

export * from "./tables";
export * from "./list";

// indent code with four [SPACE] characters (hope you aren't a "tabs" person)
const CODE_INDENT_STR = " ";
Expand Down
123 changes: 123 additions & 0 deletions src/rich-text/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NodeType, Node } from "prosemirror-model";
import { Command, EditorState, Transaction } from "prosemirror-state";
import { canJoin } from "prosemirror-transform";
import { findParentNode } from "prosemirror-utils";
import { wrapInList, liftListItem } from "prosemirror-schema-list";

/**
* Toggles a list.
* When the provided list type wrapper (e.g. bullet_list) is inactive then wrap the list with
* this type. When it is active then remove the selected line from the list.
*
* @param listType - the list node type
* @param itemType - the list item node type
*/
export function toggleList(listType: NodeType, itemType: NodeType): Command {
return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
const { $from, $to } = state.tr.selection;
const range = $from.blockRange($to);

if (!range) {
return false;
}

const parentList = findParentNode((node) => isListType(node.type))(
state.tr.selection
);

if (parentList) {
return liftListItem(itemType)(state, dispatch);
}

return wrapAndMaybeJoinList(listType)(state, dispatch);
};
}

/**
* Wraps the selected content in a list and attempts to join the newly wrapped list
* with exisiting list(s) of the same type.
*
* @param nodeType - the list node type
*/
export function wrapAndMaybeJoinList(nodeType: NodeType) {
return function (state: EditorState, dispatch: (tr: Transaction) => void) {
return wrapInList(nodeType)(state, (tr) => {
dispatch?.(tr);
const { tr: newTr } = state.apply(tr);
maybeJoinList(newTr);
dispatch?.(newTr);
});
};
}

/**
* Joins lists when they are of the same type.
* Inspired by https://github.com/remirror/remirror/blob/main/packages/remirror__extension-list/src/list-commands.ts#L535
*
* @param tr - the transaction
*/
export function maybeJoinList(tr: Transaction): boolean {
const $from = tr.selection.$from;

let joinable: number[] = [];
let index: number;
let parent: Node;
let before: Node | null | undefined;
let after: Node | null | undefined;

for (let depth = $from.depth; depth >= 0; depth--) {
parent = $from.node(depth);

// join backward
index = $from.index(depth);
before = parent.maybeChild(index - 1);
after = parent.maybeChild(index);

if (
before &&
after &&
before.type.name === after.type.name &&
isListType(before.type)
) {
const pos = $from.before(depth + 1);
joinable.push(pos);
}

// join forward
index = $from.indexAfter(depth);
before = parent.maybeChild(index - 1);
after = parent.maybeChild(index);

if (
before &&
after &&
before.type.name === after.type.name &&
isListType(before.type)
) {
const pos = $from.after(depth + 1);
joinable.push(pos);
}
}

// sort `joinable` reversely
joinable = [...new Set(joinable)].sort((a, b) => b - a);
let updated = false;

for (const pos of joinable) {
if (canJoin(tr.doc, pos)) {
tr.join(pos);
updated = true;
}
}

return updated;
}

/**
* Checks if the node type is a list type (e.g. "bullet_list", "ordered_list", etc...).
*
* @param type - the node type
*/
export function isListType(type: NodeType) {
return !!type.name.includes("_list");
}
5 changes: 3 additions & 2 deletions src/rich-text/key-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
unindentCodeBlockLinesCommand,
toggleHeadingLevel,
toggleTagLinkCommand,
toggleList,
} from "./commands";

export function allKeymaps(
Expand Down Expand Up @@ -74,8 +75,8 @@ export function allKeymaps(
"Mod-k": toggleMark(schema.marks.code),
"Mod-g": insertRichTextImageCommand,
"Ctrl-g": insertRichTextImageCommand,
"Mod-o": wrapIn(schema.nodes.ordered_list),
"Mod-u": wrapIn(schema.nodes.bullet_list),
"Mod-o": toggleList(schema.nodes.ordered_list, schema.nodes.list_item),
"Mod-u": toggleList(schema.nodes.bullet_list, schema.nodes.list_item),
"Mod-h": toggleHeadingLevel(),
"Mod-r": insertRichTextHorizontalRuleCommand,
"Mod-m": setBlockType(schema.nodes.code_block),
Expand Down
11 changes: 9 additions & 2 deletions src/shared/menu/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
insertRichTextImageCommand,
insertRichTextHorizontalRuleCommand,
insertRichTextTableCommand,
toggleList,
} from "../../rich-text/commands";
import { _t } from "../localization";
import { makeMenuButton, makeMenuDropdown } from "./helpers";
Expand Down Expand Up @@ -440,7 +441,10 @@ export const createMenuEntries = (
{
key: "toggleOrderedList",
richText: {
command: toggleWrapIn(schema.nodes.ordered_list),
command: toggleList(
schema.nodes.ordered_list,
schema.nodes.list_item
),
active: nodeTypeActive(schema.nodes.ordered_list),
},
commonmark: orderedListCommand,
Expand All @@ -455,7 +459,10 @@ export const createMenuEntries = (
{
key: "toggleUnorderedList",
richText: {
command: toggleWrapIn(schema.nodes.bullet_list),
command: toggleList(
schema.nodes.bullet_list,
schema.nodes.list_item
),
active: nodeTypeActive(schema.nodes.bullet_list),
},
commonmark: unorderedListCommand,
Expand Down
Loading

0 comments on commit 998c820

Please sign in to comment.