Skip to content

Commit 02e6cd9

Browse files
committed
fix: additional fixes to list indent/outdent, split list, toggle list, types and more tests
1 parent 488047f commit 02e6cd9

8 files changed

Lines changed: 915 additions & 159 deletions

File tree

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default [
9191
}
9292
},
9393
rules: {
94-
'no-unused-vars': 'warn', // See warnings but don't block
94+
'no-unused-vars': ['warn', { "varsIgnorePattern": "^_" }], // See warnings but don't block
9595

9696
// Relax these rules - they're more style than bugs
9797
'no-empty': ['warn', { allowEmptyCatch: true }], // Allow empty catch blocks

packages/super-editor/src/core/commands/decreaseListIndent.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,60 @@
1+
// @ts-check
2+
import { findParentNode } from '@helpers/index.js';
13
import { ListHelpers } from '@helpers/list-numbering-helpers.js';
24

35
/**
46
* Decreases the indent level of the current list item.
5-
* If the current item is at level 0, it converts it to a paragraph.
6-
* If the current item is at level 1 or higher, it decreases the level and updates the list structure.
77
* @returns {Function} A ProseMirror command function.
88
*/
99
export const decreaseListIndent =
1010
() =>
1111
({ editor, tr }) => {
1212
const { state } = editor;
13-
const currentNode = ListHelpers.getCurrentListItem(state);
14-
if (!currentNode) return false;
1513

16-
const parentList = ListHelpers.getParentOrderedList(state);
14+
// 1) Current list item
15+
const currentItem =
16+
(ListHelpers.getCurrentListItem && ListHelpers.getCurrentListItem(state)) ||
17+
findParentNode((n) => n.type && n.type.name === 'listItem')(state.selection);
18+
if (!currentItem) return false;
19+
20+
// 2) Parent list (ordered OR bullet)
21+
const parentOrdered = ListHelpers.getParentOrderedList && ListHelpers.getParentOrderedList(state);
22+
const parentBullet = ListHelpers.getParentBulletList && ListHelpers.getParentBulletList(state);
23+
24+
const parentList =
25+
parentOrdered ||
26+
parentBullet ||
27+
findParentNode((n) => n.type && (n.type.name === 'orderedList' || n.type.name === 'bulletList'))(state.selection);
1728
if (!parentList) return false;
1829

19-
const currentLevel = currentNode.node.attrs.level;
20-
const newLevel = currentLevel - 1;
30+
const attrs = currentItem.node.attrs || {};
31+
const currLevel = typeof attrs.level === 'number' ? attrs.level : 0;
2132

22-
// Don't allow negative levels
23-
if (newLevel < 0) {
24-
return false;
33+
// No-op at level 0 (consume the key so the browser doesn't Shift-Tab focus)
34+
if (currLevel <= 0) {
35+
return true;
2536
}
2637

27-
const numId = currentNode.node.attrs.numId;
38+
// Decrease level by 1; keep/repair numId
39+
const newLevel = currLevel - 1;
40+
let numId =
41+
attrs.numId ??
42+
parentList.node?.attrs?.listId ??
43+
(ListHelpers.getNewListId ? ListHelpers.getNewListId(editor) : null);
44+
45+
// Ensure definition exists for this list/id (safe no-op if already exists)
46+
if (numId != null && ListHelpers.generateNewListDefinition) {
47+
ListHelpers.generateNewListDefinition({
48+
numId,
49+
listType: parentList.node.type, // orderedList or bulletList NodeType
50+
editor,
51+
});
52+
}
2853

29-
tr.setNodeMarkup(currentNode.pos, null, {
30-
...currentNode.node.attrs,
54+
tr.setNodeMarkup(currentItem.pos, null, {
55+
...attrs,
3156
level: newLevel,
32-
numId: numId,
57+
numId,
3358
});
3459

3560
return true;
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// @ts-check
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import { ListHelpers } from '../helpers/list-numbering-helpers.js';
4+
import { decreaseListIndent } from './decreaseListIndent.js';
5+
6+
// Mock the helper modules used by the command
7+
vi.mock('../helpers/list-numbering-helpers.js', () => {
8+
const fns = {
9+
getCurrentListItem: vi.fn(),
10+
getParentOrderedList: vi.fn(),
11+
getParentBulletList: vi.fn(),
12+
getNewListId: vi.fn(),
13+
generateNewListDefinition: vi.fn(),
14+
};
15+
return { ListHelpers: fns };
16+
});
17+
18+
vi.mock('../helpers/index.js', () => {
19+
// The command falls back to findParentNode(...) only if the ListHelpers returns null.
20+
// We'll default to returning null so ListHelpers drive the tests.
21+
return {
22+
findParentNode: () => () => null,
23+
};
24+
});
25+
26+
describe('decreaseListIndent', () => {
27+
/** @type {{ state: any }} */
28+
let editor;
29+
/** @type {{ setNodeMarkup: ReturnType<typeof vi.fn> }} */
30+
let tr;
31+
32+
const OrderedListType = { name: 'orderedList' };
33+
const BulletListType = { name: 'bulletList' };
34+
35+
beforeEach(() => {
36+
vi.clearAllMocks();
37+
editor = { state: { selection: {} } };
38+
tr = { setNodeMarkup: vi.fn() };
39+
});
40+
41+
afterEach(() => {
42+
vi.resetModules();
43+
});
44+
45+
it('returns false when no current list item is found', () => {
46+
ListHelpers.getCurrentListItem.mockReturnValue(null);
47+
ListHelpers.getParentOrderedList.mockReturnValue(null);
48+
ListHelpers.getParentBulletList.mockReturnValue(null);
49+
50+
const result = decreaseListIndent()({ editor, tr });
51+
expect(result).toBe(false);
52+
expect(tr.setNodeMarkup).not.toHaveBeenCalled();
53+
});
54+
55+
it('returns false when no parent list is found', () => {
56+
const currentItem = {
57+
node: { type: { name: 'listItem' }, attrs: { level: 2, numId: 123 } },
58+
pos: 10,
59+
};
60+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
61+
ListHelpers.getParentOrderedList.mockReturnValue(null);
62+
ListHelpers.getParentBulletList.mockReturnValue(null);
63+
64+
const result = decreaseListIndent()({ editor, tr });
65+
expect(result).toBe(false);
66+
expect(tr.setNodeMarkup).not.toHaveBeenCalled();
67+
});
68+
69+
it('no-ops (returns true) at level 0 and does not mutate the doc', () => {
70+
const currentItem = {
71+
node: { type: { name: 'listItem' }, attrs: { level: 0 /* no numId */ } },
72+
pos: 5,
73+
};
74+
const parentList = {
75+
node: { type: OrderedListType, attrs: { listId: 777 } },
76+
};
77+
78+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
79+
ListHelpers.getParentOrderedList.mockReturnValue(parentList);
80+
ListHelpers.getParentBulletList.mockReturnValue(null);
81+
82+
const result = decreaseListIndent()({ editor, tr });
83+
expect(result).toBe(true);
84+
expect(tr.setNodeMarkup).not.toHaveBeenCalled();
85+
expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
86+
});
87+
88+
it('decreases level by 1 and keeps existing numId; ensures list definition', () => {
89+
const currentItem = {
90+
node: { type: { name: 'listItem' }, attrs: { level: 2, numId: 123, foo: 'bar' } },
91+
pos: 42,
92+
};
93+
const parentList = {
94+
node: { type: OrderedListType, attrs: { listId: 777 } },
95+
};
96+
97+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
98+
ListHelpers.getParentOrderedList.mockReturnValue(parentList);
99+
ListHelpers.getParentBulletList.mockReturnValue(null);
100+
101+
const result = decreaseListIndent()({ editor, tr });
102+
103+
expect(result).toBe(true);
104+
expect(tr.setNodeMarkup).toHaveBeenCalledTimes(1);
105+
expect(tr.setNodeMarkup).toHaveBeenCalledWith(42, null, {
106+
foo: 'bar',
107+
level: 1, // 2 -> 1
108+
numId: 123, // keeps existing
109+
});
110+
111+
expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledTimes(1);
112+
expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledWith({
113+
numId: 123,
114+
listType: OrderedListType,
115+
editor,
116+
});
117+
});
118+
119+
it('uses parent list listId when current item has no numId', () => {
120+
const currentItem = {
121+
node: { type: { name: 'listItem' }, attrs: { level: 3 } },
122+
pos: 7,
123+
};
124+
const parentList = {
125+
node: { type: BulletListType, attrs: { listId: 888 } },
126+
};
127+
128+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
129+
ListHelpers.getParentOrderedList.mockReturnValue(null);
130+
ListHelpers.getParentBulletList.mockReturnValue(parentList);
131+
132+
const result = decreaseListIndent()({ editor, tr });
133+
134+
expect(result).toBe(true);
135+
expect(tr.setNodeMarkup).toHaveBeenCalledWith(7, null, {
136+
level: 2, // 3 -> 2
137+
numId: 888, // inherited from parent
138+
});
139+
140+
expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledWith({
141+
numId: 888,
142+
listType: BulletListType,
143+
editor,
144+
});
145+
});
146+
147+
it('falls back to ListHelpers.getNewListId when neither item nor parent have ids', () => {
148+
const currentItem = {
149+
node: { type: { name: 'listItem' }, attrs: { level: 1 } },
150+
pos: 11,
151+
};
152+
const parentList = {
153+
node: {
154+
type: OrderedListType,
155+
attrs: {
156+
/* no listId */
157+
},
158+
},
159+
};
160+
161+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
162+
ListHelpers.getParentOrderedList.mockReturnValue(parentList);
163+
ListHelpers.getParentBulletList.mockReturnValue(null);
164+
ListHelpers.getNewListId.mockReturnValue(9999);
165+
166+
const result = decreaseListIndent()({ editor, tr });
167+
168+
expect(result).toBe(true);
169+
expect(ListHelpers.getNewListId).toHaveBeenCalledWith(editor);
170+
expect(tr.setNodeMarkup).toHaveBeenCalledWith(11, null, {
171+
level: 0, // 1 -> 0
172+
numId: 9999, // fallback
173+
});
174+
175+
expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledWith({
176+
numId: 9999,
177+
listType: OrderedListType,
178+
editor,
179+
});
180+
});
181+
182+
it('does not generate a list definition if resolved numId is null/undefined', () => {
183+
const currentItem = {
184+
node: { type: { name: 'listItem' }, attrs: { level: 2 } },
185+
pos: 21,
186+
};
187+
const parentList = {
188+
node: { type: OrderedListType, attrs: {} }, // no listId
189+
};
190+
191+
ListHelpers.getCurrentListItem.mockReturnValue(currentItem);
192+
ListHelpers.getParentOrderedList.mockReturnValue(parentList);
193+
ListHelpers.getParentBulletList.mockReturnValue(null);
194+
ListHelpers.getNewListId.mockReturnValue(null); // still no id
195+
196+
const result = decreaseListIndent()({ editor, tr });
197+
198+
expect(result).toBe(true);
199+
expect(tr.setNodeMarkup).toHaveBeenCalledWith(21, null, {
200+
level: 1,
201+
numId: null, // explicit null is fine; command should still set it
202+
});
203+
expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
204+
});
205+
});
Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,56 @@
1+
import { findParentNode } from '@helpers/index.js';
12
import { ListHelpers } from '@helpers/list-numbering-helpers.js';
23

34
/**
45
* Increases the indent level of the current list item.
5-
* @returns {Function} A ProseMirror command function.
6+
* Works for both ordered and bullet lists, including lists toggled from ordered→bullet.
67
*/
78
export const increaseListIndent =
89
() =>
910
({ editor, tr }) => {
1011
const { state } = editor;
11-
const currentNode = ListHelpers.getCurrentListItem(state);
12-
if (!currentNode) return false;
1312

14-
const parentList = ListHelpers.getParentOrderedList(state);
15-
if (!parentList) return false;
13+
// 1) Current list item (prefer your helper; fallback to generic)
14+
const currentItem =
15+
(ListHelpers.getCurrentListItem && ListHelpers.getCurrentListItem(state)) ||
16+
findParentNode((n) => n.type && n.type.name === 'listItem')(state.selection);
17+
if (!currentItem) return false;
1618

17-
const newLevel = currentNode.node.attrs.level + 1;
18-
const numId = currentNode.node.attrs.numId;
19+
// 2) Parent list (ordered OR bullet). Try helpers if available; otherwise generic.
20+
const parentOrdered = ListHelpers.getParentOrderedList && ListHelpers.getParentOrderedList(state);
21+
const parentBullet = ListHelpers.getParentBulletList && ListHelpers.getParentBulletList(state);
1922

20-
tr.setNodeMarkup(currentNode.pos, null, {
21-
...currentNode.node.attrs,
23+
const parentList =
24+
parentOrdered ||
25+
parentBullet ||
26+
findParentNode((n) => n.type && (n.type.name === 'orderedList' || n.type.name === 'bulletList'))(state.selection);
27+
28+
if (!parentList) return false; // not inside a list container
29+
30+
// 3) Compute new level; preserve numId if present (your bullets carry numId after toggle)
31+
const currAttrs = currentItem.node.attrs || {};
32+
const newLevel = (typeof currAttrs.level === 'number' ? currAttrs.level : 0) + 1;
33+
34+
// If numId is missing (edge-case), try to inherit from parent or mint a new one.
35+
let numId = currAttrs.numId;
36+
if (numId == null) {
37+
// Prefer container's listId if present, else generate
38+
numId = parentList.node?.attrs?.listId ?? ListHelpers.getNewListId(editor);
39+
// Ensure definition exists for this list type/id (safe no-op if already exists)
40+
if (ListHelpers.generateNewListDefinition) {
41+
const listType =
42+
parentList.node.type === editor.schema.nodes.orderedList
43+
? editor.schema.nodes.orderedList
44+
: editor.schema.nodes.bulletList;
45+
ListHelpers.generateNewListDefinition({ numId, listType, editor });
46+
}
47+
}
48+
49+
tr.setNodeMarkup(currentItem.pos, null, {
50+
...currAttrs,
2251
level: newLevel,
23-
numId: numId,
52+
numId,
2453
});
2554

26-
return true;
55+
return true; // IMPORTANT: consume Tab so we don't indent paragraph text
2756
};

0 commit comments

Comments
 (0)