@stll/folio-core 0.1.2 → 0.2.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.
Files changed (101) hide show
  1. package/dist/docx/headerFooterParser.js +3 -0
  2. package/dist/docx/headerFooterVerbatim.d.ts +19 -0
  3. package/dist/docx/headerFooterVerbatim.js +25 -0
  4. package/dist/docx/index.d.ts +2 -0
  5. package/dist/docx/index.js +2 -0
  6. package/dist/docx/metafileRaster.d.ts +30 -0
  7. package/dist/docx/metafileRaster.js +80 -0
  8. package/dist/docx/paragraphParser.js +5 -1
  9. package/dist/docx/parser.d.ts +12 -2
  10. package/dist/docx/parser.js +31 -1
  11. package/dist/docx/serializer/headerFooterSerializer.js +3 -0
  12. package/dist/docx/styleParser.js +2 -2
  13. package/dist/docx/tableParser.js +2 -3
  14. package/dist/docx/unzip.js +7 -1
  15. package/dist/docx/wrapTypes.d.ts +8 -1
  16. package/dist/docx/wrapTypes.js +0 -8
  17. package/dist/docx/xmlParser.d.ts +7 -1
  18. package/dist/docx/xmlParser.js +17 -1
  19. package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
  20. package/dist/i18n/messages/catalogs.gen.js +2742 -0
  21. package/dist/i18n/messages/messages.gen.d.ts +166 -0
  22. package/dist/i18n/messages/messages.gen.js +0 -0
  23. package/dist/i18n/messages.d.ts +13 -0
  24. package/dist/i18n/messages.js +39 -0
  25. package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
  26. package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
  27. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -2
  28. package/dist/layout-bridge/convert/headerFooterLayout.js +39 -15
  29. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  30. package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
  31. package/dist/layout-bridge/headerFooterLayout.js +216 -0
  32. package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
  33. package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
  34. package/dist/layout-bridge/sectionColumns.d.ts +11 -0
  35. package/dist/layout-bridge/sectionColumns.js +21 -0
  36. package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
  37. package/dist/layout-bridge/tableInsertHover.js +102 -0
  38. package/dist/layout-engine/index.d.ts +2 -1
  39. package/dist/layout-engine/index.js +2 -1
  40. package/dist/layout-engine/measure/lineBreaks.d.ts +10 -0
  41. package/dist/layout-engine/measure/lineBreaks.js +30 -0
  42. package/dist/layout-engine/measure/measureContainer.js +1 -1
  43. package/dist/layout-engine/measure/measureParagraph.js +63 -22
  44. package/dist/layout-engine/pmPageIndex.d.ts +24 -0
  45. package/dist/layout-engine/pmPageIndex.js +37 -0
  46. package/dist/layout-engine/types.d.ts +2 -1
  47. package/dist/layout-painter/imageLayout.d.ts +101 -0
  48. package/dist/layout-painter/imageLayout.js +161 -0
  49. package/dist/layout-painter/renderPage.d.ts +16 -1
  50. package/dist/layout-painter/renderPage.js +28 -1
  51. package/dist/layout-painter/renderParagraph.js +2 -1
  52. package/dist/layout-painter/sdtBoundary.d.ts +24 -1
  53. package/dist/layout-painter/sdtBoundary.js +40 -1
  54. package/dist/managers/AutoSaveManager.d.ts +89 -0
  55. package/dist/managers/AutoSaveManager.js +279 -0
  56. package/dist/managers/ClipboardManager.d.ts +31 -0
  57. package/dist/managers/ClipboardManager.js +147 -0
  58. package/dist/paged-layout/paragraphFlash.d.ts +17 -0
  59. package/dist/paged-layout/paragraphFlash.js +56 -0
  60. package/dist/paged-layout/paragraphFlashTypes.d.ts +18 -0
  61. package/dist/paged-layout/paragraphFlashTypes.js +0 -0
  62. package/dist/paged-layout/rangeProjection.d.ts +23 -1
  63. package/dist/paged-layout/rangeProjection.js +34 -1
  64. package/dist/prosemirror/cellDragSelection.d.ts +34 -0
  65. package/dist/prosemirror/cellDragSelection.js +69 -0
  66. package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
  67. package/dist/prosemirror/commands/sectionBreak.js +54 -0
  68. package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
  69. package/dist/prosemirror/commentIdAllocator.js +35 -0
  70. package/dist/prosemirror/commentOps.d.ts +35 -0
  71. package/dist/prosemirror/commentOps.js +104 -0
  72. package/dist/prosemirror/conversion/toProseDoc.js +29 -6
  73. package/dist/prosemirror/extensions/index.d.ts +3 -0
  74. package/dist/prosemirror/extensions/index.js +3 -0
  75. package/dist/prosemirror/imageCommit.d.ts +42 -0
  76. package/dist/prosemirror/imageCommit.js +118 -0
  77. package/dist/prosemirror/paraText.d.ts +26 -0
  78. package/dist/prosemirror/paraText.js +73 -0
  79. package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
  80. package/dist/prosemirror/plugins/templateDirectives.js +40 -1
  81. package/dist/prosemirror/queries.d.ts +46 -0
  82. package/dist/prosemirror/queries.js +74 -0
  83. package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
  84. package/dist/prosemirror/styles/styleResolver.js +34 -0
  85. package/dist/prosemirror/tableResize.d.ts +49 -0
  86. package/dist/prosemirror/tableResize.js +162 -0
  87. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
  88. package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
  89. package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
  90. package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
  91. package/dist/style-engine/index.d.ts +2 -2
  92. package/dist/style-engine/styleEngine.d.ts +14 -1
  93. package/dist/style-engine/styleEngine.js +15 -0
  94. package/dist/utils/colorResolver.d.ts +8 -1
  95. package/dist/utils/colorResolver.js +12 -1
  96. package/dist/utils/findVerticalScrollParent.d.ts +20 -0
  97. package/dist/utils/findVerticalScrollParent.js +30 -0
  98. package/dist/utils/fontResolver.d.ts +22 -3
  99. package/dist/utils/fontResolver.js +216 -31
  100. package/dist/utils/headerFooter.js +2 -0
  101. package/package.json +9 -1
@@ -0,0 +1,161 @@
1
+ import { pixelsToEmu } from "../utils/units.js";
2
+ //#region src/layout-painter/imageLayout.ts
3
+ const LAYOUT_IMAGE_CLASSES = {
4
+ /** Inline image rendered inside `.layout-line`. */
5
+ runImage: "layout-run-image",
6
+ /** Block (centered, topAndBottom) image. */
7
+ blockImage: "layout-block-image",
8
+ /** Anchored image rendered in the page-level floating layer. */
9
+ pageFloatingImage: "layout-page-floating-image",
10
+ /** Anchored image rendered inside a table cell's floating layer. */
11
+ cellFloatingImage: "layout-cell-floating-image",
12
+ pageContent: "layout-page-content",
13
+ paragraph: "layout-paragraph"
14
+ };
15
+ const IMAGE_HIT_SELECTOR = [
16
+ `.${LAYOUT_IMAGE_CLASSES.pageFloatingImage}`,
17
+ `.${LAYOUT_IMAGE_CLASSES.cellFloatingImage}`,
18
+ `.${LAYOUT_IMAGE_CLASSES.blockImage}`,
19
+ `.${LAYOUT_IMAGE_CLASSES.runImage}`
20
+ ].join(", ");
21
+ /**
22
+ * Walk up from a click target to the nearest rendered image element, returning
23
+ * just the element (no PM position parsing). Used by the left-click selection
24
+ * path in both adapters — `data-pm-start` is read separately by callers that
25
+ * need the position. Returns the element when the click was directly on an
26
+ * inline `<img.layout-run-image>` OR inside one of the container classes
27
+ * registered in `LAYOUT_IMAGE_CLASSES`.
28
+ */
29
+ function findImageElement(target) {
30
+ if (!(target instanceof HTMLElement)) return null;
31
+ if (target.tagName === "IMG" && target.classList.contains(LAYOUT_IMAGE_CLASSES.runImage)) return target;
32
+ const container = target.closest(IMAGE_HIT_SELECTOR);
33
+ if (container && container.dataset["pmStart"]) return container;
34
+ return null;
35
+ }
36
+ /**
37
+ * Capture the rendered position of an inline image as EMUs, normalised to
38
+ * unzoomed coordinates. Returns horizontal offset relative to the page
39
+ * content area (column origin) and vertical offset relative to the
40
+ * containing paragraph — matches the OOXML attrs the resolver writes
41
+ * (`relativeFrom: 'column'` / `relativeFrom: 'paragraph'`).
42
+ *
43
+ * `zoom` defaults to 1; pass the editor's current zoom factor when the
44
+ * pages container has a CSS scale applied so `getBoundingClientRect` deltas
45
+ * are converted back to authored-pixel space before going to EMU.
46
+ *
47
+ * Returns undefined for non-inline images or detached DOM.
48
+ */
49
+ function captureInlinePositionEmu(imageEl, zoom = 1) {
50
+ if (!imageEl.classList.contains(LAYOUT_IMAGE_CLASSES.runImage)) return;
51
+ const pageContent = imageEl.closest(`.${LAYOUT_IMAGE_CLASSES.pageContent}`);
52
+ const paragraph = imageEl.closest(`.${LAYOUT_IMAGE_CLASSES.paragraph}`);
53
+ if (!pageContent || !paragraph) return;
54
+ const imgRect = imageEl.getBoundingClientRect();
55
+ const pageRect = pageContent.getBoundingClientRect();
56
+ const paraRect = paragraph.getBoundingClientRect();
57
+ const safeZoom = zoom > 0 ? zoom : 1;
58
+ return {
59
+ horizontalEmu: Math.round(pixelsToEmu((imgRect.left - pageRect.left) / safeZoom)),
60
+ verticalEmu: Math.round(pixelsToEmu((imgRect.top - paraRect.top) / safeZoom))
61
+ };
62
+ }
63
+ /**
64
+ * Map an image's current OOXML attrs onto the menu's choice vocabulary so the
65
+ * menu can highlight the active option. Returns null for `topAndBottom` and
66
+ * any unknown wrap type — those don't have a directional menu entry.
67
+ *
68
+ * `cssFloat` accepts both `null` and `undefined` so framework adapters can
69
+ * use either as their "unset" sentinel without an extra normalisation step.
70
+ */
71
+ function deriveLayoutChoice(wrapType, cssFloat) {
72
+ if (wrapType === "inline") return "inline";
73
+ if (wrapType === "behind") return "behind";
74
+ if (wrapType === "inFront") return "inFront";
75
+ if (wrapType === "square" || wrapType === "tight" || wrapType === "through") return cssFloat === "right" ? "squareRight" : "squareLeft";
76
+ return null;
77
+ }
78
+ /** Mirrors Word's Wrap Text menu — five directional options. */
79
+ const IMAGE_LAYOUT_OPTIONS = [
80
+ {
81
+ choice: "inline",
82
+ i18nLabelKey: "inLineWithText",
83
+ i18nDescKey: "inLineWithText",
84
+ iconHint: "inline"
85
+ },
86
+ {
87
+ choice: "squareLeft",
88
+ i18nLabelKey: "squareLeft",
89
+ i18nDescKey: "squareLeft",
90
+ iconHint: "squareLeft"
91
+ },
92
+ {
93
+ choice: "squareRight",
94
+ i18nLabelKey: "squareRight",
95
+ i18nDescKey: "squareRight",
96
+ iconHint: "squareRight"
97
+ },
98
+ {
99
+ choice: "behind",
100
+ i18nLabelKey: "behindText",
101
+ i18nDescKey: "behindText",
102
+ iconHint: "behind"
103
+ },
104
+ {
105
+ choice: "inFront",
106
+ i18nLabelKey: "inFrontOfText",
107
+ i18nDescKey: "inFrontOfText",
108
+ iconHint: "inFront"
109
+ }
110
+ ];
111
+ /**
112
+ * Whether a given option is enabled for an image with the given current wrap
113
+ * type. Every option stays clickable — picking the option that matches the
114
+ * current state is a no-op (the PM command early-returns), which matches
115
+ * Word's behavior. Kept around for forward-compatibility (e.g. future
116
+ * read-only mode), but currently always returns true.
117
+ */
118
+ function isImageLayoutOptionEnabled(_option, _currentWrapType) {
119
+ return true;
120
+ }
121
+ /**
122
+ * Translate the legacy toolbar wrap-type vocabulary (`wrapLeft` / `wrapRight`
123
+ * / `square` / `tight` / `through` / `topAndBottom` / `behind` / `inFront` /
124
+ * `inline`) into an `ImageLayoutTarget` so toolbar dispatch shares the same PM
125
+ * command path as the right-click menu.
126
+ *
127
+ * Returns `undefined` for unknown values; callers should treat that as
128
+ * "no-op".
129
+ */
130
+ function toolbarValueToLayoutTarget(value) {
131
+ switch (value) {
132
+ case "inline": return "inline";
133
+ case "square":
134
+ case "tight":
135
+ case "through": return "squareLeft";
136
+ case "topAndBottom": return "topAndBottom";
137
+ case "behind": return "behind";
138
+ case "inFront": return "inFront";
139
+ case "wrapRight": return "squareLeft";
140
+ case "wrapLeft": return "squareRight";
141
+ default: return;
142
+ }
143
+ }
144
+ /**
145
+ * Build the painter's `block.id → { block, measure }` lookup from the parallel
146
+ * blocks/measures arrays. Shared by both adapters' paint step.
147
+ */
148
+ function buildBlockLookup(blocks, measures) {
149
+ const lookup = /* @__PURE__ */ new Map();
150
+ for (let i = 0; i < blocks.length; i++) {
151
+ const block = blocks[i];
152
+ const measure = measures[i];
153
+ if (block && measure) lookup.set(String(block.id), {
154
+ block,
155
+ measure
156
+ });
157
+ }
158
+ return lookup;
159
+ }
160
+ //#endregion
161
+ export { IMAGE_LAYOUT_OPTIONS, LAYOUT_IMAGE_CLASSES, buildBlockLookup, captureInlinePositionEmu, deriveLayoutChoice, findImageElement, isImageLayoutOptionEnabled, toolbarValueToLayoutTarget };
@@ -242,6 +242,21 @@ type PainterPaintedDetail = {
242
242
  container: HTMLElement;
243
243
  pageCount: number;
244
244
  };
245
+ /**
246
+ * Force every virtualized page shell in `container` to be fully rendered.
247
+ *
248
+ * Virtualization keeps off-screen pages as empty shells, so cloning the pages
249
+ * container for print (or any DOM snapshot) yields blank pages past the visible
250
+ * band. Callers that need every page populated — print, export-to-HTML, pdf
251
+ * snapshot — should call this first.
252
+ *
253
+ * No-op for small documents (rendered eagerly) or containers that were never
254
+ * managed by `renderPages`. Returns the number of shells populated by this
255
+ * call (useful for tests).
256
+ *
257
+ * Ported from eigenpal/docx-editor `layout-painter/renderPage/virtualization.ts`.
258
+ */
259
+ declare function renderAllPagesNow(container: HTMLElement): number;
245
260
  /**
246
261
  * Find the page shell whose layout fragments cover (or are nearest
247
262
  * before) a given ProseMirror position. Returns null if the
@@ -261,4 +276,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
261
276
  isExact: boolean;
262
277
  } | null;
263
278
  //#endregion
264
- export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
279
+ export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -1668,6 +1668,33 @@ function populatePageShell(shell, pageDataMap, totalPages, options) {
1668
1668
  if (container instanceof HTMLElement) emitPainterPainted(container);
1669
1669
  }
1670
1670
  /**
1671
+ * Force every virtualized page shell in `container` to be fully rendered.
1672
+ *
1673
+ * Virtualization keeps off-screen pages as empty shells, so cloning the pages
1674
+ * container for print (or any DOM snapshot) yields blank pages past the visible
1675
+ * band. Callers that need every page populated — print, export-to-HTML, pdf
1676
+ * snapshot — should call this first.
1677
+ *
1678
+ * No-op for small documents (rendered eagerly) or containers that were never
1679
+ * managed by `renderPages`. Returns the number of shells populated by this
1680
+ * call (useful for tests).
1681
+ *
1682
+ * Ported from eigenpal/docx-editor `layout-painter/renderPage/virtualization.ts`.
1683
+ */
1684
+ function renderAllPagesNow(container) {
1685
+ const state = container.__pageRenderState;
1686
+ if (!state) return 0;
1687
+ const { pageStates, totalPages, currentOptions, pageDataMap } = state;
1688
+ let populated = 0;
1689
+ for (const { element } of pageStates) {
1690
+ const data = pageDataMap.get(element);
1691
+ if (!data || data.rendered) continue;
1692
+ populatePageShell(element, pageDataMap, totalPages, currentOptions);
1693
+ populated++;
1694
+ }
1695
+ return populated;
1696
+ }
1697
+ /**
1671
1698
  * Surgically replace only the content area of a rendered page shell.
1672
1699
  * Preserves header/footer elements to avoid blinking.
1673
1700
  */
@@ -1792,4 +1819,4 @@ function depopulatePageShell(shell, pageDataMap) {
1792
1819
  }
1793
1820
  }
1794
1821
  //#endregion
1795
- export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
1822
+ export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -3,6 +3,7 @@ import { resolveFontFamily } from "../utils/fontResolver.js";
3
3
  import "../utils/fontWeights.js";
4
4
  import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
5
5
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
6
+ import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
6
7
  import { isFloatingImageRun } from "../layout-engine/types.js";
7
8
  import { getListMarkerInlineWidth } from "../layout-engine/measure/listMarkerWidth.js";
8
9
  import { applySdtDataAttrs } from "./sdtBoundary.js";
@@ -11,7 +12,6 @@ import { ommlToMathml } from "../docx/mathToMathml.js";
11
12
  import { evaluateFieldInstruction } from "../fields/evaluateField.js";
12
13
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../utils/authorColors.js";
13
14
  import { detectBaseDirection } from "../utils/baseDirection.js";
14
- import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
15
15
  import { getAutomaticTextColorForBackground } from "./documentColors.js";
16
16
  import { resolveImageLineAlign } from "./renderUtils.js";
17
17
  //#region src/layout-painter/renderParagraph.ts
@@ -1075,6 +1075,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1075
1075
  fragmentEl.className = PARAGRAPH_CLASS_NAMES.fragment;
1076
1076
  fragmentEl.style.position = "relative";
1077
1077
  fragmentEl.dataset["blockId"] = String(fragment.blockId);
1078
+ if (block.paraId) fragmentEl.dataset["paraId"] = block.paraId;
1078
1079
  fragmentEl.dataset["fromLine"] = String(fragment.fromLine);
1079
1080
  fragmentEl.dataset["toLine"] = String(fragment.toLine);
1080
1081
  applyPmPositions(fragmentEl, fragment.pmStart, fragment.pmEnd);
@@ -1,6 +1,29 @@
1
1
  import { SdtGroup } from "../layout-engine/types.js";
2
+ import { Node } from "prosemirror-model";
2
3
 
3
4
  //#region src/layout-painter/sdtBoundary.d.ts
4
5
  declare function applySdtDataAttrs(el: HTMLElement, sdtGroups: readonly SdtGroup[] | undefined): void;
6
+ /**
7
+ * Identities of every block-level content control (`w:sdt`) that encloses the
8
+ * given selection — used to keep a control's boundary visible while the caret
9
+ * is inside it (Word-style focus), independent of mouse hover. Both ends of a
10
+ * range are collected so a selection straddling a control still lights it up.
11
+ *
12
+ * Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
13
+ * keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
14
+ * stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
15
+ * elements (see `applySdtDataAttrs`), so the identity here is the stringified
16
+ * `$pos.before(d)` — which is exactly that PM position — keeping the returned
17
+ * ids in lockstep with what `applySdtFocus` hit-tests against.
18
+ */
19
+ declare function enclosingSdtGroupIds(doc: Node, from: number, to: number): Set<string>;
20
+ /**
21
+ * Toggle the `.is-focused` reveal class on every painted block whose enclosing
22
+ * content control encloses the caret. Kept separate from any hover-driven
23
+ * reveal so the two paths never clear each other. Matches the fork's
24
+ * `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
25
+ * `data-sdt-group-id`, which this fork does not paint).
26
+ */
27
+ declare function applySdtFocus(container: HTMLElement, focusedIds: Set<string>): void;
5
28
  //#endregion
6
- export { applySdtDataAttrs };
29
+ export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
@@ -24,5 +24,44 @@ function applySdtDataAttrs(el, sdtGroups) {
24
24
  alias: g.alias ?? null
25
25
  })));
26
26
  }
27
+ /**
28
+ * Identities of every block-level content control (`w:sdt`) that encloses the
29
+ * given selection — used to keep a control's boundary visible while the caret
30
+ * is inside it (Word-style focus), independent of mouse hover. Both ends of a
31
+ * range are collected so a selection straddling a control still lights it up.
32
+ *
33
+ * Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
34
+ * keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
35
+ * stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
36
+ * elements (see `applySdtDataAttrs`), so the identity here is the stringified
37
+ * `$pos.before(d)` — which is exactly that PM position — keeping the returned
38
+ * ids in lockstep with what `applySdtFocus` hit-tests against.
39
+ */
40
+ function enclosingSdtGroupIds(doc, from, to) {
41
+ const ids = /* @__PURE__ */ new Set();
42
+ const max = doc.content.size;
43
+ const collect = (pos) => {
44
+ const $pos = doc.resolve(Math.max(0, Math.min(pos, max)));
45
+ for (let d = 1; d <= $pos.depth; d++) if ($pos.node(d).type.name === "blockSdt") ids.add(String($pos.before(d)));
46
+ };
47
+ collect(from);
48
+ if (to !== from) collect(to);
49
+ return ids;
50
+ }
51
+ /**
52
+ * Toggle the `.is-focused` reveal class on every painted block whose enclosing
53
+ * content control encloses the caret. Kept separate from any hover-driven
54
+ * reveal so the two paths never clear each other. Matches the fork's
55
+ * `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
56
+ * `data-sdt-group-id`, which this fork does not paint).
57
+ */
58
+ function applySdtFocus(container, focusedIds) {
59
+ const boxes = container.querySelectorAll("[data-sdt-boundary]");
60
+ for (const box of boxes) {
61
+ const id = box.dataset["sdtPmPos"];
62
+ const on = id != null && focusedIds.has(id);
63
+ if (on !== box.classList.contains("is-focused")) box.classList.toggle("is-focused", on);
64
+ }
65
+ }
27
66
  //#endregion
28
- export { applySdtDataAttrs };
67
+ export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
@@ -0,0 +1,89 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { Subscribable } from "./Subscribable.js";
3
+
4
+ //#region src/managers/AutoSaveManager.d.ts
5
+ /** Auto-save status. */
6
+ type AutoSaveStatus = "idle" | "saving" | "saved" | "error";
7
+ /** Configuration for {@link AutoSaveManager}. */
8
+ type AutoSaveManagerOptions = {
9
+ /** Storage key for localStorage (default: 'docx-editor-autosave'). */storageKey?: string; /** Save interval in milliseconds (default: 30000 - 30 seconds). */
10
+ interval?: number; /** Maximum age of auto-save before it is considered stale (default: 24 hours). */
11
+ maxAge?: number; /** Whether to save on document change with debounce (default: true). */
12
+ saveOnChange?: boolean; /** Debounce delay for saveOnChange in milliseconds (default: 2000). */
13
+ debounceDelay?: number; /** Callback when save succeeds. */
14
+ onSave?: (timestamp: Date) => void; /** Callback when save fails. */
15
+ onError?: (error: Error) => void; /** Callback when recovery data is found. */
16
+ onRecoveryAvailable?: (savedDocument: SavedDocumentData) => void;
17
+ };
18
+ /** Saved document data structure. */
19
+ type SavedDocumentData = {
20
+ /** The document. */document: document_d_exports.Document; /** When the document was saved (ISO string). */
21
+ savedAt: string; /** Version for format compatibility. */
22
+ version: number; /** Optional document identifier. */
23
+ documentId?: string;
24
+ };
25
+ /** AutoSaveManager snapshot for UI consumption. */
26
+ type AutoSaveSnapshot = {
27
+ status: AutoSaveStatus;
28
+ lastSaveTime: Date | null;
29
+ hasRecoveryData: boolean;
30
+ isEnabled: boolean;
31
+ };
32
+ declare class AutoSaveManager extends Subscribable<AutoSaveSnapshot> {
33
+ private readonly storageKey;
34
+ private readonly interval;
35
+ private readonly maxAge;
36
+ private readonly saveOnChange;
37
+ private readonly debounceDelay;
38
+ private readonly onSaveCallback;
39
+ private readonly onErrorCallback;
40
+ private readonly onRecoveryAvailableCallback;
41
+ private readonly storageAvailable;
42
+ private currentDocument;
43
+ private lastSavedJson;
44
+ private intervalTimer;
45
+ private debounceTimer;
46
+ private status;
47
+ private lastSaveTime;
48
+ private hasRecoveryData;
49
+ private isEnabled;
50
+ constructor(options?: AutoSaveManagerOptions);
51
+ /** Update the current document. Triggers a debounced save if enabled. */
52
+ onDocumentChanged(document: document_d_exports.Document | null): void;
53
+ /** Manually trigger a save. */
54
+ save(): boolean;
55
+ /** Clear auto-saved data from storage. */
56
+ clear(): void;
57
+ /** Get recovery data from storage. */
58
+ getRecoveryData(): SavedDocumentData | null;
59
+ /** Accept recovery and return the document. */
60
+ acceptRecovery(): document_d_exports.Document | null;
61
+ /** Dismiss recovery and clear saved data. */
62
+ dismissRecovery(): void;
63
+ /** Enable auto-save and start the interval timer. */
64
+ enable(): void;
65
+ /** Disable auto-save and stop all timers. */
66
+ disable(): void;
67
+ /** Start the interval timer. Call after enabling or on init. */
68
+ startInterval(): void;
69
+ /** Save synchronously on destroy (best-effort). */
70
+ destroy(): void;
71
+ private checkRecoveryData;
72
+ private persistToStorage;
73
+ private debounceSave;
74
+ private stopTimers;
75
+ private updateStatus;
76
+ private emitSnapshot;
77
+ }
78
+ /** Format last save time for display. */
79
+ declare function formatLastSaveTime(date: Date | null): string;
80
+ /** Get auto-save status label. */
81
+ declare function getAutoSaveStatusLabel(status: AutoSaveStatus): string;
82
+ /** Get storage size used by auto-save (bytes). */
83
+ declare function getAutoSaveStorageSize(storageKey?: string): number;
84
+ /** Format storage size for display. */
85
+ declare function formatStorageSize(bytes: number): string;
86
+ /** Check whether auto-save is supported. */
87
+ declare function isAutoSaveSupported(): boolean;
88
+ //#endregion
89
+ export { AutoSaveManager, AutoSaveManagerOptions, AutoSaveSnapshot, AutoSaveStatus, SavedDocumentData, formatLastSaveTime, formatStorageSize, getAutoSaveStatusLabel, getAutoSaveStorageSize, isAutoSaveSupported };