@thomasfosterau/effect-richtext 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +118 -2
- package/dist/_virtual/_rolldown/runtime.js +12 -0
- package/dist/commands/commands.d.ts +323 -0
- package/dist/commands/commands.js +722 -0
- package/dist/commands/internal.js +43 -0
- package/dist/commands/keymaps.d.ts +46 -0
- package/dist/commands/keymaps.js +64 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +3 -0
- package/dist/compat/commands.d.ts +250 -0
- package/dist/compat/commands.js +275 -0
- package/dist/compat/internal/core.d.ts +20 -0
- package/dist/compat/internal/core.js +148 -0
- package/dist/compat/internal/props.js +82 -0
- package/dist/compat/keymap.d.ts +21 -0
- package/dist/compat/keymap.js +38 -0
- package/dist/compat/model.d.ts +893 -0
- package/dist/compat/model.js +1212 -0
- package/dist/compat/orderedmap.d.ts +74 -0
- package/dist/compat/orderedmap.js +124 -0
- package/dist/compat/schema-basic.d.ts +38 -0
- package/dist/compat/schema-basic.js +238 -0
- package/dist/compat/schema-list.d.ts +81 -0
- package/dist/compat/schema-list.js +156 -0
- package/dist/compat/state.d.ts +415 -0
- package/dist/compat/state.js +689 -0
- package/dist/compat/test-builder.d.ts +98 -0
- package/dist/compat/test-builder.js +197 -0
- package/dist/compat/transform.d.ts +458 -0
- package/dist/compat/transform.js +803 -0
- package/dist/compat/view.d.ts +411 -0
- package/dist/compat/view.js +409 -0
- package/dist/dropcursor.d.ts +46 -0
- package/dist/dropcursor.js +176 -0
- package/dist/extensions/defaults.d.ts +716 -0
- package/dist/extensions/defaults.js +209 -0
- package/dist/extensions/editor.d.ts +246 -0
- package/dist/extensions/editor.js +271 -0
- package/dist/extensions/extension.d.ts +375 -0
- package/dist/extensions/extension.js +233 -0
- package/dist/extensions.d.ts +4 -0
- package/dist/extensions.js +4 -0
- package/dist/gapcursor.d.ts +66 -0
- package/dist/gapcursor.js +273 -0
- package/dist/history.d.ts +178 -0
- package/dist/history.js +412 -0
- package/dist/html.d.ts +158 -0
- package/dist/html.js +164 -0
- package/dist/inputrules.d.ts +271 -0
- package/dist/inputrules.js +321 -0
- package/dist/internal/attempt.js +15 -0
- package/dist/internal/json.js +29 -0
- package/dist/keymap/keymap.d.ts +130 -0
- package/dist/keymap/keymap.js +159 -0
- package/dist/keymap.d.ts +2 -0
- package/dist/keymap.js +1 -0
- package/dist/markdown/errors.d.ts +23 -0
- package/dist/markdown/errors.js +26 -0
- package/dist/markdown/from_markdown.d.ts +185 -0
- package/dist/markdown/from_markdown.js +266 -0
- package/dist/markdown/schema.d.ts +522 -0
- package/dist/markdown/schema.js +292 -0
- package/dist/markdown/to_markdown.d.ts +251 -0
- package/dist/markdown/to_markdown.js +413 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +5 -0
- package/dist/model/attrsCodec.js +48 -0
- package/dist/model/comparedeep.js +17 -0
- package/dist/model/content.d.ts +232 -0
- package/dist/model/content.js +630 -0
- package/dist/model/diff.js +66 -0
- package/dist/model/dom.d.ts +3 -0
- package/dist/model/errors.d.ts +90 -0
- package/dist/model/errors.js +76 -0
- package/dist/model/fragment.d.ts +131 -0
- package/dist/model/fragment.js +262 -0
- package/dist/model/from_dom.d.ts +299 -0
- package/dist/model/from_dom.js +618 -0
- package/dist/model/json.d.ts +37 -0
- package/dist/model/json.js +126 -0
- package/dist/model/mark.d.ts +73 -0
- package/dist/model/mark.js +121 -0
- package/dist/model/node.d.ts +267 -0
- package/dist/model/node.js +434 -0
- package/dist/model/replace.d.ts +78 -0
- package/dist/model/replace.js +207 -0
- package/dist/model/resolvedpos.d.ts +196 -0
- package/dist/model/resolvedpos.js +305 -0
- package/dist/model/schema.d.ts +740 -0
- package/dist/model/schema.js +451 -0
- package/dist/model/to_dom.d.ts +169 -0
- package/dist/model/to_dom.js +238 -0
- package/dist/model.d.ts +12 -0
- package/dist/model.js +12 -0
- package/dist/placeholder.d.ts +53 -0
- package/dist/placeholder.js +79 -0
- package/dist/reactive/observer.d.ts +54 -0
- package/dist/reactive/observer.js +61 -0
- package/dist/reactive/selectors.d.ts +22 -0
- package/dist/reactive/selectors.js +61 -0
- package/dist/reactive/stateref.d.ts +72 -0
- package/dist/reactive/stateref.js +71 -0
- package/dist/reactive/streams.d.ts +83 -0
- package/dist/reactive/streams.js +85 -0
- package/dist/reactive.d.ts +16 -0
- package/dist/reactive.js +26 -0
- package/dist/schema-basic.d.ts +448 -0
- package/dist/schema-basic.js +269 -0
- package/dist/schema-list/commands.d.ts +66 -0
- package/dist/schema-list/commands.js +203 -0
- package/dist/schema-list.d.ts +92 -0
- package/dist/schema-list.js +100 -0
- package/dist/state/errors.d.ts +39 -0
- package/dist/state/errors.js +32 -0
- package/dist/state/plugin.d.ts +232 -0
- package/dist/state/plugin.js +82 -0
- package/dist/state/selection.d.ts +297 -0
- package/dist/state/selection.js +486 -0
- package/dist/state/state.d.ts +212 -0
- package/dist/state/state.js +489 -0
- package/dist/state/transaction.d.ts +145 -0
- package/dist/state/transaction.js +195 -0
- package/dist/state.d.ts +6 -0
- package/dist/state.js +6 -0
- package/dist/test-builder.d.ts +3582 -0
- package/dist/test-builder.js +239 -0
- package/dist/transform/attr_step.d.ts +60 -0
- package/dist/transform/attr_step.js +105 -0
- package/dist/transform/errors.d.ts +50 -0
- package/dist/transform/errors.js +35 -0
- package/dist/transform/map.d.ts +182 -0
- package/dist/transform/map.js +280 -0
- package/dist/transform/mark.js +98 -0
- package/dist/transform/mark_set_step.js +69 -0
- package/dist/transform/mark_step.d.ts +113 -0
- package/dist/transform/mark_step.js +225 -0
- package/dist/transform/replace.d.ts +18 -0
- package/dist/transform/replace.js +381 -0
- package/dist/transform/replace_step.d.ts +109 -0
- package/dist/transform/replace_step.js +175 -0
- package/dist/transform/step.d.ts +157 -0
- package/dist/transform/step.js +168 -0
- package/dist/transform/structure.d.ts +89 -0
- package/dist/transform/structure.js +350 -0
- package/dist/transform/transform.d.ts +248 -0
- package/dist/transform/transform.js +373 -0
- package/dist/transform.d.ts +10 -0
- package/dist/transform.js +10 -0
- package/dist/view/browser.js +39 -0
- package/dist/view/capturekeys.js +282 -0
- package/dist/view/clipboard.d.ts +8 -0
- package/dist/view/clipboard.js +245 -0
- package/dist/view/decoration.d.ts +339 -0
- package/dist/view/decoration.js +566 -0
- package/dist/view/dom.d.ts +18 -0
- package/dist/view/dom.js +140 -0
- package/dist/view/domchange.js +295 -0
- package/dist/view/domcoords.d.ts +22 -0
- package/dist/view/domcoords.js +448 -0
- package/dist/view/domobserver.d.ts +47 -0
- package/dist/view/domobserver.js +328 -0
- package/dist/view/errors.d.ts +18 -0
- package/dist/view/errors.js +21 -0
- package/dist/view/input.d.ts +58 -0
- package/dist/view/input.js +704 -0
- package/dist/view/selection.js +198 -0
- package/dist/view/view.d.ts +699 -0
- package/dist/view/view.js +623 -0
- package/dist/view/viewdesc.d.ts +236 -0
- package/dist/view/viewdesc.js +1205 -0
- package/dist/view-css.d.ts +43 -0
- package/dist/view-css.js +124 -0
- package/dist/view.d.ts +6 -0
- package/dist/view.js +4 -0
- package/package.json +206 -3
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { Fragment } from "../model/fragment.js";
|
|
2
|
+
import { Slice } from "../model/replace.js";
|
|
3
|
+
import { AllSelection, NodeSelection, Selection, SelectionRange, TextSelection } from "../state/selection.js";
|
|
4
|
+
import { canJoin, canSplit, findWrappingOrNull, joinPoint, liftTargetOrNull } from "../transform/structure.js";
|
|
5
|
+
import { ReplaceAroundStep, ReplaceStep } from "../transform/replace_step.js";
|
|
6
|
+
import { replaceStepOrNull } from "../transform/replace.js";
|
|
7
|
+
import { done, keepMarks, succeedFalse, succeedTrue, syncCommand } from "./internal.js";
|
|
8
|
+
import * as Arr from "effect/Array";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Num from "effect/Number";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import * as Predicate from "effect/Predicate";
|
|
13
|
+
// src/commands/commands.ts
|
|
14
|
+
/**
|
|
15
|
+
* Editing commands, ported from `prosemirror-commands`.
|
|
16
|
+
*
|
|
17
|
+
* Ported from prosemirror-commands (MIT, Marijn Haverbeke and others).
|
|
18
|
+
*
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a view implements `TextblockBoundaryView`.
|
|
23
|
+
*
|
|
24
|
+
* @since 0.1.0
|
|
25
|
+
* @category guards
|
|
26
|
+
*/
|
|
27
|
+
const isTextblockBoundaryView = (view) => Predicate.isObjectOrArray(view) && Predicate.isFunction(view.endOfTextblock);
|
|
28
|
+
/** `isTextblockBoundaryView`, typed on the views commands receive. */
|
|
29
|
+
const isBoundaryView = (view) => Predicate.isObjectOrArray(view) && Predicate.isFunction(view.endOfTextblock);
|
|
30
|
+
const cursorOf = (state) => {
|
|
31
|
+
const sel = state.selection;
|
|
32
|
+
return sel instanceof TextSelection ? sel.$cursorOrNull : null;
|
|
33
|
+
};
|
|
34
|
+
const firstChildOf = (node) => node.content.content[0];
|
|
35
|
+
const lastChildOf = (node) => node.content.content[node.childCount - 1];
|
|
36
|
+
function atBlockStart(state, view) {
|
|
37
|
+
const $cursor = cursorOf(state);
|
|
38
|
+
if (!$cursor || (isBoundaryView(view) ? !view.endOfTextblock("backward", state) : $cursor.parentOffset > 0)) return null;
|
|
39
|
+
return $cursor;
|
|
40
|
+
}
|
|
41
|
+
function atBlockEnd(state, view) {
|
|
42
|
+
const $cursor = cursorOf(state);
|
|
43
|
+
if (!$cursor || (isBoundaryView(view) ? !view.endOfTextblock("forward", state) : $cursor.parentOffset < $cursor.parent.content.size)) return null;
|
|
44
|
+
return $cursor;
|
|
45
|
+
}
|
|
46
|
+
function findCutBefore($pos) {
|
|
47
|
+
if (!$pos.parent.type.spec.isolating) for (let i = $pos.depth - 1; i >= 0; i--) {
|
|
48
|
+
if ($pos.index(i) > 0) return $pos.doc.resolveUnsafe($pos.beforeUnsafe(i + 1));
|
|
49
|
+
if ($pos.node(i).type.spec.isolating) break;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function findCutAfter($pos) {
|
|
54
|
+
if (!$pos.parent.type.spec.isolating) for (let i = $pos.depth - 1; i >= 0; i--) {
|
|
55
|
+
const parent = $pos.node(i);
|
|
56
|
+
if ($pos.index(i) + 1 < parent.childCount) return $pos.doc.resolveUnsafe($pos.afterUnsafe(i + 1));
|
|
57
|
+
if (parent.type.spec.isolating) break;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function textblockAt(node, side, only = false) {
|
|
62
|
+
for (let scan = node; scan; scan = side === "start" ? firstChildOf(scan) : lastChildOf(scan)) {
|
|
63
|
+
if (scan.isTextblock) return true;
|
|
64
|
+
if (only && scan.childCount !== 1) return false;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function isDeletingStep(step) {
|
|
69
|
+
const s = step;
|
|
70
|
+
return s.slice.size < s.to - s.from;
|
|
71
|
+
}
|
|
72
|
+
function defaultBlockAt(match) {
|
|
73
|
+
for (let i = 0; i < match.edgeCount; i++) {
|
|
74
|
+
const { type } = match.next[i];
|
|
75
|
+
if (type.isTextblock && !type.hasRequiredAttrs()) return type;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function joinTextblocksAround(state, $cut, dispatching) {
|
|
80
|
+
let beforeText = $cut.nodeBeforeOrNull, beforePos = $cut.pos - 1;
|
|
81
|
+
for (; !beforeText.isTextblock; beforePos--) {
|
|
82
|
+
if (beforeText.type.spec.isolating) return false;
|
|
83
|
+
const child = lastChildOf(beforeText);
|
|
84
|
+
if (!child) return false;
|
|
85
|
+
beforeText = child;
|
|
86
|
+
}
|
|
87
|
+
let afterText = $cut.nodeAfterOrNull, afterPos = $cut.pos + 1;
|
|
88
|
+
for (; !afterText.isTextblock; afterPos++) {
|
|
89
|
+
if (afterText.type.spec.isolating) return false;
|
|
90
|
+
const child = firstChildOf(afterText);
|
|
91
|
+
if (!child) return false;
|
|
92
|
+
afterText = child;
|
|
93
|
+
}
|
|
94
|
+
const step = replaceStepOrNull(state.doc, beforePos, afterPos, Slice.empty);
|
|
95
|
+
if (!step || step.from !== beforePos || step instanceof ReplaceStep && step.slice.size >= afterPos - beforePos) return false;
|
|
96
|
+
return done(dispatching, () => {
|
|
97
|
+
const tr = state.tr.stepUnsafe(step);
|
|
98
|
+
tr.setSelectionUnsafe(TextSelection.createUnsafe(tr.doc, beforePos));
|
|
99
|
+
return tr.scrollIntoView();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function joinMaybeClear(state, $pos, dispatching) {
|
|
103
|
+
const before = $pos.nodeBeforeOrNull, after = $pos.nodeAfterOrNull, index = $pos.index();
|
|
104
|
+
if (!before || !after || !before.type.compatibleContent(after.type)) return false;
|
|
105
|
+
if (!before.content.size && $pos.parent.canReplace(index - 1, index)) return done(dispatching, () => state.tr.deleteUnsafe($pos.pos - before.nodeSize, $pos.pos).scrollIntoView());
|
|
106
|
+
if (!$pos.parent.canReplace(index, index + 1) || !(after.isTextblock || canJoin(state.doc, $pos.pos))) return false;
|
|
107
|
+
return done(dispatching, () => state.tr.joinUnsafe($pos.pos).scrollIntoView());
|
|
108
|
+
}
|
|
109
|
+
function deleteBarrier(state, $cut, dispatching, dir) {
|
|
110
|
+
const before = $cut.nodeBeforeOrNull, after = $cut.nodeAfterOrNull;
|
|
111
|
+
let conn, match;
|
|
112
|
+
const isolated = before.type.spec.isolating || after.type.spec.isolating;
|
|
113
|
+
if (!isolated) {
|
|
114
|
+
const joined = joinMaybeClear(state, $cut, dispatching);
|
|
115
|
+
if (joined !== false) return joined;
|
|
116
|
+
}
|
|
117
|
+
const canDelAfter = !isolated && $cut.parent.canReplace($cut.index(), $cut.index() + 1);
|
|
118
|
+
if (canDelAfter && (conn = (match = before.contentMatchAtUnsafe(before.childCount)).findWrappingOrNull(after.type)) && match.matchTypeOrNull(conn[0] || after.type).validEnd) {
|
|
119
|
+
const wrappers = conn;
|
|
120
|
+
return done(dispatching, () => {
|
|
121
|
+
const end = $cut.pos + after.nodeSize;
|
|
122
|
+
let wrap = Fragment.empty;
|
|
123
|
+
for (let i = wrappers.length - 1; i >= 0; i--) wrap = Fragment.from(wrappers[i].createUnsafe(null, wrap));
|
|
124
|
+
wrap = Fragment.from(before.copy(wrap));
|
|
125
|
+
const tr = state.tr.stepUnsafe(new ReplaceAroundStep($cut.pos - 1, end, $cut.pos, end, new Slice(wrap, 1, 0), wrappers.length, true));
|
|
126
|
+
const $joinAt = tr.doc.resolveUnsafe(end + 2 * wrappers.length);
|
|
127
|
+
const joinAfter = $joinAt.nodeAfterOrNull;
|
|
128
|
+
if (joinAfter && joinAfter.type === before.type && canJoin(tr.doc, $joinAt.pos)) tr.joinUnsafe($joinAt.pos);
|
|
129
|
+
return tr.scrollIntoView();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const selAfter = after.type.spec.isolating || dir > 0 && isolated ? null : Selection.findFromOrNull($cut, 1);
|
|
133
|
+
const range = selAfter && selAfter.$from.blockRangeOrNull(selAfter.$to);
|
|
134
|
+
const target = range && liftTargetOrNull(range);
|
|
135
|
+
if (target != null && target >= $cut.depth) return done(dispatching, () => state.tr.liftUnsafe(range, target).scrollIntoView());
|
|
136
|
+
if (canDelAfter && textblockAt(after, "start", true) && textblockAt(before, "end")) {
|
|
137
|
+
let at = before;
|
|
138
|
+
const wrap = [];
|
|
139
|
+
for (;;) {
|
|
140
|
+
wrap.push(at);
|
|
141
|
+
if (at.isTextblock) break;
|
|
142
|
+
at = lastChildOf(at);
|
|
143
|
+
}
|
|
144
|
+
let afterText = after, afterDepth = 1;
|
|
145
|
+
for (; !afterText.isTextblock; afterText = firstChildOf(afterText)) afterDepth++;
|
|
146
|
+
if (at.canReplace(at.childCount, at.childCount, afterText.content)) return done(dispatching, () => {
|
|
147
|
+
let end = Fragment.empty;
|
|
148
|
+
for (let i = wrap.length - 1; i >= 0; i--) end = Fragment.from(wrap[i].copy(end));
|
|
149
|
+
return state.tr.stepUnsafe(new ReplaceAroundStep($cut.pos - wrap.length, $cut.pos + after.nodeSize, $cut.pos + afterDepth, $cut.pos + after.nodeSize - afterDepth, new Slice(end, wrap.length, 0), 0, true)).scrollIntoView();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Delete the selection, if there is one.
|
|
156
|
+
*
|
|
157
|
+
* @since 0.1.0
|
|
158
|
+
* @category commands
|
|
159
|
+
*/
|
|
160
|
+
const deleteSelection = syncCommand((state, dispatching) => {
|
|
161
|
+
if (state.selection.empty) return false;
|
|
162
|
+
return done(dispatching, () => state.tr.deleteSelection().scrollIntoView());
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* If the selection is empty and at the start of a textblock, try to
|
|
166
|
+
* reduce the distance between that block and the one before it—if
|
|
167
|
+
* there's a block directly before it that can be joined, join them.
|
|
168
|
+
* If not, try to move the selected block closer to the next one in
|
|
169
|
+
* the document structure by lifting it out of its parent or moving it
|
|
170
|
+
* into a parent of the previous block. Will use the view for accurate
|
|
171
|
+
* (bidi-aware) start-of-textblock detection if given (see
|
|
172
|
+
* `TextblockBoundaryView`).
|
|
173
|
+
*
|
|
174
|
+
* @since 0.1.0
|
|
175
|
+
* @category commands
|
|
176
|
+
*/
|
|
177
|
+
const joinBackward = syncCommand((state, dispatching, view) => {
|
|
178
|
+
const $cursor = atBlockStart(state, view);
|
|
179
|
+
if (!$cursor) return false;
|
|
180
|
+
const $cut = findCutBefore($cursor);
|
|
181
|
+
if (!$cut) {
|
|
182
|
+
const range = $cursor.blockRangeOrNull(), target = range && liftTargetOrNull(range);
|
|
183
|
+
if (target == null) return false;
|
|
184
|
+
return done(dispatching, () => state.tr.liftUnsafe(range, target).scrollIntoView());
|
|
185
|
+
}
|
|
186
|
+
const before = $cut.nodeBeforeOrNull;
|
|
187
|
+
const barrier = deleteBarrier(state, $cut, dispatching, -1);
|
|
188
|
+
if (barrier !== false) return barrier;
|
|
189
|
+
if ($cursor.parent.content.size === 0 && (textblockAt(before, "end") || NodeSelection.isSelectable(before))) for (let depth = $cursor.depth;; depth--) {
|
|
190
|
+
const delStep = replaceStepOrNull(state.doc, $cursor.beforeUnsafe(depth), $cursor.afterUnsafe(depth));
|
|
191
|
+
if (delStep && isDeletingStep(delStep)) return done(dispatching, () => {
|
|
192
|
+
const tr = state.tr.stepUnsafe(delStep);
|
|
193
|
+
tr.setSelectionUnsafe(textblockAt(before, "end") ? Selection.findFromOrNull(tr.doc.resolveUnsafe(tr.mapping.map($cut.pos, -1)), -1) : NodeSelection.createUnsafe(tr.doc, $cut.pos - before.nodeSize));
|
|
194
|
+
return tr.scrollIntoView();
|
|
195
|
+
});
|
|
196
|
+
if (depth === 1 || $cursor.node(depth - 1).childCount > 1) break;
|
|
197
|
+
}
|
|
198
|
+
if (before.isAtom && $cut.depth === $cursor.depth - 1) return done(dispatching, () => state.tr.deleteUnsafe($cut.pos - before.nodeSize, $cut.pos).scrollIntoView());
|
|
199
|
+
return false;
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* A more limited form of `joinBackward` that only tries to join the
|
|
203
|
+
* current textblock to the one before it, if the cursor is at the start
|
|
204
|
+
* of a textblock.
|
|
205
|
+
*
|
|
206
|
+
* @since 0.1.0
|
|
207
|
+
* @category commands
|
|
208
|
+
*/
|
|
209
|
+
const joinTextblockBackward = syncCommand((state, dispatching, view) => {
|
|
210
|
+
const $cursor = atBlockStart(state, view);
|
|
211
|
+
if (!$cursor) return false;
|
|
212
|
+
const $cut = findCutBefore($cursor);
|
|
213
|
+
return $cut ? joinTextblocksAround(state, $cut, dispatching) : false;
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* A more limited form of `joinForward` that only tries to join the
|
|
217
|
+
* current textblock to the one after it, if the cursor is at the end of
|
|
218
|
+
* a textblock.
|
|
219
|
+
*
|
|
220
|
+
* @since 0.1.0
|
|
221
|
+
* @category commands
|
|
222
|
+
*/
|
|
223
|
+
const joinTextblockForward = syncCommand((state, dispatching, view) => {
|
|
224
|
+
const $cursor = atBlockEnd(state, view);
|
|
225
|
+
if (!$cursor) return false;
|
|
226
|
+
const $cut = findCutAfter($cursor);
|
|
227
|
+
return $cut ? joinTextblocksAround(state, $cut, dispatching) : false;
|
|
228
|
+
});
|
|
229
|
+
/**
|
|
230
|
+
* When the selection is empty and at the start of a textblock, select
|
|
231
|
+
* the node before that textblock, if possible. This is intended to be
|
|
232
|
+
* bound to keys like backspace, after `joinBackward` or other deleting
|
|
233
|
+
* commands, as a fall-back behavior when the schema doesn't allow
|
|
234
|
+
* deletion at the selected point.
|
|
235
|
+
*
|
|
236
|
+
* @since 0.1.0
|
|
237
|
+
* @category commands
|
|
238
|
+
*/
|
|
239
|
+
const selectNodeBackward = syncCommand((state, dispatching, view) => {
|
|
240
|
+
const { $head, empty } = state.selection;
|
|
241
|
+
let $cut = $head;
|
|
242
|
+
if (!empty) return false;
|
|
243
|
+
if ($head.parent.isTextblock) {
|
|
244
|
+
if (isBoundaryView(view) ? !view.endOfTextblock("backward", state) : $head.parentOffset > 0) return false;
|
|
245
|
+
$cut = findCutBefore($head);
|
|
246
|
+
}
|
|
247
|
+
const node = $cut && $cut.nodeBeforeOrNull;
|
|
248
|
+
if (!node || !NodeSelection.isSelectable(node)) return false;
|
|
249
|
+
return done(dispatching, () => state.tr.setSelectionUnsafe(NodeSelection.createUnsafe(state.doc, $cut.pos - node.nodeSize)).scrollIntoView());
|
|
250
|
+
});
|
|
251
|
+
/**
|
|
252
|
+
* If the selection is empty and the cursor is at the end of a
|
|
253
|
+
* textblock, try to reduce or remove the boundary between that block
|
|
254
|
+
* and the one after it, either by joining them or by moving the other
|
|
255
|
+
* block closer to this one in the tree structure. Will use the view for
|
|
256
|
+
* accurate start-of-textblock detection if given.
|
|
257
|
+
*
|
|
258
|
+
* @since 0.1.0
|
|
259
|
+
* @category commands
|
|
260
|
+
*/
|
|
261
|
+
const joinForward = syncCommand((state, dispatching, view) => {
|
|
262
|
+
const $cursor = atBlockEnd(state, view);
|
|
263
|
+
if (!$cursor) return false;
|
|
264
|
+
const $cut = findCutAfter($cursor);
|
|
265
|
+
if (!$cut) return false;
|
|
266
|
+
const after = $cut.nodeAfterOrNull;
|
|
267
|
+
const barrier = deleteBarrier(state, $cut, dispatching, 1);
|
|
268
|
+
if (barrier !== false) return barrier;
|
|
269
|
+
if ($cursor.parent.content.size === 0 && (textblockAt(after, "start") || NodeSelection.isSelectable(after))) {
|
|
270
|
+
const delStep = replaceStepOrNull(state.doc, $cursor.beforeUnsafe(), $cursor.afterUnsafe());
|
|
271
|
+
if (delStep && isDeletingStep(delStep)) return done(dispatching, () => {
|
|
272
|
+
const tr = state.tr.stepUnsafe(delStep);
|
|
273
|
+
tr.setSelectionUnsafe(textblockAt(after, "start") ? Selection.findFromOrNull(tr.doc.resolveUnsafe(tr.mapping.map($cut.pos)), 1) : NodeSelection.createUnsafe(tr.doc, tr.mapping.map($cut.pos)));
|
|
274
|
+
return tr.scrollIntoView();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (after.isAtom && $cut.depth === $cursor.depth - 1) return done(dispatching, () => state.tr.deleteUnsafe($cut.pos, $cut.pos + after.nodeSize).scrollIntoView());
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* When the selection is empty and at the end of a textblock, select the
|
|
282
|
+
* node coming after that textblock, if possible. This is intended to be
|
|
283
|
+
* bound to keys like delete, after `joinForward` and similar deleting
|
|
284
|
+
* commands, to provide a fall-back behavior when the schema doesn't
|
|
285
|
+
* allow deletion at the selected point.
|
|
286
|
+
*
|
|
287
|
+
* @since 0.1.0
|
|
288
|
+
* @category commands
|
|
289
|
+
*/
|
|
290
|
+
const selectNodeForward = syncCommand((state, dispatching, view) => {
|
|
291
|
+
const { $head, empty } = state.selection;
|
|
292
|
+
let $cut = $head;
|
|
293
|
+
if (!empty) return false;
|
|
294
|
+
if ($head.parent.isTextblock) {
|
|
295
|
+
if (isTextblockBoundaryView(view) ? !view.endOfTextblock("forward", state) : $head.parentOffset < $head.parent.content.size) return false;
|
|
296
|
+
$cut = findCutAfter($head);
|
|
297
|
+
}
|
|
298
|
+
const node = $cut && $cut.nodeAfterOrNull;
|
|
299
|
+
if (!node || !NodeSelection.isSelectable(node)) return false;
|
|
300
|
+
return done(dispatching, () => state.tr.setSelectionUnsafe(NodeSelection.createUnsafe(state.doc, $cut.pos)).scrollIntoView());
|
|
301
|
+
});
|
|
302
|
+
/**
|
|
303
|
+
* Join the selected block or, if there is a text selection, the closest
|
|
304
|
+
* ancestor block of the selection that can be joined, with the sibling
|
|
305
|
+
* above it.
|
|
306
|
+
*
|
|
307
|
+
* @since 0.1.0
|
|
308
|
+
* @category commands
|
|
309
|
+
*/
|
|
310
|
+
const joinUp = syncCommand((state, dispatching) => {
|
|
311
|
+
const sel = state.selection, nodeSel = sel instanceof NodeSelection;
|
|
312
|
+
let point;
|
|
313
|
+
if (nodeSel) {
|
|
314
|
+
if (sel.node.isTextblock || !canJoin(state.doc, sel.from)) return false;
|
|
315
|
+
point = sel.from;
|
|
316
|
+
} else {
|
|
317
|
+
const found = Option.getOrNull(joinPoint(state.doc, sel.from, -1));
|
|
318
|
+
if (found == null) return false;
|
|
319
|
+
point = found;
|
|
320
|
+
}
|
|
321
|
+
return done(dispatching, () => {
|
|
322
|
+
const tr = state.tr.joinUnsafe(point);
|
|
323
|
+
if (nodeSel) tr.setSelectionUnsafe(NodeSelection.createUnsafe(tr.doc, point - state.doc.resolveUnsafe(point).nodeBeforeOrNull.nodeSize));
|
|
324
|
+
return tr.scrollIntoView();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
/**
|
|
328
|
+
* Join the selected block, or the closest ancestor of the selection
|
|
329
|
+
* that can be joined, with the sibling after it.
|
|
330
|
+
*
|
|
331
|
+
* @since 0.1.0
|
|
332
|
+
* @category commands
|
|
333
|
+
*/
|
|
334
|
+
const joinDown = syncCommand((state, dispatching) => {
|
|
335
|
+
const sel = state.selection;
|
|
336
|
+
let point;
|
|
337
|
+
if (sel instanceof NodeSelection) {
|
|
338
|
+
if (sel.node.isTextblock || !canJoin(state.doc, sel.to)) return false;
|
|
339
|
+
point = sel.to;
|
|
340
|
+
} else {
|
|
341
|
+
const found = Option.getOrNull(joinPoint(state.doc, sel.to, 1));
|
|
342
|
+
if (found == null) return false;
|
|
343
|
+
point = found;
|
|
344
|
+
}
|
|
345
|
+
return done(dispatching, () => state.tr.joinUnsafe(point).scrollIntoView());
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Lift the selected block, or the closest ancestor block of the
|
|
349
|
+
* selection that can be lifted, out of its parent node.
|
|
350
|
+
*
|
|
351
|
+
* @since 0.1.0
|
|
352
|
+
* @category commands
|
|
353
|
+
*/
|
|
354
|
+
const lift = syncCommand((state, dispatching) => {
|
|
355
|
+
const { $from, $to } = state.selection;
|
|
356
|
+
const range = $from.blockRangeOrNull($to), target = range && liftTargetOrNull(range);
|
|
357
|
+
if (target == null) return false;
|
|
358
|
+
return done(dispatching, () => state.tr.liftUnsafe(range, target).scrollIntoView());
|
|
359
|
+
});
|
|
360
|
+
/**
|
|
361
|
+
* If the selection is in a node whose type has a truthy `code` property
|
|
362
|
+
* in its spec, replace the selection with a newline character.
|
|
363
|
+
*
|
|
364
|
+
* @since 0.1.0
|
|
365
|
+
* @category commands
|
|
366
|
+
*/
|
|
367
|
+
const newlineInCode = syncCommand((state, dispatching) => {
|
|
368
|
+
const { $anchor, $head } = state.selection;
|
|
369
|
+
if (!$head.parent.type.spec.code || !$head.sameParent($anchor)) return false;
|
|
370
|
+
return done(dispatching, () => state.tr.insertTextUnsafe("\n").scrollIntoView());
|
|
371
|
+
});
|
|
372
|
+
/**
|
|
373
|
+
* When the selection is in a node with a truthy `code` property in its
|
|
374
|
+
* spec, create a default block after the code block, and move the
|
|
375
|
+
* cursor there.
|
|
376
|
+
*
|
|
377
|
+
* @since 0.1.0
|
|
378
|
+
* @category commands
|
|
379
|
+
*/
|
|
380
|
+
const exitCode = syncCommand((state, dispatching) => {
|
|
381
|
+
const { $anchor, $head } = state.selection;
|
|
382
|
+
if (!$head.parent.type.spec.code || !$head.sameParent($anchor)) return false;
|
|
383
|
+
const above = $head.node(-1), after = $head.indexAfter(-1);
|
|
384
|
+
const type = defaultBlockAt(above.contentMatchAtUnsafe(after));
|
|
385
|
+
if (!type || !above.canReplaceWith(after, after, type)) return false;
|
|
386
|
+
return done(dispatching, () => {
|
|
387
|
+
const pos = $head.afterUnsafe(), tr = state.tr.replaceWithUnsafe(pos, pos, type.createAndFillOrNull());
|
|
388
|
+
tr.setSelectionUnsafe(Selection.near(tr.doc.resolveUnsafe(pos), 1));
|
|
389
|
+
return tr.scrollIntoView();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
/**
|
|
393
|
+
* If a block node is selected, create an empty paragraph before (if it
|
|
394
|
+
* is its parent's first child) or after it.
|
|
395
|
+
*
|
|
396
|
+
* @since 0.1.0
|
|
397
|
+
* @category commands
|
|
398
|
+
*/
|
|
399
|
+
const createParagraphNear = syncCommand((state, dispatching) => {
|
|
400
|
+
const sel = state.selection, { $from, $to } = sel;
|
|
401
|
+
if (sel instanceof AllSelection || $from.parent.inlineContent || $to.parent.inlineContent) return false;
|
|
402
|
+
const type = defaultBlockAt($to.parent.contentMatchAtUnsafe($to.indexAfter()));
|
|
403
|
+
if (!type || !type.isTextblock) return false;
|
|
404
|
+
return done(dispatching, () => {
|
|
405
|
+
const side = (!$from.parentOffset && $to.index() < $to.parent.childCount ? $from : $to).pos;
|
|
406
|
+
const tr = state.tr.insertUnsafe(side, type.createAndFillOrNull());
|
|
407
|
+
tr.setSelectionUnsafe(TextSelection.createUnsafe(tr.doc, side + 1));
|
|
408
|
+
return tr.scrollIntoView();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
/**
|
|
412
|
+
* If the cursor is in an empty textblock that can be lifted, lift the
|
|
413
|
+
* block.
|
|
414
|
+
*
|
|
415
|
+
* @since 0.1.0
|
|
416
|
+
* @category commands
|
|
417
|
+
*/
|
|
418
|
+
const liftEmptyBlock = syncCommand((state, dispatching) => {
|
|
419
|
+
const $cursor = cursorOf(state);
|
|
420
|
+
if (!$cursor || $cursor.parent.content.size) return false;
|
|
421
|
+
if ($cursor.depth > 1 && $cursor.afterUnsafe() !== $cursor.end(-1)) {
|
|
422
|
+
const before = $cursor.beforeUnsafe();
|
|
423
|
+
if (canSplit(state.doc, before)) return done(dispatching, () => state.tr.splitUnsafe(before).scrollIntoView());
|
|
424
|
+
}
|
|
425
|
+
const range = $cursor.blockRangeOrNull(), target = range && liftTargetOrNull(range);
|
|
426
|
+
if (target == null) return false;
|
|
427
|
+
return done(dispatching, () => state.tr.liftUnsafe(range, target).scrollIntoView());
|
|
428
|
+
});
|
|
429
|
+
/**
|
|
430
|
+
* Create a variant of `splitBlock` that uses a custom function to
|
|
431
|
+
* determine the type of the newly split off block.
|
|
432
|
+
*
|
|
433
|
+
* @since 0.1.0
|
|
434
|
+
* @category commands
|
|
435
|
+
*/
|
|
436
|
+
const splitBlockAs = (splitNode) => syncCommand((state, dispatching) => {
|
|
437
|
+
const sel = state.selection;
|
|
438
|
+
if (sel instanceof NodeSelection && sel.node.isBlock) {
|
|
439
|
+
const { $from } = sel;
|
|
440
|
+
if (!$from.parentOffset || !canSplit(state.doc, $from.pos)) return false;
|
|
441
|
+
return done(dispatching, () => state.tr.splitUnsafe($from.pos).scrollIntoView());
|
|
442
|
+
}
|
|
443
|
+
if (!sel.$from.depth) return false;
|
|
444
|
+
const tr = state.tr;
|
|
445
|
+
if (!sel.empty && (sel instanceof TextSelection || sel instanceof AllSelection)) tr.deleteSelection();
|
|
446
|
+
const { $from } = tr.selection, mapFrom = tr.steps.length;
|
|
447
|
+
const types = [];
|
|
448
|
+
let splitDepth = 0, deflt = null, atEnd = false, atStart = false;
|
|
449
|
+
for (let d = $from.depth;; d--) if ($from.node(d).isBlock) {
|
|
450
|
+
atEnd = $from.end(d) === $from.pos + ($from.depth - d);
|
|
451
|
+
atStart = $from.start(d) === $from.pos - ($from.depth - d);
|
|
452
|
+
deflt = defaultBlockAt($from.node(d - 1).contentMatchAtUnsafe($from.indexAfter(d - 1)));
|
|
453
|
+
const splitType = splitNode && splitNode($from.parent, atEnd, $from);
|
|
454
|
+
types.unshift(splitType || (atEnd && deflt ? { type: deflt } : null));
|
|
455
|
+
splitDepth = d;
|
|
456
|
+
break;
|
|
457
|
+
} else {
|
|
458
|
+
if (d === 1) return false;
|
|
459
|
+
types.unshift(null);
|
|
460
|
+
}
|
|
461
|
+
const splitPos = $from.pos;
|
|
462
|
+
let can = canSplit(tr.doc, splitPos, types.length, types);
|
|
463
|
+
if (!can) {
|
|
464
|
+
types[0] = deflt ? { type: deflt } : null;
|
|
465
|
+
can = canSplit(tr.doc, splitPos, types.length, types);
|
|
466
|
+
}
|
|
467
|
+
if (!can) return false;
|
|
468
|
+
tr.splitUnsafe(splitPos, types.length, types);
|
|
469
|
+
if (!atEnd && atStart && $from.node(splitDepth).type !== deflt) {
|
|
470
|
+
const mapping = tr.mapping.slice(mapFrom);
|
|
471
|
+
const first = mapping.map($from.beforeUnsafe(splitDepth)), $first = tr.doc.resolveUnsafe(first);
|
|
472
|
+
if (deflt && $from.node(splitDepth - 1).canReplaceWith($first.index(), $first.index() + 1, deflt)) tr.setNodeMarkupUnsafe(mapping.map($from.beforeUnsafe(splitDepth)), deflt);
|
|
473
|
+
}
|
|
474
|
+
return tr.scrollIntoView();
|
|
475
|
+
});
|
|
476
|
+
/**
|
|
477
|
+
* Split the parent block of the selection. If the selection is a text
|
|
478
|
+
* selection, also delete its content.
|
|
479
|
+
*
|
|
480
|
+
* @since 0.1.0
|
|
481
|
+
* @category commands
|
|
482
|
+
*/
|
|
483
|
+
const splitBlock = splitBlockAs();
|
|
484
|
+
/**
|
|
485
|
+
* Acts like `splitBlock`, but without resetting the set of active marks
|
|
486
|
+
* at the cursor.
|
|
487
|
+
*
|
|
488
|
+
* @since 0.1.0
|
|
489
|
+
* @category commands
|
|
490
|
+
*/
|
|
491
|
+
const splitBlockKeepMarks = (state, dispatch) => splitBlock(state, keepMarks(state, dispatch));
|
|
492
|
+
/**
|
|
493
|
+
* Move the selection to the node wrapping the current selection, if
|
|
494
|
+
* any. (Will not select the document node.)
|
|
495
|
+
*
|
|
496
|
+
* @since 0.1.0
|
|
497
|
+
* @category commands
|
|
498
|
+
*/
|
|
499
|
+
const selectParentNode = syncCommand((state, dispatching) => {
|
|
500
|
+
const { $from, to } = state.selection;
|
|
501
|
+
const same = $from.sharedDepth(to);
|
|
502
|
+
if (same === 0) return false;
|
|
503
|
+
const pos = $from.beforeUnsafe(same);
|
|
504
|
+
return done(dispatching, () => state.tr.setSelectionUnsafe(NodeSelection.createUnsafe(state.doc, pos)));
|
|
505
|
+
});
|
|
506
|
+
/**
|
|
507
|
+
* Select the whole document.
|
|
508
|
+
*
|
|
509
|
+
* @since 0.1.0
|
|
510
|
+
* @category commands
|
|
511
|
+
*/
|
|
512
|
+
const selectAll = syncCommand((state, dispatching) => done(dispatching, () => state.tr.setSelectionUnsafe(new AllSelection(state.doc))));
|
|
513
|
+
function selectTextblockSide(side) {
|
|
514
|
+
return syncCommand((state, dispatching) => {
|
|
515
|
+
const sel = state.selection, $pos = side < 0 ? sel.$from : sel.$to;
|
|
516
|
+
let depth = $pos.depth;
|
|
517
|
+
while ($pos.node(depth).isInline) {
|
|
518
|
+
if (!depth) return false;
|
|
519
|
+
depth--;
|
|
520
|
+
}
|
|
521
|
+
if (!$pos.node(depth).isTextblock) return false;
|
|
522
|
+
return done(dispatching, () => state.tr.setSelectionUnsafe(TextSelection.createUnsafe(state.doc, side < 0 ? $pos.start(depth) : $pos.end(depth))));
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Moves the cursor to the start of current text block.
|
|
527
|
+
*
|
|
528
|
+
* @since 0.1.0
|
|
529
|
+
* @category commands
|
|
530
|
+
*/
|
|
531
|
+
const selectTextblockStart = selectTextblockSide(-1);
|
|
532
|
+
/**
|
|
533
|
+
* Moves the cursor to the end of current text block.
|
|
534
|
+
*
|
|
535
|
+
* @since 0.1.0
|
|
536
|
+
* @category commands
|
|
537
|
+
*/
|
|
538
|
+
const selectTextblockEnd = selectTextblockSide(1);
|
|
539
|
+
/**
|
|
540
|
+
* Wrap the selection in a node of the given type with the given
|
|
541
|
+
* attributes.
|
|
542
|
+
*
|
|
543
|
+
* @since 0.1.0
|
|
544
|
+
* @category commands
|
|
545
|
+
*/
|
|
546
|
+
const wrapIn = (nodeType, attrs = null) => syncCommand((state, dispatching) => {
|
|
547
|
+
const { $from, $to } = state.selection;
|
|
548
|
+
const range = $from.blockRangeOrNull($to), wrapping = range && findWrappingOrNull(range, nodeType, attrs);
|
|
549
|
+
if (!wrapping) return false;
|
|
550
|
+
return done(dispatching, () => state.tr.wrapUnsafe(range, wrapping).scrollIntoView());
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* Returns a command that tries to set the selected textblocks to the
|
|
554
|
+
* given node type with the given attributes.
|
|
555
|
+
*
|
|
556
|
+
* @since 0.1.0
|
|
557
|
+
* @category commands
|
|
558
|
+
*/
|
|
559
|
+
const setBlockType = (nodeType, attrs = null) => syncCommand((state, dispatching) => {
|
|
560
|
+
let applicable = false;
|
|
561
|
+
const ranges = state.selection.ranges;
|
|
562
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
563
|
+
if (applicable) break;
|
|
564
|
+
const { $from: { pos: from }, $to: { pos: to } } = ranges[i];
|
|
565
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
566
|
+
if (applicable) return false;
|
|
567
|
+
if (!node.isTextblock || node.hasMarkup(nodeType, attrs)) return;
|
|
568
|
+
if (node.type === nodeType) applicable = true;
|
|
569
|
+
else {
|
|
570
|
+
const $pos = state.doc.resolveUnsafe(pos), index = $pos.index();
|
|
571
|
+
applicable = $pos.parent.canReplaceWith(index, index + 1, nodeType);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (!applicable) return false;
|
|
576
|
+
return done(dispatching, () => {
|
|
577
|
+
const tr = state.tr;
|
|
578
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
579
|
+
const { $from: { pos: from }, $to: { pos: to } } = ranges[i];
|
|
580
|
+
tr.setBlockTypeUnsafe(from, to, nodeType, attrs);
|
|
581
|
+
}
|
|
582
|
+
return tr.scrollIntoView();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
function markApplies(doc, ranges, type, enterAtoms) {
|
|
586
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
587
|
+
const { $from, $to } = ranges[i];
|
|
588
|
+
let can = $from.depth === 0 ? doc.inlineContent && doc.type.allowsMarkType(type) : false;
|
|
589
|
+
doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
|
590
|
+
if (can || !enterAtoms && node.isAtom && node.isInline && pos >= $from.pos && pos + node.nodeSize <= $to.pos) return false;
|
|
591
|
+
can = node.inlineContent && node.type.allowsMarkType(type);
|
|
592
|
+
});
|
|
593
|
+
if (can) return true;
|
|
594
|
+
}
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
function removeInlineAtoms(ranges) {
|
|
598
|
+
const result = [];
|
|
599
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
600
|
+
let { $from } = ranges[i];
|
|
601
|
+
const { $to } = ranges[i];
|
|
602
|
+
$from.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
|
603
|
+
if (node.isAtom && node.content.size && node.isInline && pos >= $from.pos && pos + node.nodeSize <= $to.pos) {
|
|
604
|
+
if (pos + 1 > $from.pos) result.push(new SelectionRange($from, $from.doc.resolveUnsafe(pos + 1)));
|
|
605
|
+
$from = $from.doc.resolveUnsafe(pos + 1 + node.content.size);
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
if ($from.pos < $to.pos) result.push(new SelectionRange($from, $to));
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Create a command function that toggles the given mark with the given
|
|
615
|
+
* attributes. Will return `false` when the current selection doesn't
|
|
616
|
+
* support that mark. This will remove the mark if any marks of that
|
|
617
|
+
* type exist in the selection, or add it otherwise. If the selection is
|
|
618
|
+
* empty, this applies to the stored marks instead of a range of the
|
|
619
|
+
* document.
|
|
620
|
+
*
|
|
621
|
+
* @since 0.1.0
|
|
622
|
+
* @category commands
|
|
623
|
+
*/
|
|
624
|
+
const toggleMark = (markType, attrs, options) => {
|
|
625
|
+
const removeWhenPresent = (options && options.removeWhenPresent) !== false;
|
|
626
|
+
const enterAtoms = (options && options.enterInlineAtoms) !== false;
|
|
627
|
+
const dropSpace = !(options && options.includeWhitespace);
|
|
628
|
+
return syncCommand((state, dispatching) => {
|
|
629
|
+
const sel = state.selection, empty = sel.empty, $cursor = cursorOf(state);
|
|
630
|
+
let ranges = sel.ranges;
|
|
631
|
+
if (empty && !$cursor || !markApplies(state.doc, ranges, markType, enterAtoms)) return false;
|
|
632
|
+
if (!dispatching) return true;
|
|
633
|
+
if ($cursor) {
|
|
634
|
+
if (markType.findInSet(state.storedMarksOrNull || $cursor.marks())) return state.tr.removeStoredMark(markType);
|
|
635
|
+
return state.tr.addStoredMark(markType.createUnsafe(attrs));
|
|
636
|
+
}
|
|
637
|
+
let add;
|
|
638
|
+
const tr = state.tr;
|
|
639
|
+
if (!enterAtoms) ranges = removeInlineAtoms(ranges);
|
|
640
|
+
if (removeWhenPresent) add = !ranges.some((r) => state.doc.rangeHasMark(r.$from.pos, r.$to.pos, markType));
|
|
641
|
+
else add = !ranges.every((r) => {
|
|
642
|
+
let missing = false;
|
|
643
|
+
tr.doc.nodesBetween(r.$from.pos, r.$to.pos, (node, pos, parent) => {
|
|
644
|
+
if (missing) return false;
|
|
645
|
+
missing = !markType.findInSet(node.marks) && Option.isSome(parent) && parent.value.type.allowsMarkType(markType) && !(node.isText && /^\s*$/.test(node.textBetween(Math.max(0, r.$from.pos - pos), Math.min(node.nodeSize, r.$to.pos - pos))));
|
|
646
|
+
});
|
|
647
|
+
return !missing;
|
|
648
|
+
});
|
|
649
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
650
|
+
const { $from, $to } = ranges[i];
|
|
651
|
+
if (!add) tr.removeMark($from.pos, $to.pos, markType);
|
|
652
|
+
else {
|
|
653
|
+
let from = $from.pos, to = $to.pos;
|
|
654
|
+
const start = $from.nodeAfterOrNull, end = $to.nodeBeforeOrNull;
|
|
655
|
+
const spaceStart = dropSpace && start && start.isText ? /^\s*/.exec(start.text)[0].length : 0;
|
|
656
|
+
const spaceEnd = dropSpace && end && end.isText ? /\s*$/.exec(end.text)[0].length : 0;
|
|
657
|
+
if (from + spaceStart < to) {
|
|
658
|
+
from += spaceStart;
|
|
659
|
+
to -= spaceEnd;
|
|
660
|
+
}
|
|
661
|
+
tr.addMark(from, to, markType.createUnsafe(attrs));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return tr.scrollIntoView();
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
function wrapDispatchForJoin(dispatch, isJoinable) {
|
|
668
|
+
return (tr) => Effect.suspend(() => {
|
|
669
|
+
if (!tr.isGeneric) return dispatch(tr);
|
|
670
|
+
const ranges = [];
|
|
671
|
+
for (let i = 0; i < tr.mapping.maps.length; i++) {
|
|
672
|
+
const map = tr.mapping.maps[i];
|
|
673
|
+
for (let j = 0; j < ranges.length; j++) ranges[j] = map.map(ranges[j]);
|
|
674
|
+
map.forEach((_s, _e, from, to) => ranges.push(from, to));
|
|
675
|
+
}
|
|
676
|
+
const joinable = [];
|
|
677
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
678
|
+
const from = ranges[i], to = ranges[i + 1];
|
|
679
|
+
const $from = tr.doc.resolveUnsafe(from), depth = $from.sharedDepth(to), parent = $from.node(depth);
|
|
680
|
+
for (let index = $from.indexAfter(depth), pos = $from.afterUnsafe(depth + 1); pos <= to; ++index) {
|
|
681
|
+
const after = parent.content.content[index];
|
|
682
|
+
if (!after) break;
|
|
683
|
+
if (index && joinable.indexOf(pos) === -1) {
|
|
684
|
+
const before = parent.content.content[index - 1];
|
|
685
|
+
if (before.type === after.type && isJoinable(before, after)) joinable.push(pos);
|
|
686
|
+
}
|
|
687
|
+
pos += after.nodeSize;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const sorted = Arr.sort(joinable, Num.Order);
|
|
691
|
+
for (let i = sorted.length - 1; i >= 0; i--) if (canJoin(tr.doc, sorted[i])) tr.joinUnsafe(sorted[i]);
|
|
692
|
+
return dispatch(tr);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Wrap a command so that, when it produces a transform that causes two
|
|
697
|
+
* joinable nodes to end up next to each other, those are joined. Nodes
|
|
698
|
+
* are considered joinable when they are of the same type and when the
|
|
699
|
+
* `isJoinable` predicate returns true for them or, if an array of
|
|
700
|
+
* strings was passed, if their node type name is in that array.
|
|
701
|
+
*
|
|
702
|
+
* @since 0.1.0
|
|
703
|
+
* @category combinators
|
|
704
|
+
*/
|
|
705
|
+
const autoJoin = (command, isJoinable) => {
|
|
706
|
+
const canJoinNodes = Array.isArray(isJoinable) ? (node) => isJoinable.indexOf(node.type.name) > -1 : isJoinable;
|
|
707
|
+
return (state, dispatch, view) => command(state, dispatch && wrapDispatchForJoin(dispatch, canJoinNodes), view);
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* Combine a number of command functions into a single function (which
|
|
711
|
+
* calls them one by one until one returns true). The resulting
|
|
712
|
+
* command's errors and requirements are the unions of those of the
|
|
713
|
+
* given commands.
|
|
714
|
+
*
|
|
715
|
+
* @since 0.1.0
|
|
716
|
+
* @category combinators
|
|
717
|
+
*/
|
|
718
|
+
const chainCommands = (...commands) => (state, dispatch, view) => {
|
|
719
|
+
const loop = (i) => i === commands.length ? succeedFalse : Effect.flatMap(Effect.suspend(() => commands[i](state, dispatch, view)), (handled) => handled ? succeedTrue : loop(i + 1));
|
|
720
|
+
return loop(0);
|
|
721
|
+
};
|
|
722
|
+
export { autoJoin, chainCommands, createParagraphNear, deleteSelection, exitCode, isTextblockBoundaryView, joinBackward, joinDown, joinForward, joinTextblockBackward, joinTextblockForward, joinUp, lift, liftEmptyBlock, newlineInCode, selectAll, selectNodeBackward, selectNodeForward, selectParentNode, selectTextblockEnd, selectTextblockStart, setBlockType, splitBlock, splitBlockAs, splitBlockKeepMarks, toggleMark, wrapIn };
|