Skip to content

Commit 669a196

Browse files
committed
feat(Clipboard): updated trimListItems
1 parent 5e447e2 commit 669a196

File tree

5 files changed

+72
-62
lines changed

5 files changed

+72
-62
lines changed

src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type {Node, NodeSpec} from 'prosemirror-model';
1+
import type {NodeSpec} from 'prosemirror-model';
2+
3+
import {isEmptyString} from 'src/utils';
24

35
import type {ExtensionAuto} from '../../../../core';
46
import {nodeTypeFactory} from '../../../../utils/schema';
@@ -92,19 +94,3 @@ export const BaseSchemaSpecs: ExtensionAuto<BaseSchemaSpecsOptions> = (builder,
9294
},
9395
}));
9496
};
95-
96-
const isEmptyString = (node: Node) => {
97-
if (!node.content.size) {
98-
return true;
99-
}
100-
101-
if (
102-
node.childCount === 1 &&
103-
node.child(0).type.name === 'text' &&
104-
node.child(0).text?.trim() === ''
105-
) {
106-
return true;
107-
}
108-
109-
return false;
110-
};

src/extensions/behavior/Clipboard/clipboard.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,12 +297,6 @@ function getCopyContent(state: EditorState): Slice {
297297

298298
let slice = getSelectionContent(sel);
299299

300-
// trim empty list items
301-
const trimedContent = trimListItems(slice.content);
302-
if (trimedContent !== slice.content) {
303-
slice = new Slice(trimedContent, 1, 1);
304-
}
305-
306300
// replace first node with paragraph if needed
307301
if (
308302
slice.content.firstChild?.isTextblock &&
@@ -326,6 +320,12 @@ function getCopyContent(state: EditorState): Slice {
326320
slice.openStart,
327321
);
328322
}
323+
} else {
324+
// trim empty list items
325+
const trimedContent = trimListItems(slice.content);
326+
if (trimedContent !== slice.content) {
327+
slice = new Slice(trimedContent, 1, 1);
328+
}
329329
}
330330

331331
return slice;

src/extensions/behavior/Clipboard/utils.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ describe('trimListItems', () => {
6363
);
6464
});
6565

66-
it('removes an entire list if it contains only empty list items', () => {
66+
it('trims a list to one empty list item if it contains only empty list items', () => {
6767
const fragment = doc(bulletList(listItem(), listItem(), listItem())).content;
6868

6969
const trimmedFragment = trimListItems(fragment);
70-
expect(schema.nodes.doc.create(null, trimmedFragment)).toMatchNode(doc());
70+
expect(schema.nodes.doc.create(null, trimmedFragment)).toMatchNode(
71+
doc(bulletList(listItem())),
72+
);
7173
});
7274

7375
it('correctly handles multiple lists in a single fragment', () => {

src/extensions/behavior/Clipboard/utils.ts

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {Fragment, type Node} from 'prosemirror-model';
22

3+
import {isListItemNode, isListNode} from 'src/extensions/markdown/Lists/utils';
4+
import {isEmptyString} from 'src/utils';
5+
36
/** Сontains all data formats known to us */
47
export enum DataTransferType {
58
Text = 'text/plain',
@@ -59,55 +62,58 @@ export function extractTextContentFromHtml(html: string) {
5962
return null;
6063
}
6164

65+
function isListItemEmpty(node: Node): boolean {
66+
let isEmpty = true;
67+
node.content.forEach((child) => {
68+
if (!isEmptyString(child)) {
69+
isEmpty = false;
70+
}
71+
});
72+
return isEmpty;
73+
}
74+
6275
export function trimListItems(fragment: Fragment): Fragment {
6376
let modified = false;
6477
const newChildren: Node[] = [];
6578

6679
fragment.forEach((contentNode) => {
67-
if (['bullet_list', 'ordered_list'].includes(contentNode.type.name)) {
68-
const listItems: Node[] = [];
69-
let firstNonEmptyFound = false;
70-
let lastNonEmptyIndex = -1;
71-
72-
contentNode.forEach((node) => {
73-
if (node.type.name === 'list_item') {
74-
let hasContent = false;
75-
76-
node.content.forEach((child) => {
77-
if (
78-
(child.isText && child.text?.trim().length) ||
79-
(!child.isText && child.content.size > 0)
80-
) {
81-
hasContent = true;
82-
}
83-
});
84-
85-
if (hasContent) {
86-
firstNonEmptyFound = true;
87-
lastNonEmptyIndex = listItems.length;
88-
} else if (!firstNonEmptyFound) {
89-
modified = true;
90-
return;
91-
}
92-
}
80+
let result = contentNode;
9381

94-
listItems.push(node);
95-
});
96-
97-
if (lastNonEmptyIndex >= 0 && listItems.length > lastNonEmptyIndex + 1) {
98-
modified = true;
82+
if (isListNode(contentNode)) {
83+
const itemsArray: Node[] = Array.from(contentNode.content.content);
84+
if (itemsArray.length === 0) {
85+
return;
9986
}
10087

101-
if (modified) {
102-
listItems.length = lastNonEmptyIndex + 1;
88+
const initialStart = 0;
89+
const initialEnd = itemsArray.length - 1;
90+
let start = initialStart;
91+
let end = initialEnd;
92+
93+
while (start <= end) {
94+
if (isListItemNode(itemsArray[start])) {
95+
if (!isListItemEmpty(itemsArray[start])) {
96+
break;
97+
}
98+
start++;
99+
}
100+
101+
if (start <= end && isListItemNode(itemsArray[end])) {
102+
if (!isListItemEmpty(itemsArray[end])) {
103+
break;
104+
}
105+
end--;
106+
}
103107
}
104108

105-
if (listItems.length > 0) {
106-
newChildren.push(contentNode.copy(Fragment.fromArray(listItems)));
109+
if (start !== initialStart || end !== initialEnd) {
110+
const pos = start > end ? [0, 1] : [start, end + 1];
111+
result = contentNode.copy(Fragment.fromArray(itemsArray.slice(...pos)));
112+
modified = true;
107113
}
108-
} else {
109-
newChildren.push(contentNode);
110114
}
115+
116+
newChildren.push(result);
111117
});
112118

113119
return modified ? Fragment.fromArray(newChildren) : fragment;

src/utils/nodes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,19 @@ export function getLastChildOfNode(node: Node | Fragment): NodeChild {
6060
const children = getChildrenOfNode(node);
6161
return children[children.length - 1];
6262
}
63+
64+
export const isEmptyString = (node: Node) => {
65+
if (!node.content.size) {
66+
return true;
67+
}
68+
69+
if (
70+
node.childCount === 1 &&
71+
node.child(0).type.name === 'text' &&
72+
node.child(0).text?.trim() === ''
73+
) {
74+
return true;
75+
}
76+
77+
return false;
78+
};

0 commit comments

Comments
 (0)