superdoc 2.4.0-next.10 → 2.4.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{FindReplaceSurface-9VkPw_-X.cjs → FindReplaceSurface-0p7EtMZh.cjs} +4 -3
- package/dist/chunks/{FindReplaceSurface-BuvaIrQ5.es.js → FindReplaceSurface-40AnrqWk.es.js} +4 -2
- package/dist/chunks/{PasswordPromptSurface-Bu5xHm91.cjs → PasswordPromptSurface-31lYPcCK.cjs} +4 -3
- package/dist/chunks/{PasswordPromptSurface-Bz9_sKhX.es.js → PasswordPromptSurface-CwYeO3iQ.es.js} +4 -2
- package/dist/chunks/{PdfViewer-CdVGB6Fe.es.js → PdfViewer-D8h7LRZy.es.js} +103 -19
- package/dist/chunks/{PdfViewer-D4eD2dV1.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-ByvmsAAG.cjs → create-super-doc-ui-BWYOinLD.cjs} +1506 -455
- package/dist/chunks/{create-super-doc-ui-DBXOhHdW.es.js → create-super-doc-ui-D0mNmAHJ.es.js} +1695 -416
- 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/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 +35 -0
- package/dist/style.layered.css +35 -0
- package/dist/superdoc.cjs +5774 -1147
- package/dist/superdoc.es.js +5649 -1021
- package/dist-cdn/style.layered.css +1 -1
- package/dist-cdn/superdoc.min.css +1 -1
- package/dist-cdn/superdoc.min.js +36 -36
- package/package.json +14 -14
- package/dist/chunks/_plugin-vue_export-helper-CZ1Nl59K.cjs +0 -11
- package/dist/chunks/_plugin-vue_export-helper-DSMAhhwD.es.js +0 -6
- package/dist/chunks/constants-BfH0br5u.es.js +0 -3
- package/dist/chunks/constants-D7TNtyJd.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-DBXOhHdW.es.js → create-super-doc-ui-D0mNmAHJ.es.js}
RENAMED
|
@@ -1,44 +1,128 @@
|
|
|
1
|
-
|
|
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. */
|
|
2
13
|
PAGE: "superdoc-page",
|
|
14
|
+
/** Fragment container (paragraph, table, image block, etc.). */
|
|
3
15
|
FRAGMENT: "superdoc-fragment",
|
|
16
|
+
/** Line container within a fragment. */
|
|
4
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
|
+
*/
|
|
5
25
|
INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
|
|
26
|
+
/** Inline structured-content label chrome. */
|
|
6
27
|
INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
|
|
28
|
+
/** Block-level structured-content container. */
|
|
7
29
|
BLOCK_SDT: "superdoc-structured-content-block",
|
|
30
|
+
/** Block-level structured-content label chrome. */
|
|
8
31
|
BLOCK_SDT_LABEL: "superdoc-structured-content__label",
|
|
32
|
+
/** Table fragment container (resize overlay and click-mapping target). */
|
|
9
33
|
TABLE_FRAGMENT: "superdoc-table-fragment",
|
|
34
|
+
/** Document section container. */
|
|
10
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
|
+
*/
|
|
11
40
|
SDT_GROUP_HOVER: "sdt-group-hover",
|
|
41
|
+
/** Paragraph fragment rendered as a Table of Contents entry. */
|
|
12
42
|
TOC_ENTRY: "superdoc-toc-entry",
|
|
43
|
+
/** TOC analogue of `SDT_GROUP_HOVER`, applied to every fragment sharing a `data-toc-id`. */
|
|
13
44
|
TOC_GROUP_HOVER: "toc-group-hover",
|
|
45
|
+
/** Block-level image fragment (ImageBlock). */
|
|
14
46
|
IMAGE_FRAGMENT: "superdoc-image-fragment",
|
|
47
|
+
/** Inline image element (ImageRun inside a paragraph). */
|
|
15
48
|
INLINE_IMAGE: "superdoc-inline-image",
|
|
49
|
+
/** Wrapper around a paragraph's list marker (bullet glyph or ordered number). */
|
|
16
50
|
LIST_MARKER: "superdoc-list-marker",
|
|
51
|
+
/** Clip wrapper around a cropped inline image. */
|
|
17
52
|
INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
|
|
53
|
+
/** Field annotation outer wrapper. */
|
|
18
54
|
ANNOTATION: "annotation",
|
|
55
|
+
/** Field annotation inner content wrapper. */
|
|
19
56
|
ANNOTATION_CONTENT: "annotation-content",
|
|
57
|
+
/** Hidden caret anchor span appended after field annotation content. */
|
|
20
58
|
ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
|
|
21
59
|
};
|
|
22
60
|
DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL;
|
|
23
|
-
|
|
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. */
|
|
24
86
|
PM_START: "data-pm-start",
|
|
87
|
+
/** ProseMirror end position of the element's content range. */
|
|
25
88
|
PM_END: "data-pm-end",
|
|
89
|
+
/** Layout epoch stamp — incremented on each layout pass. */
|
|
26
90
|
LAYOUT_EPOCH: "data-layout-epoch",
|
|
91
|
+
/** JSON-encoded table boundary metadata for resize overlays. */
|
|
27
92
|
TABLE_BOUNDARIES: "data-table-boundaries",
|
|
93
|
+
/** SDT unique identifier. */
|
|
28
94
|
SDT_ID: "data-sdt-id",
|
|
95
|
+
/** SDT type (fieldAnnotation, structuredContent, documentSection, etc.). */
|
|
29
96
|
SDT_TYPE: "data-sdt-type",
|
|
97
|
+
/** Field annotation field identifier. */
|
|
30
98
|
FIELD_ID: "data-field-id",
|
|
99
|
+
/** Field annotation field type (signer, text, checkbox, etc.). */
|
|
31
100
|
FIELD_TYPE: "data-field-type",
|
|
101
|
+
/** Marks an element as draggable by the editor. */
|
|
32
102
|
DRAGGABLE: "data-draggable",
|
|
103
|
+
/** Display label text for drag toast / accessibility. */
|
|
33
104
|
DISPLAY_LABEL: "data-display-label",
|
|
105
|
+
/** Field annotation variant (text, image, signature, checkbox, html, link). */
|
|
34
106
|
VARIANT: "data-variant",
|
|
107
|
+
/** Element type discriminator (annotation variant, etc.). */
|
|
35
108
|
TYPE: "data-type",
|
|
109
|
+
/** Schema version for the editor-neutral layout boundary attributes. */
|
|
36
110
|
LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
|
|
111
|
+
/** Stable opaque id of the rendered fragment (see `LayoutFragmentId`). */
|
|
37
112
|
LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
|
|
113
|
+
/** Encoded story locator (e.g. `body`, `header:rId4`, `footer:rId7`). */
|
|
38
114
|
LAYOUT_STORY: "data-layout-story",
|
|
115
|
+
/** Source block reference (today: producer's `blockId`). */
|
|
39
116
|
LAYOUT_BLOCK_REF: "data-layout-block-ref"
|
|
40
117
|
};
|
|
41
|
-
|
|
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) => {
|
|
42
126
|
if (!raw) return { kind: "unknown" };
|
|
43
127
|
if (raw === "body") return { kind: "body" };
|
|
44
128
|
const idx = raw.indexOf(":");
|
|
@@ -59,6 +143,19 @@ const decodeLayoutStoryDataset = (raw) => {
|
|
|
59
143
|
};
|
|
60
144
|
`${DOM_CLASS_NAMES.BLOCK_SDT}${DATA_ATTRS.SDT_ID}`;
|
|
61
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
|
+
*/
|
|
62
159
|
function getParagraphInlineDirection(attrs) {
|
|
63
160
|
const fromContext = attrs?.directionContext?.inlineDirection;
|
|
64
161
|
if (fromContext != null) return fromContext;
|
|
@@ -66,74 +163,261 @@ function getParagraphInlineDirection(attrs) {
|
|
|
66
163
|
if (ppRtl === true) return "rtl";
|
|
67
164
|
if (ppRtl === false) return "ltr";
|
|
68
165
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
"#116329",
|
|
81
|
-
"#7d4e00"
|
|
82
|
-
];
|
|
83
|
-
const authorIdentityKey = (author) => {
|
|
84
|
-
if (!author) return "";
|
|
85
|
-
return `${typeof author.name === "string" ? author.name : ""} ${typeof author.email === "string" ? author.email : ""}`;
|
|
86
|
-
};
|
|
87
|
-
var hashString = (value) => {
|
|
88
|
-
let hash = 2166136261;
|
|
89
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
90
|
-
hash ^= value.charCodeAt(i);
|
|
91
|
-
hash = Math.imul(hash, 16777619);
|
|
92
|
-
}
|
|
93
|
-
return hash >>> 0;
|
|
94
|
-
};
|
|
95
|
-
const fallbackAuthorColor = (author) => {
|
|
96
|
-
return FALLBACK_PALETTE[hashString(authorIdentityKey(author)) % FALLBACK_PALETTE.length];
|
|
97
|
-
};
|
|
98
|
-
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
99
|
-
const composeAuthorColorResolver = (config) => {
|
|
100
|
-
if (!config || config.enabled === false) return void 0;
|
|
101
|
-
const overrides = config.overrides && typeof config.overrides === "object" ? config.overrides : void 0;
|
|
102
|
-
const resolve = typeof config.resolve === "function" ? config.resolve : void 0;
|
|
103
|
-
return (author) => {
|
|
104
|
-
const safeAuthor = author ?? {};
|
|
105
|
-
if (overrides) {
|
|
106
|
-
if (isNonEmptyString(safeAuthor.email) && isNonEmptyString(overrides[safeAuthor.email])) return overrides[safeAuthor.email];
|
|
107
|
-
if (isNonEmptyString(safeAuthor.name) && isNonEmptyString(overrides[safeAuthor.name])) return overrides[safeAuthor.name];
|
|
108
|
-
}
|
|
109
|
-
if (resolve) try {
|
|
110
|
-
const resolved = resolve(safeAuthor);
|
|
111
|
-
if (isNonEmptyString(resolved)) return resolved;
|
|
112
|
-
} catch {}
|
|
113
|
-
return fallbackAuthorColor(safeAuthor);
|
|
114
|
-
};
|
|
115
|
-
};
|
|
116
|
-
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. */
|
|
117
177
|
externalImageDeferred: "render.media.external-image-deferred",
|
|
178
|
+
/** Drawing references a relationship id that does not exist on the owner part. */
|
|
118
179
|
missingRelationship: "render.drawing.missing-relationship",
|
|
180
|
+
/** Relationship resolves but the target media part bytes are missing/empty. */
|
|
119
181
|
missingMediaPart: "render.media.missing-part",
|
|
182
|
+
/** Relationship exists but is not an image relationship type. */
|
|
120
183
|
unsupportedRelationshipType: "render.drawing.unsupported-relationship-type",
|
|
184
|
+
/** Media MIME is not in the supported allowlist. */
|
|
121
185
|
unsupportedMime: "render.media.unsupported-mime",
|
|
186
|
+
/** Media part exceeds the host byte-size policy before bytes are materialized. */
|
|
122
187
|
imageTooLarge: "render.media.image-too-large",
|
|
188
|
+
/** SVG content rejected by the SVG safety policy. */
|
|
123
189
|
unsafeSvg: "render.media.unsafe-svg",
|
|
190
|
+
/** Metafile / TIFF (EMF, WMF, TIFF) with no approved conversion path. */
|
|
124
191
|
unsupportedFormat: "render.media.unsupported-format",
|
|
192
|
+
/** OLE / ActiveX / embedded package object. */
|
|
125
193
|
embeddedObjectNotSupported: "render.embedded-object-not-supported",
|
|
194
|
+
/** SmartArt / diagram / unknown external graphic-data object. */
|
|
126
195
|
unsupportedObject: "render.drawing.unsupported-object",
|
|
196
|
+
/** VML structure outside any supported image-like subset. */
|
|
127
197
|
vmlUnsupported: "render.drawing.vml-unsupported",
|
|
198
|
+
/** VML image-like content that could not be promoted to a supported image. */
|
|
128
199
|
vmlImageUnsupported: "render.drawing.vml-image-unsupported",
|
|
200
|
+
/** Custom geometry command the extractor does not implement (e.g. `arcTo`). */
|
|
129
201
|
unsupportedGeometryCommand: "render.drawing.unsupported-geometry-command",
|
|
202
|
+
/** Anchor fields required for honest placement are unsupported. */
|
|
130
203
|
anchorUnsupported: "render.drawing.anchor-unsupported",
|
|
204
|
+
/** Wrap fields required for honest placement are unsupported. */
|
|
131
205
|
wrapUnsupported: "render.drawing.wrap-unsupported",
|
|
206
|
+
/** `mc:AlternateContent` had no supported choice and no usable fallback. */
|
|
132
207
|
altContentNoSupportedChoice: "render.drawing.altcontent-no-supported-choice",
|
|
208
|
+
/** A child of an otherwise-supported group is unsupported. */
|
|
133
209
|
groupChildUnsupported: "render.drawing.group-child-unsupported"
|
|
134
210
|
};
|
|
135
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;
|
|
136
|
-
|
|
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);
|
|
137
421
|
Object.freeze([
|
|
138
422
|
"multiple-sections",
|
|
139
423
|
"furniture-page-tokens",
|
|
@@ -145,18 +429,31 @@ Object.freeze([
|
|
|
145
429
|
"tables",
|
|
146
430
|
"furniture-anchored-objects"
|
|
147
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
|
+
*/
|
|
148
444
|
function getV2TrackedChangeMutationImpact(event) {
|
|
149
445
|
if (event?.type !== "mutation:committed") return null;
|
|
150
446
|
const payload = event.origin === "history" ? event.result : event.receipt;
|
|
151
447
|
if (!payload || typeof payload !== "object") return null;
|
|
152
448
|
const upsertIds = /* @__PURE__ */ new Set();
|
|
153
449
|
const removedIds = /* @__PURE__ */ new Set();
|
|
450
|
+
/** @type {Array<{ from: string, to: string }>} */
|
|
154
451
|
const remappedPairs = [];
|
|
155
452
|
const collectEntity = (entry, into) => {
|
|
156
453
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return;
|
|
157
454
|
if (typeof entry.entityId === "string" && entry.entityId.length > 0) into.add(entry.entityId);
|
|
158
455
|
};
|
|
159
|
-
const readEntityId
|
|
456
|
+
const readEntityId = (entry) => {
|
|
160
457
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return null;
|
|
161
458
|
return typeof entry.entityId === "string" && entry.entityId.length > 0 ? entry.entityId : null;
|
|
162
459
|
};
|
|
@@ -166,8 +463,8 @@ function getV2TrackedChangeMutationImpact(event) {
|
|
|
166
463
|
if (Array.isArray(payload.removed)) payload.removed.forEach((entry) => collectEntity(entry, removedIds));
|
|
167
464
|
if (Array.isArray(payload.invalidatedRefs)) payload.invalidatedRefs.forEach((entry) => collectEntity(entry, removedIds));
|
|
168
465
|
if (Array.isArray(payload.remappedRefs)) for (const mapping of payload.remappedRefs) {
|
|
169
|
-
const fromId = readEntityId
|
|
170
|
-
const toId = readEntityId
|
|
466
|
+
const fromId = readEntityId(mapping?.from);
|
|
467
|
+
const toId = readEntityId(mapping?.to);
|
|
171
468
|
if (fromId) removedIds.add(fromId);
|
|
172
469
|
if (toId) upsertIds.add(toId);
|
|
173
470
|
if (fromId && toId && fromId !== toId) remappedPairs.push({
|
|
@@ -191,6 +488,8 @@ function readAllResolvedFact(event, receipt) {
|
|
|
191
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;
|
|
192
489
|
return fact;
|
|
193
490
|
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/helpers/v2-review-mutation-reconciler.js
|
|
194
493
|
var normalizeIds = (values) => new Set(Array.from(values ?? []).filter((value) => typeof value === "string" && value.length > 0));
|
|
195
494
|
var itemId = (item) => {
|
|
196
495
|
if (typeof item?.id === "string" && item.id.length > 0) return item.id;
|
|
@@ -199,6 +498,13 @@ var itemId = (item) => {
|
|
|
199
498
|
return null;
|
|
200
499
|
};
|
|
201
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
|
+
*/
|
|
202
508
|
function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onReconciled }) {
|
|
203
509
|
const pendingUpserts = /* @__PURE__ */ new Map();
|
|
204
510
|
let pendingAllResolved = null;
|
|
@@ -266,7 +572,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
266
572
|
reconcileMode: "targeted"
|
|
267
573
|
});
|
|
268
574
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
269
|
-
|
|
575
|
+
const resolvedIds = idsFromResult(result);
|
|
576
|
+
const clearedIds = clearResolved(resolvedIds, snapshot);
|
|
577
|
+
await notifyReconciled(context, clearedIds, result);
|
|
270
578
|
if (new Set([...snapshot].filter(([id, version]) => pendingUpserts.get(id) === version).map(([id]) => id)).size === 0) return;
|
|
271
579
|
const fallback = await hydrate(context, {
|
|
272
580
|
trackedChangesListMode: "interaction-prime",
|
|
@@ -274,7 +582,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
274
582
|
blocking: false
|
|
275
583
|
});
|
|
276
584
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
277
|
-
|
|
585
|
+
const fallbackResolvedIds = idsFromResult(fallback);
|
|
586
|
+
const fallbackClearedIds = clearResolved(fallbackResolvedIds, snapshot);
|
|
587
|
+
await notifyReconciled(context, fallbackClearedIds, fallback);
|
|
278
588
|
};
|
|
279
589
|
const reconcileAuthoritativeSnapshot = async (context, snapshot, capturedGeneration) => {
|
|
280
590
|
const result = await hydrate(context, {
|
|
@@ -398,7 +708,7 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
398
708
|
resolve
|
|
399
709
|
});
|
|
400
710
|
});
|
|
401
|
-
const impactedIds = new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
|
|
711
|
+
const impactedIds = /* @__PURE__ */ new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
|
|
402
712
|
const capturedEntries = new Map([...impactedIds].flatMap((id) => {
|
|
403
713
|
const entry = pendingUpserts.get(id);
|
|
404
714
|
return entry ? [[id, entry]] : [];
|
|
@@ -452,7 +762,19 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
452
762
|
}
|
|
453
763
|
};
|
|
454
764
|
}
|
|
455
|
-
|
|
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([
|
|
456
778
|
"insert-text",
|
|
457
779
|
"replace-text",
|
|
458
780
|
"plain-text-paste",
|
|
@@ -474,33 +796,158 @@ var V2_TYPING_COMMAND_KINDS = new Set([
|
|
|
474
796
|
"backspace:list-remove",
|
|
475
797
|
"backspace:list-outdent"
|
|
476
798
|
]);
|
|
477
|
-
|
|
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) => {
|
|
478
812
|
if (event?.type !== "mutation:committed" || event.origin !== "command") return false;
|
|
479
813
|
return V2_TYPING_COMMAND_KINDS.has(event.editableCommandKind);
|
|
480
814
|
};
|
|
481
|
-
|
|
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. */
|
|
482
847
|
notReady: "not-ready",
|
|
848
|
+
/** The editor is ready but its Document API facade is not available or has not been published yet. */
|
|
483
849
|
documentApiUnavailable: "document-api-unavailable",
|
|
850
|
+
/** The document is in viewing / read-only mode, so mutating commands are blocked. */
|
|
484
851
|
documentReadonly: "document-readonly",
|
|
852
|
+
/** A selection is required and none is active. */
|
|
485
853
|
selectionRequired: "selection-required",
|
|
854
|
+
/** A non-empty (range) selection is required and the current selection is collapsed/empty. */
|
|
486
855
|
rangeSelectionRequired: "range-selection-required",
|
|
856
|
+
/** Required pointer / entity / document context could not be resolved. */
|
|
487
857
|
contextUnavailable: "context-unavailable",
|
|
858
|
+
/** Painted geometry for the target could not be resolved. */
|
|
488
859
|
geometryUnavailable: "geometry-unavailable",
|
|
860
|
+
/** A navigation / geometry target could not be resolved to a live document address. */
|
|
489
861
|
targetUnresolved: "target-unresolved",
|
|
862
|
+
/** A navigation target was resolved but could not be brought into the visible viewport. */
|
|
490
863
|
targetNotVisible: "target-not-visible",
|
|
864
|
+
/** The command is not supported by v2 at all. */
|
|
491
865
|
commandUnsupported: "command-unsupported",
|
|
866
|
+
/** Reserved migration reason for a recognized command that is intentionally deferred. */
|
|
492
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
|
+
*/
|
|
493
876
|
tableContextUnavailable: "table-context-unavailable",
|
|
877
|
+
/** The backing Document API / SuperDoc operation is not present on this host. */
|
|
494
878
|
operationUnavailable: "operation-unavailable",
|
|
879
|
+
/** Bulk accept/reject decisions are disabled unless the host opts in. */
|
|
495
880
|
bulkDecisionsDisabled: "bulk-decisions-disabled",
|
|
881
|
+
/** A host-owned capability (geometry, hit-test, navigation) is unavailable. */
|
|
496
882
|
hostCapabilityUnavailable: "host-capability-unavailable",
|
|
883
|
+
/** Undo / redo cannot run because the host history stack has no matching entry. */
|
|
497
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
|
+
*/
|
|
498
891
|
searchUnavailable: "search-unavailable",
|
|
892
|
+
/** The search pattern is an invalid or unsafe regular expression. */
|
|
499
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
|
+
*/
|
|
500
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
|
+
*/
|
|
501
908
|
contentControlLocked: "content-control-locked"
|
|
502
909
|
};
|
|
503
|
-
|
|
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 = {
|
|
504
951
|
bold: "bold",
|
|
505
952
|
italic: "italic",
|
|
506
953
|
underline: "underline",
|
|
@@ -518,6 +965,7 @@ const BUILT_IN_COMMAND_IDS = {
|
|
|
518
965
|
bulletList: "bullet-list",
|
|
519
966
|
numberedList: "numbered-list"
|
|
520
967
|
};
|
|
968
|
+
/** Strip a trailing `pt` unit from v1-style font sizes (`"12pt"` → `"12"`). */
|
|
521
969
|
function normalizeFontSizePayload(payload) {
|
|
522
970
|
const strip = (value) => typeof value === "string" ? value.replace(/\s*pt$/i, "").trim() : value;
|
|
523
971
|
if (typeof payload === "string") return strip(payload);
|
|
@@ -530,6 +978,12 @@ function normalizeFontSizePayload(payload) {
|
|
|
530
978
|
}
|
|
531
979
|
return payload;
|
|
532
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
|
+
*/
|
|
533
987
|
function normalizeZoomPayload(payload) {
|
|
534
988
|
const normalizeNumber = (value, percentLiteral) => {
|
|
535
989
|
if (!percentLiteral && value > 0 && value <= 5) return value * 100;
|
|
@@ -544,6 +998,12 @@ function normalizeZoomPayload(payload) {
|
|
|
544
998
|
if (typeof payload === "number" && Number.isFinite(payload)) return normalizeNumber(payload, false);
|
|
545
999
|
return payload;
|
|
546
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
|
+
*/
|
|
547
1007
|
function normalizeMeasurementUnitPayload(payload) {
|
|
548
1008
|
const raw = payload && typeof payload === "object" && "value" in payload ? payload.value : payload;
|
|
549
1009
|
if (raw === "in" || raw === "cm") return raw;
|
|
@@ -554,6 +1014,12 @@ function normalizeMeasurementUnitPayload(payload) {
|
|
|
554
1014
|
}
|
|
555
1015
|
return raw;
|
|
556
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
|
+
*/
|
|
557
1023
|
function normalizeColorPayload(payload) {
|
|
558
1024
|
const coerce = (value) => {
|
|
559
1025
|
if (value === null) return null;
|
|
@@ -573,6 +1039,7 @@ function normalizeColorPayload(payload) {
|
|
|
573
1039
|
}
|
|
574
1040
|
return coerce(payload);
|
|
575
1041
|
}
|
|
1042
|
+
/** Unwrap a `{ value }` / `{ alignment } ` / `{ styleId }` wrapper to its scalar. */
|
|
576
1043
|
function unwrapScalar(payload, keys) {
|
|
577
1044
|
if (payload && typeof payload === "object") {
|
|
578
1045
|
const record = payload;
|
|
@@ -580,6 +1047,7 @@ function unwrapScalar(payload, keys) {
|
|
|
580
1047
|
}
|
|
581
1048
|
return payload;
|
|
582
1049
|
}
|
|
1050
|
+
/** Normalize a v1 alignment payload (`"left"`, `{ value }`, `"justified"`) to a `ParagraphAlignment`. */
|
|
583
1051
|
function normalizeAlignmentPayload(payload) {
|
|
584
1052
|
const raw = unwrapScalar(payload, ["alignment", "value"]);
|
|
585
1053
|
if (typeof raw !== "string") return raw;
|
|
@@ -587,6 +1055,11 @@ function normalizeAlignmentPayload(payload) {
|
|
|
587
1055
|
if (v === "justified" || v === "both") return "justify";
|
|
588
1056
|
return v;
|
|
589
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
|
+
*/
|
|
590
1063
|
function normalizeLineHeightPayload(payload) {
|
|
591
1064
|
const raw = unwrapScalar(payload, [
|
|
592
1065
|
"line",
|
|
@@ -597,6 +1070,7 @@ function normalizeLineHeightPayload(payload) {
|
|
|
597
1070
|
if (!Number.isFinite(num) || num <= 0) return raw;
|
|
598
1071
|
return num <= 10 ? Math.round(num * 240) : Math.round(num);
|
|
599
1072
|
}
|
|
1073
|
+
/** Preserve semantic style intent; normalize legacy linked-style payloads to a concrete ID. */
|
|
600
1074
|
function normalizeStyleIdPayload(payload) {
|
|
601
1075
|
if (payload && typeof payload === "object" && "role" in payload) return payload;
|
|
602
1076
|
const raw = unwrapScalar(payload, [
|
|
@@ -606,6 +1080,7 @@ function normalizeStyleIdPayload(payload) {
|
|
|
606
1080
|
]);
|
|
607
1081
|
return typeof raw === "string" ? raw.trim() : raw;
|
|
608
1082
|
}
|
|
1083
|
+
/** Resolve a document-mode payload (`"viewing"` or `{ mode }`) to the mode string. */
|
|
609
1084
|
function normalizeDocumentModePayload(payload) {
|
|
610
1085
|
if (typeof payload === "string") return payload;
|
|
611
1086
|
if (payload && typeof payload === "object") {
|
|
@@ -616,7 +1091,14 @@ function normalizeDocumentModePayload(payload) {
|
|
|
616
1091
|
}
|
|
617
1092
|
var UNSUPPORTED = SUPERDOC_UI_REASONS.commandUnsupported;
|
|
618
1093
|
var TABLE_CONTEXT = SUPERDOC_UI_REASONS.tableContextUnavailable;
|
|
619
|
-
|
|
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 = [
|
|
620
1102
|
{
|
|
621
1103
|
id: "bold",
|
|
622
1104
|
family: "inline marks",
|
|
@@ -1136,10 +1618,23 @@ const COMMAND_CATALOG = [
|
|
|
1136
1618
|
}
|
|
1137
1619
|
];
|
|
1138
1620
|
var CATALOG_BY_ID = new Map(COMMAND_CATALOG.map((descriptor) => [descriptor.id, descriptor]));
|
|
1139
|
-
|
|
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. */
|
|
1140
1624
|
function getCommandDescriptor(id) {
|
|
1141
1625
|
return CATALOG_BY_ID.get(id) ?? null;
|
|
1142
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
|
+
*/
|
|
1143
1638
|
function shallowEqual(a, b) {
|
|
1144
1639
|
if (Object.is(a, b)) return true;
|
|
1145
1640
|
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
@@ -1153,6 +1648,16 @@ function shallowEqual(a, b) {
|
|
|
1153
1648
|
}
|
|
1154
1649
|
return true;
|
|
1155
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
|
+
*/
|
|
1156
1661
|
function sdRunPropsToInlineRunPatch(props) {
|
|
1157
1662
|
if (!props || typeof props !== "object") return {};
|
|
1158
1663
|
const patch = {};
|
|
@@ -1243,6 +1748,16 @@ function projectBorderToInlinePatch(border) {
|
|
|
1243
1748
|
if (color != null) patch.color = color;
|
|
1244
1749
|
return Object.keys(patch).length > 0 ? patch : void 0;
|
|
1245
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
|
+
*/
|
|
1246
1761
|
function selectionKey(selection) {
|
|
1247
1762
|
const target = selection?.selectionTarget ?? selection?.target;
|
|
1248
1763
|
if (!selection || !target) return "";
|
|
@@ -1252,9 +1767,21 @@ function selectionKey(selection) {
|
|
|
1252
1767
|
return String(target);
|
|
1253
1768
|
}
|
|
1254
1769
|
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/public/ui/entity-at.ts
|
|
1255
1772
|
function parseCommaSeparatedIds(value) {
|
|
1256
1773
|
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
1257
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
|
+
*/
|
|
1258
1785
|
function getTrackChangeIds(node) {
|
|
1259
1786
|
const out = [];
|
|
1260
1787
|
const add = (id) => {
|
|
@@ -1266,6 +1793,21 @@ function getTrackChangeIds(node) {
|
|
|
1266
1793
|
add(node.getAttribute("data-track-change-id"));
|
|
1267
1794
|
return out;
|
|
1268
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
|
+
*/
|
|
1269
1811
|
function collectEntityHitsFromChain(start, stopAt) {
|
|
1270
1812
|
if (!start || typeof start.getAttribute !== "function") return [];
|
|
1271
1813
|
const hits = [];
|
|
@@ -1327,6 +1869,8 @@ function collectEntityHitsFromChain(start, stopAt) {
|
|
|
1327
1869
|
}
|
|
1328
1870
|
return hits;
|
|
1329
1871
|
}
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region ../document-api/src/format/inline-run-patch.ts
|
|
1330
1874
|
var schemaBooleanOrNull = () => ({ oneOf: [{ type: "boolean" }, { type: "null" }] });
|
|
1331
1875
|
var schemaStringOrNull = () => ({ oneOf: [{
|
|
1332
1876
|
type: "string",
|
|
@@ -1418,7 +1962,7 @@ var runAttribute = (key, type, ooxmlElement, schema, runPropertyKey) => ({
|
|
|
1418
1962
|
carrier: runAttributeCarrier(runPropertyKey ?? key),
|
|
1419
1963
|
schema
|
|
1420
1964
|
});
|
|
1421
|
-
|
|
1965
|
+
var INLINE_PROPERTY_REGISTRY = [
|
|
1422
1966
|
markBoolean("bold", "w:b", "bold"),
|
|
1423
1967
|
markBoolean("italic", "w:i", "italic"),
|
|
1424
1968
|
markBoolean("strike", "w:strike", "strike"),
|
|
@@ -1576,8 +2120,23 @@ const INLINE_PROPERTY_REGISTRY = [
|
|
|
1576
2120
|
runAttribute("contextualAlternates", "boolean", "w14:cntxtAlts", schemaBooleanOrNull())
|
|
1577
2121
|
];
|
|
1578
2122
|
new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key));
|
|
1579
|
-
|
|
2123
|
+
var INLINE_PROPERTY_BY_KEY = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry]));
|
|
1580
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
|
+
*/
|
|
1581
2140
|
var sharedUiTrackedChangesCatalogByHost = /* @__PURE__ */ new WeakMap();
|
|
1582
2141
|
function acquireSharedUiTrackedChangesCatalog(host) {
|
|
1583
2142
|
let state = sharedUiTrackedChangesCatalogByHost.get(host);
|
|
@@ -1606,6 +2165,12 @@ var SUPERDOC_UI_REASON_VALUES = new Set(Object.values(SUPERDOC_UI_REASONS));
|
|
|
1606
2165
|
function coerceSuperDocUIReason(reason, fallback) {
|
|
1607
2166
|
return typeof reason === "string" && SUPERDOC_UI_REASON_VALUES.has(reason) ? reason : fallback;
|
|
1608
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
|
+
*/
|
|
1609
2174
|
function normalizeHostSelectionApplyResult(result) {
|
|
1610
2175
|
if (result && typeof result === "object") {
|
|
1611
2176
|
if (result.ok === false) {
|
|
@@ -1622,6 +2187,7 @@ function normalizeHostSelectionApplyResult(result) {
|
|
|
1622
2187
|
}
|
|
1623
2188
|
return { ok: true };
|
|
1624
2189
|
}
|
|
2190
|
+
/** Lifecycle events the controller subscribes to. */
|
|
1625
2191
|
var HOST_EVENTS = [
|
|
1626
2192
|
"document-replaced",
|
|
1627
2193
|
"editorCreate",
|
|
@@ -1632,6 +2198,12 @@ var HOST_EVENTS = [
|
|
|
1632
2198
|
"viewport-change",
|
|
1633
2199
|
"fonts-changed"
|
|
1634
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
|
+
*/
|
|
1635
2207
|
var LIST_TOGGLE_KINDS = {
|
|
1636
2208
|
[BUILT_IN_COMMAND_IDS.bulletList]: "bullet",
|
|
1637
2209
|
[BUILT_IN_COMMAND_IDS.numberedList]: "ordered"
|
|
@@ -1639,6 +2211,15 @@ var LIST_TOGGLE_KINDS = {
|
|
|
1639
2211
|
function listToggleKind(id) {
|
|
1640
2212
|
return LIST_TOGGLE_KINDS[id] ?? null;
|
|
1641
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
|
+
*/
|
|
1642
2223
|
var TOOLBAR_LIST_STYLE_PRESETS = {
|
|
1643
2224
|
disc: "disc",
|
|
1644
2225
|
circle: "circle",
|
|
@@ -1669,7 +2250,7 @@ var EMPTY_SELECTION = {
|
|
|
1669
2250
|
activeChangeIds: [],
|
|
1670
2251
|
quotedText: ""
|
|
1671
2252
|
};
|
|
1672
|
-
|
|
2253
|
+
var HEAVY_DOC_READ_POLICY = [
|
|
1673
2254
|
{
|
|
1674
2255
|
key: "contentControls",
|
|
1675
2256
|
match: "exact",
|
|
@@ -1716,19 +2297,22 @@ const HEAVY_DOC_READ_POLICY = [
|
|
|
1716
2297
|
note: "reserved: full sections list"
|
|
1717
2298
|
}
|
|
1718
2299
|
];
|
|
1719
|
-
var COMMENTS_CATALOG_PART_URIS = new Set([
|
|
2300
|
+
var COMMENTS_CATALOG_PART_URIS = /* @__PURE__ */ new Set([
|
|
1720
2301
|
"/word/comments.xml",
|
|
1721
2302
|
"/word/commentsExtended.xml",
|
|
1722
2303
|
"/word/commentsIds.xml"
|
|
1723
2304
|
]);
|
|
2305
|
+
/** Whether a coordinator cache key falls under the heavy-read policy. */
|
|
1724
2306
|
function isHeavyDocReadKey(key) {
|
|
1725
2307
|
return HEAVY_DOC_READ_POLICY.some((entry) => entry.match === "exact" ? entry.key === key : key.startsWith(entry.key));
|
|
1726
2308
|
}
|
|
2309
|
+
/** Status precedence: a combined status is as "unsettled" as its worst part. */
|
|
1727
2310
|
var STATUS_RANK = {
|
|
1728
2311
|
ready: 0,
|
|
1729
2312
|
stale: 1,
|
|
1730
2313
|
pending: 2
|
|
1731
2314
|
};
|
|
2315
|
+
/** Combine read statuses; `pending` (no data yet) dominates `stale`, which dominates `ready`. */
|
|
1732
2316
|
function combineStatus(...statuses) {
|
|
1733
2317
|
let worst = "ready";
|
|
1734
2318
|
for (const status of statuses) if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;
|
|
@@ -1746,6 +2330,7 @@ function safeCall(fn, fallback) {
|
|
|
1746
2330
|
function isPromiseLike(value) {
|
|
1747
2331
|
return Boolean(value) && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
1748
2332
|
}
|
|
2333
|
+
/** Resolve a dotted path (e.g. `format.bold`) to a callable on the doc facade. */
|
|
1749
2334
|
function resolveDocOperation(doc, path) {
|
|
1750
2335
|
if (!doc) return null;
|
|
1751
2336
|
const parts = path.split(".");
|
|
@@ -1780,6 +2365,7 @@ function partialLinkEditFailure(hyperlinkResult, textResult) {
|
|
|
1780
2365
|
textResult
|
|
1781
2366
|
};
|
|
1782
2367
|
}
|
|
2368
|
+
/** Read a stable public `target` off a doc-api entity row. */
|
|
1783
2369
|
function readEntityTarget(row) {
|
|
1784
2370
|
if (!row || typeof row !== "object") return null;
|
|
1785
2371
|
const target = row.target;
|
|
@@ -1796,8 +2382,8 @@ function isHostGeometryTarget(target) {
|
|
|
1796
2382
|
const isTextSegment = (segment) => {
|
|
1797
2383
|
if (!segment || typeof segment !== "object") return false;
|
|
1798
2384
|
const candidate = segment;
|
|
1799
|
-
const range
|
|
1800
|
-
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";
|
|
1801
2387
|
};
|
|
1802
2388
|
if (Array.isArray(record.segments)) {
|
|
1803
2389
|
const first = record.segments[0];
|
|
@@ -1809,6 +2395,12 @@ function isHostGeometryTarget(target) {
|
|
|
1809
2395
|
const range = record.range;
|
|
1810
2396
|
return typeof range?.start === "number" && typeof range.end === "number";
|
|
1811
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
|
+
*/
|
|
1812
2404
|
function readTrackedChangeNavigationTarget(row) {
|
|
1813
2405
|
if (!row || typeof row !== "object") return null;
|
|
1814
2406
|
const target = readEntityTarget(row);
|
|
@@ -1867,11 +2459,35 @@ function entityRowMatchesRequest(row, id, story) {
|
|
|
1867
2459
|
if (!story) return true;
|
|
1868
2460
|
return storyLocatorSignature(readEntityStory(row)) === storyLocatorSignature(story);
|
|
1869
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
|
+
*/
|
|
1870
2471
|
function readEntityRequestStory(row) {
|
|
1871
2472
|
const story = readEntityStory(row);
|
|
1872
2473
|
if (!story || storyLocatorSignature(story) === storyLocatorSignature(null)) return void 0;
|
|
1873
2474
|
return story;
|
|
1874
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
|
+
*/
|
|
1875
2491
|
function layoutStoryDatasetToStoryLocator(raw) {
|
|
1876
2492
|
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
1877
2493
|
const decoded = decodeLayoutStoryDataset(raw);
|
|
@@ -1896,6 +2512,22 @@ function layoutStoryDatasetToStoryLocator(raw) {
|
|
|
1896
2512
|
default: return;
|
|
1897
2513
|
}
|
|
1898
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
|
+
*/
|
|
1899
2531
|
function storyKeyToStoryLocator(raw) {
|
|
1900
2532
|
if (typeof raw !== "string" || raw.length === 0 || raw === "body") return void 0;
|
|
1901
2533
|
if (raw.startsWith("hf:part:")) {
|
|
@@ -1923,6 +2555,7 @@ function storyKeyToStoryLocator(raw) {
|
|
|
1923
2555
|
} : void 0;
|
|
1924
2556
|
}
|
|
1925
2557
|
}
|
|
2558
|
+
/** Read a public `selectionTarget` off a content-control row when available. */
|
|
1926
2559
|
function readSelectionTarget(row) {
|
|
1927
2560
|
if (!row || typeof row !== "object") return null;
|
|
1928
2561
|
const target = row.selectionTarget;
|
|
@@ -1944,12 +2577,19 @@ function collapseSelectionTargetToCaret(target) {
|
|
|
1944
2577
|
end: start
|
|
1945
2578
|
};
|
|
1946
2579
|
}
|
|
2580
|
+
/** Read a stable public row id from discovery/list/detail shapes. */
|
|
1947
2581
|
function readEntityId(row) {
|
|
1948
2582
|
if (!row || typeof row !== "object") return null;
|
|
1949
2583
|
const record = row;
|
|
1950
2584
|
const id = record.id ?? record.commentId ?? record.changeId;
|
|
1951
2585
|
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1952
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
|
+
*/
|
|
1953
2593
|
function readCollapsedHostTrackChangeId(snapshot) {
|
|
1954
2594
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
1955
2595
|
const record = snapshot;
|
|
@@ -1962,6 +2602,11 @@ function readCollapsedHostTrackChangeId(snapshot) {
|
|
|
1962
2602
|
return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
|
|
1963
2603
|
}
|
|
1964
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
|
+
*/
|
|
1965
2610
|
function readTrackChangeProvenanceIds(item) {
|
|
1966
2611
|
const out = [];
|
|
1967
2612
|
const push = (value) => {
|
|
@@ -2002,6 +2647,7 @@ function readTrackChangeProvenanceIds(item) {
|
|
|
2002
2647
|
if (change && change !== item) scan(change);
|
|
2003
2648
|
return out;
|
|
2004
2649
|
}
|
|
2650
|
+
/** Build (or reuse) the alias↔canonical context for a tracked-change list slice. */
|
|
2005
2651
|
function buildTrackedChangeIdContext(items) {
|
|
2006
2652
|
const cached = trackedChangeIdContextCache.get(items);
|
|
2007
2653
|
if (cached) return cached;
|
|
@@ -2034,6 +2680,19 @@ function buildTrackedChangeIdContext(items) {
|
|
|
2034
2680
|
}
|
|
2035
2681
|
var storyScopedTrackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
|
|
2036
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
|
+
*/
|
|
2037
2696
|
function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
2038
2697
|
const signature = storyLocatorSignature(story);
|
|
2039
2698
|
let byStory = storyScopedTrackedChangeIdContextCache.get(items);
|
|
@@ -2050,6 +2709,21 @@ function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
|
2050
2709
|
function isTextboxStory(story) {
|
|
2051
2710
|
return !!story && typeof story === "object" && story.storyType === "textbox";
|
|
2052
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
|
+
*/
|
|
2053
2727
|
function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
2054
2728
|
const storyless = selectionIds.filter((id) => items.some((item) => readEntityId(item) === id && readEntityStory(item) == null));
|
|
2055
2729
|
if (storyless.length === 0) return null;
|
|
@@ -2065,6 +2739,13 @@ function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
|
2065
2739
|
}
|
|
2066
2740
|
return storyless[0];
|
|
2067
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
|
+
*/
|
|
2068
2749
|
function declaredBodyItems(items) {
|
|
2069
2750
|
const cached = declaredBodyItemsCache.get(items);
|
|
2070
2751
|
if (cached) return cached;
|
|
@@ -2073,6 +2754,17 @@ function declaredBodyItems(items) {
|
|
|
2073
2754
|
declaredBodyItemsCache.set(items, result);
|
|
2074
2755
|
return result;
|
|
2075
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
|
+
*/
|
|
2076
2768
|
function resolveSelectionActiveChangeId(options) {
|
|
2077
2769
|
const { selectionIds, publicIds, selection, bodyItems, allStoryItems } = options;
|
|
2078
2770
|
if (selectionIds.length === 0) return null;
|
|
@@ -2097,11 +2789,20 @@ function resolveSelectionActiveChangeId(options) {
|
|
|
2097
2789
|
if (isBodySelection) return mapIds(buildTrackedChangeIdContext(bodyItems)) ?? selectionIds[0] ?? null;
|
|
2098
2790
|
return null;
|
|
2099
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
|
+
*/
|
|
2100
2800
|
function resolveActiveCommentId(items, commentId) {
|
|
2101
2801
|
const item = items.find((candidate) => readEntityId(candidate) === commentId || candidate.importedId === commentId);
|
|
2102
2802
|
if (!item) return null;
|
|
2103
2803
|
return item.rootCommentId ?? item.parentCommentId ?? readEntityId(item) ?? commentId;
|
|
2104
2804
|
}
|
|
2805
|
+
/** Derive a `{ startBlockId, endBlockId }` block range from a selection slice. */
|
|
2105
2806
|
function selectionBlockRange(selection) {
|
|
2106
2807
|
const sel = selection.selectionTarget;
|
|
2107
2808
|
if (sel) {
|
|
@@ -2124,6 +2825,16 @@ function selectionBlockRange(selection) {
|
|
|
2124
2825
|
};
|
|
2125
2826
|
return null;
|
|
2126
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
|
+
*/
|
|
2127
2838
|
function selectionBlockIds(selection) {
|
|
2128
2839
|
const ids = [];
|
|
2129
2840
|
const push = (id) => {
|
|
@@ -2143,6 +2854,7 @@ var MAX_REACTIVE_SELECTION_BLOCK_READS = 64;
|
|
|
2143
2854
|
function canProbeEverySelectedBlock(blockIds) {
|
|
2144
2855
|
return blockIds.length <= MAX_REACTIVE_SELECTION_BLOCK_READS;
|
|
2145
2856
|
}
|
|
2857
|
+
/** Resolve the selection's story, defaulting to the main body story. */
|
|
2146
2858
|
function selectionStory(selection) {
|
|
2147
2859
|
const target = selection.target;
|
|
2148
2860
|
const explicit = selection.selectionTarget;
|
|
@@ -2154,6 +2866,7 @@ function selectionStory(selection) {
|
|
|
2154
2866
|
storyType: "body"
|
|
2155
2867
|
};
|
|
2156
2868
|
}
|
|
2869
|
+
/** Build a `ParagraphTarget` ( `format.paragraph.*` / `styles.paragraph.*` ). */
|
|
2157
2870
|
function paragraphTarget(blockId, story) {
|
|
2158
2871
|
return {
|
|
2159
2872
|
kind: "block",
|
|
@@ -2162,6 +2875,7 @@ function paragraphTarget(blockId, story) {
|
|
|
2162
2875
|
...story && typeof story === "object" ? { story } : {}
|
|
2163
2876
|
};
|
|
2164
2877
|
}
|
|
2878
|
+
/** Build a `ListsBlockTarget` ( `lists.apply` / `lists.remove` / `lists.getState` ). */
|
|
2165
2879
|
function listsBlockTarget(blockId) {
|
|
2166
2880
|
return {
|
|
2167
2881
|
kind: "block",
|
|
@@ -2169,6 +2883,7 @@ function listsBlockTarget(blockId) {
|
|
|
2169
2883
|
nodeId: blockId
|
|
2170
2884
|
};
|
|
2171
2885
|
}
|
|
2886
|
+
/** Build a `ListItemAddress` ( `lists.indent` / `lists.outdent` / `lists.applyStyle` ). */
|
|
2172
2887
|
function listItemTarget(blockId, story) {
|
|
2173
2888
|
return {
|
|
2174
2889
|
kind: "block",
|
|
@@ -2206,6 +2921,7 @@ function readParagraphIndentationFromResult(result) {
|
|
|
2206
2921
|
...hanging != null ? { hanging } : {}
|
|
2207
2922
|
};
|
|
2208
2923
|
}
|
|
2924
|
+
/** Build one `TextAddress` per covered text segment for `hyperlinks.wrap`. */
|
|
2209
2925
|
function selectionTextAddresses(selection) {
|
|
2210
2926
|
const target = selection.target;
|
|
2211
2927
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
@@ -2347,12 +3063,12 @@ function selectionTextSegments(selection) {
|
|
|
2347
3063
|
const target = selection.target;
|
|
2348
3064
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
2349
3065
|
const out = [];
|
|
2350
|
-
const push = (blockId, start
|
|
2351
|
-
if (typeof blockId !== "string" || typeof start
|
|
3066
|
+
const push = (blockId, start, end) => {
|
|
3067
|
+
if (typeof blockId !== "string" || typeof start !== "number" || typeof end !== "number") return;
|
|
2352
3068
|
out.push({
|
|
2353
3069
|
blockId,
|
|
2354
|
-
start: Math.min(start
|
|
2355
|
-
end: Math.max(start
|
|
3070
|
+
start: Math.min(start, end),
|
|
3071
|
+
end: Math.max(start, end)
|
|
2356
3072
|
});
|
|
2357
3073
|
};
|
|
2358
3074
|
for (const segment of segments) {
|
|
@@ -2392,6 +3108,12 @@ function selectionInlineValueSignature(selection) {
|
|
|
2392
3108
|
if (segments.length === 0) return null;
|
|
2393
3109
|
return [selectionInlineValueStorySignature(selection), ...segments.map((segment) => `${segment.blockId}:${segment.start}-${segment.end}`)].join("|");
|
|
2394
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
|
+
*/
|
|
2395
3117
|
function selectionEffectiveUniformitySignature(selection) {
|
|
2396
3118
|
const target = selection.selectionTarget;
|
|
2397
3119
|
if (!target || typeof target !== "object") return null;
|
|
@@ -2434,6 +3156,11 @@ function sameSelectionTextSegments(left, right) {
|
|
|
2434
3156
|
return candidate?.blockId === segment.blockId && candidate?.start === segment.start && candidate?.end === segment.end;
|
|
2435
3157
|
});
|
|
2436
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
|
+
*/
|
|
2437
3164
|
function pickSelectionTextQueryItem(result, selection) {
|
|
2438
3165
|
const segments = selectionTextSegments(selection);
|
|
2439
3166
|
if (segments.length === 0) return null;
|
|
@@ -2459,6 +3186,7 @@ function readProjectedInlineStyleValue(styles, key) {
|
|
|
2459
3186
|
if (key === "color" || key === "highlight") return trimmed.toUpperCase();
|
|
2460
3187
|
return trimmed;
|
|
2461
3188
|
}
|
|
3189
|
+
/** Primary named family from a resolved CSS font stack, dropping generic fallbacks (SD-3652). */
|
|
2462
3190
|
function normalizeLayoutFontFamily(value) {
|
|
2463
3191
|
if (typeof value !== "string") return void 0;
|
|
2464
3192
|
const first = value.split(",")[0]?.trim().replace(/^['"]+|['"]+$/g, "").trim();
|
|
@@ -2467,6 +3195,7 @@ function normalizeLayoutFontFamily(value) {
|
|
|
2467
3195
|
if (lower === "serif" || lower === "sans-serif" || lower === "monospace" || lower === "cursive" || lower === "fantasy") return;
|
|
2468
3196
|
return first;
|
|
2469
3197
|
}
|
|
3198
|
+
/** Convert a resolved run font size in CSS px to the toolbar's point value (SD-3652). */
|
|
2470
3199
|
function normalizeLayoutFontSizePt(value) {
|
|
2471
3200
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
|
|
2472
3201
|
const pt = Math.round(value * .75 * 2) / 2;
|
|
@@ -2527,6 +3256,7 @@ function projectInlineValuesFromQueryItem(item, selection) {
|
|
|
2527
3256
|
}
|
|
2528
3257
|
return projection;
|
|
2529
3258
|
}
|
|
3259
|
+
/** A create-location `at` derived from the current block, or `documentEnd`. */
|
|
2530
3260
|
function createLocationAt(selection, mode) {
|
|
2531
3261
|
const blockId = selectionBlockIds(selection)[0];
|
|
2532
3262
|
if (!blockId) return { kind: "documentEnd" };
|
|
@@ -2588,6 +3318,12 @@ function commandResultSucceeded(result) {
|
|
|
2588
3318
|
function isLooseObject(value) {
|
|
2589
3319
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2590
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
|
+
*/
|
|
2591
3327
|
function collectProjectionTextBlocks(blocks) {
|
|
2592
3328
|
const out = [];
|
|
2593
3329
|
const visit = (node) => {
|
|
@@ -2607,6 +3343,7 @@ function collectProjectionTextBlocks(blocks) {
|
|
|
2607
3343
|
if (Array.isArray(blocks)) for (const block of blocks) visit(block);
|
|
2608
3344
|
return out;
|
|
2609
3345
|
}
|
|
3346
|
+
/** A projection block matches a selection block id by `sourceAnchor.sourceNodeId` or `block.id`. */
|
|
2610
3347
|
function projectionBlockMatchesId(block, id) {
|
|
2611
3348
|
if (block.sourceAnchor?.sourceNodeId === id) return true;
|
|
2612
3349
|
return block.id === id;
|
|
@@ -2655,6 +3392,7 @@ function hyperlinkTargetFromHref(href) {
|
|
|
2655
3392
|
url: href
|
|
2656
3393
|
};
|
|
2657
3394
|
}
|
|
3395
|
+
/** Tracked-change decision spec for a command id, from the descriptor catalog. */
|
|
2658
3396
|
function trackDecisionCommand(id) {
|
|
2659
3397
|
return getCommandDescriptor(id)?.trackDecision ?? null;
|
|
2660
3398
|
}
|
|
@@ -2666,6 +3404,7 @@ function rectResult(rects) {
|
|
|
2666
3404
|
rect: rects[0]
|
|
2667
3405
|
};
|
|
2668
3406
|
}
|
|
3407
|
+
/** Fail-closed empty geometry result carrying a stable reason. */
|
|
2669
3408
|
function rectFailure(reason) {
|
|
2670
3409
|
return {
|
|
2671
3410
|
found: false,
|
|
@@ -2674,6 +3413,7 @@ function rectFailure(reason) {
|
|
|
2674
3413
|
reason
|
|
2675
3414
|
};
|
|
2676
3415
|
}
|
|
3416
|
+
/** Offset client-space rects so they are relative to `relativeTo`, when given. */
|
|
2677
3417
|
function relativizeRects(result, relativeTo) {
|
|
2678
3418
|
if (!relativeTo || typeof relativeTo.getBoundingClientRect !== "function" || result.rects.length === 0) return result;
|
|
2679
3419
|
let origin;
|
|
@@ -2699,6 +3439,16 @@ function relativizeRects(result, relativeTo) {
|
|
|
2699
3439
|
rect: rects[0]
|
|
2700
3440
|
};
|
|
2701
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
|
+
*/
|
|
2702
3452
|
function isAddressableSegment(segment) {
|
|
2703
3453
|
if (!segment || typeof segment !== "object") return false;
|
|
2704
3454
|
const candidate = segment;
|
|
@@ -2734,6 +3484,15 @@ function selectionTargetForRestore(capture) {
|
|
|
2734
3484
|
if (capture.selectionTarget) return capture.selectionTarget;
|
|
2735
3485
|
return selectionTargetFromTextTarget(capture.target);
|
|
2736
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
|
+
*/
|
|
2737
3496
|
function resolveInlineSelectionTarget(selection) {
|
|
2738
3497
|
if (selection.empty) return null;
|
|
2739
3498
|
if (selection.selectionTarget) return selection.selectionTarget;
|
|
@@ -2741,10 +3500,17 @@ function resolveInlineSelectionTarget(selection) {
|
|
|
2741
3500
|
if (fallback && fallback.start.kind === "text" && fallback.end.kind === "text" && fallback.start.blockId !== fallback.end.blockId) return null;
|
|
2742
3501
|
return fallback;
|
|
2743
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
|
+
*/
|
|
2744
3509
|
function inlineFormatMethod(descriptor) {
|
|
2745
3510
|
if (!descriptor.inline || typeof descriptor.docRoute !== "string") return null;
|
|
2746
3511
|
return descriptor.docRoute.startsWith("format.") ? descriptor.docRoute.slice(7) : null;
|
|
2747
3512
|
}
|
|
3513
|
+
/** Inline run-property keys cleared by `clear-formatting` via `format.apply`. */
|
|
2748
3514
|
var CLEAR_INLINE_PATCH = {
|
|
2749
3515
|
bold: null,
|
|
2750
3516
|
italic: null,
|
|
@@ -2760,7 +3526,22 @@ var CLEAR_INLINE_PATCH = {
|
|
|
2760
3526
|
letterSpacing: null,
|
|
2761
3527
|
dstrike: null
|
|
2762
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
|
+
*/
|
|
2763
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
|
+
*/
|
|
2764
3545
|
function buildInlineFormatInput(spec, target, payload, active) {
|
|
2765
3546
|
switch (spec.kind) {
|
|
2766
3547
|
case "toggle": {
|
|
@@ -2802,6 +3583,7 @@ function buildInlineFormatInput(spec, target, payload, active) {
|
|
|
2802
3583
|
default: return null;
|
|
2803
3584
|
}
|
|
2804
3585
|
}
|
|
3586
|
+
/** Read a `{ start, end }` cell-range from the host snapshot, when well-formed. */
|
|
2805
3587
|
function readMergeRange(range) {
|
|
2806
3588
|
const point = (value) => {
|
|
2807
3589
|
if (!value || typeof value !== "object") return null;
|
|
@@ -2824,6 +3606,12 @@ function readMergeRange(range) {
|
|
|
2824
3606
|
end
|
|
2825
3607
|
};
|
|
2826
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
|
+
*/
|
|
2827
3615
|
function buildTableCommandInput(action, context) {
|
|
2828
3616
|
const { tableNodeId: nodeId, rowIndex, columnIndex, cellNodeId, mergeRange } = context;
|
|
2829
3617
|
switch (action) {
|
|
@@ -2912,18 +3700,22 @@ function createSuperDocUI(options) {
|
|
|
2912
3700
|
let frozenProjectedValues = {};
|
|
2913
3701
|
let frozenProjectedValuesKey = "";
|
|
2914
3702
|
let heldSettledInlineValues = null;
|
|
3703
|
+
/** Read the live active editor (or null). */
|
|
2915
3704
|
const getEditor = () => {
|
|
2916
3705
|
const editor = superdoc?.activeEditor;
|
|
2917
3706
|
return editor && typeof editor === "object" ? editor : null;
|
|
2918
3707
|
};
|
|
3708
|
+
/** Read the live browser Document API facade (or null). */
|
|
2919
3709
|
const getDoc = () => {
|
|
2920
3710
|
const doc = getEditor()?.doc;
|
|
2921
3711
|
return doc && typeof doc === "object" ? doc : null;
|
|
2922
3712
|
};
|
|
3713
|
+
/** Read the live v2 tracked-change facade, when exposed. */
|
|
2923
3714
|
const getV2TrackedChanges = () => {
|
|
2924
3715
|
const trackedChanges = getEditor()?.v2TrackedChanges;
|
|
2925
3716
|
return trackedChanges && typeof trackedChanges === "object" ? trackedChanges : null;
|
|
2926
3717
|
};
|
|
3718
|
+
/** Read the live v2 editor host (inline mode only), when exposed. */
|
|
2927
3719
|
const getHost = () => {
|
|
2928
3720
|
const host = getEditor()?.host;
|
|
2929
3721
|
return host && typeof host === "object" ? host : null;
|
|
@@ -2962,6 +3754,7 @@ function createSuperDocUI(options) {
|
|
|
2962
3754
|
}
|
|
2963
3755
|
return null;
|
|
2964
3756
|
};
|
|
3757
|
+
/** Read the host's stored inline marks (SD-3654/SD-3652), when any pending. */
|
|
2965
3758
|
const readPendingInlineFormat = () => {
|
|
2966
3759
|
const host = getHost();
|
|
2967
3760
|
const fn = host?.getPendingInlineFormat;
|
|
@@ -2969,6 +3762,11 @@ function createSuperDocUI(options) {
|
|
|
2969
3762
|
const pending = safeCall(() => fn.call(host), null);
|
|
2970
3763
|
return pending && typeof pending === "object" ? pending : null;
|
|
2971
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
|
+
*/
|
|
2972
3770
|
const pendingInlineActive = (descriptor) => {
|
|
2973
3771
|
const method = inlineFormatMethod(descriptor);
|
|
2974
3772
|
if (!method) return null;
|
|
@@ -2978,6 +3776,7 @@ function createSuperDocUI(options) {
|
|
|
2978
3776
|
if (descriptor.inline?.kind === "toggle") return value === true;
|
|
2979
3777
|
return value != null && value !== "";
|
|
2980
3778
|
};
|
|
3779
|
+
/** The pending caret font/size value for this command's `format.*` method, if any (SD-3652). */
|
|
2981
3780
|
const pendingInlineValueFor = (descriptor) => {
|
|
2982
3781
|
const method = inlineFormatMethod(descriptor);
|
|
2983
3782
|
if (!method) return void 0;
|
|
@@ -2987,6 +3786,7 @@ function createSuperDocUI(options) {
|
|
|
2987
3786
|
if (typeof value === "string" && value !== "") return value;
|
|
2988
3787
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2989
3788
|
};
|
|
3789
|
+
/** Record / clear the stored inline mark for a collapsed-caret pick (SD-3654/SD-3652). */
|
|
2990
3790
|
const setPendingInlineFormatOnHost = (method, value) => {
|
|
2991
3791
|
const host = getHost();
|
|
2992
3792
|
const fn = host?.setPendingInlineFormat;
|
|
@@ -2997,16 +3797,23 @@ function createSuperDocUI(options) {
|
|
|
2997
3797
|
const fn = host?.clearPendingInlineFormat;
|
|
2998
3798
|
if (typeof fn === "function") safeCall(() => fn.call(host, method), void 0);
|
|
2999
3799
|
};
|
|
3000
|
-
|
|
3800
|
+
/** Inline `format.*` methods whose active state is a boolean mark (name === method). */
|
|
3801
|
+
const BOOLEAN_INLINE_FORMAT_METHODS = /* @__PURE__ */ new Set([
|
|
3001
3802
|
"bold",
|
|
3002
3803
|
"italic",
|
|
3003
3804
|
"underline",
|
|
3004
3805
|
"strikethrough"
|
|
3005
3806
|
]);
|
|
3006
|
-
|
|
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) => {
|
|
3007
3814
|
const pending = readPendingInlineFormat();
|
|
3008
3815
|
if (!pending) return;
|
|
3009
|
-
const activeMarks = Array.isArray(selection
|
|
3816
|
+
const activeMarks = Array.isArray(selection.activeMarks) ? selection.activeMarks : [];
|
|
3010
3817
|
let projected = null;
|
|
3011
3818
|
for (const method of Object.keys(pending)) {
|
|
3012
3819
|
const value = pending[method];
|
|
@@ -3014,33 +3821,47 @@ function createSuperDocUI(options) {
|
|
|
3014
3821
|
if (BOOLEAN_INLINE_FORMAT_METHODS.has(method)) matched = activeMarks.includes(method) === (value === true);
|
|
3015
3822
|
else if (isProjectedInlineSelectionValueKey(method)) {
|
|
3016
3823
|
if (value == null) continue;
|
|
3017
|
-
if (!projected) projected = projectSelectionInlineValues(selection
|
|
3824
|
+
if (!projected) projected = projectSelectionInlineValues(selection);
|
|
3018
3825
|
matched = projected[method] === value;
|
|
3019
3826
|
} else matched = false;
|
|
3020
3827
|
if (matched) clearPendingInlineFormatOnHost(method);
|
|
3021
3828
|
}
|
|
3022
3829
|
};
|
|
3830
|
+
/** Read the live v2 edit-command adapters (`activeEditor.editCommands`), when exposed. */
|
|
3023
3831
|
const getEditCommands = () => {
|
|
3024
3832
|
const editCommands = getEditor()?.editCommands;
|
|
3025
3833
|
return editCommands && typeof editCommands === "object" ? editCommands : null;
|
|
3026
3834
|
};
|
|
3835
|
+
/** Read one edit-command snapshot entry (`activeEditor.editCommands.getSnapshot().commands[id]`). */
|
|
3027
3836
|
const readEditCommandStateEntry = (commandId) => {
|
|
3028
3837
|
const editCommands = getEditCommands();
|
|
3029
3838
|
if (!editCommands || typeof editCommands.getSnapshot !== "function") return null;
|
|
3030
3839
|
const entry = (safeCall(() => editCommands.getSnapshot(), null)?.commands)?.[commandId];
|
|
3031
3840
|
return entry && typeof entry === "object" ? entry : null;
|
|
3032
3841
|
};
|
|
3842
|
+
/** Read the `lists.apply` command state surfaced by the v2 edit-command snapshot. */
|
|
3033
3843
|
const readListApplyStateEntry = () => {
|
|
3034
3844
|
return readEditCommandStateEntry("lists.apply");
|
|
3035
3845
|
};
|
|
3846
|
+
/** Read the additive list active state surfaced by the edit-command snapshot. */
|
|
3036
3847
|
const readListActiveSeed = (entry) => {
|
|
3037
3848
|
const seed = (entry?.value)?.seed;
|
|
3038
3849
|
return seed === "bullet" || seed === "ordered" ? seed : null;
|
|
3039
3850
|
};
|
|
3851
|
+
/** Read the lower host-state entry mirrored into the public `undo` / `redo` ids. */
|
|
3040
3852
|
const readMirroredEditCommandStateEntry = (commandId) => {
|
|
3041
3853
|
const mirrored = MIRRORED_EDIT_COMMAND_IDS[commandId];
|
|
3042
3854
|
return mirrored ? readEditCommandStateEntry(mirrored) : null;
|
|
3043
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
|
+
*/
|
|
3044
3865
|
const readHostTableContext = () => {
|
|
3045
3866
|
const host = getHost();
|
|
3046
3867
|
if (!host || typeof host.getTableContext !== "function") return null;
|
|
@@ -3107,11 +3928,11 @@ function createSuperDocUI(options) {
|
|
|
3107
3928
|
if (typeof review.getActiveReviewTarget !== "function" || typeof review.clearActiveReviewTarget !== "function") return;
|
|
3108
3929
|
if (safeCall(() => review.getActiveReviewTarget(), null)?.entityType === "trackedChange") safeCall(() => review.clearActiveReviewTarget(), void 0);
|
|
3109
3930
|
};
|
|
3110
|
-
const setExplicitActiveChange = (next, options
|
|
3931
|
+
const setExplicitActiveChange = (next, options) => {
|
|
3111
3932
|
explicitActiveChange = next;
|
|
3112
3933
|
explicitActiveChangeRevision += 1;
|
|
3113
3934
|
trackChangeRevealInvalidation += 1;
|
|
3114
|
-
if (options
|
|
3935
|
+
if (options?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
|
|
3115
3936
|
};
|
|
3116
3937
|
const listeners = /* @__PURE__ */ new Set();
|
|
3117
3938
|
let disposed = false;
|
|
@@ -3233,6 +4054,7 @@ function createSuperDocUI(options) {
|
|
|
3233
4054
|
let allTrackedChangesResolvedToken = null;
|
|
3234
4055
|
let selectionEpoch = 0;
|
|
3235
4056
|
let lastCoordinatorEditor = null;
|
|
4057
|
+
/** Token shared by document-content reads (editor identity + mutation revision). */
|
|
3236
4058
|
const contentToken = () => `${editorIdentityId(getEditor())}|m${documentMutationRevision}`;
|
|
3237
4059
|
const clearPostDecisionTrackChanges = () => {
|
|
3238
4060
|
postDecisionTrackChangesToken = null;
|
|
@@ -3309,6 +4131,14 @@ function createSuperDocUI(options) {
|
|
|
3309
4131
|
};
|
|
3310
4132
|
notifyPostDecisionTrackChanges(receipt);
|
|
3311
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
|
+
*/
|
|
3312
4142
|
const publishAllTrackedChangesResolved = (receipt) => {
|
|
3313
4143
|
const previous = state.trackChanges;
|
|
3314
4144
|
const changed = previous.items.length > 0 || previous.total !== 0 || previous.activeId !== null || previous.authors.length > 0;
|
|
@@ -3333,11 +4163,18 @@ function createSuperDocUI(options) {
|
|
|
3333
4163
|
const newlyRemoved = new Set([...ids].filter((id) => !alreadyRemoved.has(id)));
|
|
3334
4164
|
if (newlyRemoved.size === 0) return;
|
|
3335
4165
|
postDecisionTrackChangesToken = token;
|
|
3336
|
-
postDecisionTrackChangeIds = new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
4166
|
+
postDecisionTrackChangeIds = /* @__PURE__ */ new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
3337
4167
|
applyPostDecisionTrackChangesToCache(postDecisionTrackChangeIds);
|
|
3338
4168
|
publishPostDecisionTrackChanges(newlyRemoved, receipt);
|
|
3339
4169
|
};
|
|
3340
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
|
+
*/
|
|
3341
4178
|
const mergeTrackedChangeItemsIntoCache = (items, orderedItems = []) => {
|
|
3342
4179
|
const projected = items.map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) != null);
|
|
3343
4180
|
if (projected.length === 0) return;
|
|
@@ -3405,22 +4242,22 @@ function createSuperDocUI(options) {
|
|
|
3405
4242
|
inflightToken: null
|
|
3406
4243
|
});
|
|
3407
4244
|
};
|
|
3408
|
-
const hydrateTrackedChanges = async (context, options
|
|
4245
|
+
const hydrateTrackedChanges = async (context, options) => {
|
|
3409
4246
|
try {
|
|
3410
4247
|
const listTrackedChanges = context.v2TrackedChanges?.listTrackedChanges;
|
|
3411
4248
|
if (typeof listTrackedChanges === "function") {
|
|
3412
|
-
const result
|
|
3413
|
-
mode: options
|
|
3414
|
-
refreshReason: options
|
|
3415
|
-
blocking: options
|
|
3416
|
-
...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 } : {}
|
|
3417
4254
|
}));
|
|
3418
|
-
const ok = options
|
|
4255
|
+
const ok = options.trackedChangesListMode === "all" && options.refreshReason === "mutation-history-render-reconcile" ? result?.ok === true : result?.ok !== false;
|
|
3419
4256
|
return {
|
|
3420
4257
|
ok,
|
|
3421
|
-
items: ok && Array.isArray(result
|
|
3422
|
-
...typeof result
|
|
3423
|
-
...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 } : {}
|
|
3424
4261
|
};
|
|
3425
4262
|
}
|
|
3426
4263
|
const list = context.docTrackChanges?.list;
|
|
@@ -3441,6 +4278,13 @@ function createSuperDocUI(options) {
|
|
|
3441
4278
|
};
|
|
3442
4279
|
}
|
|
3443
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
|
+
*/
|
|
3444
4288
|
const reviewMutationReconciler = createV2ReviewMutationReconciler({
|
|
3445
4289
|
getContext: () => {
|
|
3446
4290
|
const editor = getEditor();
|
|
@@ -3484,12 +4328,12 @@ function createSuperDocUI(options) {
|
|
|
3484
4328
|
const reads = await Promise.all(requestedIds.map(async (id) => {
|
|
3485
4329
|
try {
|
|
3486
4330
|
if (typeof getTrackedChange === "function") {
|
|
3487
|
-
const result
|
|
4331
|
+
const result = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
|
|
3488
4332
|
return {
|
|
3489
4333
|
id,
|
|
3490
|
-
ok: result
|
|
3491
|
-
items: result
|
|
3492
|
-
result
|
|
4334
|
+
ok: result?.ok !== false,
|
|
4335
|
+
items: result?.ok !== false && Array.isArray(result?.items) ? result.items : [],
|
|
4336
|
+
result
|
|
3493
4337
|
};
|
|
3494
4338
|
}
|
|
3495
4339
|
const result = await Promise.resolve(getRawTrackedChange.call(context.docTrackChanges, { id }));
|
|
@@ -3556,11 +4400,12 @@ function createSuperDocUI(options) {
|
|
|
3556
4400
|
recompute();
|
|
3557
4401
|
return;
|
|
3558
4402
|
}
|
|
3559
|
-
|
|
4403
|
+
const items = (result?.items ?? []).filter((item) => {
|
|
3560
4404
|
const projected = projectTrackChangesItem(item);
|
|
3561
4405
|
const id = projected == null ? null : readEntityId(projected);
|
|
3562
4406
|
return id != null && ids.has(id);
|
|
3563
|
-
})
|
|
4407
|
+
});
|
|
4408
|
+
mergeTrackedChangeItemsIntoCache(items, result?.orderedItems ?? result?.items ?? []);
|
|
3564
4409
|
recompute();
|
|
3565
4410
|
}
|
|
3566
4411
|
});
|
|
@@ -3574,10 +4419,12 @@ function createSuperDocUI(options) {
|
|
|
3574
4419
|
reviewMutationReconciler.onRender(true);
|
|
3575
4420
|
}, delayMs);
|
|
3576
4421
|
};
|
|
4422
|
+
/** Token for the live selection read, including the mode that controls its public projection. */
|
|
3577
4423
|
const selectionReadToken = () => `${contentToken()}|m${readDocumentMode()}|s${selectionEpoch}`;
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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
|
|
3581
4428
|
});
|
|
3582
4429
|
const SELECTION_SCOPED_ASYNC_READ_PREFIXES = [
|
|
3583
4430
|
"contentControls:inRange:",
|
|
@@ -3586,6 +4433,7 @@ function createSuperDocUI(options) {
|
|
|
3586
4433
|
];
|
|
3587
4434
|
const FOREGROUND_ASYNC_RETRY_MS = 120;
|
|
3588
4435
|
const COLD_ASYNC_READ_START_DELAY_MS = 180;
|
|
4436
|
+
/** Tri-state host source-load phase derived from the loading snapshot seam. */
|
|
3589
4437
|
const hostSourceLoadPhase = () => {
|
|
3590
4438
|
const host = getHost();
|
|
3591
4439
|
const read = host?.getDocumentLoadingSnapshot;
|
|
@@ -3608,9 +4456,14 @@ function createSuperDocUI(options) {
|
|
|
3608
4456
|
return Array.isArray(entry.value) && entry.value.length > 0;
|
|
3609
4457
|
};
|
|
3610
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
|
+
*/
|
|
3611
4463
|
const ensureContentControlsCatalog = (_reason) => {
|
|
3612
4464
|
demandHeavyDocRead("contentControls");
|
|
3613
4465
|
};
|
|
4466
|
+
/** Demand-driven route for an explicitly consumed tracked-change list. */
|
|
3614
4467
|
const ensureTrackChangesCatalog = () => {
|
|
3615
4468
|
const entry = asyncReads.get("trackChanges");
|
|
3616
4469
|
if (entry?.hasSettled || entry?.inflightToken != null) return;
|
|
@@ -3620,9 +4473,21 @@ function createSuperDocUI(options) {
|
|
|
3620
4473
|
const HEAVY_READ_IDLE_CEILING_MS = 8e3;
|
|
3621
4474
|
const HEAVY_READ_IDLE_POLL_MS = 250;
|
|
3622
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
|
+
*/
|
|
3623
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
|
+
*/
|
|
3624
4487
|
let heavyReadsHeldUntilIdle = false;
|
|
4488
|
+
/** When the STEADY-phase typing hold began; bounds it to the same ceiling. */
|
|
3625
4489
|
let heavyReadsTypingHoldSinceMs = 0;
|
|
4490
|
+
/** Lets one ceiling-triggered recompute refresh every heavy key as a group. */
|
|
3626
4491
|
let heavyReadCeilingReleaseActive = false;
|
|
3627
4492
|
let heavyReadCompletionRecomputeTimer = null;
|
|
3628
4493
|
const scheduleHeavyReadCompletionRecompute = () => {
|
|
@@ -3682,15 +4547,15 @@ function createSuperDocUI(options) {
|
|
|
3682
4547
|
const host = getEditor()?.host;
|
|
3683
4548
|
const read = host?.getForegroundMutationState;
|
|
3684
4549
|
if (typeof read !== "function") return null;
|
|
3685
|
-
const state
|
|
4550
|
+
const state = safeCall(() => read.call(host), null);
|
|
3686
4551
|
return {
|
|
3687
|
-
active: typeof state
|
|
3688
|
-
pending: typeof state
|
|
4552
|
+
active: typeof state?.active === "number" ? state.active : 0,
|
|
4553
|
+
pending: typeof state?.pending === "number" ? state.pending : 0
|
|
3689
4554
|
};
|
|
3690
4555
|
};
|
|
3691
4556
|
const foregroundMutationActive = () => {
|
|
3692
|
-
const state
|
|
3693
|
-
return Boolean(state
|
|
4557
|
+
const state = foregroundMutationState();
|
|
4558
|
+
return Boolean(state && (state.active > 0 || state.pending > 0));
|
|
3694
4559
|
};
|
|
3695
4560
|
const scheduleForegroundAsyncRetry = (delayMs = FOREGROUND_ASYNC_RETRY_MS) => {
|
|
3696
4561
|
if (disposed || foregroundAsyncRetryTimer) return;
|
|
@@ -3716,6 +4581,12 @@ function createSuperDocUI(options) {
|
|
|
3716
4581
|
scheduleForegroundAsyncRetry(COLD_ASYNC_READ_START_DELAY_MS);
|
|
3717
4582
|
return true;
|
|
3718
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
|
+
*/
|
|
3719
4590
|
const shouldDeferEffectiveInlineRead = (key) => {
|
|
3720
4591
|
if (lastEffectiveInlineReadKey == null || key === lastEffectiveInlineReadKey) {
|
|
3721
4592
|
if (pendingEffectiveInlineRead?.timer) clearTimeout(pendingEffectiveInlineRead.timer);
|
|
@@ -3862,6 +4733,12 @@ function createSuperDocUI(options) {
|
|
|
3862
4733
|
});
|
|
3863
4734
|
return null;
|
|
3864
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
|
+
*/
|
|
3865
4742
|
const readAsync = (key, token, run, normalize) => {
|
|
3866
4743
|
const entry = asyncReads.get(key);
|
|
3867
4744
|
if (entry && entry.token === token && entry.hasSettled) return {
|
|
@@ -3960,23 +4837,24 @@ function createSuperDocUI(options) {
|
|
|
3960
4837
|
const clearAsyncReadKeys = (predicate) => {
|
|
3961
4838
|
for (const key of asyncReads.keys()) if (predicate(key)) asyncReads.delete(key);
|
|
3962
4839
|
};
|
|
3963
|
-
const selectionScopedAsyncReadKeys = (selection
|
|
3964
|
-
const signature = selectionSignature(selection
|
|
3965
|
-
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection
|
|
3966
|
-
return new Set([
|
|
4840
|
+
const selectionScopedAsyncReadKeys = (selection) => {
|
|
4841
|
+
const signature = selectionSignature(selection);
|
|
4842
|
+
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection);
|
|
4843
|
+
return /* @__PURE__ */ new Set([
|
|
3967
4844
|
`contentControls:inRange:${signature}`,
|
|
3968
4845
|
`query:${signature}`,
|
|
3969
4846
|
...effectiveUniformitySignature ? [`effInline:${effectiveUniformitySignature}`] : []
|
|
3970
4847
|
]);
|
|
3971
4848
|
};
|
|
3972
|
-
const pruneSelectionScopedAsyncReads = (selection
|
|
3973
|
-
const retained = selectionScopedAsyncReadKeys(selection
|
|
4849
|
+
const pruneSelectionScopedAsyncReads = (selection) => {
|
|
4850
|
+
const retained = selectionScopedAsyncReadKeys(selection);
|
|
3974
4851
|
clearAsyncReadKeys((key) => SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => key.startsWith(prefix)) && !retained.has(key));
|
|
3975
4852
|
for (const key of coldAsyncReadDeferrals) {
|
|
3976
4853
|
const readKey = key.split("\0", 1)[0];
|
|
3977
4854
|
if (SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => readKey.startsWith(prefix)) && !retained.has(readKey)) coldAsyncReadDeferrals.delete(key);
|
|
3978
4855
|
}
|
|
3979
4856
|
};
|
|
4857
|
+
/** Drop cached reads when the active editor identity changes (avoids cross-editor leakage). */
|
|
3980
4858
|
const syncCoordinatorEditor = () => {
|
|
3981
4859
|
const editor = getEditor();
|
|
3982
4860
|
if (editor === lastCoordinatorEditor) return;
|
|
@@ -4006,12 +4884,20 @@ function createSuperDocUI(options) {
|
|
|
4006
4884
|
clearPostDecisionTrackChanges();
|
|
4007
4885
|
allTrackedChangesResolvedToken = null;
|
|
4008
4886
|
};
|
|
4887
|
+
/** Bump the document-mutation revision so content reads re-fetch on the next compute. */
|
|
4009
4888
|
const invalidateDocumentContent = () => {
|
|
4010
4889
|
const carryAuthoritativeTrackChangesHold = authoritativeTrackChangesPendingToken !== null;
|
|
4011
4890
|
documentMutationRevision += 1;
|
|
4012
4891
|
if (carryAuthoritativeTrackChangesHold) authoritativeTrackChangesPendingToken = contentToken();
|
|
4013
4892
|
clearPostDecisionTrackChanges();
|
|
4014
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
|
+
*/
|
|
4015
4901
|
const invalidateAfterCommandSettlement = () => {
|
|
4016
4902
|
const carryAllResolvedCatalog = allTrackedChangesResolvedToken === contentToken();
|
|
4017
4903
|
invalidateDocumentContent();
|
|
@@ -4021,6 +4907,12 @@ function createSuperDocUI(options) {
|
|
|
4021
4907
|
}
|
|
4022
4908
|
recompute();
|
|
4023
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
|
+
*/
|
|
4024
4916
|
const insertText = (text) => {
|
|
4025
4917
|
if (typeof text !== "string" || text.length === 0) return failedReceipt("insertText requires a non-empty string.", "INVALID_INPUT");
|
|
4026
4918
|
if (readDocumentMode() === "viewing") return failedReceipt("The document is read-only.", "DOCUMENT_READONLY");
|
|
@@ -4029,8 +4921,8 @@ function createSuperDocUI(options) {
|
|
|
4029
4921
|
if (!doc || typeof op !== "function") return failedReceipt("insert is unavailable.");
|
|
4030
4922
|
const currentSelection = doc.selection?.current;
|
|
4031
4923
|
if (typeof currentSelection !== "function") return failedReceipt("selection.current is unavailable.");
|
|
4032
|
-
const insertAtLiveSelection = (selection
|
|
4033
|
-
const record = selection
|
|
4924
|
+
const insertAtLiveSelection = (selection) => {
|
|
4925
|
+
const record = selection && typeof selection === "object" ? selection : null;
|
|
4034
4926
|
const target = record?.selectionTarget ?? record?.target ?? null;
|
|
4035
4927
|
if (!target || typeof target !== "object") return failedReceipt("insertText requires a live selection target.", "PRECONDITION_FAILED");
|
|
4036
4928
|
return safeCall(() => settleWorkflowReceipt(op.call(doc, {
|
|
@@ -4039,15 +4931,16 @@ function createSuperDocUI(options) {
|
|
|
4039
4931
|
target
|
|
4040
4932
|
}), failedReceipt("insert failed.")), failedReceipt("insert failed."));
|
|
4041
4933
|
};
|
|
4042
|
-
let selection
|
|
4934
|
+
let selection;
|
|
4043
4935
|
try {
|
|
4044
|
-
selection
|
|
4936
|
+
selection = currentSelection.call(doc.selection);
|
|
4045
4937
|
} catch {
|
|
4046
4938
|
return failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED");
|
|
4047
4939
|
}
|
|
4048
|
-
if (isPromiseLike(selection
|
|
4049
|
-
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);
|
|
4050
4942
|
};
|
|
4943
|
+
/** Build the shared V2-truthful callback context for a custom command / button. */
|
|
4051
4944
|
const buildCustomCommandContext = (payload, context) => ({
|
|
4052
4945
|
payload,
|
|
4053
4946
|
state,
|
|
@@ -4069,10 +4962,11 @@ function createSuperDocUI(options) {
|
|
|
4069
4962
|
const editor = getEditor();
|
|
4070
4963
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4071
4964
|
const fromConfig = normalizeDocumentMode((superdocRecord?.config)?.documentMode);
|
|
4072
|
-
const
|
|
4965
|
+
const runtimeSnapshot = safeCall(typeof superdocRecord?.getActiveRuntime === "function" ? () => {
|
|
4073
4966
|
const runtime = superdocRecord.getActiveRuntime();
|
|
4074
4967
|
return typeof runtime?.getSnapshot === "function" ? runtime.getSnapshot() : null;
|
|
4075
|
-
} : void 0, null)
|
|
4968
|
+
} : void 0, null);
|
|
4969
|
+
const fromRuntime = normalizeDocumentMode(runtimeSnapshot?.documentMode);
|
|
4076
4970
|
const fromOptions = normalizeDocumentMode((editor?.options)?.documentMode);
|
|
4077
4971
|
if (fromConfig && fromRuntime && fromConfig !== fromRuntime) return fromConfig;
|
|
4078
4972
|
return fromRuntime ?? fromConfig ?? fromOptions;
|
|
@@ -4081,15 +4975,20 @@ function createSuperDocUI(options) {
|
|
|
4081
4975
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4082
4976
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
4083
4977
|
if (resolved && typeof resolved.readOnly === "boolean") return resolved.readOnly === true;
|
|
4084
|
-
const comments
|
|
4085
|
-
return !!comments
|
|
4086
|
-
};
|
|
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
|
+
*/
|
|
4087
4986
|
const resolveIsForbidden = () => {
|
|
4088
4987
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4089
4988
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
4090
4989
|
if (resolved && typeof resolved.allowResolve === "boolean") return resolved.allowResolve === false;
|
|
4091
|
-
const comments
|
|
4092
|
-
return !!comments
|
|
4990
|
+
const comments = ((superdocRecord?.config)?.modules)?.comments;
|
|
4991
|
+
return !!comments && typeof comments === "object" && comments.allowResolve === false;
|
|
4093
4992
|
};
|
|
4094
4993
|
const reviewMutationsAreReadOnly = () => readDocumentMode() === "viewing" || commentsReadOnlyForReview();
|
|
4095
4994
|
const normalizeSelectionInfo = (raw) => raw && typeof raw === "object" ? raw : null;
|
|
@@ -4123,6 +5022,7 @@ function createSuperDocUI(options) {
|
|
|
4123
5022
|
const selectionApi = getDoc()?.selection;
|
|
4124
5023
|
return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
|
|
4125
5024
|
};
|
|
5025
|
+
/** True when a host sync snapshot is a resolved collapsed caret (not a range). */
|
|
4126
5026
|
const isCollapsedCaretSnapshot = (info) => {
|
|
4127
5027
|
if (info.empty !== true) return false;
|
|
4128
5028
|
const target = info.selectionTarget;
|
|
@@ -4130,6 +5030,21 @@ function createSuperDocUI(options) {
|
|
|
4130
5030
|
const end = target?.end;
|
|
4131
5031
|
return start?.kind === "text" && end?.kind === "text" && typeof start.blockId === "string" && start.blockId === end.blockId && typeof start.offset === "number" && start.offset === end.offset;
|
|
4132
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
|
+
*/
|
|
4133
5048
|
const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
|
|
4134
5049
|
const host = getHost();
|
|
4135
5050
|
const read = host?.readLiveSelectionSyncSnapshot;
|
|
@@ -4201,15 +5116,15 @@ function createSuperDocUI(options) {
|
|
|
4201
5116
|
recompute();
|
|
4202
5117
|
return state.selection;
|
|
4203
5118
|
};
|
|
4204
|
-
const computeComments = (selection
|
|
5119
|
+
const computeComments = (selection) => {
|
|
4205
5120
|
const commentsApi = getDoc()?.comments;
|
|
4206
5121
|
const { value, status: listStatus } = readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4207
5122
|
const items = value ?? [];
|
|
4208
|
-
const activeIds = selection
|
|
5123
|
+
const activeIds = selection.activeCommentIds;
|
|
4209
5124
|
if (explicitActiveCommentId && !items.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
|
|
4210
5125
|
const activeId = explicitActiveCommentId ?? activeIds[0] ?? null;
|
|
4211
5126
|
return {
|
|
4212
|
-
status: combineStatus(listStatus, selection
|
|
5127
|
+
status: combineStatus(listStatus, selection.status),
|
|
4213
5128
|
listStatus,
|
|
4214
5129
|
items,
|
|
4215
5130
|
total: items.length,
|
|
@@ -4239,7 +5154,7 @@ function createSuperDocUI(options) {
|
|
|
4239
5154
|
});
|
|
4240
5155
|
return status === "ready" ? value : null;
|
|
4241
5156
|
};
|
|
4242
|
-
const computeTrackChanges = (selection
|
|
5157
|
+
const computeTrackChanges = (selection) => {
|
|
4243
5158
|
const tcApi = getDoc()?.trackChanges;
|
|
4244
5159
|
const v2TrackedChanges = getV2TrackedChanges();
|
|
4245
5160
|
const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
|
|
@@ -4272,51 +5187,58 @@ function createSuperDocUI(options) {
|
|
|
4272
5187
|
} else if (!items.some((item) => readEntityId(item) === active.id)) setExplicitActiveChange(null);
|
|
4273
5188
|
}
|
|
4274
5189
|
const publicIdItems = allStoryItems ?? items;
|
|
4275
|
-
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection
|
|
4276
|
-
const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection
|
|
5190
|
+
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection));
|
|
5191
|
+
const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection.activeChangeIds.filter((id) => !postDecisionIds.has(id)) : selection.activeChangeIds;
|
|
4277
5192
|
const selectionPublicChangeIds = selectionActiveChangeIds.map((id) => selectionIdContext.toPublicId(id) ?? id);
|
|
4278
5193
|
const explicitActiveIdContext = explicitActiveChange?.story ? buildStoryScopedTrackedChangeIdContext(publicIdItems, explicitActiveChange.story) : buildTrackedChangeIdContext(publicIdItems);
|
|
4279
5194
|
const explicitActiveId = explicitActiveChange ? explicitActiveIdContext.toPublicId(explicitActiveChange.id) ?? explicitActiveChange.id : null;
|
|
4280
5195
|
const selectionActiveId = resolveSelectionActiveChangeId({
|
|
4281
5196
|
selectionIds: selectionActiveChangeIds,
|
|
4282
5197
|
publicIds: selectionPublicChangeIds,
|
|
4283
|
-
selection
|
|
5198
|
+
selection,
|
|
4284
5199
|
bodyItems: items,
|
|
4285
5200
|
allStoryItems
|
|
4286
5201
|
});
|
|
4287
5202
|
const activeId = explicitActiveId ?? selectionActiveId;
|
|
4288
5203
|
return {
|
|
4289
|
-
status: combineStatus(listStatus, selection
|
|
5204
|
+
status: combineStatus(listStatus, selection.status),
|
|
4290
5205
|
items,
|
|
4291
5206
|
total: items.length,
|
|
4292
5207
|
activeId,
|
|
4293
5208
|
authors: [...authors]
|
|
4294
5209
|
};
|
|
4295
5210
|
};
|
|
4296
|
-
const computeContentControls = (selection
|
|
5211
|
+
const computeContentControls = (selection) => {
|
|
4297
5212
|
const ccApi = getDoc()?.contentControls;
|
|
4298
5213
|
const { value, status: listStatus } = readAsync("contentControls", contentToken(), () => ccApi?.list ? ccApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4299
5214
|
const items = value ?? [];
|
|
4300
|
-
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection
|
|
5215
|
+
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection);
|
|
4301
5216
|
return {
|
|
4302
|
-
status: combineStatus(listStatus, rangeStatus, selection
|
|
5217
|
+
status: combineStatus(listStatus, rangeStatus, selection.status),
|
|
4303
5218
|
items,
|
|
4304
5219
|
total: items.length,
|
|
4305
5220
|
activeId: activeIds[0] ?? null,
|
|
4306
5221
|
activeIds
|
|
4307
5222
|
};
|
|
4308
5223
|
};
|
|
4309
|
-
|
|
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) => {
|
|
4310
5232
|
if (!ccApi || typeof ccApi.listInRange !== "function") return {
|
|
4311
5233
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4312
5234
|
status: "ready"
|
|
4313
5235
|
};
|
|
4314
|
-
const range = selectionBlockRange(selection
|
|
5236
|
+
const range = selectionBlockRange(selection);
|
|
4315
5237
|
if (!range) return {
|
|
4316
5238
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4317
5239
|
status: "ready"
|
|
4318
5240
|
};
|
|
4319
|
-
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 : []);
|
|
4320
5242
|
const rows = value ?? [];
|
|
4321
5243
|
const lockModesById = /* @__PURE__ */ new Map();
|
|
4322
5244
|
for (const row of rows) if (typeof row?.id === "string") lockModesById.set(row.id, typeof row.lockMode === "string" ? row.lockMode : "unlocked");
|
|
@@ -4325,8 +5247,8 @@ function createSuperDocUI(options) {
|
|
|
4325
5247
|
status
|
|
4326
5248
|
};
|
|
4327
5249
|
};
|
|
4328
|
-
const computeActiveContentControlIds = (ccApi, selection
|
|
4329
|
-
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection
|
|
5250
|
+
const computeActiveContentControlIds = (ccApi, selection) => {
|
|
5251
|
+
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4330
5252
|
return {
|
|
4331
5253
|
ids: [...lockModesById.keys()],
|
|
4332
5254
|
status
|
|
@@ -4340,14 +5262,14 @@ function createSuperDocUI(options) {
|
|
|
4340
5262
|
};
|
|
4341
5263
|
};
|
|
4342
5264
|
const computeZoom = () => {
|
|
4343
|
-
const state
|
|
4344
|
-
const value = typeof state
|
|
4345
|
-
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;
|
|
4346
5268
|
return {
|
|
4347
5269
|
mode: rawMode === "manual" || rawMode === "fit-width" ? rawMode : rawMode === "fixed" ? "manual" : null,
|
|
4348
5270
|
value,
|
|
4349
|
-
min: typeof state
|
|
4350
|
-
max: typeof state
|
|
5271
|
+
min: typeof state?.min === "number" ? state.min : 10,
|
|
5272
|
+
max: typeof state?.max === "number" ? state.max : 100
|
|
4351
5273
|
};
|
|
4352
5274
|
};
|
|
4353
5275
|
const readMeasurementUnit = () => {
|
|
@@ -4361,12 +5283,18 @@ function createSuperDocUI(options) {
|
|
|
4361
5283
|
dirty: editor ? Boolean(editor.isDirty) : false
|
|
4362
5284
|
};
|
|
4363
5285
|
};
|
|
4364
|
-
|
|
4365
|
-
|
|
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;
|
|
4366
5294
|
if (typeof pattern !== "string" || pattern.length === 0) return null;
|
|
4367
|
-
const segments = selectionTextSegments(selection
|
|
5295
|
+
const segments = selectionTextSegments(selection);
|
|
4368
5296
|
if (segments.length === 0 || !canProbeEverySelectedBlock(segments.map((segment) => segment.blockId))) return null;
|
|
4369
|
-
const target = selection
|
|
5297
|
+
const target = selection.target;
|
|
4370
5298
|
const within = segments.length === 1 ? paragraphTarget(segments[0].blockId, target?.story) : void 0;
|
|
4371
5299
|
return {
|
|
4372
5300
|
select: {
|
|
@@ -4379,24 +5307,30 @@ function createSuperDocUI(options) {
|
|
|
4379
5307
|
require: "any"
|
|
4380
5308
|
};
|
|
4381
5309
|
};
|
|
4382
|
-
const resolveSelectionTextQueryItem = (selection
|
|
5310
|
+
const resolveSelectionTextQueryItem = (selection) => {
|
|
4383
5311
|
const query = getDoc()?.query;
|
|
4384
5312
|
if (typeof query?.match !== "function") return null;
|
|
4385
|
-
const request = selectionTextQueryRequest(selection
|
|
5313
|
+
const request = selectionTextQueryRequest(selection);
|
|
4386
5314
|
if (!request) return null;
|
|
4387
|
-
const { value } = readAsync(`query:${selectionSignature(selection
|
|
4388
|
-
return pickSelectionTextQueryItem(value, selection
|
|
4389
|
-
};
|
|
4390
|
-
|
|
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) => {
|
|
4391
5325
|
const host = getHost();
|
|
4392
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5326
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4393
5327
|
if (blockIds.size === 0) return {};
|
|
4394
5328
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4395
5329
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4396
5330
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4397
5331
|
const blocks = safeCall(() => {
|
|
4398
5332
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4399
|
-
const story = selectionStoryLocator(selection
|
|
5333
|
+
const story = selectionStoryLocator(selection);
|
|
4400
5334
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4401
5335
|
}, null);
|
|
4402
5336
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4411,7 +5345,7 @@ function createSuperDocUI(options) {
|
|
|
4411
5345
|
if (typeof run.highlight === "string" && run.highlight.trim() !== "") out.highlight = run.highlight.trim().toUpperCase();
|
|
4412
5346
|
return out;
|
|
4413
5347
|
};
|
|
4414
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5348
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4415
5349
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4416
5350
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4417
5351
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4435,7 +5369,7 @@ function createSuperDocUI(options) {
|
|
|
4435
5369
|
return runFontValues(chosen);
|
|
4436
5370
|
}
|
|
4437
5371
|
}
|
|
4438
|
-
const segments = selectionTextSegments(selection
|
|
5372
|
+
const segments = selectionTextSegments(selection);
|
|
4439
5373
|
if (segments.length > 0) {
|
|
4440
5374
|
const rangeFamilies = /* @__PURE__ */ new Set();
|
|
4441
5375
|
const rangeSizes = /* @__PURE__ */ new Set();
|
|
@@ -4465,10 +5399,10 @@ function createSuperDocUI(options) {
|
|
|
4465
5399
|
}
|
|
4466
5400
|
}
|
|
4467
5401
|
if (offsetSafe && rangeSawRun) {
|
|
4468
|
-
const projection
|
|
4469
|
-
if (rangeFamilies.size === 1) projection
|
|
4470
|
-
if (rangeSizes.size === 1) projection
|
|
4471
|
-
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;
|
|
4472
5406
|
}
|
|
4473
5407
|
}
|
|
4474
5408
|
const families = /* @__PURE__ */ new Set();
|
|
@@ -4491,16 +5425,29 @@ function createSuperDocUI(options) {
|
|
|
4491
5425
|
if (sizes.size === 1) projection.fontSize = [...sizes][0];
|
|
4492
5426
|
return projection;
|
|
4493
5427
|
};
|
|
4494
|
-
|
|
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) => {
|
|
4495
5442
|
const host = getHost();
|
|
4496
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5443
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4497
5444
|
if (blockIds.size === 0) return {};
|
|
4498
5445
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4499
5446
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4500
5447
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4501
5448
|
const blocks = safeCall(() => {
|
|
4502
5449
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4503
|
-
const story = selectionStoryLocator(selection
|
|
5450
|
+
const story = selectionStoryLocator(selection);
|
|
4504
5451
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4505
5452
|
}, null);
|
|
4506
5453
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4519,7 +5466,7 @@ function createSuperDocUI(options) {
|
|
|
4519
5466
|
}
|
|
4520
5467
|
return out;
|
|
4521
5468
|
};
|
|
4522
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5469
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4523
5470
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4524
5471
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4525
5472
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4543,7 +5490,7 @@ function createSuperDocUI(options) {
|
|
|
4543
5490
|
return runMarkValues(chosen);
|
|
4544
5491
|
}
|
|
4545
5492
|
}
|
|
4546
|
-
const segments = selectionTextSegments(selection
|
|
5493
|
+
const segments = selectionTextSegments(selection);
|
|
4547
5494
|
if (segments.length > 0) {
|
|
4548
5495
|
const rangeSets = {
|
|
4549
5496
|
bold: /* @__PURE__ */ new Set(),
|
|
@@ -4597,12 +5544,12 @@ function createSuperDocUI(options) {
|
|
|
4597
5544
|
if (!sawWholeRun) return {};
|
|
4598
5545
|
return collapseMarkSets(wholeSets);
|
|
4599
5546
|
};
|
|
4600
|
-
const completeProjectedInlineValues = (selection
|
|
5547
|
+
const completeProjectedInlineValues = (selection, direct) => {
|
|
4601
5548
|
if (direct.fontFamily !== void 0 && direct.fontSize !== void 0 && direct.color !== void 0 && direct.highlight !== void 0) return {
|
|
4602
5549
|
values: direct,
|
|
4603
5550
|
effectiveUniformityStatus: "ready"
|
|
4604
5551
|
};
|
|
4605
|
-
const resolved = resolveEffectiveInlineValuesFromLayout(selection
|
|
5552
|
+
const resolved = resolveEffectiveInlineValuesFromLayout(selection);
|
|
4606
5553
|
const combined = {
|
|
4607
5554
|
...resolved.fontFamily !== void 0 ? { fontFamily: resolved.fontFamily } : {},
|
|
4608
5555
|
...resolved.fontSize !== void 0 ? { fontSize: resolved.fontSize } : {},
|
|
@@ -4610,8 +5557,8 @@ function createSuperDocUI(options) {
|
|
|
4610
5557
|
...resolved.highlight !== void 0 ? { highlight: resolved.highlight } : {},
|
|
4611
5558
|
...direct
|
|
4612
5559
|
};
|
|
4613
|
-
if (!selection
|
|
4614
|
-
const uniformity = resolveEffectiveInlineUniformityValues(selection
|
|
5560
|
+
if (!selection.empty && (combined.fontFamily === void 0 || combined.fontSize === void 0)) {
|
|
5561
|
+
const uniformity = resolveEffectiveInlineUniformityValues(selection);
|
|
4615
5562
|
if (uniformity.values) {
|
|
4616
5563
|
if (combined.fontFamily === void 0 && uniformity.values.fontFamily !== void 0) combined.fontFamily = uniformity.values.fontFamily;
|
|
4617
5564
|
if (combined.fontSize === void 0 && uniformity.values.fontSize !== void 0) combined.fontSize = uniformity.values.fontSize;
|
|
@@ -4626,6 +5573,13 @@ function createSuperDocUI(options) {
|
|
|
4626
5573
|
effectiveUniformityStatus: "ready"
|
|
4627
5574
|
};
|
|
4628
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
|
+
*/
|
|
4629
5583
|
const EFFECTIVE_INLINE_UNIFORMITY_KEYS = [
|
|
4630
5584
|
"fontFamily",
|
|
4631
5585
|
"fontSize",
|
|
@@ -4634,8 +5588,8 @@ function createSuperDocUI(options) {
|
|
|
4634
5588
|
"underline",
|
|
4635
5589
|
"strikethrough"
|
|
4636
5590
|
];
|
|
4637
|
-
const readEffectiveInlineUniformityCached = (selection
|
|
4638
|
-
const target = selection
|
|
5591
|
+
const readEffectiveInlineUniformityCached = (selection) => {
|
|
5592
|
+
const target = selection.selectionTarget;
|
|
4639
5593
|
if (!target) return {
|
|
4640
5594
|
value: null,
|
|
4641
5595
|
status: "ready"
|
|
@@ -4645,14 +5599,15 @@ function createSuperDocUI(options) {
|
|
|
4645
5599
|
value: null,
|
|
4646
5600
|
status: "ready"
|
|
4647
5601
|
};
|
|
4648
|
-
|
|
5602
|
+
const signature = selectionEffectiveUniformitySignature(selection) ?? selectionSignature(selection);
|
|
5603
|
+
return readAsync(`effInline:${signature}`, contentToken(), () => op({
|
|
4649
5604
|
target,
|
|
4650
5605
|
offsetSpace: "selection",
|
|
4651
5606
|
keys: EFFECTIVE_INLINE_UNIFORMITY_KEYS
|
|
4652
5607
|
}), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4653
5608
|
};
|
|
4654
|
-
const resolveEffectiveInlineUniformityValues = (selection
|
|
4655
|
-
const { value, status } = readEffectiveInlineUniformityCached(selection
|
|
5609
|
+
const resolveEffectiveInlineUniformityValues = (selection) => {
|
|
5610
|
+
const { value, status } = readEffectiveInlineUniformityCached(selection);
|
|
4656
5611
|
if (!value || value.success !== true) return {
|
|
4657
5612
|
values: null,
|
|
4658
5613
|
status
|
|
@@ -4671,8 +5626,9 @@ function createSuperDocUI(options) {
|
|
|
4671
5626
|
status
|
|
4672
5627
|
};
|
|
4673
5628
|
};
|
|
4674
|
-
|
|
4675
|
-
|
|
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);
|
|
4676
5632
|
if (!value || value.success !== true) return {
|
|
4677
5633
|
values: null,
|
|
4678
5634
|
status
|
|
@@ -4688,18 +5644,25 @@ function createSuperDocUI(options) {
|
|
|
4688
5644
|
status
|
|
4689
5645
|
};
|
|
4690
5646
|
};
|
|
4691
|
-
|
|
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) => {
|
|
4692
5655
|
const mark = descriptor.activeMark;
|
|
4693
5656
|
if (!mark || !EFFECTIVE_MARK_KEYS.includes(mark)) return null;
|
|
4694
5657
|
const key = mark;
|
|
4695
|
-
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection
|
|
5658
|
+
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection)[key];
|
|
4696
5659
|
if (fromLayout !== void 0) return fromLayout;
|
|
4697
|
-
const { values } = resolveEffectiveMarkUniformityValues(selection
|
|
5660
|
+
const { values } = resolveEffectiveMarkUniformityValues(selection);
|
|
4698
5661
|
const fromWorker = values?.[key];
|
|
4699
5662
|
return fromWorker !== void 0 ? fromWorker : null;
|
|
4700
5663
|
};
|
|
4701
|
-
const readEffectiveInlineUniformityNow = async (selection
|
|
4702
|
-
const target = selection
|
|
5664
|
+
const readEffectiveInlineUniformityNow = async (selection) => {
|
|
5665
|
+
const target = selection.selectionTarget;
|
|
4703
5666
|
if (!target) return null;
|
|
4704
5667
|
const op = resolveDocOperation(getDoc(), "format.readEffectiveInlineUniformity");
|
|
4705
5668
|
if (!op) return null;
|
|
@@ -4722,23 +5685,30 @@ function createSuperDocUI(options) {
|
|
|
4722
5685
|
else delete projected[key];
|
|
4723
5686
|
}
|
|
4724
5687
|
};
|
|
4725
|
-
const projectSelectionInlineValuesWithStatus = (selection
|
|
4726
|
-
const projectSelectionInlineValues = (selection
|
|
4727
|
-
|
|
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) => {
|
|
4728
5698
|
const query = getDoc()?.query;
|
|
4729
|
-
const request = selectionTextQueryRequest(selection
|
|
4730
|
-
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection
|
|
5699
|
+
const request = selectionTextQueryRequest(selection);
|
|
5700
|
+
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection);
|
|
4731
5701
|
try {
|
|
4732
5702
|
const raw = await Promise.resolve(query.match(request));
|
|
4733
|
-
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection
|
|
4734
|
-
const completed = completeProjectedInlineValues(selection
|
|
5703
|
+
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection), selection);
|
|
5704
|
+
const completed = completeProjectedInlineValues(selection, direct);
|
|
4735
5705
|
if (completed.effectiveUniformityStatus !== "ready" && (direct.fontFamily === void 0 || direct.fontSize === void 0)) {
|
|
4736
|
-
const effective = await readEffectiveInlineUniformityNow(selection
|
|
5706
|
+
const effective = await readEffectiveInlineUniformityNow(selection);
|
|
4737
5707
|
if (effective) applyEffectiveInlineUniformity(completed.values, direct, effective);
|
|
4738
5708
|
}
|
|
4739
5709
|
return completed.values;
|
|
4740
5710
|
} catch {
|
|
4741
|
-
return projectSelectionInlineValues(selection
|
|
5711
|
+
return projectSelectionInlineValues(selection);
|
|
4742
5712
|
}
|
|
4743
5713
|
};
|
|
4744
5714
|
const readBlockNode = async (address) => {
|
|
@@ -4750,21 +5720,35 @@ function createSuperDocUI(options) {
|
|
|
4750
5720
|
nodeType: address["nodeType"]
|
|
4751
5721
|
}));
|
|
4752
5722
|
};
|
|
4753
|
-
|
|
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) => {
|
|
4754
5729
|
const query = getDoc()?.query;
|
|
4755
5730
|
if (typeof query?.match !== "function") return "ready";
|
|
4756
|
-
const request = selectionTextQueryRequest(selection
|
|
5731
|
+
const request = selectionTextQueryRequest(selection);
|
|
4757
5732
|
if (!request) return "ready";
|
|
4758
|
-
const { status } = readAsync(`query:${selectionSignature(selection
|
|
5733
|
+
const { status } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4759
5734
|
return status;
|
|
4760
5735
|
};
|
|
4761
|
-
|
|
4762
|
-
|
|
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);
|
|
4763
5747
|
const projected = projection.values;
|
|
4764
|
-
if (selection
|
|
4765
|
-
const signature = selectionInlineValueSignature(selection
|
|
5748
|
+
if (selection.empty) return projection;
|
|
5749
|
+
const signature = selectionInlineValueSignature(selection);
|
|
4766
5750
|
if (!signature) return projection;
|
|
4767
|
-
if (selection
|
|
5751
|
+
if (selection.status === "ready" && selectionQueryStatus(selection) === "ready" && projection.effectiveUniformityStatus === "ready") {
|
|
4768
5752
|
heldSettledInlineValues = {
|
|
4769
5753
|
key: signature,
|
|
4770
5754
|
values: projected
|
|
@@ -4780,39 +5764,56 @@ function createSuperDocUI(options) {
|
|
|
4780
5764
|
};
|
|
4781
5765
|
return projection;
|
|
4782
5766
|
};
|
|
4783
|
-
const computeCommandStates = (selection
|
|
5767
|
+
const computeCommandStates = (selection) => {
|
|
4784
5768
|
const doc = getDoc();
|
|
4785
|
-
const projectedInlineRead = projectInlineValuesWithSettledHold(selection
|
|
5769
|
+
const projectedInlineRead = projectInlineValuesWithSettledHold(selection);
|
|
4786
5770
|
const projectedInlineValues = projectedInlineRead.values;
|
|
4787
|
-
if (selection
|
|
5771
|
+
if (selection.status === "ready" && !selection.empty && selectionQueryStatus(selection) === "ready" && projectedInlineRead.effectiveUniformityStatus === "ready") {
|
|
4788
5772
|
frozenProjectedValues = projectedInlineValues;
|
|
4789
|
-
frozenProjectedValuesKey = `${selectionKey(selection
|
|
5773
|
+
frozenProjectedValuesKey = `${selectionKey(selection)}:${contentToken()}`;
|
|
4790
5774
|
}
|
|
4791
5775
|
const ccApi = doc?.contentControls;
|
|
4792
|
-
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection
|
|
5776
|
+
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4793
5777
|
const states = {};
|
|
4794
|
-
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);
|
|
4795
5779
|
return states;
|
|
4796
5780
|
};
|
|
5781
|
+
/** Stable reason a routed command cannot reach its Document API operation. */
|
|
4797
5782
|
const unavailableRouteReason = (doc) => {
|
|
4798
5783
|
if (doc) return SUPERDOC_UI_REASONS.operationUnavailable;
|
|
4799
5784
|
return getEditor() ? SUPERDOC_UI_REASONS.documentApiUnavailable : SUPERDOC_UI_REASONS.notReady;
|
|
4800
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
|
+
*/
|
|
4801
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
|
+
*/
|
|
4802
5802
|
const contentControlLockReason = (lockModesById) => {
|
|
4803
5803
|
for (const lockMode of lockModesById.values()) if (blocksContentStyling(lockMode)) return SUPERDOC_UI_REASONS.contentControlLocked;
|
|
4804
5804
|
};
|
|
4805
|
-
|
|
5805
|
+
/** Stable reason a tracked-change decision command is disabled, or undefined when enabled. */
|
|
5806
|
+
const trackDecisionReason = (command, supported, readonly, selection, doc) => {
|
|
4806
5807
|
if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
|
|
4807
5808
|
if (readonly) return SUPERDOC_UI_REASONS.documentReadonly;
|
|
4808
|
-
if (command.scope !== "all" && selection
|
|
5809
|
+
if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
|
|
4809
5810
|
};
|
|
4810
5811
|
const hostCommandSupport = (command) => {
|
|
4811
5812
|
const host = getHost();
|
|
4812
|
-
const commands
|
|
4813
|
-
if (Array.isArray(commands
|
|
4814
|
-
if (commands
|
|
4815
|
-
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];
|
|
4816
5817
|
return record && typeof record === "object" ? record : null;
|
|
4817
5818
|
}
|
|
4818
5819
|
return null;
|
|
@@ -4824,7 +5825,7 @@ function createSuperDocUI(options) {
|
|
|
4824
5825
|
if (support && (support.status === "supported" || support.enabled === true)) return void 0;
|
|
4825
5826
|
return SUPERDOC_UI_REASONS.bulkDecisionsDisabled;
|
|
4826
5827
|
};
|
|
4827
|
-
const computeCommandState = (id, doc, selection
|
|
5828
|
+
const computeCommandState = (id, doc, selection, projectedInlineValues, lockModesById = /* @__PURE__ */ new Map()) => {
|
|
4828
5829
|
if (customCommands.has(id)) return normalizeCommandState({
|
|
4829
5830
|
supported: true,
|
|
4830
5831
|
enabled: true,
|
|
@@ -4851,7 +5852,7 @@ function createSuperDocUI(options) {
|
|
|
4851
5852
|
const supportsSingle = typeof tcApi?.decide === "function" || typeof tcApi?.[trackCommand.kind] === "function";
|
|
4852
5853
|
const supportsAll = typeof tcApi?.decide === "function" || typeof tcApi?.[`${trackCommand.kind}All`] === "function";
|
|
4853
5854
|
const supported = trackCommand.scope === "all" ? supportsAll : supportsSingle;
|
|
4854
|
-
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection
|
|
5855
|
+
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection, doc);
|
|
4855
5856
|
return normalizeCommandState({
|
|
4856
5857
|
enabled: reason == null,
|
|
4857
5858
|
active: false,
|
|
@@ -4906,7 +5907,7 @@ function createSuperDocUI(options) {
|
|
|
4906
5907
|
supported: true,
|
|
4907
5908
|
reason: lockReason
|
|
4908
5909
|
}, "builtin");
|
|
4909
|
-
const enabled = selectionBlockIds(selection
|
|
5910
|
+
const enabled = selectionBlockIds(selection).length > 0 || resolveInlineSelectionTarget(selection) != null;
|
|
4910
5911
|
return normalizeCommandState({
|
|
4911
5912
|
enabled,
|
|
4912
5913
|
active: false,
|
|
@@ -4949,10 +5950,10 @@ function createSuperDocUI(options) {
|
|
|
4949
5950
|
reason
|
|
4950
5951
|
}, "builtin");
|
|
4951
5952
|
}
|
|
4952
|
-
const active = commandActiveState(descriptor, doc, selection
|
|
4953
|
-
const value = routedCommandValue(descriptor, doc, selection
|
|
4954
|
-
if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection
|
|
4955
|
-
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);
|
|
4956
5957
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
4957
5958
|
enabled: false,
|
|
4958
5959
|
active,
|
|
@@ -4960,7 +5961,7 @@ function createSuperDocUI(options) {
|
|
|
4960
5961
|
value,
|
|
4961
5962
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
4962
5963
|
}, "builtin");
|
|
4963
|
-
if (descriptor.inline && !resolveInlineSelectionTarget(selection
|
|
5964
|
+
if (descriptor.inline && !resolveInlineSelectionTarget(selection)) {
|
|
4964
5965
|
const lockReason = contentControlLockReason(lockModesById);
|
|
4965
5966
|
if (lockReason) return normalizeCommandState({
|
|
4966
5967
|
enabled: false,
|
|
@@ -4969,7 +5970,7 @@ function createSuperDocUI(options) {
|
|
|
4969
5970
|
value,
|
|
4970
5971
|
reason: lockReason
|
|
4971
5972
|
}, "builtin");
|
|
4972
|
-
return normalizeCommandState(selectionBlockIds(selection
|
|
5973
|
+
return normalizeCommandState(selectionBlockIds(selection).length > 0 && typeof getHost()?.setPendingInlineFormat === "function" ? {
|
|
4973
5974
|
enabled: true,
|
|
4974
5975
|
active,
|
|
4975
5976
|
supported: true,
|
|
@@ -4982,14 +5983,14 @@ function createSuperDocUI(options) {
|
|
|
4982
5983
|
reason: SUPERDOC_UI_REASONS.rangeSelectionRequired
|
|
4983
5984
|
}, "builtin");
|
|
4984
5985
|
}
|
|
4985
|
-
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection
|
|
5986
|
+
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection).length === 0) return normalizeCommandState({
|
|
4986
5987
|
enabled: false,
|
|
4987
5988
|
active,
|
|
4988
5989
|
supported: true,
|
|
4989
5990
|
value,
|
|
4990
5991
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
4991
5992
|
}, "builtin");
|
|
4992
|
-
if (descriptor.link && !active && selectionTextAddresses(selection
|
|
5993
|
+
if (descriptor.link && !active && selectionTextAddresses(selection).length === 0 && !collapsedTextAddressFromSelection(selection)) return normalizeCommandState({
|
|
4993
5994
|
enabled: false,
|
|
4994
5995
|
active,
|
|
4995
5996
|
supported: true,
|
|
@@ -5012,7 +6013,7 @@ function createSuperDocUI(options) {
|
|
|
5012
6013
|
value
|
|
5013
6014
|
}, "builtin");
|
|
5014
6015
|
};
|
|
5015
|
-
function hyperlinkOverlapsSelection(link, selection
|
|
6016
|
+
function hyperlinkOverlapsSelection(link, selection) {
|
|
5016
6017
|
const anchor = link.address?.anchor;
|
|
5017
6018
|
const start = anchor?.start;
|
|
5018
6019
|
const end = anchor?.end;
|
|
@@ -5022,7 +6023,7 @@ function createSuperDocUI(options) {
|
|
|
5022
6023
|
const linkStart = typeof start?.offset === "number" ? start.offset : null;
|
|
5023
6024
|
const linkEnd = typeof end?.offset === "number" ? end.offset : null;
|
|
5024
6025
|
if (linkStart == null || linkEnd == null) return false;
|
|
5025
|
-
const target = selection
|
|
6026
|
+
const target = selection.target;
|
|
5026
6027
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
5027
6028
|
for (const segment of segments) {
|
|
5028
6029
|
const segmentBlockId = typeof segment?.blockId === "string" ? segment.blockId : null;
|
|
@@ -5045,10 +6046,15 @@ function createSuperDocUI(options) {
|
|
|
5045
6046
|
}
|
|
5046
6047
|
return false;
|
|
5047
6048
|
}
|
|
5048
|
-
|
|
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) => {
|
|
5049
6055
|
const linksApi = doc?.hyperlinks;
|
|
5050
6056
|
if (!linksApi || typeof linksApi.list !== "function") return null;
|
|
5051
|
-
const blockIds = selectionBlockIds(selection
|
|
6057
|
+
const blockIds = selectionBlockIds(selection);
|
|
5052
6058
|
if (!canProbeEverySelectedBlock(blockIds)) return null;
|
|
5053
6059
|
for (const blockId of blockIds) {
|
|
5054
6060
|
const within = {
|
|
@@ -5057,44 +6063,52 @@ function createSuperDocUI(options) {
|
|
|
5057
6063
|
nodeId: blockId
|
|
5058
6064
|
};
|
|
5059
6065
|
const { value: result } = readAsync(`hyperlinks:${blockId}`, contentToken(), () => linksApi.list({ within }), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5060
|
-
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));
|
|
5061
6067
|
if (hit) return hit;
|
|
5062
6068
|
}
|
|
5063
6069
|
return null;
|
|
5064
6070
|
};
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
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;
|
|
5068
6080
|
const pending = pendingInlineActive(descriptor);
|
|
5069
6081
|
if (pending !== null) return pending;
|
|
5070
6082
|
const optimistic = optimisticInlineToggles.get(descriptor.id);
|
|
5071
|
-
const selectionSignature
|
|
5072
|
-
if (selectionSignature
|
|
5073
|
-
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);
|
|
5074
6086
|
if (effective !== null) return effective;
|
|
5075
|
-
return commandIsActive(descriptor, selection
|
|
6087
|
+
return commandIsActive(descriptor, selection);
|
|
5076
6088
|
};
|
|
5077
|
-
|
|
6089
|
+
/** Live `value` for a routed command, when modeled (current paragraph style / link href). */
|
|
6090
|
+
const routedCommandValue = (descriptor, doc, selection, projectedInlineValues) => {
|
|
5078
6091
|
if (descriptor.inline && isProjectedInlineSelectionValueKey(descriptor.inline.key)) {
|
|
5079
6092
|
const pending = pendingInlineValueFor(descriptor);
|
|
5080
6093
|
if (pending !== void 0) return pending;
|
|
5081
6094
|
const projected = projectedInlineValues[descriptor.inline.key];
|
|
5082
6095
|
if (projected !== void 0) return projected;
|
|
5083
|
-
const signature = selectionInlineValueSignature(selection
|
|
6096
|
+
const signature = selectionInlineValueSignature(selection);
|
|
5084
6097
|
const cached = signature ? optimisticInlineValues.get(descriptor.inline.key) : void 0;
|
|
5085
6098
|
if (signature && cached?.selectionSignature === signature) return cached.value;
|
|
5086
6099
|
}
|
|
5087
6100
|
if (descriptor.id === "linked-style") {
|
|
5088
|
-
const { style: active } = computeActiveParagraphStyle(selection
|
|
6101
|
+
const { style: active } = computeActiveParagraphStyle(selection, getStyleCatalog().cache);
|
|
5089
6102
|
if (!active.styleId || active.mixed) return void 0;
|
|
5090
6103
|
return {
|
|
5091
6104
|
styleId: active.styleId,
|
|
5092
6105
|
styleName: active.styleName
|
|
5093
6106
|
};
|
|
5094
6107
|
}
|
|
5095
|
-
if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection
|
|
5096
|
-
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;
|
|
5097
6110
|
};
|
|
6111
|
+
/** Read one block's list state and cache status through the selected story's list-state seam. */
|
|
5098
6112
|
const readListStateSnapshotForBlock = (doc, blockId, story) => {
|
|
5099
6113
|
const listsApi = doc?.lists;
|
|
5100
6114
|
if (!listsApi) return {
|
|
@@ -5116,14 +6130,22 @@ function createSuperDocUI(options) {
|
|
|
5116
6130
|
status
|
|
5117
6131
|
};
|
|
5118
6132
|
};
|
|
6133
|
+
/** Read one block's settled list state through the selected story's list-state seam. */
|
|
5119
6134
|
const readListStateForBlock = (doc, blockId, story) => {
|
|
5120
6135
|
return readListStateSnapshotForBlock(doc, blockId, story).value;
|
|
5121
6136
|
};
|
|
6137
|
+
/** Read one block's list seed (`'bullet'` / `'ordered'`) in the selected story. */
|
|
5122
6138
|
const readListSeedForBlock = (doc, blockId, story) => {
|
|
5123
6139
|
const result = readListStateForBlock(doc, blockId, story);
|
|
5124
6140
|
if (!result || result.isListItem !== true) return null;
|
|
5125
6141
|
return result.seed === "bullet" || result.seed === "ordered" ? result.seed : null;
|
|
5126
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
|
+
*/
|
|
5127
6149
|
const readListMembershipSnapshotForBlock = (doc, blockId, story) => {
|
|
5128
6150
|
const snapshot = readListStateSnapshotForBlock(doc, blockId, story);
|
|
5129
6151
|
const result = snapshot.value;
|
|
@@ -5132,15 +6154,17 @@ function createSuperDocUI(options) {
|
|
|
5132
6154
|
status: snapshot.status
|
|
5133
6155
|
};
|
|
5134
6156
|
};
|
|
5135
|
-
|
|
5136
|
-
|
|
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);
|
|
5137
6160
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return null;
|
|
5138
|
-
const story = selectionStory(selection
|
|
6161
|
+
const story = selectionStory(selection);
|
|
5139
6162
|
const firstSeed = readListSeedForBlock(doc, blockIds[0], story);
|
|
5140
6163
|
if (firstSeed !== "bullet" && firstSeed !== "ordered") return null;
|
|
5141
6164
|
for (const blockId of blockIds.slice(1)) if (readListSeedForBlock(doc, blockId, story) !== firstSeed) return null;
|
|
5142
6165
|
return firstSeed;
|
|
5143
6166
|
};
|
|
6167
|
+
/** Read a paragraph node in its story through the async read coordinator. */
|
|
5144
6168
|
const readNodeById = (doc, blockId, story) => {
|
|
5145
6169
|
if (!doc) return {
|
|
5146
6170
|
value: null,
|
|
@@ -5177,11 +6201,12 @@ function createSuperDocUI(options) {
|
|
|
5177
6201
|
const isProjectionResolvedParagraphAlignment = (value) => {
|
|
5178
6202
|
return value === "start" || value === "end" || value === "distributed" || value === "numTab" || value === "lowKashida" || value === "mediumKashida" || value === "highKashida" || value === "thaiDistribute";
|
|
5179
6203
|
};
|
|
5180
|
-
|
|
6204
|
+
/** Read effective paragraph alignments from the mounted, style-resolved layout. */
|
|
6205
|
+
const readEffectiveParagraphAlignments = (selection, blockIds) => {
|
|
5181
6206
|
const host = getHost();
|
|
5182
6207
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
5183
6208
|
if (typeof readByIds !== "function") return null;
|
|
5184
|
-
const story = selectionStoryLocator(selection
|
|
6209
|
+
const story = selectionStoryLocator(selection);
|
|
5185
6210
|
const blocks = safeCall(() => story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]), null);
|
|
5186
6211
|
if (!Array.isArray(blocks)) return /* @__PURE__ */ new Map();
|
|
5187
6212
|
const projectionBlocks = collectProjectionTextBlocks(blocks);
|
|
@@ -5196,6 +6221,7 @@ function createSuperDocUI(options) {
|
|
|
5196
6221
|
}
|
|
5197
6222
|
return alignments;
|
|
5198
6223
|
};
|
|
6224
|
+
/** Read public paragraph alignment, then fall back to the resolved projection. */
|
|
5199
6225
|
const readParagraphAlignment = (doc, blockId, story, effectiveAlignments) => {
|
|
5200
6226
|
const { value: result, status, refreshing } = readNodeById(doc, blockId, story);
|
|
5201
6227
|
const effective = effectiveAlignments?.get(blockId);
|
|
@@ -5232,11 +6258,12 @@ function createSuperDocUI(options) {
|
|
|
5232
6258
|
};
|
|
5233
6259
|
return { status: "unavailable" };
|
|
5234
6260
|
};
|
|
5235
|
-
|
|
5236
|
-
|
|
6261
|
+
/** Resolve a uniform effective alignment across every selected paragraph. */
|
|
6262
|
+
const readUniformParagraphAlignment = (doc, selection) => {
|
|
6263
|
+
const blockIds = selectionBlockIds(selection);
|
|
5237
6264
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return { status: "unavailable" };
|
|
5238
|
-
const story = selectionStory(selection
|
|
5239
|
-
const effectiveAlignments = readEffectiveParagraphAlignments(selection
|
|
6265
|
+
const story = selectionStory(selection);
|
|
6266
|
+
const effectiveAlignments = readEffectiveParagraphAlignments(selection, blockIds);
|
|
5240
6267
|
const first = readParagraphAlignment(doc, blockIds[0], story, effectiveAlignments);
|
|
5241
6268
|
if (first.status !== "uniform") return first;
|
|
5242
6269
|
for (const blockId of blockIds.slice(1)) {
|
|
@@ -5246,22 +6273,23 @@ function createSuperDocUI(options) {
|
|
|
5246
6273
|
}
|
|
5247
6274
|
return first;
|
|
5248
6275
|
};
|
|
5249
|
-
const paragraphAlignmentSelectionSignature = (selection
|
|
5250
|
-
const blockIds = selectionBlockIds(selection
|
|
6276
|
+
const paragraphAlignmentSelectionSignature = (selection) => {
|
|
6277
|
+
const blockIds = selectionBlockIds(selection);
|
|
5251
6278
|
if (blockIds.length === 0) return null;
|
|
5252
|
-
return `${storyLocatorSignature(selectionStory(selection
|
|
6279
|
+
return `${storyLocatorSignature(selectionStory(selection))}:${blockIds.join(",")}`;
|
|
5253
6280
|
};
|
|
5254
|
-
|
|
5255
|
-
|
|
6281
|
+
/** Keep a toolbar pick stable until its painted paragraph state is readable. */
|
|
6282
|
+
const readToolbarParagraphAlignment = (doc, selection) => {
|
|
6283
|
+
const resolution = readUniformParagraphAlignment(doc, selection);
|
|
5256
6284
|
const resolved = resolution.status === "uniform" ? resolution.value : void 0;
|
|
5257
6285
|
const optimistic = optimisticParagraphAlignment;
|
|
5258
6286
|
if (!optimistic) return resolved;
|
|
5259
|
-
const selectionSignature
|
|
5260
|
-
if (selectionSignature
|
|
6287
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6288
|
+
if (selectionSignature && selectionSignature !== optimistic.selectionSignature) {
|
|
5261
6289
|
optimisticParagraphAlignment = null;
|
|
5262
6290
|
return resolved;
|
|
5263
6291
|
}
|
|
5264
|
-
if (!selectionSignature
|
|
6292
|
+
if (!selectionSignature) return optimistic.value;
|
|
5265
6293
|
if (!optimistic.settled) return optimistic.value;
|
|
5266
6294
|
if (resolution.status !== "pending") {
|
|
5267
6295
|
optimisticParagraphAlignment = null;
|
|
@@ -5269,14 +6297,14 @@ function createSuperDocUI(options) {
|
|
|
5269
6297
|
}
|
|
5270
6298
|
return optimistic.value;
|
|
5271
6299
|
};
|
|
5272
|
-
const armOptimisticParagraphAlignment = (selection
|
|
6300
|
+
const armOptimisticParagraphAlignment = (selection, payload) => {
|
|
5273
6301
|
const alignment = normalizeParagraphAlignment(payload);
|
|
5274
|
-
const selectionSignature
|
|
5275
|
-
const blockIds = selectionBlockIds(selection
|
|
5276
|
-
if (!alignment || !selectionSignature
|
|
6302
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6303
|
+
const blockIds = selectionBlockIds(selection);
|
|
6304
|
+
if (!alignment || !selectionSignature || blockIds.length === 0) return null;
|
|
5277
6305
|
const generation = ++optimisticParagraphAlignmentGeneration;
|
|
5278
6306
|
optimisticParagraphAlignment = {
|
|
5279
|
-
selectionSignature
|
|
6307
|
+
selectionSignature,
|
|
5280
6308
|
value: alignment,
|
|
5281
6309
|
generation,
|
|
5282
6310
|
settled: false,
|
|
@@ -5299,6 +6327,16 @@ function createSuperDocUI(options) {
|
|
|
5299
6327
|
}
|
|
5300
6328
|
optimistic.settled = true;
|
|
5301
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
|
+
*/
|
|
5302
6340
|
const resolveListMembershipForBlockAsync = async (doc, blockId, story) => {
|
|
5303
6341
|
const listsApi = doc?.lists;
|
|
5304
6342
|
if (!listsApi) return null;
|
|
@@ -5315,6 +6353,7 @@ function createSuperDocUI(options) {
|
|
|
5315
6353
|
return null;
|
|
5316
6354
|
}
|
|
5317
6355
|
};
|
|
6356
|
+
/** Read a list seed directly for mutation planning instead of trusting the reactive cache. */
|
|
5318
6357
|
const resolveListSeedForBlock = (doc, blockId, story) => {
|
|
5319
6358
|
const unavailable = {
|
|
5320
6359
|
resolved: false,
|
|
@@ -5343,6 +6382,7 @@ function createSuperDocUI(options) {
|
|
|
5343
6382
|
return unavailable;
|
|
5344
6383
|
}
|
|
5345
6384
|
};
|
|
6385
|
+
/** Await a block's current paragraph indentation authoritatively. */
|
|
5346
6386
|
const resolveParagraphIndentationForBlockAsync = async (doc, blockId, story) => {
|
|
5347
6387
|
if (!doc) return null;
|
|
5348
6388
|
try {
|
|
@@ -5356,7 +6396,7 @@ function createSuperDocUI(options) {
|
|
|
5356
6396
|
return null;
|
|
5357
6397
|
}
|
|
5358
6398
|
};
|
|
5359
|
-
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection
|
|
6399
|
+
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5360
6400
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5361
6401
|
enabled: false,
|
|
5362
6402
|
active,
|
|
@@ -5364,7 +6404,7 @@ function createSuperDocUI(options) {
|
|
|
5364
6404
|
value,
|
|
5365
6405
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5366
6406
|
}, "builtin");
|
|
5367
|
-
const blockIds = selectionBlockIds(selection
|
|
6407
|
+
const blockIds = selectionBlockIds(selection);
|
|
5368
6408
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5369
6409
|
enabled: false,
|
|
5370
6410
|
active,
|
|
@@ -5377,7 +6417,7 @@ function createSuperDocUI(options) {
|
|
|
5377
6417
|
const paragraphClearIndent = resolveDocOperation(doc, "format.paragraph.clearIndentation");
|
|
5378
6418
|
const mode = descriptor.list?.mode;
|
|
5379
6419
|
const paragraphOpAvailable = mode === "indent" ? paragraphSetIndent != null : paragraphSetIndent != null || paragraphClearIndent != null;
|
|
5380
|
-
const story = selectionStory(selection
|
|
6420
|
+
const story = selectionStory(selection);
|
|
5381
6421
|
let listMembership = null;
|
|
5382
6422
|
if (story.storyType !== "body") {
|
|
5383
6423
|
listMembership = [];
|
|
@@ -5461,7 +6501,7 @@ function createSuperDocUI(options) {
|
|
|
5461
6501
|
reason: unavailableRouteReason(doc)
|
|
5462
6502
|
}, "builtin");
|
|
5463
6503
|
};
|
|
5464
|
-
const computeListToggleCommandState = (descriptor, doc, readonly, selection
|
|
6504
|
+
const computeListToggleCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5465
6505
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5466
6506
|
enabled: false,
|
|
5467
6507
|
active,
|
|
@@ -5469,7 +6509,7 @@ function createSuperDocUI(options) {
|
|
|
5469
6509
|
value,
|
|
5470
6510
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5471
6511
|
}, "builtin");
|
|
5472
|
-
const blockIds = selectionBlockIds(selection
|
|
6512
|
+
const blockIds = selectionBlockIds(selection);
|
|
5473
6513
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5474
6514
|
enabled: false,
|
|
5475
6515
|
active,
|
|
@@ -5477,7 +6517,7 @@ function createSuperDocUI(options) {
|
|
|
5477
6517
|
value,
|
|
5478
6518
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
5479
6519
|
}, "builtin");
|
|
5480
|
-
const story = selectionStory(selection
|
|
6520
|
+
const story = selectionStory(selection);
|
|
5481
6521
|
const listsApi = doc?.lists;
|
|
5482
6522
|
if (!(typeof listsApi?.getStateInStory === "function" || story.storyType === "body" && typeof listsApi?.getState === "function")) return normalizeCommandState({
|
|
5483
6523
|
enabled: false,
|
|
@@ -5505,7 +6545,7 @@ function createSuperDocUI(options) {
|
|
|
5505
6545
|
supported: true,
|
|
5506
6546
|
value
|
|
5507
6547
|
}, "builtin");
|
|
5508
|
-
if (snapshots.some(({ value: state
|
|
6548
|
+
if (snapshots.some(({ value: state }) => state === null)) return normalizeCommandState({
|
|
5509
6549
|
enabled: false,
|
|
5510
6550
|
active,
|
|
5511
6551
|
supported: false,
|
|
@@ -5513,7 +6553,7 @@ function createSuperDocUI(options) {
|
|
|
5513
6553
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5514
6554
|
}, "builtin");
|
|
5515
6555
|
const seed = descriptor.list?.seed;
|
|
5516
|
-
const shouldRemove = snapshots.every(({ value: state
|
|
6556
|
+
const shouldRemove = snapshots.every(({ value: state }) => state?.isListItem === true && state.seed === seed);
|
|
5517
6557
|
if (story.storyType === "body") return normalizeCommandState(!shouldRemove || resolveDocOperation(doc, "lists.remove") != null ? {
|
|
5518
6558
|
enabled: true,
|
|
5519
6559
|
active,
|
|
@@ -5541,10 +6581,12 @@ function createSuperDocUI(options) {
|
|
|
5541
6581
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5542
6582
|
}, "builtin");
|
|
5543
6583
|
};
|
|
5544
|
-
|
|
5545
|
-
|
|
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;
|
|
5546
6587
|
return typeof href === "string" ? href : null;
|
|
5547
6588
|
};
|
|
6589
|
+
/** State for a command routed through a public SuperDoc-instance method. */
|
|
5548
6590
|
const computeInstanceCommandState = (descriptor) => {
|
|
5549
6591
|
if (!getEditor()) return normalizeCommandState({
|
|
5550
6592
|
enabled: false,
|
|
@@ -5566,12 +6608,14 @@ function createSuperDocUI(options) {
|
|
|
5566
6608
|
value: descriptorValue(descriptor)
|
|
5567
6609
|
}, "builtin");
|
|
5568
6610
|
};
|
|
6611
|
+
/** Live active state for a host-owned chrome control (`ruler`, `formatting-marks`). */
|
|
5569
6612
|
const chromeActiveState = (descriptor) => {
|
|
5570
6613
|
const config = superdoc?.config ?? void 0;
|
|
5571
6614
|
if (descriptor.valueFrom === "ruler") return Boolean(config?.rulers);
|
|
5572
6615
|
if (descriptor.valueFrom === "formattingMarks") return Boolean((config?.layoutEngineOptions)?.showFormattingMarks);
|
|
5573
6616
|
return false;
|
|
5574
6617
|
};
|
|
6618
|
+
/** State for a table cell-context command routed through `tables.*`. */
|
|
5575
6619
|
const computeTableCommandState = (descriptor, doc, readonly) => {
|
|
5576
6620
|
if (readonly) return normalizeCommandState({
|
|
5577
6621
|
enabled: false,
|
|
@@ -5598,12 +6642,14 @@ function createSuperDocUI(options) {
|
|
|
5598
6642
|
supported: true
|
|
5599
6643
|
}, "builtin");
|
|
5600
6644
|
};
|
|
6645
|
+
/** Resolve a descriptor's live `value` from public controller state, when modeled. */
|
|
5601
6646
|
const descriptorValue = (descriptor) => {
|
|
5602
6647
|
if (descriptor.valueFrom === "zoom") return computeZoom().value;
|
|
5603
6648
|
if (descriptor.valueFrom === "documentMode") return readDocumentMode();
|
|
5604
6649
|
if (descriptor.valueFrom === "measurementUnit") return readMeasurementUnit();
|
|
5605
6650
|
if (descriptor.valueFrom === "ruler" || descriptor.valueFrom === "formattingMarks") return chromeActiveState(descriptor);
|
|
5606
6651
|
};
|
|
6652
|
+
/** Validate normalized payloads before invoking public SuperDoc-instance methods. */
|
|
5607
6653
|
const instanceCommandPayloadIsValid = (descriptor, payload) => {
|
|
5608
6654
|
if (descriptor.id === "zoom") return typeof payload === "number" && Number.isFinite(payload) && payload > 0;
|
|
5609
6655
|
if (descriptor.id === "document-mode") return payload === "editing" || payload === "suggesting" || payload === "viewing";
|
|
@@ -5611,7 +6657,7 @@ function createSuperDocUI(options) {
|
|
|
5611
6657
|
return true;
|
|
5612
6658
|
};
|
|
5613
6659
|
const allCommandIds = () => {
|
|
5614
|
-
return [
|
|
6660
|
+
return [.../* @__PURE__ */ new Set([...ALL_BUILT_IN_COMMAND_IDS, ...customCommands.keys()])];
|
|
5615
6661
|
};
|
|
5616
6662
|
const DEFAULT_PARAGRAPH_STYLE_ID = "Normal";
|
|
5617
6663
|
const EMPTY_STYLES_SLICE = {
|
|
@@ -5625,6 +6671,14 @@ function createSuperDocUI(options) {
|
|
|
5625
6671
|
sourceStatus: null,
|
|
5626
6672
|
diagnostics: []
|
|
5627
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
|
+
*/
|
|
5628
6682
|
const readStyleCatalogLive = (input) => {
|
|
5629
6683
|
const stylesApi = getDoc()?.styles;
|
|
5630
6684
|
if (!stylesApi || typeof stylesApi.getCatalog !== "function") return {
|
|
@@ -5633,6 +6687,7 @@ function createSuperDocUI(options) {
|
|
|
5633
6687
|
};
|
|
5634
6688
|
return readAsync(`styles:catalog:${JSON.stringify(input ?? {})}`, contentToken(), () => stylesApi.getCatalog(input), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5635
6689
|
};
|
|
6690
|
+
/** Resolve the catalogue (coordinator-cached by content revision) plus its readiness. */
|
|
5636
6691
|
const getStyleCatalog = () => {
|
|
5637
6692
|
const full = readStyleCatalogLive({ includePreview: true });
|
|
5638
6693
|
if (!full.value) return {
|
|
@@ -5655,6 +6710,13 @@ function createSuperDocUI(options) {
|
|
|
5655
6710
|
status: combineStatus(full.status, quickResult.status)
|
|
5656
6711
|
};
|
|
5657
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
|
+
*/
|
|
5658
6720
|
const readBlockStyleRef = (doc, blockId, story) => {
|
|
5659
6721
|
const { value: result, status } = readNodeById(doc, blockId, story);
|
|
5660
6722
|
if (!result) return {
|
|
@@ -5677,7 +6739,13 @@ function createSuperDocUI(options) {
|
|
|
5677
6739
|
status
|
|
5678
6740
|
};
|
|
5679
6741
|
};
|
|
5680
|
-
|
|
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) => {
|
|
5681
6749
|
const diagnostics = [];
|
|
5682
6750
|
const doc = getDoc();
|
|
5683
6751
|
if (!doc) {
|
|
@@ -5696,7 +6764,7 @@ function createSuperDocUI(options) {
|
|
|
5696
6764
|
status: "ready"
|
|
5697
6765
|
};
|
|
5698
6766
|
}
|
|
5699
|
-
const blockIds = selectionBlockIds(selection
|
|
6767
|
+
const blockIds = selectionBlockIds(selection);
|
|
5700
6768
|
if (blockIds.length === 0) {
|
|
5701
6769
|
diagnostics.push({
|
|
5702
6770
|
severity: "info",
|
|
@@ -5735,7 +6803,7 @@ function createSuperDocUI(options) {
|
|
|
5735
6803
|
let blockReadFailures = 0;
|
|
5736
6804
|
let defaultReadFailures = 0;
|
|
5737
6805
|
let readStatus = "ready";
|
|
5738
|
-
const story = selectionStory(selection
|
|
6806
|
+
const story = selectionStory(selection);
|
|
5739
6807
|
for (const blockId of blockIds) {
|
|
5740
6808
|
const read = readBlockStyleRef(doc, blockId, story);
|
|
5741
6809
|
readStatus = combineStatus(readStatus, read.status);
|
|
@@ -5747,13 +6815,13 @@ function createSuperDocUI(options) {
|
|
|
5747
6815
|
defaultReadFailures += 1;
|
|
5748
6816
|
continue;
|
|
5749
6817
|
}
|
|
5750
|
-
const styleId
|
|
5751
|
-
if (!styleId
|
|
6818
|
+
const styleId = read.styleRef ?? defaultId;
|
|
6819
|
+
if (!styleId) {
|
|
5752
6820
|
defaultReadFailures += 1;
|
|
5753
6821
|
continue;
|
|
5754
6822
|
}
|
|
5755
6823
|
resolved += 1;
|
|
5756
|
-
resolvedIds.add(styleId
|
|
6824
|
+
resolvedIds.add(styleId);
|
|
5757
6825
|
}
|
|
5758
6826
|
if (resolved === 0) {
|
|
5759
6827
|
const onlyDefaultUnavailable = defaultReadFailures > 0 && blockReadFailures === 0;
|
|
@@ -5809,10 +6877,10 @@ function createSuperDocUI(options) {
|
|
|
5809
6877
|
status: readStatus
|
|
5810
6878
|
};
|
|
5811
6879
|
};
|
|
5812
|
-
const computeStyles = (selection
|
|
6880
|
+
const computeStyles = (selection) => {
|
|
5813
6881
|
if (!getEditor()) return EMPTY_STYLES_SLICE;
|
|
5814
6882
|
const { cache: catalog, status: catalogStatus } = getStyleCatalog();
|
|
5815
|
-
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection
|
|
6883
|
+
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection, catalog);
|
|
5816
6884
|
let diagnostics;
|
|
5817
6885
|
if (catalog) diagnostics = active.diagnostics.length === 0 ? catalog.full.diagnostics : [...catalog.full.diagnostics, ...active.diagnostics];
|
|
5818
6886
|
else diagnostics = [{
|
|
@@ -5822,7 +6890,7 @@ function createSuperDocUI(options) {
|
|
|
5822
6890
|
}, ...active.diagnostics];
|
|
5823
6891
|
return {
|
|
5824
6892
|
ready: true,
|
|
5825
|
-
status: combineStatus(catalogStatus, activeStatus, selection
|
|
6893
|
+
status: combineStatus(catalogStatus, activeStatus, selection.status),
|
|
5826
6894
|
catalogRevision: catalog?.full.revision ?? null,
|
|
5827
6895
|
quickGallery: catalog?.quickGallery ?? [],
|
|
5828
6896
|
activeParagraphStyleId: active.styleId,
|
|
@@ -5834,32 +6902,32 @@ function createSuperDocUI(options) {
|
|
|
5834
6902
|
};
|
|
5835
6903
|
const computeState = () => {
|
|
5836
6904
|
const documentMode = readDocumentMode();
|
|
5837
|
-
const selection
|
|
5838
|
-
if (selection
|
|
5839
|
-
reconcilePendingInlineFormat(selection
|
|
5840
|
-
const selectionSignature
|
|
5841
|
-
for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature
|
|
5842
|
-
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);
|
|
5843
6911
|
return {
|
|
5844
6912
|
ready: getEditor() != null,
|
|
5845
6913
|
documentMode,
|
|
5846
6914
|
document: computeDocument(),
|
|
5847
|
-
selection
|
|
6915
|
+
selection,
|
|
5848
6916
|
toolbar: {
|
|
5849
6917
|
context: documentMode,
|
|
5850
|
-
commands: computeCommandStates(selection
|
|
6918
|
+
commands: computeCommandStates(selection),
|
|
5851
6919
|
copyFormatActive: painter.mode !== "idle"
|
|
5852
6920
|
},
|
|
5853
|
-
comments: computeComments(selection
|
|
5854
|
-
trackChanges: computeTrackChanges(selection
|
|
5855
|
-
contentControls: computeContentControls(selection
|
|
6921
|
+
comments: computeComments(selection),
|
|
6922
|
+
trackChanges: computeTrackChanges(selection),
|
|
6923
|
+
contentControls: computeContentControls(selection),
|
|
5856
6924
|
zoom: computeZoom(),
|
|
5857
6925
|
fonts: computeFonts(),
|
|
5858
|
-
styles: computeStyles(selection
|
|
6926
|
+
styles: computeStyles(selection)
|
|
5859
6927
|
};
|
|
5860
6928
|
};
|
|
5861
|
-
const syncOptimisticInlineSelection = (selection
|
|
5862
|
-
const nextSignature = selectionInlineValueSignature(selection
|
|
6929
|
+
const syncOptimisticInlineSelection = (selection) => {
|
|
6930
|
+
const nextSignature = selectionInlineValueSignature(selection);
|
|
5863
6931
|
if (nextSignature && lastOptimisticInlineSelectionSignature && nextSignature !== lastOptimisticInlineSelectionSignature) {
|
|
5864
6932
|
optimisticInlineValues.clear();
|
|
5865
6933
|
optimisticInlineToggles.clear();
|
|
@@ -5907,8 +6975,8 @@ function createSuperDocUI(options) {
|
|
|
5907
6975
|
let detachDocumentSelection = null;
|
|
5908
6976
|
const readHostSelectionSource = () => {
|
|
5909
6977
|
const host = getEditor()?.host;
|
|
5910
|
-
const selection
|
|
5911
|
-
return selection
|
|
6978
|
+
const selection = safeCall(host?.getHandles ? () => host.getHandles() : void 0, null)?.editing?.selection;
|
|
6979
|
+
return selection && typeof selection.subscribe === "function" ? selection : null;
|
|
5912
6980
|
};
|
|
5913
6981
|
const syncHostSelectionSubscription = () => {
|
|
5914
6982
|
const next = readHostSelectionSource();
|
|
@@ -5980,7 +7048,8 @@ function createSuperDocUI(options) {
|
|
|
5980
7048
|
try {
|
|
5981
7049
|
const unsubscribe = next.subscribe((snapshot) => {
|
|
5982
7050
|
const snapshotRecord = snapshot && typeof snapshot === "object" ? snapshot : null;
|
|
5983
|
-
|
|
7051
|
+
const target = snapshotRecord && Object.prototype.hasOwnProperty.call(snapshotRecord, "activeReviewTarget") ? snapshotRecord.activeReviewTarget : readHostActiveReviewTarget(next);
|
|
7052
|
+
if (!syncTrackedChangeFocusFromHostReviewTarget(target, snapshot) || attaching) return;
|
|
5984
7053
|
recompute();
|
|
5985
7054
|
});
|
|
5986
7055
|
if (typeof unsubscribe === "function") detachHostReview = unsubscribe;
|
|
@@ -6134,7 +7203,29 @@ function createSuperDocUI(options) {
|
|
|
6134
7203
|
state = nextState;
|
|
6135
7204
|
for (const listener of [...listeners]) listener(state);
|
|
6136
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
|
+
*/
|
|
6137
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
|
+
*/
|
|
6138
7229
|
const documentResetHooks = [];
|
|
6139
7230
|
const runHooks = (hooks) => {
|
|
6140
7231
|
for (const reset of hooks) try {
|
|
@@ -6197,6 +7288,20 @@ function createSuperDocUI(options) {
|
|
|
6197
7288
|
};
|
|
6198
7289
|
};
|
|
6199
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
|
+
*/
|
|
6200
7305
|
const snapshotHandle = (sub) => {
|
|
6201
7306
|
const observe = (listener) => {
|
|
6202
7307
|
const safe = (snapshot) => {
|
|
@@ -6218,8 +7323,8 @@ function createSuperDocUI(options) {
|
|
|
6218
7323
|
function readTrackDecisionTargetId(target) {
|
|
6219
7324
|
return target && typeof target === "object" && typeof target.id === "string" ? String(target.id) : null;
|
|
6220
7325
|
}
|
|
6221
|
-
function nonBodySelectionStoryLocator(selection
|
|
6222
|
-
const story = selectionStoryLocator(selection
|
|
7326
|
+
function nonBodySelectionStoryLocator(selection) {
|
|
7327
|
+
const story = selectionStoryLocator(selection);
|
|
6223
7328
|
return story && storyLocatorSignature(story) !== storyLocatorSignature(null) ? story : void 0;
|
|
6224
7329
|
}
|
|
6225
7330
|
function trackDecisionIdTarget(changeId, story) {
|
|
@@ -6229,15 +7334,24 @@ function createSuperDocUI(options) {
|
|
|
6229
7334
|
...story ? { story } : {}
|
|
6230
7335
|
};
|
|
6231
7336
|
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
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;
|
|
6235
7349
|
const start = explicit?.start;
|
|
6236
7350
|
const end = explicit?.end;
|
|
6237
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;
|
|
6238
7352
|
const rangeStart = Math.min(start.offset, end.offset);
|
|
6239
7353
|
const rangeEnd = Math.max(start.offset, end.offset);
|
|
6240
|
-
const story = selectionStoryLocator(selection
|
|
7354
|
+
const story = selectionStoryLocator(selection);
|
|
6241
7355
|
return {
|
|
6242
7356
|
kind: "range",
|
|
6243
7357
|
coordinateSpace: explicit.coordinateSpace === "tracked" ? "tracked" : "visible",
|
|
@@ -6256,17 +7370,17 @@ function createSuperDocUI(options) {
|
|
|
6256
7370
|
}
|
|
6257
7371
|
function readBodyTrackChangeForDecision(activeId, story) {
|
|
6258
7372
|
if (storyLocatorSignature(story) !== storyLocatorSignature(null)) return void 0;
|
|
6259
|
-
const trackChanges
|
|
6260
|
-
const list = trackChanges
|
|
7373
|
+
const trackChanges = getDoc()?.trackChanges;
|
|
7374
|
+
const list = trackChanges?.list;
|
|
6261
7375
|
if (typeof list !== "function") return void 0;
|
|
6262
|
-
const raw = safeCall(() => list.call(trackChanges
|
|
7376
|
+
const raw = safeCall(() => list.call(trackChanges), null);
|
|
6263
7377
|
if (isPromiseLike(raw) || !raw || typeof raw !== "object") return void 0;
|
|
6264
7378
|
const items = raw.items;
|
|
6265
7379
|
if (!Array.isArray(items)) return void 0;
|
|
6266
7380
|
return items.map(projectTrackChangesItem).find((item) => item != null && entityRowMatchesRequest(item, activeId, story));
|
|
6267
7381
|
}
|
|
6268
|
-
function selectedTextCoversTrackChangeText(selection
|
|
6269
|
-
const selected = normalizeTrackDecisionText(selection
|
|
7382
|
+
function selectedTextCoversTrackChangeText(selection, item) {
|
|
7383
|
+
const selected = normalizeTrackDecisionText(selection.quotedText);
|
|
6270
7384
|
if (!selected) return false;
|
|
6271
7385
|
const payload = trackChangesItemPayload(item);
|
|
6272
7386
|
return [
|
|
@@ -6298,7 +7412,8 @@ function createSuperDocUI(options) {
|
|
|
6298
7412
|
if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
|
|
6299
7413
|
return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
|
|
6300
7414
|
}));
|
|
6301
|
-
|
|
7415
|
+
const commandResult = commandResultFromOperationResult(result);
|
|
7416
|
+
return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
|
|
6302
7417
|
} catch {
|
|
6303
7418
|
return false;
|
|
6304
7419
|
}
|
|
@@ -6441,8 +7556,8 @@ function createSuperDocUI(options) {
|
|
|
6441
7556
|
const scheduleInlineToggleMutation = (key, mutate) => {
|
|
6442
7557
|
if (!inlineToggleMutationActive) {
|
|
6443
7558
|
inlineToggleMutationActive = true;
|
|
6444
|
-
inlineToggleMutationIdle = new Promise((resolve
|
|
6445
|
-
resolveInlineToggleMutationIdle = resolve
|
|
7559
|
+
inlineToggleMutationIdle = new Promise((resolve) => {
|
|
7560
|
+
resolveInlineToggleMutationIdle = resolve;
|
|
6446
7561
|
});
|
|
6447
7562
|
try {
|
|
6448
7563
|
return {
|
|
@@ -6479,20 +7594,21 @@ function createSuperDocUI(options) {
|
|
|
6479
7594
|
release: releaseInlineToggleMutationOnce()
|
|
6480
7595
|
};
|
|
6481
7596
|
};
|
|
6482
|
-
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = new Set(["create.tableOfContents"]);
|
|
7597
|
+
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = /* @__PURE__ */ new Set(["create.tableOfContents"]);
|
|
6483
7598
|
const unsupportedTrackedMutationReceipt = (route) => failedReceipt(`Tracked authoring is not supported for ${route}.`);
|
|
6484
7599
|
const editorMutationOptionsForRoute = (route) => {
|
|
6485
7600
|
if (readDocumentMode() !== "suggesting") return void 0;
|
|
6486
7601
|
if (UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES.has(route)) return unsupportedTrackedMutationReceipt(route);
|
|
6487
7602
|
return { changeMode: "tracked" };
|
|
6488
7603
|
};
|
|
7604
|
+
/** Clear-patch to send for the current document mode (see TRACKED_CLEAR_INLINE_PATCH). */
|
|
6489
7605
|
const clearInlinePatchForMode = () => readDocumentMode() === "suggesting" ? TRACKED_CLEAR_INLINE_PATCH : CLEAR_INLINE_PATCH;
|
|
6490
7606
|
const callEditorMutation = (route, op, input) => {
|
|
6491
|
-
const options
|
|
6492
|
-
if (options
|
|
6493
|
-
return options
|
|
7607
|
+
const options = editorMutationOptionsForRoute(route);
|
|
7608
|
+
if (options && options.success === false) return options;
|
|
7609
|
+
return options ? op(input, options) : op(input);
|
|
6494
7610
|
};
|
|
6495
|
-
const captureOptimisticInlineValueResult = (descriptor, selection
|
|
7611
|
+
const captureOptimisticInlineValueResult = (descriptor, selection, input, result) => {
|
|
6496
7612
|
const inline = descriptor.inline;
|
|
6497
7613
|
if (!inline) return;
|
|
6498
7614
|
const commandResult = commandResultFromOperationResult(result);
|
|
@@ -6503,35 +7619,35 @@ function createSuperDocUI(options) {
|
|
|
6503
7619
|
return;
|
|
6504
7620
|
}
|
|
6505
7621
|
if (inline.kind === "toggle" || !isProjectedInlineSelectionValueKey(inline.key)) return;
|
|
6506
|
-
const selectionSignature
|
|
6507
|
-
if (!selectionSignature
|
|
7622
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7623
|
+
if (!selectionSignature) return;
|
|
6508
7624
|
const value = normalizeOptimisticInlineSelectionValue(inline.key, input.value);
|
|
6509
7625
|
if (value == null) {
|
|
6510
7626
|
optimisticInlineValues.delete(inline.key);
|
|
6511
7627
|
return;
|
|
6512
7628
|
}
|
|
6513
7629
|
optimisticInlineValues.set(inline.key, {
|
|
6514
|
-
selectionSignature
|
|
7630
|
+
selectionSignature,
|
|
6515
7631
|
value
|
|
6516
7632
|
});
|
|
6517
7633
|
};
|
|
6518
|
-
const decorateInlineCommandResult = (descriptor, selection
|
|
7634
|
+
const decorateInlineCommandResult = (descriptor, selection, input, result) => {
|
|
6519
7635
|
if (!descriptor.inline || descriptor.inline.kind === "toggle") return result;
|
|
6520
7636
|
if (isPromiseLike(result)) return Promise.resolve(result).then((resolved) => {
|
|
6521
|
-
captureOptimisticInlineValueResult(descriptor, selection
|
|
7637
|
+
captureOptimisticInlineValueResult(descriptor, selection, input, resolved);
|
|
6522
7638
|
return resolved;
|
|
6523
7639
|
});
|
|
6524
7640
|
return result;
|
|
6525
7641
|
};
|
|
6526
|
-
const captureOptimisticInlineToggle = (descriptor, selection
|
|
7642
|
+
const captureOptimisticInlineToggle = (descriptor, selection, input, result) => {
|
|
6527
7643
|
if (descriptor.inline?.kind !== "toggle") return null;
|
|
6528
7644
|
if (!commandResultSucceeded(commandResultFromOperationResult(result))) return null;
|
|
6529
7645
|
if (typeof input.value !== "boolean" && input.value !== null) return null;
|
|
6530
|
-
const selectionSignature
|
|
6531
|
-
if (!selectionSignature
|
|
7646
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7647
|
+
if (!selectionSignature) return null;
|
|
6532
7648
|
const generation = ++optimisticInlineToggleGeneration;
|
|
6533
7649
|
optimisticInlineToggles.set(descriptor.id, {
|
|
6534
|
-
selectionSignature
|
|
7650
|
+
selectionSignature,
|
|
6535
7651
|
active: input.value === true,
|
|
6536
7652
|
generation,
|
|
6537
7653
|
settled: false
|
|
@@ -6599,6 +7715,11 @@ function createSuperDocUI(options) {
|
|
|
6599
7715
|
recompute();
|
|
6600
7716
|
return normalizedReceipt;
|
|
6601
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
|
+
*/
|
|
6602
7723
|
const applyPerBlock = (route, op, blockIds, buildInput) => {
|
|
6603
7724
|
const immediateResults = [];
|
|
6604
7725
|
const settledResults = [];
|
|
@@ -6617,6 +7738,7 @@ function createSuperDocUI(options) {
|
|
|
6617
7738
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6618
7739
|
return combineCommandResults(immediateResults);
|
|
6619
7740
|
};
|
|
7741
|
+
/** Apply a Document API operation once per covered text segment. */
|
|
6620
7742
|
const applyPerTextAddress = (route, op, targets, buildInput) => {
|
|
6621
7743
|
const immediateResults = [];
|
|
6622
7744
|
const settledResults = [];
|
|
@@ -6635,15 +7757,25 @@ function createSuperDocUI(options) {
|
|
|
6635
7757
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6636
7758
|
return combineCommandResults(immediateResults);
|
|
6637
7759
|
};
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
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);
|
|
6642
7773
|
return op(input, {
|
|
6643
|
-
...options
|
|
7774
|
+
...options ?? {},
|
|
6644
7775
|
offsetSpace: "selection"
|
|
6645
7776
|
});
|
|
6646
7777
|
};
|
|
7778
|
+
/** Build the `format.paragraph.*` / `styles.paragraph.*` input for a block. */
|
|
6647
7779
|
const buildBlockParagraphInput = (spec, blockId, payload, story) => {
|
|
6648
7780
|
const target = paragraphTarget(blockId, story);
|
|
6649
7781
|
switch (spec.kind) {
|
|
@@ -6688,6 +7820,14 @@ function createSuperDocUI(options) {
|
|
|
6688
7820
|
default: return null;
|
|
6689
7821
|
}
|
|
6690
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
|
+
*/
|
|
6691
7831
|
const storePendingInlineFormat = (descriptor, payload) => {
|
|
6692
7832
|
const spec = descriptor.inline;
|
|
6693
7833
|
const method = inlineFormatMethod(descriptor);
|
|
@@ -6725,9 +7865,9 @@ function createSuperDocUI(options) {
|
|
|
6725
7865
|
immediateResults.push(settled.immediate);
|
|
6726
7866
|
settledResults.push(settled.settled);
|
|
6727
7867
|
};
|
|
6728
|
-
const run = (op, input, options
|
|
7868
|
+
const run = (op, input, options) => {
|
|
6729
7869
|
try {
|
|
6730
|
-
record(options
|
|
7870
|
+
record(options ? op(input, options) : op(input));
|
|
6731
7871
|
} catch {
|
|
6732
7872
|
immediateResults.push(false);
|
|
6733
7873
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6786,6 +7926,7 @@ function createSuperDocUI(options) {
|
|
|
6786
7926
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6787
7927
|
return combineCommandResults(immediateResults.length ? immediateResults : [false]);
|
|
6788
7928
|
};
|
|
7929
|
+
/** Route a block-level paragraph command (`format.paragraph.*` / `styles.paragraph.*`). */
|
|
6789
7930
|
const executeBlockParagraph = (descriptor, op, payload) => {
|
|
6790
7931
|
const blockIds = selectionBlockIds(state.selection);
|
|
6791
7932
|
if (blockIds.length === 0) return false;
|
|
@@ -6891,6 +8032,7 @@ function createSuperDocUI(options) {
|
|
|
6891
8032
|
})();
|
|
6892
8033
|
return true;
|
|
6893
8034
|
};
|
|
8035
|
+
/** Route a list command (`lists.apply` / `lists.remove` / list level changes). */
|
|
6894
8036
|
const executeListCommand = (descriptor, op) => {
|
|
6895
8037
|
const spec = descriptor.list;
|
|
6896
8038
|
const doc = getDoc();
|
|
@@ -6928,9 +8070,9 @@ function createSuperDocUI(options) {
|
|
|
6928
8070
|
seed
|
|
6929
8071
|
};
|
|
6930
8072
|
try {
|
|
6931
|
-
const result
|
|
6932
|
-
immediateResults.push(result
|
|
6933
|
-
settledResults.push(result
|
|
8073
|
+
const result = settleOperationResult(callEditorMutation(route, operation, input));
|
|
8074
|
+
immediateResults.push(result.immediate);
|
|
8075
|
+
settledResults.push(result.settled);
|
|
6934
8076
|
} catch {
|
|
6935
8077
|
immediateResults.push(false);
|
|
6936
8078
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6944,8 +8086,8 @@ function createSuperDocUI(options) {
|
|
|
6944
8086
|
const stateReads = blockIds.map((blockId) => resolveListSeedForBlock(doc, blockId, story));
|
|
6945
8087
|
if (stateReads.some(isPromiseLike)) {
|
|
6946
8088
|
pendingCommandSettlement = Promise.all(stateReads).then(async (states) => {
|
|
6947
|
-
const result
|
|
6948
|
-
return result
|
|
8089
|
+
const result = executeResolved(states);
|
|
8090
|
+
return result.settled ? await result.settled : result.immediate;
|
|
6949
8091
|
});
|
|
6950
8092
|
return true;
|
|
6951
8093
|
}
|
|
@@ -6971,6 +8113,7 @@ function createSuperDocUI(options) {
|
|
|
6971
8113
|
const capture = record.capture && typeof record.capture === "object" ? record.capture : null;
|
|
6972
8114
|
return record.target ?? capture?.target ?? capture?.selectionTarget ?? record.selectionTarget ?? record.textTarget ?? null;
|
|
6973
8115
|
}
|
|
8116
|
+
/** Route a link command (`hyperlinks.wrap` / `insert` / `patch` / `remove`). */
|
|
6974
8117
|
const executeLinkCommand = (payload) => {
|
|
6975
8118
|
const doc = getDoc();
|
|
6976
8119
|
const linksApi = doc?.hyperlinks;
|
|
@@ -7106,8 +8249,8 @@ function createSuperDocUI(options) {
|
|
|
7106
8249
|
if (wrapTargets.length > 0) {
|
|
7107
8250
|
if (typeof wrap !== "function") return false;
|
|
7108
8251
|
try {
|
|
7109
|
-
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target
|
|
7110
|
-
target
|
|
8252
|
+
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target) => ({
|
|
8253
|
+
target,
|
|
7111
8254
|
link: { destination: { href: normalizedHref } }
|
|
7112
8255
|
}));
|
|
7113
8256
|
} catch {
|
|
@@ -7115,14 +8258,14 @@ function createSuperDocUI(options) {
|
|
|
7115
8258
|
}
|
|
7116
8259
|
}
|
|
7117
8260
|
const insert = linksApi.insert;
|
|
7118
|
-
const text
|
|
7119
|
-
if (typeof insert !== "function" || text
|
|
8261
|
+
const text = readLinkPayloadText(payload, normalizedHref);
|
|
8262
|
+
if (typeof insert !== "function" || text.trim() === "") return false;
|
|
7120
8263
|
const target = collapsedTextAddressFromTarget(payloadTarget) ?? collapsedTextAddressFromSelection(state.selection);
|
|
7121
8264
|
if (!target) return false;
|
|
7122
8265
|
try {
|
|
7123
8266
|
return callEditorMutation("hyperlinks.insert", insert, {
|
|
7124
8267
|
target,
|
|
7125
|
-
text
|
|
8268
|
+
text,
|
|
7126
8269
|
link: { destination: { href: normalizedHref } }
|
|
7127
8270
|
});
|
|
7128
8271
|
} catch {
|
|
@@ -7131,6 +8274,7 @@ function createSuperDocUI(options) {
|
|
|
7131
8274
|
}
|
|
7132
8275
|
return false;
|
|
7133
8276
|
};
|
|
8277
|
+
/** Route a create command (`create.table` / `create.image` / `create.tableOfContents`). */
|
|
7134
8278
|
const executeCreateCommand = (descriptor, op, payload) => {
|
|
7135
8279
|
const kind = descriptor.create.kind;
|
|
7136
8280
|
const record = payload && typeof payload === "object" ? payload : {};
|
|
@@ -7176,16 +8320,16 @@ function createSuperDocUI(options) {
|
|
|
7176
8320
|
}
|
|
7177
8321
|
};
|
|
7178
8322
|
}
|
|
7179
|
-
const input
|
|
8323
|
+
const input = {
|
|
7180
8324
|
src,
|
|
7181
8325
|
at
|
|
7182
8326
|
};
|
|
7183
|
-
if (nonBodyStory) input
|
|
7184
|
-
if (typeof record.alt === "string") input
|
|
7185
|
-
if (typeof record.title === "string") input
|
|
7186
|
-
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;
|
|
7187
8331
|
try {
|
|
7188
|
-
return callEditorMutation(descriptor.docRoute, op, input
|
|
8332
|
+
return callEditorMutation(descriptor.docRoute, op, input);
|
|
7189
8333
|
} catch {
|
|
7190
8334
|
return false;
|
|
7191
8335
|
}
|
|
@@ -7199,6 +8343,13 @@ function createSuperDocUI(options) {
|
|
|
7199
8343
|
return false;
|
|
7200
8344
|
}
|
|
7201
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
|
+
*/
|
|
7202
8353
|
const executeTableCommand = (descriptor, op) => {
|
|
7203
8354
|
const spec = descriptor.table;
|
|
7204
8355
|
const context = resolveTableContext();
|
|
@@ -7211,9 +8362,9 @@ function createSuperDocUI(options) {
|
|
|
7211
8362
|
const host = getHost();
|
|
7212
8363
|
const insertRow = ((typeof host?.getHandles === "function" ? host.getHandles() : null)?.editing)?.tables;
|
|
7213
8364
|
if (typeof insertRow?.insertRow !== "function") return false;
|
|
7214
|
-
const options
|
|
7215
|
-
if (options
|
|
7216
|
-
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);
|
|
7217
8368
|
}
|
|
7218
8369
|
return callEditorMutation(descriptor.docRoute, op, input);
|
|
7219
8370
|
} catch {
|
|
@@ -7254,7 +8405,8 @@ function createSuperDocUI(options) {
|
|
|
7254
8405
|
if (disposed) return false;
|
|
7255
8406
|
const custom = customCommands.get(id);
|
|
7256
8407
|
if (custom) try {
|
|
7257
|
-
|
|
8408
|
+
const result = custom.execute(buildCustomCommandContext(payload, context));
|
|
8409
|
+
return settleCommandExecution(result);
|
|
7258
8410
|
} catch {
|
|
7259
8411
|
return false;
|
|
7260
8412
|
}
|
|
@@ -7275,7 +8427,8 @@ function createSuperDocUI(options) {
|
|
|
7275
8427
|
if (typeof method !== "function") return false;
|
|
7276
8428
|
try {
|
|
7277
8429
|
const arg = descriptor.fixedArg !== void 0 ? descriptor.fixedArg : normalized;
|
|
7278
|
-
|
|
8430
|
+
const result = method.call(superdoc, arg);
|
|
8431
|
+
return settleCommandExecution(result);
|
|
7279
8432
|
} catch {
|
|
7280
8433
|
return false;
|
|
7281
8434
|
}
|
|
@@ -7309,12 +8462,13 @@ function createSuperDocUI(options) {
|
|
|
7309
8462
|
if (getEditor() !== commandEditor || getDoc() !== doc || readDocumentMode() !== commandMode) return false;
|
|
7310
8463
|
return callInlineFormatMutation(route, op, input);
|
|
7311
8464
|
};
|
|
7312
|
-
const selectionSignature
|
|
7313
|
-
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;
|
|
7314
8467
|
releaseScheduledMutation = scheduledMutation?.release ?? null;
|
|
7315
8468
|
const mutationResult = scheduledMutation ? scheduledMutation.result : mutate();
|
|
7316
8469
|
const optimisticGeneration = captureOptimisticInlineToggle(descriptor, state.selection, input, mutationResult);
|
|
7317
|
-
const
|
|
8470
|
+
const result = decorateInlineCommandResult(descriptor, state.selection, input, mutationResult);
|
|
8471
|
+
const immediate = settleCommandExecution(result, (settled) => {
|
|
7318
8472
|
settleOptimisticInlineToggle(descriptor, optimisticGeneration, settled);
|
|
7319
8473
|
releaseScheduledMutation?.();
|
|
7320
8474
|
releaseScheduledMutation = null;
|
|
@@ -7333,10 +8487,22 @@ function createSuperDocUI(options) {
|
|
|
7333
8487
|
settleOptimisticParagraphAlignment(optimisticAlignmentGeneration, settled);
|
|
7334
8488
|
});
|
|
7335
8489
|
}
|
|
7336
|
-
if (descriptor.list)
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
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
|
+
}
|
|
7340
8506
|
try {
|
|
7341
8507
|
return settleCommandExecution(descriptor.mutates ? callEditorMutation(route, op, normalized) : op(normalized));
|
|
7342
8508
|
} catch {
|
|
@@ -7403,8 +8569,8 @@ function createSuperDocUI(options) {
|
|
|
7403
8569
|
recompute();
|
|
7404
8570
|
return true;
|
|
7405
8571
|
};
|
|
7406
|
-
const captureFormatPainter = async (selection
|
|
7407
|
-
const selTarget = captureSlice?.target ?? selection
|
|
8572
|
+
const captureFormatPainter = async (selection, captureSlice, captureProjected, visibleActiveMarks = []) => {
|
|
8573
|
+
const selTarget = captureSlice?.target ?? selection?.target;
|
|
7408
8574
|
if (!selTarget || selTarget["kind"] !== "text") return null;
|
|
7409
8575
|
const segments = Array.isArray(selTarget["segments"]) ? selTarget["segments"] : [];
|
|
7410
8576
|
if (segments.length === 0) return null;
|
|
@@ -7416,7 +8582,8 @@ function createSuperDocUI(options) {
|
|
|
7416
8582
|
const node = (await readBlockNode(paragraphTarget(segment["blockId"], sourceStory)))?.["node"] ?? null;
|
|
7417
8583
|
if (!node) continue;
|
|
7418
8584
|
const para = getParagraphLikeData(node);
|
|
7419
|
-
const
|
|
8585
|
+
const runs = Array.isArray(para?.["inlines"]) ? para["inlines"] : [];
|
|
8586
|
+
const slicedProps = sliceAndIntersectRunProps(runs, segment["range"]);
|
|
7420
8587
|
if (!mergedRunProps) mergedRunProps = slicedProps;
|
|
7421
8588
|
else mergedRunProps = intersectRunProps(mergedRunProps, slicedProps);
|
|
7422
8589
|
}
|
|
@@ -7427,9 +8594,9 @@ function createSuperDocUI(options) {
|
|
|
7427
8594
|
underline: "underline",
|
|
7428
8595
|
strikethrough: "strike"
|
|
7429
8596
|
};
|
|
7430
|
-
const activeMarks = new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection
|
|
8597
|
+
const activeMarks = /* @__PURE__ */ new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection?.["activeMarks"]) ? selection["activeMarks"] : []), ...visibleActiveMarks]);
|
|
7431
8598
|
for (const [markName, patchKey] of Object.entries(MARK_TO_PATCH)) if (activeMarks.has(markName)) inlinePatch[patchKey] = true;
|
|
7432
|
-
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection
|
|
8599
|
+
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection, "ready"));
|
|
7433
8600
|
if (projectedInlineValues.fontFamily) inlinePatch["fontFamily"] = projectedInlineValues.fontFamily;
|
|
7434
8601
|
if (projectedInlineValues.color) inlinePatch["color"] = projectedInlineValues.color;
|
|
7435
8602
|
if (projectedInlineValues.highlight) inlinePatch["highlight"] = projectedInlineValues.highlight;
|
|
@@ -7466,12 +8633,12 @@ function createSuperDocUI(options) {
|
|
|
7466
8633
|
return r["run"]?.["props"];
|
|
7467
8634
|
};
|
|
7468
8635
|
if (range.start === range.end) {
|
|
7469
|
-
let offset
|
|
8636
|
+
let offset = 0;
|
|
7470
8637
|
for (const run of runs) {
|
|
7471
8638
|
const text = getRunText(run);
|
|
7472
|
-
const runStart = offset
|
|
7473
|
-
const runEnd = offset
|
|
7474
|
-
offset
|
|
8639
|
+
const runStart = offset;
|
|
8640
|
+
const runEnd = offset + text.length;
|
|
8641
|
+
offset = runEnd;
|
|
7475
8642
|
if (range.start >= runStart && range.start <= runEnd) {
|
|
7476
8643
|
const rPr = getRunProps(run);
|
|
7477
8644
|
return rPr ? { ...rPr } : {};
|
|
@@ -7548,7 +8715,8 @@ function createSuperDocUI(options) {
|
|
|
7548
8715
|
if (painter.pointerSelecting || painter.keyboardSelecting) return;
|
|
7549
8716
|
const selectionApi = getDoc()?.selection;
|
|
7550
8717
|
const readSelection = async () => {
|
|
7551
|
-
|
|
8718
|
+
const rawSelection = typeof selectionApi?.["current"] === "function" ? await Promise.resolve(selectionApi["current"]({ includeText: true })) : readSelectionInfoLive().value;
|
|
8719
|
+
return normalizeSelectionInfo(rawSelection);
|
|
7552
8720
|
};
|
|
7553
8721
|
const waitForSelectionSettle = () => new Promise((resolve) => setTimeout(resolve, 16));
|
|
7554
8722
|
const MAX_SELECTION_ATTEMPTS = 8;
|
|
@@ -7574,7 +8742,7 @@ function createSuperDocUI(options) {
|
|
|
7574
8742
|
await applyFormatPainter(caretSel);
|
|
7575
8743
|
}
|
|
7576
8744
|
};
|
|
7577
|
-
const applyFormatPainter = async (selection
|
|
8745
|
+
const applyFormatPainter = async (selection) => {
|
|
7578
8746
|
const snap = painter.snapshot;
|
|
7579
8747
|
if (!snap) {
|
|
7580
8748
|
if (painter.mode === "armed") exitFormatPainter();
|
|
@@ -7582,21 +8750,21 @@ function createSuperDocUI(options) {
|
|
|
7582
8750
|
}
|
|
7583
8751
|
const doc = getDoc();
|
|
7584
8752
|
if (!doc) return;
|
|
7585
|
-
const targetSelection = selectionSliceFromInfo(selection
|
|
8753
|
+
const targetSelection = selectionSliceFromInfo(selection, "ready");
|
|
7586
8754
|
const { lockModesById } = computeActiveContentControlLockModes(doc.contentControls, targetSelection);
|
|
7587
8755
|
if (contentControlLockReason(lockModesById)) {
|
|
7588
8756
|
recompute();
|
|
7589
8757
|
return;
|
|
7590
8758
|
}
|
|
7591
|
-
const selTarget = selection
|
|
7592
|
-
const selectionTarget = selection
|
|
7593
|
-
if (!(selection
|
|
8759
|
+
const selTarget = selection["target"];
|
|
8760
|
+
const selectionTarget = selection["selectionTarget"];
|
|
8761
|
+
if (!(selection["empty"] === true) && Object.keys(snap.inline).length > 0) {
|
|
7594
8762
|
const inlineOp = resolveDocOperation(doc, "format.apply");
|
|
7595
8763
|
if (inlineOp && selectionTarget) try {
|
|
7596
8764
|
if (!commandResultSucceeded(commandResultFromOperationResult(await Promise.resolve(callInlineFormatMutation("format.apply", inlineOp, {
|
|
7597
8765
|
target: selectionTarget,
|
|
7598
8766
|
inline: snap.inline
|
|
7599
|
-
}, selection
|
|
8767
|
+
}, selection))))) {
|
|
7600
8768
|
recompute();
|
|
7601
8769
|
return;
|
|
7602
8770
|
}
|
|
@@ -7632,11 +8800,11 @@ function createSuperDocUI(options) {
|
|
|
7632
8800
|
if (spacing) {
|
|
7633
8801
|
const sp = spacing;
|
|
7634
8802
|
const rawLr = sp["lineRule"] ?? "auto";
|
|
7635
|
-
const lr = new Set([
|
|
8803
|
+
const lr = (/* @__PURE__ */ new Set([
|
|
7636
8804
|
"auto",
|
|
7637
8805
|
"exact",
|
|
7638
8806
|
"atLeast"
|
|
7639
|
-
]).has(rawLr) ? rawLr : "auto";
|
|
8807
|
+
])).has(rawLr) ? rawLr : "auto";
|
|
7640
8808
|
const rawSpacing = {
|
|
7641
8809
|
...sp["before"] != null ? { before: Math.round(sp["before"] * 20) } : {},
|
|
7642
8810
|
...sp["after"] != null ? { after: Math.round(sp["after"] * 20) } : {},
|
|
@@ -7726,6 +8894,14 @@ function createSuperDocUI(options) {
|
|
|
7726
8894
|
};
|
|
7727
8895
|
const selectionSub = sliceHandle((s) => s.selection);
|
|
7728
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
|
+
*/
|
|
7729
8905
|
const getSelectionApplyHelper = () => {
|
|
7730
8906
|
const host = getHost();
|
|
7731
8907
|
if (typeof host?.getHandles !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
|
|
@@ -7744,8 +8920,8 @@ function createSuperDocUI(options) {
|
|
|
7744
8920
|
const host = getHost();
|
|
7745
8921
|
if (typeof host?.getHandles !== "function") return;
|
|
7746
8922
|
try {
|
|
7747
|
-
const contentControls
|
|
7748
|
-
if (typeof contentControls
|
|
8923
|
+
const contentControls = (host.getHandles()?.editing)?.contentControls;
|
|
8924
|
+
if (typeof contentControls?.activate === "function") contentControls.activate({ id });
|
|
7749
8925
|
} catch {}
|
|
7750
8926
|
};
|
|
7751
8927
|
const selection = {
|
|
@@ -7873,7 +9049,17 @@ function createSuperDocUI(options) {
|
|
|
7873
9049
|
execute: executeCommand,
|
|
7874
9050
|
executeAsync: executeCommandAsync
|
|
7875
9051
|
};
|
|
7876
|
-
|
|
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) => {
|
|
7877
9063
|
if (!getEditor()) return {
|
|
7878
9064
|
success: false,
|
|
7879
9065
|
ok: false,
|
|
@@ -7894,10 +9080,10 @@ function createSuperDocUI(options) {
|
|
|
7894
9080
|
try {
|
|
7895
9081
|
const input = {
|
|
7896
9082
|
target,
|
|
7897
|
-
block: options
|
|
7898
|
-
behavior: options
|
|
9083
|
+
block: options?.block ?? "center",
|
|
9084
|
+
behavior: options?.behavior ?? "smooth"
|
|
7899
9085
|
};
|
|
7900
|
-
const result = await Promise.resolve(options
|
|
9086
|
+
const result = await Promise.resolve(options?.shouldContinue ? scroll.call(host, input, options.shouldContinue) : scroll.call(host, input));
|
|
7901
9087
|
if (result && typeof result === "object" && result.success === false) return {
|
|
7902
9088
|
success: false,
|
|
7903
9089
|
ok: false,
|
|
@@ -7915,6 +9101,11 @@ function createSuperDocUI(options) {
|
|
|
7915
9101
|
};
|
|
7916
9102
|
}
|
|
7917
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
|
+
*/
|
|
7918
9109
|
const resolveEntityTarget = async (namespace, id, loaded, request) => {
|
|
7919
9110
|
const requestedStory = namespace === "trackChanges" ? request?.story : void 0;
|
|
7920
9111
|
const withRequestedStory = (target) => {
|
|
@@ -7967,6 +9158,18 @@ function createSuperDocUI(options) {
|
|
|
7967
9158
|
status
|
|
7968
9159
|
}), fallback), fallback);
|
|
7969
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
|
+
*/
|
|
7970
9173
|
const commentWriteRefusal = () => reviewMutationsAreReadOnly() ? failedReceipt("Comments are read-only.", "DOCUMENT_READONLY") : null;
|
|
7971
9174
|
const filterCommentsSnapshot = (items, query) => {
|
|
7972
9175
|
const record = query && typeof query === "object" ? query : null;
|
|
@@ -8101,7 +9304,7 @@ function createSuperDocUI(options) {
|
|
|
8101
9304
|
}
|
|
8102
9305
|
};
|
|
8103
9306
|
const trackChangesSub = sliceHandle((s) => s.trackChanges);
|
|
8104
|
-
const navigateTrackChange = (step, options
|
|
9307
|
+
const navigateTrackChange = (step, options) => {
|
|
8105
9308
|
const allRows = trackChangesSub.get().items.filter((item) => readEntityId(item) !== null);
|
|
8106
9309
|
if (allRows.length === 0) return null;
|
|
8107
9310
|
const currentId = state.trackChanges.activeId;
|
|
@@ -8116,7 +9319,7 @@ function createSuperDocUI(options) {
|
|
|
8116
9319
|
setExplicitActiveChange(story ? {
|
|
8117
9320
|
id: activeId,
|
|
8118
9321
|
story
|
|
8119
|
-
} : { id: activeId }, options
|
|
9322
|
+
} : { id: activeId }, options);
|
|
8120
9323
|
recompute();
|
|
8121
9324
|
return activeId;
|
|
8122
9325
|
};
|
|
@@ -8337,11 +9540,12 @@ function createSuperDocUI(options) {
|
|
|
8337
9540
|
id: input,
|
|
8338
9541
|
story: void 0
|
|
8339
9542
|
} : input;
|
|
8340
|
-
|
|
9543
|
+
const target = {
|
|
8341
9544
|
kind: "id",
|
|
8342
9545
|
id,
|
|
8343
9546
|
...story ? { story } : {}
|
|
8344
|
-
}
|
|
9547
|
+
};
|
|
9548
|
+
return executeTrackDecisionTarget(kind, target, id, story);
|
|
8345
9549
|
};
|
|
8346
9550
|
const executeTrackDecisionTarget = (kind, target, changeId, story) => {
|
|
8347
9551
|
try {
|
|
@@ -8591,9 +9795,9 @@ function createSuperDocUI(options) {
|
|
|
8591
9795
|
const hits = [];
|
|
8592
9796
|
for (const hit of rawHits) {
|
|
8593
9797
|
if (hit.type !== "trackedChange") {
|
|
8594
|
-
const key
|
|
8595
|
-
if (seen.has(key
|
|
8596
|
-
seen.add(key
|
|
9798
|
+
const key = `${hit.type}:${hit.id}`;
|
|
9799
|
+
if (seen.has(key)) continue;
|
|
9800
|
+
seen.add(key);
|
|
8597
9801
|
hits.push(hit);
|
|
8598
9802
|
continue;
|
|
8599
9803
|
}
|
|
@@ -8649,6 +9853,21 @@ function createSuperDocUI(options) {
|
|
|
8649
9853
|
}
|
|
8650
9854
|
entry.boundHost = null;
|
|
8651
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
|
+
*/
|
|
8652
9871
|
const releaseGeometryObserversIfStillHostless = () => {
|
|
8653
9872
|
geometryReleaseScheduled = false;
|
|
8654
9873
|
if (disposed || getHost()) return;
|
|
@@ -8785,6 +10004,7 @@ function createSuperDocUI(options) {
|
|
|
8785
10004
|
return target && typeof target === "object" ? target : null;
|
|
8786
10005
|
};
|
|
8787
10006
|
const metadataResolveKey = (id) => `metadata:resolve:${id}`;
|
|
10007
|
+
/** Resolve a metadata id to its SelectionTarget, or null when unresolved. */
|
|
8788
10008
|
const resolveMetadataTarget = (id) => {
|
|
8789
10009
|
const metaApi = getDoc()?.metadata;
|
|
8790
10010
|
if (!metaApi || typeof metaApi.resolve !== "function") return null;
|
|
@@ -8868,16 +10088,16 @@ function createSuperDocUI(options) {
|
|
|
8868
10088
|
let searchState = { ...SEARCH_UNAVAILABLE_SLICE };
|
|
8869
10089
|
const searchListeners = /* @__PURE__ */ new Set();
|
|
8870
10090
|
const getHostSearch = () => {
|
|
8871
|
-
const search
|
|
8872
|
-
return search
|
|
10091
|
+
const search = getHost()?.search;
|
|
10092
|
+
return search && typeof search === "object" ? search : null;
|
|
8873
10093
|
};
|
|
8874
10094
|
const getEditCommandSearch = () => {
|
|
8875
|
-
const search
|
|
8876
|
-
return search
|
|
10095
|
+
const search = getEditCommands()?.search;
|
|
10096
|
+
return search && typeof search === "object" ? search : null;
|
|
8877
10097
|
};
|
|
8878
10098
|
const searchIsAvailable = () => {
|
|
8879
|
-
const search
|
|
8880
|
-
if (search
|
|
10099
|
+
const search = getHostSearch();
|
|
10100
|
+
if (search && typeof search.setSession === "function") return true;
|
|
8881
10101
|
const editSearch = getEditCommandSearch();
|
|
8882
10102
|
return Boolean(editSearch) && typeof editSearch.query === "function" && typeof editSearch.getState === "function";
|
|
8883
10103
|
};
|
|
@@ -8895,7 +10115,21 @@ function createSuperDocUI(options) {
|
|
|
8895
10115
|
return searchState;
|
|
8896
10116
|
};
|
|
8897
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
|
+
*/
|
|
8898
10131
|
let releaseActiveSearchSession = null;
|
|
10132
|
+
/** Release whatever session is open, then forget it. */
|
|
8899
10133
|
const releaseSearchSession = () => {
|
|
8900
10134
|
const release = releaseActiveSearchSession;
|
|
8901
10135
|
releaseActiveSearchSession = null;
|
|
@@ -8905,6 +10139,23 @@ function createSuperDocUI(options) {
|
|
|
8905
10139
|
return null;
|
|
8906
10140
|
}, null);
|
|
8907
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
|
+
*/
|
|
8908
10159
|
const resetSearchForActiveEditorChange = () => {
|
|
8909
10160
|
searchRequestGeneration += 1;
|
|
8910
10161
|
releaseSearchSession();
|
|
@@ -9085,11 +10336,11 @@ function createSuperDocUI(options) {
|
|
|
9085
10336
|
if (!searchState.available) searchState.reason = SUPERDOC_UI_REASONS.searchUnavailable;
|
|
9086
10337
|
emitSearch();
|
|
9087
10338
|
},
|
|
9088
|
-
search: (query, options
|
|
10339
|
+
search: (query, options) => {
|
|
9089
10340
|
const host = getHostSearch();
|
|
9090
|
-
const caseSensitive = Boolean(options
|
|
9091
|
-
const includeDeletedText = options
|
|
9092
|
-
const regex = options
|
|
10341
|
+
const caseSensitive = Boolean(options?.caseSensitive);
|
|
10342
|
+
const includeDeletedText = options?.includeDeletedText === true;
|
|
10343
|
+
const regex = options?.regex === true;
|
|
9093
10344
|
const generation = ++searchRequestGeneration;
|
|
9094
10345
|
if (!host || typeof host.setSession !== "function") {
|
|
9095
10346
|
const editSearch = getEditCommandSearch();
|
|
@@ -9149,12 +10400,13 @@ function createSuperDocUI(options) {
|
|
|
9149
10400
|
releaseActiveSearchSession = () => {
|
|
9150
10401
|
if (typeof host.clear === "function") host.clear();
|
|
9151
10402
|
};
|
|
9152
|
-
|
|
10403
|
+
const result = safeCall(() => host.setSession(query, {
|
|
9153
10404
|
caseSensitive,
|
|
9154
10405
|
includeDeletedText,
|
|
9155
10406
|
...regex ? { regex: true } : {},
|
|
9156
10407
|
highlight: true
|
|
9157
|
-
}), null)
|
|
10408
|
+
}), null);
|
|
10409
|
+
applyHostSearchResult(result);
|
|
9158
10410
|
if (query.length > 0 && searchState.total === 0 && searchState.reason !== SUPERDOC_UI_REASONS.searchInvalidPattern) {
|
|
9159
10411
|
const editSearch = getEditCommandSearch();
|
|
9160
10412
|
if (editSearch && typeof editSearch.query === "function" && typeof editSearch.getState === "function") {
|
|
@@ -9284,13 +10536,13 @@ function createSuperDocUI(options) {
|
|
|
9284
10536
|
ok: false,
|
|
9285
10537
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9286
10538
|
};
|
|
9287
|
-
const result
|
|
9288
|
-
if (isPromiseLike(result
|
|
10539
|
+
const result = safeCall(() => editSearch.replace({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10540
|
+
if (isPromiseLike(result)) {
|
|
9289
10541
|
const generation = searchRequestGeneration;
|
|
9290
10542
|
syncSearchStateFromHost();
|
|
9291
10543
|
emitSearch();
|
|
9292
10544
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9293
|
-
return Promise.resolve(result
|
|
10545
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9294
10546
|
if (isCurrent()) {
|
|
9295
10547
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9296
10548
|
syncSearchStateFromHost();
|
|
@@ -9310,7 +10562,7 @@ function createSuperDocUI(options) {
|
|
|
9310
10562
|
}
|
|
9311
10563
|
syncSearchStateFromHost();
|
|
9312
10564
|
emitSearch();
|
|
9313
|
-
return mapHostReplaceResult(result
|
|
10565
|
+
return mapHostReplaceResult(result);
|
|
9314
10566
|
}
|
|
9315
10567
|
if (typeof host.replaceCurrent !== "function") return {
|
|
9316
10568
|
ok: false,
|
|
@@ -9333,13 +10585,13 @@ function createSuperDocUI(options) {
|
|
|
9333
10585
|
ok: false,
|
|
9334
10586
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9335
10587
|
};
|
|
9336
|
-
const result
|
|
9337
|
-
if (isPromiseLike(result
|
|
10588
|
+
const result = safeCall(() => editSearch.replaceAll({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10589
|
+
if (isPromiseLike(result)) {
|
|
9338
10590
|
const generation = searchRequestGeneration;
|
|
9339
10591
|
syncSearchStateFromHost();
|
|
9340
10592
|
emitSearch();
|
|
9341
10593
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9342
|
-
return Promise.resolve(result
|
|
10594
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9343
10595
|
if (isCurrent()) {
|
|
9344
10596
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9345
10597
|
syncSearchStateFromHost();
|
|
@@ -9359,7 +10611,7 @@ function createSuperDocUI(options) {
|
|
|
9359
10611
|
}
|
|
9360
10612
|
syncSearchStateFromHost();
|
|
9361
10613
|
emitSearch();
|
|
9362
|
-
return mapHostReplaceResult(result
|
|
10614
|
+
return mapHostReplaceResult(result);
|
|
9363
10615
|
}
|
|
9364
10616
|
if (typeof host.replaceAll !== "function") return {
|
|
9365
10617
|
ok: false,
|
|
@@ -9378,7 +10630,7 @@ function createSuperDocUI(options) {
|
|
|
9378
10630
|
getSnapshot: stylesSnap.getSnapshot,
|
|
9379
10631
|
subscribe: stylesSnap.subscribe,
|
|
9380
10632
|
observe: stylesSnap.observe,
|
|
9381
|
-
getCatalog: (options
|
|
10633
|
+
getCatalog: (options) => readStyleCatalogLive(options).value,
|
|
9382
10634
|
getQuickGallery: () => stylesSub.get().quickGallery,
|
|
9383
10635
|
getActiveParagraphStyle: () => computeActiveParagraphStyle(state.selection, getStyleCatalog().cache).style
|
|
9384
10636
|
};
|
|
@@ -9525,6 +10777,12 @@ function createSuperDocUI(options) {
|
|
|
9525
10777
|
controllerRef = controller;
|
|
9526
10778
|
return controller;
|
|
9527
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
|
+
*/
|
|
9528
10786
|
var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
9529
10787
|
{
|
|
9530
10788
|
value: "Arial",
|
|
@@ -9557,6 +10815,11 @@ var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
|
9557
10815
|
previewFamily: "Verdana, sans-serif"
|
|
9558
10816
|
}
|
|
9559
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
|
+
*/
|
|
9560
10823
|
function canonicalFontFamilyName(family) {
|
|
9561
10824
|
const trimmed = family.trim();
|
|
9562
10825
|
const quote = trimmed[0];
|
|
@@ -9567,6 +10830,10 @@ function canonicalFontFamilyName(family) {
|
|
|
9567
10830
|
const comma = trimmed.indexOf(",");
|
|
9568
10831
|
return (comma === -1 ? trimmed : trimmed.slice(0, comma)).trim().replace(/^["']|["']$/g, "");
|
|
9569
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
|
+
*/
|
|
9570
10837
|
function normalizeDocumentFontOptions(raw) {
|
|
9571
10838
|
if (!Array.isArray(raw)) return [];
|
|
9572
10839
|
const out = [];
|
|
@@ -9584,6 +10851,11 @@ function normalizeDocumentFontOptions(raw) {
|
|
|
9584
10851
|
}
|
|
9585
10852
|
return out;
|
|
9586
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
|
+
*/
|
|
9587
10859
|
function normalizePickerFontOptions(raw) {
|
|
9588
10860
|
if (!Array.isArray(raw)) return [];
|
|
9589
10861
|
const out = [];
|
|
@@ -9602,6 +10874,11 @@ function normalizePickerFontOptions(raw) {
|
|
|
9602
10874
|
}
|
|
9603
10875
|
return out;
|
|
9604
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
|
+
*/
|
|
9605
10882
|
function composeFontFamilyOptions(documentOptions, pickerOptions = []) {
|
|
9606
10883
|
const out = [];
|
|
9607
10884
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9658,7 +10935,9 @@ var DEFAULT_FONT_SIZE_OPTIONS = [
|
|
|
9658
10935
|
label: "36"
|
|
9659
10936
|
}
|
|
9660
10937
|
];
|
|
10938
|
+
/** Active state for a routed command: true when its mark is live at the selection. */
|
|
9661
10939
|
function commandIsActive(descriptor, selection) {
|
|
9662
10940
|
return descriptor.activeMark ? selection.activeMarks.includes(descriptor.activeMark) : false;
|
|
9663
10941
|
}
|
|
9664
|
-
|
|
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 };
|