@stll/folio-core 0.15.11 → 0.15.13

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.
@@ -42,6 +42,10 @@ const applyReplaceBlockStyleId = ({ item, tr }) => {
42
42
  styleId: item.operation.styleId
43
43
  });
44
44
  };
45
+ const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
46
+ highlight: false,
47
+ runShading: false
48
+ };
45
49
  const applyInlineFormatting = ({ tr, schema, from, to, formatting }) => {
46
50
  for (const [name, enabled] of Object.entries(formatting)) {
47
51
  const markType = schema.marks[name];
@@ -60,6 +64,29 @@ const formattingWouldChange = (marks, formatting) => Object.entries(formatting).
60
64
  if (name === "underline") return mark?.attrs["style"] !== "single";
61
65
  return mark === void 0;
62
66
  });
67
+ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
68
+ if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
69
+ if (mode === "direct") return applyInlineFormatting({
70
+ tr,
71
+ schema,
72
+ from,
73
+ to,
74
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING
75
+ });
76
+ return applyTrackedInlineFormatting({
77
+ tr,
78
+ schema,
79
+ doc: tr.doc,
80
+ from,
81
+ to,
82
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
83
+ revisionId,
84
+ author,
85
+ date,
86
+ initials,
87
+ suggestionId
88
+ });
89
+ };
63
90
  const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date, initials, suggestionId = null }) => {
64
91
  const propertyChangeType = schema.marks["runPropertyChange"];
65
92
  if (!propertyChangeType) return tr;
@@ -418,6 +445,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
418
445
  case "replaceRange": {
419
446
  const revisionIdDelete = revisionSeed++;
420
447
  const revisionIdInsert = revisionSeed++;
448
+ const revisionIdBackground = revisionSeed++;
449
+ const stepsBeforeBackgroundClear = tr.steps.length;
450
+ tr = clearReplacementBackground({
451
+ tr,
452
+ schema: view.state.schema,
453
+ from: item.from,
454
+ to: item.to,
455
+ mode,
456
+ revisionId: revisionIdBackground,
457
+ author,
458
+ date,
459
+ initials,
460
+ suggestionId
461
+ });
462
+ const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
421
463
  tr = applyTextReplacement({
422
464
  tr,
423
465
  item,
@@ -430,7 +472,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
430
472
  suggestionId,
431
473
  initials
432
474
  });
433
- if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
475
+ if (producesTrackedChanges) appliedRevisionIds = [
476
+ revisionIdDelete,
477
+ revisionIdInsert,
478
+ ...clearedBackground ? [revisionIdBackground] : []
479
+ ];
434
480
  break;
435
481
  }
436
482
  case "commentOnRange":
@@ -466,6 +512,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
466
512
  case "replaceBlock": {
467
513
  const revisionIdDelete = revisionSeed++;
468
514
  const revisionIdInsert = revisionSeed++;
515
+ const revisionIdBackground = revisionSeed++;
469
516
  if (item.operation.preserveFormatting === false && mode === "direct") {
470
517
  const replacement = item.operation.text;
471
518
  const paragraphType = view.state.schema.nodes["paragraph"];
@@ -485,6 +532,20 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
485
532
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
486
533
  break;
487
534
  }
535
+ const stepsBeforeBackgroundClear = tr.steps.length;
536
+ tr = clearReplacementBackground({
537
+ tr,
538
+ schema: view.state.schema,
539
+ from: item.from,
540
+ to: item.to,
541
+ mode,
542
+ revisionId: revisionIdBackground,
543
+ author,
544
+ date,
545
+ initials,
546
+ suggestionId
547
+ });
548
+ const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
488
549
  tr = applyTextReplacement({
489
550
  tr,
490
551
  item,
@@ -501,7 +562,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
501
562
  item,
502
563
  tr
503
564
  });
504
- if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
565
+ if (producesTrackedChanges) appliedRevisionIds = [
566
+ revisionIdDelete,
567
+ revisionIdInsert,
568
+ ...clearedBackground ? [revisionIdBackground] : []
569
+ ];
505
570
  break;
506
571
  }
507
572
  case "insertAfterBlock":
@@ -69,7 +69,7 @@ function formatNumberedMarker(counters, level) {
69
69
  const parts = [];
70
70
  for (let i = 0; i <= level; i += 1) {
71
71
  const value = counters[i] ?? 0;
72
- if (value <= 0) break;
72
+ if (!Number.isFinite(value) || value <= 0) break;
73
73
  parts.push(value);
74
74
  }
75
75
  if (parts.length === 0) return "1.";
@@ -156,23 +156,23 @@ function computeListMarker(pmAttrs, listCounters, abstractCounters, seenNumIds)
156
156
  const abstractNumId = pmAttrs.listAbstractNumId;
157
157
  if (level > 0) {
158
158
  const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId);
159
- if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
159
+ if (counters.slice(0, level).every((counter) => !Number.isFinite(counter))) for (let i = 0; i < level; i += 1) {
160
160
  const latestCounter = latestAbstractCounters?.[i];
161
- counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
161
+ counters[i] = latestCounter !== void 0 && Number.isFinite(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
162
162
  }
163
163
  }
164
164
  const seenKey = `${numId}:${level}`;
165
165
  if (!seenNumIds.has(seenKey)) {
166
166
  seenNumIds.add(seenKey);
167
167
  if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1;
168
- else if (Number.isNaN(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
169
168
  }
169
+ if (!Number.isFinite(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
170
170
  counters[level] = (counters[level] ?? 0) + 1;
171
171
  for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
172
172
  const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0;
173
173
  if (childAdvances > 0 && level + 1 < counters.length) {
174
174
  const childCounter = counters[level + 1];
175
- counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
175
+ counters[level + 1] = (childCounter === void 0 || !Number.isFinite(childCounter) ? 0 : childCounter) + childAdvances;
176
176
  }
177
177
  listCounters.set(numId, counters);
178
178
  if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]);
@@ -61,6 +61,31 @@ type DomCaretPosition = {
61
61
  height: number;
62
62
  pageIndex: number;
63
63
  };
64
+ type CollapsedLineEdgeCaretGeometry = {
65
+ left: number;
66
+ top: number;
67
+ /** Viewport height of the visual anchor. Full-line caret painters retain
68
+ * their own line-height policy instead of using this glyph-box height. */
69
+ height: number;
70
+ };
71
+ /**
72
+ * Resolve viewport geometry for a line-edge space span that the painter keeps
73
+ * addressable but collapses to zero font size. Browser ranges inside those
74
+ * spans report their zero-height baseline as `top`, which makes a caret appear
75
+ * below the line until visible text is typed. Anchor the caret to the nearest
76
+ * painted sibling instead; an all-whitespace line falls back to its line box.
77
+ */
78
+ declare function getCollapsedLineEdgeCaretGeometry(spanEl: HTMLElement, pmPos?: number): CollapsedLineEdgeCaretGeometry | null;
79
+ type CollapsedLineEdgeCaretTarget = {
80
+ span: HTMLElement;
81
+ geometry: CollapsedLineEdgeCaretGeometry;
82
+ };
83
+ /**
84
+ * Prefer a collapsed line-edge span when multiple painted spans share the PM
85
+ * position. Generic text runs use inclusive endpoints, so DOM order alone
86
+ * cannot decide which span owns a boundary.
87
+ */
88
+ declare function findCollapsedLineEdgeCaretTarget(spans: readonly HTMLElement[], pmPos: number): CollapsedLineEdgeCaretTarget | null;
64
89
  declare function getCaretPositionFromDom(container: HTMLElement, pmPos: number, overlayRect: DOMRect): DomCaretPosition | null;
65
90
  //#endregion
66
- export { DomCaretPosition, DomSelectionRect, clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getSelectionRectsFromDom };
91
+ export { CollapsedLineEdgeCaretGeometry, CollapsedLineEdgeCaretTarget, DomCaretPosition, DomSelectionRect, clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -225,8 +225,67 @@ function getSelectionRectsFromDom(container, from, to, overlayRect) {
225
225
  }
226
226
  return rects;
227
227
  }
228
+ /**
229
+ * Resolve viewport geometry for a line-edge space span that the painter keeps
230
+ * addressable but collapses to zero font size. Browser ranges inside those
231
+ * spans report their zero-height baseline as `top`, which makes a caret appear
232
+ * below the line until visible text is typed. Anchor the caret to the nearest
233
+ * painted sibling instead; an all-whitespace line falls back to its line box.
234
+ */
235
+ function getCollapsedLineEdgeCaretGeometry(spanEl, pmPos) {
236
+ const isLeading = spanEl.dataset["collapsedLeadingSpaces"] === "true";
237
+ const isTrailing = spanEl.dataset["collapsedTrailingSpaces"] === "true";
238
+ if (!isLeading && !isTrailing) return null;
239
+ let sibling = isTrailing ? spanEl.previousElementSibling : spanEl.nextElementSibling;
240
+ while (sibling && (sibling.getAttribute("data-collapsed-leading-spaces") === "true" || sibling.getAttribute("data-collapsed-trailing-spaces") === "true")) sibling = isTrailing ? sibling.previousElementSibling : sibling.nextElementSibling;
241
+ const spanRect = spanEl.getBoundingClientRect();
242
+ const line = closestHtmlElement(spanEl, ".layout-line");
243
+ const anchorRect = sibling?.getBoundingClientRect() ?? line?.getBoundingClientRect() ?? spanRect;
244
+ const lineRect = line?.getBoundingClientRect();
245
+ const pmStart = Number(spanEl.dataset["pmStart"]);
246
+ const pmEnd = Number(spanEl.dataset["pmEnd"]);
247
+ const storedSpaceAdvance = Number(spanEl.dataset["collapsedSpaceAdvance"]);
248
+ const lineScale = line && line.offsetWidth > 0 && lineRect ? lineRect.width / line.offsetWidth : 1;
249
+ const trailingSpaceCount = isTrailing && pmPos !== void 0 && Number.isFinite(pmStart) && Number.isFinite(pmEnd) ? Math.max(0, Math.min(pmPos, pmEnd) - pmStart) : 0;
250
+ const inlineDirection = spanRect.left <= anchorRect.left ? -1 : 1;
251
+ const trailingAdvance = Number.isFinite(storedSpaceAdvance) && lineScale > 0 ? trailingSpaceCount * storedSpaceAdvance * lineScale * inlineDirection : 0;
252
+ return {
253
+ left: spanRect.left + trailingAdvance,
254
+ top: anchorRect.top,
255
+ height: anchorRect.height || lineRect?.height || 16
256
+ };
257
+ }
258
+ /**
259
+ * Prefer a collapsed line-edge span when multiple painted spans share the PM
260
+ * position. Generic text runs use inclusive endpoints, so DOM order alone
261
+ * cannot decide which span owns a boundary.
262
+ */
263
+ function findCollapsedLineEdgeCaretTarget(spans, pmPos) {
264
+ for (const span of spans) {
265
+ const pmStart = Number(span.dataset["pmStart"]);
266
+ const pmEnd = Number(span.dataset["pmEnd"]);
267
+ if (pmPos < pmStart || pmPos > pmEnd) continue;
268
+ const geometry = getCollapsedLineEdgeCaretGeometry(span, pmPos);
269
+ if (geometry) return {
270
+ span,
271
+ geometry
272
+ };
273
+ }
274
+ return null;
275
+ }
228
276
  function getCaretPositionFromDom(container, pmPos, overlayRect) {
229
277
  const spans = htmlQueryAll(container, ".layout-page-content span[data-pm-start][data-pm-end]");
278
+ const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
279
+ if (collapsedTarget) {
280
+ const pageEl = closestHtmlElement(collapsedTarget.span, ".layout-page");
281
+ const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
282
+ return {
283
+ x: collapsedTarget.geometry.left - overlayRect.left,
284
+ y: collapsedTarget.geometry.top - overlayRect.top,
285
+ height: collapsedTarget.geometry.height,
286
+ pageIndex
287
+ };
288
+ }
230
289
  for (const spanEl of spans) {
231
290
  const pmStart = Number(spanEl.dataset["pmStart"]);
232
291
  const pmEnd = Number(spanEl.dataset["pmEnd"]);
@@ -301,4 +360,4 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
301
360
  return null;
302
361
  }
303
362
  //#endregion
304
- export { clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getSelectionRectsFromDom };
363
+ export { clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -0,0 +1,42 @@
1
+ //#region src/layout-bridge/dom/imeCaretAnchor.d.ts
2
+ type ImeCaretHost = {
3
+ style: {
4
+ transform: string;
5
+ };
6
+ };
7
+ type ImeCaretEditorView = {
8
+ readonly composing: boolean;
9
+ readonly dom?: HTMLElement;
10
+ readonly state: {
11
+ readonly selection: {
12
+ readonly empty: boolean;
13
+ readonly head: number;
14
+ };
15
+ };
16
+ hasFocus: () => boolean;
17
+ coordsAtPos: (position: number) => {
18
+ left: number;
19
+ top: number;
20
+ };
21
+ };
22
+ type VisibleCaretViewportRect = {
23
+ left: number;
24
+ top: number;
25
+ };
26
+ type SyncImeCaretAnchorOptions = {
27
+ hiddenHost: ImeCaretHost | null | undefined;
28
+ editorView: ImeCaretEditorView | null | undefined;
29
+ visibleCaret: VisibleCaretViewportRect | null | undefined;
30
+ };
31
+ /**
32
+ * Align the real off-screen ProseMirror caret with the painted document caret.
33
+ *
34
+ * Native IME candidate windows follow the focused contenteditable selection,
35
+ * not folio's visual caret overlay. Translating the hidden input host preserves
36
+ * split rendering while anchoring CJK candidate UI at the visible insertion
37
+ * point.
38
+ */
39
+ declare const syncImeCaretAnchor: ({ hiddenHost, editorView, visibleCaret }: SyncImeCaretAnchorOptions) => boolean;
40
+ declare const resetImeCaretAnchor: (hiddenHost: ImeCaretHost | null | undefined) => void;
41
+ //#endregion
42
+ export { resetImeCaretAnchor, syncImeCaretAnchor };
@@ -0,0 +1,59 @@
1
+ //#region src/layout-bridge/dom/imeCaretAnchor.ts
2
+ const resetHostTransform = (hiddenHost) => {
3
+ if (hiddenHost) hiddenHost.style.transform = "";
4
+ };
5
+ const getNativeSelectionCaret = (editorView) => {
6
+ const editorDom = editorView.dom;
7
+ const selection = editorDom?.ownerDocument.getSelection();
8
+ if (!editorDom || !selection || selection.rangeCount === 0) return null;
9
+ const range = selection.getRangeAt(0);
10
+ if (!editorDom.contains(range.startContainer)) return null;
11
+ const rect = range.getBoundingClientRect();
12
+ if (range.getClientRects().length === 0 || rect.left === 0 && rect.top === 0 && rect.width === 0 && rect.height === 0) return null;
13
+ return {
14
+ left: rect.left,
15
+ top: rect.top
16
+ };
17
+ };
18
+ /**
19
+ * Align the real off-screen ProseMirror caret with the painted document caret.
20
+ *
21
+ * Native IME candidate windows follow the focused contenteditable selection,
22
+ * not folio's visual caret overlay. Translating the hidden input host preserves
23
+ * split rendering while anchoring CJK candidate UI at the visible insertion
24
+ * point.
25
+ */
26
+ const syncImeCaretAnchor = ({ hiddenHost, editorView, visibleCaret }) => {
27
+ if (!hiddenHost) return false;
28
+ if (!editorView) {
29
+ resetHostTransform(hiddenHost);
30
+ return false;
31
+ }
32
+ if (editorView.composing) return false;
33
+ if (!visibleCaret || !editorView.hasFocus() || !editorView.state.selection.empty) {
34
+ resetHostTransform(hiddenHost);
35
+ return false;
36
+ }
37
+ const previousTransform = hiddenHost.style.transform;
38
+ hiddenHost.style.transform = "";
39
+ let hiddenCaret;
40
+ try {
41
+ hiddenCaret = getNativeSelectionCaret(editorView) ?? editorView.coordsAtPos(editorView.state.selection.head);
42
+ } catch {
43
+ hiddenHost.style.transform = previousTransform;
44
+ return false;
45
+ }
46
+ if (!Number.isFinite(hiddenCaret.left) || !Number.isFinite(hiddenCaret.top) || !Number.isFinite(visibleCaret.left) || !Number.isFinite(visibleCaret.top)) {
47
+ resetHostTransform(hiddenHost);
48
+ return false;
49
+ }
50
+ const dx = Math.round(visibleCaret.left - hiddenCaret.left);
51
+ const dy = Math.round(visibleCaret.top - hiddenCaret.top);
52
+ hiddenHost.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
53
+ return true;
54
+ };
55
+ const resetImeCaretAnchor = (hiddenHost) => {
56
+ resetHostTransform(hiddenHost);
57
+ };
58
+ //#endregion
59
+ export { resetImeCaretAnchor, syncImeCaretAnchor };
@@ -1,3 +1,4 @@
1
+ import { getCollapsedLineEdgeCaretGeometry } from "./dom/clickToPositionDom.js";
1
2
  //#region src/layout-bridge/headerFooterLayout.ts
2
3
  const hfDomCache = {
3
4
  header: null,
@@ -68,6 +69,12 @@ function computeHfCaretRectFromView(view, section, doc = globalThis.document) {
68
69
  const end = Number(span.dataset["pmEnd"]);
69
70
  if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
70
71
  if (pmPos >= start && pmPos <= end) {
72
+ const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(span, pmPos);
73
+ if (collapsedGeometry) return {
74
+ top: collapsedGeometry.top,
75
+ left: collapsedGeometry.left,
76
+ height: collapsedGeometry.height
77
+ };
71
78
  const range = host.ownerDocument.createRange();
72
79
  const walker = host.ownerDocument.createTreeWalker(span, NodeFilter.SHOW_TEXT);
73
80
  let remaining = pmPos - start;
@@ -988,11 +988,14 @@ function renderLine(block, line, alignment, doc, options) {
988
988
  const splitRuns = splitTextRunsByEastAsia(sliceRunsForLine(block, line));
989
989
  const { runs: runsForLine, collapsedLeadingRuns: collapsedLeadingSpaceRuns, collapsedTrailingRuns: collapsedTrailingSpaceRuns } = splitCollapsibleLineEdgeSpaces(splitRuns, startsAfterSoftWrap(block, line));
990
990
  const isCollapsedLineEdgeSpaceRun = (run) => collapsedLeadingSpaceRuns.has(run) || collapsedTrailingSpaceRuns.has(run);
991
+ const collapsedSpaceMeasureText = collapsedLeadingSpaceRuns.size > 0 || collapsedTrailingSpaceRuns.size > 0 ? createTextMeasurer(doc) : void 0;
991
992
  const renderLineTextRun = (run) => {
992
993
  const runEl = renderTextRun(run, doc);
993
994
  if (collapsedLeadingSpaceRuns.has(run)) runEl.dataset["collapsedLeadingSpaces"] = "true";
994
995
  if (collapsedTrailingSpaceRuns.has(run)) runEl.dataset["collapsedTrailingSpaces"] = "true";
995
996
  if (isCollapsedLineEdgeSpaceRun(run)) {
997
+ const spaceAdvance = (collapsedSpaceMeasureText?.(" ", run.fontSize || 11, run.fontFamily || "Calibri", runMeasureStyle(run)) ?? 0) * ((run.horizontalScale ?? 100) / 100);
998
+ runEl.dataset["collapsedSpaceAdvance"] = String(spaceAdvance);
996
999
  runEl.style.fontSize = "0";
997
1000
  runEl.style.letterSpacing = "0";
998
1001
  runEl.style.wordSpacing = "0";
@@ -1055,7 +1058,7 @@ function renderLine(block, line, alignment, doc, options) {
1055
1058
  lineEl.style.overflow = "visible";
1056
1059
  let tabContext;
1057
1060
  const hasScaledTextRun = runsForLine.some((run) => (isTextRun(run) || isFieldRun(run) || isMathRun(run)) && run.horizontalScale !== void 0 && run.horizontalScale !== 100);
1058
- const measureText = hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0;
1061
+ const measureText = collapsedSpaceMeasureText ?? (hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0);
1059
1062
  if (hasTabRuns) {
1060
1063
  const explicitStops = options?.tabStops?.map(convertTabStopToCalc);
1061
1064
  const leftIndentTwips = options?.leftIndentPx ? Math.round(options.leftIndentPx * 15) : 0;
@@ -1,3 +1,4 @@
1
+ import { findCollapsedLineEdgeCaretTarget } from "../layout-bridge/dom/clickToPositionDom.js";
1
2
  import { findBodyEmptyRuns, findBodyPmSpans } from "../layout-bridge/dom/findBodyPmSpans.js";
2
3
  import { closestHtmlElement } from "../utils/domGuards.js";
3
4
  //#region src/render-dom/RenderedDomContext.ts
@@ -26,7 +27,14 @@ var RenderedDomContextImpl = class {
26
27
  }
27
28
  getCoordinatesForPosition(pmPos) {
28
29
  const containerRect = this.#pagesContainer.getBoundingClientRect();
29
- for (const span of findBodyPmSpans(this.#pagesContainer)) {
30
+ const spans = findBodyPmSpans(this.#pagesContainer);
31
+ const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
32
+ if (collapsedTarget) return {
33
+ x: (collapsedTarget.geometry.left - containerRect.left) / this.#zoom,
34
+ y: (collapsedTarget.geometry.top - containerRect.top) / this.#zoom,
35
+ height: lineHeightFor(collapsedTarget.span, this.#zoom)
36
+ };
37
+ for (const span of spans) {
30
38
  const pmStart = Number(span.dataset["pmStart"]);
31
39
  const pmEnd = Number(span.dataset["pmEnd"]);
32
40
  if (!(span.classList.contains("layout-run-tab") ? pmPos >= pmStart && pmPos < pmEnd : pmPos >= pmStart && pmPos <= pmEnd)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.15.11",
3
+ "version": "0.15.13",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",