Skip to content

Commit 5c132f2

Browse files
committed
feat: refactor core
1 parent 4989ba5 commit 5c132f2

15 files changed

Lines changed: 615 additions & 7 deletions

packages/core/src/commands/attributes.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import {
66
type Transaction,
77
} from "@mxm-editor/pm";
88
import type { Editor } from "../Editor";
9-
import type { RawCommands } from "../types";
9+
import type {
10+
Range,
11+
RawCommands,
12+
} from "../types";
1013
import { getMarkRange } from "../helpers";
14+
import { clamp } from "../utilities";
1115

1216
function normalizeAttributeNames(attributes: string | string[]) {
1317
return Array.isArray(attributes) ? attributes : [attributes];
@@ -307,7 +311,42 @@ function resetNodeAttributes(
307311
return true;
308312
}
309313

310-
type AttributeCommands = Pick<RawCommands, "updateAttributes" | "resetAttributes">;
314+
function resolveRange(
315+
transaction: Transaction,
316+
position?: number | Range,
317+
) {
318+
if (typeof position === "number") {
319+
const nextPosition = clamp(position, 0, transaction.doc.content.size);
320+
321+
return {
322+
from: nextPosition,
323+
to: nextPosition,
324+
};
325+
}
326+
327+
if (position) {
328+
const from = clamp(position.from, 0, transaction.doc.content.size);
329+
const to = clamp(position.to, from, transaction.doc.content.size);
330+
331+
return {
332+
from,
333+
to,
334+
};
335+
}
336+
337+
return {
338+
from: transaction.selection.from,
339+
to: transaction.selection.to,
340+
};
341+
}
342+
343+
type AttributeCommands = Pick<
344+
RawCommands,
345+
| "updateAttributes"
346+
| "resetAttributes"
347+
| "setTextDirection"
348+
| "unsetTextDirection"
349+
>;
311350

312351
export function createAttributeCommands(editor: Editor): AttributeCommands {
313352
return {
@@ -356,5 +395,58 @@ export function createAttributeCommands(editor: Editor): AttributeCommands {
356395

357396
return false;
358397
},
398+
setTextDirection:
399+
(
400+
direction: "ltr" | "rtl" | "auto",
401+
position?: number | Range,
402+
) =>
403+
({ tr, dispatch }) => {
404+
const range = resolveRange(tr, position);
405+
406+
if (!dispatch) {
407+
return true;
408+
}
409+
410+
tr.doc.nodesBetween(range.from, range.to, (node, pos) => {
411+
if (node.isText || !node.type.spec.attrs?.dir) {
412+
return true;
413+
}
414+
415+
tr.setNodeMarkup(pos, undefined, {
416+
...node.attrs,
417+
dir: direction,
418+
});
419+
420+
return true;
421+
});
422+
423+
return true;
424+
},
425+
unsetTextDirection:
426+
(position?: number | Range) =>
427+
({ tr, dispatch }) => {
428+
const range = resolveRange(tr, position);
429+
430+
if (!dispatch) {
431+
return true;
432+
}
433+
434+
tr.doc.nodesBetween(range.from, range.to, (node, pos) => {
435+
if (node.isText || !node.type.spec.attrs?.dir) {
436+
return true;
437+
}
438+
439+
const nextAttributes = {
440+
...node.attrs,
441+
};
442+
443+
delete nextAttributes.dir;
444+
tr.setNodeMarkup(pos, undefined, nextAttributes);
445+
446+
return true;
447+
});
448+
449+
return true;
450+
},
359451
};
360452
}

packages/core/src/commands/content.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { selectionToInsertionEnd } from "../helpers";
1010
import type {
1111
Content,
12+
Range,
1213
InsertContentAtPosition,
1314
InsertContentOptions,
1415
RawCommands,
@@ -84,7 +85,7 @@ function isOnlyBlockContent(fragment: Fragment) {
8485

8586
type ContentCommands = Pick<
8687
RawCommands,
87-
"setContent" | "clearContent" | "insertContent" | "insertContentAt"
88+
"setContent" | "clearContent" | "insertContent" | "insertContentAt" | "cut"
8889
>;
8990

9091
export function createContentCommands(editor: Editor): ContentCommands {
@@ -174,6 +175,31 @@ export function createContentCommands(editor: Editor): ContentCommands {
174175
selectionToInsertionEnd(tr, startLength, -1);
175176
}
176177

178+
return true;
179+
},
180+
cut:
181+
(range: Range, targetPos: number) =>
182+
({ tr }) => {
183+
const from = clamp(range.from, 0, tr.doc.content.size);
184+
const to = clamp(range.to, from, tr.doc.content.size);
185+
const contentSlice = tr.doc.slice(from, to);
186+
187+
tr.deleteRange(from, to);
188+
189+
const newPosition = clamp(
190+
tr.mapping.map(targetPos),
191+
0,
192+
tr.doc.content.size,
193+
);
194+
195+
tr.insert(newPosition, contentSlice.content);
196+
tr.setSelection(
197+
Selection.near(
198+
tr.doc.resolve(clamp(Math.max(newPosition - 1, 0), 0, tr.doc.content.size)),
199+
1,
200+
),
201+
);
202+
177203
return true;
178204
},
179205
};

packages/core/src/commands/focus.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,23 @@ export function createFocusCommands(editor: Editor): FocusCommands {
9797
},
9898
blur:
9999
() =>
100-
({ dispatch }) => {
101-
if (dispatch) {
102-
editor.view?.dom.blur();
100+
({ view }) => {
101+
const run = () => {
102+
if (!view || editor.isDestroyed) {
103+
return;
104+
}
105+
106+
(view.dom as HTMLElement).blur();
107+
108+
if (typeof window !== "undefined") {
109+
window.getSelection()?.removeAllRanges();
110+
}
111+
};
112+
113+
if (typeof requestAnimationFrame === "function") {
114+
requestAnimationFrame(run);
115+
} else {
116+
run();
103117
}
104118

105119
return true;

packages/core/src/commands/node.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function joinListForwards(
9191

9292
type NodeCommands = Pick<
9393
RawCommands,
94+
| "deleteCurrentNode"
9495
| "deleteNode"
9596
| "clearNodes"
9697
| "wrapInList"
@@ -108,6 +109,36 @@ type NodeCommands = Pick<
108109

109110
export function createNodeCommands(editor: Editor): NodeCommands {
110111
return {
112+
deleteCurrentNode:
113+
() =>
114+
({ tr, dispatch }) => {
115+
const currentNode = tr.selection.$anchor.node();
116+
117+
if (currentNode.content.size > 0) {
118+
return false;
119+
}
120+
121+
const $position = tr.selection.$anchor;
122+
123+
for (let depth = $position.depth; depth > 0; depth -= 1) {
124+
const node = $position.node(depth);
125+
126+
if (node.type !== currentNode.type) {
127+
continue;
128+
}
129+
130+
if (dispatch) {
131+
const from = $position.before(depth);
132+
const to = $position.after(depth);
133+
134+
tr.delete(from, to).scrollIntoView();
135+
}
136+
137+
return true;
138+
}
139+
140+
return false;
141+
},
111142
deleteNode:
112143
(nameOrType: string | NodeType) =>
113144
({ tr, dispatch }) => {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Node as ProseMirrorNode } from "@mxm-editor/pm";
2+
3+
export function findChildrenInRange(
4+
node: ProseMirrorNode,
5+
range: { from: number; to: number },
6+
predicate: (node: ProseMirrorNode) => boolean,
7+
) {
8+
const nodes: Array<{ node: ProseMirrorNode; pos: number }> = [];
9+
10+
node.nodesBetween(range.from, range.to, (child, pos) => {
11+
if (predicate(child)) {
12+
nodes.push({
13+
node: child,
14+
pos,
15+
});
16+
}
17+
18+
return true;
19+
});
20+
21+
return nodes;
22+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type {
2+
EditorState,
3+
MarkType,
4+
NodeType,
5+
} from "@mxm-editor/pm";
6+
import { getMarkAttributes } from "./getMarkAttributes";
7+
import { getNodeAttributes } from "./getNodeAttributes";
8+
9+
export function getAttributes(
10+
state: EditorState,
11+
typeOrName: string | NodeType | MarkType,
12+
) {
13+
if (typeof typeOrName === "string") {
14+
if (state.schema.nodes[typeOrName]) {
15+
return getNodeAttributes(state, typeOrName);
16+
}
17+
18+
if (state.schema.marks[typeOrName]) {
19+
return getMarkAttributes(state, typeOrName);
20+
}
21+
22+
return {};
23+
}
24+
25+
if (state.schema.nodes[typeOrName.name] === typeOrName) {
26+
return getNodeAttributes(state, typeOrName);
27+
}
28+
29+
if (state.schema.marks[typeOrName.name] === typeOrName) {
30+
return getMarkAttributes(state, typeOrName);
31+
}
32+
33+
return {};
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type {
2+
EditorState,
3+
Mark as ProseMirrorMark,
4+
MarkType,
5+
} from "@mxm-editor/pm";
6+
import { getMarkType } from "./getMarkType";
7+
8+
export function getMarkAttributes(
9+
state: EditorState,
10+
typeOrName: string | MarkType,
11+
) {
12+
const type = getMarkType(typeOrName, state.schema);
13+
const { from, to, empty } = state.selection;
14+
const marks: ProseMirrorMark[] = [];
15+
16+
if (empty) {
17+
if (state.storedMarks) {
18+
marks.push(...state.storedMarks);
19+
}
20+
21+
marks.push(...state.selection.$head.marks());
22+
} else {
23+
state.doc.nodesBetween(from, to, (node) => {
24+
marks.push(...node.marks);
25+
return true;
26+
});
27+
}
28+
29+
const mark = marks.find((item) => item.type === type);
30+
31+
if (!mark) {
32+
return {};
33+
}
34+
35+
return {
36+
...mark.attrs,
37+
};
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type {
2+
EditorState,
3+
Node as ProseMirrorNode,
4+
NodeType,
5+
} from "@mxm-editor/pm";
6+
import { getNodeType } from "./getNodeType";
7+
8+
export function getNodeAtPosition(
9+
state: EditorState,
10+
typeOrName: string | NodeType,
11+
position: number,
12+
maxDepth = 20,
13+
): [ProseMirrorNode | null, number] {
14+
const type = getNodeType(typeOrName, state.schema);
15+
const $position = state.doc.resolve(position);
16+
let currentDepth = Math.min(maxDepth, $position.depth);
17+
let node: ProseMirrorNode | null = null;
18+
19+
while (currentDepth > 0 && node === null) {
20+
const currentNode = $position.node(currentDepth);
21+
22+
if (currentNode.type === type) {
23+
node = currentNode;
24+
break;
25+
}
26+
27+
currentDepth -= 1;
28+
}
29+
30+
return [node, currentDepth];
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type {
2+
EditorState,
3+
Node as ProseMirrorNode,
4+
NodeType,
5+
} from "@mxm-editor/pm";
6+
import { getNodeType } from "./getNodeType";
7+
8+
export function getNodeAttributes(
9+
state: EditorState,
10+
typeOrName: string | NodeType,
11+
) {
12+
const type = getNodeType(typeOrName, state.schema);
13+
const { from, to } = state.selection;
14+
const nodes: ProseMirrorNode[] = [];
15+
16+
state.doc.nodesBetween(from, to, (node) => {
17+
nodes.push(node);
18+
return true;
19+
});
20+
21+
const node = nodes.reverse().find((item) => item.type === type);
22+
23+
if (!node) {
24+
return {};
25+
}
26+
27+
return {
28+
...node.attrs,
29+
};
30+
}

0 commit comments

Comments
 (0)