@stll/folio-core 0.15.10 → 0.15.12
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/docx/rezip.js +44 -3
- package/dist/docx/serializer/noteSerializer.d.ts +7 -1
- package/dist/docx/serializer/noteSerializer.js +30 -1
- package/dist/layout-bridge/dom/clickToPositionDom.d.ts +16 -1
- package/dist/layout-bridge/dom/clickToPositionDom.js +35 -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/render-dom/RenderedDomContext.js +7 -0
- package/package.json +1 -1
package/dist/docx/rezip.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExten
|
|
|
11
11
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
12
12
|
import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
|
|
13
13
|
import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
|
|
14
|
-
import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
|
|
14
|
+
import { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart } from "./serializer/noteSerializer.js";
|
|
15
15
|
import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
|
|
16
16
|
import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
|
|
17
17
|
import { serializeStylesXml } from "./serializer/stylesSerializer.js";
|
|
@@ -978,9 +978,50 @@ function serializeHeadersFootersToZip(doc, zip, compressionLevel) {
|
|
|
978
978
|
*/
|
|
979
979
|
async function serializeNotesToZip(doc, originalZip, newZip, compressionLevel) {
|
|
980
980
|
const footnotes = doc.package.footnotes ?? [];
|
|
981
|
-
if (footnotes.length > 0) await patchNotePartIntoZip("word/footnotes.xml", serializeFootnotes(footnotes), (xml) => serializeFootnotes(parseFootnotes(xml).getNormalFootnotes()), originalZip, newZip, compressionLevel);
|
|
981
|
+
if (footnotes.length > 0) if (findNotePartEntry(originalZip, "word/footnotes.xml")) await patchNotePartIntoZip("word/footnotes.xml", serializeFootnotes(footnotes), (xml) => serializeFootnotes(parseFootnotes(xml).getNormalFootnotes()), originalZip, newZip, compressionLevel);
|
|
982
|
+
else await materializeNewNotePart({
|
|
983
|
+
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
984
|
+
newZip,
|
|
985
|
+
partPath: "word/footnotes.xml",
|
|
986
|
+
relationshipType: RELATIONSHIP_TYPES.footnotes,
|
|
987
|
+
serializedPart: serializeNewFootnotesPart(footnotes),
|
|
988
|
+
compressionLevel
|
|
989
|
+
});
|
|
982
990
|
const endnotes = doc.package.endnotes ?? [];
|
|
983
|
-
if (endnotes.length > 0) await patchNotePartIntoZip("word/endnotes.xml", serializeEndnotes(endnotes), (xml) => serializeEndnotes(parseEndnotes(xml).getNormalEndnotes()), originalZip, newZip, compressionLevel);
|
|
991
|
+
if (endnotes.length > 0) if (findNotePartEntry(originalZip, "word/endnotes.xml")) await patchNotePartIntoZip("word/endnotes.xml", serializeEndnotes(endnotes), (xml) => serializeEndnotes(parseEndnotes(xml).getNormalEndnotes()), originalZip, newZip, compressionLevel);
|
|
992
|
+
else await materializeNewNotePart({
|
|
993
|
+
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
994
|
+
newZip,
|
|
995
|
+
partPath: "word/endnotes.xml",
|
|
996
|
+
relationshipType: RELATIONSHIP_TYPES.endnotes,
|
|
997
|
+
serializedPart: serializeNewEndnotesPart(endnotes),
|
|
998
|
+
compressionLevel
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
async function materializeNewNotePart({ contentType, newZip, partPath, relationshipType, serializedPart, compressionLevel }) {
|
|
1002
|
+
const contentTypesFile = findNotePartEntry(newZip, "[content_types].xml");
|
|
1003
|
+
const relationshipsFile = findNotePartEntry(newZip, "word/_rels/document.xml.rels");
|
|
1004
|
+
if (!contentTypesFile || !relationshipsFile) throw new DocxPackageFidelityError(`Cannot create ${partPath}: the DOCX package is missing content types or document relationships`);
|
|
1005
|
+
const compressionOptions = { level: compressionLevel };
|
|
1006
|
+
const contentTypesXml = await contentTypesFile.async("text");
|
|
1007
|
+
const partName = `/${partPath}`;
|
|
1008
|
+
if (!contentTypesXml.toLowerCase().includes(partName.toLowerCase())) newZip.file(contentTypesFile.name, contentTypesXml.replace("</Types>", () => `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`), {
|
|
1009
|
+
compression: "DEFLATE",
|
|
1010
|
+
compressionOptions
|
|
1011
|
+
});
|
|
1012
|
+
const relationshipsXml = await relationshipsFile.async("text");
|
|
1013
|
+
if (!relationshipsXml.includes(relationshipType)) {
|
|
1014
|
+
const relationshipId = `rId${findMaxRId(relationshipsXml) + 1}`;
|
|
1015
|
+
const target = partPath.slice(5);
|
|
1016
|
+
newZip.file(relationshipsFile.name, relationshipsXml.replace("</Relationships>", () => `<Relationship Id="${relationshipId}" Type="${relationshipType}" Target="${target}"/></Relationships>`), {
|
|
1017
|
+
compression: "DEFLATE",
|
|
1018
|
+
compressionOptions
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
newZip.file(partPath, serializedPart, {
|
|
1022
|
+
compression: "DEFLATE",
|
|
1023
|
+
compressionOptions
|
|
1024
|
+
});
|
|
984
1025
|
}
|
|
985
1026
|
const numberingBaselines = /* @__PURE__ */ new WeakMap();
|
|
986
1027
|
async function serializeNumberingIntoZip(doc, originalZip, newZip, compressionLevel) {
|
|
@@ -14,5 +14,11 @@ declare function serializeFootnotes(footnotes: readonly document_d_exports.Footn
|
|
|
14
14
|
* @returns Complete endnotes.xml string.
|
|
15
15
|
*/
|
|
16
16
|
declare function serializeEndnotes(endnotes: readonly document_d_exports.Endnote[]): string;
|
|
17
|
+
/** Serialize a brand-new footnote part, including Word's required separators
|
|
18
|
+
* and the automatic reference mark at the start of every normal note. */
|
|
19
|
+
declare function serializeNewFootnotesPart(footnotes: readonly document_d_exports.Footnote[]): string;
|
|
20
|
+
/** Serialize a brand-new endnote part, including Word's required separators
|
|
21
|
+
* and the automatic reference mark at the start of every normal note. */
|
|
22
|
+
declare function serializeNewEndnotesPart(endnotes: readonly document_d_exports.Endnote[]): string;
|
|
17
23
|
//#endregion
|
|
18
|
-
export { serializeEndnotes, serializeFootnotes };
|
|
24
|
+
export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
|
|
@@ -48,6 +48,25 @@ function serializeNote(elementName, note) {
|
|
|
48
48
|
const body = note.content.map((block) => serializeBlock(block)).join("");
|
|
49
49
|
return `<w:${elementName} ${attrs.join(" ")}>${body}</w:${elementName}>`;
|
|
50
50
|
}
|
|
51
|
+
const NOTE_SEPARATOR_BODY = "<w:p><w:pPr><w:spacing w:after=\"0\" w:line=\"240\" w:lineRule=\"auto\"/></w:pPr>";
|
|
52
|
+
function serializeRequiredNoteSeparators(elementName) {
|
|
53
|
+
return `<w:${elementName} w:type="separator" w:id="-1">${NOTE_SEPARATOR_BODY}<w:r><w:separator/></w:r></w:p></w:${elementName}><w:${elementName} w:type="continuationSeparator" w:id="0">${NOTE_SEPARATOR_BODY}<w:r><w:continuationSeparator/></w:r></w:p></w:${elementName}>`;
|
|
54
|
+
}
|
|
55
|
+
function insertNoteReferenceMark(xml, elementName) {
|
|
56
|
+
const paragraphOpen = /<w:p(?=[\s>])[^>]*>/u.exec(xml);
|
|
57
|
+
if (!paragraphOpen) return `${`<w:p><w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r></w:p>`}${xml}`;
|
|
58
|
+
const paragraphStart = paragraphOpen.index + paragraphOpen[0].length;
|
|
59
|
+
const paragraphEnd = xml.indexOf("</w:p>", paragraphStart);
|
|
60
|
+
const propertiesEnd = xml.indexOf("</w:pPr>", paragraphStart);
|
|
61
|
+
const insertAt = propertiesEnd !== -1 && propertiesEnd < paragraphEnd ? propertiesEnd + 8 : paragraphStart;
|
|
62
|
+
const referenceRun = `<w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r>`;
|
|
63
|
+
return `${xml.slice(0, insertAt)}${referenceRun}${xml.slice(insertAt)}`;
|
|
64
|
+
}
|
|
65
|
+
function serializeNewNotePart(elementName, notes) {
|
|
66
|
+
const nsDecl = buildNamespaceDeclarations();
|
|
67
|
+
const serializedNotes = notes.map((note) => insertNoteReferenceMark(serializeNote(elementName, note), elementName)).join("");
|
|
68
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:${elementName}s ${nsDecl} mc:Ignorable="w14 w15 wp14">` + serializeRequiredNoteSeparators(elementName) + serializedNotes + `</w:${elementName}s>`;
|
|
69
|
+
}
|
|
51
70
|
/**
|
|
52
71
|
* Serialize footnotes to a complete word/footnotes.xml string.
|
|
53
72
|
*
|
|
@@ -66,5 +85,15 @@ function serializeFootnotes(footnotes) {
|
|
|
66
85
|
function serializeEndnotes(endnotes) {
|
|
67
86
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:endnotes ${buildNamespaceDeclarations()} mc:Ignorable="w14 w15 wp14">${endnotes.map((en) => serializeNote("endnote", en)).join("")}</w:endnotes>`;
|
|
68
87
|
}
|
|
88
|
+
/** Serialize a brand-new footnote part, including Word's required separators
|
|
89
|
+
* and the automatic reference mark at the start of every normal note. */
|
|
90
|
+
function serializeNewFootnotesPart(footnotes) {
|
|
91
|
+
return serializeNewNotePart("footnote", footnotes);
|
|
92
|
+
}
|
|
93
|
+
/** Serialize a brand-new endnote part, including Word's required separators
|
|
94
|
+
* and the automatic reference mark at the start of every normal note. */
|
|
95
|
+
function serializeNewEndnotesPart(endnotes) {
|
|
96
|
+
return serializeNewNotePart("endnote", endnotes);
|
|
97
|
+
}
|
|
69
98
|
//#endregion
|
|
70
|
-
export { serializeEndnotes, serializeFootnotes };
|
|
99
|
+
export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
|
|
@@ -61,6 +61,21 @@ 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): CollapsedLineEdgeCaretGeometry | null;
|
|
64
79
|
declare function getCaretPositionFromDom(container: HTMLElement, pmPos: number, overlayRect: DOMRect): DomCaretPosition | null;
|
|
65
80
|
//#endregion
|
|
66
|
-
export { DomCaretPosition, DomSelectionRect, clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getSelectionRectsFromDom };
|
|
81
|
+
export { CollapsedLineEdgeCaretGeometry, DomCaretPosition, DomSelectionRect, clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
|
|
@@ -225,6 +225,29 @@ 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) {
|
|
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
|
+
return {
|
|
246
|
+
left: spanRect.left,
|
|
247
|
+
top: anchorRect.top,
|
|
248
|
+
height: anchorRect.height || lineRect?.height || 16
|
|
249
|
+
};
|
|
250
|
+
}
|
|
228
251
|
function getCaretPositionFromDom(container, pmPos, overlayRect) {
|
|
229
252
|
const spans = htmlQueryAll(container, ".layout-page-content span[data-pm-start][data-pm-end]");
|
|
230
253
|
for (const spanEl of spans) {
|
|
@@ -247,6 +270,17 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
|
|
|
247
270
|
continue;
|
|
248
271
|
}
|
|
249
272
|
if (pmPos >= pmStart && pmPos <= pmEnd) {
|
|
273
|
+
const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(spanEl);
|
|
274
|
+
if (collapsedGeometry) {
|
|
275
|
+
const pageEl = closestHtmlElement(spanEl, ".layout-page");
|
|
276
|
+
const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
|
|
277
|
+
return {
|
|
278
|
+
x: collapsedGeometry.left - overlayRect.left,
|
|
279
|
+
y: collapsedGeometry.top - overlayRect.top,
|
|
280
|
+
height: collapsedGeometry.height,
|
|
281
|
+
pageIndex
|
|
282
|
+
};
|
|
283
|
+
}
|
|
250
284
|
const textNode = spanEl.firstChild;
|
|
251
285
|
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
|
|
252
286
|
const spanRect = spanEl.getBoundingClientRect();
|
|
@@ -301,4 +335,4 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
|
|
|
301
335
|
return null;
|
|
302
336
|
}
|
|
303
337
|
//#endregion
|
|
304
|
-
export { clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getSelectionRectsFromDom };
|
|
338
|
+
export { clickToPositionDom, 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);
|
|
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCollapsedLineEdgeCaretGeometry } 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
|
|
@@ -36,6 +37,12 @@ var RenderedDomContextImpl = class {
|
|
|
36
37
|
y: (spanRect.top - containerRect.top) / this.#zoom,
|
|
37
38
|
height: lineHeightFor(span, this.#zoom)
|
|
38
39
|
};
|
|
40
|
+
const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(span);
|
|
41
|
+
if (collapsedGeometry) return {
|
|
42
|
+
x: (collapsedGeometry.left - containerRect.left) / this.#zoom,
|
|
43
|
+
y: (collapsedGeometry.top - containerRect.top) / this.#zoom,
|
|
44
|
+
height: lineHeightFor(span, this.#zoom)
|
|
45
|
+
};
|
|
39
46
|
const textNode = textNodeForSpan(span);
|
|
40
47
|
if (!textNode) return {
|
|
41
48
|
x: (spanRect.left - containerRect.left) / this.#zoom,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.12",
|
|
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",
|