superdoc 2.4.0-next.1 → 2.4.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{FindReplaceSurface-Dx0llWqP.cjs → FindReplaceSurface-0p7EtMZh.cjs} +4 -4
- package/dist/chunks/{FindReplaceSurface-BEW-1NhM.es.js → FindReplaceSurface-40AnrqWk.es.js} +5 -4
- package/dist/chunks/{PasswordPromptSurface-BUUVC2ee.cjs → PasswordPromptSurface-31lYPcCK.cjs} +4 -3
- package/dist/chunks/{PasswordPromptSurface-BA6HHqEa.es.js → PasswordPromptSurface-CwYeO3iQ.es.js} +4 -2
- package/dist/chunks/{PdfViewer-CNOtyS-I.es.js → PdfViewer-D8h7LRZy.es.js} +103 -19
- package/dist/chunks/{PdfViewer-qpvzwYMz.cjs → PdfViewer-Dug6yDRC.cjs} +105 -20
- package/dist/chunks/_plugin-vue_export-helper-BOaGB7Aw.es.js +8 -0
- package/dist/chunks/_plugin-vue_export-helper-SDR04tiH.cjs +13 -0
- package/dist/chunks/{blank-docx-DP8RUPW-.cjs → blank-docx-BuFAbRjs.cjs} +2 -0
- package/dist/chunks/{blank-docx-XRX6Ker2.es.js → blank-docx-DzQccOlW.es.js} +2 -0
- package/dist/chunks/constants-B6VBlmKp.es.js +4 -0
- package/dist/chunks/constants-DpXuDx_g.cjs +9 -0
- package/dist/chunks/{create-super-doc-ui-DXdWuTzm.cjs → create-super-doc-ui-BWYOinLD.cjs} +1836 -532
- package/dist/chunks/{create-super-doc-ui-D4kk9AjM.es.js → create-super-doc-ui-D0mNmAHJ.es.js} +2026 -500
- package/dist/chunks/{eventemitter3-DqY4aSMf.cjs → eventemitter3-C_TAnXOl.cjs} +125 -16
- package/dist/chunks/{eventemitter3-Bt2s0X0a.es.js → eventemitter3-DEIiXiH2.es.js} +124 -16
- package/dist/chunks/jszip-BzJ3CyxR.es.js +4717 -0
- package/dist/chunks/jszip-D8mAFF-r.cjs +4758 -0
- package/dist/chunks/{rolldown-runtime-D7PMmH3s.es.js → rolldown-runtime-0pSA04fp.es.js} +12 -3
- package/dist/chunks/{rolldown-runtime-1Y-nnZJ3.cjs → rolldown-runtime-74VwNCz7.cjs} +11 -2
- package/dist/chunks/{uuid-CFp0WGVU.cjs → uuid-BhG0ngwk.cjs} +10 -6
- package/dist/chunks/{uuid-B2Sqk-3p.es.js → uuid-H0Xcmmhy.es.js} +8 -3
- package/dist/collaboration-upgrade-engine.cjs +34 -9
- package/dist/collaboration-upgrade-engine.es.js +30 -5
- package/dist/document-api/src/types/track-changes.types.d.ts +1 -3
- package/dist/public/ui-react.cjs +66 -2
- package/dist/public/ui-react.es.js +66 -1
- package/dist/public/ui.cjs +1 -1
- package/dist/public/ui.es.js +1 -1
- package/dist/style.css +163 -128
- package/dist/style.layered.css +163 -128
- package/dist/superdoc/src/core/types/index.d.ts +125 -5
- package/dist/superdoc/src/helpers/v2-review-mutation-impact.d.ts +1 -0
- package/dist/superdoc/src/helpers/v2-review-mutation-reconciler.d.ts +1 -1
- package/dist/superdoc/src/internal/toolbar/compatibility-catalog.d.ts +1 -1
- package/dist/superdoc/src/public/index.d.cts +8 -0
- package/dist/superdoc/src/public/index.d.ts +4 -0
- package/dist/superdoc/src/public/ui/types.d.ts +25 -1
- package/dist/superdoc/src/stores/comments-store.d.ts +100 -22
- package/dist/superdoc/src/stores/superdoc-store.d.ts +300 -66
- package/dist/superdoc.cjs +6348 -1202
- package/dist/superdoc.es.js +6226 -1079
- package/dist-cdn/style.layered.css +1 -1
- package/dist-cdn/superdoc.min.css +1 -1
- package/dist-cdn/superdoc.min.js +37 -37
- package/package.json +14 -14
- package/dist/chunks/_plugin-vue_export-helper-BTwbGDKw.cjs +0 -11
- package/dist/chunks/_plugin-vue_export-helper-CInC0bKI.es.js +0 -6
- package/dist/chunks/constants-CY3R3_kF.es.js +0 -3
- package/dist/chunks/constants-sbCZ2O_A.cjs +0 -14
- package/dist/chunks/jszip-C8srOKAO.es.js +0 -4650
- package/dist/chunks/jszip-Cs9JBLlJ.cjs +0 -4691
package/dist/chunks/{create-super-doc-ui-D4kk9AjM.es.js → create-super-doc-ui-D0mNmAHJ.es.js}
RENAMED
|
@@ -1,3 +1,161 @@
|
|
|
1
|
+
//#region ../layout-engine/dom-contract/src/class-names.ts
|
|
2
|
+
/**
|
|
3
|
+
* DOM Contract: Class Names
|
|
4
|
+
*
|
|
5
|
+
* CSS class names stamped on rendered document elements by the DOM painter.
|
|
6
|
+
* These names form a public contract read by the painter (emitter) and by
|
|
7
|
+
* editor-side DOM observation code (reader).
|
|
8
|
+
*
|
|
9
|
+
* Changing a value here is a breaking change for both systems.
|
|
10
|
+
*/
|
|
11
|
+
var DOM_CLASS_NAMES = {
|
|
12
|
+
/** Top-level page container element. */
|
|
13
|
+
PAGE: "superdoc-page",
|
|
14
|
+
/** Fragment container (paragraph, table, image block, etc.). */
|
|
15
|
+
FRAGMENT: "superdoc-fragment",
|
|
16
|
+
/** Line container within a fragment. */
|
|
17
|
+
LINE: "superdoc-line",
|
|
18
|
+
/**
|
|
19
|
+
* Inline structured-content (SDT) wrapper.
|
|
20
|
+
*
|
|
21
|
+
* Carries `data-pm-start` / `data-pm-end` for selection highlighting.
|
|
22
|
+
* Should be EXCLUDED from click-to-position mapping — child spans are
|
|
23
|
+
* the character-level targets.
|
|
24
|
+
*/
|
|
25
|
+
INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
|
|
26
|
+
/** Inline structured-content label chrome. */
|
|
27
|
+
INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
|
|
28
|
+
/** Block-level structured-content container. */
|
|
29
|
+
BLOCK_SDT: "superdoc-structured-content-block",
|
|
30
|
+
/** Block-level structured-content label chrome. */
|
|
31
|
+
BLOCK_SDT_LABEL: "superdoc-structured-content__label",
|
|
32
|
+
/** Table fragment container (resize overlay and click-mapping target). */
|
|
33
|
+
TABLE_FRAGMENT: "superdoc-table-fragment",
|
|
34
|
+
/** Document section container. */
|
|
35
|
+
DOCUMENT_SECTION: "superdoc-document-section",
|
|
36
|
+
/**
|
|
37
|
+
* Grouped hover highlight applied to all fragments of the same block SDT.
|
|
38
|
+
* Set by document-runtime hover coordination via event delegation.
|
|
39
|
+
*/
|
|
40
|
+
SDT_GROUP_HOVER: "sdt-group-hover",
|
|
41
|
+
/** Paragraph fragment rendered as a Table of Contents entry. */
|
|
42
|
+
TOC_ENTRY: "superdoc-toc-entry",
|
|
43
|
+
/** TOC analogue of `SDT_GROUP_HOVER`, applied to every fragment sharing a `data-toc-id`. */
|
|
44
|
+
TOC_GROUP_HOVER: "toc-group-hover",
|
|
45
|
+
/** Block-level image fragment (ImageBlock). */
|
|
46
|
+
IMAGE_FRAGMENT: "superdoc-image-fragment",
|
|
47
|
+
/** Inline image element (ImageRun inside a paragraph). */
|
|
48
|
+
INLINE_IMAGE: "superdoc-inline-image",
|
|
49
|
+
/** Wrapper around a paragraph's list marker (bullet glyph or ordered number). */
|
|
50
|
+
LIST_MARKER: "superdoc-list-marker",
|
|
51
|
+
/** Clip wrapper around a cropped inline image. */
|
|
52
|
+
INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
|
|
53
|
+
/** Field annotation outer wrapper. */
|
|
54
|
+
ANNOTATION: "annotation",
|
|
55
|
+
/** Field annotation inner content wrapper. */
|
|
56
|
+
ANNOTATION_CONTENT: "annotation-content",
|
|
57
|
+
/** Hidden caret anchor span appended after field annotation content. */
|
|
58
|
+
ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
|
|
59
|
+
};
|
|
60
|
+
DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../layout-engine/dom-contract/src/data-attrs.ts
|
|
63
|
+
/**
|
|
64
|
+
* DOM Contract: Data Attributes
|
|
65
|
+
*
|
|
66
|
+
* Named constants for `data-*` attributes stamped on rendered DOM elements.
|
|
67
|
+
* These attributes are read by editor-side DOM observers, click-to-position
|
|
68
|
+
* mapping, and bridge compatibility code.
|
|
69
|
+
*
|
|
70
|
+
* Each constant stores the full attribute name (e.g. `"data-pm-start"`) as it
|
|
71
|
+
* appears in `getAttribute()` / `setAttribute()` calls and CSS selectors.
|
|
72
|
+
*
|
|
73
|
+
* The `DATASET_KEYS` mirror provides the camelCase equivalents used with
|
|
74
|
+
* `element.dataset.*` access.
|
|
75
|
+
*
|
|
76
|
+
* Editor-neutral (prep-001) attributes live alongside the legacy `data-pm-*`
|
|
77
|
+
* attributes; both surfaces are emitted so that v1 consumers continue to work
|
|
78
|
+
* unmodified while future editor-neutral consumers (hit-test / range mapping)
|
|
79
|
+
* can address rendered output without consulting ProseMirror positions.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Full attribute names for use with `getAttribute` / `setAttribute` / CSS selectors.
|
|
83
|
+
*/
|
|
84
|
+
var DATA_ATTRS = {
|
|
85
|
+
/** ProseMirror start position of the element's content range. */
|
|
86
|
+
PM_START: "data-pm-start",
|
|
87
|
+
/** ProseMirror end position of the element's content range. */
|
|
88
|
+
PM_END: "data-pm-end",
|
|
89
|
+
/** Layout epoch stamp — incremented on each layout pass. */
|
|
90
|
+
LAYOUT_EPOCH: "data-layout-epoch",
|
|
91
|
+
/** JSON-encoded table boundary metadata for resize overlays. */
|
|
92
|
+
TABLE_BOUNDARIES: "data-table-boundaries",
|
|
93
|
+
/** SDT unique identifier. */
|
|
94
|
+
SDT_ID: "data-sdt-id",
|
|
95
|
+
/** SDT type (fieldAnnotation, structuredContent, documentSection, etc.). */
|
|
96
|
+
SDT_TYPE: "data-sdt-type",
|
|
97
|
+
/** Field annotation field identifier. */
|
|
98
|
+
FIELD_ID: "data-field-id",
|
|
99
|
+
/** Field annotation field type (signer, text, checkbox, etc.). */
|
|
100
|
+
FIELD_TYPE: "data-field-type",
|
|
101
|
+
/** Marks an element as draggable by the editor. */
|
|
102
|
+
DRAGGABLE: "data-draggable",
|
|
103
|
+
/** Display label text for drag toast / accessibility. */
|
|
104
|
+
DISPLAY_LABEL: "data-display-label",
|
|
105
|
+
/** Field annotation variant (text, image, signature, checkbox, html, link). */
|
|
106
|
+
VARIANT: "data-variant",
|
|
107
|
+
/** Element type discriminator (annotation variant, etc.). */
|
|
108
|
+
TYPE: "data-type",
|
|
109
|
+
/** Schema version for the editor-neutral layout boundary attributes. */
|
|
110
|
+
LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
|
|
111
|
+
/** Stable opaque id of the rendered fragment (see `LayoutFragmentId`). */
|
|
112
|
+
LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
|
|
113
|
+
/** Encoded story locator (e.g. `body`, `header:rId4`, `footer:rId7`). */
|
|
114
|
+
LAYOUT_STORY: "data-layout-story",
|
|
115
|
+
/** Source block reference (today: producer's `blockId`). */
|
|
116
|
+
LAYOUT_BLOCK_REF: "data-layout-block-ref"
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Decode the dataset string back into a `LayoutStoryLocator`-shaped object.
|
|
120
|
+
*
|
|
121
|
+
* Used by editor-side DOM observers. Unknown kinds fall back to
|
|
122
|
+
* `{ kind: 'unknown' }` so downstream code can treat the value as a
|
|
123
|
+
* diagnostic, not as a default body.
|
|
124
|
+
*/
|
|
125
|
+
var decodeLayoutStoryDataset = (raw) => {
|
|
126
|
+
if (!raw) return { kind: "unknown" };
|
|
127
|
+
if (raw === "body") return { kind: "body" };
|
|
128
|
+
const idx = raw.indexOf(":");
|
|
129
|
+
const kind = idx === -1 ? raw : raw.slice(0, idx);
|
|
130
|
+
const id = idx === -1 ? void 0 : raw.slice(idx + 1);
|
|
131
|
+
switch (kind) {
|
|
132
|
+
case "body":
|
|
133
|
+
case "header":
|
|
134
|
+
case "footer":
|
|
135
|
+
case "footnote":
|
|
136
|
+
case "endnote":
|
|
137
|
+
case "textbox": return id ? {
|
|
138
|
+
kind,
|
|
139
|
+
id
|
|
140
|
+
} : { kind };
|
|
141
|
+
default: return { kind: "unknown" };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
`${DOM_CLASS_NAMES.BLOCK_SDT}${DATA_ATTRS.SDT_ID}`;
|
|
145
|
+
`${DATA_ATTRS.DRAGGABLE}`;
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region ../layout-engine/contracts/src/direction-context.ts
|
|
148
|
+
/**
|
|
149
|
+
* Read a paragraph's inline base direction from its attributes.
|
|
150
|
+
*
|
|
151
|
+
* Prefers the resolved {@link ParagraphDirectionContext} (SD-2776) when
|
|
152
|
+
* present. Falls back to `paragraphProperties.rightToLeft` for PM-node /
|
|
153
|
+
* editor paths that store direction on the raw OOXML properties rather
|
|
154
|
+
* than the typed direction context.
|
|
155
|
+
*
|
|
156
|
+
* Consumers should call this instead of inspecting attrs ad hoc so the
|
|
157
|
+
* direction source check stays in one place.
|
|
158
|
+
*/
|
|
1
159
|
function getParagraphInlineDirection(attrs) {
|
|
2
160
|
const fromContext = attrs?.directionContext?.inlineDirection;
|
|
3
161
|
if (fromContext != null) return fromContext;
|
|
@@ -5,74 +163,261 @@ function getParagraphInlineDirection(attrs) {
|
|
|
5
163
|
if (ppRtl === true) return "rtl";
|
|
6
164
|
if (ppRtl === false) return "ltr";
|
|
7
165
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"#116329",
|
|
20
|
-
"#7d4e00"
|
|
21
|
-
];
|
|
22
|
-
const authorIdentityKey = (author) => {
|
|
23
|
-
if (!author) return "";
|
|
24
|
-
return `${typeof author.name === "string" ? author.name : ""} ${typeof author.email === "string" ? author.email : ""}`;
|
|
25
|
-
};
|
|
26
|
-
var hashString = (value) => {
|
|
27
|
-
let hash = 2166136261;
|
|
28
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
29
|
-
hash ^= value.charCodeAt(i);
|
|
30
|
-
hash = Math.imul(hash, 16777619);
|
|
31
|
-
}
|
|
32
|
-
return hash >>> 0;
|
|
33
|
-
};
|
|
34
|
-
const fallbackAuthorColor = (author) => {
|
|
35
|
-
return FALLBACK_PALETTE[hashString(authorIdentityKey(author)) % FALLBACK_PALETTE.length];
|
|
36
|
-
};
|
|
37
|
-
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
38
|
-
const composeAuthorColorResolver = (config) => {
|
|
39
|
-
if (!config || config.enabled === false) return void 0;
|
|
40
|
-
const overrides = config.overrides && typeof config.overrides === "object" ? config.overrides : void 0;
|
|
41
|
-
const resolve = typeof config.resolve === "function" ? config.resolve : void 0;
|
|
42
|
-
return (author) => {
|
|
43
|
-
const safeAuthor = author ?? {};
|
|
44
|
-
if (overrides) {
|
|
45
|
-
if (isNonEmptyString(safeAuthor.email) && isNonEmptyString(overrides[safeAuthor.email])) return overrides[safeAuthor.email];
|
|
46
|
-
if (isNonEmptyString(safeAuthor.name) && isNonEmptyString(overrides[safeAuthor.name])) return overrides[safeAuthor.name];
|
|
47
|
-
}
|
|
48
|
-
if (resolve) try {
|
|
49
|
-
const resolved = resolve(safeAuthor);
|
|
50
|
-
if (isNonEmptyString(resolved)) return resolved;
|
|
51
|
-
} catch {}
|
|
52
|
-
return fallbackAuthorColor(safeAuthor);
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
const DRAWING_DIAGNOSTIC_CODES = {
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../layout-engine/contracts/src/drawing-taxonomy.ts
|
|
168
|
+
/**
|
|
169
|
+
* Frozen canonical diagnostic codes for drawing rendering.
|
|
170
|
+
*
|
|
171
|
+
* New extractor/adapter diagnostics MUST use these exact strings. Family-
|
|
172
|
+
* specific codes are required — do not collapse everything to
|
|
173
|
+
* `render.unsupported-inline-ooxml` (plan §4).
|
|
174
|
+
*/
|
|
175
|
+
var DRAWING_DIAGNOSTIC_CODES = {
|
|
176
|
+
/** External image relationship (`r:link` / `TargetMode="External"`). Not fetched. */
|
|
56
177
|
externalImageDeferred: "render.media.external-image-deferred",
|
|
178
|
+
/** Drawing references a relationship id that does not exist on the owner part. */
|
|
57
179
|
missingRelationship: "render.drawing.missing-relationship",
|
|
180
|
+
/** Relationship resolves but the target media part bytes are missing/empty. */
|
|
58
181
|
missingMediaPart: "render.media.missing-part",
|
|
182
|
+
/** Relationship exists but is not an image relationship type. */
|
|
59
183
|
unsupportedRelationshipType: "render.drawing.unsupported-relationship-type",
|
|
184
|
+
/** Media MIME is not in the supported allowlist. */
|
|
60
185
|
unsupportedMime: "render.media.unsupported-mime",
|
|
186
|
+
/** Media part exceeds the host byte-size policy before bytes are materialized. */
|
|
61
187
|
imageTooLarge: "render.media.image-too-large",
|
|
188
|
+
/** SVG content rejected by the SVG safety policy. */
|
|
62
189
|
unsafeSvg: "render.media.unsafe-svg",
|
|
190
|
+
/** Metafile / TIFF (EMF, WMF, TIFF) with no approved conversion path. */
|
|
63
191
|
unsupportedFormat: "render.media.unsupported-format",
|
|
192
|
+
/** OLE / ActiveX / embedded package object. */
|
|
64
193
|
embeddedObjectNotSupported: "render.embedded-object-not-supported",
|
|
194
|
+
/** SmartArt / diagram / unknown external graphic-data object. */
|
|
65
195
|
unsupportedObject: "render.drawing.unsupported-object",
|
|
196
|
+
/** VML structure outside any supported image-like subset. */
|
|
66
197
|
vmlUnsupported: "render.drawing.vml-unsupported",
|
|
198
|
+
/** VML image-like content that could not be promoted to a supported image. */
|
|
67
199
|
vmlImageUnsupported: "render.drawing.vml-image-unsupported",
|
|
200
|
+
/** Custom geometry command the extractor does not implement (e.g. `arcTo`). */
|
|
68
201
|
unsupportedGeometryCommand: "render.drawing.unsupported-geometry-command",
|
|
202
|
+
/** Anchor fields required for honest placement are unsupported. */
|
|
69
203
|
anchorUnsupported: "render.drawing.anchor-unsupported",
|
|
204
|
+
/** Wrap fields required for honest placement are unsupported. */
|
|
70
205
|
wrapUnsupported: "render.drawing.wrap-unsupported",
|
|
206
|
+
/** `mc:AlternateContent` had no supported choice and no usable fallback. */
|
|
71
207
|
altContentNoSupportedChoice: "render.drawing.altcontent-no-supported-choice",
|
|
208
|
+
/** A child of an otherwise-supported group is unsupported. */
|
|
72
209
|
groupChildUnsupported: "render.drawing.group-child-unsupported"
|
|
73
210
|
};
|
|
74
211
|
DRAWING_DIAGNOSTIC_CODES.missingRelationship, DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType, DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType, DRAWING_DIAGNOSTIC_CODES.imageTooLarge, DRAWING_DIAGNOSTIC_CODES.missingMediaPart, DRAWING_DIAGNOSTIC_CODES.unsupportedObject, DRAWING_DIAGNOSTIC_CODES.vmlUnsupported;
|
|
75
|
-
|
|
212
|
+
/**
|
|
213
|
+
* The frozen drawing support taxonomy.
|
|
214
|
+
*
|
|
215
|
+
* Invariants (enforced by `drawing-taxonomy.test.ts`):
|
|
216
|
+
* - every `supported` family has a real contract target (`!== 'none'`);
|
|
217
|
+
* - every `DrawingBlock`-targeted family declares a `drawingKind`;
|
|
218
|
+
* - every `fail-closed` family declares a canonical `diagnostic`;
|
|
219
|
+
* - every canonical diagnostic code is referenced by at least one fail-closed
|
|
220
|
+
* family;
|
|
221
|
+
* - `deferred` families make no support claim (`contract: 'none'`, no
|
|
222
|
+
* diagnostic).
|
|
223
|
+
*/
|
|
224
|
+
var DRAWING_SUPPORT_TAXONOMY = {
|
|
225
|
+
inlineBitmap: {
|
|
226
|
+
family: "inlineBitmap",
|
|
227
|
+
support: "supported",
|
|
228
|
+
description: "wp:inline pic:pic with internal media and browser-safe MIME (PNG/JPEG; WebP/SVG once the host media resolver lands).",
|
|
229
|
+
contract: "ImageRun"
|
|
230
|
+
},
|
|
231
|
+
anchoredBitmap: {
|
|
232
|
+
family: "anchoredBitmap",
|
|
233
|
+
support: "supported",
|
|
234
|
+
description: "wp:anchor bitmap with anchor/wrap expressible by ImageAnchor/ImageWrap. Never silently inlined.",
|
|
235
|
+
contract: "ImageBlock"
|
|
236
|
+
},
|
|
237
|
+
imageChildInGroup: {
|
|
238
|
+
family: "imageChildInGroup",
|
|
239
|
+
support: "supported",
|
|
240
|
+
description: "Bitmap child of a supported DrawingML group/drawing wrapper. Supported only when the parent is.",
|
|
241
|
+
contract: "ImageDrawing"
|
|
242
|
+
},
|
|
243
|
+
vectorShape: {
|
|
244
|
+
family: "vectorShape",
|
|
245
|
+
support: "supported",
|
|
246
|
+
description: "Block/floating DrawingML preset/custom shape with the supported geometry/style subset.",
|
|
247
|
+
contract: "DrawingBlock",
|
|
248
|
+
drawingKind: "vectorShape"
|
|
249
|
+
},
|
|
250
|
+
shapeGroup: {
|
|
251
|
+
family: "shapeGroup",
|
|
252
|
+
support: "supported",
|
|
253
|
+
description: "DrawingML group whose rendered children are supported vector/image children.",
|
|
254
|
+
contract: "DrawingBlock",
|
|
255
|
+
drawingKind: "shapeGroup"
|
|
256
|
+
},
|
|
257
|
+
chart: {
|
|
258
|
+
family: "chart",
|
|
259
|
+
support: "supported",
|
|
260
|
+
description: "DrawingML chart projected into a ChartDrawing with parsed cached chart XML data. Complete Word chart fidelity remains deferred to chartFidelity.",
|
|
261
|
+
contract: "DrawingBlock",
|
|
262
|
+
drawingKind: "chart"
|
|
263
|
+
},
|
|
264
|
+
alternateContent: {
|
|
265
|
+
family: "alternateContent",
|
|
266
|
+
support: "supported",
|
|
267
|
+
description: "mc:AlternateContent traversal/select policy. Selected Choice/Fallback maps to its family contract; unselected branches preserved for save/reopen.",
|
|
268
|
+
contract: "selected-choice"
|
|
269
|
+
},
|
|
270
|
+
externalImage: {
|
|
271
|
+
family: "externalImage",
|
|
272
|
+
support: "fail-closed",
|
|
273
|
+
description: "External image relationship (r:link / TargetMode=\"External\"). Not fetched.",
|
|
274
|
+
contract: "none",
|
|
275
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.externalImageDeferred
|
|
276
|
+
},
|
|
277
|
+
missingRelationship: {
|
|
278
|
+
family: "missingRelationship",
|
|
279
|
+
support: "fail-closed",
|
|
280
|
+
description: "Drawing references a relationship id absent from the owner part .rels.",
|
|
281
|
+
contract: "none",
|
|
282
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.missingRelationship
|
|
283
|
+
},
|
|
284
|
+
missingMediaPart: {
|
|
285
|
+
family: "missingMediaPart",
|
|
286
|
+
support: "fail-closed",
|
|
287
|
+
description: "Relationship resolves but the target media part bytes are missing or empty.",
|
|
288
|
+
contract: "none",
|
|
289
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.missingMediaPart
|
|
290
|
+
},
|
|
291
|
+
wrongRelationshipType: {
|
|
292
|
+
family: "wrongRelationshipType",
|
|
293
|
+
support: "fail-closed",
|
|
294
|
+
description: "Relationship exists but is not an image relationship type.",
|
|
295
|
+
contract: "none",
|
|
296
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType
|
|
297
|
+
},
|
|
298
|
+
unsupportedMime: {
|
|
299
|
+
family: "unsupportedMime",
|
|
300
|
+
support: "fail-closed",
|
|
301
|
+
description: "Media MIME outside the supported allowlist (incl. GIF/BMP/ICO).",
|
|
302
|
+
contract: "none",
|
|
303
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsupportedMime
|
|
304
|
+
},
|
|
305
|
+
oversizedMediaPart: {
|
|
306
|
+
family: "oversizedMediaPart",
|
|
307
|
+
support: "fail-closed",
|
|
308
|
+
description: "Media part exceeds the host byte-size policy before bytes are materialized.",
|
|
309
|
+
contract: "none",
|
|
310
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.imageTooLarge
|
|
311
|
+
},
|
|
312
|
+
unsafeSvg: {
|
|
313
|
+
family: "unsafeSvg",
|
|
314
|
+
support: "fail-closed",
|
|
315
|
+
description: "SVG content rejected by the SVG safety policy.",
|
|
316
|
+
contract: "none",
|
|
317
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsafeSvg
|
|
318
|
+
},
|
|
319
|
+
metafileOrTiff: {
|
|
320
|
+
family: "metafileOrTiff",
|
|
321
|
+
support: "fail-closed",
|
|
322
|
+
description: "EMF/WMF/TIFF and other non-browser-native formats with no approved conversion path.",
|
|
323
|
+
contract: "none",
|
|
324
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsupportedFormat
|
|
325
|
+
},
|
|
326
|
+
embeddedObject: {
|
|
327
|
+
family: "embeddedObject",
|
|
328
|
+
support: "fail-closed",
|
|
329
|
+
description: "OLE / ActiveX / embedded package object.",
|
|
330
|
+
contract: "none",
|
|
331
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.embeddedObjectNotSupported
|
|
332
|
+
},
|
|
333
|
+
smartArtOrDiagram: {
|
|
334
|
+
family: "smartArtOrDiagram",
|
|
335
|
+
support: "fail-closed",
|
|
336
|
+
description: "SmartArt / diagram / unknown external graphic-data object.",
|
|
337
|
+
contract: "none",
|
|
338
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsupportedObject
|
|
339
|
+
},
|
|
340
|
+
vml: {
|
|
341
|
+
family: "vml",
|
|
342
|
+
support: "fail-closed",
|
|
343
|
+
description: "VML structure outside any supported image-like subset (default fail-closed policy).",
|
|
344
|
+
contract: "none",
|
|
345
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.vmlUnsupported
|
|
346
|
+
},
|
|
347
|
+
vmlImageLike: {
|
|
348
|
+
family: "vmlImageLike",
|
|
349
|
+
support: "fail-closed",
|
|
350
|
+
description: "Simple VML image references (v:imagedata) / watermark image metadata. Fail-closed unless a later change promotes a named subset with deterministic fixtures and exact oracles.",
|
|
351
|
+
contract: "none",
|
|
352
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.vmlImageUnsupported
|
|
353
|
+
},
|
|
354
|
+
unsupportedGeometryCommand: {
|
|
355
|
+
family: "unsupportedGeometryCommand",
|
|
356
|
+
support: "fail-closed",
|
|
357
|
+
description: "Custom geometry command the extractor does not implement, e.g. arcTo.",
|
|
358
|
+
contract: "none",
|
|
359
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.unsupportedGeometryCommand
|
|
360
|
+
},
|
|
361
|
+
unsupportedAnchorFields: {
|
|
362
|
+
family: "unsupportedAnchorFields",
|
|
363
|
+
support: "fail-closed",
|
|
364
|
+
description: "Anchor fields required for honest placement are unsupported (char-relative, alignment modes, etc.).",
|
|
365
|
+
contract: "none",
|
|
366
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.anchorUnsupported
|
|
367
|
+
},
|
|
368
|
+
unsupportedWrapFields: {
|
|
369
|
+
family: "unsupportedWrapFields",
|
|
370
|
+
support: "fail-closed",
|
|
371
|
+
description: "Wrap fields required for honest placement are unsupported (arbitrary wrap polygons, etc.).",
|
|
372
|
+
contract: "none",
|
|
373
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.wrapUnsupported
|
|
374
|
+
},
|
|
375
|
+
alternateContentNoSupportedChoice: {
|
|
376
|
+
family: "alternateContentNoSupportedChoice",
|
|
377
|
+
support: "fail-closed",
|
|
378
|
+
description: "mc:AlternateContent with no supported choice and no usable fallback.",
|
|
379
|
+
contract: "none",
|
|
380
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.altContentNoSupportedChoice
|
|
381
|
+
},
|
|
382
|
+
groupChildUnsupported: {
|
|
383
|
+
family: "groupChildUnsupported",
|
|
384
|
+
support: "fail-closed",
|
|
385
|
+
description: "Unsupported child inside an otherwise-supported group. Supported siblings are not dropped.",
|
|
386
|
+
contract: "none",
|
|
387
|
+
diagnostic: DRAWING_DIAGNOSTIC_CODES.groupChildUnsupported
|
|
388
|
+
},
|
|
389
|
+
objectEditing: {
|
|
390
|
+
family: "objectEditing",
|
|
391
|
+
support: "deferred",
|
|
392
|
+
description: "Editing/mutation of anchored/vector objects.",
|
|
393
|
+
contract: "none"
|
|
394
|
+
},
|
|
395
|
+
customWrapPolygon: {
|
|
396
|
+
family: "customWrapPolygon",
|
|
397
|
+
support: "deferred",
|
|
398
|
+
description: "Full Word-compatible text wrapping for arbitrary custom wrap polygons.",
|
|
399
|
+
contract: "none"
|
|
400
|
+
},
|
|
401
|
+
chartFidelity: {
|
|
402
|
+
family: "chartFidelity",
|
|
403
|
+
support: "deferred",
|
|
404
|
+
description: "Complete chart fidelity (a dedicated chart effort owns the parsed subset).",
|
|
405
|
+
contract: "none"
|
|
406
|
+
},
|
|
407
|
+
vmlFidelity: {
|
|
408
|
+
family: "vmlFidelity",
|
|
409
|
+
support: "deferred",
|
|
410
|
+
description: "Complete VML fidelity.",
|
|
411
|
+
contract: "none"
|
|
412
|
+
},
|
|
413
|
+
decorativeAccessibilityUi: {
|
|
414
|
+
family: "decorativeAccessibilityUi",
|
|
415
|
+
support: "deferred",
|
|
416
|
+
description: "Full accessibility UI for decorative image semantics beyond preserving source data.",
|
|
417
|
+
contract: "none"
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
Object.keys(DRAWING_SUPPORT_TAXONOMY);
|
|
76
421
|
Object.freeze([
|
|
77
422
|
"multiple-sections",
|
|
78
423
|
"furniture-page-tokens",
|
|
@@ -84,18 +429,31 @@ Object.freeze([
|
|
|
84
429
|
"tables",
|
|
85
430
|
"furniture-anchored-objects"
|
|
86
431
|
]);
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/helpers/v2-review-mutation-impact.js
|
|
434
|
+
/**
|
|
435
|
+
* Classify the tracked-change identities carried by a v2 mutation event.
|
|
436
|
+
*
|
|
437
|
+
* Receipt entities are the durable invalidation contract between the document
|
|
438
|
+
* kernel and derived review UI. Consumers can refresh `upsertIds` narrowly via
|
|
439
|
+
* `trackChanges.get(...)`, drop `removedIds` immediately, and leave a bounded
|
|
440
|
+
* full-list reconciliation as background verification. History results are
|
|
441
|
+
* different: undo/redo can restore many identities at once, so consumers use
|
|
442
|
+
* one authoritative catalog after render instead of issuing one read per id.
|
|
443
|
+
*/
|
|
87
444
|
function getV2TrackedChangeMutationImpact(event) {
|
|
88
445
|
if (event?.type !== "mutation:committed") return null;
|
|
89
446
|
const payload = event.origin === "history" ? event.result : event.receipt;
|
|
90
447
|
if (!payload || typeof payload !== "object") return null;
|
|
91
448
|
const upsertIds = /* @__PURE__ */ new Set();
|
|
92
449
|
const removedIds = /* @__PURE__ */ new Set();
|
|
450
|
+
/** @type {Array<{ from: string, to: string }>} */
|
|
93
451
|
const remappedPairs = [];
|
|
94
452
|
const collectEntity = (entry, into) => {
|
|
95
453
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return;
|
|
96
454
|
if (typeof entry.entityId === "string" && entry.entityId.length > 0) into.add(entry.entityId);
|
|
97
455
|
};
|
|
98
|
-
const readEntityId
|
|
456
|
+
const readEntityId = (entry) => {
|
|
99
457
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return null;
|
|
100
458
|
return typeof entry.entityId === "string" && entry.entityId.length > 0 ? entry.entityId : null;
|
|
101
459
|
};
|
|
@@ -105,8 +463,8 @@ function getV2TrackedChangeMutationImpact(event) {
|
|
|
105
463
|
if (Array.isArray(payload.removed)) payload.removed.forEach((entry) => collectEntity(entry, removedIds));
|
|
106
464
|
if (Array.isArray(payload.invalidatedRefs)) payload.invalidatedRefs.forEach((entry) => collectEntity(entry, removedIds));
|
|
107
465
|
if (Array.isArray(payload.remappedRefs)) for (const mapping of payload.remappedRefs) {
|
|
108
|
-
const fromId = readEntityId
|
|
109
|
-
const toId = readEntityId
|
|
466
|
+
const fromId = readEntityId(mapping?.from);
|
|
467
|
+
const toId = readEntityId(mapping?.to);
|
|
110
468
|
if (fromId) removedIds.add(fromId);
|
|
111
469
|
if (toId) upsertIds.add(toId);
|
|
112
470
|
if (fromId && toId && fromId !== toId) remappedPairs.push({
|
|
@@ -115,14 +473,23 @@ function getV2TrackedChangeMutationImpact(event) {
|
|
|
115
473
|
});
|
|
116
474
|
}
|
|
117
475
|
for (const id of removedIds) upsertIds.delete(id);
|
|
118
|
-
|
|
476
|
+
const allResolved = readAllResolvedFact(event, payload);
|
|
477
|
+
if (upsertIds.size === 0 && removedIds.size === 0 && !allResolved) return null;
|
|
119
478
|
return {
|
|
120
479
|
upsertIds,
|
|
121
480
|
removedIds,
|
|
122
481
|
remappedPairs,
|
|
123
|
-
reconcileMode: event.origin === "history" ? "authoritative" : "targeted"
|
|
482
|
+
reconcileMode: event.origin === "history" ? "authoritative" : "targeted",
|
|
483
|
+
...allResolved ? { allResolved } : {}
|
|
124
484
|
};
|
|
125
485
|
}
|
|
486
|
+
function readAllResolvedFact(event, receipt) {
|
|
487
|
+
const fact = event?.trackedChangeAllResolved;
|
|
488
|
+
if (event?.origin === "history" || !fact || fact.schemaVersion !== 1 || fact.targetKind !== "all" || fact.decision !== "accept" && fact.decision !== "reject" || fact.remainingLogicalCount !== 0 || typeof fact.catalogRevision !== "string" || !fact.catalogRevision || typeof fact.sourceCoverageRevision !== "string" || !fact.sourceCoverageRevision || !Number.isSafeInteger(fact.logicalTargetCount) || fact.logicalTargetCount <= 0 || !Number.isSafeInteger(fact.physicalCarrierCount) || fact.physicalCarrierCount < fact.logicalTargetCount || typeof fact.txId !== "string" || fact.txId !== receipt.txId || typeof fact.documentEpoch !== "string" || !Number.isSafeInteger(fact.commitSequence) || typeof fact.packagePreviousRevision !== "string" || typeof fact.packageNextRevision !== "string" || fact.packagePreviousRevision === fact.packageNextRevision) return null;
|
|
489
|
+
return fact;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/helpers/v2-review-mutation-reconciler.js
|
|
126
493
|
var normalizeIds = (values) => new Set(Array.from(values ?? []).filter((value) => typeof value === "string" && value.length > 0));
|
|
127
494
|
var itemId = (item) => {
|
|
128
495
|
if (typeof item?.id === "string" && item.id.length > 0) return item.id;
|
|
@@ -131,12 +498,29 @@ var itemId = (item) => {
|
|
|
131
498
|
return null;
|
|
132
499
|
};
|
|
133
500
|
var contextMatches = (left, right) => left?.adapter === right?.adapter && left?.documentId === right?.documentId && left?.editor === right?.editor && left?.reconcileToken === right?.reconcileToken;
|
|
501
|
+
/**
|
|
502
|
+
* Reconcile tracked changes only after their canonical render has made the
|
|
503
|
+
* read model observable. Command receipts use narrow reads; history results
|
|
504
|
+
* use one authoritative catalog because undo/redo can restore hundreds of
|
|
505
|
+
* identities in one transaction. An absent history identity only converges
|
|
506
|
+
* when both catalog and source coverage are explicitly complete.
|
|
507
|
+
*/
|
|
134
508
|
function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onReconciled }) {
|
|
135
509
|
const pendingUpserts = /* @__PURE__ */ new Map();
|
|
510
|
+
let pendingAllResolved = null;
|
|
136
511
|
let sequence = 0;
|
|
137
512
|
let generation = 0;
|
|
138
513
|
let flushInFlight = null;
|
|
139
514
|
let flushRequested = false;
|
|
515
|
+
const settlementWaiters = [];
|
|
516
|
+
const resolveSettlementsThrough = (version, value) => {
|
|
517
|
+
for (let index = settlementWaiters.length - 1; index >= 0; index -= 1) {
|
|
518
|
+
const waiter = settlementWaiters[index];
|
|
519
|
+
if (waiter.generation !== generation || waiter.version > version) continue;
|
|
520
|
+
settlementWaiters.splice(index, 1);
|
|
521
|
+
waiter.resolve(value === true && waiter.requiresAllResolved ? pendingAllResolved == null : value);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
140
524
|
const isCurrent = (capturedGeneration, context) => generation === capturedGeneration && contextMatches(context, getContext());
|
|
141
525
|
const clearResolved = (ids, snapshot) => {
|
|
142
526
|
const clearedIds = /* @__PURE__ */ new Set();
|
|
@@ -150,6 +534,10 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
150
534
|
const readySnapshot = (reconcileMode) => new Map([...pendingUpserts].filter(([, entry]) => entry.ready === true && entry.reconcileMode === reconcileMode));
|
|
151
535
|
const markRenderGatedReady = () => {
|
|
152
536
|
let changed = false;
|
|
537
|
+
if (pendingAllResolved?.gate === "render" && !pendingAllResolved.ready) {
|
|
538
|
+
pendingAllResolved.ready = true;
|
|
539
|
+
changed = true;
|
|
540
|
+
}
|
|
153
541
|
for (const entry of pendingUpserts.values()) {
|
|
154
542
|
if (entry.gate !== "render" || entry.ready) continue;
|
|
155
543
|
entry.ready = true;
|
|
@@ -159,6 +547,10 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
159
547
|
};
|
|
160
548
|
const markEntriesReadyThrough = (maxVersion) => {
|
|
161
549
|
let changed = false;
|
|
550
|
+
if (pendingAllResolved && pendingAllResolved.version <= maxVersion && !pendingAllResolved.ready) {
|
|
551
|
+
pendingAllResolved.ready = true;
|
|
552
|
+
changed = true;
|
|
553
|
+
}
|
|
162
554
|
for (const entry of pendingUpserts.values()) {
|
|
163
555
|
if (entry.version > maxVersion || entry.ready) continue;
|
|
164
556
|
entry.ready = true;
|
|
@@ -180,7 +572,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
180
572
|
reconcileMode: "targeted"
|
|
181
573
|
});
|
|
182
574
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
183
|
-
|
|
575
|
+
const resolvedIds = idsFromResult(result);
|
|
576
|
+
const clearedIds = clearResolved(resolvedIds, snapshot);
|
|
577
|
+
await notifyReconciled(context, clearedIds, result);
|
|
184
578
|
if (new Set([...snapshot].filter(([id, version]) => pendingUpserts.get(id) === version).map(([id]) => id)).size === 0) return;
|
|
185
579
|
const fallback = await hydrate(context, {
|
|
186
580
|
trackedChangesListMode: "interaction-prime",
|
|
@@ -188,7 +582,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
188
582
|
blocking: false
|
|
189
583
|
});
|
|
190
584
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
191
|
-
|
|
585
|
+
const fallbackResolvedIds = idsFromResult(fallback);
|
|
586
|
+
const fallbackClearedIds = clearResolved(fallbackResolvedIds, snapshot);
|
|
587
|
+
await notifyReconciled(context, fallbackClearedIds, fallback);
|
|
192
588
|
};
|
|
193
589
|
const reconcileAuthoritativeSnapshot = async (context, snapshot, capturedGeneration) => {
|
|
194
590
|
const result = await hydrate(context, {
|
|
@@ -209,14 +605,31 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
209
605
|
};
|
|
210
606
|
const flush = () => {
|
|
211
607
|
if (flushInFlight) return flushInFlight;
|
|
608
|
+
const allResolvedSnapshot = pendingAllResolved?.ready ? pendingAllResolved : null;
|
|
212
609
|
const targetedSnapshot = readySnapshot("targeted");
|
|
213
610
|
const authoritativeSnapshot = readySnapshot("authoritative");
|
|
214
|
-
if (targetedSnapshot.size === 0 && authoritativeSnapshot.size === 0) return null;
|
|
611
|
+
if (!allResolvedSnapshot && targetedSnapshot.size === 0 && authoritativeSnapshot.size === 0) return null;
|
|
215
612
|
const context = getContext();
|
|
216
613
|
if (!context?.adapter) return null;
|
|
217
614
|
flushRequested = false;
|
|
218
615
|
const capturedGeneration = generation;
|
|
219
616
|
flushInFlight = Promise.resolve().then(async () => {
|
|
617
|
+
if (allResolvedSnapshot) {
|
|
618
|
+
let result = null;
|
|
619
|
+
try {
|
|
620
|
+
result = await reconcile(context, {
|
|
621
|
+
upsertIds: /* @__PURE__ */ new Set(),
|
|
622
|
+
removedIds: /* @__PURE__ */ new Set(),
|
|
623
|
+
allResolved: allResolvedSnapshot.fact,
|
|
624
|
+
reconcileMode: "targeted"
|
|
625
|
+
});
|
|
626
|
+
} catch {}
|
|
627
|
+
if (!isCurrent(capturedGeneration, context)) return;
|
|
628
|
+
if (result?.ok === true) {
|
|
629
|
+
if (pendingAllResolved === allResolvedSnapshot) pendingAllResolved = null;
|
|
630
|
+
await notifyReconciled(context, /* @__PURE__ */ new Set(), result, true);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
220
633
|
if (authoritativeSnapshot.size > 0) await reconcileAuthoritativeSnapshot(context, authoritativeSnapshot, capturedGeneration).catch(() => void 0);
|
|
221
634
|
if (targetedSnapshot.size > 0) await reconcileTargetedSnapshot(context, targetedSnapshot, capturedGeneration).catch(() => void 0);
|
|
222
635
|
}).catch(() => {}).finally(() => {
|
|
@@ -227,6 +640,15 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
227
640
|
};
|
|
228
641
|
const enqueueWithGate = (impact, gate) => {
|
|
229
642
|
if (!impact) return;
|
|
643
|
+
if (impact.allResolved) {
|
|
644
|
+
pendingUpserts.clear();
|
|
645
|
+
pendingAllResolved = {
|
|
646
|
+
fact: impact.allResolved,
|
|
647
|
+
version: ++sequence,
|
|
648
|
+
gate,
|
|
649
|
+
ready: false
|
|
650
|
+
};
|
|
651
|
+
}
|
|
230
652
|
const reconcileMode = impact.reconcileMode === "authoritative" ? "authoritative" : "targeted";
|
|
231
653
|
const removedIds = normalizeIds(impact.removedIds);
|
|
232
654
|
const upsertIds = normalizeIds(impact.upsertIds);
|
|
@@ -241,7 +663,7 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
241
663
|
else pendingUpserts.delete(id);
|
|
242
664
|
}
|
|
243
665
|
const context = getContext();
|
|
244
|
-
if (removedIds.size > 0 && upsertIds.size === 0 && context?.adapter) try {
|
|
666
|
+
if (removedIds.size > 0 && upsertIds.size === 0 && context?.adapter && !impact.allResolved) try {
|
|
245
667
|
Promise.resolve(reconcile(context, {
|
|
246
668
|
upsertIds: /* @__PURE__ */ new Set(),
|
|
247
669
|
removedIds
|
|
@@ -268,20 +690,34 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
268
690
|
enqueueAfterPaint(impact, waitForPaint) {
|
|
269
691
|
if (!impact || typeof waitForPaint !== "function") {
|
|
270
692
|
enqueue(impact);
|
|
271
|
-
return;
|
|
693
|
+
return Promise.resolve(false);
|
|
272
694
|
}
|
|
273
695
|
enqueueWithGate(impact, "exact-paint");
|
|
274
696
|
const context = getContext();
|
|
275
|
-
if (!context?.adapter) return;
|
|
276
697
|
const capturedGeneration = generation;
|
|
277
698
|
const capturedSequence = sequence;
|
|
278
|
-
|
|
699
|
+
if (!context?.adapter) {
|
|
700
|
+
markEntriesReadyThrough(capturedSequence);
|
|
701
|
+
return Promise.resolve(false);
|
|
702
|
+
}
|
|
703
|
+
const completion = new Promise((resolve) => {
|
|
704
|
+
settlementWaiters.push({
|
|
705
|
+
generation: capturedGeneration,
|
|
706
|
+
version: capturedSequence,
|
|
707
|
+
requiresAllResolved: Boolean(impact.allResolved),
|
|
708
|
+
resolve
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
const impactedIds = /* @__PURE__ */ new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
|
|
279
712
|
const capturedEntries = new Map([...impactedIds].flatMap((id) => {
|
|
280
713
|
const entry = pendingUpserts.get(id);
|
|
281
714
|
return entry ? [[id, entry]] : [];
|
|
282
715
|
}));
|
|
283
|
-
Promise.resolve().then(() => waitForPaint()).then(() => {
|
|
284
|
-
if (!isCurrent(capturedGeneration, context))
|
|
716
|
+
Promise.resolve().then(() => waitForPaint()).then(async () => {
|
|
717
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
718
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
285
721
|
let madeReady = markRenderGatedReady();
|
|
286
722
|
if (markEntriesReadyThrough(capturedSequence)) madeReady = true;
|
|
287
723
|
for (const [id, entry] of capturedEntries) {
|
|
@@ -290,16 +726,35 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
290
726
|
entry.ready = true;
|
|
291
727
|
}
|
|
292
728
|
if (flushInFlight && (madeReady || capturedSequence > 0)) flushRequested = true;
|
|
293
|
-
|
|
729
|
+
const preceding = flushInFlight;
|
|
730
|
+
const pending = flush();
|
|
731
|
+
if (pending) await pending;
|
|
732
|
+
if (preceding) {
|
|
733
|
+
const followUp = flushInFlight;
|
|
734
|
+
if (followUp && followUp !== preceding) await followUp;
|
|
735
|
+
}
|
|
736
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
737
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
resolveSettlementsThrough(capturedSequence, true);
|
|
294
741
|
}).catch(() => {
|
|
295
|
-
if (!isCurrent(capturedGeneration, context))
|
|
742
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
743
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
296
746
|
for (const [id, entry] of capturedEntries) if (pendingUpserts.get(id) === entry) entry.gate = "render";
|
|
747
|
+
if (pendingAllResolved?.gate === "exact-paint") pendingAllResolved.gate = "render";
|
|
748
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
297
749
|
});
|
|
750
|
+
return completion;
|
|
298
751
|
},
|
|
299
752
|
onRender,
|
|
300
753
|
reset() {
|
|
754
|
+
for (const waiter of settlementWaiters.splice(0)) waiter.resolve(false);
|
|
301
755
|
generation += 1;
|
|
302
756
|
pendingUpserts.clear();
|
|
757
|
+
pendingAllResolved = null;
|
|
303
758
|
flushRequested = false;
|
|
304
759
|
},
|
|
305
760
|
getPendingIds() {
|
|
@@ -307,7 +762,19 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
307
762
|
}
|
|
308
763
|
};
|
|
309
764
|
}
|
|
310
|
-
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/helpers/v2-typing-mutation-event.js
|
|
767
|
+
/**
|
|
768
|
+
* Command kinds whose committed mutations belong to the sustained typing
|
|
769
|
+
* cadence class (insert / replace / plain paste / Backspace / Delete).
|
|
770
|
+
*
|
|
771
|
+
* The SuperDoc shell debounces per-keystroke review-row hydration for these
|
|
772
|
+
* kinds instead of refreshing comments + tracked changes on every commit —
|
|
773
|
+
* hydrating immediately per keystroke was a measured worker-read storm during
|
|
774
|
+
* typing and backspace bursts. The `text.*` aliases cover integrations that
|
|
775
|
+
* forward Document API style command ids rather than editable-input kinds.
|
|
776
|
+
*/
|
|
777
|
+
var V2_TYPING_COMMAND_KINDS = /* @__PURE__ */ new Set([
|
|
311
778
|
"insert-text",
|
|
312
779
|
"replace-text",
|
|
313
780
|
"plain-text-paste",
|
|
@@ -329,33 +796,158 @@ var V2_TYPING_COMMAND_KINDS = new Set([
|
|
|
329
796
|
"backspace:list-remove",
|
|
330
797
|
"backspace:list-outdent"
|
|
331
798
|
]);
|
|
332
|
-
|
|
799
|
+
/**
|
|
800
|
+
* True when a forwarded v2 host event is a committed editable-input mutation
|
|
801
|
+
* of the sustained-typing class, i.e. review-row hydration for it should be
|
|
802
|
+
* debounced rather than fired immediately.
|
|
803
|
+
*
|
|
804
|
+
* Events without a forwarded `editableCommandKind` (programmatic Document API
|
|
805
|
+
* mutations, history commits, unclassified structural commands) return false
|
|
806
|
+
* so they keep hydrating immediately.
|
|
807
|
+
*
|
|
808
|
+
* @param {{ type?: string, origin?: string, editableCommandKind?: string } | null | undefined} event
|
|
809
|
+
* @returns {boolean}
|
|
810
|
+
*/
|
|
811
|
+
var isV2EditableTextMutationEvent = (event) => {
|
|
333
812
|
if (event?.type !== "mutation:committed" || event.origin !== "command") return false;
|
|
334
813
|
return V2_TYPING_COMMAND_KINDS.has(event.editableCommandKind);
|
|
335
814
|
};
|
|
336
|
-
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/public/ui/reasons.ts
|
|
817
|
+
/**
|
|
818
|
+
* Stable public reason taxonomy for the v2-native `superdoc/ui` controller.
|
|
819
|
+
*
|
|
820
|
+
* These strings are the canonical, stable vocabulary the controller uses to
|
|
821
|
+
* explain WHY a command or workflow handle is disabled, unsupported, deferred,
|
|
822
|
+
* or otherwise fails closed. They are part of the public custom-UI contract:
|
|
823
|
+
* consumer toolbars/panels can branch on them and migration docs reference
|
|
824
|
+
* them. Once shipped, a value must not be repurposed — add a new member rather
|
|
825
|
+
* than changing the meaning of an existing one.
|
|
826
|
+
*
|
|
827
|
+
* The reasons distinguish the failure-mode families a custom UI must tell
|
|
828
|
+
* apart (see `plans/v2-custom-ui-toolbar-parity-plan-set.md`, Workstream 2):
|
|
829
|
+
*
|
|
830
|
+
* - command unsupported by v2 → `command-unsupported`
|
|
831
|
+
* - reserved recognized/deferred command → `command-deferred`
|
|
832
|
+
* - editor / Document API not ready yet → `not-ready`, `document-api-unavailable`
|
|
833
|
+
* - blocked by viewing / read-only state → `document-readonly`
|
|
834
|
+
* - blocked by missing selection/context → `selection-required`,
|
|
835
|
+
* `range-selection-required`, `context-unavailable`, `target-unresolved`,
|
|
836
|
+
* `target-not-visible`, `geometry-unavailable`
|
|
837
|
+
* - operation / host capability missing → `operation-unavailable`,
|
|
838
|
+
* `host-capability-unavailable`, `bulk-decisions-disabled`
|
|
839
|
+
*
|
|
840
|
+
* Geometry results (`viewport.getRect`, `metadata.getRect`) carry a separate,
|
|
841
|
+
* geometry-local `reason` vocabulary (`not-mounted`, `unresolved`,
|
|
842
|
+
* `invalid-target`, `not-ready`, `unavailable`); see `ViewportRectResult`.
|
|
843
|
+
* That vocabulary predates this taxonomy and is intentionally narrower.
|
|
844
|
+
*/
|
|
845
|
+
var SUPERDOC_UI_REASONS = {
|
|
846
|
+
/** The active editor is not mounted / ready yet. */
|
|
337
847
|
notReady: "not-ready",
|
|
848
|
+
/** The editor is ready but its Document API facade is not available or has not been published yet. */
|
|
338
849
|
documentApiUnavailable: "document-api-unavailable",
|
|
850
|
+
/** The document is in viewing / read-only mode, so mutating commands are blocked. */
|
|
339
851
|
documentReadonly: "document-readonly",
|
|
852
|
+
/** A selection is required and none is active. */
|
|
340
853
|
selectionRequired: "selection-required",
|
|
854
|
+
/** A non-empty (range) selection is required and the current selection is collapsed/empty. */
|
|
341
855
|
rangeSelectionRequired: "range-selection-required",
|
|
856
|
+
/** Required pointer / entity / document context could not be resolved. */
|
|
342
857
|
contextUnavailable: "context-unavailable",
|
|
858
|
+
/** Painted geometry for the target could not be resolved. */
|
|
343
859
|
geometryUnavailable: "geometry-unavailable",
|
|
860
|
+
/** A navigation / geometry target could not be resolved to a live document address. */
|
|
344
861
|
targetUnresolved: "target-unresolved",
|
|
862
|
+
/** A navigation target was resolved but could not be brought into the visible viewport. */
|
|
345
863
|
targetNotVisible: "target-not-visible",
|
|
864
|
+
/** The command is not supported by v2 at all. */
|
|
346
865
|
commandUnsupported: "command-unsupported",
|
|
866
|
+
/** Reserved migration reason for a recognized command that is intentionally deferred. */
|
|
347
867
|
commandDeferred: "command-deferred",
|
|
868
|
+
/**
|
|
869
|
+
* The command is a real v2 operation, but its required document context
|
|
870
|
+
* (the current table / row / column / cell) cannot be resolved from any
|
|
871
|
+
* public custom-UI state: `selection.current` exposes only text block ids,
|
|
872
|
+
* and no public operation resolves the table ancestry of a block. This is a
|
|
873
|
+
* precise, named context-facade gap — distinct from `command-deferred` — for
|
|
874
|
+
* the table cell-context command family.
|
|
875
|
+
*/
|
|
348
876
|
tableContextUnavailable: "table-context-unavailable",
|
|
877
|
+
/** The backing Document API / SuperDoc operation is not present on this host. */
|
|
349
878
|
operationUnavailable: "operation-unavailable",
|
|
879
|
+
/** Bulk accept/reject decisions are disabled unless the host opts in. */
|
|
350
880
|
bulkDecisionsDisabled: "bulk-decisions-disabled",
|
|
881
|
+
/** A host-owned capability (geometry, hit-test, navigation) is unavailable. */
|
|
351
882
|
hostCapabilityUnavailable: "host-capability-unavailable",
|
|
883
|
+
/** Undo / redo cannot run because the host history stack has no matching entry. */
|
|
352
884
|
historyEmpty: "history-empty",
|
|
885
|
+
/**
|
|
886
|
+
* The shared search/find surface is unavailable because the host does not
|
|
887
|
+
* expose a search facade (e.g. a pre-ready editor, a worker-backed v2 host,
|
|
888
|
+
* or a build without the search substrate). Search reads return empty and
|
|
889
|
+
* search actions fail closed rather than fabricating matches.
|
|
890
|
+
*/
|
|
353
891
|
searchUnavailable: "search-unavailable",
|
|
892
|
+
/** The search pattern is an invalid or unsafe regular expression. */
|
|
354
893
|
searchInvalidPattern: "search-invalid-pattern",
|
|
894
|
+
/**
|
|
895
|
+
* Find-and-replace is intentionally out of the first v2 search parity tranche.
|
|
896
|
+
* The shared search surface implements query / navigation only; replace and
|
|
897
|
+
* replace-all fail closed with this reason until replace ships. This is an
|
|
898
|
+
* explicit product posture, not a missing-capability accident.
|
|
899
|
+
*/
|
|
355
900
|
replaceUnsupported: "replace-unsupported",
|
|
901
|
+
/**
|
|
902
|
+
* The selection overlaps a content control (SDT) whose lock mode
|
|
903
|
+
* (`contentLocked` / `sdtContentLocked`) forbids styling its content — Word
|
|
904
|
+
* parity (SD-3274): the toolbar must not leave styling controls clickable
|
|
905
|
+
* when the style change cannot apply. Alignment / paragraph-level commands
|
|
906
|
+
* are never gated by this reason.
|
|
907
|
+
*/
|
|
356
908
|
contentControlLocked: "content-control-locked"
|
|
357
909
|
};
|
|
358
|
-
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/public/ui/commands.ts
|
|
912
|
+
/**
|
|
913
|
+
* Built-in command catalog for the v2-native UI controller.
|
|
914
|
+
*
|
|
915
|
+
* This module is the SINGLE SOURCE OF TRUTH for the controller's built-in
|
|
916
|
+
* command ids, their aliases, how each one routes (public Document API
|
|
917
|
+
* operation, public SuperDoc-instance method, tracked-change decision, or no
|
|
918
|
+
* supported route yet), whether it mutates the document, how v1-style payloads
|
|
919
|
+
* are normalized, and the stable fail-closed reason a recognized-but-unrouted
|
|
920
|
+
* command reports. `create-super-doc-ui.ts` reads these descriptors; it no
|
|
921
|
+
* longer carries an ad hoc route map.
|
|
922
|
+
*
|
|
923
|
+
* Posture (migration-only): every v1 headless-toolbar command id is present
|
|
924
|
+
* here with an explicit disposition —
|
|
925
|
+
*
|
|
926
|
+
* - `routed` → backed by a public surface today (Document API operation,
|
|
927
|
+
* SuperDoc-instance method, or tracked-change decision) and
|
|
928
|
+
* browser-proven through `ui.commands.execute`;
|
|
929
|
+
* - `context-gap` → a real v2 operation whose required document context (the
|
|
930
|
+
* current table / row / column / cell) cannot be resolved
|
|
931
|
+
* from public custom-UI state; reports the precise, named
|
|
932
|
+
* `table-context-unavailable` reason and fails closed;
|
|
933
|
+
* - `unsupported` → no clear public v2 equivalent (product decision) or an
|
|
934
|
+
* unknown id; reports `command-unsupported` and fails closed.
|
|
935
|
+
*
|
|
936
|
+
* No routeable v1 toolbar command remains `command-deferred` (that disposition
|
|
937
|
+
* stays in the type for migration scaffolding but is unused by the catalog).
|
|
938
|
+
*
|
|
939
|
+
* No descriptor imports a v1 editor internal or a private v2 runtime package,
|
|
940
|
+
* and no routed command mutates while the document is read-only / viewing.
|
|
941
|
+
*
|
|
942
|
+
* See `docs/architecture/custom-ui-command-matrix.md` for the per-id matrix.
|
|
943
|
+
*/
|
|
944
|
+
/**
|
|
945
|
+
* The 14 canonical v2-native command ids. These are the historically stable
|
|
946
|
+
* `superdoc/ui` built-ins; the broader catalog below adds v1 headless-toolbar
|
|
947
|
+
* coverage. Kept as a named export because it is part of the public `superdoc/ui`
|
|
948
|
+
* surface (`verify-public-facade-emit.cjs`, consumer typechecks).
|
|
949
|
+
*/
|
|
950
|
+
var BUILT_IN_COMMAND_IDS = {
|
|
359
951
|
bold: "bold",
|
|
360
952
|
italic: "italic",
|
|
361
953
|
underline: "underline",
|
|
@@ -373,6 +965,7 @@ const BUILT_IN_COMMAND_IDS = {
|
|
|
373
965
|
bulletList: "bullet-list",
|
|
374
966
|
numberedList: "numbered-list"
|
|
375
967
|
};
|
|
968
|
+
/** Strip a trailing `pt` unit from v1-style font sizes (`"12pt"` → `"12"`). */
|
|
376
969
|
function normalizeFontSizePayload(payload) {
|
|
377
970
|
const strip = (value) => typeof value === "string" ? value.replace(/\s*pt$/i, "").trim() : value;
|
|
378
971
|
if (typeof payload === "string") return strip(payload);
|
|
@@ -385,6 +978,12 @@ function normalizeFontSizePayload(payload) {
|
|
|
385
978
|
}
|
|
386
979
|
return payload;
|
|
387
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Normalize v1-style zoom payloads to the percentage `SuperDoc.setZoom`
|
|
983
|
+
* expects (`100 === 100%`). Accepts `150`, `"150%"`, `"1.5"`, or `1.5`.
|
|
984
|
+
* Percentages pass through as numbers; legacy fraction-style values in the
|
|
985
|
+
* `0..5` range are converted to percentages (`1.5` → `150`).
|
|
986
|
+
*/
|
|
388
987
|
function normalizeZoomPayload(payload) {
|
|
389
988
|
const normalizeNumber = (value, percentLiteral) => {
|
|
390
989
|
if (!percentLiteral && value > 0 && value <= 5) return value * 100;
|
|
@@ -399,6 +998,12 @@ function normalizeZoomPayload(payload) {
|
|
|
399
998
|
if (typeof payload === "number" && Number.isFinite(payload)) return normalizeNumber(payload, false);
|
|
400
999
|
return payload;
|
|
401
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Normalize a measurement-unit payload to the `'in'` / `'cm'` form
|
|
1003
|
+
* `SuperDoc.setMeasurementUnit` expects. Accepts the canonical `'in'` / `'cm'`,
|
|
1004
|
+
* a `{ value }` wrapper, or common long forms (`"inches"`, `"centimeters"`).
|
|
1005
|
+
* An unrecognized value passes through so the instance method can reject it.
|
|
1006
|
+
*/
|
|
402
1007
|
function normalizeMeasurementUnitPayload(payload) {
|
|
403
1008
|
const raw = payload && typeof payload === "object" && "value" in payload ? payload.value : payload;
|
|
404
1009
|
if (raw === "in" || raw === "cm") return raw;
|
|
@@ -409,6 +1014,12 @@ function normalizeMeasurementUnitPayload(payload) {
|
|
|
409
1014
|
}
|
|
410
1015
|
return raw;
|
|
411
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Normalize a v1-style color payload to the `#RRGGBB` form the inline
|
|
1019
|
+
* `format.color` / `format.highlight` operations expect. Accepts `"#RRGGBB"`,
|
|
1020
|
+
* `"RRGGBB"`, or `{ value }`. `null` (clear) passes through unchanged; an
|
|
1021
|
+
* unrecognized value is returned as-is so the operation can reject it.
|
|
1022
|
+
*/
|
|
412
1023
|
function normalizeColorPayload(payload) {
|
|
413
1024
|
const coerce = (value) => {
|
|
414
1025
|
if (value === null) return null;
|
|
@@ -428,6 +1039,7 @@ function normalizeColorPayload(payload) {
|
|
|
428
1039
|
}
|
|
429
1040
|
return coerce(payload);
|
|
430
1041
|
}
|
|
1042
|
+
/** Unwrap a `{ value }` / `{ alignment } ` / `{ styleId }` wrapper to its scalar. */
|
|
431
1043
|
function unwrapScalar(payload, keys) {
|
|
432
1044
|
if (payload && typeof payload === "object") {
|
|
433
1045
|
const record = payload;
|
|
@@ -435,6 +1047,7 @@ function unwrapScalar(payload, keys) {
|
|
|
435
1047
|
}
|
|
436
1048
|
return payload;
|
|
437
1049
|
}
|
|
1050
|
+
/** Normalize a v1 alignment payload (`"left"`, `{ value }`, `"justified"`) to a `ParagraphAlignment`. */
|
|
438
1051
|
function normalizeAlignmentPayload(payload) {
|
|
439
1052
|
const raw = unwrapScalar(payload, ["alignment", "value"]);
|
|
440
1053
|
if (typeof raw !== "string") return raw;
|
|
@@ -442,6 +1055,11 @@ function normalizeAlignmentPayload(payload) {
|
|
|
442
1055
|
if (v === "justified" || v === "both") return "justify";
|
|
443
1056
|
return v;
|
|
444
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Normalize a v1 line-height payload to OOXML auto line spacing (240ths of a
|
|
1060
|
+
* line: 240 = single, 360 = 1.5×, 480 = double). A multiplier in the `0..10`
|
|
1061
|
+
* range is converted (`1.5` → `360`); a larger raw value passes through.
|
|
1062
|
+
*/
|
|
445
1063
|
function normalizeLineHeightPayload(payload) {
|
|
446
1064
|
const raw = unwrapScalar(payload, [
|
|
447
1065
|
"line",
|
|
@@ -452,6 +1070,7 @@ function normalizeLineHeightPayload(payload) {
|
|
|
452
1070
|
if (!Number.isFinite(num) || num <= 0) return raw;
|
|
453
1071
|
return num <= 10 ? Math.round(num * 240) : Math.round(num);
|
|
454
1072
|
}
|
|
1073
|
+
/** Preserve semantic style intent; normalize legacy linked-style payloads to a concrete ID. */
|
|
455
1074
|
function normalizeStyleIdPayload(payload) {
|
|
456
1075
|
if (payload && typeof payload === "object" && "role" in payload) return payload;
|
|
457
1076
|
const raw = unwrapScalar(payload, [
|
|
@@ -461,6 +1080,7 @@ function normalizeStyleIdPayload(payload) {
|
|
|
461
1080
|
]);
|
|
462
1081
|
return typeof raw === "string" ? raw.trim() : raw;
|
|
463
1082
|
}
|
|
1083
|
+
/** Resolve a document-mode payload (`"viewing"` or `{ mode }`) to the mode string. */
|
|
464
1084
|
function normalizeDocumentModePayload(payload) {
|
|
465
1085
|
if (typeof payload === "string") return payload;
|
|
466
1086
|
if (payload && typeof payload === "object") {
|
|
@@ -471,7 +1091,14 @@ function normalizeDocumentModePayload(payload) {
|
|
|
471
1091
|
}
|
|
472
1092
|
var UNSUPPORTED = SUPERDOC_UI_REASONS.commandUnsupported;
|
|
473
1093
|
var TABLE_CONTEXT = SUPERDOC_UI_REASONS.tableContextUnavailable;
|
|
474
|
-
|
|
1094
|
+
/**
|
|
1095
|
+
* The built-in command catalog. Order is documentation-only. Every v1
|
|
1096
|
+
* headless-toolbar id appears here with an explicit disposition; the 14
|
|
1097
|
+
* canonical v2 ids route today, the kebab track-change selection ids and the
|
|
1098
|
+
* zoom / document-mode ids are newly routed in this unit, and the remaining v1
|
|
1099
|
+
* workflows are deferred or marked product-unsupported with a stable reason.
|
|
1100
|
+
*/
|
|
1101
|
+
var COMMAND_CATALOG = [
|
|
475
1102
|
{
|
|
476
1103
|
id: "bold",
|
|
477
1104
|
family: "inline marks",
|
|
@@ -991,10 +1618,23 @@ const COMMAND_CATALOG = [
|
|
|
991
1618
|
}
|
|
992
1619
|
];
|
|
993
1620
|
var CATALOG_BY_ID = new Map(COMMAND_CATALOG.map((descriptor) => [descriptor.id, descriptor]));
|
|
994
|
-
|
|
1621
|
+
/** All built-in command ids known to the controller (routed + deferred + unsupported). */
|
|
1622
|
+
var ALL_BUILT_IN_COMMAND_IDS = COMMAND_CATALOG.map((d) => d.id);
|
|
1623
|
+
/** Resolve a command descriptor by id, or `null` when the id is unknown. */
|
|
995
1624
|
function getCommandDescriptor(id) {
|
|
996
1625
|
return CATALOG_BY_ID.get(id) ?? null;
|
|
997
1626
|
}
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/public/ui/equality.ts
|
|
1629
|
+
/**
|
|
1630
|
+
* Shallow equality used by the controller to suppress redundant slice
|
|
1631
|
+
* notifications. v2-native, dependency-free.
|
|
1632
|
+
*/
|
|
1633
|
+
/**
|
|
1634
|
+
* Returns `true` when `a` and `b` are the same reference, or are objects with
|
|
1635
|
+
* the same own-enumerable keys and `Object.is`-equal values one level deep.
|
|
1636
|
+
* Arrays compare by index. Primitives compare with `Object.is`.
|
|
1637
|
+
*/
|
|
998
1638
|
function shallowEqual(a, b) {
|
|
999
1639
|
if (Object.is(a, b)) return true;
|
|
1000
1640
|
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
@@ -1008,6 +1648,16 @@ function shallowEqual(a, b) {
|
|
|
1008
1648
|
}
|
|
1009
1649
|
return true;
|
|
1010
1650
|
}
|
|
1651
|
+
//#endregion
|
|
1652
|
+
//#region src/public/ui/format-painter-helpers.js
|
|
1653
|
+
/**
|
|
1654
|
+
* Maps SDRunProps field names to InlineRunPatch field names.
|
|
1655
|
+
* SDRunProps (read-side) and InlineRunPatch (write-side) use different keys
|
|
1656
|
+
* for the same properties; direct assignment between them is forbidden.
|
|
1657
|
+
*
|
|
1658
|
+
* @param {Record<string, unknown>} props - SDRunProps object from doc.getNodeById
|
|
1659
|
+
* @returns {Record<string, unknown>} InlineRunPatch ready for doc.format.apply
|
|
1660
|
+
*/
|
|
1011
1661
|
function sdRunPropsToInlineRunPatch(props) {
|
|
1012
1662
|
if (!props || typeof props !== "object") return {};
|
|
1013
1663
|
const patch = {};
|
|
@@ -1098,6 +1748,16 @@ function projectBorderToInlinePatch(border) {
|
|
|
1098
1748
|
if (color != null) patch.color = color;
|
|
1099
1749
|
return Object.keys(patch).length > 0 ? patch : void 0;
|
|
1100
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Serializes a selection to a stable string key used to detect whether
|
|
1753
|
+
* the target selection is the same as the source (skip apply in that case).
|
|
1754
|
+
*
|
|
1755
|
+
* Falls back to the plain text target when the host has not populated an
|
|
1756
|
+
* explicit `selectionTarget` yet.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {{ selectionTarget?: unknown; target?: unknown } | null | undefined} selection
|
|
1759
|
+
* @returns {string}
|
|
1760
|
+
*/
|
|
1101
1761
|
function selectionKey(selection) {
|
|
1102
1762
|
const target = selection?.selectionTarget ?? selection?.target;
|
|
1103
1763
|
if (!selection || !target) return "";
|
|
@@ -1107,9 +1767,21 @@ function selectionKey(selection) {
|
|
|
1107
1767
|
return String(target);
|
|
1108
1768
|
}
|
|
1109
1769
|
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/public/ui/entity-at.ts
|
|
1110
1772
|
function parseCommaSeparatedIds(value) {
|
|
1111
1773
|
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
1112
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Collect every public-id candidate a painted run can carry for a tracked
|
|
1777
|
+
* change, deduped, in priority order: the canonical
|
|
1778
|
+
* `data-track-change-preferred-target-id` first (when the painter stamps one),
|
|
1779
|
+
* then the `data-track-change-ids` list, then the singular
|
|
1780
|
+
* `data-track-change-id`. The caller validates these against the live
|
|
1781
|
+
* tracked-changes slice and keeps whichever is the public id, so it does not
|
|
1782
|
+
* matter which attribute carries it — an imported/raw alias on one attribute
|
|
1783
|
+
* never hides the public id on another (a union, not a prefer-one-attribute).
|
|
1784
|
+
*/
|
|
1113
1785
|
function getTrackChangeIds(node) {
|
|
1114
1786
|
const out = [];
|
|
1115
1787
|
const add = (id) => {
|
|
@@ -1121,6 +1793,21 @@ function getTrackChangeIds(node) {
|
|
|
1121
1793
|
add(node.getAttribute("data-track-change-id"));
|
|
1122
1794
|
return out;
|
|
1123
1795
|
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Read painted entities off `el` and every ancestor up to the document root, or
|
|
1798
|
+
* up to and INCLUDING `stopAt` when given. Innermost-first ordering: a tracked
|
|
1799
|
+
* change inside a comment highlight returns `[{ trackedChange }, { comment }]`,
|
|
1800
|
+
* matching what a switch on `hits[0]` expects when picking the most specific
|
|
1801
|
+
* entity.
|
|
1802
|
+
*
|
|
1803
|
+
* `stopAt` bounds the walk to the visible host: its own attributes are read, but
|
|
1804
|
+
* the walk never climbs into an app wrapper ABOVE it, so wrapper data-* cannot
|
|
1805
|
+
* leak into the hits. Omitting it keeps the original walk-to-document-root.
|
|
1806
|
+
*
|
|
1807
|
+
* Returns `[]` for null / non-Element starts. Uses duck-typed `getAttribute`
|
|
1808
|
+
* access so it works under any DOM implementation (happy-dom, jsdom, real
|
|
1809
|
+
* browser) without an `instanceof` check that could fail across realms.
|
|
1810
|
+
*/
|
|
1124
1811
|
function collectEntityHitsFromChain(start, stopAt) {
|
|
1125
1812
|
if (!start || typeof start.getAttribute !== "function") return [];
|
|
1126
1813
|
const hits = [];
|
|
@@ -1182,67 +1869,8 @@ function collectEntityHitsFromChain(start, stopAt) {
|
|
|
1182
1869
|
}
|
|
1183
1870
|
return hits;
|
|
1184
1871
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
FRAGMENT: "superdoc-fragment",
|
|
1188
|
-
LINE: "superdoc-line",
|
|
1189
|
-
INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
|
|
1190
|
-
INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
|
|
1191
|
-
BLOCK_SDT: "superdoc-structured-content-block",
|
|
1192
|
-
BLOCK_SDT_LABEL: "superdoc-structured-content__label",
|
|
1193
|
-
TABLE_FRAGMENT: "superdoc-table-fragment",
|
|
1194
|
-
DOCUMENT_SECTION: "superdoc-document-section",
|
|
1195
|
-
SDT_GROUP_HOVER: "sdt-group-hover",
|
|
1196
|
-
TOC_ENTRY: "superdoc-toc-entry",
|
|
1197
|
-
TOC_GROUP_HOVER: "toc-group-hover",
|
|
1198
|
-
IMAGE_FRAGMENT: "superdoc-image-fragment",
|
|
1199
|
-
INLINE_IMAGE: "superdoc-inline-image",
|
|
1200
|
-
LIST_MARKER: "superdoc-list-marker",
|
|
1201
|
-
INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
|
|
1202
|
-
ANNOTATION: "annotation",
|
|
1203
|
-
ANNOTATION_CONTENT: "annotation-content",
|
|
1204
|
-
ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
|
|
1205
|
-
};
|
|
1206
|
-
DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL;
|
|
1207
|
-
const DATA_ATTRS = {
|
|
1208
|
-
PM_START: "data-pm-start",
|
|
1209
|
-
PM_END: "data-pm-end",
|
|
1210
|
-
LAYOUT_EPOCH: "data-layout-epoch",
|
|
1211
|
-
TABLE_BOUNDARIES: "data-table-boundaries",
|
|
1212
|
-
SDT_ID: "data-sdt-id",
|
|
1213
|
-
SDT_TYPE: "data-sdt-type",
|
|
1214
|
-
FIELD_ID: "data-field-id",
|
|
1215
|
-
FIELD_TYPE: "data-field-type",
|
|
1216
|
-
DRAGGABLE: "data-draggable",
|
|
1217
|
-
DISPLAY_LABEL: "data-display-label",
|
|
1218
|
-
VARIANT: "data-variant",
|
|
1219
|
-
TYPE: "data-type",
|
|
1220
|
-
LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
|
|
1221
|
-
LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
|
|
1222
|
-
LAYOUT_STORY: "data-layout-story",
|
|
1223
|
-
LAYOUT_BLOCK_REF: "data-layout-block-ref"
|
|
1224
|
-
};
|
|
1225
|
-
const decodeLayoutStoryDataset = (raw) => {
|
|
1226
|
-
if (!raw) return { kind: "unknown" };
|
|
1227
|
-
if (raw === "body") return { kind: "body" };
|
|
1228
|
-
const idx = raw.indexOf(":");
|
|
1229
|
-
const kind = idx === -1 ? raw : raw.slice(0, idx);
|
|
1230
|
-
const id = idx === -1 ? void 0 : raw.slice(idx + 1);
|
|
1231
|
-
switch (kind) {
|
|
1232
|
-
case "body":
|
|
1233
|
-
case "header":
|
|
1234
|
-
case "footer":
|
|
1235
|
-
case "footnote":
|
|
1236
|
-
case "endnote":
|
|
1237
|
-
case "textbox": return id ? {
|
|
1238
|
-
kind,
|
|
1239
|
-
id
|
|
1240
|
-
} : { kind };
|
|
1241
|
-
default: return { kind: "unknown" };
|
|
1242
|
-
}
|
|
1243
|
-
};
|
|
1244
|
-
`${DOM_CLASS_NAMES.BLOCK_SDT}${DATA_ATTRS.SDT_ID}`;
|
|
1245
|
-
`${DATA_ATTRS.DRAGGABLE}`;
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region ../document-api/src/format/inline-run-patch.ts
|
|
1246
1874
|
var schemaBooleanOrNull = () => ({ oneOf: [{ type: "boolean" }, { type: "null" }] });
|
|
1247
1875
|
var schemaStringOrNull = () => ({ oneOf: [{
|
|
1248
1876
|
type: "string",
|
|
@@ -1334,7 +1962,7 @@ var runAttribute = (key, type, ooxmlElement, schema, runPropertyKey) => ({
|
|
|
1334
1962
|
carrier: runAttributeCarrier(runPropertyKey ?? key),
|
|
1335
1963
|
schema
|
|
1336
1964
|
});
|
|
1337
|
-
|
|
1965
|
+
var INLINE_PROPERTY_REGISTRY = [
|
|
1338
1966
|
markBoolean("bold", "w:b", "bold"),
|
|
1339
1967
|
markBoolean("italic", "w:i", "italic"),
|
|
1340
1968
|
markBoolean("strike", "w:strike", "strike"),
|
|
@@ -1492,12 +2120,57 @@ const INLINE_PROPERTY_REGISTRY = [
|
|
|
1492
2120
|
runAttribute("contextualAlternates", "boolean", "w14:cntxtAlts", schemaBooleanOrNull())
|
|
1493
2121
|
];
|
|
1494
2122
|
new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key));
|
|
1495
|
-
|
|
2123
|
+
var INLINE_PROPERTY_BY_KEY = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry]));
|
|
1496
2124
|
INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "mark").map((entry) => entry.key), INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "runAttribute").map((entry) => entry.key);
|
|
2125
|
+
//#endregion
|
|
2126
|
+
//#region src/public/ui/create-super-doc-ui.ts
|
|
2127
|
+
/**
|
|
2128
|
+
* v2-native `createSuperDocUI` controller.
|
|
2129
|
+
*
|
|
2130
|
+
* A small, truthful layer over the public v2 active-editor facade
|
|
2131
|
+
* (`superdoc.activeEditor`), its read-only-guarded Document API
|
|
2132
|
+
* (`activeEditor.doc`), and SuperDoc lifecycle events. No v1 editor imports,
|
|
2133
|
+
* no private v2 runtime imports. In browser mode the underlying Document API
|
|
2134
|
+
* facade is async-capable, so the controller normalizes it into live slices
|
|
2135
|
+
* plus promise-capable workflow helpers. Every document read/mutation is
|
|
2136
|
+
* attempted through the public Document API and degrades to a stable noop /
|
|
2137
|
+
* `false` when the surface is unavailable (view mode, a pre-ready editor, or a
|
|
2138
|
+
* host that has not exposed a Document API facade) rather than throwing.
|
|
2139
|
+
*/
|
|
2140
|
+
var sharedUiTrackedChangesCatalogByHost = /* @__PURE__ */ new WeakMap();
|
|
2141
|
+
function acquireSharedUiTrackedChangesCatalog(host) {
|
|
2142
|
+
let state = sharedUiTrackedChangesCatalogByHost.get(host);
|
|
2143
|
+
if (!state) {
|
|
2144
|
+
state = {
|
|
2145
|
+
refCount: 0,
|
|
2146
|
+
generation: 0,
|
|
2147
|
+
abortController: new AbortController(),
|
|
2148
|
+
inFlight: null,
|
|
2149
|
+
activeMutationTokens: /* @__PURE__ */ new Set()
|
|
2150
|
+
};
|
|
2151
|
+
sharedUiTrackedChangesCatalogByHost.set(host, state);
|
|
2152
|
+
}
|
|
2153
|
+
state.refCount += 1;
|
|
2154
|
+
return state;
|
|
2155
|
+
}
|
|
2156
|
+
function releaseSharedUiTrackedChangesCatalog(host, state) {
|
|
2157
|
+
state.refCount = Math.max(0, state.refCount - 1);
|
|
2158
|
+
if (state.refCount > 0) return;
|
|
2159
|
+
state.abortController.abort("ui-controller-destroyed");
|
|
2160
|
+
state.inFlight = null;
|
|
2161
|
+
state.activeMutationTokens.clear();
|
|
2162
|
+
sharedUiTrackedChangesCatalogByHost.delete(host);
|
|
2163
|
+
}
|
|
1497
2164
|
var SUPERDOC_UI_REASON_VALUES = new Set(Object.values(SUPERDOC_UI_REASONS));
|
|
1498
2165
|
function coerceSuperDocUIReason(reason, fallback) {
|
|
1499
2166
|
return typeof reason === "string" && SUPERDOC_UI_REASON_VALUES.has(reason) ? reason : fallback;
|
|
1500
2167
|
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Normalize the host selection apply helper's return value for
|
|
2170
|
+
* `selection.apply` / `selection.restore`: host `{ ok: false, reason }`
|
|
2171
|
+
* results pass through (unknown reasons coerce to `target-unresolved`);
|
|
2172
|
+
* anything else — including legacy void/boolean returns — counts as success.
|
|
2173
|
+
*/
|
|
1501
2174
|
function normalizeHostSelectionApplyResult(result) {
|
|
1502
2175
|
if (result && typeof result === "object") {
|
|
1503
2176
|
if (result.ok === false) {
|
|
@@ -1514,6 +2187,7 @@ function normalizeHostSelectionApplyResult(result) {
|
|
|
1514
2187
|
}
|
|
1515
2188
|
return { ok: true };
|
|
1516
2189
|
}
|
|
2190
|
+
/** Lifecycle events the controller subscribes to. */
|
|
1517
2191
|
var HOST_EVENTS = [
|
|
1518
2192
|
"document-replaced",
|
|
1519
2193
|
"editorCreate",
|
|
@@ -1524,6 +2198,12 @@ var HOST_EVENTS = [
|
|
|
1524
2198
|
"viewport-change",
|
|
1525
2199
|
"fonts-changed"
|
|
1526
2200
|
];
|
|
2201
|
+
/**
|
|
2202
|
+
* row-862 — list toolbar ids routed through the v2 editor host edit-command
|
|
2203
|
+
* surface (`activeEditor.editCommands.lists.apply`) rather than a raw one-off
|
|
2204
|
+
* `doc.lists.apply` mutation, so readiness / mount / selection / read-only
|
|
2205
|
+
* gating and command state stay coherent with the rest of the editor chrome.
|
|
2206
|
+
*/
|
|
1527
2207
|
var LIST_TOGGLE_KINDS = {
|
|
1528
2208
|
[BUILT_IN_COMMAND_IDS.bulletList]: "bullet",
|
|
1529
2209
|
[BUILT_IN_COMMAND_IDS.numberedList]: "ordered"
|
|
@@ -1531,6 +2211,15 @@ var LIST_TOGGLE_KINDS = {
|
|
|
1531
2211
|
function listToggleKind(id) {
|
|
1532
2212
|
return LIST_TOGGLE_KINDS[id] ?? null;
|
|
1533
2213
|
}
|
|
2214
|
+
/**
|
|
2215
|
+
* SD-3571 — the built-in toolbar's bullet/numbered style dropdowns emit a bare
|
|
2216
|
+
* toolbar style-key string (e.g. `'upper-roman'`, `'decimal-paren'`) as the
|
|
2217
|
+
* command argument. Map each to the Document API `ListPresetId` so the chosen
|
|
2218
|
+
* glyph/number format actually applies instead of being dropped to the default
|
|
2219
|
+
* decimal/disc list. Keys mirror `internal/toolbar/built-in/list-style-buttons.js`
|
|
2220
|
+
* (asserted by a coverage test); the public command surface also still accepts
|
|
2221
|
+
* an options object with an explicit `preset`.
|
|
2222
|
+
*/
|
|
1534
2223
|
var TOOLBAR_LIST_STYLE_PRESETS = {
|
|
1535
2224
|
disc: "disc",
|
|
1536
2225
|
circle: "circle",
|
|
@@ -1561,7 +2250,7 @@ var EMPTY_SELECTION = {
|
|
|
1561
2250
|
activeChangeIds: [],
|
|
1562
2251
|
quotedText: ""
|
|
1563
2252
|
};
|
|
1564
|
-
|
|
2253
|
+
var HEAVY_DOC_READ_POLICY = [
|
|
1565
2254
|
{
|
|
1566
2255
|
key: "contentControls",
|
|
1567
2256
|
match: "exact",
|
|
@@ -1590,12 +2279,12 @@ const HEAVY_DOC_READ_POLICY = [
|
|
|
1590
2279
|
{
|
|
1591
2280
|
key: "trackChanges",
|
|
1592
2281
|
match: "exact",
|
|
1593
|
-
note: "audited: all-story tracked-changes
|
|
2282
|
+
note: "audited: shared all-story tracked-changes catalog; stale-served during load"
|
|
1594
2283
|
},
|
|
1595
2284
|
{
|
|
1596
2285
|
key: "trackChanges:all",
|
|
1597
2286
|
match: "exact",
|
|
1598
|
-
note: "audited: all-story
|
|
2287
|
+
note: "audited: current-token validation over the shared all-story catalog; consumers fail closed on non-ready"
|
|
1599
2288
|
},
|
|
1600
2289
|
{
|
|
1601
2290
|
key: "tables",
|
|
@@ -1608,19 +2297,22 @@ const HEAVY_DOC_READ_POLICY = [
|
|
|
1608
2297
|
note: "reserved: full sections list"
|
|
1609
2298
|
}
|
|
1610
2299
|
];
|
|
1611
|
-
var COMMENTS_CATALOG_PART_URIS = new Set([
|
|
2300
|
+
var COMMENTS_CATALOG_PART_URIS = /* @__PURE__ */ new Set([
|
|
1612
2301
|
"/word/comments.xml",
|
|
1613
2302
|
"/word/commentsExtended.xml",
|
|
1614
2303
|
"/word/commentsIds.xml"
|
|
1615
2304
|
]);
|
|
2305
|
+
/** Whether a coordinator cache key falls under the heavy-read policy. */
|
|
1616
2306
|
function isHeavyDocReadKey(key) {
|
|
1617
2307
|
return HEAVY_DOC_READ_POLICY.some((entry) => entry.match === "exact" ? entry.key === key : key.startsWith(entry.key));
|
|
1618
2308
|
}
|
|
2309
|
+
/** Status precedence: a combined status is as "unsettled" as its worst part. */
|
|
1619
2310
|
var STATUS_RANK = {
|
|
1620
2311
|
ready: 0,
|
|
1621
2312
|
stale: 1,
|
|
1622
2313
|
pending: 2
|
|
1623
2314
|
};
|
|
2315
|
+
/** Combine read statuses; `pending` (no data yet) dominates `stale`, which dominates `ready`. */
|
|
1624
2316
|
function combineStatus(...statuses) {
|
|
1625
2317
|
let worst = "ready";
|
|
1626
2318
|
for (const status of statuses) if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;
|
|
@@ -1638,6 +2330,7 @@ function safeCall(fn, fallback) {
|
|
|
1638
2330
|
function isPromiseLike(value) {
|
|
1639
2331
|
return Boolean(value) && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
1640
2332
|
}
|
|
2333
|
+
/** Resolve a dotted path (e.g. `format.bold`) to a callable on the doc facade. */
|
|
1641
2334
|
function resolveDocOperation(doc, path) {
|
|
1642
2335
|
if (!doc) return null;
|
|
1643
2336
|
const parts = path.split(".");
|
|
@@ -1672,6 +2365,7 @@ function partialLinkEditFailure(hyperlinkResult, textResult) {
|
|
|
1672
2365
|
textResult
|
|
1673
2366
|
};
|
|
1674
2367
|
}
|
|
2368
|
+
/** Read a stable public `target` off a doc-api entity row. */
|
|
1675
2369
|
function readEntityTarget(row) {
|
|
1676
2370
|
if (!row || typeof row !== "object") return null;
|
|
1677
2371
|
const target = row.target;
|
|
@@ -1688,8 +2382,8 @@ function isHostGeometryTarget(target) {
|
|
|
1688
2382
|
const isTextSegment = (segment) => {
|
|
1689
2383
|
if (!segment || typeof segment !== "object") return false;
|
|
1690
2384
|
const candidate = segment;
|
|
1691
|
-
const range
|
|
1692
|
-
return typeof candidate.blockId === "string" && typeof range
|
|
2385
|
+
const range = candidate.range;
|
|
2386
|
+
return typeof candidate.blockId === "string" && typeof range?.start === "number" && typeof range.end === "number";
|
|
1693
2387
|
};
|
|
1694
2388
|
if (Array.isArray(record.segments)) {
|
|
1695
2389
|
const first = record.segments[0];
|
|
@@ -1701,6 +2395,12 @@ function isHostGeometryTarget(target) {
|
|
|
1701
2395
|
const range = record.range;
|
|
1702
2396
|
return typeof range?.start === "number" && typeof range.end === "number";
|
|
1703
2397
|
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Convert the tracked-change catalog's document-stable block address into the
|
|
2400
|
+
* text address accepted by the host geometry surface. Keeping the entity
|
|
2401
|
+
* address lets the host prefer the exact painted review carrier after the
|
|
2402
|
+
* containing block has been materialized.
|
|
2403
|
+
*/
|
|
1704
2404
|
function readTrackedChangeNavigationTarget(row) {
|
|
1705
2405
|
if (!row || typeof row !== "object") return null;
|
|
1706
2406
|
const target = readEntityTarget(row);
|
|
@@ -1759,11 +2459,35 @@ function entityRowMatchesRequest(row, id, story) {
|
|
|
1759
2459
|
if (!story) return true;
|
|
1760
2460
|
return storyLocatorSignature(readEntityStory(row)) === storyLocatorSignature(story);
|
|
1761
2461
|
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Story to thread through a navigation / scroll request for a loaded list row:
|
|
2464
|
+
* the row's own non-body story, or `undefined` for body / story-less rows.
|
|
2465
|
+
* Mirrors `getAt`, which omits body stories from its hits, so body-only
|
|
2466
|
+
* documents keep their id-only matching byte-for-byte unchanged while a
|
|
2467
|
+
* non-body row (footnote / endnote / header / footer) pins target and carrier
|
|
2468
|
+
* resolution to its own story — an id repeated across stories can no longer
|
|
2469
|
+
* resolve to another story's occurrence (IT-1250).
|
|
2470
|
+
*/
|
|
1762
2471
|
function readEntityRequestStory(row) {
|
|
1763
2472
|
const story = readEntityStory(row);
|
|
1764
2473
|
if (!story || storyLocatorSignature(story) === storyLocatorSignature(null)) return void 0;
|
|
1765
2474
|
return story;
|
|
1766
2475
|
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Bridge a painter `data-layout-story` value to the public Document API
|
|
2478
|
+
* {@link StoryLocator} shape the controller's story-aware matchers speak.
|
|
2479
|
+
*
|
|
2480
|
+
* The painter encodes the layout story as `"body"` or `"<kind>:<id>"`;
|
|
2481
|
+
* `decodeLayoutStoryDataset` turns that back into `{ kind, id }`. Tracked-change
|
|
2482
|
+
* rows, however, carry a Document API `StoryLocator` (`{ kind: 'story',
|
|
2483
|
+
* storyType, noteId | refId }`), and `storyLocatorSignature` keys on
|
|
2484
|
+
* `storyType`, so the two forms must be reconciled before they can be compared.
|
|
2485
|
+
* Body, unknown, and id-less stories all return `undefined` so callers fall back
|
|
2486
|
+
* to id-only matching and never stamp a story onto the public hit — that keeps
|
|
2487
|
+
* body-only documents byte-for-byte unchanged. Story disambiguation only kicks
|
|
2488
|
+
* in for the non-body stories (footnote, endnote, header/footer) that can repeat
|
|
2489
|
+
* a tracked-change id across stories.
|
|
2490
|
+
*/
|
|
1767
2491
|
function layoutStoryDatasetToStoryLocator(raw) {
|
|
1768
2492
|
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
1769
2493
|
const decoded = decodeLayoutStoryDataset(raw);
|
|
@@ -1788,6 +2512,22 @@ function layoutStoryDatasetToStoryLocator(raw) {
|
|
|
1788
2512
|
default: return;
|
|
1789
2513
|
}
|
|
1790
2514
|
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Bridge a painter `data-story-key` value to the public Document API
|
|
2517
|
+
* {@link StoryLocator} shape. PREFERRED over {@link layoutStoryDatasetToStoryLocator}
|
|
2518
|
+
* for tracked changes: the run/marker element carries the real owning story in
|
|
2519
|
+
* `data-story-key` (`'body'`, `'fn:<noteId>'`, `'en:<noteId>'`,
|
|
2520
|
+
* `'hf:part:<refId>'`), while the layout fragment's `data-layout-story` falls
|
|
2521
|
+
* back to `'body'` for the footnote/endnote band — so reading only the layout
|
|
2522
|
+
* story loses the story identity needed to disambiguate repeated source ids.
|
|
2523
|
+
*
|
|
2524
|
+
* Split on the FIRST delimiter group only: the `fn:`/`en:` id and the `hf:part:`
|
|
2525
|
+
* refId are taken verbatim after their prefix, so an id that itself contains a
|
|
2526
|
+
* colon survives intact. The resulting shapes match what `storyLocatorSignature`
|
|
2527
|
+
* keys on and what the all-story `list({ in: 'all' })` rows carry. Body, empty,
|
|
2528
|
+
* and unknown keys return `undefined` so callers fall back to id-only matching
|
|
2529
|
+
* and body-only documents stay byte-for-byte unchanged.
|
|
2530
|
+
*/
|
|
1791
2531
|
function storyKeyToStoryLocator(raw) {
|
|
1792
2532
|
if (typeof raw !== "string" || raw.length === 0 || raw === "body") return void 0;
|
|
1793
2533
|
if (raw.startsWith("hf:part:")) {
|
|
@@ -1815,6 +2555,7 @@ function storyKeyToStoryLocator(raw) {
|
|
|
1815
2555
|
} : void 0;
|
|
1816
2556
|
}
|
|
1817
2557
|
}
|
|
2558
|
+
/** Read a public `selectionTarget` off a content-control row when available. */
|
|
1818
2559
|
function readSelectionTarget(row) {
|
|
1819
2560
|
if (!row || typeof row !== "object") return null;
|
|
1820
2561
|
const target = row.selectionTarget;
|
|
@@ -1836,12 +2577,19 @@ function collapseSelectionTargetToCaret(target) {
|
|
|
1836
2577
|
end: start
|
|
1837
2578
|
};
|
|
1838
2579
|
}
|
|
2580
|
+
/** Read a stable public row id from discovery/list/detail shapes. */
|
|
1839
2581
|
function readEntityId(row) {
|
|
1840
2582
|
if (!row || typeof row !== "object") return null;
|
|
1841
2583
|
const record = row;
|
|
1842
2584
|
const id = record.id ?? record.commentId ?? record.changeId;
|
|
1843
2585
|
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1844
2586
|
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Read the tracked-change carrier shared by both ends of a collapsed host
|
|
2589
|
+
* selection. Unlike the public sync seed, this painted caret metadata is
|
|
2590
|
+
* available without a worker read and proves that the caret still belongs to
|
|
2591
|
+
* the same review carrier.
|
|
2592
|
+
*/
|
|
1845
2593
|
function readCollapsedHostTrackChangeId(snapshot) {
|
|
1846
2594
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
1847
2595
|
const record = snapshot;
|
|
@@ -1854,6 +2602,11 @@ function readCollapsedHostTrackChangeId(snapshot) {
|
|
|
1854
2602
|
return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
|
|
1855
2603
|
}
|
|
1856
2604
|
var trackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
|
|
2605
|
+
/**
|
|
2606
|
+
* Collect the raw source-provenance ids a tracked-change list item exposes,
|
|
2607
|
+
* reading from both the top-level row and its nested `change` payload (the
|
|
2608
|
+
* public projection surfaces provenance on either shape).
|
|
2609
|
+
*/
|
|
1857
2610
|
function readTrackChangeProvenanceIds(item) {
|
|
1858
2611
|
const out = [];
|
|
1859
2612
|
const push = (value) => {
|
|
@@ -1894,6 +2647,7 @@ function readTrackChangeProvenanceIds(item) {
|
|
|
1894
2647
|
if (change && change !== item) scan(change);
|
|
1895
2648
|
return out;
|
|
1896
2649
|
}
|
|
2650
|
+
/** Build (or reuse) the alias↔canonical context for a tracked-change list slice. */
|
|
1897
2651
|
function buildTrackedChangeIdContext(items) {
|
|
1898
2652
|
const cached = trackedChangeIdContextCache.get(items);
|
|
1899
2653
|
if (cached) return cached;
|
|
@@ -1926,6 +2680,19 @@ function buildTrackedChangeIdContext(items) {
|
|
|
1926
2680
|
}
|
|
1927
2681
|
var storyScopedTrackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
|
|
1928
2682
|
var declaredBodyItemsCache = /* @__PURE__ */ new WeakMap();
|
|
2683
|
+
/**
|
|
2684
|
+
* Alias↔canonical context scoped to the rows in a single `story`.
|
|
2685
|
+
*
|
|
2686
|
+
* {@link buildTrackedChangeIdContext} keys aliases purely on the raw Word
|
|
2687
|
+
* `w:id` / `imported:<id>` painter form, so when body / footnote / header
|
|
2688
|
+
* changes reuse the same `w:id`, the first row inserted wins the alias for
|
|
2689
|
+
* EVERY story. A click in a later story would then map through
|
|
2690
|
+
* `toPublicId` to the first story's public id and get dropped by the
|
|
2691
|
+
* subsequent `entityRowMatchesRequest(..., story)` check (or focus the wrong
|
|
2692
|
+
* same-story split). Restricting the alias map to same-story candidates keeps
|
|
2693
|
+
* each story's ids independent so the reconciliation resolves within the
|
|
2694
|
+
* painted story. Falls back to id-only when the row carries no comparable story.
|
|
2695
|
+
*/
|
|
1929
2696
|
function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
1930
2697
|
const signature = storyLocatorSignature(story);
|
|
1931
2698
|
let byStory = storyScopedTrackedChangeIdContextCache.get(items);
|
|
@@ -1942,6 +2709,21 @@ function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
|
1942
2709
|
function isTextboxStory(story) {
|
|
1943
2710
|
return !!story && typeof story === "object" && story.storyType === "textbox";
|
|
1944
2711
|
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Resolve a textbox selection against rows that carry no story locator.
|
|
2714
|
+
*
|
|
2715
|
+
* Textbox rows arrive from the v2 adapter without a comparable locator, but body
|
|
2716
|
+
* rows omit theirs too (body is the documented default). A missing story is
|
|
2717
|
+
* therefore not proof of textbox ownership: when a textbox change and a body
|
|
2718
|
+
* change reuse one Word revision id, live selection can supply both canonical
|
|
2719
|
+
* ids, and taking the first story-less match would publish whichever the catalog
|
|
2720
|
+
* happened to list first.
|
|
2721
|
+
*
|
|
2722
|
+
* The signal is a shared revision id, not a count. A selection spanning several
|
|
2723
|
+
* independent changes in one textbox is ordinary — those ids describe different
|
|
2724
|
+
* revisions and the first is the right active one. Two ids tracing back to the
|
|
2725
|
+
* same revision id are the ambiguous case, and that fails closed.
|
|
2726
|
+
*/
|
|
1945
2727
|
function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
1946
2728
|
const storyless = selectionIds.filter((id) => items.some((item) => readEntityId(item) === id && readEntityStory(item) == null));
|
|
1947
2729
|
if (storyless.length === 0) return null;
|
|
@@ -1957,6 +2739,13 @@ function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
|
1957
2739
|
}
|
|
1958
2740
|
return storyless[0];
|
|
1959
2741
|
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Keep only rows that explicitly declare a story locator.
|
|
2744
|
+
*
|
|
2745
|
+
* A missing locator is not the same claim as `{ storyType: 'body' }`, but both
|
|
2746
|
+
* share the body story signature. Filtering the undeclared rows out keeps a body
|
|
2747
|
+
* selection from aliasing onto one of them.
|
|
2748
|
+
*/
|
|
1960
2749
|
function declaredBodyItems(items) {
|
|
1961
2750
|
const cached = declaredBodyItemsCache.get(items);
|
|
1962
2751
|
if (cached) return cached;
|
|
@@ -1965,6 +2754,17 @@ function declaredBodyItems(items) {
|
|
|
1965
2754
|
declaredBodyItemsCache.set(items, result);
|
|
1966
2755
|
return result;
|
|
1967
2756
|
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Map `selection.activeChangeIds` to a public tracked-change id for
|
|
2759
|
+
* `TrackChangesSlice.activeId`.
|
|
2760
|
+
*
|
|
2761
|
+
* Prefer the live selection's story against the all-story catalog so a
|
|
2762
|
+
* footnote/header/footer alias that reuses a Word revision id cannot resolve
|
|
2763
|
+
* to a colliding body change. Unscoped body mapping is only for body
|
|
2764
|
+
* selection. While the all-story catalog is unsettled for a non-body
|
|
2765
|
+
* selection, keep activeId null rather than publishing an unvalidated raw
|
|
2766
|
+
* painter alias or canonical-looking id.
|
|
2767
|
+
*/
|
|
1968
2768
|
function resolveSelectionActiveChangeId(options) {
|
|
1969
2769
|
const { selectionIds, publicIds, selection, bodyItems, allStoryItems } = options;
|
|
1970
2770
|
if (selectionIds.length === 0) return null;
|
|
@@ -1989,11 +2789,20 @@ function resolveSelectionActiveChangeId(options) {
|
|
|
1989
2789
|
if (isBodySelection) return mapIds(buildTrackedChangeIdContext(bodyItems)) ?? selectionIds[0] ?? null;
|
|
1990
2790
|
return null;
|
|
1991
2791
|
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Resolve a requested comment activation id to its thread's canonical active
|
|
2794
|
+
* id. Accepts a bare `id`, the `importedId` v2 minted when it had to repair
|
|
2795
|
+
* a malformed/duplicated source comment id on import, or a reply's id - all
|
|
2796
|
+
* resolve to the same thread-root comment, matching main's accepted aliases
|
|
2797
|
+
* for `ui.comments.setActive`. Returns `null` when `commentId` matches no
|
|
2798
|
+
* currently loaded comment under either alias.
|
|
2799
|
+
*/
|
|
1992
2800
|
function resolveActiveCommentId(items, commentId) {
|
|
1993
2801
|
const item = items.find((candidate) => readEntityId(candidate) === commentId || candidate.importedId === commentId);
|
|
1994
2802
|
if (!item) return null;
|
|
1995
2803
|
return item.rootCommentId ?? item.parentCommentId ?? readEntityId(item) ?? commentId;
|
|
1996
2804
|
}
|
|
2805
|
+
/** Derive a `{ startBlockId, endBlockId }` block range from a selection slice. */
|
|
1997
2806
|
function selectionBlockRange(selection) {
|
|
1998
2807
|
const sel = selection.selectionTarget;
|
|
1999
2808
|
if (sel) {
|
|
@@ -2016,6 +2825,16 @@ function selectionBlockRange(selection) {
|
|
|
2016
2825
|
};
|
|
2017
2826
|
return null;
|
|
2018
2827
|
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Distinct block ids the selection covers, in document order. In the v2 adapter
|
|
2830
|
+
* the selection's `blockId` is the same identifier the public block operations
|
|
2831
|
+
* accept as `nodeId` (both resolve to the OOXML `paraId`), so these are valid
|
|
2832
|
+
* `nodeId`s for `format.paragraph.*` / `styles.paragraph.*` / `lists.*`.
|
|
2833
|
+
*
|
|
2834
|
+
* A collapsed caret still yields its containing block id (the v2
|
|
2835
|
+
* `selection.current` returns a zero-length text segment), so paragraph/list
|
|
2836
|
+
* commands work from a caret — they do not require a range selection.
|
|
2837
|
+
*/
|
|
2019
2838
|
function selectionBlockIds(selection) {
|
|
2020
2839
|
const ids = [];
|
|
2021
2840
|
const push = (id) => {
|
|
@@ -2035,6 +2854,7 @@ var MAX_REACTIVE_SELECTION_BLOCK_READS = 64;
|
|
|
2035
2854
|
function canProbeEverySelectedBlock(blockIds) {
|
|
2036
2855
|
return blockIds.length <= MAX_REACTIVE_SELECTION_BLOCK_READS;
|
|
2037
2856
|
}
|
|
2857
|
+
/** Resolve the selection's story, defaulting to the main body story. */
|
|
2038
2858
|
function selectionStory(selection) {
|
|
2039
2859
|
const target = selection.target;
|
|
2040
2860
|
const explicit = selection.selectionTarget;
|
|
@@ -2046,6 +2866,7 @@ function selectionStory(selection) {
|
|
|
2046
2866
|
storyType: "body"
|
|
2047
2867
|
};
|
|
2048
2868
|
}
|
|
2869
|
+
/** Build a `ParagraphTarget` ( `format.paragraph.*` / `styles.paragraph.*` ). */
|
|
2049
2870
|
function paragraphTarget(blockId, story) {
|
|
2050
2871
|
return {
|
|
2051
2872
|
kind: "block",
|
|
@@ -2054,6 +2875,7 @@ function paragraphTarget(blockId, story) {
|
|
|
2054
2875
|
...story && typeof story === "object" ? { story } : {}
|
|
2055
2876
|
};
|
|
2056
2877
|
}
|
|
2878
|
+
/** Build a `ListsBlockTarget` ( `lists.apply` / `lists.remove` / `lists.getState` ). */
|
|
2057
2879
|
function listsBlockTarget(blockId) {
|
|
2058
2880
|
return {
|
|
2059
2881
|
kind: "block",
|
|
@@ -2061,6 +2883,7 @@ function listsBlockTarget(blockId) {
|
|
|
2061
2883
|
nodeId: blockId
|
|
2062
2884
|
};
|
|
2063
2885
|
}
|
|
2886
|
+
/** Build a `ListItemAddress` ( `lists.indent` / `lists.outdent` / `lists.applyStyle` ). */
|
|
2064
2887
|
function listItemTarget(blockId, story) {
|
|
2065
2888
|
return {
|
|
2066
2889
|
kind: "block",
|
|
@@ -2098,6 +2921,7 @@ function readParagraphIndentationFromResult(result) {
|
|
|
2098
2921
|
...hanging != null ? { hanging } : {}
|
|
2099
2922
|
};
|
|
2100
2923
|
}
|
|
2924
|
+
/** Build one `TextAddress` per covered text segment for `hyperlinks.wrap`. */
|
|
2101
2925
|
function selectionTextAddresses(selection) {
|
|
2102
2926
|
const target = selection.target;
|
|
2103
2927
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
@@ -2239,12 +3063,12 @@ function selectionTextSegments(selection) {
|
|
|
2239
3063
|
const target = selection.target;
|
|
2240
3064
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
2241
3065
|
const out = [];
|
|
2242
|
-
const push = (blockId, start
|
|
2243
|
-
if (typeof blockId !== "string" || typeof start
|
|
3066
|
+
const push = (blockId, start, end) => {
|
|
3067
|
+
if (typeof blockId !== "string" || typeof start !== "number" || typeof end !== "number") return;
|
|
2244
3068
|
out.push({
|
|
2245
3069
|
blockId,
|
|
2246
|
-
start: Math.min(start
|
|
2247
|
-
end: Math.max(start
|
|
3070
|
+
start: Math.min(start, end),
|
|
3071
|
+
end: Math.max(start, end)
|
|
2248
3072
|
});
|
|
2249
3073
|
};
|
|
2250
3074
|
for (const segment of segments) {
|
|
@@ -2284,6 +3108,12 @@ function selectionInlineValueSignature(selection) {
|
|
|
2284
3108
|
if (segments.length === 0) return null;
|
|
2285
3109
|
return [selectionInlineValueStorySignature(selection), ...segments.map((segment) => `${segment.blockId}:${segment.start}-${segment.end}`)].join("|");
|
|
2286
3110
|
}
|
|
3111
|
+
/**
|
|
3112
|
+
* The effective-uniformity worker operation accepts the durable
|
|
3113
|
+
* `selectionTarget`, not the expanded segment list. Whole-story segment lists
|
|
3114
|
+
* grow as source coverage arrives, so using them as this cache key restarts the
|
|
3115
|
+
* same worker read indefinitely while the document is loading.
|
|
3116
|
+
*/
|
|
2287
3117
|
function selectionEffectiveUniformitySignature(selection) {
|
|
2288
3118
|
const target = selection.selectionTarget;
|
|
2289
3119
|
if (!target || typeof target !== "object") return null;
|
|
@@ -2326,6 +3156,11 @@ function sameSelectionTextSegments(left, right) {
|
|
|
2326
3156
|
return candidate?.blockId === segment.blockId && candidate?.start === segment.start && candidate?.end === segment.end;
|
|
2327
3157
|
});
|
|
2328
3158
|
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Pick the `query.match` result item that best covers the selection. Pure over
|
|
3161
|
+
* a settled query result so the controller's async read coordinator owns the
|
|
3162
|
+
* (promise-capable) `query.match` call and this stays a synchronous projection.
|
|
3163
|
+
*/
|
|
2329
3164
|
function pickSelectionTextQueryItem(result, selection) {
|
|
2330
3165
|
const segments = selectionTextSegments(selection);
|
|
2331
3166
|
if (segments.length === 0) return null;
|
|
@@ -2351,6 +3186,7 @@ function readProjectedInlineStyleValue(styles, key) {
|
|
|
2351
3186
|
if (key === "color" || key === "highlight") return trimmed.toUpperCase();
|
|
2352
3187
|
return trimmed;
|
|
2353
3188
|
}
|
|
3189
|
+
/** Primary named family from a resolved CSS font stack, dropping generic fallbacks (SD-3652). */
|
|
2354
3190
|
function normalizeLayoutFontFamily(value) {
|
|
2355
3191
|
if (typeof value !== "string") return void 0;
|
|
2356
3192
|
const first = value.split(",")[0]?.trim().replace(/^['"]+|['"]+$/g, "").trim();
|
|
@@ -2359,6 +3195,7 @@ function normalizeLayoutFontFamily(value) {
|
|
|
2359
3195
|
if (lower === "serif" || lower === "sans-serif" || lower === "monospace" || lower === "cursive" || lower === "fantasy") return;
|
|
2360
3196
|
return first;
|
|
2361
3197
|
}
|
|
3198
|
+
/** Convert a resolved run font size in CSS px to the toolbar's point value (SD-3652). */
|
|
2362
3199
|
function normalizeLayoutFontSizePt(value) {
|
|
2363
3200
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
|
|
2364
3201
|
const pt = Math.round(value * .75 * 2) / 2;
|
|
@@ -2419,6 +3256,7 @@ function projectInlineValuesFromQueryItem(item, selection) {
|
|
|
2419
3256
|
}
|
|
2420
3257
|
return projection;
|
|
2421
3258
|
}
|
|
3259
|
+
/** A create-location `at` derived from the current block, or `documentEnd`. */
|
|
2422
3260
|
function createLocationAt(selection, mode) {
|
|
2423
3261
|
const blockId = selectionBlockIds(selection)[0];
|
|
2424
3262
|
if (!blockId) return { kind: "documentEnd" };
|
|
@@ -2480,6 +3318,12 @@ function commandResultSucceeded(result) {
|
|
|
2480
3318
|
function isLooseObject(value) {
|
|
2481
3319
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2482
3320
|
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Flatten mounted projection blocks into text-bearing blocks (those with a
|
|
3323
|
+
* `runs` array), descending into table rows/cells so table-cell content is
|
|
3324
|
+
* reachable (SD-3652). A block carries either `runs` (paragraph) or `rows`
|
|
3325
|
+
* (table); nested tables recurse.
|
|
3326
|
+
*/
|
|
2483
3327
|
function collectProjectionTextBlocks(blocks) {
|
|
2484
3328
|
const out = [];
|
|
2485
3329
|
const visit = (node) => {
|
|
@@ -2499,6 +3343,7 @@ function collectProjectionTextBlocks(blocks) {
|
|
|
2499
3343
|
if (Array.isArray(blocks)) for (const block of blocks) visit(block);
|
|
2500
3344
|
return out;
|
|
2501
3345
|
}
|
|
3346
|
+
/** A projection block matches a selection block id by `sourceAnchor.sourceNodeId` or `block.id`. */
|
|
2502
3347
|
function projectionBlockMatchesId(block, id) {
|
|
2503
3348
|
if (block.sourceAnchor?.sourceNodeId === id) return true;
|
|
2504
3349
|
return block.id === id;
|
|
@@ -2547,6 +3392,7 @@ function hyperlinkTargetFromHref(href) {
|
|
|
2547
3392
|
url: href
|
|
2548
3393
|
};
|
|
2549
3394
|
}
|
|
3395
|
+
/** Tracked-change decision spec for a command id, from the descriptor catalog. */
|
|
2550
3396
|
function trackDecisionCommand(id) {
|
|
2551
3397
|
return getCommandDescriptor(id)?.trackDecision ?? null;
|
|
2552
3398
|
}
|
|
@@ -2558,6 +3404,7 @@ function rectResult(rects) {
|
|
|
2558
3404
|
rect: rects[0]
|
|
2559
3405
|
};
|
|
2560
3406
|
}
|
|
3407
|
+
/** Fail-closed empty geometry result carrying a stable reason. */
|
|
2561
3408
|
function rectFailure(reason) {
|
|
2562
3409
|
return {
|
|
2563
3410
|
found: false,
|
|
@@ -2566,6 +3413,7 @@ function rectFailure(reason) {
|
|
|
2566
3413
|
reason
|
|
2567
3414
|
};
|
|
2568
3415
|
}
|
|
3416
|
+
/** Offset client-space rects so they are relative to `relativeTo`, when given. */
|
|
2569
3417
|
function relativizeRects(result, relativeTo) {
|
|
2570
3418
|
if (!relativeTo || typeof relativeTo.getBoundingClientRect !== "function" || result.rects.length === 0) return result;
|
|
2571
3419
|
let origin;
|
|
@@ -2591,6 +3439,16 @@ function relativizeRects(result, relativeTo) {
|
|
|
2591
3439
|
rect: rects[0]
|
|
2592
3440
|
};
|
|
2593
3441
|
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Whether a text-target segment carries the addressable shape AND the value
|
|
3444
|
+
* invariants the derived selection target relies on (`blockId` string plus a
|
|
3445
|
+
* `range` of valid integer bounds, `start >= 0` and `start <= end` — the
|
|
3446
|
+
* documented `TextTarget` contract). Captures can be stale or deserialized from
|
|
3447
|
+
* a host store, so entries are validated instead of trusted: a segment that is
|
|
3448
|
+
* malformed (missing `range`) or out of bounds (negative, non-integer, or
|
|
3449
|
+
* inverted) makes the caller return `null` (fail closed) rather than throw
|
|
3450
|
+
* mid-derivation or restore a clamped, different range than was captured.
|
|
3451
|
+
*/
|
|
2594
3452
|
function isAddressableSegment(segment) {
|
|
2595
3453
|
if (!segment || typeof segment !== "object") return false;
|
|
2596
3454
|
const candidate = segment;
|
|
@@ -2626,6 +3484,15 @@ function selectionTargetForRestore(capture) {
|
|
|
2626
3484
|
if (capture.selectionTarget) return capture.selectionTarget;
|
|
2627
3485
|
return selectionTargetFromTextTarget(capture.target);
|
|
2628
3486
|
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Resolve the live selection slice to a `SelectionTarget` the inline
|
|
3489
|
+
* `format.*` operations accept. Returns the explicit `selectionTarget` when the
|
|
3490
|
+
* source provides one, otherwise derives a same-story selection target from the
|
|
3491
|
+
* resolved text target's first/last covered segments. Returns `null` when the
|
|
3492
|
+
* selection is empty or has no resolvable range — the inline command then fails closed with
|
|
3493
|
+
* `range-selection-required` rather than calling `format.*` with a missing
|
|
3494
|
+
* target (which the public Document API rejects with `INVALID_INPUT`).
|
|
3495
|
+
*/
|
|
2629
3496
|
function resolveInlineSelectionTarget(selection) {
|
|
2630
3497
|
if (selection.empty) return null;
|
|
2631
3498
|
if (selection.selectionTarget) return selection.selectionTarget;
|
|
@@ -2633,10 +3500,17 @@ function resolveInlineSelectionTarget(selection) {
|
|
|
2633
3500
|
if (fallback && fallback.start.kind === "text" && fallback.end.kind === "text" && fallback.start.blockId !== fallback.end.blockId) return null;
|
|
2634
3501
|
return fallback;
|
|
2635
3502
|
}
|
|
3503
|
+
/**
|
|
3504
|
+
* The `format.*` method name a stored inline mark is keyed by (SD-3654). The
|
|
3505
|
+
* host store and the toggle command both address a mark by its Document API
|
|
3506
|
+
* method (`docRoute` minus the `format.` prefix), which differs from the inline
|
|
3507
|
+
* `key` for strikethrough (`key: 'strike'` vs `docRoute: 'format.strikethrough'`).
|
|
3508
|
+
*/
|
|
2636
3509
|
function inlineFormatMethod(descriptor) {
|
|
2637
3510
|
if (!descriptor.inline || typeof descriptor.docRoute !== "string") return null;
|
|
2638
3511
|
return descriptor.docRoute.startsWith("format.") ? descriptor.docRoute.slice(7) : null;
|
|
2639
3512
|
}
|
|
3513
|
+
/** Inline run-property keys cleared by `clear-formatting` via `format.apply`. */
|
|
2640
3514
|
var CLEAR_INLINE_PATCH = {
|
|
2641
3515
|
bold: null,
|
|
2642
3516
|
italic: null,
|
|
@@ -2652,7 +3526,22 @@ var CLEAR_INLINE_PATCH = {
|
|
|
2652
3526
|
letterSpacing: null,
|
|
2653
3527
|
dstrike: null
|
|
2654
3528
|
};
|
|
3529
|
+
/**
|
|
3530
|
+
* Subset of `CLEAR_INLINE_PATCH` whose registry entry supports tracked
|
|
3531
|
+
* changes. Tracked `format.apply` rejects the whole patch with
|
|
3532
|
+
* `CAPABILITY_UNAVAILABLE` if any key isn't `tracked: true` (e.g. `smallCaps`,
|
|
3533
|
+
* `dstrike`), regardless of whether the selection actually carries them, so
|
|
3534
|
+
* suggesting mode must send this filtered patch instead of the full one.
|
|
3535
|
+
*/
|
|
2655
3536
|
var TRACKED_CLEAR_INLINE_PATCH = Object.fromEntries(Object.entries(CLEAR_INLINE_PATCH).filter(([key]) => INLINE_PROPERTY_BY_KEY[key]?.tracked === true));
|
|
3537
|
+
/**
|
|
3538
|
+
* Build the public Document API input for an inline-format command from the
|
|
3539
|
+
* resolved selection target, the normalized payload, and the command's live
|
|
3540
|
+
* active state. Boolean mark commands can also receive an explicit boolean/null
|
|
3541
|
+
* payload, which is useful for hosts that know the desired state themselves.
|
|
3542
|
+
* Returns `null` when the payload is invalid for the spec (so the controller
|
|
3543
|
+
* fails closed instead of forwarding a malformed value).
|
|
3544
|
+
*/
|
|
2656
3545
|
function buildInlineFormatInput(spec, target, payload, active) {
|
|
2657
3546
|
switch (spec.kind) {
|
|
2658
3547
|
case "toggle": {
|
|
@@ -2694,6 +3583,7 @@ function buildInlineFormatInput(spec, target, payload, active) {
|
|
|
2694
3583
|
default: return null;
|
|
2695
3584
|
}
|
|
2696
3585
|
}
|
|
3586
|
+
/** Read a `{ start, end }` cell-range from the host snapshot, when well-formed. */
|
|
2697
3587
|
function readMergeRange(range) {
|
|
2698
3588
|
const point = (value) => {
|
|
2699
3589
|
if (!value || typeof value !== "object") return null;
|
|
@@ -2716,6 +3606,12 @@ function readMergeRange(range) {
|
|
|
2716
3606
|
end
|
|
2717
3607
|
};
|
|
2718
3608
|
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Build the public `tables.*` input for a table cell-context command from the
|
|
3611
|
+
* resolved table context. Returns `null` when the action's required context is
|
|
3612
|
+
* missing (e.g. split with no resolved cell), so the controller fails closed
|
|
3613
|
+
* with `table-context-unavailable` rather than calling with a malformed locator.
|
|
3614
|
+
*/
|
|
2719
3615
|
function buildTableCommandInput(action, context) {
|
|
2720
3616
|
const { tableNodeId: nodeId, rowIndex, columnIndex, cellNodeId, mergeRange } = context;
|
|
2721
3617
|
switch (action) {
|
|
@@ -2804,18 +3700,22 @@ function createSuperDocUI(options) {
|
|
|
2804
3700
|
let frozenProjectedValues = {};
|
|
2805
3701
|
let frozenProjectedValuesKey = "";
|
|
2806
3702
|
let heldSettledInlineValues = null;
|
|
3703
|
+
/** Read the live active editor (or null). */
|
|
2807
3704
|
const getEditor = () => {
|
|
2808
3705
|
const editor = superdoc?.activeEditor;
|
|
2809
3706
|
return editor && typeof editor === "object" ? editor : null;
|
|
2810
3707
|
};
|
|
3708
|
+
/** Read the live browser Document API facade (or null). */
|
|
2811
3709
|
const getDoc = () => {
|
|
2812
3710
|
const doc = getEditor()?.doc;
|
|
2813
3711
|
return doc && typeof doc === "object" ? doc : null;
|
|
2814
3712
|
};
|
|
3713
|
+
/** Read the live v2 tracked-change facade, when exposed. */
|
|
2815
3714
|
const getV2TrackedChanges = () => {
|
|
2816
3715
|
const trackedChanges = getEditor()?.v2TrackedChanges;
|
|
2817
3716
|
return trackedChanges && typeof trackedChanges === "object" ? trackedChanges : null;
|
|
2818
3717
|
};
|
|
3718
|
+
/** Read the live v2 editor host (inline mode only), when exposed. */
|
|
2819
3719
|
const getHost = () => {
|
|
2820
3720
|
const host = getEditor()?.host;
|
|
2821
3721
|
return host && typeof host === "object" ? host : null;
|
|
@@ -2854,6 +3754,7 @@ function createSuperDocUI(options) {
|
|
|
2854
3754
|
}
|
|
2855
3755
|
return null;
|
|
2856
3756
|
};
|
|
3757
|
+
/** Read the host's stored inline marks (SD-3654/SD-3652), when any pending. */
|
|
2857
3758
|
const readPendingInlineFormat = () => {
|
|
2858
3759
|
const host = getHost();
|
|
2859
3760
|
const fn = host?.getPendingInlineFormat;
|
|
@@ -2861,6 +3762,11 @@ function createSuperDocUI(options) {
|
|
|
2861
3762
|
const pending = safeCall(() => fn.call(host), null);
|
|
2862
3763
|
return pending && typeof pending === "object" ? pending : null;
|
|
2863
3764
|
};
|
|
3765
|
+
/**
|
|
3766
|
+
* Active state contributed by a stored inline mark for this command, or null
|
|
3767
|
+
* when nothing is pending for it (SD-3654). Only meaningful for a collapsed
|
|
3768
|
+
* caret; the store is cleared on any selection move, so a range never has one.
|
|
3769
|
+
*/
|
|
2864
3770
|
const pendingInlineActive = (descriptor) => {
|
|
2865
3771
|
const method = inlineFormatMethod(descriptor);
|
|
2866
3772
|
if (!method) return null;
|
|
@@ -2870,6 +3776,7 @@ function createSuperDocUI(options) {
|
|
|
2870
3776
|
if (descriptor.inline?.kind === "toggle") return value === true;
|
|
2871
3777
|
return value != null && value !== "";
|
|
2872
3778
|
};
|
|
3779
|
+
/** The pending caret font/size value for this command's `format.*` method, if any (SD-3652). */
|
|
2873
3780
|
const pendingInlineValueFor = (descriptor) => {
|
|
2874
3781
|
const method = inlineFormatMethod(descriptor);
|
|
2875
3782
|
if (!method) return void 0;
|
|
@@ -2879,6 +3786,7 @@ function createSuperDocUI(options) {
|
|
|
2879
3786
|
if (typeof value === "string" && value !== "") return value;
|
|
2880
3787
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2881
3788
|
};
|
|
3789
|
+
/** Record / clear the stored inline mark for a collapsed-caret pick (SD-3654/SD-3652). */
|
|
2882
3790
|
const setPendingInlineFormatOnHost = (method, value) => {
|
|
2883
3791
|
const host = getHost();
|
|
2884
3792
|
const fn = host?.setPendingInlineFormat;
|
|
@@ -2889,16 +3797,23 @@ function createSuperDocUI(options) {
|
|
|
2889
3797
|
const fn = host?.clearPendingInlineFormat;
|
|
2890
3798
|
if (typeof fn === "function") safeCall(() => fn.call(host, method), void 0);
|
|
2891
3799
|
};
|
|
2892
|
-
|
|
3800
|
+
/** Inline `format.*` methods whose active state is a boolean mark (name === method). */
|
|
3801
|
+
const BOOLEAN_INLINE_FORMAT_METHODS = /* @__PURE__ */ new Set([
|
|
2893
3802
|
"bold",
|
|
2894
3803
|
"italic",
|
|
2895
3804
|
"underline",
|
|
2896
3805
|
"strikethrough"
|
|
2897
3806
|
]);
|
|
2898
|
-
|
|
3807
|
+
/**
|
|
3808
|
+
* Retire a stored inline mark (SD-3654/SD-3652) once the selection's own
|
|
3809
|
+
* formatting has caught up to it: a boolean mark when the live activeMarks
|
|
3810
|
+
* match, a font/size when the projection matches. Runs each recompute so the
|
|
3811
|
+
* toolbar hands off from "pending" to the real marks/value without a gap.
|
|
3812
|
+
*/
|
|
3813
|
+
const reconcilePendingInlineFormat = (selection) => {
|
|
2899
3814
|
const pending = readPendingInlineFormat();
|
|
2900
3815
|
if (!pending) return;
|
|
2901
|
-
const activeMarks = Array.isArray(selection
|
|
3816
|
+
const activeMarks = Array.isArray(selection.activeMarks) ? selection.activeMarks : [];
|
|
2902
3817
|
let projected = null;
|
|
2903
3818
|
for (const method of Object.keys(pending)) {
|
|
2904
3819
|
const value = pending[method];
|
|
@@ -2906,33 +3821,47 @@ function createSuperDocUI(options) {
|
|
|
2906
3821
|
if (BOOLEAN_INLINE_FORMAT_METHODS.has(method)) matched = activeMarks.includes(method) === (value === true);
|
|
2907
3822
|
else if (isProjectedInlineSelectionValueKey(method)) {
|
|
2908
3823
|
if (value == null) continue;
|
|
2909
|
-
if (!projected) projected = projectSelectionInlineValues(selection
|
|
3824
|
+
if (!projected) projected = projectSelectionInlineValues(selection);
|
|
2910
3825
|
matched = projected[method] === value;
|
|
2911
3826
|
} else matched = false;
|
|
2912
3827
|
if (matched) clearPendingInlineFormatOnHost(method);
|
|
2913
3828
|
}
|
|
2914
3829
|
};
|
|
3830
|
+
/** Read the live v2 edit-command adapters (`activeEditor.editCommands`), when exposed. */
|
|
2915
3831
|
const getEditCommands = () => {
|
|
2916
3832
|
const editCommands = getEditor()?.editCommands;
|
|
2917
3833
|
return editCommands && typeof editCommands === "object" ? editCommands : null;
|
|
2918
3834
|
};
|
|
3835
|
+
/** Read one edit-command snapshot entry (`activeEditor.editCommands.getSnapshot().commands[id]`). */
|
|
2919
3836
|
const readEditCommandStateEntry = (commandId) => {
|
|
2920
3837
|
const editCommands = getEditCommands();
|
|
2921
3838
|
if (!editCommands || typeof editCommands.getSnapshot !== "function") return null;
|
|
2922
3839
|
const entry = (safeCall(() => editCommands.getSnapshot(), null)?.commands)?.[commandId];
|
|
2923
3840
|
return entry && typeof entry === "object" ? entry : null;
|
|
2924
3841
|
};
|
|
3842
|
+
/** Read the `lists.apply` command state surfaced by the v2 edit-command snapshot. */
|
|
2925
3843
|
const readListApplyStateEntry = () => {
|
|
2926
3844
|
return readEditCommandStateEntry("lists.apply");
|
|
2927
3845
|
};
|
|
3846
|
+
/** Read the additive list active state surfaced by the edit-command snapshot. */
|
|
2928
3847
|
const readListActiveSeed = (entry) => {
|
|
2929
3848
|
const seed = (entry?.value)?.seed;
|
|
2930
3849
|
return seed === "bullet" || seed === "ordered" ? seed : null;
|
|
2931
3850
|
};
|
|
3851
|
+
/** Read the lower host-state entry mirrored into the public `undo` / `redo` ids. */
|
|
2932
3852
|
const readMirroredEditCommandStateEntry = (commandId) => {
|
|
2933
3853
|
const mirrored = MIRRORED_EDIT_COMMAND_IDS[commandId];
|
|
2934
3854
|
return mirrored ? readEditCommandStateEntry(mirrored) : null;
|
|
2935
3855
|
};
|
|
3856
|
+
/**
|
|
3857
|
+
* Shared table-context resolution. Reads the V2 host table-context facade
|
|
3858
|
+
* (`host.getTableContext()`) — the single public surface that resolves the
|
|
3859
|
+
* current selection's enclosing table without private editor internals — and
|
|
3860
|
+
* normalizes it to the locator inputs the `tables.*` operations need. Returns
|
|
3861
|
+
* `null` when the caret is not inside a table, the host does not expose the
|
|
3862
|
+
* facade, or the snapshot is missing the table node id / indices. Both the
|
|
3863
|
+
* built-in toolbar authority and custom UIs consume this same resolution.
|
|
3864
|
+
*/
|
|
2936
3865
|
const readHostTableContext = () => {
|
|
2937
3866
|
const host = getHost();
|
|
2938
3867
|
if (!host || typeof host.getTableContext !== "function") return null;
|
|
@@ -2999,11 +3928,11 @@ function createSuperDocUI(options) {
|
|
|
2999
3928
|
if (typeof review.getActiveReviewTarget !== "function" || typeof review.clearActiveReviewTarget !== "function") return;
|
|
3000
3929
|
if (safeCall(() => review.getActiveReviewTarget(), null)?.entityType === "trackedChange") safeCall(() => review.clearActiveReviewTarget(), void 0);
|
|
3001
3930
|
};
|
|
3002
|
-
const setExplicitActiveChange = (next, options
|
|
3931
|
+
const setExplicitActiveChange = (next, options) => {
|
|
3003
3932
|
explicitActiveChange = next;
|
|
3004
3933
|
explicitActiveChangeRevision += 1;
|
|
3005
3934
|
trackChangeRevealInvalidation += 1;
|
|
3006
|
-
if (options
|
|
3935
|
+
if (options?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
|
|
3007
3936
|
};
|
|
3008
3937
|
const listeners = /* @__PURE__ */ new Set();
|
|
3009
3938
|
let disposed = false;
|
|
@@ -3054,6 +3983,63 @@ function createSuperDocUI(options) {
|
|
|
3054
3983
|
};
|
|
3055
3984
|
let documentMutationRevision = 0;
|
|
3056
3985
|
let reviewMutationToken = {};
|
|
3986
|
+
const uiTrackedChangesCatalogHost = options.superdoc;
|
|
3987
|
+
const uiTrackedChangesCatalogState = acquireSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost);
|
|
3988
|
+
const supersedeUiTrackedChangesCatalogRead = () => {
|
|
3989
|
+
uiTrackedChangesCatalogState.abortController.abort("review-mutation-started");
|
|
3990
|
+
uiTrackedChangesCatalogState.inFlight = null;
|
|
3991
|
+
};
|
|
3992
|
+
const renewUiTrackedChangesCatalogRead = () => {
|
|
3993
|
+
uiTrackedChangesCatalogState.generation += 1;
|
|
3994
|
+
uiTrackedChangesCatalogState.abortController = new AbortController();
|
|
3995
|
+
uiTrackedChangesCatalogState.inFlight = null;
|
|
3996
|
+
};
|
|
3997
|
+
const beginUiReviewMutation = (token) => {
|
|
3998
|
+
const activeTokens = uiTrackedChangesCatalogState.activeMutationTokens;
|
|
3999
|
+
if (typeof token !== "string" || token.length === 0 || activeTokens.has(token)) return;
|
|
4000
|
+
const firstToken = activeTokens.size === 0;
|
|
4001
|
+
activeTokens.add(token);
|
|
4002
|
+
if (firstToken) supersedeUiTrackedChangesCatalogRead();
|
|
4003
|
+
};
|
|
4004
|
+
const settleUiReviewMutation = (token) => {
|
|
4005
|
+
const activeTokens = uiTrackedChangesCatalogState.activeMutationTokens;
|
|
4006
|
+
if (typeof token !== "string" || !activeTokens.delete(token)) return;
|
|
4007
|
+
if (activeTokens.size === 0) renewUiTrackedChangesCatalogRead();
|
|
4008
|
+
};
|
|
4009
|
+
const runUiTrackedChangesCatalogRead = (fallback) => {
|
|
4010
|
+
const v2TrackedChanges = getV2TrackedChanges();
|
|
4011
|
+
const listTrackedChanges = v2TrackedChanges?.listTrackedChanges;
|
|
4012
|
+
if (typeof listTrackedChanges !== "function") {
|
|
4013
|
+
if (getEditor()?.editorVersion === 2) return Promise.reject(/* @__PURE__ */ new Error("v2-tracked-changes-bridge-pending"));
|
|
4014
|
+
return fallback();
|
|
4015
|
+
}
|
|
4016
|
+
if (uiTrackedChangesCatalogState.activeMutationTokens.size > 0) return Promise.reject(/* @__PURE__ */ new Error("ui-review-catalog-superseded"));
|
|
4017
|
+
if ((getEditor()?.reviewHydration?.getDiagnostics?.())?.inFlight === true) return Promise.reject(/* @__PURE__ */ new Error("shell-review-hydration-in-flight"));
|
|
4018
|
+
const generation = uiTrackedChangesCatalogState.generation;
|
|
4019
|
+
const existing = uiTrackedChangesCatalogState.inFlight;
|
|
4020
|
+
if (existing?.generation === generation) return existing.promise;
|
|
4021
|
+
const signal = uiTrackedChangesCatalogState.abortController.signal;
|
|
4022
|
+
const validateResult = (result) => {
|
|
4023
|
+
if (signal.aborted || generation !== uiTrackedChangesCatalogState.generation || result?.reason === "review-hydration-superseded") throw new Error("ui-review-catalog-superseded");
|
|
4024
|
+
return result;
|
|
4025
|
+
};
|
|
4026
|
+
const rawResult = listTrackedChanges.call(v2TrackedChanges, {
|
|
4027
|
+
blocking: false,
|
|
4028
|
+
signal
|
|
4029
|
+
});
|
|
4030
|
+
if (!isPromiseLike(rawResult)) return validateResult(rawResult);
|
|
4031
|
+
const promise = Promise.resolve(rawResult).then(validateResult);
|
|
4032
|
+
uiTrackedChangesCatalogState.inFlight = {
|
|
4033
|
+
generation,
|
|
4034
|
+
promise
|
|
4035
|
+
};
|
|
4036
|
+
promise.then(() => {
|
|
4037
|
+
if (uiTrackedChangesCatalogState.inFlight?.promise === promise) uiTrackedChangesCatalogState.inFlight = null;
|
|
4038
|
+
}, () => {
|
|
4039
|
+
if (uiTrackedChangesCatalogState.inFlight?.promise === promise) uiTrackedChangesCatalogState.inFlight = null;
|
|
4040
|
+
});
|
|
4041
|
+
return promise;
|
|
4042
|
+
};
|
|
3057
4043
|
let authoritativeTrackChangesPendingToken = null;
|
|
3058
4044
|
let authoritativeTrackChangesRetryTimer = null;
|
|
3059
4045
|
let authoritativeTrackChangesFailureCount = 0;
|
|
@@ -3065,8 +4051,10 @@ function createSuperDocUI(options) {
|
|
|
3065
4051
|
let scheduleAuthoritativeTrackChangesRetry = (_reconcileToken) => void 0;
|
|
3066
4052
|
let postDecisionTrackChangesToken = null;
|
|
3067
4053
|
let postDecisionTrackChangeIds = /* @__PURE__ */ new Set();
|
|
4054
|
+
let allTrackedChangesResolvedToken = null;
|
|
3068
4055
|
let selectionEpoch = 0;
|
|
3069
4056
|
let lastCoordinatorEditor = null;
|
|
4057
|
+
/** Token shared by document-content reads (editor identity + mutation revision). */
|
|
3070
4058
|
const contentToken = () => `${editorIdentityId(getEditor())}|m${documentMutationRevision}`;
|
|
3071
4059
|
const clearPostDecisionTrackChanges = () => {
|
|
3072
4060
|
postDecisionTrackChangesToken = null;
|
|
@@ -3143,6 +4131,31 @@ function createSuperDocUI(options) {
|
|
|
3143
4131
|
};
|
|
3144
4132
|
notifyPostDecisionTrackChanges(receipt);
|
|
3145
4133
|
};
|
|
4134
|
+
/**
|
|
4135
|
+
* Publish the exact empty catalog proved by an all-resolved receipt.
|
|
4136
|
+
* Reject All does not carry one removed ref per logical change, so the
|
|
4137
|
+
* ordinary identity-pruning path cannot clear selection-derived focus or
|
|
4138
|
+
* the slice total. Keep this proof scoped to the current content token: the
|
|
4139
|
+
* next independent document mutation advances the token and resumes normal
|
|
4140
|
+
* catalog/selection reads.
|
|
4141
|
+
*/
|
|
4142
|
+
const publishAllTrackedChangesResolved = (receipt) => {
|
|
4143
|
+
const previous = state.trackChanges;
|
|
4144
|
+
const changed = previous.items.length > 0 || previous.total !== 0 || previous.activeId !== null || previous.authors.length > 0;
|
|
4145
|
+
allTrackedChangesResolvedToken = contentToken();
|
|
4146
|
+
if (explicitActiveChange !== null) setExplicitActiveChange(null);
|
|
4147
|
+
state = {
|
|
4148
|
+
...state,
|
|
4149
|
+
trackChanges: {
|
|
4150
|
+
...previous,
|
|
4151
|
+
items: [],
|
|
4152
|
+
total: 0,
|
|
4153
|
+
activeId: null,
|
|
4154
|
+
authors: []
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
4157
|
+
if (changed) notifyPostDecisionTrackChanges(receipt);
|
|
4158
|
+
};
|
|
3146
4159
|
const markPostDecisionTrackChanges = (ids, receipt) => {
|
|
3147
4160
|
if (ids.size === 0) return;
|
|
3148
4161
|
const token = contentToken();
|
|
@@ -3150,11 +4163,18 @@ function createSuperDocUI(options) {
|
|
|
3150
4163
|
const newlyRemoved = new Set([...ids].filter((id) => !alreadyRemoved.has(id)));
|
|
3151
4164
|
if (newlyRemoved.size === 0) return;
|
|
3152
4165
|
postDecisionTrackChangesToken = token;
|
|
3153
|
-
postDecisionTrackChangeIds = new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
4166
|
+
postDecisionTrackChangeIds = /* @__PURE__ */ new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
3154
4167
|
applyPostDecisionTrackChangesToCache(postDecisionTrackChangeIds);
|
|
3155
4168
|
publishPostDecisionTrackChanges(newlyRemoved, receipt);
|
|
3156
4169
|
};
|
|
3157
4170
|
const postDecisionTrackChangeIdsForToken = (token) => postDecisionTrackChangesToken === token && postDecisionTrackChangeIds.size > 0 ? postDecisionTrackChangeIds : null;
|
|
4171
|
+
/**
|
|
4172
|
+
* Merge a narrow tracked-change read into the two settled review caches.
|
|
4173
|
+
* Existing enriched adapter fields are retained while the fresh Document
|
|
4174
|
+
* API fields win. The entry keeps its original content token, so a row that
|
|
4175
|
+
* settles after the typing invalidation is truthfully served as stale rather
|
|
4176
|
+
* than discarded or mislabeled as a complete current-token catalog.
|
|
4177
|
+
*/
|
|
3158
4178
|
const mergeTrackedChangeItemsIntoCache = (items, orderedItems = []) => {
|
|
3159
4179
|
const projected = items.map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) != null);
|
|
3160
4180
|
if (projected.length === 0) return;
|
|
@@ -3222,22 +4242,22 @@ function createSuperDocUI(options) {
|
|
|
3222
4242
|
inflightToken: null
|
|
3223
4243
|
});
|
|
3224
4244
|
};
|
|
3225
|
-
const hydrateTrackedChanges = async (context, options
|
|
4245
|
+
const hydrateTrackedChanges = async (context, options) => {
|
|
3226
4246
|
try {
|
|
3227
4247
|
const listTrackedChanges = context.v2TrackedChanges?.listTrackedChanges;
|
|
3228
4248
|
if (typeof listTrackedChanges === "function") {
|
|
3229
|
-
const result
|
|
3230
|
-
mode: options
|
|
3231
|
-
refreshReason: options
|
|
3232
|
-
blocking: options
|
|
3233
|
-
...options
|
|
4249
|
+
const result = await Promise.resolve(listTrackedChanges.call(context.v2TrackedChanges, {
|
|
4250
|
+
mode: options.trackedChangesListMode,
|
|
4251
|
+
refreshReason: options.refreshReason,
|
|
4252
|
+
blocking: options.blocking,
|
|
4253
|
+
...options.targetIds?.length ? { targetIds: options.targetIds } : {}
|
|
3234
4254
|
}));
|
|
3235
|
-
const ok = options
|
|
4255
|
+
const ok = options.trackedChangesListMode === "all" && options.refreshReason === "mutation-history-render-reconcile" ? result?.ok === true : result?.ok !== false;
|
|
3236
4256
|
return {
|
|
3237
4257
|
ok,
|
|
3238
|
-
items: ok && Array.isArray(result
|
|
3239
|
-
...typeof result
|
|
3240
|
-
...typeof result
|
|
4258
|
+
items: ok && Array.isArray(result?.items) ? result.items : [],
|
|
4259
|
+
...typeof result?.complete === "boolean" ? { complete: result.complete } : {},
|
|
4260
|
+
...typeof result?.sourceCoverageComplete === "boolean" ? { sourceCoverageComplete: result.sourceCoverageComplete } : {}
|
|
3241
4261
|
};
|
|
3242
4262
|
}
|
|
3243
4263
|
const list = context.docTrackChanges?.list;
|
|
@@ -3258,6 +4278,13 @@ function createSuperDocUI(options) {
|
|
|
3258
4278
|
};
|
|
3259
4279
|
}
|
|
3260
4280
|
};
|
|
4281
|
+
/**
|
|
4282
|
+
* Read receipt-addressed review effects after their exact canonical paint.
|
|
4283
|
+
* The adapter's narrow get preserves its enriched row projection; the public
|
|
4284
|
+
* Document API is the fallback for hosts without that bridge method. Empty
|
|
4285
|
+
* successful reads remain unresolved and immediately use one bounded
|
|
4286
|
+
* interaction-prime list instead of waiting for the typing-idle catalog.
|
|
4287
|
+
*/
|
|
3261
4288
|
const reviewMutationReconciler = createV2ReviewMutationReconciler({
|
|
3262
4289
|
getContext: () => {
|
|
3263
4290
|
const editor = getEditor();
|
|
@@ -3301,12 +4328,12 @@ function createSuperDocUI(options) {
|
|
|
3301
4328
|
const reads = await Promise.all(requestedIds.map(async (id) => {
|
|
3302
4329
|
try {
|
|
3303
4330
|
if (typeof getTrackedChange === "function") {
|
|
3304
|
-
const result
|
|
4331
|
+
const result = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
|
|
3305
4332
|
return {
|
|
3306
4333
|
id,
|
|
3307
|
-
ok: result
|
|
3308
|
-
items: result
|
|
3309
|
-
result
|
|
4334
|
+
ok: result?.ok !== false,
|
|
4335
|
+
items: result?.ok !== false && Array.isArray(result?.items) ? result.items : [],
|
|
4336
|
+
result
|
|
3310
4337
|
};
|
|
3311
4338
|
}
|
|
3312
4339
|
const result = await Promise.resolve(getRawTrackedChange.call(context.docTrackChanges, { id }));
|
|
@@ -3373,11 +4400,12 @@ function createSuperDocUI(options) {
|
|
|
3373
4400
|
recompute();
|
|
3374
4401
|
return;
|
|
3375
4402
|
}
|
|
3376
|
-
|
|
4403
|
+
const items = (result?.items ?? []).filter((item) => {
|
|
3377
4404
|
const projected = projectTrackChangesItem(item);
|
|
3378
4405
|
const id = projected == null ? null : readEntityId(projected);
|
|
3379
4406
|
return id != null && ids.has(id);
|
|
3380
|
-
})
|
|
4407
|
+
});
|
|
4408
|
+
mergeTrackedChangeItemsIntoCache(items, result?.orderedItems ?? result?.items ?? []);
|
|
3381
4409
|
recompute();
|
|
3382
4410
|
}
|
|
3383
4411
|
});
|
|
@@ -3391,10 +4419,12 @@ function createSuperDocUI(options) {
|
|
|
3391
4419
|
reviewMutationReconciler.onRender(true);
|
|
3392
4420
|
}, delayMs);
|
|
3393
4421
|
};
|
|
4422
|
+
/** Token for the live selection read, including the mode that controls its public projection. */
|
|
3394
4423
|
const selectionReadToken = () => `${contentToken()}|m${readDocumentMode()}|s${selectionEpoch}`;
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
4424
|
+
/** Stable signature of a settled selection, used to key selection-scoped reads. */
|
|
4425
|
+
const selectionSignature = (selection) => JSON.stringify({
|
|
4426
|
+
t: selection.target ?? null,
|
|
4427
|
+
s: selection.selectionTarget ?? null
|
|
3398
4428
|
});
|
|
3399
4429
|
const SELECTION_SCOPED_ASYNC_READ_PREFIXES = [
|
|
3400
4430
|
"contentControls:inRange:",
|
|
@@ -3403,6 +4433,7 @@ function createSuperDocUI(options) {
|
|
|
3403
4433
|
];
|
|
3404
4434
|
const FOREGROUND_ASYNC_RETRY_MS = 120;
|
|
3405
4435
|
const COLD_ASYNC_READ_START_DELAY_MS = 180;
|
|
4436
|
+
/** Tri-state host source-load phase derived from the loading snapshot seam. */
|
|
3406
4437
|
const hostSourceLoadPhase = () => {
|
|
3407
4438
|
const host = getHost();
|
|
3408
4439
|
const read = host?.getDocumentLoadingSnapshot;
|
|
@@ -3425,9 +4456,14 @@ function createSuperDocUI(options) {
|
|
|
3425
4456
|
return Array.isArray(entry.value) && entry.value.length > 0;
|
|
3426
4457
|
};
|
|
3427
4458
|
const heavyReadDemandActive = (key, token) => demandedHeavyReads.get(key) === token;
|
|
4459
|
+
/**
|
|
4460
|
+
* Demand-driven route for the content-controls catalog (panel / API use).
|
|
4461
|
+
* Explicit demand bypasses both active-loading and post-complete idle holds.
|
|
4462
|
+
*/
|
|
3428
4463
|
const ensureContentControlsCatalog = (_reason) => {
|
|
3429
4464
|
demandHeavyDocRead("contentControls");
|
|
3430
4465
|
};
|
|
4466
|
+
/** Demand-driven route for an explicitly consumed tracked-change list. */
|
|
3431
4467
|
const ensureTrackChangesCatalog = () => {
|
|
3432
4468
|
const entry = asyncReads.get("trackChanges");
|
|
3433
4469
|
if (entry?.hasSettled || entry?.inflightToken != null) return;
|
|
@@ -3437,9 +4473,21 @@ function createSuperDocUI(options) {
|
|
|
3437
4473
|
const HEAVY_READ_IDLE_CEILING_MS = 8e3;
|
|
3438
4474
|
const HEAVY_READ_IDLE_POLL_MS = 250;
|
|
3439
4475
|
let lastEditableMutationAtMs = 0;
|
|
4476
|
+
/**
|
|
4477
|
+
* Last burst-class mutation: a local typing command or a remote apply whose
|
|
4478
|
+
* originating operation is unavailable. Drives the steady-phase heavy-read
|
|
4479
|
+
* hold while one-off local programmatic mutations keep refreshing promptly.
|
|
4480
|
+
*/
|
|
3440
4481
|
let lastTypingMutationAtMs = 0;
|
|
4482
|
+
/**
|
|
4483
|
+
* True from the first deferred heavy read until the input-idle release runs.
|
|
4484
|
+
* Held reads stay deferred even after source-complete so a typing-driven
|
|
4485
|
+
* recompute cannot bypass the idle gate.
|
|
4486
|
+
*/
|
|
3441
4487
|
let heavyReadsHeldUntilIdle = false;
|
|
4488
|
+
/** When the STEADY-phase typing hold began; bounds it to the same ceiling. */
|
|
3442
4489
|
let heavyReadsTypingHoldSinceMs = 0;
|
|
4490
|
+
/** Lets one ceiling-triggered recompute refresh every heavy key as a group. */
|
|
3443
4491
|
let heavyReadCeilingReleaseActive = false;
|
|
3444
4492
|
let heavyReadCompletionRecomputeTimer = null;
|
|
3445
4493
|
const scheduleHeavyReadCompletionRecompute = () => {
|
|
@@ -3499,15 +4547,15 @@ function createSuperDocUI(options) {
|
|
|
3499
4547
|
const host = getEditor()?.host;
|
|
3500
4548
|
const read = host?.getForegroundMutationState;
|
|
3501
4549
|
if (typeof read !== "function") return null;
|
|
3502
|
-
const state
|
|
4550
|
+
const state = safeCall(() => read.call(host), null);
|
|
3503
4551
|
return {
|
|
3504
|
-
active: typeof state
|
|
3505
|
-
pending: typeof state
|
|
4552
|
+
active: typeof state?.active === "number" ? state.active : 0,
|
|
4553
|
+
pending: typeof state?.pending === "number" ? state.pending : 0
|
|
3506
4554
|
};
|
|
3507
4555
|
};
|
|
3508
4556
|
const foregroundMutationActive = () => {
|
|
3509
|
-
const state
|
|
3510
|
-
return Boolean(state
|
|
4557
|
+
const state = foregroundMutationState();
|
|
4558
|
+
return Boolean(state && (state.active > 0 || state.pending > 0));
|
|
3511
4559
|
};
|
|
3512
4560
|
const scheduleForegroundAsyncRetry = (delayMs = FOREGROUND_ASYNC_RETRY_MS) => {
|
|
3513
4561
|
if (disposed || foregroundAsyncRetryTimer) return;
|
|
@@ -3533,6 +4581,12 @@ function createSuperDocUI(options) {
|
|
|
3533
4581
|
scheduleForegroundAsyncRetry(COLD_ASYNC_READ_START_DELAY_MS);
|
|
3534
4582
|
return true;
|
|
3535
4583
|
};
|
|
4584
|
+
/**
|
|
4585
|
+
* A large-selection uniformity read walks every covered paragraph. The first
|
|
4586
|
+
* settled selection reads immediately; later distinct selection keys debounce
|
|
4587
|
+
* so pointer-drag/autoscroll updates supersede one another before worker work
|
|
4588
|
+
* starts. Refreshes of the same selection stay immediate.
|
|
4589
|
+
*/
|
|
3536
4590
|
const shouldDeferEffectiveInlineRead = (key) => {
|
|
3537
4591
|
if (lastEffectiveInlineReadKey == null || key === lastEffectiveInlineReadKey) {
|
|
3538
4592
|
if (pendingEffectiveInlineRead?.timer) clearTimeout(pendingEffectiveInlineRead.timer);
|
|
@@ -3679,6 +4733,12 @@ function createSuperDocUI(options) {
|
|
|
3679
4733
|
});
|
|
3680
4734
|
return null;
|
|
3681
4735
|
};
|
|
4736
|
+
/**
|
|
4737
|
+
* Read a doc value through the settled cache. Returns the best-known value
|
|
4738
|
+
* plus a {@link SliceStatus}: `ready` (settled for this token), `stale`
|
|
4739
|
+
* (older settled value while a refresh runs), or `pending` (nothing settled
|
|
4740
|
+
* yet). Never returns or surfaces a promise.
|
|
4741
|
+
*/
|
|
3682
4742
|
const readAsync = (key, token, run, normalize) => {
|
|
3683
4743
|
const entry = asyncReads.get(key);
|
|
3684
4744
|
if (entry && entry.token === token && entry.hasSettled) return {
|
|
@@ -3777,23 +4837,24 @@ function createSuperDocUI(options) {
|
|
|
3777
4837
|
const clearAsyncReadKeys = (predicate) => {
|
|
3778
4838
|
for (const key of asyncReads.keys()) if (predicate(key)) asyncReads.delete(key);
|
|
3779
4839
|
};
|
|
3780
|
-
const selectionScopedAsyncReadKeys = (selection
|
|
3781
|
-
const signature = selectionSignature(selection
|
|
3782
|
-
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection
|
|
3783
|
-
return new Set([
|
|
4840
|
+
const selectionScopedAsyncReadKeys = (selection) => {
|
|
4841
|
+
const signature = selectionSignature(selection);
|
|
4842
|
+
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection);
|
|
4843
|
+
return /* @__PURE__ */ new Set([
|
|
3784
4844
|
`contentControls:inRange:${signature}`,
|
|
3785
4845
|
`query:${signature}`,
|
|
3786
4846
|
...effectiveUniformitySignature ? [`effInline:${effectiveUniformitySignature}`] : []
|
|
3787
4847
|
]);
|
|
3788
4848
|
};
|
|
3789
|
-
const pruneSelectionScopedAsyncReads = (selection
|
|
3790
|
-
const retained = selectionScopedAsyncReadKeys(selection
|
|
4849
|
+
const pruneSelectionScopedAsyncReads = (selection) => {
|
|
4850
|
+
const retained = selectionScopedAsyncReadKeys(selection);
|
|
3791
4851
|
clearAsyncReadKeys((key) => SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => key.startsWith(prefix)) && !retained.has(key));
|
|
3792
4852
|
for (const key of coldAsyncReadDeferrals) {
|
|
3793
4853
|
const readKey = key.split("\0", 1)[0];
|
|
3794
4854
|
if (SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => readKey.startsWith(prefix)) && !retained.has(readKey)) coldAsyncReadDeferrals.delete(key);
|
|
3795
4855
|
}
|
|
3796
4856
|
};
|
|
4857
|
+
/** Drop cached reads when the active editor identity changes (avoids cross-editor leakage). */
|
|
3797
4858
|
const syncCoordinatorEditor = () => {
|
|
3798
4859
|
const editor = getEditor();
|
|
3799
4860
|
if (editor === lastCoordinatorEditor) return;
|
|
@@ -3821,13 +4882,37 @@ function createSuperDocUI(options) {
|
|
|
3821
4882
|
detachSourceLoading = null;
|
|
3822
4883
|
sourceLoadingSubscriptionHost = null;
|
|
3823
4884
|
clearPostDecisionTrackChanges();
|
|
4885
|
+
allTrackedChangesResolvedToken = null;
|
|
3824
4886
|
};
|
|
4887
|
+
/** Bump the document-mutation revision so content reads re-fetch on the next compute. */
|
|
3825
4888
|
const invalidateDocumentContent = () => {
|
|
3826
4889
|
const carryAuthoritativeTrackChangesHold = authoritativeTrackChangesPendingToken !== null;
|
|
3827
4890
|
documentMutationRevision += 1;
|
|
3828
4891
|
if (carryAuthoritativeTrackChangesHold) authoritativeTrackChangesPendingToken = contentToken();
|
|
3829
4892
|
clearPostDecisionTrackChanges();
|
|
3830
4893
|
};
|
|
4894
|
+
/**
|
|
4895
|
+
* Refresh command-derived document slices without discarding an exact
|
|
4896
|
+
* all-resolved catalog received while the async command was settling. Host
|
|
4897
|
+
* mutation events arrive before the command promise resolves; a generic
|
|
4898
|
+
* invalidation at that later boundary would otherwise advance the cache
|
|
4899
|
+
* token and immediately re-list the catalog we already know is empty.
|
|
4900
|
+
*/
|
|
4901
|
+
const invalidateAfterCommandSettlement = () => {
|
|
4902
|
+
const carryAllResolvedCatalog = allTrackedChangesResolvedToken === contentToken();
|
|
4903
|
+
invalidateDocumentContent();
|
|
4904
|
+
if (carryAllResolvedCatalog) {
|
|
4905
|
+
replaceTrackedChangeItemsInCache([]);
|
|
4906
|
+
allTrackedChangesResolvedToken = contentToken();
|
|
4907
|
+
}
|
|
4908
|
+
recompute();
|
|
4909
|
+
};
|
|
4910
|
+
/**
|
|
4911
|
+
* Insert plain text through the public Document API. The narrow built-in /
|
|
4912
|
+
* custom insertion helper exposed on the custom-command callback context.
|
|
4913
|
+
* Fails closed (failure receipt) in viewing mode or when `doc.insert` is
|
|
4914
|
+
* unavailable — never reaching into `activeEditor.commands`.
|
|
4915
|
+
*/
|
|
3831
4916
|
const insertText = (text) => {
|
|
3832
4917
|
if (typeof text !== "string" || text.length === 0) return failedReceipt("insertText requires a non-empty string.", "INVALID_INPUT");
|
|
3833
4918
|
if (readDocumentMode() === "viewing") return failedReceipt("The document is read-only.", "DOCUMENT_READONLY");
|
|
@@ -3836,8 +4921,8 @@ function createSuperDocUI(options) {
|
|
|
3836
4921
|
if (!doc || typeof op !== "function") return failedReceipt("insert is unavailable.");
|
|
3837
4922
|
const currentSelection = doc.selection?.current;
|
|
3838
4923
|
if (typeof currentSelection !== "function") return failedReceipt("selection.current is unavailable.");
|
|
3839
|
-
const insertAtLiveSelection = (selection
|
|
3840
|
-
const record = selection
|
|
4924
|
+
const insertAtLiveSelection = (selection) => {
|
|
4925
|
+
const record = selection && typeof selection === "object" ? selection : null;
|
|
3841
4926
|
const target = record?.selectionTarget ?? record?.target ?? null;
|
|
3842
4927
|
if (!target || typeof target !== "object") return failedReceipt("insertText requires a live selection target.", "PRECONDITION_FAILED");
|
|
3843
4928
|
return safeCall(() => settleWorkflowReceipt(op.call(doc, {
|
|
@@ -3846,15 +4931,16 @@ function createSuperDocUI(options) {
|
|
|
3846
4931
|
target
|
|
3847
4932
|
}), failedReceipt("insert failed.")), failedReceipt("insert failed."));
|
|
3848
4933
|
};
|
|
3849
|
-
let selection
|
|
4934
|
+
let selection;
|
|
3850
4935
|
try {
|
|
3851
|
-
selection
|
|
4936
|
+
selection = currentSelection.call(doc.selection);
|
|
3852
4937
|
} catch {
|
|
3853
4938
|
return failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED");
|
|
3854
4939
|
}
|
|
3855
|
-
if (isPromiseLike(selection
|
|
3856
|
-
return insertAtLiveSelection(selection
|
|
4940
|
+
if (isPromiseLike(selection)) return Promise.resolve(selection).then(insertAtLiveSelection, () => failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED"));
|
|
4941
|
+
return insertAtLiveSelection(selection);
|
|
3857
4942
|
};
|
|
4943
|
+
/** Build the shared V2-truthful callback context for a custom command / button. */
|
|
3858
4944
|
const buildCustomCommandContext = (payload, context) => ({
|
|
3859
4945
|
payload,
|
|
3860
4946
|
state,
|
|
@@ -3876,10 +4962,11 @@ function createSuperDocUI(options) {
|
|
|
3876
4962
|
const editor = getEditor();
|
|
3877
4963
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
3878
4964
|
const fromConfig = normalizeDocumentMode((superdocRecord?.config)?.documentMode);
|
|
3879
|
-
const
|
|
4965
|
+
const runtimeSnapshot = safeCall(typeof superdocRecord?.getActiveRuntime === "function" ? () => {
|
|
3880
4966
|
const runtime = superdocRecord.getActiveRuntime();
|
|
3881
4967
|
return typeof runtime?.getSnapshot === "function" ? runtime.getSnapshot() : null;
|
|
3882
|
-
} : void 0, null)
|
|
4968
|
+
} : void 0, null);
|
|
4969
|
+
const fromRuntime = normalizeDocumentMode(runtimeSnapshot?.documentMode);
|
|
3883
4970
|
const fromOptions = normalizeDocumentMode((editor?.options)?.documentMode);
|
|
3884
4971
|
if (fromConfig && fromRuntime && fromConfig !== fromRuntime) return fromConfig;
|
|
3885
4972
|
return fromRuntime ?? fromConfig ?? fromOptions;
|
|
@@ -3888,15 +4975,20 @@ function createSuperDocUI(options) {
|
|
|
3888
4975
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
3889
4976
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
3890
4977
|
if (resolved && typeof resolved.readOnly === "boolean") return resolved.readOnly === true;
|
|
3891
|
-
const comments
|
|
3892
|
-
return !!comments
|
|
3893
|
-
};
|
|
4978
|
+
const comments = ((superdocRecord?.config)?.modules)?.comments;
|
|
4979
|
+
return !!comments && typeof comments === "object" && comments.readOnly === true;
|
|
4980
|
+
};
|
|
4981
|
+
/**
|
|
4982
|
+
* Whether `interaction.comments.allowResolve` forbids changing a thread's
|
|
4983
|
+
* resolved state. Same precedence as `readOnly`: the resolved policy first,
|
|
4984
|
+
* the legacy block only when it is absent.
|
|
4985
|
+
*/
|
|
3894
4986
|
const resolveIsForbidden = () => {
|
|
3895
4987
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
3896
4988
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
3897
4989
|
if (resolved && typeof resolved.allowResolve === "boolean") return resolved.allowResolve === false;
|
|
3898
|
-
const comments
|
|
3899
|
-
return !!comments
|
|
4990
|
+
const comments = ((superdocRecord?.config)?.modules)?.comments;
|
|
4991
|
+
return !!comments && typeof comments === "object" && comments.allowResolve === false;
|
|
3900
4992
|
};
|
|
3901
4993
|
const reviewMutationsAreReadOnly = () => readDocumentMode() === "viewing" || commentsReadOnlyForReview();
|
|
3902
4994
|
const normalizeSelectionInfo = (raw) => raw && typeof raw === "object" ? raw : null;
|
|
@@ -3930,6 +5022,7 @@ function createSuperDocUI(options) {
|
|
|
3930
5022
|
const selectionApi = getDoc()?.selection;
|
|
3931
5023
|
return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
|
|
3932
5024
|
};
|
|
5025
|
+
/** True when a host sync snapshot is a resolved collapsed caret (not a range). */
|
|
3933
5026
|
const isCollapsedCaretSnapshot = (info) => {
|
|
3934
5027
|
if (info.empty !== true) return false;
|
|
3935
5028
|
const target = info.selectionTarget;
|
|
@@ -3937,6 +5030,21 @@ function createSuperDocUI(options) {
|
|
|
3937
5030
|
const end = target?.end;
|
|
3938
5031
|
return start?.kind === "text" && end?.kind === "text" && typeof start.blockId === "string" && start.blockId === end.blockId && typeof start.offset === "number" && start.offset === end.offset;
|
|
3939
5032
|
};
|
|
5033
|
+
/**
|
|
5034
|
+
* On a caret move, seed the `selection` read cache synchronously from the
|
|
5035
|
+
* host's local snapshot so the toolbar reflects the NEW caret's block (and the
|
|
5036
|
+
* effective font resolved from it) on the next recompute, instead of serving
|
|
5037
|
+
* the previous selection until the worker `selection.current` round-trip lands
|
|
5038
|
+
* (SD-3652). Only a resolvable COLLAPSED CARET is seeded - a range's snapshot
|
|
5039
|
+
* cannot be resolved synchronously in worker mode, so ranges defer to the async
|
|
5040
|
+
* read (seeding an empty value would wrongly disable range-only commands). The
|
|
5041
|
+
* authoritative async read still overwrites marks/text/review overlap, but
|
|
5042
|
+
* foreground typing defers and coalesces that worker hop through the shared
|
|
5043
|
+
* retry gate. Previous marks are carried forward for toolbar continuity. A
|
|
5044
|
+
* tracked-change id is carried only when the host's painted caret proves the
|
|
5045
|
+
* same carrier, or while an active typing dispatch advances the same caret by
|
|
5046
|
+
* one code point. Pending work alone never carries review command context.
|
|
5047
|
+
*/
|
|
3940
5048
|
const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
|
|
3941
5049
|
const host = getHost();
|
|
3942
5050
|
const read = host?.readLiveSelectionSyncSnapshot;
|
|
@@ -4008,15 +5116,15 @@ function createSuperDocUI(options) {
|
|
|
4008
5116
|
recompute();
|
|
4009
5117
|
return state.selection;
|
|
4010
5118
|
};
|
|
4011
|
-
const computeComments = (selection
|
|
5119
|
+
const computeComments = (selection) => {
|
|
4012
5120
|
const commentsApi = getDoc()?.comments;
|
|
4013
5121
|
const { value, status: listStatus } = readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4014
5122
|
const items = value ?? [];
|
|
4015
|
-
const activeIds = selection
|
|
5123
|
+
const activeIds = selection.activeCommentIds;
|
|
4016
5124
|
if (explicitActiveCommentId && !items.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
|
|
4017
5125
|
const activeId = explicitActiveCommentId ?? activeIds[0] ?? null;
|
|
4018
5126
|
return {
|
|
4019
|
-
status: combineStatus(listStatus, selection
|
|
5127
|
+
status: combineStatus(listStatus, selection.status),
|
|
4020
5128
|
listStatus,
|
|
4021
5129
|
items,
|
|
4022
5130
|
total: items.length,
|
|
@@ -4031,12 +5139,22 @@ function createSuperDocUI(options) {
|
|
|
4031
5139
|
const { value, status } = authoritativeTrackChangesPendingToken === token ? {
|
|
4032
5140
|
value: heldEntry?.hasSettled ? heldEntry.value : null,
|
|
4033
5141
|
status: heldEntry?.hasSettled ? "stale" : "pending"
|
|
4034
|
-
} : readAsync("trackChanges:all", token, () =>
|
|
5142
|
+
} : readAsync("trackChanges:all", token, () => {
|
|
5143
|
+
if (!tcApi?.list) return void 0;
|
|
5144
|
+
const fallback = () => tcApi.list({ in: "all" });
|
|
5145
|
+
const bridgeOwnsCatalog = typeof getV2TrackedChanges()?.listTrackedChanges === "function";
|
|
5146
|
+
const shared = runUiTrackedChangesCatalogRead(fallback);
|
|
5147
|
+
if (!bridgeOwnsCatalog) return shared;
|
|
5148
|
+
return Promise.resolve(shared).then((raw) => {
|
|
5149
|
+
const record = isLooseObject(raw) ? raw : null;
|
|
5150
|
+
return (Array.isArray(record?.items) ? record.items : []).some((item) => readEntityStory(item) == null) ? fallback() : raw;
|
|
5151
|
+
});
|
|
5152
|
+
}, (raw) => {
|
|
4035
5153
|
return (raw && Array.isArray(raw.items) ? raw.items : []).map(projectTrackChangesItem).filter((item) => item != null);
|
|
4036
5154
|
});
|
|
4037
5155
|
return status === "ready" ? value : null;
|
|
4038
5156
|
};
|
|
4039
|
-
const computeTrackChanges = (selection
|
|
5157
|
+
const computeTrackChanges = (selection) => {
|
|
4040
5158
|
const tcApi = getDoc()?.trackChanges;
|
|
4041
5159
|
const v2TrackedChanges = getV2TrackedChanges();
|
|
4042
5160
|
const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
|
|
@@ -4047,7 +5165,7 @@ function createSuperDocUI(options) {
|
|
|
4047
5165
|
value: heldEntry?.hasSettled ? heldEntry.value : null,
|
|
4048
5166
|
status: heldEntry?.hasSettled ? "stale" : "pending"
|
|
4049
5167
|
} : readAsync("trackChanges", token, () => {
|
|
4050
|
-
if (listTrackedChanges) return
|
|
5168
|
+
if (listTrackedChanges) return runUiTrackedChangesCatalogRead(() => tcApi?.list?.());
|
|
4051
5169
|
return tcApi?.list ? tcApi.list({ in: "all" }) : void 0;
|
|
4052
5170
|
}, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4053
5171
|
const items = (value ?? []).map(projectTrackChangesItem).filter((item) => item != null).filter((item) => {
|
|
@@ -4069,51 +5187,58 @@ function createSuperDocUI(options) {
|
|
|
4069
5187
|
} else if (!items.some((item) => readEntityId(item) === active.id)) setExplicitActiveChange(null);
|
|
4070
5188
|
}
|
|
4071
5189
|
const publicIdItems = allStoryItems ?? items;
|
|
4072
|
-
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection
|
|
4073
|
-
const selectionActiveChangeIds = postDecisionIds ? selection
|
|
5190
|
+
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection));
|
|
5191
|
+
const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection.activeChangeIds.filter((id) => !postDecisionIds.has(id)) : selection.activeChangeIds;
|
|
4074
5192
|
const selectionPublicChangeIds = selectionActiveChangeIds.map((id) => selectionIdContext.toPublicId(id) ?? id);
|
|
4075
5193
|
const explicitActiveIdContext = explicitActiveChange?.story ? buildStoryScopedTrackedChangeIdContext(publicIdItems, explicitActiveChange.story) : buildTrackedChangeIdContext(publicIdItems);
|
|
4076
5194
|
const explicitActiveId = explicitActiveChange ? explicitActiveIdContext.toPublicId(explicitActiveChange.id) ?? explicitActiveChange.id : null;
|
|
4077
5195
|
const selectionActiveId = resolveSelectionActiveChangeId({
|
|
4078
5196
|
selectionIds: selectionActiveChangeIds,
|
|
4079
5197
|
publicIds: selectionPublicChangeIds,
|
|
4080
|
-
selection
|
|
5198
|
+
selection,
|
|
4081
5199
|
bodyItems: items,
|
|
4082
5200
|
allStoryItems
|
|
4083
5201
|
});
|
|
4084
5202
|
const activeId = explicitActiveId ?? selectionActiveId;
|
|
4085
5203
|
return {
|
|
4086
|
-
status: combineStatus(listStatus, selection
|
|
5204
|
+
status: combineStatus(listStatus, selection.status),
|
|
4087
5205
|
items,
|
|
4088
5206
|
total: items.length,
|
|
4089
5207
|
activeId,
|
|
4090
5208
|
authors: [...authors]
|
|
4091
5209
|
};
|
|
4092
5210
|
};
|
|
4093
|
-
const computeContentControls = (selection
|
|
5211
|
+
const computeContentControls = (selection) => {
|
|
4094
5212
|
const ccApi = getDoc()?.contentControls;
|
|
4095
5213
|
const { value, status: listStatus } = readAsync("contentControls", contentToken(), () => ccApi?.list ? ccApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4096
5214
|
const items = value ?? [];
|
|
4097
|
-
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection
|
|
5215
|
+
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection);
|
|
4098
5216
|
return {
|
|
4099
|
-
status: combineStatus(listStatus, rangeStatus, selection
|
|
5217
|
+
status: combineStatus(listStatus, rangeStatus, selection.status),
|
|
4100
5218
|
items,
|
|
4101
5219
|
total: items.length,
|
|
4102
5220
|
activeId: activeIds[0] ?? null,
|
|
4103
5221
|
activeIds
|
|
4104
5222
|
};
|
|
4105
5223
|
};
|
|
4106
|
-
|
|
5224
|
+
/**
|
|
5225
|
+
* Lock mode of every content control overlapping the current selection's
|
|
5226
|
+
* block range, keyed by control id. Powers `contentControlLockReason` so the
|
|
5227
|
+
* toolbar can disable styling controls when the selection touches a
|
|
5228
|
+
* `contentLocked`/`sdtContentLocked` control (SD-3274) — block-range scoped,
|
|
5229
|
+
* same precision as `computeActiveContentControlIds` below.
|
|
5230
|
+
*/
|
|
5231
|
+
const computeActiveContentControlLockModes = (ccApi, selection) => {
|
|
4107
5232
|
if (!ccApi || typeof ccApi.listInRange !== "function") return {
|
|
4108
5233
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4109
5234
|
status: "ready"
|
|
4110
5235
|
};
|
|
4111
|
-
const range = selectionBlockRange(selection
|
|
5236
|
+
const range = selectionBlockRange(selection);
|
|
4112
5237
|
if (!range) return {
|
|
4113
5238
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4114
5239
|
status: "ready"
|
|
4115
5240
|
};
|
|
4116
|
-
const { value, status } = readAsync(`contentControls:inRange:${selectionSignature(selection
|
|
5241
|
+
const { value, status } = readAsync(`contentControls:inRange:${selectionSignature(selection)}`, contentToken(), () => ccApi.listInRange(range), (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4117
5242
|
const rows = value ?? [];
|
|
4118
5243
|
const lockModesById = /* @__PURE__ */ new Map();
|
|
4119
5244
|
for (const row of rows) if (typeof row?.id === "string") lockModesById.set(row.id, typeof row.lockMode === "string" ? row.lockMode : "unlocked");
|
|
@@ -4122,8 +5247,8 @@ function createSuperDocUI(options) {
|
|
|
4122
5247
|
status
|
|
4123
5248
|
};
|
|
4124
5249
|
};
|
|
4125
|
-
const computeActiveContentControlIds = (ccApi, selection
|
|
4126
|
-
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection
|
|
5250
|
+
const computeActiveContentControlIds = (ccApi, selection) => {
|
|
5251
|
+
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4127
5252
|
return {
|
|
4128
5253
|
ids: [...lockModesById.keys()],
|
|
4129
5254
|
status
|
|
@@ -4137,14 +5262,14 @@ function createSuperDocUI(options) {
|
|
|
4137
5262
|
};
|
|
4138
5263
|
};
|
|
4139
5264
|
const computeZoom = () => {
|
|
4140
|
-
const state
|
|
4141
|
-
const value = typeof state
|
|
4142
|
-
const rawMode = state
|
|
5265
|
+
const state = safeCall(superdoc?.getZoomState ? () => superdoc.getZoomState() : void 0, null);
|
|
5266
|
+
const value = typeof state?.value === "number" ? state.value : 100;
|
|
5267
|
+
const rawMode = state?.mode;
|
|
4143
5268
|
return {
|
|
4144
5269
|
mode: rawMode === "manual" || rawMode === "fit-width" ? rawMode : rawMode === "fixed" ? "manual" : null,
|
|
4145
5270
|
value,
|
|
4146
|
-
min: typeof state
|
|
4147
|
-
max: typeof state
|
|
5271
|
+
min: typeof state?.min === "number" ? state.min : 10,
|
|
5272
|
+
max: typeof state?.max === "number" ? state.max : 100
|
|
4148
5273
|
};
|
|
4149
5274
|
};
|
|
4150
5275
|
const readMeasurementUnit = () => {
|
|
@@ -4158,12 +5283,18 @@ function createSuperDocUI(options) {
|
|
|
4158
5283
|
dirty: editor ? Boolean(editor.isDirty) : false
|
|
4159
5284
|
};
|
|
4160
5285
|
};
|
|
4161
|
-
|
|
4162
|
-
|
|
5286
|
+
/**
|
|
5287
|
+
* Resolve the `query.match` row covering the current selection through the
|
|
5288
|
+
* async read coordinator. The promise-capable browser `query.match` is read
|
|
5289
|
+
* via the cache (keyed by selection signature + content revision), so inline
|
|
5290
|
+
* value projection no longer collapses to empty when the read is async.
|
|
5291
|
+
*/
|
|
5292
|
+
const selectionTextQueryRequest = (selection) => {
|
|
5293
|
+
const pattern = selection.quotedText;
|
|
4163
5294
|
if (typeof pattern !== "string" || pattern.length === 0) return null;
|
|
4164
|
-
const segments = selectionTextSegments(selection
|
|
5295
|
+
const segments = selectionTextSegments(selection);
|
|
4165
5296
|
if (segments.length === 0 || !canProbeEverySelectedBlock(segments.map((segment) => segment.blockId))) return null;
|
|
4166
|
-
const target = selection
|
|
5297
|
+
const target = selection.target;
|
|
4167
5298
|
const within = segments.length === 1 ? paragraphTarget(segments[0].blockId, target?.story) : void 0;
|
|
4168
5299
|
return {
|
|
4169
5300
|
select: {
|
|
@@ -4176,24 +5307,30 @@ function createSuperDocUI(options) {
|
|
|
4176
5307
|
require: "any"
|
|
4177
5308
|
};
|
|
4178
5309
|
};
|
|
4179
|
-
const resolveSelectionTextQueryItem = (selection
|
|
5310
|
+
const resolveSelectionTextQueryItem = (selection) => {
|
|
4180
5311
|
const query = getDoc()?.query;
|
|
4181
5312
|
if (typeof query?.match !== "function") return null;
|
|
4182
|
-
const request = selectionTextQueryRequest(selection
|
|
5313
|
+
const request = selectionTextQueryRequest(selection);
|
|
4183
5314
|
if (!request) return null;
|
|
4184
|
-
const { value } = readAsync(`query:${selectionSignature(selection
|
|
4185
|
-
return pickSelectionTextQueryItem(value, selection
|
|
4186
|
-
};
|
|
4187
|
-
|
|
5315
|
+
const { value } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5316
|
+
return pickSelectionTextQueryItem(value, selection);
|
|
5317
|
+
};
|
|
5318
|
+
/**
|
|
5319
|
+
* Effective (cascade-resolved) font family / size for the selection, read from
|
|
5320
|
+
* the mounted layout and matched by source node id (SD-3652). The Document API
|
|
5321
|
+
* query surfaces only DIRECT run properties, so inherited fonts have no
|
|
5322
|
+
* projected value; the layout has already resolved them for painting.
|
|
5323
|
+
*/
|
|
5324
|
+
const resolveEffectiveInlineValuesFromLayout = (selection) => {
|
|
4188
5325
|
const host = getHost();
|
|
4189
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5326
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4190
5327
|
if (blockIds.size === 0) return {};
|
|
4191
5328
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4192
5329
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4193
5330
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4194
5331
|
const blocks = safeCall(() => {
|
|
4195
5332
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4196
|
-
const story = selectionStoryLocator(selection
|
|
5333
|
+
const story = selectionStoryLocator(selection);
|
|
4197
5334
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4198
5335
|
}, null);
|
|
4199
5336
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4208,7 +5345,7 @@ function createSuperDocUI(options) {
|
|
|
4208
5345
|
if (typeof run.highlight === "string" && run.highlight.trim() !== "") out.highlight = run.highlight.trim().toUpperCase();
|
|
4209
5346
|
return out;
|
|
4210
5347
|
};
|
|
4211
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5348
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4212
5349
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4213
5350
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4214
5351
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4232,7 +5369,7 @@ function createSuperDocUI(options) {
|
|
|
4232
5369
|
return runFontValues(chosen);
|
|
4233
5370
|
}
|
|
4234
5371
|
}
|
|
4235
|
-
const segments = selectionTextSegments(selection
|
|
5372
|
+
const segments = selectionTextSegments(selection);
|
|
4236
5373
|
if (segments.length > 0) {
|
|
4237
5374
|
const rangeFamilies = /* @__PURE__ */ new Set();
|
|
4238
5375
|
const rangeSizes = /* @__PURE__ */ new Set();
|
|
@@ -4262,10 +5399,10 @@ function createSuperDocUI(options) {
|
|
|
4262
5399
|
}
|
|
4263
5400
|
}
|
|
4264
5401
|
if (offsetSafe && rangeSawRun) {
|
|
4265
|
-
const projection
|
|
4266
|
-
if (rangeFamilies.size === 1) projection
|
|
4267
|
-
if (rangeSizes.size === 1) projection
|
|
4268
|
-
return projection
|
|
5402
|
+
const projection = {};
|
|
5403
|
+
if (rangeFamilies.size === 1) projection.fontFamily = [...rangeFamilies][0];
|
|
5404
|
+
if (rangeSizes.size === 1) projection.fontSize = [...rangeSizes][0];
|
|
5405
|
+
return projection;
|
|
4269
5406
|
}
|
|
4270
5407
|
}
|
|
4271
5408
|
const families = /* @__PURE__ */ new Set();
|
|
@@ -4288,16 +5425,29 @@ function createSuperDocUI(options) {
|
|
|
4288
5425
|
if (sizes.size === 1) projection.fontSize = [...sizes][0];
|
|
4289
5426
|
return projection;
|
|
4290
5427
|
};
|
|
4291
|
-
|
|
5428
|
+
/**
|
|
5429
|
+
* Effective (cascade-resolved) toggle-mark state read from the mounted
|
|
5430
|
+
* projection (SD-3860). `run.bold`/`run.italic`/`run.strike`/`run.underline`
|
|
5431
|
+
* on a projected run are already the FINAL merged value (style cascade +
|
|
5432
|
+
* any direct rPr override) — the same values `DomPainter` paints with — so
|
|
5433
|
+
* unlike the raw-rPr-only `selection.activeMarks` scan, this correctly
|
|
5434
|
+
* reports `false` for an explicit `<w:b w:val="0"/>` override even when a
|
|
5435
|
+
* paragraph/table style says bold. A key is left `undefined` when the
|
|
5436
|
+
* covered runs disagree (genuine mixed selection) or nothing could be read,
|
|
5437
|
+
* so the caller falls through to the worker-side uniformity read rather
|
|
5438
|
+
* than guessing. Mounted-projection `underline` is an object (`{}` /
|
|
5439
|
+
* `{ style: 'single' }`) when active, never a bare `true` — check presence.
|
|
5440
|
+
*/
|
|
5441
|
+
const resolveEffectiveMarkValuesFromLayout = (selection) => {
|
|
4292
5442
|
const host = getHost();
|
|
4293
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5443
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4294
5444
|
if (blockIds.size === 0) return {};
|
|
4295
5445
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4296
5446
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4297
5447
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4298
5448
|
const blocks = safeCall(() => {
|
|
4299
5449
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4300
|
-
const story = selectionStoryLocator(selection
|
|
5450
|
+
const story = selectionStoryLocator(selection);
|
|
4301
5451
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4302
5452
|
}, null);
|
|
4303
5453
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4316,7 +5466,7 @@ function createSuperDocUI(options) {
|
|
|
4316
5466
|
}
|
|
4317
5467
|
return out;
|
|
4318
5468
|
};
|
|
4319
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5469
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4320
5470
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4321
5471
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4322
5472
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4340,7 +5490,7 @@ function createSuperDocUI(options) {
|
|
|
4340
5490
|
return runMarkValues(chosen);
|
|
4341
5491
|
}
|
|
4342
5492
|
}
|
|
4343
|
-
const segments = selectionTextSegments(selection
|
|
5493
|
+
const segments = selectionTextSegments(selection);
|
|
4344
5494
|
if (segments.length > 0) {
|
|
4345
5495
|
const rangeSets = {
|
|
4346
5496
|
bold: /* @__PURE__ */ new Set(),
|
|
@@ -4394,12 +5544,12 @@ function createSuperDocUI(options) {
|
|
|
4394
5544
|
if (!sawWholeRun) return {};
|
|
4395
5545
|
return collapseMarkSets(wholeSets);
|
|
4396
5546
|
};
|
|
4397
|
-
const completeProjectedInlineValues = (selection
|
|
5547
|
+
const completeProjectedInlineValues = (selection, direct) => {
|
|
4398
5548
|
if (direct.fontFamily !== void 0 && direct.fontSize !== void 0 && direct.color !== void 0 && direct.highlight !== void 0) return {
|
|
4399
5549
|
values: direct,
|
|
4400
5550
|
effectiveUniformityStatus: "ready"
|
|
4401
5551
|
};
|
|
4402
|
-
const resolved = resolveEffectiveInlineValuesFromLayout(selection
|
|
5552
|
+
const resolved = resolveEffectiveInlineValuesFromLayout(selection);
|
|
4403
5553
|
const combined = {
|
|
4404
5554
|
...resolved.fontFamily !== void 0 ? { fontFamily: resolved.fontFamily } : {},
|
|
4405
5555
|
...resolved.fontSize !== void 0 ? { fontSize: resolved.fontSize } : {},
|
|
@@ -4407,8 +5557,8 @@ function createSuperDocUI(options) {
|
|
|
4407
5557
|
...resolved.highlight !== void 0 ? { highlight: resolved.highlight } : {},
|
|
4408
5558
|
...direct
|
|
4409
5559
|
};
|
|
4410
|
-
if (!selection
|
|
4411
|
-
const uniformity = resolveEffectiveInlineUniformityValues(selection
|
|
5560
|
+
if (!selection.empty && (combined.fontFamily === void 0 || combined.fontSize === void 0)) {
|
|
5561
|
+
const uniformity = resolveEffectiveInlineUniformityValues(selection);
|
|
4412
5562
|
if (uniformity.values) {
|
|
4413
5563
|
if (combined.fontFamily === void 0 && uniformity.values.fontFamily !== void 0) combined.fontFamily = uniformity.values.fontFamily;
|
|
4414
5564
|
if (combined.fontSize === void 0 && uniformity.values.fontSize !== void 0) combined.fontSize = uniformity.values.fontSize;
|
|
@@ -4423,6 +5573,13 @@ function createSuperDocUI(options) {
|
|
|
4423
5573
|
effectiveUniformityStatus: "ready"
|
|
4424
5574
|
};
|
|
4425
5575
|
};
|
|
5576
|
+
/**
|
|
5577
|
+
* Cached async access to the INTERNAL `format.readEffectiveInlineUniformity`
|
|
5578
|
+
* read (duck-typed; absent on hosts that do not provide it). Returns only
|
|
5579
|
+
* settled UNIFORM values - mixed and unresolvable keys stay undefined so the
|
|
5580
|
+
* toolbar renders its mixed/blank state, and a pending read serves nothing
|
|
5581
|
+
* (the held-value logic covers the transition).
|
|
5582
|
+
*/
|
|
4426
5583
|
const EFFECTIVE_INLINE_UNIFORMITY_KEYS = [
|
|
4427
5584
|
"fontFamily",
|
|
4428
5585
|
"fontSize",
|
|
@@ -4431,8 +5588,8 @@ function createSuperDocUI(options) {
|
|
|
4431
5588
|
"underline",
|
|
4432
5589
|
"strikethrough"
|
|
4433
5590
|
];
|
|
4434
|
-
const readEffectiveInlineUniformityCached = (selection
|
|
4435
|
-
const target = selection
|
|
5591
|
+
const readEffectiveInlineUniformityCached = (selection) => {
|
|
5592
|
+
const target = selection.selectionTarget;
|
|
4436
5593
|
if (!target) return {
|
|
4437
5594
|
value: null,
|
|
4438
5595
|
status: "ready"
|
|
@@ -4442,14 +5599,15 @@ function createSuperDocUI(options) {
|
|
|
4442
5599
|
value: null,
|
|
4443
5600
|
status: "ready"
|
|
4444
5601
|
};
|
|
4445
|
-
|
|
5602
|
+
const signature = selectionEffectiveUniformitySignature(selection) ?? selectionSignature(selection);
|
|
5603
|
+
return readAsync(`effInline:${signature}`, contentToken(), () => op({
|
|
4446
5604
|
target,
|
|
4447
5605
|
offsetSpace: "selection",
|
|
4448
5606
|
keys: EFFECTIVE_INLINE_UNIFORMITY_KEYS
|
|
4449
5607
|
}), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4450
5608
|
};
|
|
4451
|
-
const resolveEffectiveInlineUniformityValues = (selection
|
|
4452
|
-
const { value, status } = readEffectiveInlineUniformityCached(selection
|
|
5609
|
+
const resolveEffectiveInlineUniformityValues = (selection) => {
|
|
5610
|
+
const { value, status } = readEffectiveInlineUniformityCached(selection);
|
|
4453
5611
|
if (!value || value.success !== true) return {
|
|
4454
5612
|
values: null,
|
|
4455
5613
|
status
|
|
@@ -4468,8 +5626,9 @@ function createSuperDocUI(options) {
|
|
|
4468
5626
|
status
|
|
4469
5627
|
};
|
|
4470
5628
|
};
|
|
4471
|
-
|
|
4472
|
-
|
|
5629
|
+
/** Worker-side cascade-resolved mark uniformity, for selections whose tail isn't mounted (SD-3860). */
|
|
5630
|
+
const resolveEffectiveMarkUniformityValues = (selection) => {
|
|
5631
|
+
const { value, status } = readEffectiveInlineUniformityCached(selection);
|
|
4473
5632
|
if (!value || value.success !== true) return {
|
|
4474
5633
|
values: null,
|
|
4475
5634
|
status
|
|
@@ -4485,18 +5644,25 @@ function createSuperDocUI(options) {
|
|
|
4485
5644
|
status
|
|
4486
5645
|
};
|
|
4487
5646
|
};
|
|
4488
|
-
|
|
5647
|
+
/**
|
|
5648
|
+
* Effective active state for a toggle-mark command (bold/italic/underline/
|
|
5649
|
+
* strikethrough), SD-3860: the mounted-layout read is tried first (fast,
|
|
5650
|
+
* synchronous), then the worker uniformity read for unmounted content.
|
|
5651
|
+
* Returns `null` (no opinion) only when neither source has resolved yet,
|
|
5652
|
+
* in which case the caller falls back to the raw direct-only check.
|
|
5653
|
+
*/
|
|
5654
|
+
const effectiveMarkActiveState = (descriptor, selection) => {
|
|
4489
5655
|
const mark = descriptor.activeMark;
|
|
4490
5656
|
if (!mark || !EFFECTIVE_MARK_KEYS.includes(mark)) return null;
|
|
4491
5657
|
const key = mark;
|
|
4492
|
-
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection
|
|
5658
|
+
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection)[key];
|
|
4493
5659
|
if (fromLayout !== void 0) return fromLayout;
|
|
4494
|
-
const { values } = resolveEffectiveMarkUniformityValues(selection
|
|
5660
|
+
const { values } = resolveEffectiveMarkUniformityValues(selection);
|
|
4495
5661
|
const fromWorker = values?.[key];
|
|
4496
5662
|
return fromWorker !== void 0 ? fromWorker : null;
|
|
4497
5663
|
};
|
|
4498
|
-
const readEffectiveInlineUniformityNow = async (selection
|
|
4499
|
-
const target = selection
|
|
5664
|
+
const readEffectiveInlineUniformityNow = async (selection) => {
|
|
5665
|
+
const target = selection.selectionTarget;
|
|
4500
5666
|
if (!target) return null;
|
|
4501
5667
|
const op = resolveDocOperation(getDoc(), "format.readEffectiveInlineUniformity");
|
|
4502
5668
|
if (!op) return null;
|
|
@@ -4519,23 +5685,30 @@ function createSuperDocUI(options) {
|
|
|
4519
5685
|
else delete projected[key];
|
|
4520
5686
|
}
|
|
4521
5687
|
};
|
|
4522
|
-
const projectSelectionInlineValuesWithStatus = (selection
|
|
4523
|
-
const projectSelectionInlineValues = (selection
|
|
4524
|
-
|
|
5688
|
+
const projectSelectionInlineValuesWithStatus = (selection) => completeProjectedInlineValues(selection, projectInlineValuesFromQueryItem(resolveSelectionTextQueryItem(selection), selection));
|
|
5689
|
+
const projectSelectionInlineValues = (selection) => projectSelectionInlineValuesWithStatus(selection).values;
|
|
5690
|
+
/**
|
|
5691
|
+
* Read command-critical inline values for format-painter capture. Reactive
|
|
5692
|
+
* command state intentionally serves stale values while its worker read is
|
|
5693
|
+
* refreshing, but arming the painter must snapshot the mutation that just
|
|
5694
|
+
* settled. A direct, bounded query here makes the async capture authoritative
|
|
5695
|
+
* without changing the non-blocking toolbar snapshot policy.
|
|
5696
|
+
*/
|
|
5697
|
+
const readFormatPainterInlineValues = async (selection) => {
|
|
4525
5698
|
const query = getDoc()?.query;
|
|
4526
|
-
const request = selectionTextQueryRequest(selection
|
|
4527
|
-
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection
|
|
5699
|
+
const request = selectionTextQueryRequest(selection);
|
|
5700
|
+
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection);
|
|
4528
5701
|
try {
|
|
4529
5702
|
const raw = await Promise.resolve(query.match(request));
|
|
4530
|
-
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection
|
|
4531
|
-
const completed = completeProjectedInlineValues(selection
|
|
5703
|
+
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection), selection);
|
|
5704
|
+
const completed = completeProjectedInlineValues(selection, direct);
|
|
4532
5705
|
if (completed.effectiveUniformityStatus !== "ready" && (direct.fontFamily === void 0 || direct.fontSize === void 0)) {
|
|
4533
|
-
const effective = await readEffectiveInlineUniformityNow(selection
|
|
5706
|
+
const effective = await readEffectiveInlineUniformityNow(selection);
|
|
4534
5707
|
if (effective) applyEffectiveInlineUniformity(completed.values, direct, effective);
|
|
4535
5708
|
}
|
|
4536
5709
|
return completed.values;
|
|
4537
5710
|
} catch {
|
|
4538
|
-
return projectSelectionInlineValues(selection
|
|
5711
|
+
return projectSelectionInlineValues(selection);
|
|
4539
5712
|
}
|
|
4540
5713
|
};
|
|
4541
5714
|
const readBlockNode = async (address) => {
|
|
@@ -4547,21 +5720,35 @@ function createSuperDocUI(options) {
|
|
|
4547
5720
|
nodeType: address["nodeType"]
|
|
4548
5721
|
}));
|
|
4549
5722
|
};
|
|
4550
|
-
|
|
5723
|
+
/**
|
|
5724
|
+
* Returns the settled status of the query.match read backing the current
|
|
5725
|
+
* selection's inline-value projection. Called after resolveSelectionTextQueryItem
|
|
5726
|
+
* so readAsync always finds an existing cache entry — this is a status-only lookup.
|
|
5727
|
+
*/
|
|
5728
|
+
const selectionQueryStatus = (selection) => {
|
|
4551
5729
|
const query = getDoc()?.query;
|
|
4552
5730
|
if (typeof query?.match !== "function") return "ready";
|
|
4553
|
-
const request = selectionTextQueryRequest(selection
|
|
5731
|
+
const request = selectionTextQueryRequest(selection);
|
|
4554
5732
|
if (!request) return "ready";
|
|
4555
|
-
const { status } = readAsync(`query:${selectionSignature(selection
|
|
5733
|
+
const { status } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4556
5734
|
return status;
|
|
4557
5735
|
};
|
|
4558
|
-
|
|
4559
|
-
|
|
5736
|
+
/**
|
|
5737
|
+
* Last fully-settled inline projection, keyed by the selection's inline
|
|
5738
|
+
* signature (content-token-free: the SAME selection across content
|
|
5739
|
+
* revisions keeps its key). While a refresh of the same selection is
|
|
5740
|
+
* pending/stale, settled values fill keys the in-flight projection has not
|
|
5741
|
+
* resolved yet, so the toolbar font field does not flicker blank mid-edit.
|
|
5742
|
+
* A NEW selection never reads a previous selection's held values, and a
|
|
5743
|
+
* SETTLED mixed selection overwrites the hold (blank is then correct).
|
|
5744
|
+
*/
|
|
5745
|
+
const projectInlineValuesWithSettledHold = (selection) => {
|
|
5746
|
+
const projection = projectSelectionInlineValuesWithStatus(selection);
|
|
4560
5747
|
const projected = projection.values;
|
|
4561
|
-
if (selection
|
|
4562
|
-
const signature = selectionInlineValueSignature(selection
|
|
5748
|
+
if (selection.empty) return projection;
|
|
5749
|
+
const signature = selectionInlineValueSignature(selection);
|
|
4563
5750
|
if (!signature) return projection;
|
|
4564
|
-
if (selection
|
|
5751
|
+
if (selection.status === "ready" && selectionQueryStatus(selection) === "ready" && projection.effectiveUniformityStatus === "ready") {
|
|
4565
5752
|
heldSettledInlineValues = {
|
|
4566
5753
|
key: signature,
|
|
4567
5754
|
values: projected
|
|
@@ -4577,39 +5764,56 @@ function createSuperDocUI(options) {
|
|
|
4577
5764
|
};
|
|
4578
5765
|
return projection;
|
|
4579
5766
|
};
|
|
4580
|
-
const computeCommandStates = (selection
|
|
5767
|
+
const computeCommandStates = (selection) => {
|
|
4581
5768
|
const doc = getDoc();
|
|
4582
|
-
const projectedInlineRead = projectInlineValuesWithSettledHold(selection
|
|
5769
|
+
const projectedInlineRead = projectInlineValuesWithSettledHold(selection);
|
|
4583
5770
|
const projectedInlineValues = projectedInlineRead.values;
|
|
4584
|
-
if (selection
|
|
5771
|
+
if (selection.status === "ready" && !selection.empty && selectionQueryStatus(selection) === "ready" && projectedInlineRead.effectiveUniformityStatus === "ready") {
|
|
4585
5772
|
frozenProjectedValues = projectedInlineValues;
|
|
4586
|
-
frozenProjectedValuesKey = `${selectionKey(selection
|
|
5773
|
+
frozenProjectedValuesKey = `${selectionKey(selection)}:${contentToken()}`;
|
|
4587
5774
|
}
|
|
4588
5775
|
const ccApi = doc?.contentControls;
|
|
4589
|
-
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection
|
|
5776
|
+
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4590
5777
|
const states = {};
|
|
4591
|
-
for (const id of allCommandIds()) states[id] = computeCommandState(id, doc, selection
|
|
5778
|
+
for (const id of allCommandIds()) states[id] = computeCommandState(id, doc, selection, projectedInlineValues, lockModesById);
|
|
4592
5779
|
return states;
|
|
4593
5780
|
};
|
|
5781
|
+
/** Stable reason a routed command cannot reach its Document API operation. */
|
|
4594
5782
|
const unavailableRouteReason = (doc) => {
|
|
4595
5783
|
if (doc) return SUPERDOC_UI_REASONS.operationUnavailable;
|
|
4596
5784
|
return getEditor() ? SUPERDOC_UI_REASONS.documentApiUnavailable : SUPERDOC_UI_REASONS.notReady;
|
|
4597
5785
|
};
|
|
5786
|
+
/**
|
|
5787
|
+
* Lock modes that block run-level styling mutations (bold/italic/font/etc.),
|
|
5788
|
+
* mirroring the content-mutation axis the content-controls adapter's
|
|
5789
|
+
* `guardContentUnlocked` enforces server-side (`contentLocked` /
|
|
5790
|
+
* `sdtContentLocked`). `sdtLocked` protects only the wrapper, not styling.
|
|
5791
|
+
*/
|
|
4598
5792
|
const blocksContentStyling = (lockMode) => lockMode === "contentLocked" || lockMode === "sdtContentLocked";
|
|
5793
|
+
/**
|
|
5794
|
+
* Stable reason an inline (run-level) styling command is disabled because the
|
|
5795
|
+
* selection overlaps a content control whose lock forbids styling it — Word
|
|
5796
|
+
* parity (SD-3274): SuperDoc must not leave styling controls clickable when
|
|
5797
|
+
* the mutation cannot apply. Block-range scoped (same precision as
|
|
5798
|
+
* `computeActiveContentControlLockModes`), so this can over-disable when an
|
|
5799
|
+
* unrelated selection shares a block with a locked control — accepted for
|
|
5800
|
+
* now; alignment/paragraph-level commands are never gated here.
|
|
5801
|
+
*/
|
|
4599
5802
|
const contentControlLockReason = (lockModesById) => {
|
|
4600
5803
|
for (const lockMode of lockModesById.values()) if (blocksContentStyling(lockMode)) return SUPERDOC_UI_REASONS.contentControlLocked;
|
|
4601
5804
|
};
|
|
4602
|
-
|
|
5805
|
+
/** Stable reason a tracked-change decision command is disabled, or undefined when enabled. */
|
|
5806
|
+
const trackDecisionReason = (command, supported, readonly, selection, doc) => {
|
|
4603
5807
|
if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
|
|
4604
5808
|
if (readonly) return SUPERDOC_UI_REASONS.documentReadonly;
|
|
4605
|
-
if (command.scope !== "all" && selection
|
|
5809
|
+
if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
|
|
4606
5810
|
};
|
|
4607
5811
|
const hostCommandSupport = (command) => {
|
|
4608
5812
|
const host = getHost();
|
|
4609
|
-
const commands
|
|
4610
|
-
if (Array.isArray(commands
|
|
4611
|
-
if (commands
|
|
4612
|
-
const record = commands
|
|
5813
|
+
const commands = safeCall(typeof host?.getCapabilities === "function" ? () => host.getCapabilities() : void 0, null)?.editableSubset?.commands;
|
|
5814
|
+
if (Array.isArray(commands)) return commands.find((entry) => entry?.command === command || entry?.id === command) ?? null;
|
|
5815
|
+
if (commands && typeof commands === "object") {
|
|
5816
|
+
const record = commands[command];
|
|
4613
5817
|
return record && typeof record === "object" ? record : null;
|
|
4614
5818
|
}
|
|
4615
5819
|
return null;
|
|
@@ -4621,7 +5825,7 @@ function createSuperDocUI(options) {
|
|
|
4621
5825
|
if (support && (support.status === "supported" || support.enabled === true)) return void 0;
|
|
4622
5826
|
return SUPERDOC_UI_REASONS.bulkDecisionsDisabled;
|
|
4623
5827
|
};
|
|
4624
|
-
const computeCommandState = (id, doc, selection
|
|
5828
|
+
const computeCommandState = (id, doc, selection, projectedInlineValues, lockModesById = /* @__PURE__ */ new Map()) => {
|
|
4625
5829
|
if (customCommands.has(id)) return normalizeCommandState({
|
|
4626
5830
|
supported: true,
|
|
4627
5831
|
enabled: true,
|
|
@@ -4648,7 +5852,7 @@ function createSuperDocUI(options) {
|
|
|
4648
5852
|
const supportsSingle = typeof tcApi?.decide === "function" || typeof tcApi?.[trackCommand.kind] === "function";
|
|
4649
5853
|
const supportsAll = typeof tcApi?.decide === "function" || typeof tcApi?.[`${trackCommand.kind}All`] === "function";
|
|
4650
5854
|
const supported = trackCommand.scope === "all" ? supportsAll : supportsSingle;
|
|
4651
|
-
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection
|
|
5855
|
+
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection, doc);
|
|
4652
5856
|
return normalizeCommandState({
|
|
4653
5857
|
enabled: reason == null,
|
|
4654
5858
|
active: false,
|
|
@@ -4703,7 +5907,7 @@ function createSuperDocUI(options) {
|
|
|
4703
5907
|
supported: true,
|
|
4704
5908
|
reason: lockReason
|
|
4705
5909
|
}, "builtin");
|
|
4706
|
-
const enabled = selectionBlockIds(selection
|
|
5910
|
+
const enabled = selectionBlockIds(selection).length > 0 || resolveInlineSelectionTarget(selection) != null;
|
|
4707
5911
|
return normalizeCommandState({
|
|
4708
5912
|
enabled,
|
|
4709
5913
|
active: false,
|
|
@@ -4746,10 +5950,10 @@ function createSuperDocUI(options) {
|
|
|
4746
5950
|
reason
|
|
4747
5951
|
}, "builtin");
|
|
4748
5952
|
}
|
|
4749
|
-
const active = commandActiveState(descriptor, doc, selection
|
|
4750
|
-
const value = routedCommandValue(descriptor, doc, selection
|
|
4751
|
-
if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection
|
|
4752
|
-
if (descriptor.list?.mode === "toggle-seed") return computeListToggleCommandState(descriptor, doc, readonly, selection
|
|
5953
|
+
const active = commandActiveState(descriptor, doc, selection);
|
|
5954
|
+
const value = routedCommandValue(descriptor, doc, selection, projectedInlineValues);
|
|
5955
|
+
if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection, active, value);
|
|
5956
|
+
if (descriptor.list?.mode === "toggle-seed") return computeListToggleCommandState(descriptor, doc, readonly, selection, active, value);
|
|
4753
5957
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
4754
5958
|
enabled: false,
|
|
4755
5959
|
active,
|
|
@@ -4757,7 +5961,7 @@ function createSuperDocUI(options) {
|
|
|
4757
5961
|
value,
|
|
4758
5962
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
4759
5963
|
}, "builtin");
|
|
4760
|
-
if (descriptor.inline && !resolveInlineSelectionTarget(selection
|
|
5964
|
+
if (descriptor.inline && !resolveInlineSelectionTarget(selection)) {
|
|
4761
5965
|
const lockReason = contentControlLockReason(lockModesById);
|
|
4762
5966
|
if (lockReason) return normalizeCommandState({
|
|
4763
5967
|
enabled: false,
|
|
@@ -4766,7 +5970,7 @@ function createSuperDocUI(options) {
|
|
|
4766
5970
|
value,
|
|
4767
5971
|
reason: lockReason
|
|
4768
5972
|
}, "builtin");
|
|
4769
|
-
return normalizeCommandState(selectionBlockIds(selection
|
|
5973
|
+
return normalizeCommandState(selectionBlockIds(selection).length > 0 && typeof getHost()?.setPendingInlineFormat === "function" ? {
|
|
4770
5974
|
enabled: true,
|
|
4771
5975
|
active,
|
|
4772
5976
|
supported: true,
|
|
@@ -4779,14 +5983,14 @@ function createSuperDocUI(options) {
|
|
|
4779
5983
|
reason: SUPERDOC_UI_REASONS.rangeSelectionRequired
|
|
4780
5984
|
}, "builtin");
|
|
4781
5985
|
}
|
|
4782
|
-
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection
|
|
5986
|
+
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection).length === 0) return normalizeCommandState({
|
|
4783
5987
|
enabled: false,
|
|
4784
5988
|
active,
|
|
4785
5989
|
supported: true,
|
|
4786
5990
|
value,
|
|
4787
5991
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
4788
5992
|
}, "builtin");
|
|
4789
|
-
if (descriptor.link && !active && selectionTextAddresses(selection
|
|
5993
|
+
if (descriptor.link && !active && selectionTextAddresses(selection).length === 0 && !collapsedTextAddressFromSelection(selection)) return normalizeCommandState({
|
|
4790
5994
|
enabled: false,
|
|
4791
5995
|
active,
|
|
4792
5996
|
supported: true,
|
|
@@ -4809,7 +6013,7 @@ function createSuperDocUI(options) {
|
|
|
4809
6013
|
value
|
|
4810
6014
|
}, "builtin");
|
|
4811
6015
|
};
|
|
4812
|
-
function hyperlinkOverlapsSelection(link, selection
|
|
6016
|
+
function hyperlinkOverlapsSelection(link, selection) {
|
|
4813
6017
|
const anchor = link.address?.anchor;
|
|
4814
6018
|
const start = anchor?.start;
|
|
4815
6019
|
const end = anchor?.end;
|
|
@@ -4819,7 +6023,7 @@ function createSuperDocUI(options) {
|
|
|
4819
6023
|
const linkStart = typeof start?.offset === "number" ? start.offset : null;
|
|
4820
6024
|
const linkEnd = typeof end?.offset === "number" ? end.offset : null;
|
|
4821
6025
|
if (linkStart == null || linkEnd == null) return false;
|
|
4822
|
-
const target = selection
|
|
6026
|
+
const target = selection.target;
|
|
4823
6027
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
4824
6028
|
for (const segment of segments) {
|
|
4825
6029
|
const segmentBlockId = typeof segment?.blockId === "string" ? segment.blockId : null;
|
|
@@ -4842,10 +6046,15 @@ function createSuperDocUI(options) {
|
|
|
4842
6046
|
}
|
|
4843
6047
|
return false;
|
|
4844
6048
|
}
|
|
4845
|
-
|
|
6049
|
+
/**
|
|
6050
|
+
* Resolve the hyperlink overlapping the current selection/caret via
|
|
6051
|
+
* `hyperlinks.list({ within })` scoped to the selection's covered blocks.
|
|
6052
|
+
* Returns the first overlapping list row (`{ address, properties }`) or null.
|
|
6053
|
+
*/
|
|
6054
|
+
const resolveCurrentHyperlink = (doc, selection) => {
|
|
4846
6055
|
const linksApi = doc?.hyperlinks;
|
|
4847
6056
|
if (!linksApi || typeof linksApi.list !== "function") return null;
|
|
4848
|
-
const blockIds = selectionBlockIds(selection
|
|
6057
|
+
const blockIds = selectionBlockIds(selection);
|
|
4849
6058
|
if (!canProbeEverySelectedBlock(blockIds)) return null;
|
|
4850
6059
|
for (const blockId of blockIds) {
|
|
4851
6060
|
const within = {
|
|
@@ -4854,44 +6063,52 @@ function createSuperDocUI(options) {
|
|
|
4854
6063
|
nodeId: blockId
|
|
4855
6064
|
};
|
|
4856
6065
|
const { value: result } = readAsync(`hyperlinks:${blockId}`, contentToken(), () => linksApi.list({ within }), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4857
|
-
const hit = (result && Array.isArray(result.items) ? result.items : []).find((item) => hyperlinkOverlapsSelection(item, selection
|
|
6066
|
+
const hit = (result && Array.isArray(result.items) ? result.items : []).find((item) => hyperlinkOverlapsSelection(item, selection));
|
|
4858
6067
|
if (hit) return hit;
|
|
4859
6068
|
}
|
|
4860
6069
|
return null;
|
|
4861
6070
|
};
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
6071
|
+
/**
|
|
6072
|
+
* Active state for a routed command. Links resolve by public hyperlink
|
|
6073
|
+
* address overlap; list commands read the current block's list state and
|
|
6074
|
+
* match the seeded kind. Other inline marks use the selection mark set when
|
|
6075
|
+
* the host exposes it.
|
|
6076
|
+
*/
|
|
6077
|
+
const commandActiveState = (descriptor, doc, selection) => {
|
|
6078
|
+
if (descriptor.link) return resolveCurrentHyperlink(doc, selection) != null;
|
|
6079
|
+
if (descriptor.list?.mode === "toggle-seed" && descriptor.list.seed) return readListSeed(doc, selection) === descriptor.list.seed;
|
|
4865
6080
|
const pending = pendingInlineActive(descriptor);
|
|
4866
6081
|
if (pending !== null) return pending;
|
|
4867
6082
|
const optimistic = optimisticInlineToggles.get(descriptor.id);
|
|
4868
|
-
const selectionSignature
|
|
4869
|
-
if (selectionSignature
|
|
4870
|
-
const effective = effectiveMarkActiveState(descriptor, selection
|
|
6083
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
6084
|
+
if (selectionSignature && optimistic?.selectionSignature === selectionSignature) return optimistic.active;
|
|
6085
|
+
const effective = effectiveMarkActiveState(descriptor, selection);
|
|
4871
6086
|
if (effective !== null) return effective;
|
|
4872
|
-
return commandIsActive(descriptor, selection
|
|
6087
|
+
return commandIsActive(descriptor, selection);
|
|
4873
6088
|
};
|
|
4874
|
-
|
|
6089
|
+
/** Live `value` for a routed command, when modeled (current paragraph style / link href). */
|
|
6090
|
+
const routedCommandValue = (descriptor, doc, selection, projectedInlineValues) => {
|
|
4875
6091
|
if (descriptor.inline && isProjectedInlineSelectionValueKey(descriptor.inline.key)) {
|
|
4876
6092
|
const pending = pendingInlineValueFor(descriptor);
|
|
4877
6093
|
if (pending !== void 0) return pending;
|
|
4878
6094
|
const projected = projectedInlineValues[descriptor.inline.key];
|
|
4879
6095
|
if (projected !== void 0) return projected;
|
|
4880
|
-
const signature = selectionInlineValueSignature(selection
|
|
6096
|
+
const signature = selectionInlineValueSignature(selection);
|
|
4881
6097
|
const cached = signature ? optimisticInlineValues.get(descriptor.inline.key) : void 0;
|
|
4882
6098
|
if (signature && cached?.selectionSignature === signature) return cached.value;
|
|
4883
6099
|
}
|
|
4884
6100
|
if (descriptor.id === "linked-style") {
|
|
4885
|
-
const { style: active } = computeActiveParagraphStyle(selection
|
|
6101
|
+
const { style: active } = computeActiveParagraphStyle(selection, getStyleCatalog().cache);
|
|
4886
6102
|
if (!active.styleId || active.mixed) return void 0;
|
|
4887
6103
|
return {
|
|
4888
6104
|
styleId: active.styleId,
|
|
4889
6105
|
styleName: active.styleName
|
|
4890
6106
|
};
|
|
4891
6107
|
}
|
|
4892
|
-
if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection
|
|
4893
|
-
if (descriptor.id === "link") return readActiveLinkHref(doc, selection
|
|
6108
|
+
if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection);
|
|
6109
|
+
if (descriptor.id === "link") return readActiveLinkHref(doc, selection) ?? void 0;
|
|
4894
6110
|
};
|
|
6111
|
+
/** Read one block's list state and cache status through the selected story's list-state seam. */
|
|
4895
6112
|
const readListStateSnapshotForBlock = (doc, blockId, story) => {
|
|
4896
6113
|
const listsApi = doc?.lists;
|
|
4897
6114
|
if (!listsApi) return {
|
|
@@ -4913,14 +6130,22 @@ function createSuperDocUI(options) {
|
|
|
4913
6130
|
status
|
|
4914
6131
|
};
|
|
4915
6132
|
};
|
|
6133
|
+
/** Read one block's settled list state through the selected story's list-state seam. */
|
|
4916
6134
|
const readListStateForBlock = (doc, blockId, story) => {
|
|
4917
6135
|
return readListStateSnapshotForBlock(doc, blockId, story).value;
|
|
4918
6136
|
};
|
|
6137
|
+
/** Read one block's list seed (`'bullet'` / `'ordered'`) in the selected story. */
|
|
4919
6138
|
const readListSeedForBlock = (doc, blockId, story) => {
|
|
4920
6139
|
const result = readListStateForBlock(doc, blockId, story);
|
|
4921
6140
|
if (!result || result.isListItem !== true) return null;
|
|
4922
6141
|
return result.seed === "bullet" || result.seed === "ordered" ? result.seed : null;
|
|
4923
6142
|
};
|
|
6143
|
+
/**
|
|
6144
|
+
* Read whether a block is a list item. `null` means the authoritative state is
|
|
6145
|
+
* unavailable or has not settled yet; it must never be interpreted as a plain
|
|
6146
|
+
* paragraph. A list item may legitimately have no bullet/ordered seed (for
|
|
6147
|
+
* example, a linked Word numbering definition).
|
|
6148
|
+
*/
|
|
4924
6149
|
const readListMembershipSnapshotForBlock = (doc, blockId, story) => {
|
|
4925
6150
|
const snapshot = readListStateSnapshotForBlock(doc, blockId, story);
|
|
4926
6151
|
const result = snapshot.value;
|
|
@@ -4929,15 +6154,17 @@ function createSuperDocUI(options) {
|
|
|
4929
6154
|
status: snapshot.status
|
|
4930
6155
|
};
|
|
4931
6156
|
};
|
|
4932
|
-
|
|
4933
|
-
|
|
6157
|
+
/** Read a uniform list seed across the covered blocks, or null when mixed / non-list. */
|
|
6158
|
+
const readListSeed = (doc, selection) => {
|
|
6159
|
+
const blockIds = selectionBlockIds(selection);
|
|
4934
6160
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return null;
|
|
4935
|
-
const story = selectionStory(selection
|
|
6161
|
+
const story = selectionStory(selection);
|
|
4936
6162
|
const firstSeed = readListSeedForBlock(doc, blockIds[0], story);
|
|
4937
6163
|
if (firstSeed !== "bullet" && firstSeed !== "ordered") return null;
|
|
4938
6164
|
for (const blockId of blockIds.slice(1)) if (readListSeedForBlock(doc, blockId, story) !== firstSeed) return null;
|
|
4939
6165
|
return firstSeed;
|
|
4940
6166
|
};
|
|
6167
|
+
/** Read a paragraph node in its story through the async read coordinator. */
|
|
4941
6168
|
const readNodeById = (doc, blockId, story) => {
|
|
4942
6169
|
if (!doc) return {
|
|
4943
6170
|
value: null,
|
|
@@ -4974,11 +6201,12 @@ function createSuperDocUI(options) {
|
|
|
4974
6201
|
const isProjectionResolvedParagraphAlignment = (value) => {
|
|
4975
6202
|
return value === "start" || value === "end" || value === "distributed" || value === "numTab" || value === "lowKashida" || value === "mediumKashida" || value === "highKashida" || value === "thaiDistribute";
|
|
4976
6203
|
};
|
|
4977
|
-
|
|
6204
|
+
/** Read effective paragraph alignments from the mounted, style-resolved layout. */
|
|
6205
|
+
const readEffectiveParagraphAlignments = (selection, blockIds) => {
|
|
4978
6206
|
const host = getHost();
|
|
4979
6207
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4980
6208
|
if (typeof readByIds !== "function") return null;
|
|
4981
|
-
const story = selectionStoryLocator(selection
|
|
6209
|
+
const story = selectionStoryLocator(selection);
|
|
4982
6210
|
const blocks = safeCall(() => story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]), null);
|
|
4983
6211
|
if (!Array.isArray(blocks)) return /* @__PURE__ */ new Map();
|
|
4984
6212
|
const projectionBlocks = collectProjectionTextBlocks(blocks);
|
|
@@ -4993,6 +6221,7 @@ function createSuperDocUI(options) {
|
|
|
4993
6221
|
}
|
|
4994
6222
|
return alignments;
|
|
4995
6223
|
};
|
|
6224
|
+
/** Read public paragraph alignment, then fall back to the resolved projection. */
|
|
4996
6225
|
const readParagraphAlignment = (doc, blockId, story, effectiveAlignments) => {
|
|
4997
6226
|
const { value: result, status, refreshing } = readNodeById(doc, blockId, story);
|
|
4998
6227
|
const effective = effectiveAlignments?.get(blockId);
|
|
@@ -5029,11 +6258,12 @@ function createSuperDocUI(options) {
|
|
|
5029
6258
|
};
|
|
5030
6259
|
return { status: "unavailable" };
|
|
5031
6260
|
};
|
|
5032
|
-
|
|
5033
|
-
|
|
6261
|
+
/** Resolve a uniform effective alignment across every selected paragraph. */
|
|
6262
|
+
const readUniformParagraphAlignment = (doc, selection) => {
|
|
6263
|
+
const blockIds = selectionBlockIds(selection);
|
|
5034
6264
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return { status: "unavailable" };
|
|
5035
|
-
const story = selectionStory(selection
|
|
5036
|
-
const effectiveAlignments = readEffectiveParagraphAlignments(selection
|
|
6265
|
+
const story = selectionStory(selection);
|
|
6266
|
+
const effectiveAlignments = readEffectiveParagraphAlignments(selection, blockIds);
|
|
5037
6267
|
const first = readParagraphAlignment(doc, blockIds[0], story, effectiveAlignments);
|
|
5038
6268
|
if (first.status !== "uniform") return first;
|
|
5039
6269
|
for (const blockId of blockIds.slice(1)) {
|
|
@@ -5043,22 +6273,23 @@ function createSuperDocUI(options) {
|
|
|
5043
6273
|
}
|
|
5044
6274
|
return first;
|
|
5045
6275
|
};
|
|
5046
|
-
const paragraphAlignmentSelectionSignature = (selection
|
|
5047
|
-
const blockIds = selectionBlockIds(selection
|
|
6276
|
+
const paragraphAlignmentSelectionSignature = (selection) => {
|
|
6277
|
+
const blockIds = selectionBlockIds(selection);
|
|
5048
6278
|
if (blockIds.length === 0) return null;
|
|
5049
|
-
return `${storyLocatorSignature(selectionStory(selection
|
|
6279
|
+
return `${storyLocatorSignature(selectionStory(selection))}:${blockIds.join(",")}`;
|
|
5050
6280
|
};
|
|
5051
|
-
|
|
5052
|
-
|
|
6281
|
+
/** Keep a toolbar pick stable until its painted paragraph state is readable. */
|
|
6282
|
+
const readToolbarParagraphAlignment = (doc, selection) => {
|
|
6283
|
+
const resolution = readUniformParagraphAlignment(doc, selection);
|
|
5053
6284
|
const resolved = resolution.status === "uniform" ? resolution.value : void 0;
|
|
5054
6285
|
const optimistic = optimisticParagraphAlignment;
|
|
5055
6286
|
if (!optimistic) return resolved;
|
|
5056
|
-
const selectionSignature
|
|
5057
|
-
if (selectionSignature
|
|
6287
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6288
|
+
if (selectionSignature && selectionSignature !== optimistic.selectionSignature) {
|
|
5058
6289
|
optimisticParagraphAlignment = null;
|
|
5059
6290
|
return resolved;
|
|
5060
6291
|
}
|
|
5061
|
-
if (!selectionSignature
|
|
6292
|
+
if (!selectionSignature) return optimistic.value;
|
|
5062
6293
|
if (!optimistic.settled) return optimistic.value;
|
|
5063
6294
|
if (resolution.status !== "pending") {
|
|
5064
6295
|
optimisticParagraphAlignment = null;
|
|
@@ -5066,14 +6297,14 @@ function createSuperDocUI(options) {
|
|
|
5066
6297
|
}
|
|
5067
6298
|
return optimistic.value;
|
|
5068
6299
|
};
|
|
5069
|
-
const armOptimisticParagraphAlignment = (selection
|
|
6300
|
+
const armOptimisticParagraphAlignment = (selection, payload) => {
|
|
5070
6301
|
const alignment = normalizeParagraphAlignment(payload);
|
|
5071
|
-
const selectionSignature
|
|
5072
|
-
const blockIds = selectionBlockIds(selection
|
|
5073
|
-
if (!alignment || !selectionSignature
|
|
6302
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6303
|
+
const blockIds = selectionBlockIds(selection);
|
|
6304
|
+
if (!alignment || !selectionSignature || blockIds.length === 0) return null;
|
|
5074
6305
|
const generation = ++optimisticParagraphAlignmentGeneration;
|
|
5075
6306
|
optimisticParagraphAlignment = {
|
|
5076
|
-
selectionSignature
|
|
6307
|
+
selectionSignature,
|
|
5077
6308
|
value: alignment,
|
|
5078
6309
|
generation,
|
|
5079
6310
|
settled: false,
|
|
@@ -5096,6 +6327,16 @@ function createSuperDocUI(options) {
|
|
|
5096
6327
|
}
|
|
5097
6328
|
optimistic.settled = true;
|
|
5098
6329
|
};
|
|
6330
|
+
/**
|
|
6331
|
+
* Await a block's list membership authoritatively. Membership comes from
|
|
6332
|
+
* `isListItem`, not `seed`: linked/custom Word numbering can be a valid list
|
|
6333
|
+
* item while returning `seed: null`. Command execution must also fail closed
|
|
6334
|
+
* when membership cannot be resolved instead of assuming a plain paragraph.
|
|
6335
|
+
*
|
|
6336
|
+
* The read resolves directly instead of peeking at the async cache. Only the
|
|
6337
|
+
* first selected block is warmed by the command-state snapshot, so command
|
|
6338
|
+
* execution cannot depend on that cache being warm (SD-3659).
|
|
6339
|
+
*/
|
|
5099
6340
|
const resolveListMembershipForBlockAsync = async (doc, blockId, story) => {
|
|
5100
6341
|
const listsApi = doc?.lists;
|
|
5101
6342
|
if (!listsApi) return null;
|
|
@@ -5112,6 +6353,7 @@ function createSuperDocUI(options) {
|
|
|
5112
6353
|
return null;
|
|
5113
6354
|
}
|
|
5114
6355
|
};
|
|
6356
|
+
/** Read a list seed directly for mutation planning instead of trusting the reactive cache. */
|
|
5115
6357
|
const resolveListSeedForBlock = (doc, blockId, story) => {
|
|
5116
6358
|
const unavailable = {
|
|
5117
6359
|
resolved: false,
|
|
@@ -5140,6 +6382,7 @@ function createSuperDocUI(options) {
|
|
|
5140
6382
|
return unavailable;
|
|
5141
6383
|
}
|
|
5142
6384
|
};
|
|
6385
|
+
/** Await a block's current paragraph indentation authoritatively. */
|
|
5143
6386
|
const resolveParagraphIndentationForBlockAsync = async (doc, blockId, story) => {
|
|
5144
6387
|
if (!doc) return null;
|
|
5145
6388
|
try {
|
|
@@ -5153,7 +6396,7 @@ function createSuperDocUI(options) {
|
|
|
5153
6396
|
return null;
|
|
5154
6397
|
}
|
|
5155
6398
|
};
|
|
5156
|
-
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection
|
|
6399
|
+
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5157
6400
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5158
6401
|
enabled: false,
|
|
5159
6402
|
active,
|
|
@@ -5161,7 +6404,7 @@ function createSuperDocUI(options) {
|
|
|
5161
6404
|
value,
|
|
5162
6405
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5163
6406
|
}, "builtin");
|
|
5164
|
-
const blockIds = selectionBlockIds(selection
|
|
6407
|
+
const blockIds = selectionBlockIds(selection);
|
|
5165
6408
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5166
6409
|
enabled: false,
|
|
5167
6410
|
active,
|
|
@@ -5174,7 +6417,7 @@ function createSuperDocUI(options) {
|
|
|
5174
6417
|
const paragraphClearIndent = resolveDocOperation(doc, "format.paragraph.clearIndentation");
|
|
5175
6418
|
const mode = descriptor.list?.mode;
|
|
5176
6419
|
const paragraphOpAvailable = mode === "indent" ? paragraphSetIndent != null : paragraphSetIndent != null || paragraphClearIndent != null;
|
|
5177
|
-
const story = selectionStory(selection
|
|
6420
|
+
const story = selectionStory(selection);
|
|
5178
6421
|
let listMembership = null;
|
|
5179
6422
|
if (story.storyType !== "body") {
|
|
5180
6423
|
listMembership = [];
|
|
@@ -5258,7 +6501,7 @@ function createSuperDocUI(options) {
|
|
|
5258
6501
|
reason: unavailableRouteReason(doc)
|
|
5259
6502
|
}, "builtin");
|
|
5260
6503
|
};
|
|
5261
|
-
const computeListToggleCommandState = (descriptor, doc, readonly, selection
|
|
6504
|
+
const computeListToggleCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5262
6505
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5263
6506
|
enabled: false,
|
|
5264
6507
|
active,
|
|
@@ -5266,7 +6509,7 @@ function createSuperDocUI(options) {
|
|
|
5266
6509
|
value,
|
|
5267
6510
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5268
6511
|
}, "builtin");
|
|
5269
|
-
const blockIds = selectionBlockIds(selection
|
|
6512
|
+
const blockIds = selectionBlockIds(selection);
|
|
5270
6513
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5271
6514
|
enabled: false,
|
|
5272
6515
|
active,
|
|
@@ -5274,7 +6517,7 @@ function createSuperDocUI(options) {
|
|
|
5274
6517
|
value,
|
|
5275
6518
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
5276
6519
|
}, "builtin");
|
|
5277
|
-
const story = selectionStory(selection
|
|
6520
|
+
const story = selectionStory(selection);
|
|
5278
6521
|
const listsApi = doc?.lists;
|
|
5279
6522
|
if (!(typeof listsApi?.getStateInStory === "function" || story.storyType === "body" && typeof listsApi?.getState === "function")) return normalizeCommandState({
|
|
5280
6523
|
enabled: false,
|
|
@@ -5302,7 +6545,7 @@ function createSuperDocUI(options) {
|
|
|
5302
6545
|
supported: true,
|
|
5303
6546
|
value
|
|
5304
6547
|
}, "builtin");
|
|
5305
|
-
if (snapshots.some(({ value: state
|
|
6548
|
+
if (snapshots.some(({ value: state }) => state === null)) return normalizeCommandState({
|
|
5306
6549
|
enabled: false,
|
|
5307
6550
|
active,
|
|
5308
6551
|
supported: false,
|
|
@@ -5310,7 +6553,7 @@ function createSuperDocUI(options) {
|
|
|
5310
6553
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5311
6554
|
}, "builtin");
|
|
5312
6555
|
const seed = descriptor.list?.seed;
|
|
5313
|
-
const shouldRemove = snapshots.every(({ value: state
|
|
6556
|
+
const shouldRemove = snapshots.every(({ value: state }) => state?.isListItem === true && state.seed === seed);
|
|
5314
6557
|
if (story.storyType === "body") return normalizeCommandState(!shouldRemove || resolveDocOperation(doc, "lists.remove") != null ? {
|
|
5315
6558
|
enabled: true,
|
|
5316
6559
|
active,
|
|
@@ -5338,10 +6581,12 @@ function createSuperDocUI(options) {
|
|
|
5338
6581
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5339
6582
|
}, "builtin");
|
|
5340
6583
|
};
|
|
5341
|
-
|
|
5342
|
-
|
|
6584
|
+
/** Read the active hyperlink's href overlapping the selection, when present. */
|
|
6585
|
+
const readActiveLinkHref = (doc, selection) => {
|
|
6586
|
+
const href = (resolveCurrentHyperlink(doc, selection)?.properties)?.href;
|
|
5343
6587
|
return typeof href === "string" ? href : null;
|
|
5344
6588
|
};
|
|
6589
|
+
/** State for a command routed through a public SuperDoc-instance method. */
|
|
5345
6590
|
const computeInstanceCommandState = (descriptor) => {
|
|
5346
6591
|
if (!getEditor()) return normalizeCommandState({
|
|
5347
6592
|
enabled: false,
|
|
@@ -5363,12 +6608,14 @@ function createSuperDocUI(options) {
|
|
|
5363
6608
|
value: descriptorValue(descriptor)
|
|
5364
6609
|
}, "builtin");
|
|
5365
6610
|
};
|
|
6611
|
+
/** Live active state for a host-owned chrome control (`ruler`, `formatting-marks`). */
|
|
5366
6612
|
const chromeActiveState = (descriptor) => {
|
|
5367
6613
|
const config = superdoc?.config ?? void 0;
|
|
5368
6614
|
if (descriptor.valueFrom === "ruler") return Boolean(config?.rulers);
|
|
5369
6615
|
if (descriptor.valueFrom === "formattingMarks") return Boolean((config?.layoutEngineOptions)?.showFormattingMarks);
|
|
5370
6616
|
return false;
|
|
5371
6617
|
};
|
|
6618
|
+
/** State for a table cell-context command routed through `tables.*`. */
|
|
5372
6619
|
const computeTableCommandState = (descriptor, doc, readonly) => {
|
|
5373
6620
|
if (readonly) return normalizeCommandState({
|
|
5374
6621
|
enabled: false,
|
|
@@ -5395,12 +6642,14 @@ function createSuperDocUI(options) {
|
|
|
5395
6642
|
supported: true
|
|
5396
6643
|
}, "builtin");
|
|
5397
6644
|
};
|
|
6645
|
+
/** Resolve a descriptor's live `value` from public controller state, when modeled. */
|
|
5398
6646
|
const descriptorValue = (descriptor) => {
|
|
5399
6647
|
if (descriptor.valueFrom === "zoom") return computeZoom().value;
|
|
5400
6648
|
if (descriptor.valueFrom === "documentMode") return readDocumentMode();
|
|
5401
6649
|
if (descriptor.valueFrom === "measurementUnit") return readMeasurementUnit();
|
|
5402
6650
|
if (descriptor.valueFrom === "ruler" || descriptor.valueFrom === "formattingMarks") return chromeActiveState(descriptor);
|
|
5403
6651
|
};
|
|
6652
|
+
/** Validate normalized payloads before invoking public SuperDoc-instance methods. */
|
|
5404
6653
|
const instanceCommandPayloadIsValid = (descriptor, payload) => {
|
|
5405
6654
|
if (descriptor.id === "zoom") return typeof payload === "number" && Number.isFinite(payload) && payload > 0;
|
|
5406
6655
|
if (descriptor.id === "document-mode") return payload === "editing" || payload === "suggesting" || payload === "viewing";
|
|
@@ -5408,7 +6657,7 @@ function createSuperDocUI(options) {
|
|
|
5408
6657
|
return true;
|
|
5409
6658
|
};
|
|
5410
6659
|
const allCommandIds = () => {
|
|
5411
|
-
return [
|
|
6660
|
+
return [.../* @__PURE__ */ new Set([...ALL_BUILT_IN_COMMAND_IDS, ...customCommands.keys()])];
|
|
5412
6661
|
};
|
|
5413
6662
|
const DEFAULT_PARAGRAPH_STYLE_ID = "Normal";
|
|
5414
6663
|
const EMPTY_STYLES_SLICE = {
|
|
@@ -5422,6 +6671,14 @@ function createSuperDocUI(options) {
|
|
|
5422
6671
|
sourceStatus: null,
|
|
5423
6672
|
diagnostics: []
|
|
5424
6673
|
};
|
|
6674
|
+
/**
|
|
6675
|
+
* Read the public Document API style catalogue (`doc.styles.getCatalog`)
|
|
6676
|
+
* through the async read coordinator. A promise-returning browser read
|
|
6677
|
+
* settles into the cache (keyed by content revision) instead of collapsing
|
|
6678
|
+
* the catalogue to `null`. `value` is `null` when the styles surface is
|
|
6679
|
+
* unreachable (worker-backed `doc` is null or the operation is missing); the
|
|
6680
|
+
* accompanying {@link SliceStatus} reports pending/stale/ready.
|
|
6681
|
+
*/
|
|
5425
6682
|
const readStyleCatalogLive = (input) => {
|
|
5426
6683
|
const stylesApi = getDoc()?.styles;
|
|
5427
6684
|
if (!stylesApi || typeof stylesApi.getCatalog !== "function") return {
|
|
@@ -5430,6 +6687,7 @@ function createSuperDocUI(options) {
|
|
|
5430
6687
|
};
|
|
5431
6688
|
return readAsync(`styles:catalog:${JSON.stringify(input ?? {})}`, contentToken(), () => stylesApi.getCatalog(input), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5432
6689
|
};
|
|
6690
|
+
/** Resolve the catalogue (coordinator-cached by content revision) plus its readiness. */
|
|
5433
6691
|
const getStyleCatalog = () => {
|
|
5434
6692
|
const full = readStyleCatalogLive({ includePreview: true });
|
|
5435
6693
|
if (!full.value) return {
|
|
@@ -5452,6 +6710,13 @@ function createSuperDocUI(options) {
|
|
|
5452
6710
|
status: combineStatus(full.status, quickResult.status)
|
|
5453
6711
|
};
|
|
5454
6712
|
};
|
|
6713
|
+
/**
|
|
6714
|
+
* Read one block's paragraph `styleRef` through the public `getNodeById`
|
|
6715
|
+
* read. `ok` distinguishes a successful read (styleRef may be null when the
|
|
6716
|
+
* paragraph has no explicit style) from an unavailable read (no operation, a
|
|
6717
|
+
* promise in worker mode, or a missing node) so active-style resolution can
|
|
6718
|
+
* fail closed precisely.
|
|
6719
|
+
*/
|
|
5455
6720
|
const readBlockStyleRef = (doc, blockId, story) => {
|
|
5456
6721
|
const { value: result, status } = readNodeById(doc, blockId, story);
|
|
5457
6722
|
if (!result) return {
|
|
@@ -5474,7 +6739,13 @@ function createSuperDocUI(options) {
|
|
|
5474
6739
|
status
|
|
5475
6740
|
};
|
|
5476
6741
|
};
|
|
5477
|
-
|
|
6742
|
+
/**
|
|
6743
|
+
* Resolve the active paragraph style from the current selection. Handles the
|
|
6744
|
+
* uniform, default (no explicit style → document default paragraph style),
|
|
6745
|
+
* and mixed cases, and fails closed with diagnostics when block reads are
|
|
6746
|
+
* unavailable.
|
|
6747
|
+
*/
|
|
6748
|
+
const computeActiveParagraphStyle = (selection, catalog) => {
|
|
5478
6749
|
const diagnostics = [];
|
|
5479
6750
|
const doc = getDoc();
|
|
5480
6751
|
if (!doc) {
|
|
@@ -5493,7 +6764,7 @@ function createSuperDocUI(options) {
|
|
|
5493
6764
|
status: "ready"
|
|
5494
6765
|
};
|
|
5495
6766
|
}
|
|
5496
|
-
const blockIds = selectionBlockIds(selection
|
|
6767
|
+
const blockIds = selectionBlockIds(selection);
|
|
5497
6768
|
if (blockIds.length === 0) {
|
|
5498
6769
|
diagnostics.push({
|
|
5499
6770
|
severity: "info",
|
|
@@ -5532,7 +6803,7 @@ function createSuperDocUI(options) {
|
|
|
5532
6803
|
let blockReadFailures = 0;
|
|
5533
6804
|
let defaultReadFailures = 0;
|
|
5534
6805
|
let readStatus = "ready";
|
|
5535
|
-
const story = selectionStory(selection
|
|
6806
|
+
const story = selectionStory(selection);
|
|
5536
6807
|
for (const blockId of blockIds) {
|
|
5537
6808
|
const read = readBlockStyleRef(doc, blockId, story);
|
|
5538
6809
|
readStatus = combineStatus(readStatus, read.status);
|
|
@@ -5544,13 +6815,13 @@ function createSuperDocUI(options) {
|
|
|
5544
6815
|
defaultReadFailures += 1;
|
|
5545
6816
|
continue;
|
|
5546
6817
|
}
|
|
5547
|
-
const styleId
|
|
5548
|
-
if (!styleId
|
|
6818
|
+
const styleId = read.styleRef ?? defaultId;
|
|
6819
|
+
if (!styleId) {
|
|
5549
6820
|
defaultReadFailures += 1;
|
|
5550
6821
|
continue;
|
|
5551
6822
|
}
|
|
5552
6823
|
resolved += 1;
|
|
5553
|
-
resolvedIds.add(styleId
|
|
6824
|
+
resolvedIds.add(styleId);
|
|
5554
6825
|
}
|
|
5555
6826
|
if (resolved === 0) {
|
|
5556
6827
|
const onlyDefaultUnavailable = defaultReadFailures > 0 && blockReadFailures === 0;
|
|
@@ -5606,10 +6877,10 @@ function createSuperDocUI(options) {
|
|
|
5606
6877
|
status: readStatus
|
|
5607
6878
|
};
|
|
5608
6879
|
};
|
|
5609
|
-
const computeStyles = (selection
|
|
6880
|
+
const computeStyles = (selection) => {
|
|
5610
6881
|
if (!getEditor()) return EMPTY_STYLES_SLICE;
|
|
5611
6882
|
const { cache: catalog, status: catalogStatus } = getStyleCatalog();
|
|
5612
|
-
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection
|
|
6883
|
+
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection, catalog);
|
|
5613
6884
|
let diagnostics;
|
|
5614
6885
|
if (catalog) diagnostics = active.diagnostics.length === 0 ? catalog.full.diagnostics : [...catalog.full.diagnostics, ...active.diagnostics];
|
|
5615
6886
|
else diagnostics = [{
|
|
@@ -5619,7 +6890,7 @@ function createSuperDocUI(options) {
|
|
|
5619
6890
|
}, ...active.diagnostics];
|
|
5620
6891
|
return {
|
|
5621
6892
|
ready: true,
|
|
5622
|
-
status: combineStatus(catalogStatus, activeStatus, selection
|
|
6893
|
+
status: combineStatus(catalogStatus, activeStatus, selection.status),
|
|
5623
6894
|
catalogRevision: catalog?.full.revision ?? null,
|
|
5624
6895
|
quickGallery: catalog?.quickGallery ?? [],
|
|
5625
6896
|
activeParagraphStyleId: active.styleId,
|
|
@@ -5631,32 +6902,32 @@ function createSuperDocUI(options) {
|
|
|
5631
6902
|
};
|
|
5632
6903
|
const computeState = () => {
|
|
5633
6904
|
const documentMode = readDocumentMode();
|
|
5634
|
-
const selection
|
|
5635
|
-
if (selection
|
|
5636
|
-
reconcilePendingInlineFormat(selection
|
|
5637
|
-
const selectionSignature
|
|
5638
|
-
for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature
|
|
5639
|
-
else if (optimistic.settled && selection
|
|
6905
|
+
const selection = computeSelection();
|
|
6906
|
+
if (selection.status === "ready") pruneSelectionScopedAsyncReads(selection);
|
|
6907
|
+
reconcilePendingInlineFormat(selection);
|
|
6908
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
6909
|
+
for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature || optimistic.selectionSignature !== selectionSignature) optimisticInlineToggles.delete(commandId);
|
|
6910
|
+
else if (optimistic.settled && selection.status === "ready") optimisticInlineToggles.delete(commandId);
|
|
5640
6911
|
return {
|
|
5641
6912
|
ready: getEditor() != null,
|
|
5642
6913
|
documentMode,
|
|
5643
6914
|
document: computeDocument(),
|
|
5644
|
-
selection
|
|
6915
|
+
selection,
|
|
5645
6916
|
toolbar: {
|
|
5646
6917
|
context: documentMode,
|
|
5647
|
-
commands: computeCommandStates(selection
|
|
6918
|
+
commands: computeCommandStates(selection),
|
|
5648
6919
|
copyFormatActive: painter.mode !== "idle"
|
|
5649
6920
|
},
|
|
5650
|
-
comments: computeComments(selection
|
|
5651
|
-
trackChanges: computeTrackChanges(selection
|
|
5652
|
-
contentControls: computeContentControls(selection
|
|
6921
|
+
comments: computeComments(selection),
|
|
6922
|
+
trackChanges: computeTrackChanges(selection),
|
|
6923
|
+
contentControls: computeContentControls(selection),
|
|
5653
6924
|
zoom: computeZoom(),
|
|
5654
6925
|
fonts: computeFonts(),
|
|
5655
|
-
styles: computeStyles(selection
|
|
6926
|
+
styles: computeStyles(selection)
|
|
5656
6927
|
};
|
|
5657
6928
|
};
|
|
5658
|
-
const syncOptimisticInlineSelection = (selection
|
|
5659
|
-
const nextSignature = selectionInlineValueSignature(selection
|
|
6929
|
+
const syncOptimisticInlineSelection = (selection) => {
|
|
6930
|
+
const nextSignature = selectionInlineValueSignature(selection);
|
|
5660
6931
|
if (nextSignature && lastOptimisticInlineSelectionSignature && nextSignature !== lastOptimisticInlineSelectionSignature) {
|
|
5661
6932
|
optimisticInlineValues.clear();
|
|
5662
6933
|
optimisticInlineToggles.clear();
|
|
@@ -5704,8 +6975,8 @@ function createSuperDocUI(options) {
|
|
|
5704
6975
|
let detachDocumentSelection = null;
|
|
5705
6976
|
const readHostSelectionSource = () => {
|
|
5706
6977
|
const host = getEditor()?.host;
|
|
5707
|
-
const selection
|
|
5708
|
-
return selection
|
|
6978
|
+
const selection = safeCall(host?.getHandles ? () => host.getHandles() : void 0, null)?.editing?.selection;
|
|
6979
|
+
return selection && typeof selection.subscribe === "function" ? selection : null;
|
|
5709
6980
|
};
|
|
5710
6981
|
const syncHostSelectionSubscription = () => {
|
|
5711
6982
|
const next = readHostSelectionSource();
|
|
@@ -5777,7 +7048,8 @@ function createSuperDocUI(options) {
|
|
|
5777
7048
|
try {
|
|
5778
7049
|
const unsubscribe = next.subscribe((snapshot) => {
|
|
5779
7050
|
const snapshotRecord = snapshot && typeof snapshot === "object" ? snapshot : null;
|
|
5780
|
-
|
|
7051
|
+
const target = snapshotRecord && Object.prototype.hasOwnProperty.call(snapshotRecord, "activeReviewTarget") ? snapshotRecord.activeReviewTarget : readHostActiveReviewTarget(next);
|
|
7052
|
+
if (!syncTrackedChangeFocusFromHostReviewTarget(target, snapshot) || attaching) return;
|
|
5781
7053
|
recompute();
|
|
5782
7054
|
});
|
|
5783
7055
|
if (typeof unsubscribe === "function") detachHostReview = unsubscribe;
|
|
@@ -5833,6 +7105,20 @@ function createSuperDocUI(options) {
|
|
|
5833
7105
|
try {
|
|
5834
7106
|
const off = next.subscribe((event) => {
|
|
5835
7107
|
const type = event?.type;
|
|
7108
|
+
if (type === "review-mutation:started") {
|
|
7109
|
+
beginUiReviewMutation(event.reviewMutation?.token);
|
|
7110
|
+
return;
|
|
7111
|
+
}
|
|
7112
|
+
if (type === "review-mutation:aborted") {
|
|
7113
|
+
settleUiReviewMutation(event.reviewMutation?.token);
|
|
7114
|
+
scheduleAsyncRefresh();
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
if (type === "mutation:rejected" && event.reviewMutation) {
|
|
7118
|
+
settleUiReviewMutation(event.reviewMutation.token);
|
|
7119
|
+
scheduleAsyncRefresh();
|
|
7120
|
+
}
|
|
7121
|
+
if (type === "mutation:committed" && event.reviewMutation) settleUiReviewMutation(event.reviewMutation.token);
|
|
5836
7122
|
if (type === "source:complete" || type === "source:signals-complete") {
|
|
5837
7123
|
if (authoritativeTrackChangesPendingToken !== null) {
|
|
5838
7124
|
clearAuthoritativeTrackChangesRetry();
|
|
@@ -5848,6 +7134,13 @@ function createSuperDocUI(options) {
|
|
|
5848
7134
|
if (type === "mutation:committed" || type === "save:completed" || type === "collaboration:remote-changed") {
|
|
5849
7135
|
const impact = type === "mutation:committed" ? getV2TrackedChangeMutationImpact(event) : null;
|
|
5850
7136
|
const supersedesPendingReviewReconcile = type === "mutation:committed" && reviewMutationReconciler.getPendingIds().size > 0;
|
|
7137
|
+
if (impact?.allResolved) {
|
|
7138
|
+
reviewMutationReconciler.reset();
|
|
7139
|
+
replaceTrackedChangeItemsInCache([]);
|
|
7140
|
+
authoritativeTrackChangesPendingToken = null;
|
|
7141
|
+
publishAllTrackedChangesResolved(event.receipt);
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
5851
7144
|
if (impact?.removedIds.size) markPostDecisionTrackChanges(new Set(impact.removedIds), event.receipt);
|
|
5852
7145
|
const authoritativeHistoryImpact = impact?.reconcileMode === "authoritative" && impact.upsertIds.size > 0;
|
|
5853
7146
|
if (authoritativeHistoryImpact) {
|
|
@@ -5910,7 +7203,29 @@ function createSuperDocUI(options) {
|
|
|
5910
7203
|
state = nextState;
|
|
5911
7204
|
for (const listener of [...listeners]) listener(state);
|
|
5912
7205
|
};
|
|
7206
|
+
/**
|
|
7207
|
+
* Work that is bound to one specific active editor and cannot survive a swap.
|
|
7208
|
+
* Recomputing aggregate state is not enough for these: a geometry
|
|
7209
|
+
* subscription points at a DOM host that is going away, and an in-flight
|
|
7210
|
+
* async query would publish the old document's result into the new one.
|
|
7211
|
+
*
|
|
7212
|
+
* Populated by the viewport and search handles further down. The handler only
|
|
7213
|
+
* runs on a host event, which cannot happen during construction, so
|
|
7214
|
+
* registering later than `attach` is safe.
|
|
7215
|
+
*/
|
|
5913
7216
|
const activeEditorResetHooks = [];
|
|
7217
|
+
/**
|
|
7218
|
+
* The subset bound to the DOCUMENT rather than to the editor.
|
|
7219
|
+
*
|
|
7220
|
+
* `replaceFile()` swaps content in place: the editor object and its host both
|
|
7221
|
+
* survive, so a geometry subscription is still attached to the thing now
|
|
7222
|
+
* rendering the replacement and must be left alone. Search state describes
|
|
7223
|
+
* content that is gone and must not be.
|
|
7224
|
+
*
|
|
7225
|
+
* Measured rather than assumed — after an in-place replace the geometry
|
|
7226
|
+
* subscription is never detached and keeps firing, while `search.total` still
|
|
7227
|
+
* reports the previous document's match count.
|
|
7228
|
+
*/
|
|
5914
7229
|
const documentResetHooks = [];
|
|
5915
7230
|
const runHooks = (hooks) => {
|
|
5916
7231
|
for (const reset of hooks) try {
|
|
@@ -5973,6 +7288,20 @@ function createSuperDocUI(options) {
|
|
|
5973
7288
|
};
|
|
5974
7289
|
};
|
|
5975
7290
|
const sliceHandle = (selector) => select(selector);
|
|
7291
|
+
/**
|
|
7292
|
+
* Wrap an underlying selector {@link Subscribable} (raw `get` + value
|
|
7293
|
+
* `subscribe`) in the main-compatible domain-handle subscription shape so
|
|
7294
|
+
* every customer-facing handle exposes the same contract main does:
|
|
7295
|
+
*
|
|
7296
|
+
* - `getSnapshot()` reads the current slice;
|
|
7297
|
+
* - `observe(listener)` emits the current value immediately, then on each
|
|
7298
|
+
* change (the listener receives the snapshot directly);
|
|
7299
|
+
* - `subscribe(listener)` is the `{ snapshot }`-shaped alias of `observe`,
|
|
7300
|
+
* likewise emitting immediately then on change.
|
|
7301
|
+
*
|
|
7302
|
+
* The generic `select(...)` substrate stays raw-value and unchanged; only the
|
|
7303
|
+
* domain handles are composed from this.
|
|
7304
|
+
*/
|
|
5976
7305
|
const snapshotHandle = (sub) => {
|
|
5977
7306
|
const observe = (listener) => {
|
|
5978
7307
|
const safe = (snapshot) => {
|
|
@@ -5994,8 +7323,8 @@ function createSuperDocUI(options) {
|
|
|
5994
7323
|
function readTrackDecisionTargetId(target) {
|
|
5995
7324
|
return target && typeof target === "object" && typeof target.id === "string" ? String(target.id) : null;
|
|
5996
7325
|
}
|
|
5997
|
-
function nonBodySelectionStoryLocator(selection
|
|
5998
|
-
const story = selectionStoryLocator(selection
|
|
7326
|
+
function nonBodySelectionStoryLocator(selection) {
|
|
7327
|
+
const story = selectionStoryLocator(selection);
|
|
5999
7328
|
return story && storyLocatorSignature(story) !== storyLocatorSignature(null) ? story : void 0;
|
|
6000
7329
|
}
|
|
6001
7330
|
function trackDecisionIdTarget(changeId, story) {
|
|
@@ -6005,15 +7334,24 @@ function createSuperDocUI(options) {
|
|
|
6005
7334
|
...story ? { story } : {}
|
|
6006
7335
|
};
|
|
6007
7336
|
}
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
7337
|
+
/**
|
|
7338
|
+
* Build the Document API range target for a partial tracked-change decision.
|
|
7339
|
+
* The explicit selection target is authoritative and labels its coordinates:
|
|
7340
|
+
* ordinary/insertion selections are visible-space, while a selection that
|
|
7341
|
+
* reaches painted deleted text is projected in tracked space by the host.
|
|
7342
|
+
*
|
|
7343
|
+
* A cross-block or malformed selection fails closed. Its intermediate block
|
|
7344
|
+
* coverage cannot be reconstructed from two endpoints without guessing.
|
|
7345
|
+
*/
|
|
7346
|
+
function trackDecisionRangeFromSelection(selection) {
|
|
7347
|
+
if (selection.empty) return null;
|
|
7348
|
+
const explicit = selection.selectionTarget;
|
|
6011
7349
|
const start = explicit?.start;
|
|
6012
7350
|
const end = explicit?.end;
|
|
6013
7351
|
if (explicit?.kind !== "selection" || start?.kind !== "text" || end?.kind !== "text" || typeof start.blockId !== "string" || start.blockId !== end.blockId || !Number.isInteger(start.offset) || !Number.isInteger(end.offset) || start.offset < 0 || end.offset < 0 || start.offset === end.offset) return null;
|
|
6014
7352
|
const rangeStart = Math.min(start.offset, end.offset);
|
|
6015
7353
|
const rangeEnd = Math.max(start.offset, end.offset);
|
|
6016
|
-
const story = selectionStoryLocator(selection
|
|
7354
|
+
const story = selectionStoryLocator(selection);
|
|
6017
7355
|
return {
|
|
6018
7356
|
kind: "range",
|
|
6019
7357
|
coordinateSpace: explicit.coordinateSpace === "tracked" ? "tracked" : "visible",
|
|
@@ -6032,17 +7370,17 @@ function createSuperDocUI(options) {
|
|
|
6032
7370
|
}
|
|
6033
7371
|
function readBodyTrackChangeForDecision(activeId, story) {
|
|
6034
7372
|
if (storyLocatorSignature(story) !== storyLocatorSignature(null)) return void 0;
|
|
6035
|
-
const trackChanges
|
|
6036
|
-
const list = trackChanges
|
|
7373
|
+
const trackChanges = getDoc()?.trackChanges;
|
|
7374
|
+
const list = trackChanges?.list;
|
|
6037
7375
|
if (typeof list !== "function") return void 0;
|
|
6038
|
-
const raw = safeCall(() => list.call(trackChanges
|
|
7376
|
+
const raw = safeCall(() => list.call(trackChanges), null);
|
|
6039
7377
|
if (isPromiseLike(raw) || !raw || typeof raw !== "object") return void 0;
|
|
6040
7378
|
const items = raw.items;
|
|
6041
7379
|
if (!Array.isArray(items)) return void 0;
|
|
6042
7380
|
return items.map(projectTrackChangesItem).find((item) => item != null && entityRowMatchesRequest(item, activeId, story));
|
|
6043
7381
|
}
|
|
6044
|
-
function selectedTextCoversTrackChangeText(selection
|
|
6045
|
-
const selected = normalizeTrackDecisionText(selection
|
|
7382
|
+
function selectedTextCoversTrackChangeText(selection, item) {
|
|
7383
|
+
const selected = normalizeTrackDecisionText(selection.quotedText);
|
|
6046
7384
|
if (!selected) return false;
|
|
6047
7385
|
const payload = trackChangesItemPayload(item);
|
|
6048
7386
|
return [
|
|
@@ -6074,14 +7412,15 @@ function createSuperDocUI(options) {
|
|
|
6074
7412
|
if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
|
|
6075
7413
|
return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
|
|
6076
7414
|
}));
|
|
6077
|
-
|
|
7415
|
+
const commandResult = commandResultFromOperationResult(result);
|
|
7416
|
+
return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
|
|
6078
7417
|
} catch {
|
|
6079
7418
|
return false;
|
|
6080
7419
|
}
|
|
6081
7420
|
}
|
|
6082
7421
|
function resolveTrackDecisionTarget(command, payload) {
|
|
6083
7422
|
if (command.scope === "all") return {
|
|
6084
|
-
target: {
|
|
7423
|
+
target: { kind: "all" },
|
|
6085
7424
|
changeId: null
|
|
6086
7425
|
};
|
|
6087
7426
|
if (typeof payload === "string" && payload.length > 0) return {
|
|
@@ -6217,8 +7556,8 @@ function createSuperDocUI(options) {
|
|
|
6217
7556
|
const scheduleInlineToggleMutation = (key, mutate) => {
|
|
6218
7557
|
if (!inlineToggleMutationActive) {
|
|
6219
7558
|
inlineToggleMutationActive = true;
|
|
6220
|
-
inlineToggleMutationIdle = new Promise((resolve
|
|
6221
|
-
resolveInlineToggleMutationIdle = resolve
|
|
7559
|
+
inlineToggleMutationIdle = new Promise((resolve) => {
|
|
7560
|
+
resolveInlineToggleMutationIdle = resolve;
|
|
6222
7561
|
});
|
|
6223
7562
|
try {
|
|
6224
7563
|
return {
|
|
@@ -6255,20 +7594,21 @@ function createSuperDocUI(options) {
|
|
|
6255
7594
|
release: releaseInlineToggleMutationOnce()
|
|
6256
7595
|
};
|
|
6257
7596
|
};
|
|
6258
|
-
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = new Set(["create.tableOfContents"]);
|
|
7597
|
+
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = /* @__PURE__ */ new Set(["create.tableOfContents"]);
|
|
6259
7598
|
const unsupportedTrackedMutationReceipt = (route) => failedReceipt(`Tracked authoring is not supported for ${route}.`);
|
|
6260
7599
|
const editorMutationOptionsForRoute = (route) => {
|
|
6261
7600
|
if (readDocumentMode() !== "suggesting") return void 0;
|
|
6262
7601
|
if (UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES.has(route)) return unsupportedTrackedMutationReceipt(route);
|
|
6263
7602
|
return { changeMode: "tracked" };
|
|
6264
7603
|
};
|
|
7604
|
+
/** Clear-patch to send for the current document mode (see TRACKED_CLEAR_INLINE_PATCH). */
|
|
6265
7605
|
const clearInlinePatchForMode = () => readDocumentMode() === "suggesting" ? TRACKED_CLEAR_INLINE_PATCH : CLEAR_INLINE_PATCH;
|
|
6266
7606
|
const callEditorMutation = (route, op, input) => {
|
|
6267
|
-
const options
|
|
6268
|
-
if (options
|
|
6269
|
-
return options
|
|
7607
|
+
const options = editorMutationOptionsForRoute(route);
|
|
7608
|
+
if (options && options.success === false) return options;
|
|
7609
|
+
return options ? op(input, options) : op(input);
|
|
6270
7610
|
};
|
|
6271
|
-
const captureOptimisticInlineValueResult = (descriptor, selection
|
|
7611
|
+
const captureOptimisticInlineValueResult = (descriptor, selection, input, result) => {
|
|
6272
7612
|
const inline = descriptor.inline;
|
|
6273
7613
|
if (!inline) return;
|
|
6274
7614
|
const commandResult = commandResultFromOperationResult(result);
|
|
@@ -6279,35 +7619,35 @@ function createSuperDocUI(options) {
|
|
|
6279
7619
|
return;
|
|
6280
7620
|
}
|
|
6281
7621
|
if (inline.kind === "toggle" || !isProjectedInlineSelectionValueKey(inline.key)) return;
|
|
6282
|
-
const selectionSignature
|
|
6283
|
-
if (!selectionSignature
|
|
7622
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7623
|
+
if (!selectionSignature) return;
|
|
6284
7624
|
const value = normalizeOptimisticInlineSelectionValue(inline.key, input.value);
|
|
6285
7625
|
if (value == null) {
|
|
6286
7626
|
optimisticInlineValues.delete(inline.key);
|
|
6287
7627
|
return;
|
|
6288
7628
|
}
|
|
6289
7629
|
optimisticInlineValues.set(inline.key, {
|
|
6290
|
-
selectionSignature
|
|
7630
|
+
selectionSignature,
|
|
6291
7631
|
value
|
|
6292
7632
|
});
|
|
6293
7633
|
};
|
|
6294
|
-
const decorateInlineCommandResult = (descriptor, selection
|
|
7634
|
+
const decorateInlineCommandResult = (descriptor, selection, input, result) => {
|
|
6295
7635
|
if (!descriptor.inline || descriptor.inline.kind === "toggle") return result;
|
|
6296
7636
|
if (isPromiseLike(result)) return Promise.resolve(result).then((resolved) => {
|
|
6297
|
-
captureOptimisticInlineValueResult(descriptor, selection
|
|
7637
|
+
captureOptimisticInlineValueResult(descriptor, selection, input, resolved);
|
|
6298
7638
|
return resolved;
|
|
6299
7639
|
});
|
|
6300
7640
|
return result;
|
|
6301
7641
|
};
|
|
6302
|
-
const captureOptimisticInlineToggle = (descriptor, selection
|
|
7642
|
+
const captureOptimisticInlineToggle = (descriptor, selection, input, result) => {
|
|
6303
7643
|
if (descriptor.inline?.kind !== "toggle") return null;
|
|
6304
7644
|
if (!commandResultSucceeded(commandResultFromOperationResult(result))) return null;
|
|
6305
7645
|
if (typeof input.value !== "boolean" && input.value !== null) return null;
|
|
6306
|
-
const selectionSignature
|
|
6307
|
-
if (!selectionSignature
|
|
7646
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7647
|
+
if (!selectionSignature) return null;
|
|
6308
7648
|
const generation = ++optimisticInlineToggleGeneration;
|
|
6309
7649
|
optimisticInlineToggles.set(descriptor.id, {
|
|
6310
|
-
selectionSignature
|
|
7650
|
+
selectionSignature,
|
|
6311
7651
|
active: input.value === true,
|
|
6312
7652
|
generation,
|
|
6313
7653
|
settled: false
|
|
@@ -6327,13 +7667,11 @@ function createSuperDocUI(options) {
|
|
|
6327
7667
|
};
|
|
6328
7668
|
const finalizeCommandSettlement = (promise, onSettled) => promise.then((settled) => {
|
|
6329
7669
|
onSettled?.(settled);
|
|
6330
|
-
|
|
6331
|
-
recompute();
|
|
7670
|
+
invalidateAfterCommandSettlement();
|
|
6332
7671
|
return settled;
|
|
6333
7672
|
}, () => {
|
|
6334
7673
|
onSettled?.(false);
|
|
6335
|
-
|
|
6336
|
-
recompute();
|
|
7674
|
+
invalidateAfterCommandSettlement();
|
|
6337
7675
|
return false;
|
|
6338
7676
|
});
|
|
6339
7677
|
const settleCommandExecution = (result, onSettled) => {
|
|
@@ -6356,8 +7694,7 @@ function createSuperDocUI(options) {
|
|
|
6356
7694
|
}
|
|
6357
7695
|
onSettled?.(settled);
|
|
6358
7696
|
lastCommandSettlement = Promise.resolve(settled);
|
|
6359
|
-
|
|
6360
|
-
recompute();
|
|
7697
|
+
invalidateAfterCommandSettlement();
|
|
6361
7698
|
return settled;
|
|
6362
7699
|
};
|
|
6363
7700
|
const normalizeWorkflowReceipt = (value, fallback) => {
|
|
@@ -6378,6 +7715,11 @@ function createSuperDocUI(options) {
|
|
|
6378
7715
|
recompute();
|
|
6379
7716
|
return normalizedReceipt;
|
|
6380
7717
|
};
|
|
7718
|
+
/**
|
|
7719
|
+
* Apply a Document API operation once per covered block, building the input
|
|
7720
|
+
* from the block id. Returns the last success receipt (or `true`), else the
|
|
7721
|
+
* last result (or `false`). Recompute is the caller's responsibility.
|
|
7722
|
+
*/
|
|
6381
7723
|
const applyPerBlock = (route, op, blockIds, buildInput) => {
|
|
6382
7724
|
const immediateResults = [];
|
|
6383
7725
|
const settledResults = [];
|
|
@@ -6396,6 +7738,7 @@ function createSuperDocUI(options) {
|
|
|
6396
7738
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6397
7739
|
return combineCommandResults(immediateResults);
|
|
6398
7740
|
};
|
|
7741
|
+
/** Apply a Document API operation once per covered text segment. */
|
|
6399
7742
|
const applyPerTextAddress = (route, op, targets, buildInput) => {
|
|
6400
7743
|
const immediateResults = [];
|
|
6401
7744
|
const settledResults = [];
|
|
@@ -6414,15 +7757,25 @@ function createSuperDocUI(options) {
|
|
|
6414
7757
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6415
7758
|
return combineCommandResults(immediateResults);
|
|
6416
7759
|
};
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
7760
|
+
/**
|
|
7761
|
+
* Call an inline `format.*` operation against the live selection. Mirrors
|
|
7762
|
+
* `callEditorMutation` (suggesting mode adds `changeMode: 'tracked'`), and
|
|
7763
|
+
* when the target is the host's own editable `selectionTarget` it also
|
|
7764
|
+
* carries the PRIVATE V2 option `offsetSpace: 'selection'` (browser
|
|
7765
|
+
* selection offsets count an inline object as one caret position). The
|
|
7766
|
+
* option is not part of public `MutationOptions`; this is the same cast
|
|
7767
|
+
* pattern the delete/replace callers use.
|
|
7768
|
+
*/
|
|
7769
|
+
const callInlineFormatMutation = (route, op, input, selection = state.selection) => {
|
|
7770
|
+
const options = editorMutationOptionsForRoute(route);
|
|
7771
|
+
if (options && options.success === false) return options;
|
|
7772
|
+
if (!selection.selectionTarget) return options ? op(input, options) : op(input);
|
|
6421
7773
|
return op(input, {
|
|
6422
|
-
...options
|
|
7774
|
+
...options ?? {},
|
|
6423
7775
|
offsetSpace: "selection"
|
|
6424
7776
|
});
|
|
6425
7777
|
};
|
|
7778
|
+
/** Build the `format.paragraph.*` / `styles.paragraph.*` input for a block. */
|
|
6426
7779
|
const buildBlockParagraphInput = (spec, blockId, payload, story) => {
|
|
6427
7780
|
const target = paragraphTarget(blockId, story);
|
|
6428
7781
|
switch (spec.kind) {
|
|
@@ -6467,6 +7820,14 @@ function createSuperDocUI(options) {
|
|
|
6467
7820
|
default: return null;
|
|
6468
7821
|
}
|
|
6469
7822
|
};
|
|
7823
|
+
/**
|
|
7824
|
+
* Store a caret pick as a pending mark (SD-3654/SD-3652) so the next typed text
|
|
7825
|
+
* carries it. Mirrors ProseMirror `storedMarks`: a toggle flips the stored
|
|
7826
|
+
* value against the current active state; a value command (color/font/size)
|
|
7827
|
+
* stores the value or clears it on a null/empty ("None") payload; a clear
|
|
7828
|
+
* command drops all stored marks. The host applies the store to the created
|
|
7829
|
+
* span on the next insert; settleCommandExecution drives the recompute.
|
|
7830
|
+
*/
|
|
6470
7831
|
const storePendingInlineFormat = (descriptor, payload) => {
|
|
6471
7832
|
const spec = descriptor.inline;
|
|
6472
7833
|
const method = inlineFormatMethod(descriptor);
|
|
@@ -6504,9 +7865,9 @@ function createSuperDocUI(options) {
|
|
|
6504
7865
|
immediateResults.push(settled.immediate);
|
|
6505
7866
|
settledResults.push(settled.settled);
|
|
6506
7867
|
};
|
|
6507
|
-
const run = (op, input, options
|
|
7868
|
+
const run = (op, input, options) => {
|
|
6508
7869
|
try {
|
|
6509
|
-
record(options
|
|
7870
|
+
record(options ? op(input, options) : op(input));
|
|
6510
7871
|
} catch {
|
|
6511
7872
|
immediateResults.push(false);
|
|
6512
7873
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6565,6 +7926,7 @@ function createSuperDocUI(options) {
|
|
|
6565
7926
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6566
7927
|
return combineCommandResults(immediateResults.length ? immediateResults : [false]);
|
|
6567
7928
|
};
|
|
7929
|
+
/** Route a block-level paragraph command (`format.paragraph.*` / `styles.paragraph.*`). */
|
|
6568
7930
|
const executeBlockParagraph = (descriptor, op, payload) => {
|
|
6569
7931
|
const blockIds = selectionBlockIds(state.selection);
|
|
6570
7932
|
if (blockIds.length === 0) return false;
|
|
@@ -6670,6 +8032,7 @@ function createSuperDocUI(options) {
|
|
|
6670
8032
|
})();
|
|
6671
8033
|
return true;
|
|
6672
8034
|
};
|
|
8035
|
+
/** Route a list command (`lists.apply` / `lists.remove` / list level changes). */
|
|
6673
8036
|
const executeListCommand = (descriptor, op) => {
|
|
6674
8037
|
const spec = descriptor.list;
|
|
6675
8038
|
const doc = getDoc();
|
|
@@ -6707,9 +8070,9 @@ function createSuperDocUI(options) {
|
|
|
6707
8070
|
seed
|
|
6708
8071
|
};
|
|
6709
8072
|
try {
|
|
6710
|
-
const result
|
|
6711
|
-
immediateResults.push(result
|
|
6712
|
-
settledResults.push(result
|
|
8073
|
+
const result = settleOperationResult(callEditorMutation(route, operation, input));
|
|
8074
|
+
immediateResults.push(result.immediate);
|
|
8075
|
+
settledResults.push(result.settled);
|
|
6713
8076
|
} catch {
|
|
6714
8077
|
immediateResults.push(false);
|
|
6715
8078
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6723,8 +8086,8 @@ function createSuperDocUI(options) {
|
|
|
6723
8086
|
const stateReads = blockIds.map((blockId) => resolveListSeedForBlock(doc, blockId, story));
|
|
6724
8087
|
if (stateReads.some(isPromiseLike)) {
|
|
6725
8088
|
pendingCommandSettlement = Promise.all(stateReads).then(async (states) => {
|
|
6726
|
-
const result
|
|
6727
|
-
return result
|
|
8089
|
+
const result = executeResolved(states);
|
|
8090
|
+
return result.settled ? await result.settled : result.immediate;
|
|
6728
8091
|
});
|
|
6729
8092
|
return true;
|
|
6730
8093
|
}
|
|
@@ -6750,6 +8113,7 @@ function createSuperDocUI(options) {
|
|
|
6750
8113
|
const capture = record.capture && typeof record.capture === "object" ? record.capture : null;
|
|
6751
8114
|
return record.target ?? capture?.target ?? capture?.selectionTarget ?? record.selectionTarget ?? record.textTarget ?? null;
|
|
6752
8115
|
}
|
|
8116
|
+
/** Route a link command (`hyperlinks.wrap` / `insert` / `patch` / `remove`). */
|
|
6753
8117
|
const executeLinkCommand = (payload) => {
|
|
6754
8118
|
const doc = getDoc();
|
|
6755
8119
|
const linksApi = doc?.hyperlinks;
|
|
@@ -6885,8 +8249,8 @@ function createSuperDocUI(options) {
|
|
|
6885
8249
|
if (wrapTargets.length > 0) {
|
|
6886
8250
|
if (typeof wrap !== "function") return false;
|
|
6887
8251
|
try {
|
|
6888
|
-
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target
|
|
6889
|
-
target
|
|
8252
|
+
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target) => ({
|
|
8253
|
+
target,
|
|
6890
8254
|
link: { destination: { href: normalizedHref } }
|
|
6891
8255
|
}));
|
|
6892
8256
|
} catch {
|
|
@@ -6894,14 +8258,14 @@ function createSuperDocUI(options) {
|
|
|
6894
8258
|
}
|
|
6895
8259
|
}
|
|
6896
8260
|
const insert = linksApi.insert;
|
|
6897
|
-
const text
|
|
6898
|
-
if (typeof insert !== "function" || text
|
|
8261
|
+
const text = readLinkPayloadText(payload, normalizedHref);
|
|
8262
|
+
if (typeof insert !== "function" || text.trim() === "") return false;
|
|
6899
8263
|
const target = collapsedTextAddressFromTarget(payloadTarget) ?? collapsedTextAddressFromSelection(state.selection);
|
|
6900
8264
|
if (!target) return false;
|
|
6901
8265
|
try {
|
|
6902
8266
|
return callEditorMutation("hyperlinks.insert", insert, {
|
|
6903
8267
|
target,
|
|
6904
|
-
text
|
|
8268
|
+
text,
|
|
6905
8269
|
link: { destination: { href: normalizedHref } }
|
|
6906
8270
|
});
|
|
6907
8271
|
} catch {
|
|
@@ -6910,6 +8274,7 @@ function createSuperDocUI(options) {
|
|
|
6910
8274
|
}
|
|
6911
8275
|
return false;
|
|
6912
8276
|
};
|
|
8277
|
+
/** Route a create command (`create.table` / `create.image` / `create.tableOfContents`). */
|
|
6913
8278
|
const executeCreateCommand = (descriptor, op, payload) => {
|
|
6914
8279
|
const kind = descriptor.create.kind;
|
|
6915
8280
|
const record = payload && typeof payload === "object" ? payload : {};
|
|
@@ -6955,16 +8320,16 @@ function createSuperDocUI(options) {
|
|
|
6955
8320
|
}
|
|
6956
8321
|
};
|
|
6957
8322
|
}
|
|
6958
|
-
const input
|
|
8323
|
+
const input = {
|
|
6959
8324
|
src,
|
|
6960
8325
|
at
|
|
6961
8326
|
};
|
|
6962
|
-
if (nonBodyStory) input
|
|
6963
|
-
if (typeof record.alt === "string") input
|
|
6964
|
-
if (typeof record.title === "string") input
|
|
6965
|
-
if (record.size && typeof record.size === "object") input
|
|
8327
|
+
if (nonBodyStory) input.in = at.kind === "inParagraph" ? resolveActiveHeaderFooterSlot(nonBodyStory) ?? nonBodyStory : nonBodyStory;
|
|
8328
|
+
if (typeof record.alt === "string") input.alt = record.alt;
|
|
8329
|
+
if (typeof record.title === "string") input.title = record.title;
|
|
8330
|
+
if (record.size && typeof record.size === "object") input.size = record.size;
|
|
6966
8331
|
try {
|
|
6967
|
-
return callEditorMutation(descriptor.docRoute, op, input
|
|
8332
|
+
return callEditorMutation(descriptor.docRoute, op, input);
|
|
6968
8333
|
} catch {
|
|
6969
8334
|
return false;
|
|
6970
8335
|
}
|
|
@@ -6978,6 +8343,13 @@ function createSuperDocUI(options) {
|
|
|
6978
8343
|
return false;
|
|
6979
8344
|
}
|
|
6980
8345
|
};
|
|
8346
|
+
/**
|
|
8347
|
+
* Route a table cell-context command (`tables.*`) against the live table
|
|
8348
|
+
* context resolved from the shared facade. Fails closed (`false`) when the
|
|
8349
|
+
* caret is not in a table, the context is incomplete, or the action's
|
|
8350
|
+
* required cell is missing — never calling the operation with a malformed
|
|
8351
|
+
* locator.
|
|
8352
|
+
*/
|
|
6981
8353
|
const executeTableCommand = (descriptor, op) => {
|
|
6982
8354
|
const spec = descriptor.table;
|
|
6983
8355
|
const context = resolveTableContext();
|
|
@@ -6990,9 +8362,9 @@ function createSuperDocUI(options) {
|
|
|
6990
8362
|
const host = getHost();
|
|
6991
8363
|
const insertRow = ((typeof host?.getHandles === "function" ? host.getHandles() : null)?.editing)?.tables;
|
|
6992
8364
|
if (typeof insertRow?.insertRow !== "function") return false;
|
|
6993
|
-
const options
|
|
6994
|
-
if (options
|
|
6995
|
-
return options
|
|
8365
|
+
const options = editorMutationOptionsForRoute(descriptor.docRoute);
|
|
8366
|
+
if (options && options.success === false) return options;
|
|
8367
|
+
return options ? insertRow.insertRow(input, options) : insertRow.insertRow(input);
|
|
6996
8368
|
}
|
|
6997
8369
|
return callEditorMutation(descriptor.docRoute, op, input);
|
|
6998
8370
|
} catch {
|
|
@@ -7033,7 +8405,8 @@ function createSuperDocUI(options) {
|
|
|
7033
8405
|
if (disposed) return false;
|
|
7034
8406
|
const custom = customCommands.get(id);
|
|
7035
8407
|
if (custom) try {
|
|
7036
|
-
|
|
8408
|
+
const result = custom.execute(buildCustomCommandContext(payload, context));
|
|
8409
|
+
return settleCommandExecution(result);
|
|
7037
8410
|
} catch {
|
|
7038
8411
|
return false;
|
|
7039
8412
|
}
|
|
@@ -7054,7 +8427,8 @@ function createSuperDocUI(options) {
|
|
|
7054
8427
|
if (typeof method !== "function") return false;
|
|
7055
8428
|
try {
|
|
7056
8429
|
const arg = descriptor.fixedArg !== void 0 ? descriptor.fixedArg : normalized;
|
|
7057
|
-
|
|
8430
|
+
const result = method.call(superdoc, arg);
|
|
8431
|
+
return settleCommandExecution(result);
|
|
7058
8432
|
} catch {
|
|
7059
8433
|
return false;
|
|
7060
8434
|
}
|
|
@@ -7088,12 +8462,13 @@ function createSuperDocUI(options) {
|
|
|
7088
8462
|
if (getEditor() !== commandEditor || getDoc() !== doc || readDocumentMode() !== commandMode) return false;
|
|
7089
8463
|
return callInlineFormatMutation(route, op, input);
|
|
7090
8464
|
};
|
|
7091
|
-
const selectionSignature
|
|
7092
|
-
const scheduledMutation = descriptor.inline.kind === "toggle" ? scheduleInlineToggleMutation(`${descriptor.id}:${selectionSignature
|
|
8465
|
+
const selectionSignature = selectionInlineValueSignature(state.selection) ?? selectionKey(state.selection);
|
|
8466
|
+
const scheduledMutation = descriptor.inline.kind === "toggle" ? scheduleInlineToggleMutation(`${descriptor.id}:${selectionSignature}`, mutate) : null;
|
|
7093
8467
|
releaseScheduledMutation = scheduledMutation?.release ?? null;
|
|
7094
8468
|
const mutationResult = scheduledMutation ? scheduledMutation.result : mutate();
|
|
7095
8469
|
const optimisticGeneration = captureOptimisticInlineToggle(descriptor, state.selection, input, mutationResult);
|
|
7096
|
-
const
|
|
8470
|
+
const result = decorateInlineCommandResult(descriptor, state.selection, input, mutationResult);
|
|
8471
|
+
const immediate = settleCommandExecution(result, (settled) => {
|
|
7097
8472
|
settleOptimisticInlineToggle(descriptor, optimisticGeneration, settled);
|
|
7098
8473
|
releaseScheduledMutation?.();
|
|
7099
8474
|
releaseScheduledMutation = null;
|
|
@@ -7112,10 +8487,22 @@ function createSuperDocUI(options) {
|
|
|
7112
8487
|
settleOptimisticParagraphAlignment(optimisticAlignmentGeneration, settled);
|
|
7113
8488
|
});
|
|
7114
8489
|
}
|
|
7115
|
-
if (descriptor.list)
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
8490
|
+
if (descriptor.list) {
|
|
8491
|
+
const result = executeListCommand(descriptor, op);
|
|
8492
|
+
return settleCommandExecution(result);
|
|
8493
|
+
}
|
|
8494
|
+
if (descriptor.link) {
|
|
8495
|
+
const result = executeLinkCommand(normalized);
|
|
8496
|
+
return settleCommandExecution(result);
|
|
8497
|
+
}
|
|
8498
|
+
if (descriptor.create) {
|
|
8499
|
+
const result = executeCreateCommand(descriptor, op, normalized);
|
|
8500
|
+
return settleCommandExecution(result);
|
|
8501
|
+
}
|
|
8502
|
+
if (descriptor.table) {
|
|
8503
|
+
const result = executeTableCommand(descriptor, op);
|
|
8504
|
+
return settleCommandExecution(result);
|
|
8505
|
+
}
|
|
7119
8506
|
try {
|
|
7120
8507
|
return settleCommandExecution(descriptor.mutates ? callEditorMutation(route, op, normalized) : op(normalized));
|
|
7121
8508
|
} catch {
|
|
@@ -7182,8 +8569,8 @@ function createSuperDocUI(options) {
|
|
|
7182
8569
|
recompute();
|
|
7183
8570
|
return true;
|
|
7184
8571
|
};
|
|
7185
|
-
const captureFormatPainter = async (selection
|
|
7186
|
-
const selTarget = captureSlice?.target ?? selection
|
|
8572
|
+
const captureFormatPainter = async (selection, captureSlice, captureProjected, visibleActiveMarks = []) => {
|
|
8573
|
+
const selTarget = captureSlice?.target ?? selection?.target;
|
|
7187
8574
|
if (!selTarget || selTarget["kind"] !== "text") return null;
|
|
7188
8575
|
const segments = Array.isArray(selTarget["segments"]) ? selTarget["segments"] : [];
|
|
7189
8576
|
if (segments.length === 0) return null;
|
|
@@ -7195,7 +8582,8 @@ function createSuperDocUI(options) {
|
|
|
7195
8582
|
const node = (await readBlockNode(paragraphTarget(segment["blockId"], sourceStory)))?.["node"] ?? null;
|
|
7196
8583
|
if (!node) continue;
|
|
7197
8584
|
const para = getParagraphLikeData(node);
|
|
7198
|
-
const
|
|
8585
|
+
const runs = Array.isArray(para?.["inlines"]) ? para["inlines"] : [];
|
|
8586
|
+
const slicedProps = sliceAndIntersectRunProps(runs, segment["range"]);
|
|
7199
8587
|
if (!mergedRunProps) mergedRunProps = slicedProps;
|
|
7200
8588
|
else mergedRunProps = intersectRunProps(mergedRunProps, slicedProps);
|
|
7201
8589
|
}
|
|
@@ -7206,9 +8594,9 @@ function createSuperDocUI(options) {
|
|
|
7206
8594
|
underline: "underline",
|
|
7207
8595
|
strikethrough: "strike"
|
|
7208
8596
|
};
|
|
7209
|
-
const activeMarks = new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection
|
|
8597
|
+
const activeMarks = /* @__PURE__ */ new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection?.["activeMarks"]) ? selection["activeMarks"] : []), ...visibleActiveMarks]);
|
|
7210
8598
|
for (const [markName, patchKey] of Object.entries(MARK_TO_PATCH)) if (activeMarks.has(markName)) inlinePatch[patchKey] = true;
|
|
7211
|
-
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection
|
|
8599
|
+
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection, "ready"));
|
|
7212
8600
|
if (projectedInlineValues.fontFamily) inlinePatch["fontFamily"] = projectedInlineValues.fontFamily;
|
|
7213
8601
|
if (projectedInlineValues.color) inlinePatch["color"] = projectedInlineValues.color;
|
|
7214
8602
|
if (projectedInlineValues.highlight) inlinePatch["highlight"] = projectedInlineValues.highlight;
|
|
@@ -7245,12 +8633,12 @@ function createSuperDocUI(options) {
|
|
|
7245
8633
|
return r["run"]?.["props"];
|
|
7246
8634
|
};
|
|
7247
8635
|
if (range.start === range.end) {
|
|
7248
|
-
let offset
|
|
8636
|
+
let offset = 0;
|
|
7249
8637
|
for (const run of runs) {
|
|
7250
8638
|
const text = getRunText(run);
|
|
7251
|
-
const runStart = offset
|
|
7252
|
-
const runEnd = offset
|
|
7253
|
-
offset
|
|
8639
|
+
const runStart = offset;
|
|
8640
|
+
const runEnd = offset + text.length;
|
|
8641
|
+
offset = runEnd;
|
|
7254
8642
|
if (range.start >= runStart && range.start <= runEnd) {
|
|
7255
8643
|
const rPr = getRunProps(run);
|
|
7256
8644
|
return rPr ? { ...rPr } : {};
|
|
@@ -7327,7 +8715,8 @@ function createSuperDocUI(options) {
|
|
|
7327
8715
|
if (painter.pointerSelecting || painter.keyboardSelecting) return;
|
|
7328
8716
|
const selectionApi = getDoc()?.selection;
|
|
7329
8717
|
const readSelection = async () => {
|
|
7330
|
-
|
|
8718
|
+
const rawSelection = typeof selectionApi?.["current"] === "function" ? await Promise.resolve(selectionApi["current"]({ includeText: true })) : readSelectionInfoLive().value;
|
|
8719
|
+
return normalizeSelectionInfo(rawSelection);
|
|
7331
8720
|
};
|
|
7332
8721
|
const waitForSelectionSettle = () => new Promise((resolve) => setTimeout(resolve, 16));
|
|
7333
8722
|
const MAX_SELECTION_ATTEMPTS = 8;
|
|
@@ -7353,7 +8742,7 @@ function createSuperDocUI(options) {
|
|
|
7353
8742
|
await applyFormatPainter(caretSel);
|
|
7354
8743
|
}
|
|
7355
8744
|
};
|
|
7356
|
-
const applyFormatPainter = async (selection
|
|
8745
|
+
const applyFormatPainter = async (selection) => {
|
|
7357
8746
|
const snap = painter.snapshot;
|
|
7358
8747
|
if (!snap) {
|
|
7359
8748
|
if (painter.mode === "armed") exitFormatPainter();
|
|
@@ -7361,21 +8750,21 @@ function createSuperDocUI(options) {
|
|
|
7361
8750
|
}
|
|
7362
8751
|
const doc = getDoc();
|
|
7363
8752
|
if (!doc) return;
|
|
7364
|
-
const targetSelection = selectionSliceFromInfo(selection
|
|
8753
|
+
const targetSelection = selectionSliceFromInfo(selection, "ready");
|
|
7365
8754
|
const { lockModesById } = computeActiveContentControlLockModes(doc.contentControls, targetSelection);
|
|
7366
8755
|
if (contentControlLockReason(lockModesById)) {
|
|
7367
8756
|
recompute();
|
|
7368
8757
|
return;
|
|
7369
8758
|
}
|
|
7370
|
-
const selTarget = selection
|
|
7371
|
-
const selectionTarget = selection
|
|
7372
|
-
if (!(selection
|
|
8759
|
+
const selTarget = selection["target"];
|
|
8760
|
+
const selectionTarget = selection["selectionTarget"];
|
|
8761
|
+
if (!(selection["empty"] === true) && Object.keys(snap.inline).length > 0) {
|
|
7373
8762
|
const inlineOp = resolveDocOperation(doc, "format.apply");
|
|
7374
8763
|
if (inlineOp && selectionTarget) try {
|
|
7375
8764
|
if (!commandResultSucceeded(commandResultFromOperationResult(await Promise.resolve(callInlineFormatMutation("format.apply", inlineOp, {
|
|
7376
8765
|
target: selectionTarget,
|
|
7377
8766
|
inline: snap.inline
|
|
7378
|
-
}, selection
|
|
8767
|
+
}, selection))))) {
|
|
7379
8768
|
recompute();
|
|
7380
8769
|
return;
|
|
7381
8770
|
}
|
|
@@ -7411,11 +8800,11 @@ function createSuperDocUI(options) {
|
|
|
7411
8800
|
if (spacing) {
|
|
7412
8801
|
const sp = spacing;
|
|
7413
8802
|
const rawLr = sp["lineRule"] ?? "auto";
|
|
7414
|
-
const lr = new Set([
|
|
8803
|
+
const lr = (/* @__PURE__ */ new Set([
|
|
7415
8804
|
"auto",
|
|
7416
8805
|
"exact",
|
|
7417
8806
|
"atLeast"
|
|
7418
|
-
]).has(rawLr) ? rawLr : "auto";
|
|
8807
|
+
])).has(rawLr) ? rawLr : "auto";
|
|
7419
8808
|
const rawSpacing = {
|
|
7420
8809
|
...sp["before"] != null ? { before: Math.round(sp["before"] * 20) } : {},
|
|
7421
8810
|
...sp["after"] != null ? { after: Math.round(sp["after"] * 20) } : {},
|
|
@@ -7505,6 +8894,14 @@ function createSuperDocUI(options) {
|
|
|
7505
8894
|
};
|
|
7506
8895
|
const selectionSub = sliceHandle((s) => s.selection);
|
|
7507
8896
|
const selectionSnap = snapshotHandle(selectionSub);
|
|
8897
|
+
/**
|
|
8898
|
+
* Resolve the host-owned selection apply helper (`editing.selectionTargets`)
|
|
8899
|
+
* with a truthful failure reason. `host.getHandles()` throws a
|
|
8900
|
+
* `V2EditorHostError` with a lifecycle `reason` while the host is booting or
|
|
8901
|
+
* disposed — that is a readiness condition, not a missing capability, so it
|
|
8902
|
+
* maps to `not-ready` instead of being swallowed into
|
|
8903
|
+
* `host-capability-unavailable`.
|
|
8904
|
+
*/
|
|
7508
8905
|
const getSelectionApplyHelper = () => {
|
|
7509
8906
|
const host = getHost();
|
|
7510
8907
|
if (typeof host?.getHandles !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
|
|
@@ -7519,6 +8916,14 @@ function createSuperDocUI(options) {
|
|
|
7519
8916
|
if (typeof helper?.apply !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
|
|
7520
8917
|
return { helper };
|
|
7521
8918
|
};
|
|
8919
|
+
const activateContentControlChrome = (id) => {
|
|
8920
|
+
const host = getHost();
|
|
8921
|
+
if (typeof host?.getHandles !== "function") return;
|
|
8922
|
+
try {
|
|
8923
|
+
const contentControls = (host.getHandles()?.editing)?.contentControls;
|
|
8924
|
+
if (typeof contentControls?.activate === "function") contentControls.activate({ id });
|
|
8925
|
+
} catch {}
|
|
8926
|
+
};
|
|
7522
8927
|
const selection = {
|
|
7523
8928
|
get: selectionSnap.get,
|
|
7524
8929
|
getSnapshot: selectionSnap.getSnapshot,
|
|
@@ -7644,7 +9049,17 @@ function createSuperDocUI(options) {
|
|
|
7644
9049
|
execute: executeCommand,
|
|
7645
9050
|
executeAsync: executeCommandAsync
|
|
7646
9051
|
};
|
|
7647
|
-
|
|
9052
|
+
/**
|
|
9053
|
+
* Scroll a resolved document target into view through the host navigation
|
|
9054
|
+
* surface (`host.scrollTargetIntoView`). Shared by comments / tracked changes /
|
|
9055
|
+
* content controls and the generic `viewport.scrollIntoView`. Alignment
|
|
9056
|
+
* defaults to `block: 'center'` / `behavior: 'smooth'` (matching the generic
|
|
9057
|
+
* method and v1) so every scroll path lands consistently; callers may
|
|
9058
|
+
* override. Fails closed with a stable reason rather than a no-op recompute;
|
|
9059
|
+
* a resolved target that cannot be made visible is `target-not-visible`, not
|
|
9060
|
+
* `target-unresolved`.
|
|
9061
|
+
*/
|
|
9062
|
+
const scrollTargetIntoView = async (target, options) => {
|
|
7648
9063
|
if (!getEditor()) return {
|
|
7649
9064
|
success: false,
|
|
7650
9065
|
ok: false,
|
|
@@ -7665,10 +9080,10 @@ function createSuperDocUI(options) {
|
|
|
7665
9080
|
try {
|
|
7666
9081
|
const input = {
|
|
7667
9082
|
target,
|
|
7668
|
-
block: options
|
|
7669
|
-
behavior: options
|
|
9083
|
+
block: options?.block ?? "center",
|
|
9084
|
+
behavior: options?.behavior ?? "smooth"
|
|
7670
9085
|
};
|
|
7671
|
-
const result = await Promise.resolve(options
|
|
9086
|
+
const result = await Promise.resolve(options?.shouldContinue ? scroll.call(host, input, options.shouldContinue) : scroll.call(host, input));
|
|
7672
9087
|
if (result && typeof result === "object" && result.success === false) return {
|
|
7673
9088
|
success: false,
|
|
7674
9089
|
ok: false,
|
|
@@ -7686,6 +9101,11 @@ function createSuperDocUI(options) {
|
|
|
7686
9101
|
};
|
|
7687
9102
|
}
|
|
7688
9103
|
};
|
|
9104
|
+
/**
|
|
9105
|
+
* Resolve a document entity (comment / tracked change) to its anchor target.
|
|
9106
|
+
* Prefers the already-loaded list row, falling back to a live `get({ id })`
|
|
9107
|
+
* read when the list does not carry the row.
|
|
9108
|
+
*/
|
|
7689
9109
|
const resolveEntityTarget = async (namespace, id, loaded, request) => {
|
|
7690
9110
|
const requestedStory = namespace === "trackChanges" ? request?.story : void 0;
|
|
7691
9111
|
const withRequestedStory = (target) => {
|
|
@@ -7738,6 +9158,18 @@ function createSuperDocUI(options) {
|
|
|
7738
9158
|
status
|
|
7739
9159
|
}), fallback), fallback);
|
|
7740
9160
|
}
|
|
9161
|
+
/**
|
|
9162
|
+
* The refusal every comment write shares, or `null` when the policy permits.
|
|
9163
|
+
*
|
|
9164
|
+
* `readOnly` is policy rather than presentation, so it cannot be left to the
|
|
9165
|
+
* built-in dialog hiding its affordances: a custom comment UI calls
|
|
9166
|
+
* `createFromCapture`, `createFromSelection`, `reply`, and `delete` directly,
|
|
9167
|
+
* and the Document API underneath carries no policy of its own. Missing one
|
|
9168
|
+
* route fails open on exactly the surface the policy exists for.
|
|
9169
|
+
*
|
|
9170
|
+
* `allowResolve` is deliberately not checked here — it forbids only the
|
|
9171
|
+
* resolve/reopen transition, which `patchCommentStatus` owns.
|
|
9172
|
+
*/
|
|
7741
9173
|
const commentWriteRefusal = () => reviewMutationsAreReadOnly() ? failedReceipt("Comments are read-only.", "DOCUMENT_READONLY") : null;
|
|
7742
9174
|
const filterCommentsSnapshot = (items, query) => {
|
|
7743
9175
|
const record = query && typeof query === "object" ? query : null;
|
|
@@ -7778,10 +9210,14 @@ function createSuperDocUI(options) {
|
|
|
7778
9210
|
const op = commentsApi?.create;
|
|
7779
9211
|
if (typeof op !== "function") return failedReceipt("comments.create is unavailable.");
|
|
7780
9212
|
const fallback = failedReceipt("comments.create failed.");
|
|
7781
|
-
return safeCall(() =>
|
|
7782
|
-
target
|
|
7783
|
-
|
|
7784
|
-
|
|
9213
|
+
return safeCall(() => {
|
|
9214
|
+
const target = capture && typeof capture === "object" ? capture.target ?? capture.selectionTarget : null;
|
|
9215
|
+
if (target == null) return failedReceipt("A range selection is required to comment.", "NO_SELECTION");
|
|
9216
|
+
return settleWorkflowReceipt(op.call(commentsApi, {
|
|
9217
|
+
target,
|
|
9218
|
+
text: input.text
|
|
9219
|
+
}), fallback);
|
|
9220
|
+
}, fallback);
|
|
7785
9221
|
},
|
|
7786
9222
|
createFromSelection: (input) => {
|
|
7787
9223
|
const refusal = commentWriteRefusal();
|
|
@@ -7813,6 +9249,18 @@ function createSuperDocUI(options) {
|
|
|
7813
9249
|
text: input.text
|
|
7814
9250
|
}), fallback), fallback);
|
|
7815
9251
|
},
|
|
9252
|
+
edit: (commentId, input) => {
|
|
9253
|
+
const refusal = commentWriteRefusal();
|
|
9254
|
+
if (refusal) return refusal;
|
|
9255
|
+
const commentsApi = getDoc()?.comments;
|
|
9256
|
+
const op = commentsApi?.patch;
|
|
9257
|
+
if (typeof op !== "function") return failedReceipt("comments.patch is unavailable.");
|
|
9258
|
+
const fallback = failedReceipt("comments.patch failed.");
|
|
9259
|
+
return safeCall(() => settleWorkflowReceipt(op.call(commentsApi, {
|
|
9260
|
+
commentId,
|
|
9261
|
+
text: input.text
|
|
9262
|
+
}), fallback), fallback);
|
|
9263
|
+
},
|
|
7816
9264
|
resolve: (commentId) => patchCommentStatus(commentId, "resolved"),
|
|
7817
9265
|
reopen: (commentId) => patchCommentStatus(commentId, "active"),
|
|
7818
9266
|
delete: (commentId) => {
|
|
@@ -7856,7 +9304,7 @@ function createSuperDocUI(options) {
|
|
|
7856
9304
|
}
|
|
7857
9305
|
};
|
|
7858
9306
|
const trackChangesSub = sliceHandle((s) => s.trackChanges);
|
|
7859
|
-
const navigateTrackChange = (step, options
|
|
9307
|
+
const navigateTrackChange = (step, options) => {
|
|
7860
9308
|
const allRows = trackChangesSub.get().items.filter((item) => readEntityId(item) !== null);
|
|
7861
9309
|
if (allRows.length === 0) return null;
|
|
7862
9310
|
const currentId = state.trackChanges.activeId;
|
|
@@ -7871,7 +9319,7 @@ function createSuperDocUI(options) {
|
|
|
7871
9319
|
setExplicitActiveChange(story ? {
|
|
7872
9320
|
id: activeId,
|
|
7873
9321
|
story
|
|
7874
|
-
} : { id: activeId }, options
|
|
9322
|
+
} : { id: activeId }, options);
|
|
7875
9323
|
recompute();
|
|
7876
9324
|
return activeId;
|
|
7877
9325
|
};
|
|
@@ -7953,8 +9401,8 @@ function createSuperDocUI(options) {
|
|
|
7953
9401
|
},
|
|
7954
9402
|
accept: (changeId) => executeTrackDecision("accept", changeId),
|
|
7955
9403
|
reject: (changeId) => executeTrackDecision("reject", changeId),
|
|
7956
|
-
acceptAll: () => executeTrackDecisionTarget("accept", {
|
|
7957
|
-
rejectAll: () => executeTrackDecisionTarget("reject", {
|
|
9404
|
+
acceptAll: () => executeTrackDecisionTarget("accept", { kind: "all" }, null),
|
|
9405
|
+
rejectAll: () => executeTrackDecisionTarget("reject", { kind: "all" }, null),
|
|
7958
9406
|
next: () => navigateTrackChange(1),
|
|
7959
9407
|
previous: () => navigateTrackChange(-1),
|
|
7960
9408
|
navigateNext: () => navigateAndScroll(1),
|
|
@@ -8092,11 +9540,12 @@ function createSuperDocUI(options) {
|
|
|
8092
9540
|
id: input,
|
|
8093
9541
|
story: void 0
|
|
8094
9542
|
} : input;
|
|
8095
|
-
|
|
9543
|
+
const target = {
|
|
8096
9544
|
kind: "id",
|
|
8097
9545
|
id,
|
|
8098
9546
|
...story ? { story } : {}
|
|
8099
|
-
}
|
|
9547
|
+
};
|
|
9548
|
+
return executeTrackDecisionTarget(kind, target, id, story);
|
|
8100
9549
|
};
|
|
8101
9550
|
const executeTrackDecisionTarget = (kind, target, changeId, story) => {
|
|
8102
9551
|
try {
|
|
@@ -8108,7 +9557,7 @@ function createSuperDocUI(options) {
|
|
|
8108
9557
|
const callTrackDecisionTarget = (kind, target, changeId, story) => {
|
|
8109
9558
|
if (reviewMutationsAreReadOnly()) return false;
|
|
8110
9559
|
const tcApi = getDoc()?.trackChanges;
|
|
8111
|
-
const isAllTarget = target.scope === "all";
|
|
9560
|
+
const isAllTarget = target.kind === "all" || target.scope === "all";
|
|
8112
9561
|
const isRangeTarget = target.kind === "range";
|
|
8113
9562
|
const isMultiIdTarget = target.kind === "ids";
|
|
8114
9563
|
if (bulkTrackDecisionBlockedReason({
|
|
@@ -8211,6 +9660,7 @@ function createSuperDocUI(options) {
|
|
|
8211
9660
|
success: false,
|
|
8212
9661
|
reason: "not-reachable"
|
|
8213
9662
|
};
|
|
9663
|
+
activateContentControlChrome(id);
|
|
8214
9664
|
return { success: true };
|
|
8215
9665
|
}
|
|
8216
9666
|
};
|
|
@@ -8345,9 +9795,9 @@ function createSuperDocUI(options) {
|
|
|
8345
9795
|
const hits = [];
|
|
8346
9796
|
for (const hit of rawHits) {
|
|
8347
9797
|
if (hit.type !== "trackedChange") {
|
|
8348
|
-
const key
|
|
8349
|
-
if (seen.has(key
|
|
8350
|
-
seen.add(key
|
|
9798
|
+
const key = `${hit.type}:${hit.id}`;
|
|
9799
|
+
if (seen.has(key)) continue;
|
|
9800
|
+
seen.add(key);
|
|
8351
9801
|
hits.push(hit);
|
|
8352
9802
|
continue;
|
|
8353
9803
|
}
|
|
@@ -8403,6 +9853,21 @@ function createSuperDocUI(options) {
|
|
|
8403
9853
|
}
|
|
8404
9854
|
entry.boundHost = null;
|
|
8405
9855
|
};
|
|
9856
|
+
/**
|
|
9857
|
+
* Move every live geometry subscription to the editor that is now active.
|
|
9858
|
+
* Without this, scroll and repaint on the replacement editor never reach a
|
|
9859
|
+
* listener that was registered against the previous one, so overlays stop
|
|
9860
|
+
* tracking the document while still responding to selection and zoom.
|
|
9861
|
+
*/
|
|
9862
|
+
/**
|
|
9863
|
+
* Release every geometry subscription once a hostless moment turns out to be
|
|
9864
|
+
* permanent. Deferred rather than immediate because a null host is ambiguous
|
|
9865
|
+
* at the instant it arrives: `registerV2Runtime` unregisters the outgoing
|
|
9866
|
+
* runtime and installs the replacement in ONE synchronous block, so a refresh
|
|
9867
|
+
* shows up here as a null followed by the new host with nothing in between.
|
|
9868
|
+
* By the time a microtask runs, a refresh has already installed its host and a
|
|
9869
|
+
* true clear (`removeDocument()`, a fail-closed projection) still has none.
|
|
9870
|
+
*/
|
|
8406
9871
|
const releaseGeometryObserversIfStillHostless = () => {
|
|
8407
9872
|
geometryReleaseScheduled = false;
|
|
8408
9873
|
if (disposed || getHost()) return;
|
|
@@ -8539,6 +10004,7 @@ function createSuperDocUI(options) {
|
|
|
8539
10004
|
return target && typeof target === "object" ? target : null;
|
|
8540
10005
|
};
|
|
8541
10006
|
const metadataResolveKey = (id) => `metadata:resolve:${id}`;
|
|
10007
|
+
/** Resolve a metadata id to its SelectionTarget, or null when unresolved. */
|
|
8542
10008
|
const resolveMetadataTarget = (id) => {
|
|
8543
10009
|
const metaApi = getDoc()?.metadata;
|
|
8544
10010
|
if (!metaApi || typeof metaApi.resolve !== "function") return null;
|
|
@@ -8622,16 +10088,16 @@ function createSuperDocUI(options) {
|
|
|
8622
10088
|
let searchState = { ...SEARCH_UNAVAILABLE_SLICE };
|
|
8623
10089
|
const searchListeners = /* @__PURE__ */ new Set();
|
|
8624
10090
|
const getHostSearch = () => {
|
|
8625
|
-
const search
|
|
8626
|
-
return search
|
|
10091
|
+
const search = getHost()?.search;
|
|
10092
|
+
return search && typeof search === "object" ? search : null;
|
|
8627
10093
|
};
|
|
8628
10094
|
const getEditCommandSearch = () => {
|
|
8629
|
-
const search
|
|
8630
|
-
return search
|
|
10095
|
+
const search = getEditCommands()?.search;
|
|
10096
|
+
return search && typeof search === "object" ? search : null;
|
|
8631
10097
|
};
|
|
8632
10098
|
const searchIsAvailable = () => {
|
|
8633
|
-
const search
|
|
8634
|
-
if (search
|
|
10099
|
+
const search = getHostSearch();
|
|
10100
|
+
if (search && typeof search.setSession === "function") return true;
|
|
8635
10101
|
const editSearch = getEditCommandSearch();
|
|
8636
10102
|
return Boolean(editSearch) && typeof editSearch.query === "function" && typeof editSearch.getState === "function";
|
|
8637
10103
|
};
|
|
@@ -8649,7 +10115,21 @@ function createSuperDocUI(options) {
|
|
|
8649
10115
|
return searchState;
|
|
8650
10116
|
};
|
|
8651
10117
|
let searchRequestGeneration = 0;
|
|
10118
|
+
/**
|
|
10119
|
+
* How to tear down the session that is currently open, captured when it was
|
|
10120
|
+
* opened.
|
|
10121
|
+
*
|
|
10122
|
+
* A closure rather than a reference to the facade, because the two search
|
|
10123
|
+
* paths are released differently: a host session ends with `host.clear()`,
|
|
10124
|
+
* while the worker-backed fallback ends by querying the empty string through
|
|
10125
|
+
* `editCommands.search`. Both bind to the editor that owned them, and by the
|
|
10126
|
+
* time an active-editor change reaches us `activeEditor` already points at the
|
|
10127
|
+
* replacement, so neither is reachable by looking it up again. Capturing the
|
|
10128
|
+
* teardown keeps the two paths from drifting apart: whichever one opens a
|
|
10129
|
+
* session is the one that says how to close it.
|
|
10130
|
+
*/
|
|
8652
10131
|
let releaseActiveSearchSession = null;
|
|
10132
|
+
/** Release whatever session is open, then forget it. */
|
|
8653
10133
|
const releaseSearchSession = () => {
|
|
8654
10134
|
const release = releaseActiveSearchSession;
|
|
8655
10135
|
releaseActiveSearchSession = null;
|
|
@@ -8659,6 +10139,23 @@ function createSuperDocUI(options) {
|
|
|
8659
10139
|
return null;
|
|
8660
10140
|
}, null);
|
|
8661
10141
|
};
|
|
10142
|
+
/**
|
|
10143
|
+
* Drop everything the previous active editor owned.
|
|
10144
|
+
*
|
|
10145
|
+
* Three separate hazards. An in-flight async query still holds a generation
|
|
10146
|
+
* that is current, so without bumping the token it would resolve and publish
|
|
10147
|
+
* the old document's matches into the new one. The slice itself describes a
|
|
10148
|
+
* document that is gone: leaving it in place reports a total and an active
|
|
10149
|
+
* match for content nobody is looking at any more. And the previous host still
|
|
10150
|
+
* holds the session it painted, so without clearing it the old document keeps
|
|
10151
|
+
* its highlights and switching back shows a closed slice over a host that
|
|
10152
|
+
* still has matches.
|
|
10153
|
+
*
|
|
10154
|
+
* This closes the session rather than re-running the query against the
|
|
10155
|
+
* replacement document. Re-running is a plausible product choice and arguably
|
|
10156
|
+
* the nicer one, but it is a feature decision, not the correctness fix, so it
|
|
10157
|
+
* is deliberately not made here.
|
|
10158
|
+
*/
|
|
8662
10159
|
const resetSearchForActiveEditorChange = () => {
|
|
8663
10160
|
searchRequestGeneration += 1;
|
|
8664
10161
|
releaseSearchSession();
|
|
@@ -8839,11 +10336,11 @@ function createSuperDocUI(options) {
|
|
|
8839
10336
|
if (!searchState.available) searchState.reason = SUPERDOC_UI_REASONS.searchUnavailable;
|
|
8840
10337
|
emitSearch();
|
|
8841
10338
|
},
|
|
8842
|
-
search: (query, options
|
|
10339
|
+
search: (query, options) => {
|
|
8843
10340
|
const host = getHostSearch();
|
|
8844
|
-
const caseSensitive = Boolean(options
|
|
8845
|
-
const includeDeletedText = options
|
|
8846
|
-
const regex = options
|
|
10341
|
+
const caseSensitive = Boolean(options?.caseSensitive);
|
|
10342
|
+
const includeDeletedText = options?.includeDeletedText === true;
|
|
10343
|
+
const regex = options?.regex === true;
|
|
8847
10344
|
const generation = ++searchRequestGeneration;
|
|
8848
10345
|
if (!host || typeof host.setSession !== "function") {
|
|
8849
10346
|
const editSearch = getEditCommandSearch();
|
|
@@ -8903,12 +10400,13 @@ function createSuperDocUI(options) {
|
|
|
8903
10400
|
releaseActiveSearchSession = () => {
|
|
8904
10401
|
if (typeof host.clear === "function") host.clear();
|
|
8905
10402
|
};
|
|
8906
|
-
|
|
10403
|
+
const result = safeCall(() => host.setSession(query, {
|
|
8907
10404
|
caseSensitive,
|
|
8908
10405
|
includeDeletedText,
|
|
8909
10406
|
...regex ? { regex: true } : {},
|
|
8910
10407
|
highlight: true
|
|
8911
|
-
}), null)
|
|
10408
|
+
}), null);
|
|
10409
|
+
applyHostSearchResult(result);
|
|
8912
10410
|
if (query.length > 0 && searchState.total === 0 && searchState.reason !== SUPERDOC_UI_REASONS.searchInvalidPattern) {
|
|
8913
10411
|
const editSearch = getEditCommandSearch();
|
|
8914
10412
|
if (editSearch && typeof editSearch.query === "function" && typeof editSearch.getState === "function") {
|
|
@@ -9038,13 +10536,13 @@ function createSuperDocUI(options) {
|
|
|
9038
10536
|
ok: false,
|
|
9039
10537
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9040
10538
|
};
|
|
9041
|
-
const result
|
|
9042
|
-
if (isPromiseLike(result
|
|
10539
|
+
const result = safeCall(() => editSearch.replace({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10540
|
+
if (isPromiseLike(result)) {
|
|
9043
10541
|
const generation = searchRequestGeneration;
|
|
9044
10542
|
syncSearchStateFromHost();
|
|
9045
10543
|
emitSearch();
|
|
9046
10544
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9047
|
-
return Promise.resolve(result
|
|
10545
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9048
10546
|
if (isCurrent()) {
|
|
9049
10547
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9050
10548
|
syncSearchStateFromHost();
|
|
@@ -9064,7 +10562,7 @@ function createSuperDocUI(options) {
|
|
|
9064
10562
|
}
|
|
9065
10563
|
syncSearchStateFromHost();
|
|
9066
10564
|
emitSearch();
|
|
9067
|
-
return mapHostReplaceResult(result
|
|
10565
|
+
return mapHostReplaceResult(result);
|
|
9068
10566
|
}
|
|
9069
10567
|
if (typeof host.replaceCurrent !== "function") return {
|
|
9070
10568
|
ok: false,
|
|
@@ -9087,13 +10585,13 @@ function createSuperDocUI(options) {
|
|
|
9087
10585
|
ok: false,
|
|
9088
10586
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9089
10587
|
};
|
|
9090
|
-
const result
|
|
9091
|
-
if (isPromiseLike(result
|
|
10588
|
+
const result = safeCall(() => editSearch.replaceAll({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10589
|
+
if (isPromiseLike(result)) {
|
|
9092
10590
|
const generation = searchRequestGeneration;
|
|
9093
10591
|
syncSearchStateFromHost();
|
|
9094
10592
|
emitSearch();
|
|
9095
10593
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9096
|
-
return Promise.resolve(result
|
|
10594
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9097
10595
|
if (isCurrent()) {
|
|
9098
10596
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9099
10597
|
syncSearchStateFromHost();
|
|
@@ -9113,7 +10611,7 @@ function createSuperDocUI(options) {
|
|
|
9113
10611
|
}
|
|
9114
10612
|
syncSearchStateFromHost();
|
|
9115
10613
|
emitSearch();
|
|
9116
|
-
return mapHostReplaceResult(result
|
|
10614
|
+
return mapHostReplaceResult(result);
|
|
9117
10615
|
}
|
|
9118
10616
|
if (typeof host.replaceAll !== "function") return {
|
|
9119
10617
|
ok: false,
|
|
@@ -9132,7 +10630,7 @@ function createSuperDocUI(options) {
|
|
|
9132
10630
|
getSnapshot: stylesSnap.getSnapshot,
|
|
9133
10631
|
subscribe: stylesSnap.subscribe,
|
|
9134
10632
|
observe: stylesSnap.observe,
|
|
9135
|
-
getCatalog: (options
|
|
10633
|
+
getCatalog: (options) => readStyleCatalogLive(options).value,
|
|
9136
10634
|
getQuickGallery: () => stylesSub.get().quickGallery,
|
|
9137
10635
|
getActiveParagraphStyle: () => computeActiveParagraphStyle(state.selection, getStyleCatalog().cache).style
|
|
9138
10636
|
};
|
|
@@ -9158,6 +10656,7 @@ function createSuperDocUI(options) {
|
|
|
9158
10656
|
const destroy = () => {
|
|
9159
10657
|
if (disposed) return;
|
|
9160
10658
|
disposed = true;
|
|
10659
|
+
releaseSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost, uiTrackedChangesCatalogState);
|
|
9161
10660
|
reviewMutationReconciler.reset();
|
|
9162
10661
|
clearAuthoritativeTrackChangesRetry();
|
|
9163
10662
|
if (foregroundAsyncRetryTimer) {
|
|
@@ -9278,6 +10777,12 @@ function createSuperDocUI(options) {
|
|
|
9278
10777
|
controllerRef = controller;
|
|
9279
10778
|
return controller;
|
|
9280
10779
|
}
|
|
10780
|
+
/**
|
|
10781
|
+
* Default font-family offerings, used to backfill the picker when the active document resolves few
|
|
10782
|
+
* (or no) fonts and the runtime picker list is unavailable. Values are logical Word family names —
|
|
10783
|
+
* the formatting command writes them verbatim as logical `w:rFonts` intent; the runtime resolves
|
|
10784
|
+
* substitutes only at measure/paint time.
|
|
10785
|
+
*/
|
|
9281
10786
|
var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
9282
10787
|
{
|
|
9283
10788
|
value: "Arial",
|
|
@@ -9310,6 +10815,11 @@ var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
|
9310
10815
|
previewFamily: "Verdana, sans-serif"
|
|
9311
10816
|
}
|
|
9312
10817
|
];
|
|
10818
|
+
/**
|
|
10819
|
+
* The Word-facing logical family: first family of a CSS stack, quotes stripped.
|
|
10820
|
+
* Document / command values sometimes carry `"Cambria, serif"`; the picker must
|
|
10821
|
+
* advertise `Cambria` so it dedupes against picker/default rows.
|
|
10822
|
+
*/
|
|
9313
10823
|
function canonicalFontFamilyName(family) {
|
|
9314
10824
|
const trimmed = family.trim();
|
|
9315
10825
|
const quote = trimmed[0];
|
|
@@ -9320,6 +10830,10 @@ function canonicalFontFamilyName(family) {
|
|
|
9320
10830
|
const comma = trimmed.indexOf(",");
|
|
9321
10831
|
return (comma === -1 ? trimmed : trimmed.slice(0, comma)).trim().replace(/^["']|["']$/g, "");
|
|
9322
10832
|
}
|
|
10833
|
+
/**
|
|
10834
|
+
* Normalize document-only rows (`DocumentFontOption` with `logicalFamily`) into the custom-UI
|
|
10835
|
+
* picker shape. Raw document families may be CSS stacks, so canonicalize to the first family.
|
|
10836
|
+
*/
|
|
9323
10837
|
function normalizeDocumentFontOptions(raw) {
|
|
9324
10838
|
if (!Array.isArray(raw)) return [];
|
|
9325
10839
|
const out = [];
|
|
@@ -9337,6 +10851,11 @@ function normalizeDocumentFontOptions(raw) {
|
|
|
9337
10851
|
}
|
|
9338
10852
|
return out;
|
|
9339
10853
|
}
|
|
10854
|
+
/**
|
|
10855
|
+
* Normalize picker rows (`FontFamilyOption`) without treating their value as a CSS stack. The font
|
|
10856
|
+
* runtime has already converted quoted document stacks such as `"Acme, Inc Sans", serif` into the
|
|
10857
|
+
* exact apply value (`Acme, Inc Sans`), so preserving `value` avoids truncating internal commas.
|
|
10858
|
+
*/
|
|
9340
10859
|
function normalizePickerFontOptions(raw) {
|
|
9341
10860
|
if (!Array.isArray(raw)) return [];
|
|
9342
10861
|
const out = [];
|
|
@@ -9355,6 +10874,11 @@ function normalizePickerFontOptions(raw) {
|
|
|
9355
10874
|
}
|
|
9356
10875
|
return out;
|
|
9357
10876
|
}
|
|
10877
|
+
/**
|
|
10878
|
+
* Compose document font options on top of the picker/default rows: document rows lead and win on
|
|
10879
|
+
* value collisions (case-insensitive), defaults backfill the rest. Order within each group is
|
|
10880
|
+
* preserved so document-used families stay at the front of the custom-UI picker (SD-3887).
|
|
10881
|
+
*/
|
|
9358
10882
|
function composeFontFamilyOptions(documentOptions, pickerOptions = []) {
|
|
9359
10883
|
const out = [];
|
|
9360
10884
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9411,7 +10935,9 @@ var DEFAULT_FONT_SIZE_OPTIONS = [
|
|
|
9411
10935
|
label: "36"
|
|
9412
10936
|
}
|
|
9413
10937
|
];
|
|
10938
|
+
/** Active state for a routed command: true when its mark is live at the selection. */
|
|
9414
10939
|
function commandIsActive(descriptor, selection) {
|
|
9415
10940
|
return descriptor.activeMark ? selection.activeMarks.includes(descriptor.activeMark) : false;
|
|
9416
10941
|
}
|
|
9417
|
-
|
|
10942
|
+
//#endregion
|
|
10943
|
+
export { createV2ReviewMutationReconciler as a, isV2EditableTextMutationEvent as i, shallowEqual as n, getV2TrackedChangeMutationImpact as o, BUILT_IN_COMMAND_IDS as r, DOM_CLASS_NAMES as s, createSuperDocUI as t };
|