@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.
- package/dist/ai-edits/apply.js +67 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +5 -5
- package/dist/layout-bridge/dom/clickToPositionDom.d.ts +26 -1
- package/dist/layout-bridge/dom/clickToPositionDom.js +60 -1
- package/dist/layout-bridge/dom/imeCaretAnchor.d.ts +42 -0
- package/dist/layout-bridge/dom/imeCaretAnchor.js +59 -0
- package/dist/layout-bridge/headerFooterLayout.js +7 -0
- package/dist/layout-painter/renderParagraph.js +4 -1
- package/dist/render-dom/RenderedDomContext.js +9 -1
- package/package.json +1 -1
package/dist/ai-edits/apply.js
CHANGED
|
@@ -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 = [
|
|
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 = [
|
|
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.
|
|
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 &&
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|