superdoc 2.4.0-next.1 → 2.4.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{FindReplaceSurface-Dx0llWqP.cjs → FindReplaceSurface-0p7EtMZh.cjs} +4 -4
- package/dist/chunks/{FindReplaceSurface-BEW-1NhM.es.js → FindReplaceSurface-40AnrqWk.es.js} +5 -4
- package/dist/chunks/{PasswordPromptSurface-BUUVC2ee.cjs → PasswordPromptSurface-31lYPcCK.cjs} +4 -3
- package/dist/chunks/{PasswordPromptSurface-BA6HHqEa.es.js → PasswordPromptSurface-CwYeO3iQ.es.js} +4 -2
- package/dist/chunks/{PdfViewer-CNOtyS-I.es.js → PdfViewer-D8h7LRZy.es.js} +103 -19
- package/dist/chunks/{PdfViewer-qpvzwYMz.cjs → PdfViewer-Dug6yDRC.cjs} +105 -20
- package/dist/chunks/_plugin-vue_export-helper-BOaGB7Aw.es.js +8 -0
- package/dist/chunks/_plugin-vue_export-helper-SDR04tiH.cjs +13 -0
- package/dist/chunks/{blank-docx-DP8RUPW-.cjs → blank-docx-BuFAbRjs.cjs} +2 -0
- package/dist/chunks/{blank-docx-XRX6Ker2.es.js → blank-docx-DzQccOlW.es.js} +2 -0
- package/dist/chunks/constants-B6VBlmKp.es.js +4 -0
- package/dist/chunks/constants-DpXuDx_g.cjs +9 -0
- package/dist/chunks/{create-super-doc-ui-DXdWuTzm.cjs → create-super-doc-ui-BWYOinLD.cjs} +1836 -532
- package/dist/chunks/{create-super-doc-ui-D4kk9AjM.es.js → create-super-doc-ui-D0mNmAHJ.es.js} +2026 -500
- package/dist/chunks/{eventemitter3-DqY4aSMf.cjs → eventemitter3-C_TAnXOl.cjs} +125 -16
- package/dist/chunks/{eventemitter3-Bt2s0X0a.es.js → eventemitter3-DEIiXiH2.es.js} +124 -16
- package/dist/chunks/jszip-BzJ3CyxR.es.js +4717 -0
- package/dist/chunks/jszip-D8mAFF-r.cjs +4758 -0
- package/dist/chunks/{rolldown-runtime-D7PMmH3s.es.js → rolldown-runtime-0pSA04fp.es.js} +12 -3
- package/dist/chunks/{rolldown-runtime-1Y-nnZJ3.cjs → rolldown-runtime-74VwNCz7.cjs} +11 -2
- package/dist/chunks/{uuid-CFp0WGVU.cjs → uuid-BhG0ngwk.cjs} +10 -6
- package/dist/chunks/{uuid-B2Sqk-3p.es.js → uuid-H0Xcmmhy.es.js} +8 -3
- package/dist/collaboration-upgrade-engine.cjs +34 -9
- package/dist/collaboration-upgrade-engine.es.js +30 -5
- package/dist/document-api/src/types/track-changes.types.d.ts +1 -3
- package/dist/public/ui-react.cjs +66 -2
- package/dist/public/ui-react.es.js +66 -1
- package/dist/public/ui.cjs +1 -1
- package/dist/public/ui.es.js +1 -1
- package/dist/style.css +163 -128
- package/dist/style.layered.css +163 -128
- package/dist/superdoc/src/core/types/index.d.ts +125 -5
- package/dist/superdoc/src/helpers/v2-review-mutation-impact.d.ts +1 -0
- package/dist/superdoc/src/helpers/v2-review-mutation-reconciler.d.ts +1 -1
- package/dist/superdoc/src/internal/toolbar/compatibility-catalog.d.ts +1 -1
- package/dist/superdoc/src/public/index.d.cts +8 -0
- package/dist/superdoc/src/public/index.d.ts +4 -0
- package/dist/superdoc/src/public/ui/types.d.ts +25 -1
- package/dist/superdoc/src/stores/comments-store.d.ts +100 -22
- package/dist/superdoc/src/stores/superdoc-store.d.ts +300 -66
- package/dist/superdoc.cjs +6348 -1202
- package/dist/superdoc.es.js +6226 -1079
- package/dist-cdn/style.layered.css +1 -1
- package/dist-cdn/superdoc.min.css +1 -1
- package/dist-cdn/superdoc.min.js +37 -37
- package/package.json +14 -14
- package/dist/chunks/_plugin-vue_export-helper-BTwbGDKw.cjs +0 -11
- package/dist/chunks/_plugin-vue_export-helper-CInC0bKI.es.js +0 -6
- package/dist/chunks/constants-CY3R3_kF.es.js +0 -3
- package/dist/chunks/constants-sbCZ2O_A.cjs +0 -14
- package/dist/chunks/jszip-C8srOKAO.es.js +0 -4650
- package/dist/chunks/jszip-Cs9JBLlJ.cjs +0 -4691
|
@@ -1,3 +1,161 @@
|
|
|
1
|
+
//#region ../layout-engine/dom-contract/src/class-names.ts
|
|
2
|
+
/**
|
|
3
|
+
* DOM Contract: Class Names
|
|
4
|
+
*
|
|
5
|
+
* CSS class names stamped on rendered document elements by the DOM painter.
|
|
6
|
+
* These names form a public contract read by the painter (emitter) and by
|
|
7
|
+
* editor-side DOM observation code (reader).
|
|
8
|
+
*
|
|
9
|
+
* Changing a value here is a breaking change for both systems.
|
|
10
|
+
*/
|
|
11
|
+
var DOM_CLASS_NAMES = {
|
|
12
|
+
/** Top-level page container element. */
|
|
13
|
+
PAGE: "superdoc-page",
|
|
14
|
+
/** Fragment container (paragraph, table, image block, etc.). */
|
|
15
|
+
FRAGMENT: "superdoc-fragment",
|
|
16
|
+
/** Line container within a fragment. */
|
|
17
|
+
LINE: "superdoc-line",
|
|
18
|
+
/**
|
|
19
|
+
* Inline structured-content (SDT) wrapper.
|
|
20
|
+
*
|
|
21
|
+
* Carries `data-pm-start` / `data-pm-end` for selection highlighting.
|
|
22
|
+
* Should be EXCLUDED from click-to-position mapping — child spans are
|
|
23
|
+
* the character-level targets.
|
|
24
|
+
*/
|
|
25
|
+
INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
|
|
26
|
+
/** Inline structured-content label chrome. */
|
|
27
|
+
INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
|
|
28
|
+
/** Block-level structured-content container. */
|
|
29
|
+
BLOCK_SDT: "superdoc-structured-content-block",
|
|
30
|
+
/** Block-level structured-content label chrome. */
|
|
31
|
+
BLOCK_SDT_LABEL: "superdoc-structured-content__label",
|
|
32
|
+
/** Table fragment container (resize overlay and click-mapping target). */
|
|
33
|
+
TABLE_FRAGMENT: "superdoc-table-fragment",
|
|
34
|
+
/** Document section container. */
|
|
35
|
+
DOCUMENT_SECTION: "superdoc-document-section",
|
|
36
|
+
/**
|
|
37
|
+
* Grouped hover highlight applied to all fragments of the same block SDT.
|
|
38
|
+
* Set by document-runtime hover coordination via event delegation.
|
|
39
|
+
*/
|
|
40
|
+
SDT_GROUP_HOVER: "sdt-group-hover",
|
|
41
|
+
/** Paragraph fragment rendered as a Table of Contents entry. */
|
|
42
|
+
TOC_ENTRY: "superdoc-toc-entry",
|
|
43
|
+
/** TOC analogue of `SDT_GROUP_HOVER`, applied to every fragment sharing a `data-toc-id`. */
|
|
44
|
+
TOC_GROUP_HOVER: "toc-group-hover",
|
|
45
|
+
/** Block-level image fragment (ImageBlock). */
|
|
46
|
+
IMAGE_FRAGMENT: "superdoc-image-fragment",
|
|
47
|
+
/** Inline image element (ImageRun inside a paragraph). */
|
|
48
|
+
INLINE_IMAGE: "superdoc-inline-image",
|
|
49
|
+
/** Wrapper around a paragraph's list marker (bullet glyph or ordered number). */
|
|
50
|
+
LIST_MARKER: "superdoc-list-marker",
|
|
51
|
+
/** Clip wrapper around a cropped inline image. */
|
|
52
|
+
INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
|
|
53
|
+
/** Field annotation outer wrapper. */
|
|
54
|
+
ANNOTATION: "annotation",
|
|
55
|
+
/** Field annotation inner content wrapper. */
|
|
56
|
+
ANNOTATION_CONTENT: "annotation-content",
|
|
57
|
+
/** Hidden caret anchor span appended after field annotation content. */
|
|
58
|
+
ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
|
|
59
|
+
};
|
|
60
|
+
DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../layout-engine/dom-contract/src/data-attrs.ts
|
|
63
|
+
/**
|
|
64
|
+
* DOM Contract: Data Attributes
|
|
65
|
+
*
|
|
66
|
+
* Named constants for `data-*` attributes stamped on rendered DOM elements.
|
|
67
|
+
* These attributes are read by editor-side DOM observers, click-to-position
|
|
68
|
+
* mapping, and bridge compatibility code.
|
|
69
|
+
*
|
|
70
|
+
* Each constant stores the full attribute name (e.g. `"data-pm-start"`) as it
|
|
71
|
+
* appears in `getAttribute()` / `setAttribute()` calls and CSS selectors.
|
|
72
|
+
*
|
|
73
|
+
* The `DATASET_KEYS` mirror provides the camelCase equivalents used with
|
|
74
|
+
* `element.dataset.*` access.
|
|
75
|
+
*
|
|
76
|
+
* Editor-neutral (prep-001) attributes live alongside the legacy `data-pm-*`
|
|
77
|
+
* attributes; both surfaces are emitted so that v1 consumers continue to work
|
|
78
|
+
* unmodified while future editor-neutral consumers (hit-test / range mapping)
|
|
79
|
+
* can address rendered output without consulting ProseMirror positions.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Full attribute names for use with `getAttribute` / `setAttribute` / CSS selectors.
|
|
83
|
+
*/
|
|
84
|
+
var DATA_ATTRS = {
|
|
85
|
+
/** ProseMirror start position of the element's content range. */
|
|
86
|
+
PM_START: "data-pm-start",
|
|
87
|
+
/** ProseMirror end position of the element's content range. */
|
|
88
|
+
PM_END: "data-pm-end",
|
|
89
|
+
/** Layout epoch stamp — incremented on each layout pass. */
|
|
90
|
+
LAYOUT_EPOCH: "data-layout-epoch",
|
|
91
|
+
/** JSON-encoded table boundary metadata for resize overlays. */
|
|
92
|
+
TABLE_BOUNDARIES: "data-table-boundaries",
|
|
93
|
+
/** SDT unique identifier. */
|
|
94
|
+
SDT_ID: "data-sdt-id",
|
|
95
|
+
/** SDT type (fieldAnnotation, structuredContent, documentSection, etc.). */
|
|
96
|
+
SDT_TYPE: "data-sdt-type",
|
|
97
|
+
/** Field annotation field identifier. */
|
|
98
|
+
FIELD_ID: "data-field-id",
|
|
99
|
+
/** Field annotation field type (signer, text, checkbox, etc.). */
|
|
100
|
+
FIELD_TYPE: "data-field-type",
|
|
101
|
+
/** Marks an element as draggable by the editor. */
|
|
102
|
+
DRAGGABLE: "data-draggable",
|
|
103
|
+
/** Display label text for drag toast / accessibility. */
|
|
104
|
+
DISPLAY_LABEL: "data-display-label",
|
|
105
|
+
/** Field annotation variant (text, image, signature, checkbox, html, link). */
|
|
106
|
+
VARIANT: "data-variant",
|
|
107
|
+
/** Element type discriminator (annotation variant, etc.). */
|
|
108
|
+
TYPE: "data-type",
|
|
109
|
+
/** Schema version for the editor-neutral layout boundary attributes. */
|
|
110
|
+
LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
|
|
111
|
+
/** Stable opaque id of the rendered fragment (see `LayoutFragmentId`). */
|
|
112
|
+
LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
|
|
113
|
+
/** Encoded story locator (e.g. `body`, `header:rId4`, `footer:rId7`). */
|
|
114
|
+
LAYOUT_STORY: "data-layout-story",
|
|
115
|
+
/** Source block reference (today: producer's `blockId`). */
|
|
116
|
+
LAYOUT_BLOCK_REF: "data-layout-block-ref"
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Decode the dataset string back into a `LayoutStoryLocator`-shaped object.
|
|
120
|
+
*
|
|
121
|
+
* Used by editor-side DOM observers. Unknown kinds fall back to
|
|
122
|
+
* `{ kind: 'unknown' }` so downstream code can treat the value as a
|
|
123
|
+
* diagnostic, not as a default body.
|
|
124
|
+
*/
|
|
125
|
+
var decodeLayoutStoryDataset = (raw) => {
|
|
126
|
+
if (!raw) return { kind: "unknown" };
|
|
127
|
+
if (raw === "body") return { kind: "body" };
|
|
128
|
+
const idx = raw.indexOf(":");
|
|
129
|
+
const kind = idx === -1 ? raw : raw.slice(0, idx);
|
|
130
|
+
const id = idx === -1 ? void 0 : raw.slice(idx + 1);
|
|
131
|
+
switch (kind) {
|
|
132
|
+
case "body":
|
|
133
|
+
case "header":
|
|
134
|
+
case "footer":
|
|
135
|
+
case "footnote":
|
|
136
|
+
case "endnote":
|
|
137
|
+
case "textbox": return id ? {
|
|
138
|
+
kind,
|
|
139
|
+
id
|
|
140
|
+
} : { kind };
|
|
141
|
+
default: return { kind: "unknown" };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
`${DOM_CLASS_NAMES.BLOCK_SDT}${DATA_ATTRS.SDT_ID}`;
|
|
145
|
+
`${DATA_ATTRS.DRAGGABLE}`;
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region ../layout-engine/contracts/src/direction-context.ts
|
|
148
|
+
/**
|
|
149
|
+
* Read a paragraph's inline base direction from its attributes.
|
|
150
|
+
*
|
|
151
|
+
* Prefers the resolved {@link ParagraphDirectionContext} (SD-2776) when
|
|
152
|
+
* present. Falls back to `paragraphProperties.rightToLeft` for PM-node /
|
|
153
|
+
* editor paths that store direction on the raw OOXML properties rather
|
|
154
|
+
* than the typed direction context.
|
|
155
|
+
*
|
|
156
|
+
* Consumers should call this instead of inspecting attrs ad hoc so the
|
|
157
|
+
* direction source check stays in one place.
|
|
158
|
+
*/
|
|
1
159
|
function getParagraphInlineDirection(attrs) {
|
|
2
160
|
const fromContext = attrs?.directionContext?.inlineDirection;
|
|
3
161
|
if (fromContext != null) return fromContext;
|
|
@@ -5,97 +163,65 @@ function getParagraphInlineDirection(attrs) {
|
|
|
5
163
|
if (ppRtl === true) return "rtl";
|
|
6
164
|
if (ppRtl === false) return "ltr";
|
|
7
165
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"#116329",
|
|
20
|
-
"#7d4e00"
|
|
21
|
-
];
|
|
22
|
-
const authorIdentityKey = (author) => {
|
|
23
|
-
if (!author) return "";
|
|
24
|
-
return `${typeof author.name === "string" ? author.name : ""} ${typeof author.email === "string" ? author.email : ""}`;
|
|
25
|
-
};
|
|
26
|
-
var hashString = (value) => {
|
|
27
|
-
let hash = 2166136261;
|
|
28
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
29
|
-
hash ^= value.charCodeAt(i);
|
|
30
|
-
hash = Math.imul(hash, 16777619);
|
|
31
|
-
}
|
|
32
|
-
return hash >>> 0;
|
|
33
|
-
};
|
|
34
|
-
const fallbackAuthorColor = (author) => {
|
|
35
|
-
return FALLBACK_PALETTE[hashString(authorIdentityKey(author)) % FALLBACK_PALETTE.length];
|
|
36
|
-
};
|
|
37
|
-
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
38
|
-
const composeAuthorColorResolver = (config) => {
|
|
39
|
-
if (!config || config.enabled === false) return void 0;
|
|
40
|
-
const overrides = config.overrides && typeof config.overrides === "object" ? config.overrides : void 0;
|
|
41
|
-
const resolve = typeof config.resolve === "function" ? config.resolve : void 0;
|
|
42
|
-
return (author) => {
|
|
43
|
-
const safeAuthor = author ?? {};
|
|
44
|
-
if (overrides) {
|
|
45
|
-
if (isNonEmptyString(safeAuthor.email) && isNonEmptyString(overrides[safeAuthor.email])) return overrides[safeAuthor.email];
|
|
46
|
-
if (isNonEmptyString(safeAuthor.name) && isNonEmptyString(overrides[safeAuthor.name])) return overrides[safeAuthor.name];
|
|
47
|
-
}
|
|
48
|
-
if (resolve) try {
|
|
49
|
-
const resolved = resolve(safeAuthor);
|
|
50
|
-
if (isNonEmptyString(resolved)) return resolved;
|
|
51
|
-
} catch {}
|
|
52
|
-
return fallbackAuthorColor(safeAuthor);
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
const TRACKED_CHANGE_CONFIGURABLE_SEMANTIC_COLOR_KEYS = [
|
|
56
|
-
"insertion",
|
|
57
|
-
"deletion",
|
|
58
|
-
"move",
|
|
59
|
-
"move-from",
|
|
60
|
-
"move-to",
|
|
61
|
-
"table-cell-insertion",
|
|
62
|
-
"table-cell-deletion",
|
|
63
|
-
"cell-merge",
|
|
64
|
-
"cell-split",
|
|
65
|
-
"image-insertion",
|
|
66
|
-
"image-deletion",
|
|
67
|
-
"image-property-change"
|
|
68
|
-
];
|
|
69
|
-
var CONFIGURABLE_SEMANTIC_COLOR_KEY_SET = new Set(TRACKED_CHANGE_CONFIGURABLE_SEMANTIC_COLOR_KEYS);
|
|
70
|
-
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. */
|
|
71
177
|
externalImageDeferred: "render.media.external-image-deferred",
|
|
178
|
+
/** Drawing references a relationship id that does not exist on the owner part. */
|
|
72
179
|
missingRelationship: "render.drawing.missing-relationship",
|
|
180
|
+
/** Relationship resolves but the target media part bytes are missing/empty. */
|
|
73
181
|
missingMediaPart: "render.media.missing-part",
|
|
182
|
+
/** Relationship exists but is not an image relationship type. */
|
|
74
183
|
unsupportedRelationshipType: "render.drawing.unsupported-relationship-type",
|
|
184
|
+
/** Media MIME is not in the supported allowlist. */
|
|
75
185
|
unsupportedMime: "render.media.unsupported-mime",
|
|
186
|
+
/** Media part exceeds the host byte-size policy before bytes are materialized. */
|
|
76
187
|
imageTooLarge: "render.media.image-too-large",
|
|
188
|
+
/** SVG content rejected by the SVG safety policy. */
|
|
77
189
|
unsafeSvg: "render.media.unsafe-svg",
|
|
190
|
+
/** Metafile / TIFF (EMF, WMF, TIFF) with no approved conversion path. */
|
|
78
191
|
unsupportedFormat: "render.media.unsupported-format",
|
|
192
|
+
/** OLE / ActiveX / embedded package object. */
|
|
79
193
|
embeddedObjectNotSupported: "render.embedded-object-not-supported",
|
|
194
|
+
/** SmartArt / diagram / unknown external graphic-data object. */
|
|
80
195
|
unsupportedObject: "render.drawing.unsupported-object",
|
|
196
|
+
/** VML structure outside any supported image-like subset. */
|
|
81
197
|
vmlUnsupported: "render.drawing.vml-unsupported",
|
|
198
|
+
/** VML image-like content that could not be promoted to a supported image. */
|
|
82
199
|
vmlImageUnsupported: "render.drawing.vml-image-unsupported",
|
|
200
|
+
/** Custom geometry command the extractor does not implement (e.g. `arcTo`). */
|
|
83
201
|
unsupportedGeometryCommand: "render.drawing.unsupported-geometry-command",
|
|
202
|
+
/** Anchor fields required for honest placement are unsupported. */
|
|
84
203
|
anchorUnsupported: "render.drawing.anchor-unsupported",
|
|
204
|
+
/** Wrap fields required for honest placement are unsupported. */
|
|
85
205
|
wrapUnsupported: "render.drawing.wrap-unsupported",
|
|
206
|
+
/** `mc:AlternateContent` had no supported choice and no usable fallback. */
|
|
86
207
|
altContentNoSupportedChoice: "render.drawing.altcontent-no-supported-choice",
|
|
208
|
+
/** A child of an otherwise-supported group is unsupported. */
|
|
87
209
|
groupChildUnsupported: "render.drawing.group-child-unsupported"
|
|
88
210
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
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 = {
|
|
99
225
|
inlineBitmap: {
|
|
100
226
|
family: "inlineBitmap",
|
|
101
227
|
support: "supported",
|
|
@@ -291,8 +417,8 @@ const DRAWING_SUPPORT_TAXONOMY = {
|
|
|
291
417
|
contract: "none"
|
|
292
418
|
}
|
|
293
419
|
};
|
|
294
|
-
|
|
295
|
-
|
|
420
|
+
Object.keys(DRAWING_SUPPORT_TAXONOMY);
|
|
421
|
+
Object.freeze([
|
|
296
422
|
"multiple-sections",
|
|
297
423
|
"furniture-page-tokens",
|
|
298
424
|
"non-balanceable-multi-column-sections",
|
|
@@ -303,18 +429,31 @@ const PAGE_CHECKPOINT_DEPENDENCY_CLASSES = Object.freeze([
|
|
|
303
429
|
"tables",
|
|
304
430
|
"furniture-anchored-objects"
|
|
305
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
|
+
*/
|
|
306
444
|
function getV2TrackedChangeMutationImpact(event) {
|
|
307
445
|
if (event?.type !== "mutation:committed") return null;
|
|
308
446
|
const payload = event.origin === "history" ? event.result : event.receipt;
|
|
309
447
|
if (!payload || typeof payload !== "object") return null;
|
|
310
448
|
const upsertIds = /* @__PURE__ */ new Set();
|
|
311
449
|
const removedIds = /* @__PURE__ */ new Set();
|
|
450
|
+
/** @type {Array<{ from: string, to: string }>} */
|
|
312
451
|
const remappedPairs = [];
|
|
313
452
|
const collectEntity = (entry, into) => {
|
|
314
453
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return;
|
|
315
454
|
if (typeof entry.entityId === "string" && entry.entityId.length > 0) into.add(entry.entityId);
|
|
316
455
|
};
|
|
317
|
-
const readEntityId
|
|
456
|
+
const readEntityId = (entry) => {
|
|
318
457
|
if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return null;
|
|
319
458
|
return typeof entry.entityId === "string" && entry.entityId.length > 0 ? entry.entityId : null;
|
|
320
459
|
};
|
|
@@ -324,8 +463,8 @@ function getV2TrackedChangeMutationImpact(event) {
|
|
|
324
463
|
if (Array.isArray(payload.removed)) payload.removed.forEach((entry) => collectEntity(entry, removedIds));
|
|
325
464
|
if (Array.isArray(payload.invalidatedRefs)) payload.invalidatedRefs.forEach((entry) => collectEntity(entry, removedIds));
|
|
326
465
|
if (Array.isArray(payload.remappedRefs)) for (const mapping of payload.remappedRefs) {
|
|
327
|
-
const fromId = readEntityId
|
|
328
|
-
const toId = readEntityId
|
|
466
|
+
const fromId = readEntityId(mapping?.from);
|
|
467
|
+
const toId = readEntityId(mapping?.to);
|
|
329
468
|
if (fromId) removedIds.add(fromId);
|
|
330
469
|
if (toId) upsertIds.add(toId);
|
|
331
470
|
if (fromId && toId && fromId !== toId) remappedPairs.push({
|
|
@@ -334,14 +473,23 @@ function getV2TrackedChangeMutationImpact(event) {
|
|
|
334
473
|
});
|
|
335
474
|
}
|
|
336
475
|
for (const id of removedIds) upsertIds.delete(id);
|
|
337
|
-
|
|
476
|
+
const allResolved = readAllResolvedFact(event, payload);
|
|
477
|
+
if (upsertIds.size === 0 && removedIds.size === 0 && !allResolved) return null;
|
|
338
478
|
return {
|
|
339
479
|
upsertIds,
|
|
340
480
|
removedIds,
|
|
341
481
|
remappedPairs,
|
|
342
|
-
reconcileMode: event.origin === "history" ? "authoritative" : "targeted"
|
|
482
|
+
reconcileMode: event.origin === "history" ? "authoritative" : "targeted",
|
|
483
|
+
...allResolved ? { allResolved } : {}
|
|
343
484
|
};
|
|
344
485
|
}
|
|
486
|
+
function readAllResolvedFact(event, receipt) {
|
|
487
|
+
const fact = event?.trackedChangeAllResolved;
|
|
488
|
+
if (event?.origin === "history" || !fact || fact.schemaVersion !== 1 || fact.targetKind !== "all" || fact.decision !== "accept" && fact.decision !== "reject" || fact.remainingLogicalCount !== 0 || typeof fact.catalogRevision !== "string" || !fact.catalogRevision || typeof fact.sourceCoverageRevision !== "string" || !fact.sourceCoverageRevision || !Number.isSafeInteger(fact.logicalTargetCount) || fact.logicalTargetCount <= 0 || !Number.isSafeInteger(fact.physicalCarrierCount) || fact.physicalCarrierCount < fact.logicalTargetCount || typeof fact.txId !== "string" || fact.txId !== receipt.txId || typeof fact.documentEpoch !== "string" || !Number.isSafeInteger(fact.commitSequence) || typeof fact.packagePreviousRevision !== "string" || typeof fact.packageNextRevision !== "string" || fact.packagePreviousRevision === fact.packageNextRevision) return null;
|
|
489
|
+
return fact;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/helpers/v2-review-mutation-reconciler.js
|
|
345
493
|
var normalizeIds = (values) => new Set(Array.from(values ?? []).filter((value) => typeof value === "string" && value.length > 0));
|
|
346
494
|
var itemId = (item) => {
|
|
347
495
|
if (typeof item?.id === "string" && item.id.length > 0) return item.id;
|
|
@@ -350,12 +498,29 @@ var itemId = (item) => {
|
|
|
350
498
|
return null;
|
|
351
499
|
};
|
|
352
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
|
+
*/
|
|
353
508
|
function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onReconciled }) {
|
|
354
509
|
const pendingUpserts = /* @__PURE__ */ new Map();
|
|
510
|
+
let pendingAllResolved = null;
|
|
355
511
|
let sequence = 0;
|
|
356
512
|
let generation = 0;
|
|
357
513
|
let flushInFlight = null;
|
|
358
514
|
let flushRequested = false;
|
|
515
|
+
const settlementWaiters = [];
|
|
516
|
+
const resolveSettlementsThrough = (version, value) => {
|
|
517
|
+
for (let index = settlementWaiters.length - 1; index >= 0; index -= 1) {
|
|
518
|
+
const waiter = settlementWaiters[index];
|
|
519
|
+
if (waiter.generation !== generation || waiter.version > version) continue;
|
|
520
|
+
settlementWaiters.splice(index, 1);
|
|
521
|
+
waiter.resolve(value === true && waiter.requiresAllResolved ? pendingAllResolved == null : value);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
359
524
|
const isCurrent = (capturedGeneration, context) => generation === capturedGeneration && contextMatches(context, getContext());
|
|
360
525
|
const clearResolved = (ids, snapshot) => {
|
|
361
526
|
const clearedIds = /* @__PURE__ */ new Set();
|
|
@@ -369,6 +534,10 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
369
534
|
const readySnapshot = (reconcileMode) => new Map([...pendingUpserts].filter(([, entry]) => entry.ready === true && entry.reconcileMode === reconcileMode));
|
|
370
535
|
const markRenderGatedReady = () => {
|
|
371
536
|
let changed = false;
|
|
537
|
+
if (pendingAllResolved?.gate === "render" && !pendingAllResolved.ready) {
|
|
538
|
+
pendingAllResolved.ready = true;
|
|
539
|
+
changed = true;
|
|
540
|
+
}
|
|
372
541
|
for (const entry of pendingUpserts.values()) {
|
|
373
542
|
if (entry.gate !== "render" || entry.ready) continue;
|
|
374
543
|
entry.ready = true;
|
|
@@ -378,6 +547,10 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
378
547
|
};
|
|
379
548
|
const markEntriesReadyThrough = (maxVersion) => {
|
|
380
549
|
let changed = false;
|
|
550
|
+
if (pendingAllResolved && pendingAllResolved.version <= maxVersion && !pendingAllResolved.ready) {
|
|
551
|
+
pendingAllResolved.ready = true;
|
|
552
|
+
changed = true;
|
|
553
|
+
}
|
|
381
554
|
for (const entry of pendingUpserts.values()) {
|
|
382
555
|
if (entry.version > maxVersion || entry.ready) continue;
|
|
383
556
|
entry.ready = true;
|
|
@@ -399,7 +572,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
399
572
|
reconcileMode: "targeted"
|
|
400
573
|
});
|
|
401
574
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
402
|
-
|
|
575
|
+
const resolvedIds = idsFromResult(result);
|
|
576
|
+
const clearedIds = clearResolved(resolvedIds, snapshot);
|
|
577
|
+
await notifyReconciled(context, clearedIds, result);
|
|
403
578
|
if (new Set([...snapshot].filter(([id, version]) => pendingUpserts.get(id) === version).map(([id]) => id)).size === 0) return;
|
|
404
579
|
const fallback = await hydrate(context, {
|
|
405
580
|
trackedChangesListMode: "interaction-prime",
|
|
@@ -407,7 +582,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
407
582
|
blocking: false
|
|
408
583
|
});
|
|
409
584
|
if (!isCurrent(capturedGeneration, context)) return;
|
|
410
|
-
|
|
585
|
+
const fallbackResolvedIds = idsFromResult(fallback);
|
|
586
|
+
const fallbackClearedIds = clearResolved(fallbackResolvedIds, snapshot);
|
|
587
|
+
await notifyReconciled(context, fallbackClearedIds, fallback);
|
|
411
588
|
};
|
|
412
589
|
const reconcileAuthoritativeSnapshot = async (context, snapshot, capturedGeneration) => {
|
|
413
590
|
const result = await hydrate(context, {
|
|
@@ -428,14 +605,31 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
428
605
|
};
|
|
429
606
|
const flush = () => {
|
|
430
607
|
if (flushInFlight) return flushInFlight;
|
|
608
|
+
const allResolvedSnapshot = pendingAllResolved?.ready ? pendingAllResolved : null;
|
|
431
609
|
const targetedSnapshot = readySnapshot("targeted");
|
|
432
610
|
const authoritativeSnapshot = readySnapshot("authoritative");
|
|
433
|
-
if (targetedSnapshot.size === 0 && authoritativeSnapshot.size === 0) return null;
|
|
611
|
+
if (!allResolvedSnapshot && targetedSnapshot.size === 0 && authoritativeSnapshot.size === 0) return null;
|
|
434
612
|
const context = getContext();
|
|
435
613
|
if (!context?.adapter) return null;
|
|
436
614
|
flushRequested = false;
|
|
437
615
|
const capturedGeneration = generation;
|
|
438
616
|
flushInFlight = Promise.resolve().then(async () => {
|
|
617
|
+
if (allResolvedSnapshot) {
|
|
618
|
+
let result = null;
|
|
619
|
+
try {
|
|
620
|
+
result = await reconcile(context, {
|
|
621
|
+
upsertIds: /* @__PURE__ */ new Set(),
|
|
622
|
+
removedIds: /* @__PURE__ */ new Set(),
|
|
623
|
+
allResolved: allResolvedSnapshot.fact,
|
|
624
|
+
reconcileMode: "targeted"
|
|
625
|
+
});
|
|
626
|
+
} catch {}
|
|
627
|
+
if (!isCurrent(capturedGeneration, context)) return;
|
|
628
|
+
if (result?.ok === true) {
|
|
629
|
+
if (pendingAllResolved === allResolvedSnapshot) pendingAllResolved = null;
|
|
630
|
+
await notifyReconciled(context, /* @__PURE__ */ new Set(), result, true);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
439
633
|
if (authoritativeSnapshot.size > 0) await reconcileAuthoritativeSnapshot(context, authoritativeSnapshot, capturedGeneration).catch(() => void 0);
|
|
440
634
|
if (targetedSnapshot.size > 0) await reconcileTargetedSnapshot(context, targetedSnapshot, capturedGeneration).catch(() => void 0);
|
|
441
635
|
}).catch(() => {}).finally(() => {
|
|
@@ -446,6 +640,15 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
446
640
|
};
|
|
447
641
|
const enqueueWithGate = (impact, gate) => {
|
|
448
642
|
if (!impact) return;
|
|
643
|
+
if (impact.allResolved) {
|
|
644
|
+
pendingUpserts.clear();
|
|
645
|
+
pendingAllResolved = {
|
|
646
|
+
fact: impact.allResolved,
|
|
647
|
+
version: ++sequence,
|
|
648
|
+
gate,
|
|
649
|
+
ready: false
|
|
650
|
+
};
|
|
651
|
+
}
|
|
449
652
|
const reconcileMode = impact.reconcileMode === "authoritative" ? "authoritative" : "targeted";
|
|
450
653
|
const removedIds = normalizeIds(impact.removedIds);
|
|
451
654
|
const upsertIds = normalizeIds(impact.upsertIds);
|
|
@@ -460,7 +663,7 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
460
663
|
else pendingUpserts.delete(id);
|
|
461
664
|
}
|
|
462
665
|
const context = getContext();
|
|
463
|
-
if (removedIds.size > 0 && upsertIds.size === 0 && context?.adapter) try {
|
|
666
|
+
if (removedIds.size > 0 && upsertIds.size === 0 && context?.adapter && !impact.allResolved) try {
|
|
464
667
|
Promise.resolve(reconcile(context, {
|
|
465
668
|
upsertIds: /* @__PURE__ */ new Set(),
|
|
466
669
|
removedIds
|
|
@@ -487,20 +690,34 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
487
690
|
enqueueAfterPaint(impact, waitForPaint) {
|
|
488
691
|
if (!impact || typeof waitForPaint !== "function") {
|
|
489
692
|
enqueue(impact);
|
|
490
|
-
return;
|
|
693
|
+
return Promise.resolve(false);
|
|
491
694
|
}
|
|
492
695
|
enqueueWithGate(impact, "exact-paint");
|
|
493
696
|
const context = getContext();
|
|
494
|
-
if (!context?.adapter) return;
|
|
495
697
|
const capturedGeneration = generation;
|
|
496
698
|
const capturedSequence = sequence;
|
|
497
|
-
|
|
699
|
+
if (!context?.adapter) {
|
|
700
|
+
markEntriesReadyThrough(capturedSequence);
|
|
701
|
+
return Promise.resolve(false);
|
|
702
|
+
}
|
|
703
|
+
const completion = new Promise((resolve) => {
|
|
704
|
+
settlementWaiters.push({
|
|
705
|
+
generation: capturedGeneration,
|
|
706
|
+
version: capturedSequence,
|
|
707
|
+
requiresAllResolved: Boolean(impact.allResolved),
|
|
708
|
+
resolve
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
const impactedIds = /* @__PURE__ */ new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
|
|
498
712
|
const capturedEntries = new Map([...impactedIds].flatMap((id) => {
|
|
499
713
|
const entry = pendingUpserts.get(id);
|
|
500
714
|
return entry ? [[id, entry]] : [];
|
|
501
715
|
}));
|
|
502
|
-
Promise.resolve().then(() => waitForPaint()).then(() => {
|
|
503
|
-
if (!isCurrent(capturedGeneration, context))
|
|
716
|
+
Promise.resolve().then(() => waitForPaint()).then(async () => {
|
|
717
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
718
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
504
721
|
let madeReady = markRenderGatedReady();
|
|
505
722
|
if (markEntriesReadyThrough(capturedSequence)) madeReady = true;
|
|
506
723
|
for (const [id, entry] of capturedEntries) {
|
|
@@ -509,16 +726,35 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
509
726
|
entry.ready = true;
|
|
510
727
|
}
|
|
511
728
|
if (flushInFlight && (madeReady || capturedSequence > 0)) flushRequested = true;
|
|
512
|
-
|
|
729
|
+
const preceding = flushInFlight;
|
|
730
|
+
const pending = flush();
|
|
731
|
+
if (pending) await pending;
|
|
732
|
+
if (preceding) {
|
|
733
|
+
const followUp = flushInFlight;
|
|
734
|
+
if (followUp && followUp !== preceding) await followUp;
|
|
735
|
+
}
|
|
736
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
737
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
resolveSettlementsThrough(capturedSequence, true);
|
|
513
741
|
}).catch(() => {
|
|
514
|
-
if (!isCurrent(capturedGeneration, context))
|
|
742
|
+
if (!isCurrent(capturedGeneration, context)) {
|
|
743
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
515
746
|
for (const [id, entry] of capturedEntries) if (pendingUpserts.get(id) === entry) entry.gate = "render";
|
|
747
|
+
if (pendingAllResolved?.gate === "exact-paint") pendingAllResolved.gate = "render";
|
|
748
|
+
resolveSettlementsThrough(capturedSequence, false);
|
|
516
749
|
});
|
|
750
|
+
return completion;
|
|
517
751
|
},
|
|
518
752
|
onRender,
|
|
519
753
|
reset() {
|
|
754
|
+
for (const waiter of settlementWaiters.splice(0)) waiter.resolve(false);
|
|
520
755
|
generation += 1;
|
|
521
756
|
pendingUpserts.clear();
|
|
757
|
+
pendingAllResolved = null;
|
|
522
758
|
flushRequested = false;
|
|
523
759
|
},
|
|
524
760
|
getPendingIds() {
|
|
@@ -526,7 +762,19 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
|
|
|
526
762
|
}
|
|
527
763
|
};
|
|
528
764
|
}
|
|
529
|
-
|
|
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([
|
|
530
778
|
"insert-text",
|
|
531
779
|
"replace-text",
|
|
532
780
|
"plain-text-paste",
|
|
@@ -548,33 +796,158 @@ var V2_TYPING_COMMAND_KINDS = new Set([
|
|
|
548
796
|
"backspace:list-remove",
|
|
549
797
|
"backspace:list-outdent"
|
|
550
798
|
]);
|
|
551
|
-
|
|
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) => {
|
|
552
812
|
if (event?.type !== "mutation:committed" || event.origin !== "command") return false;
|
|
553
813
|
return V2_TYPING_COMMAND_KINDS.has(event.editableCommandKind);
|
|
554
814
|
};
|
|
555
|
-
|
|
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. */
|
|
556
847
|
notReady: "not-ready",
|
|
848
|
+
/** The editor is ready but its Document API facade is not available or has not been published yet. */
|
|
557
849
|
documentApiUnavailable: "document-api-unavailable",
|
|
850
|
+
/** The document is in viewing / read-only mode, so mutating commands are blocked. */
|
|
558
851
|
documentReadonly: "document-readonly",
|
|
852
|
+
/** A selection is required and none is active. */
|
|
559
853
|
selectionRequired: "selection-required",
|
|
854
|
+
/** A non-empty (range) selection is required and the current selection is collapsed/empty. */
|
|
560
855
|
rangeSelectionRequired: "range-selection-required",
|
|
856
|
+
/** Required pointer / entity / document context could not be resolved. */
|
|
561
857
|
contextUnavailable: "context-unavailable",
|
|
858
|
+
/** Painted geometry for the target could not be resolved. */
|
|
562
859
|
geometryUnavailable: "geometry-unavailable",
|
|
860
|
+
/** A navigation / geometry target could not be resolved to a live document address. */
|
|
563
861
|
targetUnresolved: "target-unresolved",
|
|
862
|
+
/** A navigation target was resolved but could not be brought into the visible viewport. */
|
|
564
863
|
targetNotVisible: "target-not-visible",
|
|
864
|
+
/** The command is not supported by v2 at all. */
|
|
565
865
|
commandUnsupported: "command-unsupported",
|
|
866
|
+
/** Reserved migration reason for a recognized command that is intentionally deferred. */
|
|
566
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
|
+
*/
|
|
567
876
|
tableContextUnavailable: "table-context-unavailable",
|
|
877
|
+
/** The backing Document API / SuperDoc operation is not present on this host. */
|
|
568
878
|
operationUnavailable: "operation-unavailable",
|
|
879
|
+
/** Bulk accept/reject decisions are disabled unless the host opts in. */
|
|
569
880
|
bulkDecisionsDisabled: "bulk-decisions-disabled",
|
|
881
|
+
/** A host-owned capability (geometry, hit-test, navigation) is unavailable. */
|
|
570
882
|
hostCapabilityUnavailable: "host-capability-unavailable",
|
|
883
|
+
/** Undo / redo cannot run because the host history stack has no matching entry. */
|
|
571
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
|
+
*/
|
|
572
891
|
searchUnavailable: "search-unavailable",
|
|
892
|
+
/** The search pattern is an invalid or unsafe regular expression. */
|
|
573
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
|
+
*/
|
|
574
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
|
+
*/
|
|
575
908
|
contentControlLocked: "content-control-locked"
|
|
576
909
|
};
|
|
577
|
-
|
|
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 = {
|
|
578
951
|
bold: "bold",
|
|
579
952
|
italic: "italic",
|
|
580
953
|
underline: "underline",
|
|
@@ -592,6 +965,7 @@ const BUILT_IN_COMMAND_IDS = {
|
|
|
592
965
|
bulletList: "bullet-list",
|
|
593
966
|
numberedList: "numbered-list"
|
|
594
967
|
};
|
|
968
|
+
/** Strip a trailing `pt` unit from v1-style font sizes (`"12pt"` → `"12"`). */
|
|
595
969
|
function normalizeFontSizePayload(payload) {
|
|
596
970
|
const strip = (value) => typeof value === "string" ? value.replace(/\s*pt$/i, "").trim() : value;
|
|
597
971
|
if (typeof payload === "string") return strip(payload);
|
|
@@ -604,6 +978,12 @@ function normalizeFontSizePayload(payload) {
|
|
|
604
978
|
}
|
|
605
979
|
return payload;
|
|
606
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
|
+
*/
|
|
607
987
|
function normalizeZoomPayload(payload) {
|
|
608
988
|
const normalizeNumber = (value, percentLiteral) => {
|
|
609
989
|
if (!percentLiteral && value > 0 && value <= 5) return value * 100;
|
|
@@ -618,6 +998,12 @@ function normalizeZoomPayload(payload) {
|
|
|
618
998
|
if (typeof payload === "number" && Number.isFinite(payload)) return normalizeNumber(payload, false);
|
|
619
999
|
return payload;
|
|
620
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
|
+
*/
|
|
621
1007
|
function normalizeMeasurementUnitPayload(payload) {
|
|
622
1008
|
const raw = payload && typeof payload === "object" && "value" in payload ? payload.value : payload;
|
|
623
1009
|
if (raw === "in" || raw === "cm") return raw;
|
|
@@ -628,6 +1014,12 @@ function normalizeMeasurementUnitPayload(payload) {
|
|
|
628
1014
|
}
|
|
629
1015
|
return raw;
|
|
630
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
|
+
*/
|
|
631
1023
|
function normalizeColorPayload(payload) {
|
|
632
1024
|
const coerce = (value) => {
|
|
633
1025
|
if (value === null) return null;
|
|
@@ -647,6 +1039,7 @@ function normalizeColorPayload(payload) {
|
|
|
647
1039
|
}
|
|
648
1040
|
return coerce(payload);
|
|
649
1041
|
}
|
|
1042
|
+
/** Unwrap a `{ value }` / `{ alignment } ` / `{ styleId }` wrapper to its scalar. */
|
|
650
1043
|
function unwrapScalar(payload, keys) {
|
|
651
1044
|
if (payload && typeof payload === "object") {
|
|
652
1045
|
const record = payload;
|
|
@@ -654,6 +1047,7 @@ function unwrapScalar(payload, keys) {
|
|
|
654
1047
|
}
|
|
655
1048
|
return payload;
|
|
656
1049
|
}
|
|
1050
|
+
/** Normalize a v1 alignment payload (`"left"`, `{ value }`, `"justified"`) to a `ParagraphAlignment`. */
|
|
657
1051
|
function normalizeAlignmentPayload(payload) {
|
|
658
1052
|
const raw = unwrapScalar(payload, ["alignment", "value"]);
|
|
659
1053
|
if (typeof raw !== "string") return raw;
|
|
@@ -661,6 +1055,11 @@ function normalizeAlignmentPayload(payload) {
|
|
|
661
1055
|
if (v === "justified" || v === "both") return "justify";
|
|
662
1056
|
return v;
|
|
663
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
|
+
*/
|
|
664
1063
|
function normalizeLineHeightPayload(payload) {
|
|
665
1064
|
const raw = unwrapScalar(payload, [
|
|
666
1065
|
"line",
|
|
@@ -671,6 +1070,7 @@ function normalizeLineHeightPayload(payload) {
|
|
|
671
1070
|
if (!Number.isFinite(num) || num <= 0) return raw;
|
|
672
1071
|
return num <= 10 ? Math.round(num * 240) : Math.round(num);
|
|
673
1072
|
}
|
|
1073
|
+
/** Preserve semantic style intent; normalize legacy linked-style payloads to a concrete ID. */
|
|
674
1074
|
function normalizeStyleIdPayload(payload) {
|
|
675
1075
|
if (payload && typeof payload === "object" && "role" in payload) return payload;
|
|
676
1076
|
const raw = unwrapScalar(payload, [
|
|
@@ -680,6 +1080,7 @@ function normalizeStyleIdPayload(payload) {
|
|
|
680
1080
|
]);
|
|
681
1081
|
return typeof raw === "string" ? raw.trim() : raw;
|
|
682
1082
|
}
|
|
1083
|
+
/** Resolve a document-mode payload (`"viewing"` or `{ mode }`) to the mode string. */
|
|
683
1084
|
function normalizeDocumentModePayload(payload) {
|
|
684
1085
|
if (typeof payload === "string") return payload;
|
|
685
1086
|
if (payload && typeof payload === "object") {
|
|
@@ -690,7 +1091,14 @@ function normalizeDocumentModePayload(payload) {
|
|
|
690
1091
|
}
|
|
691
1092
|
var UNSUPPORTED = SUPERDOC_UI_REASONS.commandUnsupported;
|
|
692
1093
|
var TABLE_CONTEXT = SUPERDOC_UI_REASONS.tableContextUnavailable;
|
|
693
|
-
|
|
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 = [
|
|
694
1102
|
{
|
|
695
1103
|
id: "bold",
|
|
696
1104
|
family: "inline marks",
|
|
@@ -1210,10 +1618,23 @@ const COMMAND_CATALOG = [
|
|
|
1210
1618
|
}
|
|
1211
1619
|
];
|
|
1212
1620
|
var CATALOG_BY_ID = new Map(COMMAND_CATALOG.map((descriptor) => [descriptor.id, descriptor]));
|
|
1213
|
-
|
|
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. */
|
|
1214
1624
|
function getCommandDescriptor(id) {
|
|
1215
1625
|
return CATALOG_BY_ID.get(id) ?? null;
|
|
1216
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
|
+
*/
|
|
1217
1638
|
function shallowEqual(a, b) {
|
|
1218
1639
|
if (Object.is(a, b)) return true;
|
|
1219
1640
|
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
@@ -1227,6 +1648,16 @@ function shallowEqual(a, b) {
|
|
|
1227
1648
|
}
|
|
1228
1649
|
return true;
|
|
1229
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
|
+
*/
|
|
1230
1661
|
function sdRunPropsToInlineRunPatch(props) {
|
|
1231
1662
|
if (!props || typeof props !== "object") return {};
|
|
1232
1663
|
const patch = {};
|
|
@@ -1317,6 +1748,16 @@ function projectBorderToInlinePatch(border) {
|
|
|
1317
1748
|
if (color != null) patch.color = color;
|
|
1318
1749
|
return Object.keys(patch).length > 0 ? patch : void 0;
|
|
1319
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
|
+
*/
|
|
1320
1761
|
function selectionKey(selection) {
|
|
1321
1762
|
const target = selection?.selectionTarget ?? selection?.target;
|
|
1322
1763
|
if (!selection || !target) return "";
|
|
@@ -1326,9 +1767,21 @@ function selectionKey(selection) {
|
|
|
1326
1767
|
return String(target);
|
|
1327
1768
|
}
|
|
1328
1769
|
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/public/ui/entity-at.ts
|
|
1329
1772
|
function parseCommaSeparatedIds(value) {
|
|
1330
1773
|
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
1331
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
|
+
*/
|
|
1332
1785
|
function getTrackChangeIds(node) {
|
|
1333
1786
|
const out = [];
|
|
1334
1787
|
const add = (id) => {
|
|
@@ -1340,6 +1793,21 @@ function getTrackChangeIds(node) {
|
|
|
1340
1793
|
add(node.getAttribute("data-track-change-id"));
|
|
1341
1794
|
return out;
|
|
1342
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
|
+
*/
|
|
1343
1811
|
function collectEntityHitsFromChain(start, stopAt) {
|
|
1344
1812
|
if (!start || typeof start.getAttribute !== "function") return [];
|
|
1345
1813
|
const hits = [];
|
|
@@ -1401,67 +1869,8 @@ function collectEntityHitsFromChain(start, stopAt) {
|
|
|
1401
1869
|
}
|
|
1402
1870
|
return hits;
|
|
1403
1871
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
FRAGMENT: "superdoc-fragment",
|
|
1407
|
-
LINE: "superdoc-line",
|
|
1408
|
-
INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
|
|
1409
|
-
INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
|
|
1410
|
-
BLOCK_SDT: "superdoc-structured-content-block",
|
|
1411
|
-
BLOCK_SDT_LABEL: "superdoc-structured-content__label",
|
|
1412
|
-
TABLE_FRAGMENT: "superdoc-table-fragment",
|
|
1413
|
-
DOCUMENT_SECTION: "superdoc-document-section",
|
|
1414
|
-
SDT_GROUP_HOVER: "sdt-group-hover",
|
|
1415
|
-
TOC_ENTRY: "superdoc-toc-entry",
|
|
1416
|
-
TOC_GROUP_HOVER: "toc-group-hover",
|
|
1417
|
-
IMAGE_FRAGMENT: "superdoc-image-fragment",
|
|
1418
|
-
INLINE_IMAGE: "superdoc-inline-image",
|
|
1419
|
-
LIST_MARKER: "superdoc-list-marker",
|
|
1420
|
-
INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
|
|
1421
|
-
ANNOTATION: "annotation",
|
|
1422
|
-
ANNOTATION_CONTENT: "annotation-content",
|
|
1423
|
-
ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
|
|
1424
|
-
};
|
|
1425
|
-
const STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES = [DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL];
|
|
1426
|
-
const DATA_ATTRS = {
|
|
1427
|
-
PM_START: "data-pm-start",
|
|
1428
|
-
PM_END: "data-pm-end",
|
|
1429
|
-
LAYOUT_EPOCH: "data-layout-epoch",
|
|
1430
|
-
TABLE_BOUNDARIES: "data-table-boundaries",
|
|
1431
|
-
SDT_ID: "data-sdt-id",
|
|
1432
|
-
SDT_TYPE: "data-sdt-type",
|
|
1433
|
-
FIELD_ID: "data-field-id",
|
|
1434
|
-
FIELD_TYPE: "data-field-type",
|
|
1435
|
-
DRAGGABLE: "data-draggable",
|
|
1436
|
-
DISPLAY_LABEL: "data-display-label",
|
|
1437
|
-
VARIANT: "data-variant",
|
|
1438
|
-
TYPE: "data-type",
|
|
1439
|
-
LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
|
|
1440
|
-
LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
|
|
1441
|
-
LAYOUT_STORY: "data-layout-story",
|
|
1442
|
-
LAYOUT_BLOCK_REF: "data-layout-block-ref"
|
|
1443
|
-
};
|
|
1444
|
-
const decodeLayoutStoryDataset = (raw) => {
|
|
1445
|
-
if (!raw) return { kind: "unknown" };
|
|
1446
|
-
if (raw === "body") return { kind: "body" };
|
|
1447
|
-
const idx = raw.indexOf(":");
|
|
1448
|
-
const kind = idx === -1 ? raw : raw.slice(0, idx);
|
|
1449
|
-
const id = idx === -1 ? void 0 : raw.slice(idx + 1);
|
|
1450
|
-
switch (kind) {
|
|
1451
|
-
case "body":
|
|
1452
|
-
case "header":
|
|
1453
|
-
case "footer":
|
|
1454
|
-
case "footnote":
|
|
1455
|
-
case "endnote":
|
|
1456
|
-
case "textbox": return id ? {
|
|
1457
|
-
kind,
|
|
1458
|
-
id
|
|
1459
|
-
} : { kind };
|
|
1460
|
-
default: return { kind: "unknown" };
|
|
1461
|
-
}
|
|
1462
|
-
};
|
|
1463
|
-
const SDT_BLOCK_WITH_ID_SELECTOR = `.${DOM_CLASS_NAMES.BLOCK_SDT}[${DATA_ATTRS.SDT_ID}]`;
|
|
1464
|
-
const DRAGGABLE_SELECTOR = `[${DATA_ATTRS.DRAGGABLE}="true"]`;
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region ../document-api/src/format/inline-run-patch.ts
|
|
1465
1874
|
var schemaBooleanOrNull = () => ({ oneOf: [{ type: "boolean" }, { type: "null" }] });
|
|
1466
1875
|
var schemaStringOrNull = () => ({ oneOf: [{
|
|
1467
1876
|
type: "string",
|
|
@@ -1553,7 +1962,7 @@ var runAttribute = (key, type, ooxmlElement, schema, runPropertyKey) => ({
|
|
|
1553
1962
|
carrier: runAttributeCarrier(runPropertyKey ?? key),
|
|
1554
1963
|
schema
|
|
1555
1964
|
});
|
|
1556
|
-
|
|
1965
|
+
var INLINE_PROPERTY_REGISTRY = [
|
|
1557
1966
|
markBoolean("bold", "w:b", "bold"),
|
|
1558
1967
|
markBoolean("italic", "w:i", "italic"),
|
|
1559
1968
|
markBoolean("strike", "w:strike", "strike"),
|
|
@@ -1710,16 +2119,58 @@ const INLINE_PROPERTY_REGISTRY = [
|
|
|
1710
2119
|
runAttribute("stylisticSets", "array", "w14:stylisticSets", schemaStylisticSets()),
|
|
1711
2120
|
runAttribute("contextualAlternates", "boolean", "w14:cntxtAlts", schemaBooleanOrNull())
|
|
1712
2121
|
];
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2122
|
+
new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key));
|
|
2123
|
+
var INLINE_PROPERTY_BY_KEY = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry]));
|
|
2124
|
+
INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "mark").map((entry) => entry.key), INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "runAttribute").map((entry) => entry.key);
|
|
2125
|
+
//#endregion
|
|
2126
|
+
//#region src/public/ui/create-super-doc-ui.ts
|
|
2127
|
+
/**
|
|
2128
|
+
* v2-native `createSuperDocUI` controller.
|
|
2129
|
+
*
|
|
2130
|
+
* A small, truthful layer over the public v2 active-editor facade
|
|
2131
|
+
* (`superdoc.activeEditor`), its read-only-guarded Document API
|
|
2132
|
+
* (`activeEditor.doc`), and SuperDoc lifecycle events. No v1 editor imports,
|
|
2133
|
+
* no private v2 runtime imports. In browser mode the underlying Document API
|
|
2134
|
+
* facade is async-capable, so the controller normalizes it into live slices
|
|
2135
|
+
* plus promise-capable workflow helpers. Every document read/mutation is
|
|
2136
|
+
* attempted through the public Document API and degrades to a stable noop /
|
|
2137
|
+
* `false` when the surface is unavailable (view mode, a pre-ready editor, or a
|
|
2138
|
+
* host that has not exposed a Document API facade) rather than throwing.
|
|
2139
|
+
*/
|
|
2140
|
+
var sharedUiTrackedChangesCatalogByHost = /* @__PURE__ */ new WeakMap();
|
|
2141
|
+
function acquireSharedUiTrackedChangesCatalog(host) {
|
|
2142
|
+
let state = sharedUiTrackedChangesCatalogByHost.get(host);
|
|
2143
|
+
if (!state) {
|
|
2144
|
+
state = {
|
|
2145
|
+
refCount: 0,
|
|
2146
|
+
generation: 0,
|
|
2147
|
+
abortController: new AbortController(),
|
|
2148
|
+
inFlight: null,
|
|
2149
|
+
activeMutationTokens: /* @__PURE__ */ new Set()
|
|
2150
|
+
};
|
|
2151
|
+
sharedUiTrackedChangesCatalogByHost.set(host, state);
|
|
2152
|
+
}
|
|
2153
|
+
state.refCount += 1;
|
|
2154
|
+
return state;
|
|
2155
|
+
}
|
|
2156
|
+
function releaseSharedUiTrackedChangesCatalog(host, state) {
|
|
2157
|
+
state.refCount = Math.max(0, state.refCount - 1);
|
|
2158
|
+
if (state.refCount > 0) return;
|
|
2159
|
+
state.abortController.abort("ui-controller-destroyed");
|
|
2160
|
+
state.inFlight = null;
|
|
2161
|
+
state.activeMutationTokens.clear();
|
|
2162
|
+
sharedUiTrackedChangesCatalogByHost.delete(host);
|
|
2163
|
+
}
|
|
1719
2164
|
var SUPERDOC_UI_REASON_VALUES = new Set(Object.values(SUPERDOC_UI_REASONS));
|
|
1720
2165
|
function coerceSuperDocUIReason(reason, fallback) {
|
|
1721
2166
|
return typeof reason === "string" && SUPERDOC_UI_REASON_VALUES.has(reason) ? reason : fallback;
|
|
1722
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
|
+
*/
|
|
1723
2174
|
function normalizeHostSelectionApplyResult(result) {
|
|
1724
2175
|
if (result && typeof result === "object") {
|
|
1725
2176
|
if (result.ok === false) {
|
|
@@ -1736,6 +2187,7 @@ function normalizeHostSelectionApplyResult(result) {
|
|
|
1736
2187
|
}
|
|
1737
2188
|
return { ok: true };
|
|
1738
2189
|
}
|
|
2190
|
+
/** Lifecycle events the controller subscribes to. */
|
|
1739
2191
|
var HOST_EVENTS = [
|
|
1740
2192
|
"document-replaced",
|
|
1741
2193
|
"editorCreate",
|
|
@@ -1746,6 +2198,12 @@ var HOST_EVENTS = [
|
|
|
1746
2198
|
"viewport-change",
|
|
1747
2199
|
"fonts-changed"
|
|
1748
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
|
+
*/
|
|
1749
2207
|
var LIST_TOGGLE_KINDS = {
|
|
1750
2208
|
[BUILT_IN_COMMAND_IDS.bulletList]: "bullet",
|
|
1751
2209
|
[BUILT_IN_COMMAND_IDS.numberedList]: "ordered"
|
|
@@ -1753,6 +2211,15 @@ var LIST_TOGGLE_KINDS = {
|
|
|
1753
2211
|
function listToggleKind(id) {
|
|
1754
2212
|
return LIST_TOGGLE_KINDS[id] ?? null;
|
|
1755
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
|
+
*/
|
|
1756
2223
|
var TOOLBAR_LIST_STYLE_PRESETS = {
|
|
1757
2224
|
disc: "disc",
|
|
1758
2225
|
circle: "circle",
|
|
@@ -1783,7 +2250,7 @@ var EMPTY_SELECTION = {
|
|
|
1783
2250
|
activeChangeIds: [],
|
|
1784
2251
|
quotedText: ""
|
|
1785
2252
|
};
|
|
1786
|
-
|
|
2253
|
+
var HEAVY_DOC_READ_POLICY = [
|
|
1787
2254
|
{
|
|
1788
2255
|
key: "contentControls",
|
|
1789
2256
|
match: "exact",
|
|
@@ -1812,12 +2279,12 @@ const HEAVY_DOC_READ_POLICY = [
|
|
|
1812
2279
|
{
|
|
1813
2280
|
key: "trackChanges",
|
|
1814
2281
|
match: "exact",
|
|
1815
|
-
note: "audited: all-story tracked-changes
|
|
2282
|
+
note: "audited: shared all-story tracked-changes catalog; stale-served during load"
|
|
1816
2283
|
},
|
|
1817
2284
|
{
|
|
1818
2285
|
key: "trackChanges:all",
|
|
1819
2286
|
match: "exact",
|
|
1820
|
-
note: "audited: all-story
|
|
2287
|
+
note: "audited: current-token validation over the shared all-story catalog; consumers fail closed on non-ready"
|
|
1821
2288
|
},
|
|
1822
2289
|
{
|
|
1823
2290
|
key: "tables",
|
|
@@ -1830,19 +2297,22 @@ const HEAVY_DOC_READ_POLICY = [
|
|
|
1830
2297
|
note: "reserved: full sections list"
|
|
1831
2298
|
}
|
|
1832
2299
|
];
|
|
1833
|
-
var COMMENTS_CATALOG_PART_URIS = new Set([
|
|
2300
|
+
var COMMENTS_CATALOG_PART_URIS = /* @__PURE__ */ new Set([
|
|
1834
2301
|
"/word/comments.xml",
|
|
1835
2302
|
"/word/commentsExtended.xml",
|
|
1836
2303
|
"/word/commentsIds.xml"
|
|
1837
2304
|
]);
|
|
2305
|
+
/** Whether a coordinator cache key falls under the heavy-read policy. */
|
|
1838
2306
|
function isHeavyDocReadKey(key) {
|
|
1839
2307
|
return HEAVY_DOC_READ_POLICY.some((entry) => entry.match === "exact" ? entry.key === key : key.startsWith(entry.key));
|
|
1840
2308
|
}
|
|
2309
|
+
/** Status precedence: a combined status is as "unsettled" as its worst part. */
|
|
1841
2310
|
var STATUS_RANK = {
|
|
1842
2311
|
ready: 0,
|
|
1843
2312
|
stale: 1,
|
|
1844
2313
|
pending: 2
|
|
1845
2314
|
};
|
|
2315
|
+
/** Combine read statuses; `pending` (no data yet) dominates `stale`, which dominates `ready`. */
|
|
1846
2316
|
function combineStatus(...statuses) {
|
|
1847
2317
|
let worst = "ready";
|
|
1848
2318
|
for (const status of statuses) if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;
|
|
@@ -1860,6 +2330,7 @@ function safeCall(fn, fallback) {
|
|
|
1860
2330
|
function isPromiseLike(value) {
|
|
1861
2331
|
return Boolean(value) && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
1862
2332
|
}
|
|
2333
|
+
/** Resolve a dotted path (e.g. `format.bold`) to a callable on the doc facade. */
|
|
1863
2334
|
function resolveDocOperation(doc, path) {
|
|
1864
2335
|
if (!doc) return null;
|
|
1865
2336
|
const parts = path.split(".");
|
|
@@ -1894,6 +2365,7 @@ function partialLinkEditFailure(hyperlinkResult, textResult) {
|
|
|
1894
2365
|
textResult
|
|
1895
2366
|
};
|
|
1896
2367
|
}
|
|
2368
|
+
/** Read a stable public `target` off a doc-api entity row. */
|
|
1897
2369
|
function readEntityTarget(row) {
|
|
1898
2370
|
if (!row || typeof row !== "object") return null;
|
|
1899
2371
|
const target = row.target;
|
|
@@ -1910,8 +2382,8 @@ function isHostGeometryTarget(target) {
|
|
|
1910
2382
|
const isTextSegment = (segment) => {
|
|
1911
2383
|
if (!segment || typeof segment !== "object") return false;
|
|
1912
2384
|
const candidate = segment;
|
|
1913
|
-
const range
|
|
1914
|
-
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";
|
|
1915
2387
|
};
|
|
1916
2388
|
if (Array.isArray(record.segments)) {
|
|
1917
2389
|
const first = record.segments[0];
|
|
@@ -1923,6 +2395,12 @@ function isHostGeometryTarget(target) {
|
|
|
1923
2395
|
const range = record.range;
|
|
1924
2396
|
return typeof range?.start === "number" && typeof range.end === "number";
|
|
1925
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
|
+
*/
|
|
1926
2404
|
function readTrackedChangeNavigationTarget(row) {
|
|
1927
2405
|
if (!row || typeof row !== "object") return null;
|
|
1928
2406
|
const target = readEntityTarget(row);
|
|
@@ -1981,11 +2459,35 @@ function entityRowMatchesRequest(row, id, story) {
|
|
|
1981
2459
|
if (!story) return true;
|
|
1982
2460
|
return storyLocatorSignature(readEntityStory(row)) === storyLocatorSignature(story);
|
|
1983
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
|
+
*/
|
|
1984
2471
|
function readEntityRequestStory(row) {
|
|
1985
2472
|
const story = readEntityStory(row);
|
|
1986
2473
|
if (!story || storyLocatorSignature(story) === storyLocatorSignature(null)) return void 0;
|
|
1987
2474
|
return story;
|
|
1988
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
|
+
*/
|
|
1989
2491
|
function layoutStoryDatasetToStoryLocator(raw) {
|
|
1990
2492
|
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
1991
2493
|
const decoded = decodeLayoutStoryDataset(raw);
|
|
@@ -2010,6 +2512,22 @@ function layoutStoryDatasetToStoryLocator(raw) {
|
|
|
2010
2512
|
default: return;
|
|
2011
2513
|
}
|
|
2012
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
|
+
*/
|
|
2013
2531
|
function storyKeyToStoryLocator(raw) {
|
|
2014
2532
|
if (typeof raw !== "string" || raw.length === 0 || raw === "body") return void 0;
|
|
2015
2533
|
if (raw.startsWith("hf:part:")) {
|
|
@@ -2037,6 +2555,7 @@ function storyKeyToStoryLocator(raw) {
|
|
|
2037
2555
|
} : void 0;
|
|
2038
2556
|
}
|
|
2039
2557
|
}
|
|
2558
|
+
/** Read a public `selectionTarget` off a content-control row when available. */
|
|
2040
2559
|
function readSelectionTarget(row) {
|
|
2041
2560
|
if (!row || typeof row !== "object") return null;
|
|
2042
2561
|
const target = row.selectionTarget;
|
|
@@ -2058,12 +2577,19 @@ function collapseSelectionTargetToCaret(target) {
|
|
|
2058
2577
|
end: start
|
|
2059
2578
|
};
|
|
2060
2579
|
}
|
|
2580
|
+
/** Read a stable public row id from discovery/list/detail shapes. */
|
|
2061
2581
|
function readEntityId(row) {
|
|
2062
2582
|
if (!row || typeof row !== "object") return null;
|
|
2063
2583
|
const record = row;
|
|
2064
2584
|
const id = record.id ?? record.commentId ?? record.changeId;
|
|
2065
2585
|
return typeof id === "string" && id.length > 0 ? id : null;
|
|
2066
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
|
+
*/
|
|
2067
2593
|
function readCollapsedHostTrackChangeId(snapshot) {
|
|
2068
2594
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
2069
2595
|
const record = snapshot;
|
|
@@ -2076,6 +2602,11 @@ function readCollapsedHostTrackChangeId(snapshot) {
|
|
|
2076
2602
|
return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
|
|
2077
2603
|
}
|
|
2078
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
|
+
*/
|
|
2079
2610
|
function readTrackChangeProvenanceIds(item) {
|
|
2080
2611
|
const out = [];
|
|
2081
2612
|
const push = (value) => {
|
|
@@ -2116,6 +2647,7 @@ function readTrackChangeProvenanceIds(item) {
|
|
|
2116
2647
|
if (change && change !== item) scan(change);
|
|
2117
2648
|
return out;
|
|
2118
2649
|
}
|
|
2650
|
+
/** Build (or reuse) the alias↔canonical context for a tracked-change list slice. */
|
|
2119
2651
|
function buildTrackedChangeIdContext(items) {
|
|
2120
2652
|
const cached = trackedChangeIdContextCache.get(items);
|
|
2121
2653
|
if (cached) return cached;
|
|
@@ -2148,6 +2680,19 @@ function buildTrackedChangeIdContext(items) {
|
|
|
2148
2680
|
}
|
|
2149
2681
|
var storyScopedTrackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
|
|
2150
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
|
+
*/
|
|
2151
2696
|
function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
2152
2697
|
const signature = storyLocatorSignature(story);
|
|
2153
2698
|
let byStory = storyScopedTrackedChangeIdContextCache.get(items);
|
|
@@ -2164,6 +2709,21 @@ function buildStoryScopedTrackedChangeIdContext(items, story) {
|
|
|
2164
2709
|
function isTextboxStory(story) {
|
|
2165
2710
|
return !!story && typeof story === "object" && story.storyType === "textbox";
|
|
2166
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
|
+
*/
|
|
2167
2727
|
function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
2168
2728
|
const storyless = selectionIds.filter((id) => items.some((item) => readEntityId(item) === id && readEntityStory(item) == null));
|
|
2169
2729
|
if (storyless.length === 0) return null;
|
|
@@ -2179,6 +2739,13 @@ function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
|
|
|
2179
2739
|
}
|
|
2180
2740
|
return storyless[0];
|
|
2181
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
|
+
*/
|
|
2182
2749
|
function declaredBodyItems(items) {
|
|
2183
2750
|
const cached = declaredBodyItemsCache.get(items);
|
|
2184
2751
|
if (cached) return cached;
|
|
@@ -2187,6 +2754,17 @@ function declaredBodyItems(items) {
|
|
|
2187
2754
|
declaredBodyItemsCache.set(items, result);
|
|
2188
2755
|
return result;
|
|
2189
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
|
+
*/
|
|
2190
2768
|
function resolveSelectionActiveChangeId(options) {
|
|
2191
2769
|
const { selectionIds, publicIds, selection, bodyItems, allStoryItems } = options;
|
|
2192
2770
|
if (selectionIds.length === 0) return null;
|
|
@@ -2211,11 +2789,20 @@ function resolveSelectionActiveChangeId(options) {
|
|
|
2211
2789
|
if (isBodySelection) return mapIds(buildTrackedChangeIdContext(bodyItems)) ?? selectionIds[0] ?? null;
|
|
2212
2790
|
return null;
|
|
2213
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
|
+
*/
|
|
2214
2800
|
function resolveActiveCommentId(items, commentId) {
|
|
2215
2801
|
const item = items.find((candidate) => readEntityId(candidate) === commentId || candidate.importedId === commentId);
|
|
2216
2802
|
if (!item) return null;
|
|
2217
2803
|
return item.rootCommentId ?? item.parentCommentId ?? readEntityId(item) ?? commentId;
|
|
2218
2804
|
}
|
|
2805
|
+
/** Derive a `{ startBlockId, endBlockId }` block range from a selection slice. */
|
|
2219
2806
|
function selectionBlockRange(selection) {
|
|
2220
2807
|
const sel = selection.selectionTarget;
|
|
2221
2808
|
if (sel) {
|
|
@@ -2238,6 +2825,16 @@ function selectionBlockRange(selection) {
|
|
|
2238
2825
|
};
|
|
2239
2826
|
return null;
|
|
2240
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
|
+
*/
|
|
2241
2838
|
function selectionBlockIds(selection) {
|
|
2242
2839
|
const ids = [];
|
|
2243
2840
|
const push = (id) => {
|
|
@@ -2257,6 +2854,7 @@ var MAX_REACTIVE_SELECTION_BLOCK_READS = 64;
|
|
|
2257
2854
|
function canProbeEverySelectedBlock(blockIds) {
|
|
2258
2855
|
return blockIds.length <= MAX_REACTIVE_SELECTION_BLOCK_READS;
|
|
2259
2856
|
}
|
|
2857
|
+
/** Resolve the selection's story, defaulting to the main body story. */
|
|
2260
2858
|
function selectionStory(selection) {
|
|
2261
2859
|
const target = selection.target;
|
|
2262
2860
|
const explicit = selection.selectionTarget;
|
|
@@ -2268,6 +2866,7 @@ function selectionStory(selection) {
|
|
|
2268
2866
|
storyType: "body"
|
|
2269
2867
|
};
|
|
2270
2868
|
}
|
|
2869
|
+
/** Build a `ParagraphTarget` ( `format.paragraph.*` / `styles.paragraph.*` ). */
|
|
2271
2870
|
function paragraphTarget(blockId, story) {
|
|
2272
2871
|
return {
|
|
2273
2872
|
kind: "block",
|
|
@@ -2276,6 +2875,7 @@ function paragraphTarget(blockId, story) {
|
|
|
2276
2875
|
...story && typeof story === "object" ? { story } : {}
|
|
2277
2876
|
};
|
|
2278
2877
|
}
|
|
2878
|
+
/** Build a `ListsBlockTarget` ( `lists.apply` / `lists.remove` / `lists.getState` ). */
|
|
2279
2879
|
function listsBlockTarget(blockId) {
|
|
2280
2880
|
return {
|
|
2281
2881
|
kind: "block",
|
|
@@ -2283,6 +2883,7 @@ function listsBlockTarget(blockId) {
|
|
|
2283
2883
|
nodeId: blockId
|
|
2284
2884
|
};
|
|
2285
2885
|
}
|
|
2886
|
+
/** Build a `ListItemAddress` ( `lists.indent` / `lists.outdent` / `lists.applyStyle` ). */
|
|
2286
2887
|
function listItemTarget(blockId, story) {
|
|
2287
2888
|
return {
|
|
2288
2889
|
kind: "block",
|
|
@@ -2320,6 +2921,7 @@ function readParagraphIndentationFromResult(result) {
|
|
|
2320
2921
|
...hanging != null ? { hanging } : {}
|
|
2321
2922
|
};
|
|
2322
2923
|
}
|
|
2924
|
+
/** Build one `TextAddress` per covered text segment for `hyperlinks.wrap`. */
|
|
2323
2925
|
function selectionTextAddresses(selection) {
|
|
2324
2926
|
const target = selection.target;
|
|
2325
2927
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
@@ -2461,12 +3063,12 @@ function selectionTextSegments(selection) {
|
|
|
2461
3063
|
const target = selection.target;
|
|
2462
3064
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
2463
3065
|
const out = [];
|
|
2464
|
-
const push = (blockId, start
|
|
2465
|
-
if (typeof blockId !== "string" || typeof start
|
|
3066
|
+
const push = (blockId, start, end) => {
|
|
3067
|
+
if (typeof blockId !== "string" || typeof start !== "number" || typeof end !== "number") return;
|
|
2466
3068
|
out.push({
|
|
2467
3069
|
blockId,
|
|
2468
|
-
start: Math.min(start
|
|
2469
|
-
end: Math.max(start
|
|
3070
|
+
start: Math.min(start, end),
|
|
3071
|
+
end: Math.max(start, end)
|
|
2470
3072
|
});
|
|
2471
3073
|
};
|
|
2472
3074
|
for (const segment of segments) {
|
|
@@ -2506,6 +3108,12 @@ function selectionInlineValueSignature(selection) {
|
|
|
2506
3108
|
if (segments.length === 0) return null;
|
|
2507
3109
|
return [selectionInlineValueStorySignature(selection), ...segments.map((segment) => `${segment.blockId}:${segment.start}-${segment.end}`)].join("|");
|
|
2508
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
|
+
*/
|
|
2509
3117
|
function selectionEffectiveUniformitySignature(selection) {
|
|
2510
3118
|
const target = selection.selectionTarget;
|
|
2511
3119
|
if (!target || typeof target !== "object") return null;
|
|
@@ -2548,6 +3156,11 @@ function sameSelectionTextSegments(left, right) {
|
|
|
2548
3156
|
return candidate?.blockId === segment.blockId && candidate?.start === segment.start && candidate?.end === segment.end;
|
|
2549
3157
|
});
|
|
2550
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
|
+
*/
|
|
2551
3164
|
function pickSelectionTextQueryItem(result, selection) {
|
|
2552
3165
|
const segments = selectionTextSegments(selection);
|
|
2553
3166
|
if (segments.length === 0) return null;
|
|
@@ -2573,6 +3186,7 @@ function readProjectedInlineStyleValue(styles, key) {
|
|
|
2573
3186
|
if (key === "color" || key === "highlight") return trimmed.toUpperCase();
|
|
2574
3187
|
return trimmed;
|
|
2575
3188
|
}
|
|
3189
|
+
/** Primary named family from a resolved CSS font stack, dropping generic fallbacks (SD-3652). */
|
|
2576
3190
|
function normalizeLayoutFontFamily(value) {
|
|
2577
3191
|
if (typeof value !== "string") return void 0;
|
|
2578
3192
|
const first = value.split(",")[0]?.trim().replace(/^['"]+|['"]+$/g, "").trim();
|
|
@@ -2581,6 +3195,7 @@ function normalizeLayoutFontFamily(value) {
|
|
|
2581
3195
|
if (lower === "serif" || lower === "sans-serif" || lower === "monospace" || lower === "cursive" || lower === "fantasy") return;
|
|
2582
3196
|
return first;
|
|
2583
3197
|
}
|
|
3198
|
+
/** Convert a resolved run font size in CSS px to the toolbar's point value (SD-3652). */
|
|
2584
3199
|
function normalizeLayoutFontSizePt(value) {
|
|
2585
3200
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
|
|
2586
3201
|
const pt = Math.round(value * .75 * 2) / 2;
|
|
@@ -2641,6 +3256,7 @@ function projectInlineValuesFromQueryItem(item, selection) {
|
|
|
2641
3256
|
}
|
|
2642
3257
|
return projection;
|
|
2643
3258
|
}
|
|
3259
|
+
/** A create-location `at` derived from the current block, or `documentEnd`. */
|
|
2644
3260
|
function createLocationAt(selection, mode) {
|
|
2645
3261
|
const blockId = selectionBlockIds(selection)[0];
|
|
2646
3262
|
if (!blockId) return { kind: "documentEnd" };
|
|
@@ -2702,6 +3318,12 @@ function commandResultSucceeded(result) {
|
|
|
2702
3318
|
function isLooseObject(value) {
|
|
2703
3319
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2704
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
|
+
*/
|
|
2705
3327
|
function collectProjectionTextBlocks(blocks) {
|
|
2706
3328
|
const out = [];
|
|
2707
3329
|
const visit = (node) => {
|
|
@@ -2721,6 +3343,7 @@ function collectProjectionTextBlocks(blocks) {
|
|
|
2721
3343
|
if (Array.isArray(blocks)) for (const block of blocks) visit(block);
|
|
2722
3344
|
return out;
|
|
2723
3345
|
}
|
|
3346
|
+
/** A projection block matches a selection block id by `sourceAnchor.sourceNodeId` or `block.id`. */
|
|
2724
3347
|
function projectionBlockMatchesId(block, id) {
|
|
2725
3348
|
if (block.sourceAnchor?.sourceNodeId === id) return true;
|
|
2726
3349
|
return block.id === id;
|
|
@@ -2769,6 +3392,7 @@ function hyperlinkTargetFromHref(href) {
|
|
|
2769
3392
|
url: href
|
|
2770
3393
|
};
|
|
2771
3394
|
}
|
|
3395
|
+
/** Tracked-change decision spec for a command id, from the descriptor catalog. */
|
|
2772
3396
|
function trackDecisionCommand(id) {
|
|
2773
3397
|
return getCommandDescriptor(id)?.trackDecision ?? null;
|
|
2774
3398
|
}
|
|
@@ -2780,6 +3404,7 @@ function rectResult(rects) {
|
|
|
2780
3404
|
rect: rects[0]
|
|
2781
3405
|
};
|
|
2782
3406
|
}
|
|
3407
|
+
/** Fail-closed empty geometry result carrying a stable reason. */
|
|
2783
3408
|
function rectFailure(reason) {
|
|
2784
3409
|
return {
|
|
2785
3410
|
found: false,
|
|
@@ -2788,6 +3413,7 @@ function rectFailure(reason) {
|
|
|
2788
3413
|
reason
|
|
2789
3414
|
};
|
|
2790
3415
|
}
|
|
3416
|
+
/** Offset client-space rects so they are relative to `relativeTo`, when given. */
|
|
2791
3417
|
function relativizeRects(result, relativeTo) {
|
|
2792
3418
|
if (!relativeTo || typeof relativeTo.getBoundingClientRect !== "function" || result.rects.length === 0) return result;
|
|
2793
3419
|
let origin;
|
|
@@ -2813,6 +3439,16 @@ function relativizeRects(result, relativeTo) {
|
|
|
2813
3439
|
rect: rects[0]
|
|
2814
3440
|
};
|
|
2815
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
|
+
*/
|
|
2816
3452
|
function isAddressableSegment(segment) {
|
|
2817
3453
|
if (!segment || typeof segment !== "object") return false;
|
|
2818
3454
|
const candidate = segment;
|
|
@@ -2848,6 +3484,15 @@ function selectionTargetForRestore(capture) {
|
|
|
2848
3484
|
if (capture.selectionTarget) return capture.selectionTarget;
|
|
2849
3485
|
return selectionTargetFromTextTarget(capture.target);
|
|
2850
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
|
+
*/
|
|
2851
3496
|
function resolveInlineSelectionTarget(selection) {
|
|
2852
3497
|
if (selection.empty) return null;
|
|
2853
3498
|
if (selection.selectionTarget) return selection.selectionTarget;
|
|
@@ -2855,10 +3500,17 @@ function resolveInlineSelectionTarget(selection) {
|
|
|
2855
3500
|
if (fallback && fallback.start.kind === "text" && fallback.end.kind === "text" && fallback.start.blockId !== fallback.end.blockId) return null;
|
|
2856
3501
|
return fallback;
|
|
2857
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
|
+
*/
|
|
2858
3509
|
function inlineFormatMethod(descriptor) {
|
|
2859
3510
|
if (!descriptor.inline || typeof descriptor.docRoute !== "string") return null;
|
|
2860
3511
|
return descriptor.docRoute.startsWith("format.") ? descriptor.docRoute.slice(7) : null;
|
|
2861
3512
|
}
|
|
3513
|
+
/** Inline run-property keys cleared by `clear-formatting` via `format.apply`. */
|
|
2862
3514
|
var CLEAR_INLINE_PATCH = {
|
|
2863
3515
|
bold: null,
|
|
2864
3516
|
italic: null,
|
|
@@ -2874,7 +3526,22 @@ var CLEAR_INLINE_PATCH = {
|
|
|
2874
3526
|
letterSpacing: null,
|
|
2875
3527
|
dstrike: null
|
|
2876
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
|
+
*/
|
|
2877
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
|
+
*/
|
|
2878
3545
|
function buildInlineFormatInput(spec, target, payload, active) {
|
|
2879
3546
|
switch (spec.kind) {
|
|
2880
3547
|
case "toggle": {
|
|
@@ -2916,6 +3583,7 @@ function buildInlineFormatInput(spec, target, payload, active) {
|
|
|
2916
3583
|
default: return null;
|
|
2917
3584
|
}
|
|
2918
3585
|
}
|
|
3586
|
+
/** Read a `{ start, end }` cell-range from the host snapshot, when well-formed. */
|
|
2919
3587
|
function readMergeRange(range) {
|
|
2920
3588
|
const point = (value) => {
|
|
2921
3589
|
if (!value || typeof value !== "object") return null;
|
|
@@ -2938,6 +3606,12 @@ function readMergeRange(range) {
|
|
|
2938
3606
|
end
|
|
2939
3607
|
};
|
|
2940
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
|
+
*/
|
|
2941
3615
|
function buildTableCommandInput(action, context) {
|
|
2942
3616
|
const { tableNodeId: nodeId, rowIndex, columnIndex, cellNodeId, mergeRange } = context;
|
|
2943
3617
|
switch (action) {
|
|
@@ -3026,18 +3700,22 @@ function createSuperDocUI(options) {
|
|
|
3026
3700
|
let frozenProjectedValues = {};
|
|
3027
3701
|
let frozenProjectedValuesKey = "";
|
|
3028
3702
|
let heldSettledInlineValues = null;
|
|
3703
|
+
/** Read the live active editor (or null). */
|
|
3029
3704
|
const getEditor = () => {
|
|
3030
3705
|
const editor = superdoc?.activeEditor;
|
|
3031
3706
|
return editor && typeof editor === "object" ? editor : null;
|
|
3032
3707
|
};
|
|
3708
|
+
/** Read the live browser Document API facade (or null). */
|
|
3033
3709
|
const getDoc = () => {
|
|
3034
3710
|
const doc = getEditor()?.doc;
|
|
3035
3711
|
return doc && typeof doc === "object" ? doc : null;
|
|
3036
3712
|
};
|
|
3713
|
+
/** Read the live v2 tracked-change facade, when exposed. */
|
|
3037
3714
|
const getV2TrackedChanges = () => {
|
|
3038
3715
|
const trackedChanges = getEditor()?.v2TrackedChanges;
|
|
3039
3716
|
return trackedChanges && typeof trackedChanges === "object" ? trackedChanges : null;
|
|
3040
3717
|
};
|
|
3718
|
+
/** Read the live v2 editor host (inline mode only), when exposed. */
|
|
3041
3719
|
const getHost = () => {
|
|
3042
3720
|
const host = getEditor()?.host;
|
|
3043
3721
|
return host && typeof host === "object" ? host : null;
|
|
@@ -3076,6 +3754,7 @@ function createSuperDocUI(options) {
|
|
|
3076
3754
|
}
|
|
3077
3755
|
return null;
|
|
3078
3756
|
};
|
|
3757
|
+
/** Read the host's stored inline marks (SD-3654/SD-3652), when any pending. */
|
|
3079
3758
|
const readPendingInlineFormat = () => {
|
|
3080
3759
|
const host = getHost();
|
|
3081
3760
|
const fn = host?.getPendingInlineFormat;
|
|
@@ -3083,6 +3762,11 @@ function createSuperDocUI(options) {
|
|
|
3083
3762
|
const pending = safeCall(() => fn.call(host), null);
|
|
3084
3763
|
return pending && typeof pending === "object" ? pending : null;
|
|
3085
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
|
+
*/
|
|
3086
3770
|
const pendingInlineActive = (descriptor) => {
|
|
3087
3771
|
const method = inlineFormatMethod(descriptor);
|
|
3088
3772
|
if (!method) return null;
|
|
@@ -3092,6 +3776,7 @@ function createSuperDocUI(options) {
|
|
|
3092
3776
|
if (descriptor.inline?.kind === "toggle") return value === true;
|
|
3093
3777
|
return value != null && value !== "";
|
|
3094
3778
|
};
|
|
3779
|
+
/** The pending caret font/size value for this command's `format.*` method, if any (SD-3652). */
|
|
3095
3780
|
const pendingInlineValueFor = (descriptor) => {
|
|
3096
3781
|
const method = inlineFormatMethod(descriptor);
|
|
3097
3782
|
if (!method) return void 0;
|
|
@@ -3101,6 +3786,7 @@ function createSuperDocUI(options) {
|
|
|
3101
3786
|
if (typeof value === "string" && value !== "") return value;
|
|
3102
3787
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
3103
3788
|
};
|
|
3789
|
+
/** Record / clear the stored inline mark for a collapsed-caret pick (SD-3654/SD-3652). */
|
|
3104
3790
|
const setPendingInlineFormatOnHost = (method, value) => {
|
|
3105
3791
|
const host = getHost();
|
|
3106
3792
|
const fn = host?.setPendingInlineFormat;
|
|
@@ -3111,16 +3797,23 @@ function createSuperDocUI(options) {
|
|
|
3111
3797
|
const fn = host?.clearPendingInlineFormat;
|
|
3112
3798
|
if (typeof fn === "function") safeCall(() => fn.call(host, method), void 0);
|
|
3113
3799
|
};
|
|
3114
|
-
|
|
3800
|
+
/** Inline `format.*` methods whose active state is a boolean mark (name === method). */
|
|
3801
|
+
const BOOLEAN_INLINE_FORMAT_METHODS = /* @__PURE__ */ new Set([
|
|
3115
3802
|
"bold",
|
|
3116
3803
|
"italic",
|
|
3117
3804
|
"underline",
|
|
3118
3805
|
"strikethrough"
|
|
3119
3806
|
]);
|
|
3120
|
-
|
|
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) => {
|
|
3121
3814
|
const pending = readPendingInlineFormat();
|
|
3122
3815
|
if (!pending) return;
|
|
3123
|
-
const activeMarks = Array.isArray(selection
|
|
3816
|
+
const activeMarks = Array.isArray(selection.activeMarks) ? selection.activeMarks : [];
|
|
3124
3817
|
let projected = null;
|
|
3125
3818
|
for (const method of Object.keys(pending)) {
|
|
3126
3819
|
const value = pending[method];
|
|
@@ -3128,33 +3821,47 @@ function createSuperDocUI(options) {
|
|
|
3128
3821
|
if (BOOLEAN_INLINE_FORMAT_METHODS.has(method)) matched = activeMarks.includes(method) === (value === true);
|
|
3129
3822
|
else if (isProjectedInlineSelectionValueKey(method)) {
|
|
3130
3823
|
if (value == null) continue;
|
|
3131
|
-
if (!projected) projected = projectSelectionInlineValues(selection
|
|
3824
|
+
if (!projected) projected = projectSelectionInlineValues(selection);
|
|
3132
3825
|
matched = projected[method] === value;
|
|
3133
3826
|
} else matched = false;
|
|
3134
3827
|
if (matched) clearPendingInlineFormatOnHost(method);
|
|
3135
3828
|
}
|
|
3136
3829
|
};
|
|
3830
|
+
/** Read the live v2 edit-command adapters (`activeEditor.editCommands`), when exposed. */
|
|
3137
3831
|
const getEditCommands = () => {
|
|
3138
3832
|
const editCommands = getEditor()?.editCommands;
|
|
3139
3833
|
return editCommands && typeof editCommands === "object" ? editCommands : null;
|
|
3140
3834
|
};
|
|
3835
|
+
/** Read one edit-command snapshot entry (`activeEditor.editCommands.getSnapshot().commands[id]`). */
|
|
3141
3836
|
const readEditCommandStateEntry = (commandId) => {
|
|
3142
3837
|
const editCommands = getEditCommands();
|
|
3143
3838
|
if (!editCommands || typeof editCommands.getSnapshot !== "function") return null;
|
|
3144
3839
|
const entry = (safeCall(() => editCommands.getSnapshot(), null)?.commands)?.[commandId];
|
|
3145
3840
|
return entry && typeof entry === "object" ? entry : null;
|
|
3146
3841
|
};
|
|
3842
|
+
/** Read the `lists.apply` command state surfaced by the v2 edit-command snapshot. */
|
|
3147
3843
|
const readListApplyStateEntry = () => {
|
|
3148
3844
|
return readEditCommandStateEntry("lists.apply");
|
|
3149
3845
|
};
|
|
3846
|
+
/** Read the additive list active state surfaced by the edit-command snapshot. */
|
|
3150
3847
|
const readListActiveSeed = (entry) => {
|
|
3151
3848
|
const seed = (entry?.value)?.seed;
|
|
3152
3849
|
return seed === "bullet" || seed === "ordered" ? seed : null;
|
|
3153
3850
|
};
|
|
3851
|
+
/** Read the lower host-state entry mirrored into the public `undo` / `redo` ids. */
|
|
3154
3852
|
const readMirroredEditCommandStateEntry = (commandId) => {
|
|
3155
3853
|
const mirrored = MIRRORED_EDIT_COMMAND_IDS[commandId];
|
|
3156
3854
|
return mirrored ? readEditCommandStateEntry(mirrored) : null;
|
|
3157
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
|
+
*/
|
|
3158
3865
|
const readHostTableContext = () => {
|
|
3159
3866
|
const host = getHost();
|
|
3160
3867
|
if (!host || typeof host.getTableContext !== "function") return null;
|
|
@@ -3221,11 +3928,11 @@ function createSuperDocUI(options) {
|
|
|
3221
3928
|
if (typeof review.getActiveReviewTarget !== "function" || typeof review.clearActiveReviewTarget !== "function") return;
|
|
3222
3929
|
if (safeCall(() => review.getActiveReviewTarget(), null)?.entityType === "trackedChange") safeCall(() => review.clearActiveReviewTarget(), void 0);
|
|
3223
3930
|
};
|
|
3224
|
-
const setExplicitActiveChange = (next, options
|
|
3931
|
+
const setExplicitActiveChange = (next, options) => {
|
|
3225
3932
|
explicitActiveChange = next;
|
|
3226
3933
|
explicitActiveChangeRevision += 1;
|
|
3227
3934
|
trackChangeRevealInvalidation += 1;
|
|
3228
|
-
if (options
|
|
3935
|
+
if (options?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
|
|
3229
3936
|
};
|
|
3230
3937
|
const listeners = /* @__PURE__ */ new Set();
|
|
3231
3938
|
let disposed = false;
|
|
@@ -3276,6 +3983,63 @@ function createSuperDocUI(options) {
|
|
|
3276
3983
|
};
|
|
3277
3984
|
let documentMutationRevision = 0;
|
|
3278
3985
|
let reviewMutationToken = {};
|
|
3986
|
+
const uiTrackedChangesCatalogHost = options.superdoc;
|
|
3987
|
+
const uiTrackedChangesCatalogState = acquireSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost);
|
|
3988
|
+
const supersedeUiTrackedChangesCatalogRead = () => {
|
|
3989
|
+
uiTrackedChangesCatalogState.abortController.abort("review-mutation-started");
|
|
3990
|
+
uiTrackedChangesCatalogState.inFlight = null;
|
|
3991
|
+
};
|
|
3992
|
+
const renewUiTrackedChangesCatalogRead = () => {
|
|
3993
|
+
uiTrackedChangesCatalogState.generation += 1;
|
|
3994
|
+
uiTrackedChangesCatalogState.abortController = new AbortController();
|
|
3995
|
+
uiTrackedChangesCatalogState.inFlight = null;
|
|
3996
|
+
};
|
|
3997
|
+
const beginUiReviewMutation = (token) => {
|
|
3998
|
+
const activeTokens = uiTrackedChangesCatalogState.activeMutationTokens;
|
|
3999
|
+
if (typeof token !== "string" || token.length === 0 || activeTokens.has(token)) return;
|
|
4000
|
+
const firstToken = activeTokens.size === 0;
|
|
4001
|
+
activeTokens.add(token);
|
|
4002
|
+
if (firstToken) supersedeUiTrackedChangesCatalogRead();
|
|
4003
|
+
};
|
|
4004
|
+
const settleUiReviewMutation = (token) => {
|
|
4005
|
+
const activeTokens = uiTrackedChangesCatalogState.activeMutationTokens;
|
|
4006
|
+
if (typeof token !== "string" || !activeTokens.delete(token)) return;
|
|
4007
|
+
if (activeTokens.size === 0) renewUiTrackedChangesCatalogRead();
|
|
4008
|
+
};
|
|
4009
|
+
const runUiTrackedChangesCatalogRead = (fallback) => {
|
|
4010
|
+
const v2TrackedChanges = getV2TrackedChanges();
|
|
4011
|
+
const listTrackedChanges = v2TrackedChanges?.listTrackedChanges;
|
|
4012
|
+
if (typeof listTrackedChanges !== "function") {
|
|
4013
|
+
if (getEditor()?.editorVersion === 2) return Promise.reject(/* @__PURE__ */ new Error("v2-tracked-changes-bridge-pending"));
|
|
4014
|
+
return fallback();
|
|
4015
|
+
}
|
|
4016
|
+
if (uiTrackedChangesCatalogState.activeMutationTokens.size > 0) return Promise.reject(/* @__PURE__ */ new Error("ui-review-catalog-superseded"));
|
|
4017
|
+
if ((getEditor()?.reviewHydration?.getDiagnostics?.())?.inFlight === true) return Promise.reject(/* @__PURE__ */ new Error("shell-review-hydration-in-flight"));
|
|
4018
|
+
const generation = uiTrackedChangesCatalogState.generation;
|
|
4019
|
+
const existing = uiTrackedChangesCatalogState.inFlight;
|
|
4020
|
+
if (existing?.generation === generation) return existing.promise;
|
|
4021
|
+
const signal = uiTrackedChangesCatalogState.abortController.signal;
|
|
4022
|
+
const validateResult = (result) => {
|
|
4023
|
+
if (signal.aborted || generation !== uiTrackedChangesCatalogState.generation || result?.reason === "review-hydration-superseded") throw new Error("ui-review-catalog-superseded");
|
|
4024
|
+
return result;
|
|
4025
|
+
};
|
|
4026
|
+
const rawResult = listTrackedChanges.call(v2TrackedChanges, {
|
|
4027
|
+
blocking: false,
|
|
4028
|
+
signal
|
|
4029
|
+
});
|
|
4030
|
+
if (!isPromiseLike(rawResult)) return validateResult(rawResult);
|
|
4031
|
+
const promise = Promise.resolve(rawResult).then(validateResult);
|
|
4032
|
+
uiTrackedChangesCatalogState.inFlight = {
|
|
4033
|
+
generation,
|
|
4034
|
+
promise
|
|
4035
|
+
};
|
|
4036
|
+
promise.then(() => {
|
|
4037
|
+
if (uiTrackedChangesCatalogState.inFlight?.promise === promise) uiTrackedChangesCatalogState.inFlight = null;
|
|
4038
|
+
}, () => {
|
|
4039
|
+
if (uiTrackedChangesCatalogState.inFlight?.promise === promise) uiTrackedChangesCatalogState.inFlight = null;
|
|
4040
|
+
});
|
|
4041
|
+
return promise;
|
|
4042
|
+
};
|
|
3279
4043
|
let authoritativeTrackChangesPendingToken = null;
|
|
3280
4044
|
let authoritativeTrackChangesRetryTimer = null;
|
|
3281
4045
|
let authoritativeTrackChangesFailureCount = 0;
|
|
@@ -3287,8 +4051,10 @@ function createSuperDocUI(options) {
|
|
|
3287
4051
|
let scheduleAuthoritativeTrackChangesRetry = (_reconcileToken) => void 0;
|
|
3288
4052
|
let postDecisionTrackChangesToken = null;
|
|
3289
4053
|
let postDecisionTrackChangeIds = /* @__PURE__ */ new Set();
|
|
4054
|
+
let allTrackedChangesResolvedToken = null;
|
|
3290
4055
|
let selectionEpoch = 0;
|
|
3291
4056
|
let lastCoordinatorEditor = null;
|
|
4057
|
+
/** Token shared by document-content reads (editor identity + mutation revision). */
|
|
3292
4058
|
const contentToken = () => `${editorIdentityId(getEditor())}|m${documentMutationRevision}`;
|
|
3293
4059
|
const clearPostDecisionTrackChanges = () => {
|
|
3294
4060
|
postDecisionTrackChangesToken = null;
|
|
@@ -3365,6 +4131,31 @@ function createSuperDocUI(options) {
|
|
|
3365
4131
|
};
|
|
3366
4132
|
notifyPostDecisionTrackChanges(receipt);
|
|
3367
4133
|
};
|
|
4134
|
+
/**
|
|
4135
|
+
* Publish the exact empty catalog proved by an all-resolved receipt.
|
|
4136
|
+
* Reject All does not carry one removed ref per logical change, so the
|
|
4137
|
+
* ordinary identity-pruning path cannot clear selection-derived focus or
|
|
4138
|
+
* the slice total. Keep this proof scoped to the current content token: the
|
|
4139
|
+
* next independent document mutation advances the token and resumes normal
|
|
4140
|
+
* catalog/selection reads.
|
|
4141
|
+
*/
|
|
4142
|
+
const publishAllTrackedChangesResolved = (receipt) => {
|
|
4143
|
+
const previous = state.trackChanges;
|
|
4144
|
+
const changed = previous.items.length > 0 || previous.total !== 0 || previous.activeId !== null || previous.authors.length > 0;
|
|
4145
|
+
allTrackedChangesResolvedToken = contentToken();
|
|
4146
|
+
if (explicitActiveChange !== null) setExplicitActiveChange(null);
|
|
4147
|
+
state = {
|
|
4148
|
+
...state,
|
|
4149
|
+
trackChanges: {
|
|
4150
|
+
...previous,
|
|
4151
|
+
items: [],
|
|
4152
|
+
total: 0,
|
|
4153
|
+
activeId: null,
|
|
4154
|
+
authors: []
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
4157
|
+
if (changed) notifyPostDecisionTrackChanges(receipt);
|
|
4158
|
+
};
|
|
3368
4159
|
const markPostDecisionTrackChanges = (ids, receipt) => {
|
|
3369
4160
|
if (ids.size === 0) return;
|
|
3370
4161
|
const token = contentToken();
|
|
@@ -3372,11 +4163,18 @@ function createSuperDocUI(options) {
|
|
|
3372
4163
|
const newlyRemoved = new Set([...ids].filter((id) => !alreadyRemoved.has(id)));
|
|
3373
4164
|
if (newlyRemoved.size === 0) return;
|
|
3374
4165
|
postDecisionTrackChangesToken = token;
|
|
3375
|
-
postDecisionTrackChangeIds = new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
4166
|
+
postDecisionTrackChangeIds = /* @__PURE__ */ new Set([...alreadyRemoved, ...newlyRemoved]);
|
|
3376
4167
|
applyPostDecisionTrackChangesToCache(postDecisionTrackChangeIds);
|
|
3377
4168
|
publishPostDecisionTrackChanges(newlyRemoved, receipt);
|
|
3378
4169
|
};
|
|
3379
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
|
+
*/
|
|
3380
4178
|
const mergeTrackedChangeItemsIntoCache = (items, orderedItems = []) => {
|
|
3381
4179
|
const projected = items.map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) != null);
|
|
3382
4180
|
if (projected.length === 0) return;
|
|
@@ -3444,22 +4242,22 @@ function createSuperDocUI(options) {
|
|
|
3444
4242
|
inflightToken: null
|
|
3445
4243
|
});
|
|
3446
4244
|
};
|
|
3447
|
-
const hydrateTrackedChanges = async (context, options
|
|
4245
|
+
const hydrateTrackedChanges = async (context, options) => {
|
|
3448
4246
|
try {
|
|
3449
4247
|
const listTrackedChanges = context.v2TrackedChanges?.listTrackedChanges;
|
|
3450
4248
|
if (typeof listTrackedChanges === "function") {
|
|
3451
|
-
const result
|
|
3452
|
-
mode: options
|
|
3453
|
-
refreshReason: options
|
|
3454
|
-
blocking: options
|
|
3455
|
-
...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 } : {}
|
|
3456
4254
|
}));
|
|
3457
|
-
const ok = options
|
|
4255
|
+
const ok = options.trackedChangesListMode === "all" && options.refreshReason === "mutation-history-render-reconcile" ? result?.ok === true : result?.ok !== false;
|
|
3458
4256
|
return {
|
|
3459
4257
|
ok,
|
|
3460
|
-
items: ok && Array.isArray(result
|
|
3461
|
-
...typeof result
|
|
3462
|
-
...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 } : {}
|
|
3463
4261
|
};
|
|
3464
4262
|
}
|
|
3465
4263
|
const list = context.docTrackChanges?.list;
|
|
@@ -3480,6 +4278,13 @@ function createSuperDocUI(options) {
|
|
|
3480
4278
|
};
|
|
3481
4279
|
}
|
|
3482
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
|
+
*/
|
|
3483
4288
|
const reviewMutationReconciler = createV2ReviewMutationReconciler({
|
|
3484
4289
|
getContext: () => {
|
|
3485
4290
|
const editor = getEditor();
|
|
@@ -3523,12 +4328,12 @@ function createSuperDocUI(options) {
|
|
|
3523
4328
|
const reads = await Promise.all(requestedIds.map(async (id) => {
|
|
3524
4329
|
try {
|
|
3525
4330
|
if (typeof getTrackedChange === "function") {
|
|
3526
|
-
const result
|
|
4331
|
+
const result = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
|
|
3527
4332
|
return {
|
|
3528
4333
|
id,
|
|
3529
|
-
ok: result
|
|
3530
|
-
items: result
|
|
3531
|
-
result
|
|
4334
|
+
ok: result?.ok !== false,
|
|
4335
|
+
items: result?.ok !== false && Array.isArray(result?.items) ? result.items : [],
|
|
4336
|
+
result
|
|
3532
4337
|
};
|
|
3533
4338
|
}
|
|
3534
4339
|
const result = await Promise.resolve(getRawTrackedChange.call(context.docTrackChanges, { id }));
|
|
@@ -3595,11 +4400,12 @@ function createSuperDocUI(options) {
|
|
|
3595
4400
|
recompute();
|
|
3596
4401
|
return;
|
|
3597
4402
|
}
|
|
3598
|
-
|
|
4403
|
+
const items = (result?.items ?? []).filter((item) => {
|
|
3599
4404
|
const projected = projectTrackChangesItem(item);
|
|
3600
4405
|
const id = projected == null ? null : readEntityId(projected);
|
|
3601
4406
|
return id != null && ids.has(id);
|
|
3602
|
-
})
|
|
4407
|
+
});
|
|
4408
|
+
mergeTrackedChangeItemsIntoCache(items, result?.orderedItems ?? result?.items ?? []);
|
|
3603
4409
|
recompute();
|
|
3604
4410
|
}
|
|
3605
4411
|
});
|
|
@@ -3613,10 +4419,12 @@ function createSuperDocUI(options) {
|
|
|
3613
4419
|
reviewMutationReconciler.onRender(true);
|
|
3614
4420
|
}, delayMs);
|
|
3615
4421
|
};
|
|
4422
|
+
/** Token for the live selection read, including the mode that controls its public projection. */
|
|
3616
4423
|
const selectionReadToken = () => `${contentToken()}|m${readDocumentMode()}|s${selectionEpoch}`;
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
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
|
|
3620
4428
|
});
|
|
3621
4429
|
const SELECTION_SCOPED_ASYNC_READ_PREFIXES = [
|
|
3622
4430
|
"contentControls:inRange:",
|
|
@@ -3625,6 +4433,7 @@ function createSuperDocUI(options) {
|
|
|
3625
4433
|
];
|
|
3626
4434
|
const FOREGROUND_ASYNC_RETRY_MS = 120;
|
|
3627
4435
|
const COLD_ASYNC_READ_START_DELAY_MS = 180;
|
|
4436
|
+
/** Tri-state host source-load phase derived from the loading snapshot seam. */
|
|
3628
4437
|
const hostSourceLoadPhase = () => {
|
|
3629
4438
|
const host = getHost();
|
|
3630
4439
|
const read = host?.getDocumentLoadingSnapshot;
|
|
@@ -3647,9 +4456,14 @@ function createSuperDocUI(options) {
|
|
|
3647
4456
|
return Array.isArray(entry.value) && entry.value.length > 0;
|
|
3648
4457
|
};
|
|
3649
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
|
+
*/
|
|
3650
4463
|
const ensureContentControlsCatalog = (_reason) => {
|
|
3651
4464
|
demandHeavyDocRead("contentControls");
|
|
3652
4465
|
};
|
|
4466
|
+
/** Demand-driven route for an explicitly consumed tracked-change list. */
|
|
3653
4467
|
const ensureTrackChangesCatalog = () => {
|
|
3654
4468
|
const entry = asyncReads.get("trackChanges");
|
|
3655
4469
|
if (entry?.hasSettled || entry?.inflightToken != null) return;
|
|
@@ -3659,9 +4473,21 @@ function createSuperDocUI(options) {
|
|
|
3659
4473
|
const HEAVY_READ_IDLE_CEILING_MS = 8e3;
|
|
3660
4474
|
const HEAVY_READ_IDLE_POLL_MS = 250;
|
|
3661
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
|
+
*/
|
|
3662
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
|
+
*/
|
|
3663
4487
|
let heavyReadsHeldUntilIdle = false;
|
|
4488
|
+
/** When the STEADY-phase typing hold began; bounds it to the same ceiling. */
|
|
3664
4489
|
let heavyReadsTypingHoldSinceMs = 0;
|
|
4490
|
+
/** Lets one ceiling-triggered recompute refresh every heavy key as a group. */
|
|
3665
4491
|
let heavyReadCeilingReleaseActive = false;
|
|
3666
4492
|
let heavyReadCompletionRecomputeTimer = null;
|
|
3667
4493
|
const scheduleHeavyReadCompletionRecompute = () => {
|
|
@@ -3721,15 +4547,15 @@ function createSuperDocUI(options) {
|
|
|
3721
4547
|
const host = getEditor()?.host;
|
|
3722
4548
|
const read = host?.getForegroundMutationState;
|
|
3723
4549
|
if (typeof read !== "function") return null;
|
|
3724
|
-
const state
|
|
4550
|
+
const state = safeCall(() => read.call(host), null);
|
|
3725
4551
|
return {
|
|
3726
|
-
active: typeof state
|
|
3727
|
-
pending: typeof state
|
|
4552
|
+
active: typeof state?.active === "number" ? state.active : 0,
|
|
4553
|
+
pending: typeof state?.pending === "number" ? state.pending : 0
|
|
3728
4554
|
};
|
|
3729
4555
|
};
|
|
3730
4556
|
const foregroundMutationActive = () => {
|
|
3731
|
-
const state
|
|
3732
|
-
return Boolean(state
|
|
4557
|
+
const state = foregroundMutationState();
|
|
4558
|
+
return Boolean(state && (state.active > 0 || state.pending > 0));
|
|
3733
4559
|
};
|
|
3734
4560
|
const scheduleForegroundAsyncRetry = (delayMs = FOREGROUND_ASYNC_RETRY_MS) => {
|
|
3735
4561
|
if (disposed || foregroundAsyncRetryTimer) return;
|
|
@@ -3755,6 +4581,12 @@ function createSuperDocUI(options) {
|
|
|
3755
4581
|
scheduleForegroundAsyncRetry(COLD_ASYNC_READ_START_DELAY_MS);
|
|
3756
4582
|
return true;
|
|
3757
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
|
+
*/
|
|
3758
4590
|
const shouldDeferEffectiveInlineRead = (key) => {
|
|
3759
4591
|
if (lastEffectiveInlineReadKey == null || key === lastEffectiveInlineReadKey) {
|
|
3760
4592
|
if (pendingEffectiveInlineRead?.timer) clearTimeout(pendingEffectiveInlineRead.timer);
|
|
@@ -3901,6 +4733,12 @@ function createSuperDocUI(options) {
|
|
|
3901
4733
|
});
|
|
3902
4734
|
return null;
|
|
3903
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
|
+
*/
|
|
3904
4742
|
const readAsync = (key, token, run, normalize) => {
|
|
3905
4743
|
const entry = asyncReads.get(key);
|
|
3906
4744
|
if (entry && entry.token === token && entry.hasSettled) return {
|
|
@@ -3999,23 +4837,24 @@ function createSuperDocUI(options) {
|
|
|
3999
4837
|
const clearAsyncReadKeys = (predicate) => {
|
|
4000
4838
|
for (const key of asyncReads.keys()) if (predicate(key)) asyncReads.delete(key);
|
|
4001
4839
|
};
|
|
4002
|
-
const selectionScopedAsyncReadKeys = (selection
|
|
4003
|
-
const signature = selectionSignature(selection
|
|
4004
|
-
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection
|
|
4005
|
-
return new Set([
|
|
4840
|
+
const selectionScopedAsyncReadKeys = (selection) => {
|
|
4841
|
+
const signature = selectionSignature(selection);
|
|
4842
|
+
const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection);
|
|
4843
|
+
return /* @__PURE__ */ new Set([
|
|
4006
4844
|
`contentControls:inRange:${signature}`,
|
|
4007
4845
|
`query:${signature}`,
|
|
4008
4846
|
...effectiveUniformitySignature ? [`effInline:${effectiveUniformitySignature}`] : []
|
|
4009
4847
|
]);
|
|
4010
4848
|
};
|
|
4011
|
-
const pruneSelectionScopedAsyncReads = (selection
|
|
4012
|
-
const retained = selectionScopedAsyncReadKeys(selection
|
|
4849
|
+
const pruneSelectionScopedAsyncReads = (selection) => {
|
|
4850
|
+
const retained = selectionScopedAsyncReadKeys(selection);
|
|
4013
4851
|
clearAsyncReadKeys((key) => SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => key.startsWith(prefix)) && !retained.has(key));
|
|
4014
4852
|
for (const key of coldAsyncReadDeferrals) {
|
|
4015
4853
|
const readKey = key.split("\0", 1)[0];
|
|
4016
4854
|
if (SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => readKey.startsWith(prefix)) && !retained.has(readKey)) coldAsyncReadDeferrals.delete(key);
|
|
4017
4855
|
}
|
|
4018
4856
|
};
|
|
4857
|
+
/** Drop cached reads when the active editor identity changes (avoids cross-editor leakage). */
|
|
4019
4858
|
const syncCoordinatorEditor = () => {
|
|
4020
4859
|
const editor = getEditor();
|
|
4021
4860
|
if (editor === lastCoordinatorEditor) return;
|
|
@@ -4043,13 +4882,37 @@ function createSuperDocUI(options) {
|
|
|
4043
4882
|
detachSourceLoading = null;
|
|
4044
4883
|
sourceLoadingSubscriptionHost = null;
|
|
4045
4884
|
clearPostDecisionTrackChanges();
|
|
4885
|
+
allTrackedChangesResolvedToken = null;
|
|
4046
4886
|
};
|
|
4887
|
+
/** Bump the document-mutation revision so content reads re-fetch on the next compute. */
|
|
4047
4888
|
const invalidateDocumentContent = () => {
|
|
4048
4889
|
const carryAuthoritativeTrackChangesHold = authoritativeTrackChangesPendingToken !== null;
|
|
4049
4890
|
documentMutationRevision += 1;
|
|
4050
4891
|
if (carryAuthoritativeTrackChangesHold) authoritativeTrackChangesPendingToken = contentToken();
|
|
4051
4892
|
clearPostDecisionTrackChanges();
|
|
4052
4893
|
};
|
|
4894
|
+
/**
|
|
4895
|
+
* Refresh command-derived document slices without discarding an exact
|
|
4896
|
+
* all-resolved catalog received while the async command was settling. Host
|
|
4897
|
+
* mutation events arrive before the command promise resolves; a generic
|
|
4898
|
+
* invalidation at that later boundary would otherwise advance the cache
|
|
4899
|
+
* token and immediately re-list the catalog we already know is empty.
|
|
4900
|
+
*/
|
|
4901
|
+
const invalidateAfterCommandSettlement = () => {
|
|
4902
|
+
const carryAllResolvedCatalog = allTrackedChangesResolvedToken === contentToken();
|
|
4903
|
+
invalidateDocumentContent();
|
|
4904
|
+
if (carryAllResolvedCatalog) {
|
|
4905
|
+
replaceTrackedChangeItemsInCache([]);
|
|
4906
|
+
allTrackedChangesResolvedToken = contentToken();
|
|
4907
|
+
}
|
|
4908
|
+
recompute();
|
|
4909
|
+
};
|
|
4910
|
+
/**
|
|
4911
|
+
* Insert plain text through the public Document API. The narrow built-in /
|
|
4912
|
+
* custom insertion helper exposed on the custom-command callback context.
|
|
4913
|
+
* Fails closed (failure receipt) in viewing mode or when `doc.insert` is
|
|
4914
|
+
* unavailable — never reaching into `activeEditor.commands`.
|
|
4915
|
+
*/
|
|
4053
4916
|
const insertText = (text) => {
|
|
4054
4917
|
if (typeof text !== "string" || text.length === 0) return failedReceipt("insertText requires a non-empty string.", "INVALID_INPUT");
|
|
4055
4918
|
if (readDocumentMode() === "viewing") return failedReceipt("The document is read-only.", "DOCUMENT_READONLY");
|
|
@@ -4058,8 +4921,8 @@ function createSuperDocUI(options) {
|
|
|
4058
4921
|
if (!doc || typeof op !== "function") return failedReceipt("insert is unavailable.");
|
|
4059
4922
|
const currentSelection = doc.selection?.current;
|
|
4060
4923
|
if (typeof currentSelection !== "function") return failedReceipt("selection.current is unavailable.");
|
|
4061
|
-
const insertAtLiveSelection = (selection
|
|
4062
|
-
const record = selection
|
|
4924
|
+
const insertAtLiveSelection = (selection) => {
|
|
4925
|
+
const record = selection && typeof selection === "object" ? selection : null;
|
|
4063
4926
|
const target = record?.selectionTarget ?? record?.target ?? null;
|
|
4064
4927
|
if (!target || typeof target !== "object") return failedReceipt("insertText requires a live selection target.", "PRECONDITION_FAILED");
|
|
4065
4928
|
return safeCall(() => settleWorkflowReceipt(op.call(doc, {
|
|
@@ -4068,15 +4931,16 @@ function createSuperDocUI(options) {
|
|
|
4068
4931
|
target
|
|
4069
4932
|
}), failedReceipt("insert failed.")), failedReceipt("insert failed."));
|
|
4070
4933
|
};
|
|
4071
|
-
let selection
|
|
4934
|
+
let selection;
|
|
4072
4935
|
try {
|
|
4073
|
-
selection
|
|
4936
|
+
selection = currentSelection.call(doc.selection);
|
|
4074
4937
|
} catch {
|
|
4075
4938
|
return failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED");
|
|
4076
4939
|
}
|
|
4077
|
-
if (isPromiseLike(selection
|
|
4078
|
-
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);
|
|
4079
4942
|
};
|
|
4943
|
+
/** Build the shared V2-truthful callback context for a custom command / button. */
|
|
4080
4944
|
const buildCustomCommandContext = (payload, context) => ({
|
|
4081
4945
|
payload,
|
|
4082
4946
|
state,
|
|
@@ -4098,10 +4962,11 @@ function createSuperDocUI(options) {
|
|
|
4098
4962
|
const editor = getEditor();
|
|
4099
4963
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4100
4964
|
const fromConfig = normalizeDocumentMode((superdocRecord?.config)?.documentMode);
|
|
4101
|
-
const
|
|
4965
|
+
const runtimeSnapshot = safeCall(typeof superdocRecord?.getActiveRuntime === "function" ? () => {
|
|
4102
4966
|
const runtime = superdocRecord.getActiveRuntime();
|
|
4103
4967
|
return typeof runtime?.getSnapshot === "function" ? runtime.getSnapshot() : null;
|
|
4104
|
-
} : void 0, null)
|
|
4968
|
+
} : void 0, null);
|
|
4969
|
+
const fromRuntime = normalizeDocumentMode(runtimeSnapshot?.documentMode);
|
|
4105
4970
|
const fromOptions = normalizeDocumentMode((editor?.options)?.documentMode);
|
|
4106
4971
|
if (fromConfig && fromRuntime && fromConfig !== fromRuntime) return fromConfig;
|
|
4107
4972
|
return fromRuntime ?? fromConfig ?? fromOptions;
|
|
@@ -4110,15 +4975,20 @@ function createSuperDocUI(options) {
|
|
|
4110
4975
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4111
4976
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
4112
4977
|
if (resolved && typeof resolved.readOnly === "boolean") return resolved.readOnly === true;
|
|
4113
|
-
const comments
|
|
4114
|
-
return !!comments
|
|
4115
|
-
};
|
|
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
|
+
*/
|
|
4116
4986
|
const resolveIsForbidden = () => {
|
|
4117
4987
|
const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
|
|
4118
4988
|
const resolved = (superdocRecord?.interactionConfig)?.comments;
|
|
4119
4989
|
if (resolved && typeof resolved.allowResolve === "boolean") return resolved.allowResolve === false;
|
|
4120
|
-
const comments
|
|
4121
|
-
return !!comments
|
|
4990
|
+
const comments = ((superdocRecord?.config)?.modules)?.comments;
|
|
4991
|
+
return !!comments && typeof comments === "object" && comments.allowResolve === false;
|
|
4122
4992
|
};
|
|
4123
4993
|
const reviewMutationsAreReadOnly = () => readDocumentMode() === "viewing" || commentsReadOnlyForReview();
|
|
4124
4994
|
const normalizeSelectionInfo = (raw) => raw && typeof raw === "object" ? raw : null;
|
|
@@ -4152,6 +5022,7 @@ function createSuperDocUI(options) {
|
|
|
4152
5022
|
const selectionApi = getDoc()?.selection;
|
|
4153
5023
|
return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
|
|
4154
5024
|
};
|
|
5025
|
+
/** True when a host sync snapshot is a resolved collapsed caret (not a range). */
|
|
4155
5026
|
const isCollapsedCaretSnapshot = (info) => {
|
|
4156
5027
|
if (info.empty !== true) return false;
|
|
4157
5028
|
const target = info.selectionTarget;
|
|
@@ -4159,6 +5030,21 @@ function createSuperDocUI(options) {
|
|
|
4159
5030
|
const end = target?.end;
|
|
4160
5031
|
return start?.kind === "text" && end?.kind === "text" && typeof start.blockId === "string" && start.blockId === end.blockId && typeof start.offset === "number" && start.offset === end.offset;
|
|
4161
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
|
+
*/
|
|
4162
5048
|
const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
|
|
4163
5049
|
const host = getHost();
|
|
4164
5050
|
const read = host?.readLiveSelectionSyncSnapshot;
|
|
@@ -4230,15 +5116,15 @@ function createSuperDocUI(options) {
|
|
|
4230
5116
|
recompute();
|
|
4231
5117
|
return state.selection;
|
|
4232
5118
|
};
|
|
4233
|
-
const computeComments = (selection
|
|
5119
|
+
const computeComments = (selection) => {
|
|
4234
5120
|
const commentsApi = getDoc()?.comments;
|
|
4235
5121
|
const { value, status: listStatus } = readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4236
5122
|
const items = value ?? [];
|
|
4237
|
-
const activeIds = selection
|
|
5123
|
+
const activeIds = selection.activeCommentIds;
|
|
4238
5124
|
if (explicitActiveCommentId && !items.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
|
|
4239
5125
|
const activeId = explicitActiveCommentId ?? activeIds[0] ?? null;
|
|
4240
5126
|
return {
|
|
4241
|
-
status: combineStatus(listStatus, selection
|
|
5127
|
+
status: combineStatus(listStatus, selection.status),
|
|
4242
5128
|
listStatus,
|
|
4243
5129
|
items,
|
|
4244
5130
|
total: items.length,
|
|
@@ -4253,12 +5139,22 @@ function createSuperDocUI(options) {
|
|
|
4253
5139
|
const { value, status } = authoritativeTrackChangesPendingToken === token ? {
|
|
4254
5140
|
value: heldEntry?.hasSettled ? heldEntry.value : null,
|
|
4255
5141
|
status: heldEntry?.hasSettled ? "stale" : "pending"
|
|
4256
|
-
} : readAsync("trackChanges:all", token, () =>
|
|
5142
|
+
} : readAsync("trackChanges:all", token, () => {
|
|
5143
|
+
if (!tcApi?.list) return void 0;
|
|
5144
|
+
const fallback = () => tcApi.list({ in: "all" });
|
|
5145
|
+
const bridgeOwnsCatalog = typeof getV2TrackedChanges()?.listTrackedChanges === "function";
|
|
5146
|
+
const shared = runUiTrackedChangesCatalogRead(fallback);
|
|
5147
|
+
if (!bridgeOwnsCatalog) return shared;
|
|
5148
|
+
return Promise.resolve(shared).then((raw) => {
|
|
5149
|
+
const record = isLooseObject(raw) ? raw : null;
|
|
5150
|
+
return (Array.isArray(record?.items) ? record.items : []).some((item) => readEntityStory(item) == null) ? fallback() : raw;
|
|
5151
|
+
});
|
|
5152
|
+
}, (raw) => {
|
|
4257
5153
|
return (raw && Array.isArray(raw.items) ? raw.items : []).map(projectTrackChangesItem).filter((item) => item != null);
|
|
4258
5154
|
});
|
|
4259
5155
|
return status === "ready" ? value : null;
|
|
4260
5156
|
};
|
|
4261
|
-
const computeTrackChanges = (selection
|
|
5157
|
+
const computeTrackChanges = (selection) => {
|
|
4262
5158
|
const tcApi = getDoc()?.trackChanges;
|
|
4263
5159
|
const v2TrackedChanges = getV2TrackedChanges();
|
|
4264
5160
|
const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
|
|
@@ -4269,7 +5165,7 @@ function createSuperDocUI(options) {
|
|
|
4269
5165
|
value: heldEntry?.hasSettled ? heldEntry.value : null,
|
|
4270
5166
|
status: heldEntry?.hasSettled ? "stale" : "pending"
|
|
4271
5167
|
} : readAsync("trackChanges", token, () => {
|
|
4272
|
-
if (listTrackedChanges) return
|
|
5168
|
+
if (listTrackedChanges) return runUiTrackedChangesCatalogRead(() => tcApi?.list?.());
|
|
4273
5169
|
return tcApi?.list ? tcApi.list({ in: "all" }) : void 0;
|
|
4274
5170
|
}, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4275
5171
|
const items = (value ?? []).map(projectTrackChangesItem).filter((item) => item != null).filter((item) => {
|
|
@@ -4291,51 +5187,58 @@ function createSuperDocUI(options) {
|
|
|
4291
5187
|
} else if (!items.some((item) => readEntityId(item) === active.id)) setExplicitActiveChange(null);
|
|
4292
5188
|
}
|
|
4293
5189
|
const publicIdItems = allStoryItems ?? items;
|
|
4294
|
-
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection
|
|
4295
|
-
const selectionActiveChangeIds = postDecisionIds ? selection
|
|
5190
|
+
const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection));
|
|
5191
|
+
const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection.activeChangeIds.filter((id) => !postDecisionIds.has(id)) : selection.activeChangeIds;
|
|
4296
5192
|
const selectionPublicChangeIds = selectionActiveChangeIds.map((id) => selectionIdContext.toPublicId(id) ?? id);
|
|
4297
5193
|
const explicitActiveIdContext = explicitActiveChange?.story ? buildStoryScopedTrackedChangeIdContext(publicIdItems, explicitActiveChange.story) : buildTrackedChangeIdContext(publicIdItems);
|
|
4298
5194
|
const explicitActiveId = explicitActiveChange ? explicitActiveIdContext.toPublicId(explicitActiveChange.id) ?? explicitActiveChange.id : null;
|
|
4299
5195
|
const selectionActiveId = resolveSelectionActiveChangeId({
|
|
4300
5196
|
selectionIds: selectionActiveChangeIds,
|
|
4301
5197
|
publicIds: selectionPublicChangeIds,
|
|
4302
|
-
selection
|
|
5198
|
+
selection,
|
|
4303
5199
|
bodyItems: items,
|
|
4304
5200
|
allStoryItems
|
|
4305
5201
|
});
|
|
4306
5202
|
const activeId = explicitActiveId ?? selectionActiveId;
|
|
4307
5203
|
return {
|
|
4308
|
-
status: combineStatus(listStatus, selection
|
|
5204
|
+
status: combineStatus(listStatus, selection.status),
|
|
4309
5205
|
items,
|
|
4310
5206
|
total: items.length,
|
|
4311
5207
|
activeId,
|
|
4312
5208
|
authors: [...authors]
|
|
4313
5209
|
};
|
|
4314
5210
|
};
|
|
4315
|
-
const computeContentControls = (selection
|
|
5211
|
+
const computeContentControls = (selection) => {
|
|
4316
5212
|
const ccApi = getDoc()?.contentControls;
|
|
4317
5213
|
const { value, status: listStatus } = readAsync("contentControls", contentToken(), () => ccApi?.list ? ccApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
|
|
4318
5214
|
const items = value ?? [];
|
|
4319
|
-
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection
|
|
5215
|
+
const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection);
|
|
4320
5216
|
return {
|
|
4321
|
-
status: combineStatus(listStatus, rangeStatus, selection
|
|
5217
|
+
status: combineStatus(listStatus, rangeStatus, selection.status),
|
|
4322
5218
|
items,
|
|
4323
5219
|
total: items.length,
|
|
4324
5220
|
activeId: activeIds[0] ?? null,
|
|
4325
5221
|
activeIds
|
|
4326
5222
|
};
|
|
4327
5223
|
};
|
|
4328
|
-
|
|
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) => {
|
|
4329
5232
|
if (!ccApi || typeof ccApi.listInRange !== "function") return {
|
|
4330
5233
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4331
5234
|
status: "ready"
|
|
4332
5235
|
};
|
|
4333
|
-
const range = selectionBlockRange(selection
|
|
5236
|
+
const range = selectionBlockRange(selection);
|
|
4334
5237
|
if (!range) return {
|
|
4335
5238
|
lockModesById: /* @__PURE__ */ new Map(),
|
|
4336
5239
|
status: "ready"
|
|
4337
5240
|
};
|
|
4338
|
-
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 : []);
|
|
4339
5242
|
const rows = value ?? [];
|
|
4340
5243
|
const lockModesById = /* @__PURE__ */ new Map();
|
|
4341
5244
|
for (const row of rows) if (typeof row?.id === "string") lockModesById.set(row.id, typeof row.lockMode === "string" ? row.lockMode : "unlocked");
|
|
@@ -4344,8 +5247,8 @@ function createSuperDocUI(options) {
|
|
|
4344
5247
|
status
|
|
4345
5248
|
};
|
|
4346
5249
|
};
|
|
4347
|
-
const computeActiveContentControlIds = (ccApi, selection
|
|
4348
|
-
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection
|
|
5250
|
+
const computeActiveContentControlIds = (ccApi, selection) => {
|
|
5251
|
+
const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4349
5252
|
return {
|
|
4350
5253
|
ids: [...lockModesById.keys()],
|
|
4351
5254
|
status
|
|
@@ -4359,14 +5262,14 @@ function createSuperDocUI(options) {
|
|
|
4359
5262
|
};
|
|
4360
5263
|
};
|
|
4361
5264
|
const computeZoom = () => {
|
|
4362
|
-
const state
|
|
4363
|
-
const value = typeof state
|
|
4364
|
-
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;
|
|
4365
5268
|
return {
|
|
4366
5269
|
mode: rawMode === "manual" || rawMode === "fit-width" ? rawMode : rawMode === "fixed" ? "manual" : null,
|
|
4367
5270
|
value,
|
|
4368
|
-
min: typeof state
|
|
4369
|
-
max: typeof state
|
|
5271
|
+
min: typeof state?.min === "number" ? state.min : 10,
|
|
5272
|
+
max: typeof state?.max === "number" ? state.max : 100
|
|
4370
5273
|
};
|
|
4371
5274
|
};
|
|
4372
5275
|
const readMeasurementUnit = () => {
|
|
@@ -4380,12 +5283,18 @@ function createSuperDocUI(options) {
|
|
|
4380
5283
|
dirty: editor ? Boolean(editor.isDirty) : false
|
|
4381
5284
|
};
|
|
4382
5285
|
};
|
|
4383
|
-
|
|
4384
|
-
|
|
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;
|
|
4385
5294
|
if (typeof pattern !== "string" || pattern.length === 0) return null;
|
|
4386
|
-
const segments = selectionTextSegments(selection
|
|
5295
|
+
const segments = selectionTextSegments(selection);
|
|
4387
5296
|
if (segments.length === 0 || !canProbeEverySelectedBlock(segments.map((segment) => segment.blockId))) return null;
|
|
4388
|
-
const target = selection
|
|
5297
|
+
const target = selection.target;
|
|
4389
5298
|
const within = segments.length === 1 ? paragraphTarget(segments[0].blockId, target?.story) : void 0;
|
|
4390
5299
|
return {
|
|
4391
5300
|
select: {
|
|
@@ -4398,24 +5307,30 @@ function createSuperDocUI(options) {
|
|
|
4398
5307
|
require: "any"
|
|
4399
5308
|
};
|
|
4400
5309
|
};
|
|
4401
|
-
const resolveSelectionTextQueryItem = (selection
|
|
5310
|
+
const resolveSelectionTextQueryItem = (selection) => {
|
|
4402
5311
|
const query = getDoc()?.query;
|
|
4403
5312
|
if (typeof query?.match !== "function") return null;
|
|
4404
|
-
const request = selectionTextQueryRequest(selection
|
|
5313
|
+
const request = selectionTextQueryRequest(selection);
|
|
4405
5314
|
if (!request) return null;
|
|
4406
|
-
const { value } = readAsync(`query:${selectionSignature(selection
|
|
4407
|
-
return pickSelectionTextQueryItem(value, selection
|
|
4408
|
-
};
|
|
4409
|
-
|
|
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) => {
|
|
4410
5325
|
const host = getHost();
|
|
4411
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5326
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4412
5327
|
if (blockIds.size === 0) return {};
|
|
4413
5328
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4414
5329
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4415
5330
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4416
5331
|
const blocks = safeCall(() => {
|
|
4417
5332
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4418
|
-
const story = selectionStoryLocator(selection
|
|
5333
|
+
const story = selectionStoryLocator(selection);
|
|
4419
5334
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4420
5335
|
}, null);
|
|
4421
5336
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4430,7 +5345,7 @@ function createSuperDocUI(options) {
|
|
|
4430
5345
|
if (typeof run.highlight === "string" && run.highlight.trim() !== "") out.highlight = run.highlight.trim().toUpperCase();
|
|
4431
5346
|
return out;
|
|
4432
5347
|
};
|
|
4433
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5348
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4434
5349
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4435
5350
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4436
5351
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4454,7 +5369,7 @@ function createSuperDocUI(options) {
|
|
|
4454
5369
|
return runFontValues(chosen);
|
|
4455
5370
|
}
|
|
4456
5371
|
}
|
|
4457
|
-
const segments = selectionTextSegments(selection
|
|
5372
|
+
const segments = selectionTextSegments(selection);
|
|
4458
5373
|
if (segments.length > 0) {
|
|
4459
5374
|
const rangeFamilies = /* @__PURE__ */ new Set();
|
|
4460
5375
|
const rangeSizes = /* @__PURE__ */ new Set();
|
|
@@ -4484,10 +5399,10 @@ function createSuperDocUI(options) {
|
|
|
4484
5399
|
}
|
|
4485
5400
|
}
|
|
4486
5401
|
if (offsetSafe && rangeSawRun) {
|
|
4487
|
-
const projection
|
|
4488
|
-
if (rangeFamilies.size === 1) projection
|
|
4489
|
-
if (rangeSizes.size === 1) projection
|
|
4490
|
-
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;
|
|
4491
5406
|
}
|
|
4492
5407
|
}
|
|
4493
5408
|
const families = /* @__PURE__ */ new Set();
|
|
@@ -4510,16 +5425,29 @@ function createSuperDocUI(options) {
|
|
|
4510
5425
|
if (sizes.size === 1) projection.fontSize = [...sizes][0];
|
|
4511
5426
|
return projection;
|
|
4512
5427
|
};
|
|
4513
|
-
|
|
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) => {
|
|
4514
5442
|
const host = getHost();
|
|
4515
|
-
const blockIds = new Set(selectionBlockIds(selection
|
|
5443
|
+
const blockIds = new Set(selectionBlockIds(selection));
|
|
4516
5444
|
if (blockIds.size === 0) return {};
|
|
4517
5445
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
4518
5446
|
const readAll = host?.readMountedProjectionBlocks;
|
|
4519
5447
|
if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
|
|
4520
5448
|
const blocks = safeCall(() => {
|
|
4521
5449
|
if (typeof readByIds !== "function") return readAll.call(host);
|
|
4522
|
-
const story = selectionStoryLocator(selection
|
|
5450
|
+
const story = selectionStoryLocator(selection);
|
|
4523
5451
|
return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
|
|
4524
5452
|
}, null);
|
|
4525
5453
|
if (!Array.isArray(blocks)) return {};
|
|
@@ -4538,7 +5466,7 @@ function createSuperDocUI(options) {
|
|
|
4538
5466
|
}
|
|
4539
5467
|
return out;
|
|
4540
5468
|
};
|
|
4541
|
-
const caret = collapsedTextAddressFromSelection(selection
|
|
5469
|
+
const caret = collapsedTextAddressFromSelection(selection);
|
|
4542
5470
|
const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
|
|
4543
5471
|
if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
|
|
4544
5472
|
const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
|
|
@@ -4562,7 +5490,7 @@ function createSuperDocUI(options) {
|
|
|
4562
5490
|
return runMarkValues(chosen);
|
|
4563
5491
|
}
|
|
4564
5492
|
}
|
|
4565
|
-
const segments = selectionTextSegments(selection
|
|
5493
|
+
const segments = selectionTextSegments(selection);
|
|
4566
5494
|
if (segments.length > 0) {
|
|
4567
5495
|
const rangeSets = {
|
|
4568
5496
|
bold: /* @__PURE__ */ new Set(),
|
|
@@ -4616,12 +5544,12 @@ function createSuperDocUI(options) {
|
|
|
4616
5544
|
if (!sawWholeRun) return {};
|
|
4617
5545
|
return collapseMarkSets(wholeSets);
|
|
4618
5546
|
};
|
|
4619
|
-
const completeProjectedInlineValues = (selection
|
|
5547
|
+
const completeProjectedInlineValues = (selection, direct) => {
|
|
4620
5548
|
if (direct.fontFamily !== void 0 && direct.fontSize !== void 0 && direct.color !== void 0 && direct.highlight !== void 0) return {
|
|
4621
5549
|
values: direct,
|
|
4622
5550
|
effectiveUniformityStatus: "ready"
|
|
4623
5551
|
};
|
|
4624
|
-
const resolved = resolveEffectiveInlineValuesFromLayout(selection
|
|
5552
|
+
const resolved = resolveEffectiveInlineValuesFromLayout(selection);
|
|
4625
5553
|
const combined = {
|
|
4626
5554
|
...resolved.fontFamily !== void 0 ? { fontFamily: resolved.fontFamily } : {},
|
|
4627
5555
|
...resolved.fontSize !== void 0 ? { fontSize: resolved.fontSize } : {},
|
|
@@ -4629,8 +5557,8 @@ function createSuperDocUI(options) {
|
|
|
4629
5557
|
...resolved.highlight !== void 0 ? { highlight: resolved.highlight } : {},
|
|
4630
5558
|
...direct
|
|
4631
5559
|
};
|
|
4632
|
-
if (!selection
|
|
4633
|
-
const uniformity = resolveEffectiveInlineUniformityValues(selection
|
|
5560
|
+
if (!selection.empty && (combined.fontFamily === void 0 || combined.fontSize === void 0)) {
|
|
5561
|
+
const uniformity = resolveEffectiveInlineUniformityValues(selection);
|
|
4634
5562
|
if (uniformity.values) {
|
|
4635
5563
|
if (combined.fontFamily === void 0 && uniformity.values.fontFamily !== void 0) combined.fontFamily = uniformity.values.fontFamily;
|
|
4636
5564
|
if (combined.fontSize === void 0 && uniformity.values.fontSize !== void 0) combined.fontSize = uniformity.values.fontSize;
|
|
@@ -4645,6 +5573,13 @@ function createSuperDocUI(options) {
|
|
|
4645
5573
|
effectiveUniformityStatus: "ready"
|
|
4646
5574
|
};
|
|
4647
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
|
+
*/
|
|
4648
5583
|
const EFFECTIVE_INLINE_UNIFORMITY_KEYS = [
|
|
4649
5584
|
"fontFamily",
|
|
4650
5585
|
"fontSize",
|
|
@@ -4653,8 +5588,8 @@ function createSuperDocUI(options) {
|
|
|
4653
5588
|
"underline",
|
|
4654
5589
|
"strikethrough"
|
|
4655
5590
|
];
|
|
4656
|
-
const readEffectiveInlineUniformityCached = (selection
|
|
4657
|
-
const target = selection
|
|
5591
|
+
const readEffectiveInlineUniformityCached = (selection) => {
|
|
5592
|
+
const target = selection.selectionTarget;
|
|
4658
5593
|
if (!target) return {
|
|
4659
5594
|
value: null,
|
|
4660
5595
|
status: "ready"
|
|
@@ -4664,14 +5599,15 @@ function createSuperDocUI(options) {
|
|
|
4664
5599
|
value: null,
|
|
4665
5600
|
status: "ready"
|
|
4666
5601
|
};
|
|
4667
|
-
|
|
5602
|
+
const signature = selectionEffectiveUniformitySignature(selection) ?? selectionSignature(selection);
|
|
5603
|
+
return readAsync(`effInline:${signature}`, contentToken(), () => op({
|
|
4668
5604
|
target,
|
|
4669
5605
|
offsetSpace: "selection",
|
|
4670
5606
|
keys: EFFECTIVE_INLINE_UNIFORMITY_KEYS
|
|
4671
5607
|
}), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4672
5608
|
};
|
|
4673
|
-
const resolveEffectiveInlineUniformityValues = (selection
|
|
4674
|
-
const { value, status } = readEffectiveInlineUniformityCached(selection
|
|
5609
|
+
const resolveEffectiveInlineUniformityValues = (selection) => {
|
|
5610
|
+
const { value, status } = readEffectiveInlineUniformityCached(selection);
|
|
4675
5611
|
if (!value || value.success !== true) return {
|
|
4676
5612
|
values: null,
|
|
4677
5613
|
status
|
|
@@ -4690,8 +5626,9 @@ function createSuperDocUI(options) {
|
|
|
4690
5626
|
status
|
|
4691
5627
|
};
|
|
4692
5628
|
};
|
|
4693
|
-
|
|
4694
|
-
|
|
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);
|
|
4695
5632
|
if (!value || value.success !== true) return {
|
|
4696
5633
|
values: null,
|
|
4697
5634
|
status
|
|
@@ -4707,18 +5644,25 @@ function createSuperDocUI(options) {
|
|
|
4707
5644
|
status
|
|
4708
5645
|
};
|
|
4709
5646
|
};
|
|
4710
|
-
|
|
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) => {
|
|
4711
5655
|
const mark = descriptor.activeMark;
|
|
4712
5656
|
if (!mark || !EFFECTIVE_MARK_KEYS.includes(mark)) return null;
|
|
4713
5657
|
const key = mark;
|
|
4714
|
-
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection
|
|
5658
|
+
const fromLayout = resolveEffectiveMarkValuesFromLayout(selection)[key];
|
|
4715
5659
|
if (fromLayout !== void 0) return fromLayout;
|
|
4716
|
-
const { values } = resolveEffectiveMarkUniformityValues(selection
|
|
5660
|
+
const { values } = resolveEffectiveMarkUniformityValues(selection);
|
|
4717
5661
|
const fromWorker = values?.[key];
|
|
4718
5662
|
return fromWorker !== void 0 ? fromWorker : null;
|
|
4719
5663
|
};
|
|
4720
|
-
const readEffectiveInlineUniformityNow = async (selection
|
|
4721
|
-
const target = selection
|
|
5664
|
+
const readEffectiveInlineUniformityNow = async (selection) => {
|
|
5665
|
+
const target = selection.selectionTarget;
|
|
4722
5666
|
if (!target) return null;
|
|
4723
5667
|
const op = resolveDocOperation(getDoc(), "format.readEffectiveInlineUniformity");
|
|
4724
5668
|
if (!op) return null;
|
|
@@ -4741,23 +5685,30 @@ function createSuperDocUI(options) {
|
|
|
4741
5685
|
else delete projected[key];
|
|
4742
5686
|
}
|
|
4743
5687
|
};
|
|
4744
|
-
const projectSelectionInlineValuesWithStatus = (selection
|
|
4745
|
-
const projectSelectionInlineValues = (selection
|
|
4746
|
-
|
|
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) => {
|
|
4747
5698
|
const query = getDoc()?.query;
|
|
4748
|
-
const request = selectionTextQueryRequest(selection
|
|
4749
|
-
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection
|
|
5699
|
+
const request = selectionTextQueryRequest(selection);
|
|
5700
|
+
if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection);
|
|
4750
5701
|
try {
|
|
4751
5702
|
const raw = await Promise.resolve(query.match(request));
|
|
4752
|
-
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection
|
|
4753
|
-
const completed = completeProjectedInlineValues(selection
|
|
5703
|
+
const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection), selection);
|
|
5704
|
+
const completed = completeProjectedInlineValues(selection, direct);
|
|
4754
5705
|
if (completed.effectiveUniformityStatus !== "ready" && (direct.fontFamily === void 0 || direct.fontSize === void 0)) {
|
|
4755
|
-
const effective = await readEffectiveInlineUniformityNow(selection
|
|
5706
|
+
const effective = await readEffectiveInlineUniformityNow(selection);
|
|
4756
5707
|
if (effective) applyEffectiveInlineUniformity(completed.values, direct, effective);
|
|
4757
5708
|
}
|
|
4758
5709
|
return completed.values;
|
|
4759
5710
|
} catch {
|
|
4760
|
-
return projectSelectionInlineValues(selection
|
|
5711
|
+
return projectSelectionInlineValues(selection);
|
|
4761
5712
|
}
|
|
4762
5713
|
};
|
|
4763
5714
|
const readBlockNode = async (address) => {
|
|
@@ -4769,21 +5720,35 @@ function createSuperDocUI(options) {
|
|
|
4769
5720
|
nodeType: address["nodeType"]
|
|
4770
5721
|
}));
|
|
4771
5722
|
};
|
|
4772
|
-
|
|
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) => {
|
|
4773
5729
|
const query = getDoc()?.query;
|
|
4774
5730
|
if (typeof query?.match !== "function") return "ready";
|
|
4775
|
-
const request = selectionTextQueryRequest(selection
|
|
5731
|
+
const request = selectionTextQueryRequest(selection);
|
|
4776
5732
|
if (!request) return "ready";
|
|
4777
|
-
const { status } = readAsync(`query:${selectionSignature(selection
|
|
5733
|
+
const { status } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
4778
5734
|
return status;
|
|
4779
5735
|
};
|
|
4780
|
-
|
|
4781
|
-
|
|
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);
|
|
4782
5747
|
const projected = projection.values;
|
|
4783
|
-
if (selection
|
|
4784
|
-
const signature = selectionInlineValueSignature(selection
|
|
5748
|
+
if (selection.empty) return projection;
|
|
5749
|
+
const signature = selectionInlineValueSignature(selection);
|
|
4785
5750
|
if (!signature) return projection;
|
|
4786
|
-
if (selection
|
|
5751
|
+
if (selection.status === "ready" && selectionQueryStatus(selection) === "ready" && projection.effectiveUniformityStatus === "ready") {
|
|
4787
5752
|
heldSettledInlineValues = {
|
|
4788
5753
|
key: signature,
|
|
4789
5754
|
values: projected
|
|
@@ -4799,39 +5764,56 @@ function createSuperDocUI(options) {
|
|
|
4799
5764
|
};
|
|
4800
5765
|
return projection;
|
|
4801
5766
|
};
|
|
4802
|
-
const computeCommandStates = (selection
|
|
5767
|
+
const computeCommandStates = (selection) => {
|
|
4803
5768
|
const doc = getDoc();
|
|
4804
|
-
const projectedInlineRead = projectInlineValuesWithSettledHold(selection
|
|
5769
|
+
const projectedInlineRead = projectInlineValuesWithSettledHold(selection);
|
|
4805
5770
|
const projectedInlineValues = projectedInlineRead.values;
|
|
4806
|
-
if (selection
|
|
5771
|
+
if (selection.status === "ready" && !selection.empty && selectionQueryStatus(selection) === "ready" && projectedInlineRead.effectiveUniformityStatus === "ready") {
|
|
4807
5772
|
frozenProjectedValues = projectedInlineValues;
|
|
4808
|
-
frozenProjectedValuesKey = `${selectionKey(selection
|
|
5773
|
+
frozenProjectedValuesKey = `${selectionKey(selection)}:${contentToken()}`;
|
|
4809
5774
|
}
|
|
4810
5775
|
const ccApi = doc?.contentControls;
|
|
4811
|
-
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection
|
|
5776
|
+
const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection);
|
|
4812
5777
|
const states = {};
|
|
4813
|
-
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);
|
|
4814
5779
|
return states;
|
|
4815
5780
|
};
|
|
5781
|
+
/** Stable reason a routed command cannot reach its Document API operation. */
|
|
4816
5782
|
const unavailableRouteReason = (doc) => {
|
|
4817
5783
|
if (doc) return SUPERDOC_UI_REASONS.operationUnavailable;
|
|
4818
5784
|
return getEditor() ? SUPERDOC_UI_REASONS.documentApiUnavailable : SUPERDOC_UI_REASONS.notReady;
|
|
4819
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
|
+
*/
|
|
4820
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
|
+
*/
|
|
4821
5802
|
const contentControlLockReason = (lockModesById) => {
|
|
4822
5803
|
for (const lockMode of lockModesById.values()) if (blocksContentStyling(lockMode)) return SUPERDOC_UI_REASONS.contentControlLocked;
|
|
4823
5804
|
};
|
|
4824
|
-
|
|
5805
|
+
/** Stable reason a tracked-change decision command is disabled, or undefined when enabled. */
|
|
5806
|
+
const trackDecisionReason = (command, supported, readonly, selection, doc) => {
|
|
4825
5807
|
if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
|
|
4826
5808
|
if (readonly) return SUPERDOC_UI_REASONS.documentReadonly;
|
|
4827
|
-
if (command.scope !== "all" && selection
|
|
5809
|
+
if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
|
|
4828
5810
|
};
|
|
4829
5811
|
const hostCommandSupport = (command) => {
|
|
4830
5812
|
const host = getHost();
|
|
4831
|
-
const commands
|
|
4832
|
-
if (Array.isArray(commands
|
|
4833
|
-
if (commands
|
|
4834
|
-
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];
|
|
4835
5817
|
return record && typeof record === "object" ? record : null;
|
|
4836
5818
|
}
|
|
4837
5819
|
return null;
|
|
@@ -4843,7 +5825,7 @@ function createSuperDocUI(options) {
|
|
|
4843
5825
|
if (support && (support.status === "supported" || support.enabled === true)) return void 0;
|
|
4844
5826
|
return SUPERDOC_UI_REASONS.bulkDecisionsDisabled;
|
|
4845
5827
|
};
|
|
4846
|
-
const computeCommandState = (id, doc, selection
|
|
5828
|
+
const computeCommandState = (id, doc, selection, projectedInlineValues, lockModesById = /* @__PURE__ */ new Map()) => {
|
|
4847
5829
|
if (customCommands.has(id)) return normalizeCommandState({
|
|
4848
5830
|
supported: true,
|
|
4849
5831
|
enabled: true,
|
|
@@ -4870,7 +5852,7 @@ function createSuperDocUI(options) {
|
|
|
4870
5852
|
const supportsSingle = typeof tcApi?.decide === "function" || typeof tcApi?.[trackCommand.kind] === "function";
|
|
4871
5853
|
const supportsAll = typeof tcApi?.decide === "function" || typeof tcApi?.[`${trackCommand.kind}All`] === "function";
|
|
4872
5854
|
const supported = trackCommand.scope === "all" ? supportsAll : supportsSingle;
|
|
4873
|
-
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection
|
|
5855
|
+
const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection, doc);
|
|
4874
5856
|
return normalizeCommandState({
|
|
4875
5857
|
enabled: reason == null,
|
|
4876
5858
|
active: false,
|
|
@@ -4925,7 +5907,7 @@ function createSuperDocUI(options) {
|
|
|
4925
5907
|
supported: true,
|
|
4926
5908
|
reason: lockReason
|
|
4927
5909
|
}, "builtin");
|
|
4928
|
-
const enabled = selectionBlockIds(selection
|
|
5910
|
+
const enabled = selectionBlockIds(selection).length > 0 || resolveInlineSelectionTarget(selection) != null;
|
|
4929
5911
|
return normalizeCommandState({
|
|
4930
5912
|
enabled,
|
|
4931
5913
|
active: false,
|
|
@@ -4968,10 +5950,10 @@ function createSuperDocUI(options) {
|
|
|
4968
5950
|
reason
|
|
4969
5951
|
}, "builtin");
|
|
4970
5952
|
}
|
|
4971
|
-
const active = commandActiveState(descriptor, doc, selection
|
|
4972
|
-
const value = routedCommandValue(descriptor, doc, selection
|
|
4973
|
-
if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection
|
|
4974
|
-
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);
|
|
4975
5957
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
4976
5958
|
enabled: false,
|
|
4977
5959
|
active,
|
|
@@ -4979,7 +5961,7 @@ function createSuperDocUI(options) {
|
|
|
4979
5961
|
value,
|
|
4980
5962
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
4981
5963
|
}, "builtin");
|
|
4982
|
-
if (descriptor.inline && !resolveInlineSelectionTarget(selection
|
|
5964
|
+
if (descriptor.inline && !resolveInlineSelectionTarget(selection)) {
|
|
4983
5965
|
const lockReason = contentControlLockReason(lockModesById);
|
|
4984
5966
|
if (lockReason) return normalizeCommandState({
|
|
4985
5967
|
enabled: false,
|
|
@@ -4988,7 +5970,7 @@ function createSuperDocUI(options) {
|
|
|
4988
5970
|
value,
|
|
4989
5971
|
reason: lockReason
|
|
4990
5972
|
}, "builtin");
|
|
4991
|
-
return normalizeCommandState(selectionBlockIds(selection
|
|
5973
|
+
return normalizeCommandState(selectionBlockIds(selection).length > 0 && typeof getHost()?.setPendingInlineFormat === "function" ? {
|
|
4992
5974
|
enabled: true,
|
|
4993
5975
|
active,
|
|
4994
5976
|
supported: true,
|
|
@@ -5001,14 +5983,14 @@ function createSuperDocUI(options) {
|
|
|
5001
5983
|
reason: SUPERDOC_UI_REASONS.rangeSelectionRequired
|
|
5002
5984
|
}, "builtin");
|
|
5003
5985
|
}
|
|
5004
|
-
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection
|
|
5986
|
+
if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection).length === 0) return normalizeCommandState({
|
|
5005
5987
|
enabled: false,
|
|
5006
5988
|
active,
|
|
5007
5989
|
supported: true,
|
|
5008
5990
|
value,
|
|
5009
5991
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
5010
5992
|
}, "builtin");
|
|
5011
|
-
if (descriptor.link && !active && selectionTextAddresses(selection
|
|
5993
|
+
if (descriptor.link && !active && selectionTextAddresses(selection).length === 0 && !collapsedTextAddressFromSelection(selection)) return normalizeCommandState({
|
|
5012
5994
|
enabled: false,
|
|
5013
5995
|
active,
|
|
5014
5996
|
supported: true,
|
|
@@ -5031,7 +6013,7 @@ function createSuperDocUI(options) {
|
|
|
5031
6013
|
value
|
|
5032
6014
|
}, "builtin");
|
|
5033
6015
|
};
|
|
5034
|
-
function hyperlinkOverlapsSelection(link, selection
|
|
6016
|
+
function hyperlinkOverlapsSelection(link, selection) {
|
|
5035
6017
|
const anchor = link.address?.anchor;
|
|
5036
6018
|
const start = anchor?.start;
|
|
5037
6019
|
const end = anchor?.end;
|
|
@@ -5041,7 +6023,7 @@ function createSuperDocUI(options) {
|
|
|
5041
6023
|
const linkStart = typeof start?.offset === "number" ? start.offset : null;
|
|
5042
6024
|
const linkEnd = typeof end?.offset === "number" ? end.offset : null;
|
|
5043
6025
|
if (linkStart == null || linkEnd == null) return false;
|
|
5044
|
-
const target = selection
|
|
6026
|
+
const target = selection.target;
|
|
5045
6027
|
const segments = target && Array.isArray(target.segments) ? target.segments : [];
|
|
5046
6028
|
for (const segment of segments) {
|
|
5047
6029
|
const segmentBlockId = typeof segment?.blockId === "string" ? segment.blockId : null;
|
|
@@ -5064,10 +6046,15 @@ function createSuperDocUI(options) {
|
|
|
5064
6046
|
}
|
|
5065
6047
|
return false;
|
|
5066
6048
|
}
|
|
5067
|
-
|
|
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) => {
|
|
5068
6055
|
const linksApi = doc?.hyperlinks;
|
|
5069
6056
|
if (!linksApi || typeof linksApi.list !== "function") return null;
|
|
5070
|
-
const blockIds = selectionBlockIds(selection
|
|
6057
|
+
const blockIds = selectionBlockIds(selection);
|
|
5071
6058
|
if (!canProbeEverySelectedBlock(blockIds)) return null;
|
|
5072
6059
|
for (const blockId of blockIds) {
|
|
5073
6060
|
const within = {
|
|
@@ -5076,44 +6063,52 @@ function createSuperDocUI(options) {
|
|
|
5076
6063
|
nodeId: blockId
|
|
5077
6064
|
};
|
|
5078
6065
|
const { value: result } = readAsync(`hyperlinks:${blockId}`, contentToken(), () => linksApi.list({ within }), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5079
|
-
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));
|
|
5080
6067
|
if (hit) return hit;
|
|
5081
6068
|
}
|
|
5082
6069
|
return null;
|
|
5083
6070
|
};
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
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;
|
|
5087
6080
|
const pending = pendingInlineActive(descriptor);
|
|
5088
6081
|
if (pending !== null) return pending;
|
|
5089
6082
|
const optimistic = optimisticInlineToggles.get(descriptor.id);
|
|
5090
|
-
const selectionSignature
|
|
5091
|
-
if (selectionSignature
|
|
5092
|
-
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);
|
|
5093
6086
|
if (effective !== null) return effective;
|
|
5094
|
-
return commandIsActive(descriptor, selection
|
|
6087
|
+
return commandIsActive(descriptor, selection);
|
|
5095
6088
|
};
|
|
5096
|
-
|
|
6089
|
+
/** Live `value` for a routed command, when modeled (current paragraph style / link href). */
|
|
6090
|
+
const routedCommandValue = (descriptor, doc, selection, projectedInlineValues) => {
|
|
5097
6091
|
if (descriptor.inline && isProjectedInlineSelectionValueKey(descriptor.inline.key)) {
|
|
5098
6092
|
const pending = pendingInlineValueFor(descriptor);
|
|
5099
6093
|
if (pending !== void 0) return pending;
|
|
5100
6094
|
const projected = projectedInlineValues[descriptor.inline.key];
|
|
5101
6095
|
if (projected !== void 0) return projected;
|
|
5102
|
-
const signature = selectionInlineValueSignature(selection
|
|
6096
|
+
const signature = selectionInlineValueSignature(selection);
|
|
5103
6097
|
const cached = signature ? optimisticInlineValues.get(descriptor.inline.key) : void 0;
|
|
5104
6098
|
if (signature && cached?.selectionSignature === signature) return cached.value;
|
|
5105
6099
|
}
|
|
5106
6100
|
if (descriptor.id === "linked-style") {
|
|
5107
|
-
const { style: active } = computeActiveParagraphStyle(selection
|
|
6101
|
+
const { style: active } = computeActiveParagraphStyle(selection, getStyleCatalog().cache);
|
|
5108
6102
|
if (!active.styleId || active.mixed) return void 0;
|
|
5109
6103
|
return {
|
|
5110
6104
|
styleId: active.styleId,
|
|
5111
6105
|
styleName: active.styleName
|
|
5112
6106
|
};
|
|
5113
6107
|
}
|
|
5114
|
-
if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection
|
|
5115
|
-
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;
|
|
5116
6110
|
};
|
|
6111
|
+
/** Read one block's list state and cache status through the selected story's list-state seam. */
|
|
5117
6112
|
const readListStateSnapshotForBlock = (doc, blockId, story) => {
|
|
5118
6113
|
const listsApi = doc?.lists;
|
|
5119
6114
|
if (!listsApi) return {
|
|
@@ -5135,14 +6130,22 @@ function createSuperDocUI(options) {
|
|
|
5135
6130
|
status
|
|
5136
6131
|
};
|
|
5137
6132
|
};
|
|
6133
|
+
/** Read one block's settled list state through the selected story's list-state seam. */
|
|
5138
6134
|
const readListStateForBlock = (doc, blockId, story) => {
|
|
5139
6135
|
return readListStateSnapshotForBlock(doc, blockId, story).value;
|
|
5140
6136
|
};
|
|
6137
|
+
/** Read one block's list seed (`'bullet'` / `'ordered'`) in the selected story. */
|
|
5141
6138
|
const readListSeedForBlock = (doc, blockId, story) => {
|
|
5142
6139
|
const result = readListStateForBlock(doc, blockId, story);
|
|
5143
6140
|
if (!result || result.isListItem !== true) return null;
|
|
5144
6141
|
return result.seed === "bullet" || result.seed === "ordered" ? result.seed : null;
|
|
5145
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
|
+
*/
|
|
5146
6149
|
const readListMembershipSnapshotForBlock = (doc, blockId, story) => {
|
|
5147
6150
|
const snapshot = readListStateSnapshotForBlock(doc, blockId, story);
|
|
5148
6151
|
const result = snapshot.value;
|
|
@@ -5151,15 +6154,17 @@ function createSuperDocUI(options) {
|
|
|
5151
6154
|
status: snapshot.status
|
|
5152
6155
|
};
|
|
5153
6156
|
};
|
|
5154
|
-
|
|
5155
|
-
|
|
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);
|
|
5156
6160
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return null;
|
|
5157
|
-
const story = selectionStory(selection
|
|
6161
|
+
const story = selectionStory(selection);
|
|
5158
6162
|
const firstSeed = readListSeedForBlock(doc, blockIds[0], story);
|
|
5159
6163
|
if (firstSeed !== "bullet" && firstSeed !== "ordered") return null;
|
|
5160
6164
|
for (const blockId of blockIds.slice(1)) if (readListSeedForBlock(doc, blockId, story) !== firstSeed) return null;
|
|
5161
6165
|
return firstSeed;
|
|
5162
6166
|
};
|
|
6167
|
+
/** Read a paragraph node in its story through the async read coordinator. */
|
|
5163
6168
|
const readNodeById = (doc, blockId, story) => {
|
|
5164
6169
|
if (!doc) return {
|
|
5165
6170
|
value: null,
|
|
@@ -5196,11 +6201,12 @@ function createSuperDocUI(options) {
|
|
|
5196
6201
|
const isProjectionResolvedParagraphAlignment = (value) => {
|
|
5197
6202
|
return value === "start" || value === "end" || value === "distributed" || value === "numTab" || value === "lowKashida" || value === "mediumKashida" || value === "highKashida" || value === "thaiDistribute";
|
|
5198
6203
|
};
|
|
5199
|
-
|
|
6204
|
+
/** Read effective paragraph alignments from the mounted, style-resolved layout. */
|
|
6205
|
+
const readEffectiveParagraphAlignments = (selection, blockIds) => {
|
|
5200
6206
|
const host = getHost();
|
|
5201
6207
|
const readByIds = host?.readMountedProjectionBlocksByIds;
|
|
5202
6208
|
if (typeof readByIds !== "function") return null;
|
|
5203
|
-
const story = selectionStoryLocator(selection
|
|
6209
|
+
const story = selectionStoryLocator(selection);
|
|
5204
6210
|
const blocks = safeCall(() => story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]), null);
|
|
5205
6211
|
if (!Array.isArray(blocks)) return /* @__PURE__ */ new Map();
|
|
5206
6212
|
const projectionBlocks = collectProjectionTextBlocks(blocks);
|
|
@@ -5215,6 +6221,7 @@ function createSuperDocUI(options) {
|
|
|
5215
6221
|
}
|
|
5216
6222
|
return alignments;
|
|
5217
6223
|
};
|
|
6224
|
+
/** Read public paragraph alignment, then fall back to the resolved projection. */
|
|
5218
6225
|
const readParagraphAlignment = (doc, blockId, story, effectiveAlignments) => {
|
|
5219
6226
|
const { value: result, status, refreshing } = readNodeById(doc, blockId, story);
|
|
5220
6227
|
const effective = effectiveAlignments?.get(blockId);
|
|
@@ -5251,11 +6258,12 @@ function createSuperDocUI(options) {
|
|
|
5251
6258
|
};
|
|
5252
6259
|
return { status: "unavailable" };
|
|
5253
6260
|
};
|
|
5254
|
-
|
|
5255
|
-
|
|
6261
|
+
/** Resolve a uniform effective alignment across every selected paragraph. */
|
|
6262
|
+
const readUniformParagraphAlignment = (doc, selection) => {
|
|
6263
|
+
const blockIds = selectionBlockIds(selection);
|
|
5256
6264
|
if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return { status: "unavailable" };
|
|
5257
|
-
const story = selectionStory(selection
|
|
5258
|
-
const effectiveAlignments = readEffectiveParagraphAlignments(selection
|
|
6265
|
+
const story = selectionStory(selection);
|
|
6266
|
+
const effectiveAlignments = readEffectiveParagraphAlignments(selection, blockIds);
|
|
5259
6267
|
const first = readParagraphAlignment(doc, blockIds[0], story, effectiveAlignments);
|
|
5260
6268
|
if (first.status !== "uniform") return first;
|
|
5261
6269
|
for (const blockId of blockIds.slice(1)) {
|
|
@@ -5265,22 +6273,23 @@ function createSuperDocUI(options) {
|
|
|
5265
6273
|
}
|
|
5266
6274
|
return first;
|
|
5267
6275
|
};
|
|
5268
|
-
const paragraphAlignmentSelectionSignature = (selection
|
|
5269
|
-
const blockIds = selectionBlockIds(selection
|
|
6276
|
+
const paragraphAlignmentSelectionSignature = (selection) => {
|
|
6277
|
+
const blockIds = selectionBlockIds(selection);
|
|
5270
6278
|
if (blockIds.length === 0) return null;
|
|
5271
|
-
return `${storyLocatorSignature(selectionStory(selection
|
|
6279
|
+
return `${storyLocatorSignature(selectionStory(selection))}:${blockIds.join(",")}`;
|
|
5272
6280
|
};
|
|
5273
|
-
|
|
5274
|
-
|
|
6281
|
+
/** Keep a toolbar pick stable until its painted paragraph state is readable. */
|
|
6282
|
+
const readToolbarParagraphAlignment = (doc, selection) => {
|
|
6283
|
+
const resolution = readUniformParagraphAlignment(doc, selection);
|
|
5275
6284
|
const resolved = resolution.status === "uniform" ? resolution.value : void 0;
|
|
5276
6285
|
const optimistic = optimisticParagraphAlignment;
|
|
5277
6286
|
if (!optimistic) return resolved;
|
|
5278
|
-
const selectionSignature
|
|
5279
|
-
if (selectionSignature
|
|
6287
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6288
|
+
if (selectionSignature && selectionSignature !== optimistic.selectionSignature) {
|
|
5280
6289
|
optimisticParagraphAlignment = null;
|
|
5281
6290
|
return resolved;
|
|
5282
6291
|
}
|
|
5283
|
-
if (!selectionSignature
|
|
6292
|
+
if (!selectionSignature) return optimistic.value;
|
|
5284
6293
|
if (!optimistic.settled) return optimistic.value;
|
|
5285
6294
|
if (resolution.status !== "pending") {
|
|
5286
6295
|
optimisticParagraphAlignment = null;
|
|
@@ -5288,14 +6297,14 @@ function createSuperDocUI(options) {
|
|
|
5288
6297
|
}
|
|
5289
6298
|
return optimistic.value;
|
|
5290
6299
|
};
|
|
5291
|
-
const armOptimisticParagraphAlignment = (selection
|
|
6300
|
+
const armOptimisticParagraphAlignment = (selection, payload) => {
|
|
5292
6301
|
const alignment = normalizeParagraphAlignment(payload);
|
|
5293
|
-
const selectionSignature
|
|
5294
|
-
const blockIds = selectionBlockIds(selection
|
|
5295
|
-
if (!alignment || !selectionSignature
|
|
6302
|
+
const selectionSignature = paragraphAlignmentSelectionSignature(selection);
|
|
6303
|
+
const blockIds = selectionBlockIds(selection);
|
|
6304
|
+
if (!alignment || !selectionSignature || blockIds.length === 0) return null;
|
|
5296
6305
|
const generation = ++optimisticParagraphAlignmentGeneration;
|
|
5297
6306
|
optimisticParagraphAlignment = {
|
|
5298
|
-
selectionSignature
|
|
6307
|
+
selectionSignature,
|
|
5299
6308
|
value: alignment,
|
|
5300
6309
|
generation,
|
|
5301
6310
|
settled: false,
|
|
@@ -5318,6 +6327,16 @@ function createSuperDocUI(options) {
|
|
|
5318
6327
|
}
|
|
5319
6328
|
optimistic.settled = true;
|
|
5320
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
|
+
*/
|
|
5321
6340
|
const resolveListMembershipForBlockAsync = async (doc, blockId, story) => {
|
|
5322
6341
|
const listsApi = doc?.lists;
|
|
5323
6342
|
if (!listsApi) return null;
|
|
@@ -5334,6 +6353,7 @@ function createSuperDocUI(options) {
|
|
|
5334
6353
|
return null;
|
|
5335
6354
|
}
|
|
5336
6355
|
};
|
|
6356
|
+
/** Read a list seed directly for mutation planning instead of trusting the reactive cache. */
|
|
5337
6357
|
const resolveListSeedForBlock = (doc, blockId, story) => {
|
|
5338
6358
|
const unavailable = {
|
|
5339
6359
|
resolved: false,
|
|
@@ -5362,6 +6382,7 @@ function createSuperDocUI(options) {
|
|
|
5362
6382
|
return unavailable;
|
|
5363
6383
|
}
|
|
5364
6384
|
};
|
|
6385
|
+
/** Await a block's current paragraph indentation authoritatively. */
|
|
5365
6386
|
const resolveParagraphIndentationForBlockAsync = async (doc, blockId, story) => {
|
|
5366
6387
|
if (!doc) return null;
|
|
5367
6388
|
try {
|
|
@@ -5375,7 +6396,7 @@ function createSuperDocUI(options) {
|
|
|
5375
6396
|
return null;
|
|
5376
6397
|
}
|
|
5377
6398
|
};
|
|
5378
|
-
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection
|
|
6399
|
+
const computeHybridIndentCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5379
6400
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5380
6401
|
enabled: false,
|
|
5381
6402
|
active,
|
|
@@ -5383,7 +6404,7 @@ function createSuperDocUI(options) {
|
|
|
5383
6404
|
value,
|
|
5384
6405
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5385
6406
|
}, "builtin");
|
|
5386
|
-
const blockIds = selectionBlockIds(selection
|
|
6407
|
+
const blockIds = selectionBlockIds(selection);
|
|
5387
6408
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5388
6409
|
enabled: false,
|
|
5389
6410
|
active,
|
|
@@ -5396,7 +6417,7 @@ function createSuperDocUI(options) {
|
|
|
5396
6417
|
const paragraphClearIndent = resolveDocOperation(doc, "format.paragraph.clearIndentation");
|
|
5397
6418
|
const mode = descriptor.list?.mode;
|
|
5398
6419
|
const paragraphOpAvailable = mode === "indent" ? paragraphSetIndent != null : paragraphSetIndent != null || paragraphClearIndent != null;
|
|
5399
|
-
const story = selectionStory(selection
|
|
6420
|
+
const story = selectionStory(selection);
|
|
5400
6421
|
let listMembership = null;
|
|
5401
6422
|
if (story.storyType !== "body") {
|
|
5402
6423
|
listMembership = [];
|
|
@@ -5480,7 +6501,7 @@ function createSuperDocUI(options) {
|
|
|
5480
6501
|
reason: unavailableRouteReason(doc)
|
|
5481
6502
|
}, "builtin");
|
|
5482
6503
|
};
|
|
5483
|
-
const computeListToggleCommandState = (descriptor, doc, readonly, selection
|
|
6504
|
+
const computeListToggleCommandState = (descriptor, doc, readonly, selection, active, value) => {
|
|
5484
6505
|
if (descriptor.mutates && readonly) return normalizeCommandState({
|
|
5485
6506
|
enabled: false,
|
|
5486
6507
|
active,
|
|
@@ -5488,7 +6509,7 @@ function createSuperDocUI(options) {
|
|
|
5488
6509
|
value,
|
|
5489
6510
|
reason: SUPERDOC_UI_REASONS.documentReadonly
|
|
5490
6511
|
}, "builtin");
|
|
5491
|
-
const blockIds = selectionBlockIds(selection
|
|
6512
|
+
const blockIds = selectionBlockIds(selection);
|
|
5492
6513
|
if (blockIds.length === 0) return normalizeCommandState({
|
|
5493
6514
|
enabled: false,
|
|
5494
6515
|
active,
|
|
@@ -5496,7 +6517,7 @@ function createSuperDocUI(options) {
|
|
|
5496
6517
|
value,
|
|
5497
6518
|
reason: SUPERDOC_UI_REASONS.selectionRequired
|
|
5498
6519
|
}, "builtin");
|
|
5499
|
-
const story = selectionStory(selection
|
|
6520
|
+
const story = selectionStory(selection);
|
|
5500
6521
|
const listsApi = doc?.lists;
|
|
5501
6522
|
if (!(typeof listsApi?.getStateInStory === "function" || story.storyType === "body" && typeof listsApi?.getState === "function")) return normalizeCommandState({
|
|
5502
6523
|
enabled: false,
|
|
@@ -5524,7 +6545,7 @@ function createSuperDocUI(options) {
|
|
|
5524
6545
|
supported: true,
|
|
5525
6546
|
value
|
|
5526
6547
|
}, "builtin");
|
|
5527
|
-
if (snapshots.some(({ value: state
|
|
6548
|
+
if (snapshots.some(({ value: state }) => state === null)) return normalizeCommandState({
|
|
5528
6549
|
enabled: false,
|
|
5529
6550
|
active,
|
|
5530
6551
|
supported: false,
|
|
@@ -5532,7 +6553,7 @@ function createSuperDocUI(options) {
|
|
|
5532
6553
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5533
6554
|
}, "builtin");
|
|
5534
6555
|
const seed = descriptor.list?.seed;
|
|
5535
|
-
const shouldRemove = snapshots.every(({ value: state
|
|
6556
|
+
const shouldRemove = snapshots.every(({ value: state }) => state?.isListItem === true && state.seed === seed);
|
|
5536
6557
|
if (story.storyType === "body") return normalizeCommandState(!shouldRemove || resolveDocOperation(doc, "lists.remove") != null ? {
|
|
5537
6558
|
enabled: true,
|
|
5538
6559
|
active,
|
|
@@ -5560,10 +6581,12 @@ function createSuperDocUI(options) {
|
|
|
5560
6581
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
5561
6582
|
}, "builtin");
|
|
5562
6583
|
};
|
|
5563
|
-
|
|
5564
|
-
|
|
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;
|
|
5565
6587
|
return typeof href === "string" ? href : null;
|
|
5566
6588
|
};
|
|
6589
|
+
/** State for a command routed through a public SuperDoc-instance method. */
|
|
5567
6590
|
const computeInstanceCommandState = (descriptor) => {
|
|
5568
6591
|
if (!getEditor()) return normalizeCommandState({
|
|
5569
6592
|
enabled: false,
|
|
@@ -5585,12 +6608,14 @@ function createSuperDocUI(options) {
|
|
|
5585
6608
|
value: descriptorValue(descriptor)
|
|
5586
6609
|
}, "builtin");
|
|
5587
6610
|
};
|
|
6611
|
+
/** Live active state for a host-owned chrome control (`ruler`, `formatting-marks`). */
|
|
5588
6612
|
const chromeActiveState = (descriptor) => {
|
|
5589
6613
|
const config = superdoc?.config ?? void 0;
|
|
5590
6614
|
if (descriptor.valueFrom === "ruler") return Boolean(config?.rulers);
|
|
5591
6615
|
if (descriptor.valueFrom === "formattingMarks") return Boolean((config?.layoutEngineOptions)?.showFormattingMarks);
|
|
5592
6616
|
return false;
|
|
5593
6617
|
};
|
|
6618
|
+
/** State for a table cell-context command routed through `tables.*`. */
|
|
5594
6619
|
const computeTableCommandState = (descriptor, doc, readonly) => {
|
|
5595
6620
|
if (readonly) return normalizeCommandState({
|
|
5596
6621
|
enabled: false,
|
|
@@ -5617,12 +6642,14 @@ function createSuperDocUI(options) {
|
|
|
5617
6642
|
supported: true
|
|
5618
6643
|
}, "builtin");
|
|
5619
6644
|
};
|
|
6645
|
+
/** Resolve a descriptor's live `value` from public controller state, when modeled. */
|
|
5620
6646
|
const descriptorValue = (descriptor) => {
|
|
5621
6647
|
if (descriptor.valueFrom === "zoom") return computeZoom().value;
|
|
5622
6648
|
if (descriptor.valueFrom === "documentMode") return readDocumentMode();
|
|
5623
6649
|
if (descriptor.valueFrom === "measurementUnit") return readMeasurementUnit();
|
|
5624
6650
|
if (descriptor.valueFrom === "ruler" || descriptor.valueFrom === "formattingMarks") return chromeActiveState(descriptor);
|
|
5625
6651
|
};
|
|
6652
|
+
/** Validate normalized payloads before invoking public SuperDoc-instance methods. */
|
|
5626
6653
|
const instanceCommandPayloadIsValid = (descriptor, payload) => {
|
|
5627
6654
|
if (descriptor.id === "zoom") return typeof payload === "number" && Number.isFinite(payload) && payload > 0;
|
|
5628
6655
|
if (descriptor.id === "document-mode") return payload === "editing" || payload === "suggesting" || payload === "viewing";
|
|
@@ -5630,7 +6657,7 @@ function createSuperDocUI(options) {
|
|
|
5630
6657
|
return true;
|
|
5631
6658
|
};
|
|
5632
6659
|
const allCommandIds = () => {
|
|
5633
|
-
return [
|
|
6660
|
+
return [.../* @__PURE__ */ new Set([...ALL_BUILT_IN_COMMAND_IDS, ...customCommands.keys()])];
|
|
5634
6661
|
};
|
|
5635
6662
|
const DEFAULT_PARAGRAPH_STYLE_ID = "Normal";
|
|
5636
6663
|
const EMPTY_STYLES_SLICE = {
|
|
@@ -5644,6 +6671,14 @@ function createSuperDocUI(options) {
|
|
|
5644
6671
|
sourceStatus: null,
|
|
5645
6672
|
diagnostics: []
|
|
5646
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
|
+
*/
|
|
5647
6682
|
const readStyleCatalogLive = (input) => {
|
|
5648
6683
|
const stylesApi = getDoc()?.styles;
|
|
5649
6684
|
if (!stylesApi || typeof stylesApi.getCatalog !== "function") return {
|
|
@@ -5652,6 +6687,7 @@ function createSuperDocUI(options) {
|
|
|
5652
6687
|
};
|
|
5653
6688
|
return readAsync(`styles:catalog:${JSON.stringify(input ?? {})}`, contentToken(), () => stylesApi.getCatalog(input), (raw) => raw && typeof raw === "object" ? raw : null);
|
|
5654
6689
|
};
|
|
6690
|
+
/** Resolve the catalogue (coordinator-cached by content revision) plus its readiness. */
|
|
5655
6691
|
const getStyleCatalog = () => {
|
|
5656
6692
|
const full = readStyleCatalogLive({ includePreview: true });
|
|
5657
6693
|
if (!full.value) return {
|
|
@@ -5674,6 +6710,13 @@ function createSuperDocUI(options) {
|
|
|
5674
6710
|
status: combineStatus(full.status, quickResult.status)
|
|
5675
6711
|
};
|
|
5676
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
|
+
*/
|
|
5677
6720
|
const readBlockStyleRef = (doc, blockId, story) => {
|
|
5678
6721
|
const { value: result, status } = readNodeById(doc, blockId, story);
|
|
5679
6722
|
if (!result) return {
|
|
@@ -5696,7 +6739,13 @@ function createSuperDocUI(options) {
|
|
|
5696
6739
|
status
|
|
5697
6740
|
};
|
|
5698
6741
|
};
|
|
5699
|
-
|
|
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) => {
|
|
5700
6749
|
const diagnostics = [];
|
|
5701
6750
|
const doc = getDoc();
|
|
5702
6751
|
if (!doc) {
|
|
@@ -5715,7 +6764,7 @@ function createSuperDocUI(options) {
|
|
|
5715
6764
|
status: "ready"
|
|
5716
6765
|
};
|
|
5717
6766
|
}
|
|
5718
|
-
const blockIds = selectionBlockIds(selection
|
|
6767
|
+
const blockIds = selectionBlockIds(selection);
|
|
5719
6768
|
if (blockIds.length === 0) {
|
|
5720
6769
|
diagnostics.push({
|
|
5721
6770
|
severity: "info",
|
|
@@ -5754,7 +6803,7 @@ function createSuperDocUI(options) {
|
|
|
5754
6803
|
let blockReadFailures = 0;
|
|
5755
6804
|
let defaultReadFailures = 0;
|
|
5756
6805
|
let readStatus = "ready";
|
|
5757
|
-
const story = selectionStory(selection
|
|
6806
|
+
const story = selectionStory(selection);
|
|
5758
6807
|
for (const blockId of blockIds) {
|
|
5759
6808
|
const read = readBlockStyleRef(doc, blockId, story);
|
|
5760
6809
|
readStatus = combineStatus(readStatus, read.status);
|
|
@@ -5766,13 +6815,13 @@ function createSuperDocUI(options) {
|
|
|
5766
6815
|
defaultReadFailures += 1;
|
|
5767
6816
|
continue;
|
|
5768
6817
|
}
|
|
5769
|
-
const styleId
|
|
5770
|
-
if (!styleId
|
|
6818
|
+
const styleId = read.styleRef ?? defaultId;
|
|
6819
|
+
if (!styleId) {
|
|
5771
6820
|
defaultReadFailures += 1;
|
|
5772
6821
|
continue;
|
|
5773
6822
|
}
|
|
5774
6823
|
resolved += 1;
|
|
5775
|
-
resolvedIds.add(styleId
|
|
6824
|
+
resolvedIds.add(styleId);
|
|
5776
6825
|
}
|
|
5777
6826
|
if (resolved === 0) {
|
|
5778
6827
|
const onlyDefaultUnavailable = defaultReadFailures > 0 && blockReadFailures === 0;
|
|
@@ -5828,10 +6877,10 @@ function createSuperDocUI(options) {
|
|
|
5828
6877
|
status: readStatus
|
|
5829
6878
|
};
|
|
5830
6879
|
};
|
|
5831
|
-
const computeStyles = (selection
|
|
6880
|
+
const computeStyles = (selection) => {
|
|
5832
6881
|
if (!getEditor()) return EMPTY_STYLES_SLICE;
|
|
5833
6882
|
const { cache: catalog, status: catalogStatus } = getStyleCatalog();
|
|
5834
|
-
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection
|
|
6883
|
+
const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection, catalog);
|
|
5835
6884
|
let diagnostics;
|
|
5836
6885
|
if (catalog) diagnostics = active.diagnostics.length === 0 ? catalog.full.diagnostics : [...catalog.full.diagnostics, ...active.diagnostics];
|
|
5837
6886
|
else diagnostics = [{
|
|
@@ -5841,7 +6890,7 @@ function createSuperDocUI(options) {
|
|
|
5841
6890
|
}, ...active.diagnostics];
|
|
5842
6891
|
return {
|
|
5843
6892
|
ready: true,
|
|
5844
|
-
status: combineStatus(catalogStatus, activeStatus, selection
|
|
6893
|
+
status: combineStatus(catalogStatus, activeStatus, selection.status),
|
|
5845
6894
|
catalogRevision: catalog?.full.revision ?? null,
|
|
5846
6895
|
quickGallery: catalog?.quickGallery ?? [],
|
|
5847
6896
|
activeParagraphStyleId: active.styleId,
|
|
@@ -5853,32 +6902,32 @@ function createSuperDocUI(options) {
|
|
|
5853
6902
|
};
|
|
5854
6903
|
const computeState = () => {
|
|
5855
6904
|
const documentMode = readDocumentMode();
|
|
5856
|
-
const selection
|
|
5857
|
-
if (selection
|
|
5858
|
-
reconcilePendingInlineFormat(selection
|
|
5859
|
-
const selectionSignature
|
|
5860
|
-
for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature
|
|
5861
|
-
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);
|
|
5862
6911
|
return {
|
|
5863
6912
|
ready: getEditor() != null,
|
|
5864
6913
|
documentMode,
|
|
5865
6914
|
document: computeDocument(),
|
|
5866
|
-
selection
|
|
6915
|
+
selection,
|
|
5867
6916
|
toolbar: {
|
|
5868
6917
|
context: documentMode,
|
|
5869
|
-
commands: computeCommandStates(selection
|
|
6918
|
+
commands: computeCommandStates(selection),
|
|
5870
6919
|
copyFormatActive: painter.mode !== "idle"
|
|
5871
6920
|
},
|
|
5872
|
-
comments: computeComments(selection
|
|
5873
|
-
trackChanges: computeTrackChanges(selection
|
|
5874
|
-
contentControls: computeContentControls(selection
|
|
6921
|
+
comments: computeComments(selection),
|
|
6922
|
+
trackChanges: computeTrackChanges(selection),
|
|
6923
|
+
contentControls: computeContentControls(selection),
|
|
5875
6924
|
zoom: computeZoom(),
|
|
5876
6925
|
fonts: computeFonts(),
|
|
5877
|
-
styles: computeStyles(selection
|
|
6926
|
+
styles: computeStyles(selection)
|
|
5878
6927
|
};
|
|
5879
6928
|
};
|
|
5880
|
-
const syncOptimisticInlineSelection = (selection
|
|
5881
|
-
const nextSignature = selectionInlineValueSignature(selection
|
|
6929
|
+
const syncOptimisticInlineSelection = (selection) => {
|
|
6930
|
+
const nextSignature = selectionInlineValueSignature(selection);
|
|
5882
6931
|
if (nextSignature && lastOptimisticInlineSelectionSignature && nextSignature !== lastOptimisticInlineSelectionSignature) {
|
|
5883
6932
|
optimisticInlineValues.clear();
|
|
5884
6933
|
optimisticInlineToggles.clear();
|
|
@@ -5926,8 +6975,8 @@ function createSuperDocUI(options) {
|
|
|
5926
6975
|
let detachDocumentSelection = null;
|
|
5927
6976
|
const readHostSelectionSource = () => {
|
|
5928
6977
|
const host = getEditor()?.host;
|
|
5929
|
-
const selection
|
|
5930
|
-
return selection
|
|
6978
|
+
const selection = safeCall(host?.getHandles ? () => host.getHandles() : void 0, null)?.editing?.selection;
|
|
6979
|
+
return selection && typeof selection.subscribe === "function" ? selection : null;
|
|
5931
6980
|
};
|
|
5932
6981
|
const syncHostSelectionSubscription = () => {
|
|
5933
6982
|
const next = readHostSelectionSource();
|
|
@@ -5999,7 +7048,8 @@ function createSuperDocUI(options) {
|
|
|
5999
7048
|
try {
|
|
6000
7049
|
const unsubscribe = next.subscribe((snapshot) => {
|
|
6001
7050
|
const snapshotRecord = snapshot && typeof snapshot === "object" ? snapshot : null;
|
|
6002
|
-
|
|
7051
|
+
const target = snapshotRecord && Object.prototype.hasOwnProperty.call(snapshotRecord, "activeReviewTarget") ? snapshotRecord.activeReviewTarget : readHostActiveReviewTarget(next);
|
|
7052
|
+
if (!syncTrackedChangeFocusFromHostReviewTarget(target, snapshot) || attaching) return;
|
|
6003
7053
|
recompute();
|
|
6004
7054
|
});
|
|
6005
7055
|
if (typeof unsubscribe === "function") detachHostReview = unsubscribe;
|
|
@@ -6055,6 +7105,20 @@ function createSuperDocUI(options) {
|
|
|
6055
7105
|
try {
|
|
6056
7106
|
const off = next.subscribe((event) => {
|
|
6057
7107
|
const type = event?.type;
|
|
7108
|
+
if (type === "review-mutation:started") {
|
|
7109
|
+
beginUiReviewMutation(event.reviewMutation?.token);
|
|
7110
|
+
return;
|
|
7111
|
+
}
|
|
7112
|
+
if (type === "review-mutation:aborted") {
|
|
7113
|
+
settleUiReviewMutation(event.reviewMutation?.token);
|
|
7114
|
+
scheduleAsyncRefresh();
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
if (type === "mutation:rejected" && event.reviewMutation) {
|
|
7118
|
+
settleUiReviewMutation(event.reviewMutation.token);
|
|
7119
|
+
scheduleAsyncRefresh();
|
|
7120
|
+
}
|
|
7121
|
+
if (type === "mutation:committed" && event.reviewMutation) settleUiReviewMutation(event.reviewMutation.token);
|
|
6058
7122
|
if (type === "source:complete" || type === "source:signals-complete") {
|
|
6059
7123
|
if (authoritativeTrackChangesPendingToken !== null) {
|
|
6060
7124
|
clearAuthoritativeTrackChangesRetry();
|
|
@@ -6070,6 +7134,13 @@ function createSuperDocUI(options) {
|
|
|
6070
7134
|
if (type === "mutation:committed" || type === "save:completed" || type === "collaboration:remote-changed") {
|
|
6071
7135
|
const impact = type === "mutation:committed" ? getV2TrackedChangeMutationImpact(event) : null;
|
|
6072
7136
|
const supersedesPendingReviewReconcile = type === "mutation:committed" && reviewMutationReconciler.getPendingIds().size > 0;
|
|
7137
|
+
if (impact?.allResolved) {
|
|
7138
|
+
reviewMutationReconciler.reset();
|
|
7139
|
+
replaceTrackedChangeItemsInCache([]);
|
|
7140
|
+
authoritativeTrackChangesPendingToken = null;
|
|
7141
|
+
publishAllTrackedChangesResolved(event.receipt);
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
6073
7144
|
if (impact?.removedIds.size) markPostDecisionTrackChanges(new Set(impact.removedIds), event.receipt);
|
|
6074
7145
|
const authoritativeHistoryImpact = impact?.reconcileMode === "authoritative" && impact.upsertIds.size > 0;
|
|
6075
7146
|
if (authoritativeHistoryImpact) {
|
|
@@ -6132,7 +7203,29 @@ function createSuperDocUI(options) {
|
|
|
6132
7203
|
state = nextState;
|
|
6133
7204
|
for (const listener of [...listeners]) listener(state);
|
|
6134
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
|
+
*/
|
|
6135
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
|
+
*/
|
|
6136
7229
|
const documentResetHooks = [];
|
|
6137
7230
|
const runHooks = (hooks) => {
|
|
6138
7231
|
for (const reset of hooks) try {
|
|
@@ -6195,6 +7288,20 @@ function createSuperDocUI(options) {
|
|
|
6195
7288
|
};
|
|
6196
7289
|
};
|
|
6197
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
|
+
*/
|
|
6198
7305
|
const snapshotHandle = (sub) => {
|
|
6199
7306
|
const observe = (listener) => {
|
|
6200
7307
|
const safe = (snapshot) => {
|
|
@@ -6216,8 +7323,8 @@ function createSuperDocUI(options) {
|
|
|
6216
7323
|
function readTrackDecisionTargetId(target) {
|
|
6217
7324
|
return target && typeof target === "object" && typeof target.id === "string" ? String(target.id) : null;
|
|
6218
7325
|
}
|
|
6219
|
-
function nonBodySelectionStoryLocator(selection
|
|
6220
|
-
const story = selectionStoryLocator(selection
|
|
7326
|
+
function nonBodySelectionStoryLocator(selection) {
|
|
7327
|
+
const story = selectionStoryLocator(selection);
|
|
6221
7328
|
return story && storyLocatorSignature(story) !== storyLocatorSignature(null) ? story : void 0;
|
|
6222
7329
|
}
|
|
6223
7330
|
function trackDecisionIdTarget(changeId, story) {
|
|
@@ -6227,15 +7334,24 @@ function createSuperDocUI(options) {
|
|
|
6227
7334
|
...story ? { story } : {}
|
|
6228
7335
|
};
|
|
6229
7336
|
}
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
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;
|
|
6233
7349
|
const start = explicit?.start;
|
|
6234
7350
|
const end = explicit?.end;
|
|
6235
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;
|
|
6236
7352
|
const rangeStart = Math.min(start.offset, end.offset);
|
|
6237
7353
|
const rangeEnd = Math.max(start.offset, end.offset);
|
|
6238
|
-
const story = selectionStoryLocator(selection
|
|
7354
|
+
const story = selectionStoryLocator(selection);
|
|
6239
7355
|
return {
|
|
6240
7356
|
kind: "range",
|
|
6241
7357
|
coordinateSpace: explicit.coordinateSpace === "tracked" ? "tracked" : "visible",
|
|
@@ -6254,17 +7370,17 @@ function createSuperDocUI(options) {
|
|
|
6254
7370
|
}
|
|
6255
7371
|
function readBodyTrackChangeForDecision(activeId, story) {
|
|
6256
7372
|
if (storyLocatorSignature(story) !== storyLocatorSignature(null)) return void 0;
|
|
6257
|
-
const trackChanges
|
|
6258
|
-
const list = trackChanges
|
|
7373
|
+
const trackChanges = getDoc()?.trackChanges;
|
|
7374
|
+
const list = trackChanges?.list;
|
|
6259
7375
|
if (typeof list !== "function") return void 0;
|
|
6260
|
-
const raw = safeCall(() => list.call(trackChanges
|
|
7376
|
+
const raw = safeCall(() => list.call(trackChanges), null);
|
|
6261
7377
|
if (isPromiseLike(raw) || !raw || typeof raw !== "object") return void 0;
|
|
6262
7378
|
const items = raw.items;
|
|
6263
7379
|
if (!Array.isArray(items)) return void 0;
|
|
6264
7380
|
return items.map(projectTrackChangesItem).find((item) => item != null && entityRowMatchesRequest(item, activeId, story));
|
|
6265
7381
|
}
|
|
6266
|
-
function selectedTextCoversTrackChangeText(selection
|
|
6267
|
-
const selected = normalizeTrackDecisionText(selection
|
|
7382
|
+
function selectedTextCoversTrackChangeText(selection, item) {
|
|
7383
|
+
const selected = normalizeTrackDecisionText(selection.quotedText);
|
|
6268
7384
|
if (!selected) return false;
|
|
6269
7385
|
const payload = trackChangesItemPayload(item);
|
|
6270
7386
|
return [
|
|
@@ -6296,14 +7412,15 @@ function createSuperDocUI(options) {
|
|
|
6296
7412
|
if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
|
|
6297
7413
|
return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
|
|
6298
7414
|
}));
|
|
6299
|
-
|
|
7415
|
+
const commandResult = commandResultFromOperationResult(result);
|
|
7416
|
+
return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
|
|
6300
7417
|
} catch {
|
|
6301
7418
|
return false;
|
|
6302
7419
|
}
|
|
6303
7420
|
}
|
|
6304
7421
|
function resolveTrackDecisionTarget(command, payload) {
|
|
6305
7422
|
if (command.scope === "all") return {
|
|
6306
|
-
target: {
|
|
7423
|
+
target: { kind: "all" },
|
|
6307
7424
|
changeId: null
|
|
6308
7425
|
};
|
|
6309
7426
|
if (typeof payload === "string" && payload.length > 0) return {
|
|
@@ -6439,8 +7556,8 @@ function createSuperDocUI(options) {
|
|
|
6439
7556
|
const scheduleInlineToggleMutation = (key, mutate) => {
|
|
6440
7557
|
if (!inlineToggleMutationActive) {
|
|
6441
7558
|
inlineToggleMutationActive = true;
|
|
6442
|
-
inlineToggleMutationIdle = new Promise((resolve
|
|
6443
|
-
resolveInlineToggleMutationIdle = resolve
|
|
7559
|
+
inlineToggleMutationIdle = new Promise((resolve) => {
|
|
7560
|
+
resolveInlineToggleMutationIdle = resolve;
|
|
6444
7561
|
});
|
|
6445
7562
|
try {
|
|
6446
7563
|
return {
|
|
@@ -6477,20 +7594,21 @@ function createSuperDocUI(options) {
|
|
|
6477
7594
|
release: releaseInlineToggleMutationOnce()
|
|
6478
7595
|
};
|
|
6479
7596
|
};
|
|
6480
|
-
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = new Set(["create.tableOfContents"]);
|
|
7597
|
+
const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = /* @__PURE__ */ new Set(["create.tableOfContents"]);
|
|
6481
7598
|
const unsupportedTrackedMutationReceipt = (route) => failedReceipt(`Tracked authoring is not supported for ${route}.`);
|
|
6482
7599
|
const editorMutationOptionsForRoute = (route) => {
|
|
6483
7600
|
if (readDocumentMode() !== "suggesting") return void 0;
|
|
6484
7601
|
if (UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES.has(route)) return unsupportedTrackedMutationReceipt(route);
|
|
6485
7602
|
return { changeMode: "tracked" };
|
|
6486
7603
|
};
|
|
7604
|
+
/** Clear-patch to send for the current document mode (see TRACKED_CLEAR_INLINE_PATCH). */
|
|
6487
7605
|
const clearInlinePatchForMode = () => readDocumentMode() === "suggesting" ? TRACKED_CLEAR_INLINE_PATCH : CLEAR_INLINE_PATCH;
|
|
6488
7606
|
const callEditorMutation = (route, op, input) => {
|
|
6489
|
-
const options
|
|
6490
|
-
if (options
|
|
6491
|
-
return options
|
|
7607
|
+
const options = editorMutationOptionsForRoute(route);
|
|
7608
|
+
if (options && options.success === false) return options;
|
|
7609
|
+
return options ? op(input, options) : op(input);
|
|
6492
7610
|
};
|
|
6493
|
-
const captureOptimisticInlineValueResult = (descriptor, selection
|
|
7611
|
+
const captureOptimisticInlineValueResult = (descriptor, selection, input, result) => {
|
|
6494
7612
|
const inline = descriptor.inline;
|
|
6495
7613
|
if (!inline) return;
|
|
6496
7614
|
const commandResult = commandResultFromOperationResult(result);
|
|
@@ -6501,35 +7619,35 @@ function createSuperDocUI(options) {
|
|
|
6501
7619
|
return;
|
|
6502
7620
|
}
|
|
6503
7621
|
if (inline.kind === "toggle" || !isProjectedInlineSelectionValueKey(inline.key)) return;
|
|
6504
|
-
const selectionSignature
|
|
6505
|
-
if (!selectionSignature
|
|
7622
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7623
|
+
if (!selectionSignature) return;
|
|
6506
7624
|
const value = normalizeOptimisticInlineSelectionValue(inline.key, input.value);
|
|
6507
7625
|
if (value == null) {
|
|
6508
7626
|
optimisticInlineValues.delete(inline.key);
|
|
6509
7627
|
return;
|
|
6510
7628
|
}
|
|
6511
7629
|
optimisticInlineValues.set(inline.key, {
|
|
6512
|
-
selectionSignature
|
|
7630
|
+
selectionSignature,
|
|
6513
7631
|
value
|
|
6514
7632
|
});
|
|
6515
7633
|
};
|
|
6516
|
-
const decorateInlineCommandResult = (descriptor, selection
|
|
7634
|
+
const decorateInlineCommandResult = (descriptor, selection, input, result) => {
|
|
6517
7635
|
if (!descriptor.inline || descriptor.inline.kind === "toggle") return result;
|
|
6518
7636
|
if (isPromiseLike(result)) return Promise.resolve(result).then((resolved) => {
|
|
6519
|
-
captureOptimisticInlineValueResult(descriptor, selection
|
|
7637
|
+
captureOptimisticInlineValueResult(descriptor, selection, input, resolved);
|
|
6520
7638
|
return resolved;
|
|
6521
7639
|
});
|
|
6522
7640
|
return result;
|
|
6523
7641
|
};
|
|
6524
|
-
const captureOptimisticInlineToggle = (descriptor, selection
|
|
7642
|
+
const captureOptimisticInlineToggle = (descriptor, selection, input, result) => {
|
|
6525
7643
|
if (descriptor.inline?.kind !== "toggle") return null;
|
|
6526
7644
|
if (!commandResultSucceeded(commandResultFromOperationResult(result))) return null;
|
|
6527
7645
|
if (typeof input.value !== "boolean" && input.value !== null) return null;
|
|
6528
|
-
const selectionSignature
|
|
6529
|
-
if (!selectionSignature
|
|
7646
|
+
const selectionSignature = selectionInlineValueSignature(selection);
|
|
7647
|
+
if (!selectionSignature) return null;
|
|
6530
7648
|
const generation = ++optimisticInlineToggleGeneration;
|
|
6531
7649
|
optimisticInlineToggles.set(descriptor.id, {
|
|
6532
|
-
selectionSignature
|
|
7650
|
+
selectionSignature,
|
|
6533
7651
|
active: input.value === true,
|
|
6534
7652
|
generation,
|
|
6535
7653
|
settled: false
|
|
@@ -6549,13 +7667,11 @@ function createSuperDocUI(options) {
|
|
|
6549
7667
|
};
|
|
6550
7668
|
const finalizeCommandSettlement = (promise, onSettled) => promise.then((settled) => {
|
|
6551
7669
|
onSettled?.(settled);
|
|
6552
|
-
|
|
6553
|
-
recompute();
|
|
7670
|
+
invalidateAfterCommandSettlement();
|
|
6554
7671
|
return settled;
|
|
6555
7672
|
}, () => {
|
|
6556
7673
|
onSettled?.(false);
|
|
6557
|
-
|
|
6558
|
-
recompute();
|
|
7674
|
+
invalidateAfterCommandSettlement();
|
|
6559
7675
|
return false;
|
|
6560
7676
|
});
|
|
6561
7677
|
const settleCommandExecution = (result, onSettled) => {
|
|
@@ -6578,8 +7694,7 @@ function createSuperDocUI(options) {
|
|
|
6578
7694
|
}
|
|
6579
7695
|
onSettled?.(settled);
|
|
6580
7696
|
lastCommandSettlement = Promise.resolve(settled);
|
|
6581
|
-
|
|
6582
|
-
recompute();
|
|
7697
|
+
invalidateAfterCommandSettlement();
|
|
6583
7698
|
return settled;
|
|
6584
7699
|
};
|
|
6585
7700
|
const normalizeWorkflowReceipt = (value, fallback) => {
|
|
@@ -6600,6 +7715,11 @@ function createSuperDocUI(options) {
|
|
|
6600
7715
|
recompute();
|
|
6601
7716
|
return normalizedReceipt;
|
|
6602
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
|
+
*/
|
|
6603
7723
|
const applyPerBlock = (route, op, blockIds, buildInput) => {
|
|
6604
7724
|
const immediateResults = [];
|
|
6605
7725
|
const settledResults = [];
|
|
@@ -6618,6 +7738,7 @@ function createSuperDocUI(options) {
|
|
|
6618
7738
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6619
7739
|
return combineCommandResults(immediateResults);
|
|
6620
7740
|
};
|
|
7741
|
+
/** Apply a Document API operation once per covered text segment. */
|
|
6621
7742
|
const applyPerTextAddress = (route, op, targets, buildInput) => {
|
|
6622
7743
|
const immediateResults = [];
|
|
6623
7744
|
const settledResults = [];
|
|
@@ -6636,15 +7757,25 @@ function createSuperDocUI(options) {
|
|
|
6636
7757
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6637
7758
|
return combineCommandResults(immediateResults);
|
|
6638
7759
|
};
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
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);
|
|
6643
7773
|
return op(input, {
|
|
6644
|
-
...options
|
|
7774
|
+
...options ?? {},
|
|
6645
7775
|
offsetSpace: "selection"
|
|
6646
7776
|
});
|
|
6647
7777
|
};
|
|
7778
|
+
/** Build the `format.paragraph.*` / `styles.paragraph.*` input for a block. */
|
|
6648
7779
|
const buildBlockParagraphInput = (spec, blockId, payload, story) => {
|
|
6649
7780
|
const target = paragraphTarget(blockId, story);
|
|
6650
7781
|
switch (spec.kind) {
|
|
@@ -6689,6 +7820,14 @@ function createSuperDocUI(options) {
|
|
|
6689
7820
|
default: return null;
|
|
6690
7821
|
}
|
|
6691
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
|
+
*/
|
|
6692
7831
|
const storePendingInlineFormat = (descriptor, payload) => {
|
|
6693
7832
|
const spec = descriptor.inline;
|
|
6694
7833
|
const method = inlineFormatMethod(descriptor);
|
|
@@ -6726,9 +7865,9 @@ function createSuperDocUI(options) {
|
|
|
6726
7865
|
immediateResults.push(settled.immediate);
|
|
6727
7866
|
settledResults.push(settled.settled);
|
|
6728
7867
|
};
|
|
6729
|
-
const run = (op, input, options
|
|
7868
|
+
const run = (op, input, options) => {
|
|
6730
7869
|
try {
|
|
6731
|
-
record(options
|
|
7870
|
+
record(options ? op(input, options) : op(input));
|
|
6732
7871
|
} catch {
|
|
6733
7872
|
immediateResults.push(false);
|
|
6734
7873
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6787,6 +7926,7 @@ function createSuperDocUI(options) {
|
|
|
6787
7926
|
pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
|
|
6788
7927
|
return combineCommandResults(immediateResults.length ? immediateResults : [false]);
|
|
6789
7928
|
};
|
|
7929
|
+
/** Route a block-level paragraph command (`format.paragraph.*` / `styles.paragraph.*`). */
|
|
6790
7930
|
const executeBlockParagraph = (descriptor, op, payload) => {
|
|
6791
7931
|
const blockIds = selectionBlockIds(state.selection);
|
|
6792
7932
|
if (blockIds.length === 0) return false;
|
|
@@ -6892,6 +8032,7 @@ function createSuperDocUI(options) {
|
|
|
6892
8032
|
})();
|
|
6893
8033
|
return true;
|
|
6894
8034
|
};
|
|
8035
|
+
/** Route a list command (`lists.apply` / `lists.remove` / list level changes). */
|
|
6895
8036
|
const executeListCommand = (descriptor, op) => {
|
|
6896
8037
|
const spec = descriptor.list;
|
|
6897
8038
|
const doc = getDoc();
|
|
@@ -6929,9 +8070,9 @@ function createSuperDocUI(options) {
|
|
|
6929
8070
|
seed
|
|
6930
8071
|
};
|
|
6931
8072
|
try {
|
|
6932
|
-
const result
|
|
6933
|
-
immediateResults.push(result
|
|
6934
|
-
settledResults.push(result
|
|
8073
|
+
const result = settleOperationResult(callEditorMutation(route, operation, input));
|
|
8074
|
+
immediateResults.push(result.immediate);
|
|
8075
|
+
settledResults.push(result.settled);
|
|
6935
8076
|
} catch {
|
|
6936
8077
|
immediateResults.push(false);
|
|
6937
8078
|
settledResults.push(Promise.resolve(false));
|
|
@@ -6945,8 +8086,8 @@ function createSuperDocUI(options) {
|
|
|
6945
8086
|
const stateReads = blockIds.map((blockId) => resolveListSeedForBlock(doc, blockId, story));
|
|
6946
8087
|
if (stateReads.some(isPromiseLike)) {
|
|
6947
8088
|
pendingCommandSettlement = Promise.all(stateReads).then(async (states) => {
|
|
6948
|
-
const result
|
|
6949
|
-
return result
|
|
8089
|
+
const result = executeResolved(states);
|
|
8090
|
+
return result.settled ? await result.settled : result.immediate;
|
|
6950
8091
|
});
|
|
6951
8092
|
return true;
|
|
6952
8093
|
}
|
|
@@ -6972,6 +8113,7 @@ function createSuperDocUI(options) {
|
|
|
6972
8113
|
const capture = record.capture && typeof record.capture === "object" ? record.capture : null;
|
|
6973
8114
|
return record.target ?? capture?.target ?? capture?.selectionTarget ?? record.selectionTarget ?? record.textTarget ?? null;
|
|
6974
8115
|
}
|
|
8116
|
+
/** Route a link command (`hyperlinks.wrap` / `insert` / `patch` / `remove`). */
|
|
6975
8117
|
const executeLinkCommand = (payload) => {
|
|
6976
8118
|
const doc = getDoc();
|
|
6977
8119
|
const linksApi = doc?.hyperlinks;
|
|
@@ -7107,8 +8249,8 @@ function createSuperDocUI(options) {
|
|
|
7107
8249
|
if (wrapTargets.length > 0) {
|
|
7108
8250
|
if (typeof wrap !== "function") return false;
|
|
7109
8251
|
try {
|
|
7110
|
-
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target
|
|
7111
|
-
target
|
|
8252
|
+
return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target) => ({
|
|
8253
|
+
target,
|
|
7112
8254
|
link: { destination: { href: normalizedHref } }
|
|
7113
8255
|
}));
|
|
7114
8256
|
} catch {
|
|
@@ -7116,14 +8258,14 @@ function createSuperDocUI(options) {
|
|
|
7116
8258
|
}
|
|
7117
8259
|
}
|
|
7118
8260
|
const insert = linksApi.insert;
|
|
7119
|
-
const text
|
|
7120
|
-
if (typeof insert !== "function" || text
|
|
8261
|
+
const text = readLinkPayloadText(payload, normalizedHref);
|
|
8262
|
+
if (typeof insert !== "function" || text.trim() === "") return false;
|
|
7121
8263
|
const target = collapsedTextAddressFromTarget(payloadTarget) ?? collapsedTextAddressFromSelection(state.selection);
|
|
7122
8264
|
if (!target) return false;
|
|
7123
8265
|
try {
|
|
7124
8266
|
return callEditorMutation("hyperlinks.insert", insert, {
|
|
7125
8267
|
target,
|
|
7126
|
-
text
|
|
8268
|
+
text,
|
|
7127
8269
|
link: { destination: { href: normalizedHref } }
|
|
7128
8270
|
});
|
|
7129
8271
|
} catch {
|
|
@@ -7132,6 +8274,7 @@ function createSuperDocUI(options) {
|
|
|
7132
8274
|
}
|
|
7133
8275
|
return false;
|
|
7134
8276
|
};
|
|
8277
|
+
/** Route a create command (`create.table` / `create.image` / `create.tableOfContents`). */
|
|
7135
8278
|
const executeCreateCommand = (descriptor, op, payload) => {
|
|
7136
8279
|
const kind = descriptor.create.kind;
|
|
7137
8280
|
const record = payload && typeof payload === "object" ? payload : {};
|
|
@@ -7177,16 +8320,16 @@ function createSuperDocUI(options) {
|
|
|
7177
8320
|
}
|
|
7178
8321
|
};
|
|
7179
8322
|
}
|
|
7180
|
-
const input
|
|
8323
|
+
const input = {
|
|
7181
8324
|
src,
|
|
7182
8325
|
at
|
|
7183
8326
|
};
|
|
7184
|
-
if (nonBodyStory) input
|
|
7185
|
-
if (typeof record.alt === "string") input
|
|
7186
|
-
if (typeof record.title === "string") input
|
|
7187
|
-
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;
|
|
7188
8331
|
try {
|
|
7189
|
-
return callEditorMutation(descriptor.docRoute, op, input
|
|
8332
|
+
return callEditorMutation(descriptor.docRoute, op, input);
|
|
7190
8333
|
} catch {
|
|
7191
8334
|
return false;
|
|
7192
8335
|
}
|
|
@@ -7200,6 +8343,13 @@ function createSuperDocUI(options) {
|
|
|
7200
8343
|
return false;
|
|
7201
8344
|
}
|
|
7202
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
|
+
*/
|
|
7203
8353
|
const executeTableCommand = (descriptor, op) => {
|
|
7204
8354
|
const spec = descriptor.table;
|
|
7205
8355
|
const context = resolveTableContext();
|
|
@@ -7212,9 +8362,9 @@ function createSuperDocUI(options) {
|
|
|
7212
8362
|
const host = getHost();
|
|
7213
8363
|
const insertRow = ((typeof host?.getHandles === "function" ? host.getHandles() : null)?.editing)?.tables;
|
|
7214
8364
|
if (typeof insertRow?.insertRow !== "function") return false;
|
|
7215
|
-
const options
|
|
7216
|
-
if (options
|
|
7217
|
-
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);
|
|
7218
8368
|
}
|
|
7219
8369
|
return callEditorMutation(descriptor.docRoute, op, input);
|
|
7220
8370
|
} catch {
|
|
@@ -7255,7 +8405,8 @@ function createSuperDocUI(options) {
|
|
|
7255
8405
|
if (disposed) return false;
|
|
7256
8406
|
const custom = customCommands.get(id);
|
|
7257
8407
|
if (custom) try {
|
|
7258
|
-
|
|
8408
|
+
const result = custom.execute(buildCustomCommandContext(payload, context));
|
|
8409
|
+
return settleCommandExecution(result);
|
|
7259
8410
|
} catch {
|
|
7260
8411
|
return false;
|
|
7261
8412
|
}
|
|
@@ -7276,7 +8427,8 @@ function createSuperDocUI(options) {
|
|
|
7276
8427
|
if (typeof method !== "function") return false;
|
|
7277
8428
|
try {
|
|
7278
8429
|
const arg = descriptor.fixedArg !== void 0 ? descriptor.fixedArg : normalized;
|
|
7279
|
-
|
|
8430
|
+
const result = method.call(superdoc, arg);
|
|
8431
|
+
return settleCommandExecution(result);
|
|
7280
8432
|
} catch {
|
|
7281
8433
|
return false;
|
|
7282
8434
|
}
|
|
@@ -7310,12 +8462,13 @@ function createSuperDocUI(options) {
|
|
|
7310
8462
|
if (getEditor() !== commandEditor || getDoc() !== doc || readDocumentMode() !== commandMode) return false;
|
|
7311
8463
|
return callInlineFormatMutation(route, op, input);
|
|
7312
8464
|
};
|
|
7313
|
-
const selectionSignature
|
|
7314
|
-
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;
|
|
7315
8467
|
releaseScheduledMutation = scheduledMutation?.release ?? null;
|
|
7316
8468
|
const mutationResult = scheduledMutation ? scheduledMutation.result : mutate();
|
|
7317
8469
|
const optimisticGeneration = captureOptimisticInlineToggle(descriptor, state.selection, input, mutationResult);
|
|
7318
|
-
const
|
|
8470
|
+
const result = decorateInlineCommandResult(descriptor, state.selection, input, mutationResult);
|
|
8471
|
+
const immediate = settleCommandExecution(result, (settled) => {
|
|
7319
8472
|
settleOptimisticInlineToggle(descriptor, optimisticGeneration, settled);
|
|
7320
8473
|
releaseScheduledMutation?.();
|
|
7321
8474
|
releaseScheduledMutation = null;
|
|
@@ -7334,10 +8487,22 @@ function createSuperDocUI(options) {
|
|
|
7334
8487
|
settleOptimisticParagraphAlignment(optimisticAlignmentGeneration, settled);
|
|
7335
8488
|
});
|
|
7336
8489
|
}
|
|
7337
|
-
if (descriptor.list)
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
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
|
+
}
|
|
7341
8506
|
try {
|
|
7342
8507
|
return settleCommandExecution(descriptor.mutates ? callEditorMutation(route, op, normalized) : op(normalized));
|
|
7343
8508
|
} catch {
|
|
@@ -7404,8 +8569,8 @@ function createSuperDocUI(options) {
|
|
|
7404
8569
|
recompute();
|
|
7405
8570
|
return true;
|
|
7406
8571
|
};
|
|
7407
|
-
const captureFormatPainter = async (selection
|
|
7408
|
-
const selTarget = captureSlice?.target ?? selection
|
|
8572
|
+
const captureFormatPainter = async (selection, captureSlice, captureProjected, visibleActiveMarks = []) => {
|
|
8573
|
+
const selTarget = captureSlice?.target ?? selection?.target;
|
|
7409
8574
|
if (!selTarget || selTarget["kind"] !== "text") return null;
|
|
7410
8575
|
const segments = Array.isArray(selTarget["segments"]) ? selTarget["segments"] : [];
|
|
7411
8576
|
if (segments.length === 0) return null;
|
|
@@ -7417,7 +8582,8 @@ function createSuperDocUI(options) {
|
|
|
7417
8582
|
const node = (await readBlockNode(paragraphTarget(segment["blockId"], sourceStory)))?.["node"] ?? null;
|
|
7418
8583
|
if (!node) continue;
|
|
7419
8584
|
const para = getParagraphLikeData(node);
|
|
7420
|
-
const
|
|
8585
|
+
const runs = Array.isArray(para?.["inlines"]) ? para["inlines"] : [];
|
|
8586
|
+
const slicedProps = sliceAndIntersectRunProps(runs, segment["range"]);
|
|
7421
8587
|
if (!mergedRunProps) mergedRunProps = slicedProps;
|
|
7422
8588
|
else mergedRunProps = intersectRunProps(mergedRunProps, slicedProps);
|
|
7423
8589
|
}
|
|
@@ -7428,9 +8594,9 @@ function createSuperDocUI(options) {
|
|
|
7428
8594
|
underline: "underline",
|
|
7429
8595
|
strikethrough: "strike"
|
|
7430
8596
|
};
|
|
7431
|
-
const activeMarks = new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection
|
|
8597
|
+
const activeMarks = /* @__PURE__ */ new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection?.["activeMarks"]) ? selection["activeMarks"] : []), ...visibleActiveMarks]);
|
|
7432
8598
|
for (const [markName, patchKey] of Object.entries(MARK_TO_PATCH)) if (activeMarks.has(markName)) inlinePatch[patchKey] = true;
|
|
7433
|
-
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection
|
|
8599
|
+
const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection, "ready"));
|
|
7434
8600
|
if (projectedInlineValues.fontFamily) inlinePatch["fontFamily"] = projectedInlineValues.fontFamily;
|
|
7435
8601
|
if (projectedInlineValues.color) inlinePatch["color"] = projectedInlineValues.color;
|
|
7436
8602
|
if (projectedInlineValues.highlight) inlinePatch["highlight"] = projectedInlineValues.highlight;
|
|
@@ -7467,12 +8633,12 @@ function createSuperDocUI(options) {
|
|
|
7467
8633
|
return r["run"]?.["props"];
|
|
7468
8634
|
};
|
|
7469
8635
|
if (range.start === range.end) {
|
|
7470
|
-
let offset
|
|
8636
|
+
let offset = 0;
|
|
7471
8637
|
for (const run of runs) {
|
|
7472
8638
|
const text = getRunText(run);
|
|
7473
|
-
const runStart = offset
|
|
7474
|
-
const runEnd = offset
|
|
7475
|
-
offset
|
|
8639
|
+
const runStart = offset;
|
|
8640
|
+
const runEnd = offset + text.length;
|
|
8641
|
+
offset = runEnd;
|
|
7476
8642
|
if (range.start >= runStart && range.start <= runEnd) {
|
|
7477
8643
|
const rPr = getRunProps(run);
|
|
7478
8644
|
return rPr ? { ...rPr } : {};
|
|
@@ -7549,7 +8715,8 @@ function createSuperDocUI(options) {
|
|
|
7549
8715
|
if (painter.pointerSelecting || painter.keyboardSelecting) return;
|
|
7550
8716
|
const selectionApi = getDoc()?.selection;
|
|
7551
8717
|
const readSelection = async () => {
|
|
7552
|
-
|
|
8718
|
+
const rawSelection = typeof selectionApi?.["current"] === "function" ? await Promise.resolve(selectionApi["current"]({ includeText: true })) : readSelectionInfoLive().value;
|
|
8719
|
+
return normalizeSelectionInfo(rawSelection);
|
|
7553
8720
|
};
|
|
7554
8721
|
const waitForSelectionSettle = () => new Promise((resolve) => setTimeout(resolve, 16));
|
|
7555
8722
|
const MAX_SELECTION_ATTEMPTS = 8;
|
|
@@ -7575,7 +8742,7 @@ function createSuperDocUI(options) {
|
|
|
7575
8742
|
await applyFormatPainter(caretSel);
|
|
7576
8743
|
}
|
|
7577
8744
|
};
|
|
7578
|
-
const applyFormatPainter = async (selection
|
|
8745
|
+
const applyFormatPainter = async (selection) => {
|
|
7579
8746
|
const snap = painter.snapshot;
|
|
7580
8747
|
if (!snap) {
|
|
7581
8748
|
if (painter.mode === "armed") exitFormatPainter();
|
|
@@ -7583,21 +8750,21 @@ function createSuperDocUI(options) {
|
|
|
7583
8750
|
}
|
|
7584
8751
|
const doc = getDoc();
|
|
7585
8752
|
if (!doc) return;
|
|
7586
|
-
const targetSelection = selectionSliceFromInfo(selection
|
|
8753
|
+
const targetSelection = selectionSliceFromInfo(selection, "ready");
|
|
7587
8754
|
const { lockModesById } = computeActiveContentControlLockModes(doc.contentControls, targetSelection);
|
|
7588
8755
|
if (contentControlLockReason(lockModesById)) {
|
|
7589
8756
|
recompute();
|
|
7590
8757
|
return;
|
|
7591
8758
|
}
|
|
7592
|
-
const selTarget = selection
|
|
7593
|
-
const selectionTarget = selection
|
|
7594
|
-
if (!(selection
|
|
8759
|
+
const selTarget = selection["target"];
|
|
8760
|
+
const selectionTarget = selection["selectionTarget"];
|
|
8761
|
+
if (!(selection["empty"] === true) && Object.keys(snap.inline).length > 0) {
|
|
7595
8762
|
const inlineOp = resolveDocOperation(doc, "format.apply");
|
|
7596
8763
|
if (inlineOp && selectionTarget) try {
|
|
7597
8764
|
if (!commandResultSucceeded(commandResultFromOperationResult(await Promise.resolve(callInlineFormatMutation("format.apply", inlineOp, {
|
|
7598
8765
|
target: selectionTarget,
|
|
7599
8766
|
inline: snap.inline
|
|
7600
|
-
}, selection
|
|
8767
|
+
}, selection))))) {
|
|
7601
8768
|
recompute();
|
|
7602
8769
|
return;
|
|
7603
8770
|
}
|
|
@@ -7633,11 +8800,11 @@ function createSuperDocUI(options) {
|
|
|
7633
8800
|
if (spacing) {
|
|
7634
8801
|
const sp = spacing;
|
|
7635
8802
|
const rawLr = sp["lineRule"] ?? "auto";
|
|
7636
|
-
const lr = new Set([
|
|
8803
|
+
const lr = (/* @__PURE__ */ new Set([
|
|
7637
8804
|
"auto",
|
|
7638
8805
|
"exact",
|
|
7639
8806
|
"atLeast"
|
|
7640
|
-
]).has(rawLr) ? rawLr : "auto";
|
|
8807
|
+
])).has(rawLr) ? rawLr : "auto";
|
|
7641
8808
|
const rawSpacing = {
|
|
7642
8809
|
...sp["before"] != null ? { before: Math.round(sp["before"] * 20) } : {},
|
|
7643
8810
|
...sp["after"] != null ? { after: Math.round(sp["after"] * 20) } : {},
|
|
@@ -7727,6 +8894,14 @@ function createSuperDocUI(options) {
|
|
|
7727
8894
|
};
|
|
7728
8895
|
const selectionSub = sliceHandle((s) => s.selection);
|
|
7729
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
|
+
*/
|
|
7730
8905
|
const getSelectionApplyHelper = () => {
|
|
7731
8906
|
const host = getHost();
|
|
7732
8907
|
if (typeof host?.getHandles !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
|
|
@@ -7741,6 +8916,14 @@ function createSuperDocUI(options) {
|
|
|
7741
8916
|
if (typeof helper?.apply !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
|
|
7742
8917
|
return { helper };
|
|
7743
8918
|
};
|
|
8919
|
+
const activateContentControlChrome = (id) => {
|
|
8920
|
+
const host = getHost();
|
|
8921
|
+
if (typeof host?.getHandles !== "function") return;
|
|
8922
|
+
try {
|
|
8923
|
+
const contentControls = (host.getHandles()?.editing)?.contentControls;
|
|
8924
|
+
if (typeof contentControls?.activate === "function") contentControls.activate({ id });
|
|
8925
|
+
} catch {}
|
|
8926
|
+
};
|
|
7744
8927
|
const selection = {
|
|
7745
8928
|
get: selectionSnap.get,
|
|
7746
8929
|
getSnapshot: selectionSnap.getSnapshot,
|
|
@@ -7866,7 +9049,17 @@ function createSuperDocUI(options) {
|
|
|
7866
9049
|
execute: executeCommand,
|
|
7867
9050
|
executeAsync: executeCommandAsync
|
|
7868
9051
|
};
|
|
7869
|
-
|
|
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) => {
|
|
7870
9063
|
if (!getEditor()) return {
|
|
7871
9064
|
success: false,
|
|
7872
9065
|
ok: false,
|
|
@@ -7887,10 +9080,10 @@ function createSuperDocUI(options) {
|
|
|
7887
9080
|
try {
|
|
7888
9081
|
const input = {
|
|
7889
9082
|
target,
|
|
7890
|
-
block: options
|
|
7891
|
-
behavior: options
|
|
9083
|
+
block: options?.block ?? "center",
|
|
9084
|
+
behavior: options?.behavior ?? "smooth"
|
|
7892
9085
|
};
|
|
7893
|
-
const result = await Promise.resolve(options
|
|
9086
|
+
const result = await Promise.resolve(options?.shouldContinue ? scroll.call(host, input, options.shouldContinue) : scroll.call(host, input));
|
|
7894
9087
|
if (result && typeof result === "object" && result.success === false) return {
|
|
7895
9088
|
success: false,
|
|
7896
9089
|
ok: false,
|
|
@@ -7908,6 +9101,11 @@ function createSuperDocUI(options) {
|
|
|
7908
9101
|
};
|
|
7909
9102
|
}
|
|
7910
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
|
+
*/
|
|
7911
9109
|
const resolveEntityTarget = async (namespace, id, loaded, request) => {
|
|
7912
9110
|
const requestedStory = namespace === "trackChanges" ? request?.story : void 0;
|
|
7913
9111
|
const withRequestedStory = (target) => {
|
|
@@ -7960,6 +9158,18 @@ function createSuperDocUI(options) {
|
|
|
7960
9158
|
status
|
|
7961
9159
|
}), fallback), fallback);
|
|
7962
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
|
+
*/
|
|
7963
9173
|
const commentWriteRefusal = () => reviewMutationsAreReadOnly() ? failedReceipt("Comments are read-only.", "DOCUMENT_READONLY") : null;
|
|
7964
9174
|
const filterCommentsSnapshot = (items, query) => {
|
|
7965
9175
|
const record = query && typeof query === "object" ? query : null;
|
|
@@ -8000,10 +9210,14 @@ function createSuperDocUI(options) {
|
|
|
8000
9210
|
const op = commentsApi?.create;
|
|
8001
9211
|
if (typeof op !== "function") return failedReceipt("comments.create is unavailable.");
|
|
8002
9212
|
const fallback = failedReceipt("comments.create failed.");
|
|
8003
|
-
return safeCall(() =>
|
|
8004
|
-
target
|
|
8005
|
-
|
|
8006
|
-
|
|
9213
|
+
return safeCall(() => {
|
|
9214
|
+
const target = capture && typeof capture === "object" ? capture.target ?? capture.selectionTarget : null;
|
|
9215
|
+
if (target == null) return failedReceipt("A range selection is required to comment.", "NO_SELECTION");
|
|
9216
|
+
return settleWorkflowReceipt(op.call(commentsApi, {
|
|
9217
|
+
target,
|
|
9218
|
+
text: input.text
|
|
9219
|
+
}), fallback);
|
|
9220
|
+
}, fallback);
|
|
8007
9221
|
},
|
|
8008
9222
|
createFromSelection: (input) => {
|
|
8009
9223
|
const refusal = commentWriteRefusal();
|
|
@@ -8035,6 +9249,18 @@ function createSuperDocUI(options) {
|
|
|
8035
9249
|
text: input.text
|
|
8036
9250
|
}), fallback), fallback);
|
|
8037
9251
|
},
|
|
9252
|
+
edit: (commentId, input) => {
|
|
9253
|
+
const refusal = commentWriteRefusal();
|
|
9254
|
+
if (refusal) return refusal;
|
|
9255
|
+
const commentsApi = getDoc()?.comments;
|
|
9256
|
+
const op = commentsApi?.patch;
|
|
9257
|
+
if (typeof op !== "function") return failedReceipt("comments.patch is unavailable.");
|
|
9258
|
+
const fallback = failedReceipt("comments.patch failed.");
|
|
9259
|
+
return safeCall(() => settleWorkflowReceipt(op.call(commentsApi, {
|
|
9260
|
+
commentId,
|
|
9261
|
+
text: input.text
|
|
9262
|
+
}), fallback), fallback);
|
|
9263
|
+
},
|
|
8038
9264
|
resolve: (commentId) => patchCommentStatus(commentId, "resolved"),
|
|
8039
9265
|
reopen: (commentId) => patchCommentStatus(commentId, "active"),
|
|
8040
9266
|
delete: (commentId) => {
|
|
@@ -8078,7 +9304,7 @@ function createSuperDocUI(options) {
|
|
|
8078
9304
|
}
|
|
8079
9305
|
};
|
|
8080
9306
|
const trackChangesSub = sliceHandle((s) => s.trackChanges);
|
|
8081
|
-
const navigateTrackChange = (step, options
|
|
9307
|
+
const navigateTrackChange = (step, options) => {
|
|
8082
9308
|
const allRows = trackChangesSub.get().items.filter((item) => readEntityId(item) !== null);
|
|
8083
9309
|
if (allRows.length === 0) return null;
|
|
8084
9310
|
const currentId = state.trackChanges.activeId;
|
|
@@ -8093,7 +9319,7 @@ function createSuperDocUI(options) {
|
|
|
8093
9319
|
setExplicitActiveChange(story ? {
|
|
8094
9320
|
id: activeId,
|
|
8095
9321
|
story
|
|
8096
|
-
} : { id: activeId }, options
|
|
9322
|
+
} : { id: activeId }, options);
|
|
8097
9323
|
recompute();
|
|
8098
9324
|
return activeId;
|
|
8099
9325
|
};
|
|
@@ -8175,8 +9401,8 @@ function createSuperDocUI(options) {
|
|
|
8175
9401
|
},
|
|
8176
9402
|
accept: (changeId) => executeTrackDecision("accept", changeId),
|
|
8177
9403
|
reject: (changeId) => executeTrackDecision("reject", changeId),
|
|
8178
|
-
acceptAll: () => executeTrackDecisionTarget("accept", {
|
|
8179
|
-
rejectAll: () => executeTrackDecisionTarget("reject", {
|
|
9404
|
+
acceptAll: () => executeTrackDecisionTarget("accept", { kind: "all" }, null),
|
|
9405
|
+
rejectAll: () => executeTrackDecisionTarget("reject", { kind: "all" }, null),
|
|
8180
9406
|
next: () => navigateTrackChange(1),
|
|
8181
9407
|
previous: () => navigateTrackChange(-1),
|
|
8182
9408
|
navigateNext: () => navigateAndScroll(1),
|
|
@@ -8314,11 +9540,12 @@ function createSuperDocUI(options) {
|
|
|
8314
9540
|
id: input,
|
|
8315
9541
|
story: void 0
|
|
8316
9542
|
} : input;
|
|
8317
|
-
|
|
9543
|
+
const target = {
|
|
8318
9544
|
kind: "id",
|
|
8319
9545
|
id,
|
|
8320
9546
|
...story ? { story } : {}
|
|
8321
|
-
}
|
|
9547
|
+
};
|
|
9548
|
+
return executeTrackDecisionTarget(kind, target, id, story);
|
|
8322
9549
|
};
|
|
8323
9550
|
const executeTrackDecisionTarget = (kind, target, changeId, story) => {
|
|
8324
9551
|
try {
|
|
@@ -8330,7 +9557,7 @@ function createSuperDocUI(options) {
|
|
|
8330
9557
|
const callTrackDecisionTarget = (kind, target, changeId, story) => {
|
|
8331
9558
|
if (reviewMutationsAreReadOnly()) return false;
|
|
8332
9559
|
const tcApi = getDoc()?.trackChanges;
|
|
8333
|
-
const isAllTarget = target.scope === "all";
|
|
9560
|
+
const isAllTarget = target.kind === "all" || target.scope === "all";
|
|
8334
9561
|
const isRangeTarget = target.kind === "range";
|
|
8335
9562
|
const isMultiIdTarget = target.kind === "ids";
|
|
8336
9563
|
if (bulkTrackDecisionBlockedReason({
|
|
@@ -8433,6 +9660,7 @@ function createSuperDocUI(options) {
|
|
|
8433
9660
|
success: false,
|
|
8434
9661
|
reason: "not-reachable"
|
|
8435
9662
|
};
|
|
9663
|
+
activateContentControlChrome(id);
|
|
8436
9664
|
return { success: true };
|
|
8437
9665
|
}
|
|
8438
9666
|
};
|
|
@@ -8567,9 +9795,9 @@ function createSuperDocUI(options) {
|
|
|
8567
9795
|
const hits = [];
|
|
8568
9796
|
for (const hit of rawHits) {
|
|
8569
9797
|
if (hit.type !== "trackedChange") {
|
|
8570
|
-
const key
|
|
8571
|
-
if (seen.has(key
|
|
8572
|
-
seen.add(key
|
|
9798
|
+
const key = `${hit.type}:${hit.id}`;
|
|
9799
|
+
if (seen.has(key)) continue;
|
|
9800
|
+
seen.add(key);
|
|
8573
9801
|
hits.push(hit);
|
|
8574
9802
|
continue;
|
|
8575
9803
|
}
|
|
@@ -8625,6 +9853,21 @@ function createSuperDocUI(options) {
|
|
|
8625
9853
|
}
|
|
8626
9854
|
entry.boundHost = null;
|
|
8627
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
|
+
*/
|
|
8628
9871
|
const releaseGeometryObserversIfStillHostless = () => {
|
|
8629
9872
|
geometryReleaseScheduled = false;
|
|
8630
9873
|
if (disposed || getHost()) return;
|
|
@@ -8761,6 +10004,7 @@ function createSuperDocUI(options) {
|
|
|
8761
10004
|
return target && typeof target === "object" ? target : null;
|
|
8762
10005
|
};
|
|
8763
10006
|
const metadataResolveKey = (id) => `metadata:resolve:${id}`;
|
|
10007
|
+
/** Resolve a metadata id to its SelectionTarget, or null when unresolved. */
|
|
8764
10008
|
const resolveMetadataTarget = (id) => {
|
|
8765
10009
|
const metaApi = getDoc()?.metadata;
|
|
8766
10010
|
if (!metaApi || typeof metaApi.resolve !== "function") return null;
|
|
@@ -8844,16 +10088,16 @@ function createSuperDocUI(options) {
|
|
|
8844
10088
|
let searchState = { ...SEARCH_UNAVAILABLE_SLICE };
|
|
8845
10089
|
const searchListeners = /* @__PURE__ */ new Set();
|
|
8846
10090
|
const getHostSearch = () => {
|
|
8847
|
-
const search
|
|
8848
|
-
return search
|
|
10091
|
+
const search = getHost()?.search;
|
|
10092
|
+
return search && typeof search === "object" ? search : null;
|
|
8849
10093
|
};
|
|
8850
10094
|
const getEditCommandSearch = () => {
|
|
8851
|
-
const search
|
|
8852
|
-
return search
|
|
10095
|
+
const search = getEditCommands()?.search;
|
|
10096
|
+
return search && typeof search === "object" ? search : null;
|
|
8853
10097
|
};
|
|
8854
10098
|
const searchIsAvailable = () => {
|
|
8855
|
-
const search
|
|
8856
|
-
if (search
|
|
10099
|
+
const search = getHostSearch();
|
|
10100
|
+
if (search && typeof search.setSession === "function") return true;
|
|
8857
10101
|
const editSearch = getEditCommandSearch();
|
|
8858
10102
|
return Boolean(editSearch) && typeof editSearch.query === "function" && typeof editSearch.getState === "function";
|
|
8859
10103
|
};
|
|
@@ -8871,7 +10115,21 @@ function createSuperDocUI(options) {
|
|
|
8871
10115
|
return searchState;
|
|
8872
10116
|
};
|
|
8873
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
|
+
*/
|
|
8874
10131
|
let releaseActiveSearchSession = null;
|
|
10132
|
+
/** Release whatever session is open, then forget it. */
|
|
8875
10133
|
const releaseSearchSession = () => {
|
|
8876
10134
|
const release = releaseActiveSearchSession;
|
|
8877
10135
|
releaseActiveSearchSession = null;
|
|
@@ -8881,6 +10139,23 @@ function createSuperDocUI(options) {
|
|
|
8881
10139
|
return null;
|
|
8882
10140
|
}, null);
|
|
8883
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
|
+
*/
|
|
8884
10159
|
const resetSearchForActiveEditorChange = () => {
|
|
8885
10160
|
searchRequestGeneration += 1;
|
|
8886
10161
|
releaseSearchSession();
|
|
@@ -9061,11 +10336,11 @@ function createSuperDocUI(options) {
|
|
|
9061
10336
|
if (!searchState.available) searchState.reason = SUPERDOC_UI_REASONS.searchUnavailable;
|
|
9062
10337
|
emitSearch();
|
|
9063
10338
|
},
|
|
9064
|
-
search: (query, options
|
|
10339
|
+
search: (query, options) => {
|
|
9065
10340
|
const host = getHostSearch();
|
|
9066
|
-
const caseSensitive = Boolean(options
|
|
9067
|
-
const includeDeletedText = options
|
|
9068
|
-
const regex = options
|
|
10341
|
+
const caseSensitive = Boolean(options?.caseSensitive);
|
|
10342
|
+
const includeDeletedText = options?.includeDeletedText === true;
|
|
10343
|
+
const regex = options?.regex === true;
|
|
9069
10344
|
const generation = ++searchRequestGeneration;
|
|
9070
10345
|
if (!host || typeof host.setSession !== "function") {
|
|
9071
10346
|
const editSearch = getEditCommandSearch();
|
|
@@ -9125,12 +10400,13 @@ function createSuperDocUI(options) {
|
|
|
9125
10400
|
releaseActiveSearchSession = () => {
|
|
9126
10401
|
if (typeof host.clear === "function") host.clear();
|
|
9127
10402
|
};
|
|
9128
|
-
|
|
10403
|
+
const result = safeCall(() => host.setSession(query, {
|
|
9129
10404
|
caseSensitive,
|
|
9130
10405
|
includeDeletedText,
|
|
9131
10406
|
...regex ? { regex: true } : {},
|
|
9132
10407
|
highlight: true
|
|
9133
|
-
}), null)
|
|
10408
|
+
}), null);
|
|
10409
|
+
applyHostSearchResult(result);
|
|
9134
10410
|
if (query.length > 0 && searchState.total === 0 && searchState.reason !== SUPERDOC_UI_REASONS.searchInvalidPattern) {
|
|
9135
10411
|
const editSearch = getEditCommandSearch();
|
|
9136
10412
|
if (editSearch && typeof editSearch.query === "function" && typeof editSearch.getState === "function") {
|
|
@@ -9260,13 +10536,13 @@ function createSuperDocUI(options) {
|
|
|
9260
10536
|
ok: false,
|
|
9261
10537
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9262
10538
|
};
|
|
9263
|
-
const result
|
|
9264
|
-
if (isPromiseLike(result
|
|
10539
|
+
const result = safeCall(() => editSearch.replace({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10540
|
+
if (isPromiseLike(result)) {
|
|
9265
10541
|
const generation = searchRequestGeneration;
|
|
9266
10542
|
syncSearchStateFromHost();
|
|
9267
10543
|
emitSearch();
|
|
9268
10544
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9269
|
-
return Promise.resolve(result
|
|
10545
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9270
10546
|
if (isCurrent()) {
|
|
9271
10547
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9272
10548
|
syncSearchStateFromHost();
|
|
@@ -9286,7 +10562,7 @@ function createSuperDocUI(options) {
|
|
|
9286
10562
|
}
|
|
9287
10563
|
syncSearchStateFromHost();
|
|
9288
10564
|
emitSearch();
|
|
9289
|
-
return mapHostReplaceResult(result
|
|
10565
|
+
return mapHostReplaceResult(result);
|
|
9290
10566
|
}
|
|
9291
10567
|
if (typeof host.replaceCurrent !== "function") return {
|
|
9292
10568
|
ok: false,
|
|
@@ -9309,13 +10585,13 @@ function createSuperDocUI(options) {
|
|
|
9309
10585
|
ok: false,
|
|
9310
10586
|
reason: SUPERDOC_UI_REASONS.operationUnavailable
|
|
9311
10587
|
};
|
|
9312
|
-
const result
|
|
9313
|
-
if (isPromiseLike(result
|
|
10588
|
+
const result = safeCall(() => editSearch.replaceAll({ replacement: typeof replacement === "string" ? replacement : "" }), null);
|
|
10589
|
+
if (isPromiseLike(result)) {
|
|
9314
10590
|
const generation = searchRequestGeneration;
|
|
9315
10591
|
syncSearchStateFromHost();
|
|
9316
10592
|
emitSearch();
|
|
9317
10593
|
const isCurrent = () => generation === searchRequestGeneration;
|
|
9318
|
-
return Promise.resolve(result
|
|
10594
|
+
return Promise.resolve(result).then((resolved) => {
|
|
9319
10595
|
if (isCurrent()) {
|
|
9320
10596
|
applyHostSearchResult(resolved, readEditCommandCanReplace());
|
|
9321
10597
|
syncSearchStateFromHost();
|
|
@@ -9335,7 +10611,7 @@ function createSuperDocUI(options) {
|
|
|
9335
10611
|
}
|
|
9336
10612
|
syncSearchStateFromHost();
|
|
9337
10613
|
emitSearch();
|
|
9338
|
-
return mapHostReplaceResult(result
|
|
10614
|
+
return mapHostReplaceResult(result);
|
|
9339
10615
|
}
|
|
9340
10616
|
if (typeof host.replaceAll !== "function") return {
|
|
9341
10617
|
ok: false,
|
|
@@ -9354,7 +10630,7 @@ function createSuperDocUI(options) {
|
|
|
9354
10630
|
getSnapshot: stylesSnap.getSnapshot,
|
|
9355
10631
|
subscribe: stylesSnap.subscribe,
|
|
9356
10632
|
observe: stylesSnap.observe,
|
|
9357
|
-
getCatalog: (options
|
|
10633
|
+
getCatalog: (options) => readStyleCatalogLive(options).value,
|
|
9358
10634
|
getQuickGallery: () => stylesSub.get().quickGallery,
|
|
9359
10635
|
getActiveParagraphStyle: () => computeActiveParagraphStyle(state.selection, getStyleCatalog().cache).style
|
|
9360
10636
|
};
|
|
@@ -9380,6 +10656,7 @@ function createSuperDocUI(options) {
|
|
|
9380
10656
|
const destroy = () => {
|
|
9381
10657
|
if (disposed) return;
|
|
9382
10658
|
disposed = true;
|
|
10659
|
+
releaseSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost, uiTrackedChangesCatalogState);
|
|
9383
10660
|
reviewMutationReconciler.reset();
|
|
9384
10661
|
clearAuthoritativeTrackChangesRetry();
|
|
9385
10662
|
if (foregroundAsyncRetryTimer) {
|
|
@@ -9500,6 +10777,12 @@ function createSuperDocUI(options) {
|
|
|
9500
10777
|
controllerRef = controller;
|
|
9501
10778
|
return controller;
|
|
9502
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
|
+
*/
|
|
9503
10786
|
var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
9504
10787
|
{
|
|
9505
10788
|
value: "Arial",
|
|
@@ -9532,6 +10815,11 @@ var DEFAULT_FONT_FAMILY_OPTIONS = [
|
|
|
9532
10815
|
previewFamily: "Verdana, sans-serif"
|
|
9533
10816
|
}
|
|
9534
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
|
+
*/
|
|
9535
10823
|
function canonicalFontFamilyName(family) {
|
|
9536
10824
|
const trimmed = family.trim();
|
|
9537
10825
|
const quote = trimmed[0];
|
|
@@ -9542,6 +10830,10 @@ function canonicalFontFamilyName(family) {
|
|
|
9542
10830
|
const comma = trimmed.indexOf(",");
|
|
9543
10831
|
return (comma === -1 ? trimmed : trimmed.slice(0, comma)).trim().replace(/^["']|["']$/g, "");
|
|
9544
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
|
+
*/
|
|
9545
10837
|
function normalizeDocumentFontOptions(raw) {
|
|
9546
10838
|
if (!Array.isArray(raw)) return [];
|
|
9547
10839
|
const out = [];
|
|
@@ -9559,6 +10851,11 @@ function normalizeDocumentFontOptions(raw) {
|
|
|
9559
10851
|
}
|
|
9560
10852
|
return out;
|
|
9561
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
|
+
*/
|
|
9562
10859
|
function normalizePickerFontOptions(raw) {
|
|
9563
10860
|
if (!Array.isArray(raw)) return [];
|
|
9564
10861
|
const out = [];
|
|
@@ -9577,6 +10874,11 @@ function normalizePickerFontOptions(raw) {
|
|
|
9577
10874
|
}
|
|
9578
10875
|
return out;
|
|
9579
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
|
+
*/
|
|
9580
10882
|
function composeFontFamilyOptions(documentOptions, pickerOptions = []) {
|
|
9581
10883
|
const out = [];
|
|
9582
10884
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9633,19 +10935,21 @@ var DEFAULT_FONT_SIZE_OPTIONS = [
|
|
|
9633
10935
|
label: "36"
|
|
9634
10936
|
}
|
|
9635
10937
|
];
|
|
10938
|
+
/** Active state for a routed command: true when its mark is live at the selection. */
|
|
9636
10939
|
function commandIsActive(descriptor, selection) {
|
|
9637
10940
|
return descriptor.activeMark ? selection.activeMarks.includes(descriptor.activeMark) : false;
|
|
9638
10941
|
}
|
|
10942
|
+
//#endregion
|
|
9639
10943
|
Object.defineProperty(exports, "BUILT_IN_COMMAND_IDS", {
|
|
9640
10944
|
enumerable: true,
|
|
9641
10945
|
get: function() {
|
|
9642
10946
|
return BUILT_IN_COMMAND_IDS;
|
|
9643
10947
|
}
|
|
9644
10948
|
});
|
|
9645
|
-
Object.defineProperty(exports, "
|
|
10949
|
+
Object.defineProperty(exports, "DOM_CLASS_NAMES", {
|
|
9646
10950
|
enumerable: true,
|
|
9647
10951
|
get: function() {
|
|
9648
|
-
return
|
|
10952
|
+
return DOM_CLASS_NAMES;
|
|
9649
10953
|
}
|
|
9650
10954
|
});
|
|
9651
10955
|
Object.defineProperty(exports, "createSuperDocUI", {
|