@ai-matrx/rich-editor 0.2.9 → 0.2.11
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/CHANGELOG.md +13 -0
- package/dist/core/caret-context.d.ts +14 -0
- package/dist/core/caret-context.js +11 -1
- package/dist/core/markdown-format.js +113 -9
- package/dist/editor/RichEditorImpl.js +17 -0
- package/dist/source/SourceEditor.js +19 -4
- package/dist/visual/VisualEditor.d.ts +4 -2
- package/dist/visual/VisualEditor.js +30 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.11
|
|
4
|
+
### Bold (and every inline mark) toggles like a real editor
|
|
5
|
+
|
|
6
|
+
- A selection that is partly bold becomes ONE bold span: the bold inside it loses its markers and one pair wraps the whole (`Alpha **bravo** delta` -> `**Alpha bravo delta**`, never `**Alpha **bravo** delta**`). A selection reaching into a bold span joins it.
|
|
7
|
+
- A selection inside one bold span unbolds just that part (`**a [b] c**` -> `**a** b **c**`), and the toolbar's pressed state is on there.
|
|
8
|
+
- Same rule for italic, strike and inline code — one command layer, every host.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 0.2.10
|
|
12
|
+
### The caret survives a view switch
|
|
13
|
+
|
|
14
|
+
- Switching Visual <-> Source (and back) carries the caret or selection: read from the leaving view as text around each end (`getCaret`, `CaretCarry`), placed in the new view once it is ready (`placeCaret(context, to)`). It used to land at offset 0.
|
|
15
|
+
|
|
3
16
|
## 0.2.9
|
|
4
17
|
|
|
5
18
|
- Outline "Copy link" shows ONE toast (AP-2): the words ("Link to … copied.") ride the kit copy instead
|
|
@@ -22,3 +22,17 @@ export declare function domPointAt(doc: Document, x: number, y: number): {
|
|
|
22
22
|
node: Node;
|
|
23
23
|
offset: number;
|
|
24
24
|
} | null;
|
|
25
|
+
/** The caret or selection of one view, as text around its two ends — carried across a view switch. */
|
|
26
|
+
export interface CaretCarry {
|
|
27
|
+
from: CaretContext;
|
|
28
|
+
/** The selection's other end; null for a plain caret. */
|
|
29
|
+
to: CaretContext | null;
|
|
30
|
+
}
|
|
31
|
+
/** The text around `index` of `flat` (a view's own text). */
|
|
32
|
+
export declare function contextAtIndex(flat: string, index: number): CaretContext;
|
|
33
|
+
/**
|
|
34
|
+
* A context read from markdown SOURCE, made comparable with the rendered text: the markup characters a
|
|
35
|
+
* rendered view does not show (`**`, `_`, backticks, heading and quote marks) are dropped, so Source ->
|
|
36
|
+
* Visual finds the same words. (Visual -> Source needs no help: the locator falls back to the words.)
|
|
37
|
+
*/
|
|
38
|
+
export declare function withoutMarkup(context: CaretContext): CaretContext;
|
|
@@ -65,9 +65,19 @@ function domPointAt(doc, x, y) {
|
|
|
65
65
|
if (range) return { node: range.startContainer, offset: range.startOffset };
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
|
+
function contextAtIndex(flat, index) {
|
|
69
|
+
const at = Math.max(0, Math.min(index, flat.length));
|
|
70
|
+
return { before: flat.slice(Math.max(0, at - CONTEXT_CHARS), at), after: flat.slice(at, at + CONTEXT_CHARS) };
|
|
71
|
+
}
|
|
72
|
+
function withoutMarkup(context) {
|
|
73
|
+
const clean = (text) => text.replace(/[*_~`]+/g, "").replace(/^[ \t]*(?:#{1,6}|>|[-+*]|\d+[.)])[ \t]+/gm, "");
|
|
74
|
+
return { before: clean(context.before), after: clean(context.after) };
|
|
75
|
+
}
|
|
68
76
|
export {
|
|
69
77
|
CONTEXT_CHARS,
|
|
70
78
|
caretContextAt,
|
|
79
|
+
contextAtIndex,
|
|
71
80
|
domPointAt,
|
|
72
|
-
locateCaret
|
|
81
|
+
locateCaret,
|
|
82
|
+
withoutMarkup
|
|
73
83
|
};
|
|
@@ -94,6 +94,83 @@ function mapPos(pos, changes, assoc) {
|
|
|
94
94
|
function sorted(changes) {
|
|
95
95
|
return [...changes].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
96
96
|
}
|
|
97
|
+
function markSpansOnLine(text, pos, spec) {
|
|
98
|
+
const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
|
|
99
|
+
const nl = text.indexOf("\n", pos);
|
|
100
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
101
|
+
const spans = [];
|
|
102
|
+
const open = {};
|
|
103
|
+
let i = lineStart;
|
|
104
|
+
while (i < lineEnd) {
|
|
105
|
+
const ch = text[i];
|
|
106
|
+
if (ch === "`" && !spec.chars.includes("`")) {
|
|
107
|
+
const close = text.indexOf("`", i + 1);
|
|
108
|
+
i = close === -1 || close >= lineEnd ? i + 1 : close + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!spec.chars.includes(ch)) {
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const len = runLength(text, i, 1, ch);
|
|
116
|
+
if (spec.runs.includes(len)) {
|
|
117
|
+
const start = open[ch];
|
|
118
|
+
if (start === void 0) {
|
|
119
|
+
open[ch] = i + len;
|
|
120
|
+
} else {
|
|
121
|
+
if (i > start) spans.push({ innerFrom: start, innerTo: i });
|
|
122
|
+
open[ch] = void 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
i += len;
|
|
126
|
+
}
|
|
127
|
+
return spans;
|
|
128
|
+
}
|
|
129
|
+
function contentOf(seg, spans, spec) {
|
|
130
|
+
let { from, to } = seg;
|
|
131
|
+
for (const sp of spans) {
|
|
132
|
+
if (from >= sp.innerFrom - spec.size && from < sp.innerFrom) from = sp.innerFrom;
|
|
133
|
+
if (to > sp.innerTo && to <= sp.innerTo + spec.size) to = sp.innerTo;
|
|
134
|
+
}
|
|
135
|
+
return { from, to: Math.max(from, to) };
|
|
136
|
+
}
|
|
137
|
+
function mergeOn(seg, spans, spec, changes) {
|
|
138
|
+
const touched = spans.filter((sp) => sp.innerFrom <= seg.to && sp.innerTo >= seg.from);
|
|
139
|
+
const from = Math.min(seg.from, ...touched.map((sp) => sp.innerFrom));
|
|
140
|
+
const to = Math.max(seg.to, ...touched.map((sp) => sp.innerTo));
|
|
141
|
+
let keepOpen = false;
|
|
142
|
+
let keepClose = false;
|
|
143
|
+
for (const sp of touched) {
|
|
144
|
+
if (sp.innerFrom === from && !keepOpen) keepOpen = true;
|
|
145
|
+
else changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
|
|
146
|
+
if (sp.innerTo === to && !keepClose) keepClose = true;
|
|
147
|
+
else changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
|
|
148
|
+
}
|
|
149
|
+
if (!keepOpen) changes.push({ from, to: from, insert: spec.marker });
|
|
150
|
+
if (!keepClose) changes.push({ from: to, to, insert: spec.marker });
|
|
151
|
+
return { from, to };
|
|
152
|
+
}
|
|
153
|
+
function splitOff(text, seg, sp, spec, changes) {
|
|
154
|
+
if (seg.from <= sp.innerFrom) {
|
|
155
|
+
changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
|
|
156
|
+
} else {
|
|
157
|
+
let at = seg.from;
|
|
158
|
+
while (at > sp.innerFrom && /\s/.test(text[at - 1] ?? "")) at -= 1;
|
|
159
|
+
if (at === sp.innerFrom) changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
|
|
160
|
+
else changes.push({ from: at, to: at, insert: spec.marker });
|
|
161
|
+
}
|
|
162
|
+
if (seg.to >= sp.innerTo) {
|
|
163
|
+
changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
|
|
164
|
+
} else {
|
|
165
|
+
let at = seg.to;
|
|
166
|
+
while (at < sp.innerTo && /\s/.test(text[at] ?? "")) at += 1;
|
|
167
|
+
if (at === sp.innerTo) changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
|
|
168
|
+
else changes.push({ from: at, to: at, insert: spec.marker });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function spanHolding(seg, spans) {
|
|
172
|
+
return spans.find((sp) => sp.innerFrom <= seg.from && seg.to <= sp.innerTo) ?? null;
|
|
173
|
+
}
|
|
97
174
|
function inlineCommand(text, from, to, spec) {
|
|
98
175
|
if (from === to) {
|
|
99
176
|
const word = wordAt(text, from);
|
|
@@ -109,21 +186,43 @@ function inlineCommand(text, from, to, spec) {
|
|
|
109
186
|
const segments = inlineSegments(text, from, to);
|
|
110
187
|
if (segments.length === 0) return { changes: [], anchor: from, head: to };
|
|
111
188
|
const wraps = segments.map((seg) => wrapOf(text, seg, spec));
|
|
112
|
-
|
|
189
|
+
if (wraps.every((w) => w !== null)) {
|
|
190
|
+
const changes2 = [];
|
|
191
|
+
wraps.forEach((w) => {
|
|
192
|
+
changes2.push({ from: w.innerFrom - spec.size, to: w.innerFrom, insert: "" });
|
|
193
|
+
changes2.push({ from: w.innerTo, to: w.innerTo + spec.size, insert: "" });
|
|
194
|
+
});
|
|
195
|
+
const ordered2 = sorted(changes2);
|
|
196
|
+
const anchor2 = mapPos(wraps[0].innerFrom, ordered2, 1);
|
|
197
|
+
const head2 = mapPos(wraps[wraps.length - 1].innerTo, ordered2, -1);
|
|
198
|
+
return { changes: ordered2, anchor: anchor2, head: Math.max(anchor2, head2) };
|
|
199
|
+
}
|
|
200
|
+
const lineSpans = segments.map((seg) => markSpansOnLine(text, seg.from, spec));
|
|
201
|
+
const contents = segments.map((seg, i) => contentOf(seg, lineSpans[i], spec));
|
|
202
|
+
const holders = contents.map((c, i) => wraps[i] ? null : spanHolding(c, lineSpans[i]));
|
|
203
|
+
const allOn = wraps.every((w, i) => w !== null || holders[i] !== null);
|
|
113
204
|
const changes = [];
|
|
205
|
+
let startAt = contents[0].from;
|
|
206
|
+
let endAt = contents[contents.length - 1].to;
|
|
114
207
|
segments.forEach((seg, i) => {
|
|
115
208
|
const w = wraps[i];
|
|
116
|
-
if (allOn
|
|
117
|
-
|
|
118
|
-
|
|
209
|
+
if (allOn) {
|
|
210
|
+
if (w) {
|
|
211
|
+
changes.push({ from: w.innerFrom - spec.size, to: w.innerFrom, insert: "" });
|
|
212
|
+
changes.push({ from: w.innerTo, to: w.innerTo + spec.size, insert: "" });
|
|
213
|
+
} else {
|
|
214
|
+
splitOff(text, contents[i], holders[i], spec, changes);
|
|
215
|
+
}
|
|
119
216
|
} else if (!w) {
|
|
120
|
-
|
|
121
|
-
|
|
217
|
+
const merged = mergeOn(contents[i], lineSpans[i], spec, changes);
|
|
218
|
+
if (i === 0) startAt = merged.from;
|
|
219
|
+
if (i === segments.length - 1) endAt = merged.to;
|
|
220
|
+
} else {
|
|
221
|
+
if (i === 0) startAt = w.innerFrom;
|
|
222
|
+
if (i === segments.length - 1) endAt = w.innerTo;
|
|
122
223
|
}
|
|
123
224
|
});
|
|
124
225
|
const ordered = sorted(changes);
|
|
125
|
-
const startAt = allOn ? wraps[0].innerFrom : segments[0].from;
|
|
126
|
-
const endAt = allOn ? wraps[wraps.length - 1].innerTo : segments[segments.length - 1].to;
|
|
127
226
|
const anchor = mapPos(startAt, ordered, 1);
|
|
128
227
|
const head = mapPos(endAt, ordered, -1);
|
|
129
228
|
return { changes: ordered, anchor, head: Math.max(anchor, head) };
|
|
@@ -293,7 +392,12 @@ function isFormatActive(text, from, to, command) {
|
|
|
293
392
|
const range = a === b ? wordAt(text, a) : { from: a, to: b };
|
|
294
393
|
if (!range) return false;
|
|
295
394
|
const segs = inlineSegments(text, range.from, range.to);
|
|
296
|
-
|
|
395
|
+
const spec = INLINE[command];
|
|
396
|
+
return segs.length > 0 && segs.every((s) => {
|
|
397
|
+
if (wrapOf(text, s, spec) !== null) return true;
|
|
398
|
+
const spans = markSpansOnLine(text, s.from, spec);
|
|
399
|
+
return spanHolding(contentOf(s, spans, spec), spans) !== null;
|
|
400
|
+
});
|
|
297
401
|
}
|
|
298
402
|
case "link":
|
|
299
403
|
return WHOLE_LINK.test(text.slice(a, b)) || text[a - 1] === "[" && text.slice(b, b + 2) === "](";
|
|
@@ -131,6 +131,7 @@ function RichEditorImpl({
|
|
|
131
131
|
const [kindRequest, setKindRequest] = useState(null);
|
|
132
132
|
const approved = useRef(/* @__PURE__ */ new Set());
|
|
133
133
|
const handle = useRef(null);
|
|
134
|
+
const pendingCaret = useRef(null);
|
|
134
135
|
const imageInput = useRef(null);
|
|
135
136
|
const { upload } = useFileUpload();
|
|
136
137
|
const lastValue = useRef(value);
|
|
@@ -183,10 +184,26 @@ function RichEditorImpl({
|
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
handle.current?.clearFind();
|
|
187
|
+
pendingCaret.current = handle.current?.getCaret?.() ?? null;
|
|
186
188
|
setView(next);
|
|
187
189
|
setMountKey((key) => key + 1);
|
|
188
190
|
if (next !== controlledView) onViewChange?.(next);
|
|
189
191
|
};
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const carry = pendingCaret.current;
|
|
194
|
+
if (!carry || view === "preview") return;
|
|
195
|
+
pendingCaret.current = null;
|
|
196
|
+
let tries = 0;
|
|
197
|
+
let timer = null;
|
|
198
|
+
const attempt = () => {
|
|
199
|
+
if (handle.current?.placeCaret(carry.from, carry.to) || ++tries > 20) return;
|
|
200
|
+
timer = setTimeout(attempt, 50);
|
|
201
|
+
};
|
|
202
|
+
timer = setTimeout(attempt, 0);
|
|
203
|
+
return () => {
|
|
204
|
+
if (timer) clearTimeout(timer);
|
|
205
|
+
};
|
|
206
|
+
}, [view, mountKey]);
|
|
190
207
|
useEffect(() => {
|
|
191
208
|
if (controlledView !== void 0 && controlledView !== view) switchView(controlledView);
|
|
192
209
|
}, [controlledView]);
|
|
@@ -21,7 +21,7 @@ import { getSchema } from "@tiptap/core";
|
|
|
21
21
|
import { cn } from "@ai-matrx/design-system";
|
|
22
22
|
import { toast } from "@ai-matrx/rich-content/host/toast";
|
|
23
23
|
import { createRichEditorExtensions } from "../core/extensions.js";
|
|
24
|
-
import { locateCaret } from "../core/caret-context.js";
|
|
24
|
+
import { contextAtIndex, locateCaret, withoutMarkup } from "../core/caret-context.js";
|
|
25
25
|
import { htmlToMarkdown } from "../core/html-to-markdown.js";
|
|
26
26
|
import { mergedCellsNotice, normalizePastedHtml } from "../core/paste-html.js";
|
|
27
27
|
import { findMatches, replaceMatches } from "../core/find-replace.js";
|
|
@@ -314,15 +314,30 @@ ${text}`;
|
|
|
314
314
|
const v = view.current;
|
|
315
315
|
return v ? { undo: undoDepth(v.state), redo: redoDepth(v.state) } : { undo: 0, redo: 0 };
|
|
316
316
|
},
|
|
317
|
-
placeCaret: (context2) => {
|
|
317
|
+
placeCaret: (context2, to) => {
|
|
318
318
|
const v = view.current;
|
|
319
319
|
if (!v) return false;
|
|
320
|
-
const
|
|
320
|
+
const text = v.state.doc.toString();
|
|
321
|
+
const index = locateCaret(text, context2);
|
|
321
322
|
if (index === null) return false;
|
|
322
|
-
|
|
323
|
+
const toIndex = to ? locateCaret(text, to) : null;
|
|
324
|
+
v.dispatch({
|
|
325
|
+
selection: toIndex !== null && toIndex !== index ? { anchor: index, head: toIndex } : { anchor: index },
|
|
326
|
+
effects: EditorView.scrollIntoView(index, { y: "center" })
|
|
327
|
+
});
|
|
323
328
|
v.focus();
|
|
324
329
|
return true;
|
|
325
330
|
},
|
|
331
|
+
getCaret: () => {
|
|
332
|
+
const v = view.current;
|
|
333
|
+
if (!v) return null;
|
|
334
|
+
const text = v.state.doc.toString();
|
|
335
|
+
const { from, to, empty } = v.state.selection.main;
|
|
336
|
+
return {
|
|
337
|
+
from: withoutMarkup(contextAtIndex(text, from)),
|
|
338
|
+
to: empty ? null : withoutMarkup(contextAtIndex(text, to))
|
|
339
|
+
};
|
|
340
|
+
},
|
|
326
341
|
scrollToHeading: (_slug, offset) => {
|
|
327
342
|
const v = view.current;
|
|
328
343
|
if (!v) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Ref } from "react";
|
|
2
2
|
import { type VisualLoadStats } from "../core/visual-document.js";
|
|
3
|
-
import { type CaretContext } from "../core/caret-context.js";
|
|
3
|
+
import { type CaretCarry, type CaretContext } from "../core/caret-context.js";
|
|
4
4
|
import type { FindOptions } from "../core/find-replace.js";
|
|
5
5
|
import { type RichShellActions } from "./visual-extensions.js";
|
|
6
6
|
import type { FormatTarget } from "../format/format-target.js";
|
|
@@ -32,7 +32,9 @@ export interface EditorViewHandle {
|
|
|
32
32
|
};
|
|
33
33
|
scrollToHeading: (slug: string, offset: number) => void;
|
|
34
34
|
/** Put the caret at the text the person double-clicked in the rendered view; false when not found. */
|
|
35
|
-
placeCaret: (context: CaretContext) => boolean;
|
|
35
|
+
placeCaret: (context: CaretContext, to?: CaretContext | null) => boolean;
|
|
36
|
+
/** The caret or selection as text around it, to carry across a view switch; null when there is none. */
|
|
37
|
+
getCaret: () => CaretCarry | null;
|
|
36
38
|
find: (query: string, options: FindOptions, step?: 1 | -1) => ViewFindState;
|
|
37
39
|
replaceCurrent: (query: string, replacement: string, options: FindOptions) => string[];
|
|
38
40
|
replaceAll: (query: string, replacement: string, options: FindOptions) => string[];
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
serializeVisualDocument
|
|
18
18
|
} from "../core/visual-document.js";
|
|
19
19
|
import { findInDoc, replaceInDoc } from "../core/visual-find.js";
|
|
20
|
-
import { locateCaret } from "../core/caret-context.js";
|
|
20
|
+
import { contextAtIndex, locateCaret } from "../core/caret-context.js";
|
|
21
21
|
import { createVisualExtensions } from "./visual-extensions.js";
|
|
22
22
|
import { findHighlightKey } from "./decorations.js";
|
|
23
23
|
import { BlockHandle } from "./BlockHandle.js";
|
|
@@ -26,6 +26,21 @@ import { RICH_EDITOR_HOST_KEY, islandSelectionOf, tableAnchorOf } from "./format
|
|
|
26
26
|
import { useRichEditorContext } from "../editor/RichEditorContext.js";
|
|
27
27
|
const HEADLESS_SCHEMA = getSchema(createRichEditorExtensions());
|
|
28
28
|
const REPORT_DELAY_MS = 120;
|
|
29
|
+
function flattenDoc(doc) {
|
|
30
|
+
let flat = "";
|
|
31
|
+
const positions = [];
|
|
32
|
+
doc.descendants((node, pos) => {
|
|
33
|
+
if (node.isText && node.text) {
|
|
34
|
+
for (let i = 0; i < node.text.length; i++) positions.push(pos + i);
|
|
35
|
+
flat += node.text;
|
|
36
|
+
} else if (node.isBlock && flat.length > 0 && !flat.endsWith("\n")) {
|
|
37
|
+
positions.push(pos);
|
|
38
|
+
flat += "\n";
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
return { flat, positions };
|
|
43
|
+
}
|
|
29
44
|
function VisualEditor({
|
|
30
45
|
initialText,
|
|
31
46
|
onChange,
|
|
@@ -180,31 +195,30 @@ function VisualEditor({
|
|
|
180
195
|
editor.chain().setTextSelection(pos).focus(void 0, { scrollIntoView: false }).run();
|
|
181
196
|
}
|
|
182
197
|
},
|
|
183
|
-
placeCaret: (context2) => {
|
|
198
|
+
placeCaret: (context2, to) => {
|
|
184
199
|
if (!editor) return false;
|
|
185
|
-
|
|
186
|
-
const positions = [];
|
|
187
|
-
editor.state.doc.descendants((node, pos2) => {
|
|
188
|
-
if (node.isText && node.text) {
|
|
189
|
-
for (let i = 0; i < node.text.length; i++) positions.push(pos2 + i);
|
|
190
|
-
flat += node.text;
|
|
191
|
-
} else if (node.isBlock && flat.length > 0 && !flat.endsWith("\n")) {
|
|
192
|
-
positions.push(pos2);
|
|
193
|
-
flat += "\n";
|
|
194
|
-
}
|
|
195
|
-
return true;
|
|
196
|
-
});
|
|
200
|
+
const { flat, positions } = flattenDoc(editor.state.doc);
|
|
197
201
|
const index = locateCaret(flat, context2);
|
|
198
202
|
if (index === null) return false;
|
|
199
|
-
const
|
|
203
|
+
const posOf = (i) => i > 0 && positions[i - 1] !== void 0 ? positions[i - 1] + 1 : positions[i] ?? editor.state.doc.content.size - 1;
|
|
204
|
+
const toIndex = to ? locateCaret(flat, to) : null;
|
|
200
205
|
try {
|
|
201
|
-
editor.
|
|
206
|
+
const doc = editor.state.doc;
|
|
207
|
+
const selection = toIndex !== null && toIndex !== index ? TextSelection.between(doc.resolve(posOf(index)), doc.resolve(posOf(toIndex))) : TextSelection.near(doc.resolve(posOf(index)));
|
|
208
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection).scrollIntoView());
|
|
202
209
|
editor.view.focus();
|
|
203
210
|
return true;
|
|
204
211
|
} catch {
|
|
205
212
|
return false;
|
|
206
213
|
}
|
|
207
214
|
},
|
|
215
|
+
getCaret: () => {
|
|
216
|
+
if (!editor) return null;
|
|
217
|
+
const { flat, positions } = flattenDoc(editor.state.doc);
|
|
218
|
+
const indexOf = (pos) => positions.filter((p) => p < pos).length;
|
|
219
|
+
const { from, to, empty } = editor.state.selection;
|
|
220
|
+
return { from: contextAtIndex(flat, indexOf(from)), to: empty ? null : contextAtIndex(flat, indexOf(to)) };
|
|
221
|
+
},
|
|
208
222
|
find: (query, options, step) => {
|
|
209
223
|
if (!editor) return { count: 0, current: -1, skippedProtected: 0, error: null };
|
|
210
224
|
const result = findInDoc(editor.state.doc, query, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/rich-editor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "The AI Matrx editor: Visual (Tiptap), Source (CodeMirror), Preview, the markdown formatting command layer, and edit-in-place — over @ai-matrx/rich-content. Consumers that only render never install it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
"@ai-matrx/alchemy": "latest",
|
|
95
95
|
"@ai-matrx/content-ir": "latest",
|
|
96
96
|
"@ai-matrx/design-system": "latest",
|
|
97
|
-
"@ai-matrx/print": "latest",
|
|
98
97
|
"@ai-matrx/kit": "latest",
|
|
98
|
+
"@ai-matrx/print": "latest",
|
|
99
99
|
"@ai-matrx/rich-content": "latest"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|