@beyondwork/docx-react-component 1.0.42 → 1.0.45
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/README.md +17 -0
- package/package.json +5 -4
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +333 -4
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +60 -10
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/search/search-text.ts +15 -2
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +29 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +692 -2
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +116 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +661 -42
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +63 -2
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +430 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +45 -7
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +142 -237
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +115 -12
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
|
@@ -30,7 +30,12 @@ import { resolvePageFieldDisplayText } from "../../runtime/layout/resolve-page-f
|
|
|
30
30
|
export const PAGE_CHROME_DEFAULTS = {
|
|
31
31
|
headerBandPx: 32,
|
|
32
32
|
footerBandPx: 32,
|
|
33
|
-
|
|
33
|
+
// L8 polish (2026-04-19): bumped from 24 → 48 so the workspace canvas
|
|
34
|
+
// between papers reads as a real break between discrete pages, not a
|
|
35
|
+
// seam inside one long paper. Phase D's discrete paper cards will
|
|
36
|
+
// re-tune once overlay-owned gaps ship; until then the widget spacer
|
|
37
|
+
// carries the full gap height.
|
|
38
|
+
interGapPx: 48,
|
|
34
39
|
} as const;
|
|
35
40
|
|
|
36
41
|
export function totalPageBreakGapPx(
|
|
@@ -73,6 +78,15 @@ export interface PageBreakDecorationInput {
|
|
|
73
78
|
headerPreviewByPageId?: ReadonlyMap<string, string>;
|
|
74
79
|
/** Same shape for footers. */
|
|
75
80
|
footerPreviewByPageId?: ReadonlyMap<string, string>;
|
|
81
|
+
/**
|
|
82
|
+
* L7 Phase 2 Task 2.2.4a — optional per-page block-index range so the
|
|
83
|
+
* viewport-tracking hook can translate page visibility into block indices
|
|
84
|
+
* without re-walking the surface blocks. Keyed by pageIndex (0-based).
|
|
85
|
+
* When omitted, the chrome widgets are emitted without block-index
|
|
86
|
+
* attributes and the hook falls back to its ESTIMATED_BLOCKS_PER_PAGE
|
|
87
|
+
* heuristic.
|
|
88
|
+
*/
|
|
89
|
+
blockIndexRangeByPageIndex?: ReadonlyMap<number, { first: number; last: number }>;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
export function buildPageBreakDecorations(
|
|
@@ -88,6 +102,7 @@ export function buildPageBreakDecorations(
|
|
|
88
102
|
const interGapPx = input.interGapPx ?? PAGE_CHROME_DEFAULTS.interGapPx;
|
|
89
103
|
|
|
90
104
|
const decorations: Decoration[] = [];
|
|
105
|
+
|
|
91
106
|
for (let i = 1; i < graph.pages.length; i += 1) {
|
|
92
107
|
const prev = graph.pages[i - 1]!;
|
|
93
108
|
const next = graph.pages[i]!;
|
|
@@ -106,6 +121,8 @@ export function buildPageBreakDecorations(
|
|
|
106
121
|
const nextHeaderPreview =
|
|
107
122
|
input.headerPreviewByPageId?.get(next.pageId) ?? "";
|
|
108
123
|
|
|
124
|
+
const nextBlockRange = input.blockIndexRangeByPageIndex?.get(next.pageIndex);
|
|
125
|
+
|
|
109
126
|
decorations.push(
|
|
110
127
|
Decoration.widget(
|
|
111
128
|
pmOffset,
|
|
@@ -125,6 +142,8 @@ export function buildPageBreakDecorations(
|
|
|
125
142
|
hasNextHeaderStory: Boolean(next.stories.header),
|
|
126
143
|
prevFooterPreview,
|
|
127
144
|
nextHeaderPreview,
|
|
145
|
+
nextPageFirstBlockIndex: nextBlockRange?.first ?? -1,
|
|
146
|
+
nextPageLastBlockIndex: nextBlockRange?.last ?? -1,
|
|
128
147
|
}),
|
|
129
148
|
{
|
|
130
149
|
side: -1,
|
|
@@ -176,6 +195,16 @@ interface ChromeWidgetInput {
|
|
|
176
195
|
hasNextHeaderStory: boolean;
|
|
177
196
|
prevFooterPreview: string;
|
|
178
197
|
nextHeaderPreview: string;
|
|
198
|
+
/**
|
|
199
|
+
* L7 Phase 2 Task 2.2.4a — inclusive first block index of the next page.
|
|
200
|
+
* -1 when block-index info was not supplied to the decoration builder.
|
|
201
|
+
*/
|
|
202
|
+
nextPageFirstBlockIndex: number;
|
|
203
|
+
/**
|
|
204
|
+
* L7 Phase 2 Task 2.2.4a — inclusive last block index of the next page.
|
|
205
|
+
* -1 when block-index info was not supplied to the decoration builder.
|
|
206
|
+
*/
|
|
207
|
+
nextPageLastBlockIndex: number;
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
// P14.c — cache the widget DOM by input identity. PM rebuilds the
|
|
@@ -205,6 +234,8 @@ function widgetCacheKey(input: ChromeWidgetInput): string {
|
|
|
205
234
|
input.hasNextHeaderStory ? "1" : "0",
|
|
206
235
|
input.prevFooterPreview,
|
|
207
236
|
input.nextHeaderPreview,
|
|
237
|
+
input.nextPageFirstBlockIndex,
|
|
238
|
+
input.nextPageLastBlockIndex,
|
|
208
239
|
].join("\x1f");
|
|
209
240
|
}
|
|
210
241
|
|
|
@@ -238,14 +269,27 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
|
238
269
|
root.setAttribute("data-posture", input.posture);
|
|
239
270
|
root.setAttribute("data-prev-page-id", input.prevPageId);
|
|
240
271
|
root.setAttribute("data-next-page-id", input.nextPageId);
|
|
241
|
-
|
|
242
|
-
|
|
272
|
+
// NOTE: data-prev-page-index and data-next-page-index were removed — they had
|
|
273
|
+
// no reader after data-page-frame was added (Task 2.2.4a). The page-id attrs
|
|
274
|
+
// above are still consumed by the chrome overlay (see line ~265 in this file).
|
|
243
275
|
// P3.a: expose page-frame boundary markers so the page stack and tests
|
|
244
276
|
// can enumerate pages without re-walking the render graph. Each widget
|
|
245
277
|
// ends page N (`prev`) and starts page N+1 (`next`); the outer workspace
|
|
246
278
|
// frame still accounts for the boundaries at both ends of the document.
|
|
247
279
|
root.setAttribute("data-page-frame-end", input.prevPageId);
|
|
248
280
|
root.setAttribute("data-page-frame-start", input.nextPageId);
|
|
281
|
+
// L7 Phase 2 Task 2.2.4a — per-page markers for the viewport-tracking hook
|
|
282
|
+
// (`useVisibleBlockRange`). Each boundary widget identifies the page that
|
|
283
|
+
// STARTS at this boundary (i.e. `nextPageIndex`). The hook's
|
|
284
|
+
// IntersectionObserver picks these up via `[data-page-frame]` to compute the
|
|
285
|
+
// visible block range. Block-index attributes are set only when the caller
|
|
286
|
+
// supplied a `blockIndexRangeByPageIndex` map (value ≥ 0); otherwise they
|
|
287
|
+
// are omitted so the hook's fallback estimate is used cleanly.
|
|
288
|
+
root.setAttribute("data-page-frame", String(input.nextPageIndex));
|
|
289
|
+
if (input.nextPageFirstBlockIndex >= 0) {
|
|
290
|
+
root.setAttribute("data-page-first-block-index", String(input.nextPageFirstBlockIndex));
|
|
291
|
+
root.setAttribute("data-page-last-block-index", String(input.nextPageLastBlockIndex));
|
|
292
|
+
}
|
|
249
293
|
root.contentEditable = "false";
|
|
250
294
|
root.setAttribute("aria-hidden", "false");
|
|
251
295
|
root.style.display = "block";
|
|
@@ -286,175 +330,42 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
|
286
330
|
return root;
|
|
287
331
|
}
|
|
288
332
|
|
|
289
|
-
//
|
|
333
|
+
// L8 Phase B (2026-04-19): the page-posture widget is a **transparent
|
|
334
|
+
// spacer**. Inline band / gap / band chrome is retired — the real H/F
|
|
335
|
+
// bands are painted by `TwPageStackChromeLayer` as per-page overlays
|
|
336
|
+
// (P8.11). The spacer keeps the same total height as the retired stack
|
|
337
|
+
// so `TwPageStackOverlayLayer.measureWidgetsViaBoundingRect` continues to
|
|
338
|
+
// produce the same page-overlay rect math; L8 Phase D will take over
|
|
339
|
+
// painting the gap and reduce this to 0.
|
|
340
|
+
//
|
|
341
|
+
// L8 polish (2026-04-19): the spacer carries a subtle, borderless
|
|
342
|
+
// page-count label centered in the inter-page gap so the workspace
|
|
343
|
+
// canvas reads as "two papers with a page number between them" — no
|
|
344
|
+
// pill, no border, just small muted text.
|
|
290
345
|
root.style.height = `${
|
|
291
346
|
input.footerBandPx + input.interGapPx + input.headerBandPx
|
|
292
347
|
}px`;
|
|
293
348
|
root.style.position = "relative";
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
kind: "footer",
|
|
297
|
-
pageId: input.prevPageId,
|
|
298
|
-
pageIndex: input.prevPageIndex,
|
|
299
|
-
pageLabel: input.prevPageLabel,
|
|
300
|
-
bandPx: input.footerBandPx,
|
|
301
|
-
position: "top",
|
|
302
|
-
hasStory: input.hasPrevFooterStory,
|
|
303
|
-
previewText: input.prevFooterPreview,
|
|
304
|
-
});
|
|
305
|
-
root.appendChild(footer);
|
|
306
|
-
|
|
307
|
-
// P3.a: the inter-page gap is now a visible canvas strip that reads as
|
|
308
|
-
// "the space between two papers", not a subtle gradient inside one white
|
|
309
|
-
// page. The strip paints in the workspace canvas color (the same color
|
|
310
|
-
// the page frames float on in page mode), with subtle drop/rise shadows
|
|
311
|
-
// on either edge so the preceding footer reads as "bottom of page N's
|
|
312
|
-
// paper" and the following header reads as "top of page N+1's paper".
|
|
313
|
-
//
|
|
314
|
-
// The visual goal: bringing the user closer to a Word-native perception
|
|
315
|
-
// of distinct pages without requiring the PM editable tree to be split
|
|
316
|
-
// into per-page subtrees (that lands in P3.b).
|
|
317
|
-
const separator = document.createElement("div");
|
|
318
|
-
separator.className = "wre-page-chrome-separator";
|
|
319
|
-
separator.setAttribute("data-kind", "page-chrome-separator");
|
|
320
|
-
separator.style.position = "absolute";
|
|
321
|
-
separator.style.left = "0";
|
|
322
|
-
separator.style.right = "0";
|
|
323
|
-
separator.style.top = `${input.footerBandPx}px`;
|
|
324
|
-
separator.style.height = `${input.interGapPx}px`;
|
|
325
|
-
// Canvas color (same as the scroll root's bg-surface) so the strip reads
|
|
326
|
-
// as "gap between two papers". Page mode and canvas mode both use the
|
|
327
|
-
// same token so the UX remains consistent at any chrome preset.
|
|
328
|
-
separator.style.backgroundColor = "var(--color-surface, #f1f5f9)";
|
|
329
|
-
// Inner shadows on top/bottom: the previous footer's bottom edge gains
|
|
330
|
-
// a subtle paper-edge shadow, and the next header's top edge likewise.
|
|
331
|
-
// Inset shadows let us avoid touching the footer / header DOM while
|
|
332
|
-
// keeping the shadows flush with the band borders.
|
|
333
|
-
separator.style.boxShadow = [
|
|
334
|
-
// Top edge — simulates the bottom shadow of page N's paper.
|
|
335
|
-
"inset 0 1px 0 rgba(15, 23, 42, 0.06)",
|
|
336
|
-
"inset 0 2px 3px -2px rgba(15, 23, 42, 0.12)",
|
|
337
|
-
// Bottom edge — simulates the top shadow of page N+1's paper.
|
|
338
|
-
"inset 0 -1px 0 rgba(15, 23, 42, 0.06)",
|
|
339
|
-
"inset 0 -2px 3px -2px rgba(15, 23, 42, 0.12)",
|
|
340
|
-
].join(", ");
|
|
341
|
-
root.appendChild(separator);
|
|
342
|
-
|
|
343
|
-
const header = buildBand({
|
|
344
|
-
kind: "header",
|
|
345
|
-
pageId: input.nextPageId,
|
|
346
|
-
pageIndex: input.nextPageIndex,
|
|
347
|
-
pageLabel: input.nextPageLabel,
|
|
348
|
-
bandPx: input.headerBandPx,
|
|
349
|
-
position: "bottom",
|
|
350
|
-
topOffsetPx: input.footerBandPx + input.interGapPx,
|
|
351
|
-
hasStory: input.hasNextHeaderStory,
|
|
352
|
-
previewText: input.nextHeaderPreview,
|
|
353
|
-
});
|
|
354
|
-
root.appendChild(header);
|
|
355
|
-
|
|
356
|
-
return root;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function buildBand(input: {
|
|
360
|
-
kind: "header" | "footer";
|
|
361
|
-
pageId: string;
|
|
362
|
-
pageIndex: number;
|
|
363
|
-
pageLabel: string;
|
|
364
|
-
bandPx: number;
|
|
365
|
-
position: "top" | "bottom";
|
|
366
|
-
topOffsetPx?: number;
|
|
367
|
-
hasStory: boolean;
|
|
368
|
-
previewText: string;
|
|
369
|
-
}): HTMLElement {
|
|
370
|
-
const band = document.createElement("div");
|
|
371
|
-
band.className = `wre-page-chrome-band wre-page-chrome-band-${input.kind}`;
|
|
372
|
-
band.setAttribute("data-band-kind", input.kind);
|
|
373
|
-
band.setAttribute("data-page-id", input.pageId);
|
|
374
|
-
band.setAttribute("data-page-index", String(input.pageIndex));
|
|
375
|
-
band.style.position = "absolute";
|
|
376
|
-
band.style.left = "0";
|
|
377
|
-
band.style.right = "0";
|
|
378
|
-
band.style.top = `${input.topOffsetPx ?? 0}px`;
|
|
379
|
-
band.style.height = `${input.bandPx}px`;
|
|
380
|
-
band.style.display = "flex";
|
|
381
|
-
band.style.alignItems = "center";
|
|
382
|
-
band.style.justifyContent = "space-between";
|
|
383
|
-
band.style.padding = "0 16px";
|
|
384
|
-
band.style.fontSize = "10px";
|
|
385
|
-
band.style.letterSpacing = "0.12em";
|
|
386
|
-
band.style.textTransform = "uppercase";
|
|
387
|
-
band.style.color = "var(--color-text-tertiary, #6b7280)";
|
|
388
|
-
band.style.backgroundColor =
|
|
389
|
-
"var(--color-surface-subtle, rgba(0,0,0,0.02))";
|
|
390
|
-
band.style.borderTop =
|
|
391
|
-
input.kind === "header"
|
|
392
|
-
? "1px solid var(--color-border, rgba(0,0,0,0.08))"
|
|
393
|
-
: "none";
|
|
394
|
-
band.style.borderBottom =
|
|
395
|
-
input.kind === "footer"
|
|
396
|
-
? "1px solid var(--color-border, rgba(0,0,0,0.08))"
|
|
397
|
-
: "none";
|
|
398
|
-
// Bands are interactive: double-click fires a custom event the shell
|
|
399
|
-
// forwards to `runtime.openStory()`.
|
|
400
|
-
band.style.pointerEvents = "auto";
|
|
401
|
-
band.style.cursor = input.hasStory ? "pointer" : "default";
|
|
402
|
-
band.title = input.hasStory
|
|
403
|
-
? `Double-click to edit ${input.kind}`
|
|
404
|
-
: `No ${input.kind} defined for this page`;
|
|
349
|
+
root.style.pointerEvents = "none";
|
|
350
|
+
root.setAttribute("aria-hidden", "true");
|
|
405
351
|
|
|
406
352
|
const label = document.createElement("span");
|
|
407
|
-
label.className = "wre-page-chrome-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
} else {
|
|
422
|
-
label.textContent = input.kind === "header" ? "Header" : "Footer";
|
|
423
|
-
}
|
|
424
|
-
band.appendChild(label);
|
|
425
|
-
|
|
426
|
-
const pageLabel = document.createElement("span");
|
|
427
|
-
pageLabel.className = "wre-page-chrome-band-page";
|
|
428
|
-
pageLabel.textContent = input.pageLabel;
|
|
429
|
-
band.appendChild(pageLabel);
|
|
353
|
+
label.className = "wre-page-chrome-gap-label";
|
|
354
|
+
label.setAttribute("data-kind", "page-gap-label");
|
|
355
|
+
label.textContent = input.nextPageLabel;
|
|
356
|
+
label.style.position = "absolute";
|
|
357
|
+
label.style.left = "50%";
|
|
358
|
+
label.style.top = "50%";
|
|
359
|
+
label.style.transform = "translate(-50%, -50%)";
|
|
360
|
+
label.style.fontSize = "10px";
|
|
361
|
+
label.style.letterSpacing = "0.12em";
|
|
362
|
+
label.style.textTransform = "uppercase";
|
|
363
|
+
label.style.color = "var(--color-tertiary, #6b7280)";
|
|
364
|
+
// Intentionally no background, no border, no padding — the label floats
|
|
365
|
+
// transparently in the workspace-canvas gap.
|
|
366
|
+
root.appendChild(label);
|
|
430
367
|
|
|
431
|
-
|
|
432
|
-
band.addEventListener("dblclick", (event) => {
|
|
433
|
-
event.stopPropagation();
|
|
434
|
-
event.preventDefault();
|
|
435
|
-
const eventName =
|
|
436
|
-
input.kind === "header"
|
|
437
|
-
? "wre-open-header-story-for-page"
|
|
438
|
-
: "wre-open-footer-story-for-page";
|
|
439
|
-
// Use the band's owning document's `CustomEvent` constructor so the
|
|
440
|
-
// event passes through jsdom's instance-of check. In a real browser
|
|
441
|
-
// `band.ownerDocument.defaultView.CustomEvent` is the same as the
|
|
442
|
-
// global `CustomEvent`; in jsdom the two differ and the global one
|
|
443
|
-
// fails `dispatchEvent`'s internal Event-type convert step.
|
|
444
|
-
const view = band.ownerDocument?.defaultView as
|
|
445
|
-
| (Window & typeof globalThis)
|
|
446
|
-
| null;
|
|
447
|
-
const Ctor = view?.CustomEvent ?? CustomEvent;
|
|
448
|
-
band.dispatchEvent(
|
|
449
|
-
new Ctor(eventName, {
|
|
450
|
-
bubbles: true,
|
|
451
|
-
detail: { pageIndex: input.pageIndex, pageId: input.pageId },
|
|
452
|
-
}),
|
|
453
|
-
);
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return band;
|
|
368
|
+
return root;
|
|
458
369
|
}
|
|
459
370
|
|
|
460
371
|
/**
|
|
@@ -160,9 +160,26 @@ export const editorSchema = new Schema({
|
|
|
160
160
|
bidi: { default: null },
|
|
161
161
|
pageBreakBefore: { default: null },
|
|
162
162
|
hiddenTextOnly: { default: null },
|
|
163
|
+
placeholderCulled: { default: null },
|
|
164
|
+
blockId: { default: null },
|
|
163
165
|
},
|
|
164
166
|
parseDOM: [{ tag: "p" }],
|
|
165
167
|
toDOM(node) {
|
|
168
|
+
// Viewport-culled placeholder paragraph — cheap size-preserving leaf.
|
|
169
|
+
if (node.attrs.placeholderCulled) {
|
|
170
|
+
return [
|
|
171
|
+
"div",
|
|
172
|
+
{
|
|
173
|
+
"data-node-type": "paragraph-placeholder",
|
|
174
|
+
"data-placeholder-culled": "true",
|
|
175
|
+
"data-placeholder-size": String(node.nodeSize),
|
|
176
|
+
"data-placeholder-block-id": node.attrs.blockId ?? "",
|
|
177
|
+
style: "min-height: 20px; contain: strict;",
|
|
178
|
+
"aria-hidden": "true",
|
|
179
|
+
},
|
|
180
|
+
0,
|
|
181
|
+
];
|
|
182
|
+
}
|
|
166
183
|
const classes: string[] = ["leading-relaxed"];
|
|
167
184
|
const styleId = node.attrs.styleId as string | null;
|
|
168
185
|
const outlineLevel = node.attrs.outlineLevel as number | null;
|
|
@@ -634,16 +651,51 @@ export const editorSchema = new Schema({
|
|
|
634
651
|
selectable: false,
|
|
635
652
|
attrs: {
|
|
636
653
|
previewMediaId: { default: null },
|
|
654
|
+
previewSrc: { default: null },
|
|
637
655
|
detail: { default: null },
|
|
638
656
|
},
|
|
639
657
|
toDOM(node) {
|
|
658
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
659
|
+
const detail = (node.attrs.detail as string) ?? "Chart";
|
|
660
|
+
if (previewSrc) {
|
|
661
|
+
// Bitmap-backed: render the fallback image Word cached in mc:Fallback.
|
|
662
|
+
// The corner chip preserves the typed identity so agents and humans
|
|
663
|
+
// still see "this is a chart" at a glance.
|
|
664
|
+
return [
|
|
665
|
+
"span",
|
|
666
|
+
{
|
|
667
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
668
|
+
"data-node-type": "chart_atom",
|
|
669
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
670
|
+
contenteditable: "false",
|
|
671
|
+
title: detail,
|
|
672
|
+
},
|
|
673
|
+
[
|
|
674
|
+
"img",
|
|
675
|
+
{
|
|
676
|
+
src: previewSrc,
|
|
677
|
+
alt: detail,
|
|
678
|
+
class: "block max-w-full h-auto",
|
|
679
|
+
draggable: "false",
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
[
|
|
683
|
+
"span",
|
|
684
|
+
{
|
|
685
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-blue-200 bg-blue-50/90 px-1 py-0.5 text-[10px] text-blue-700",
|
|
686
|
+
"aria-hidden": "true",
|
|
687
|
+
},
|
|
688
|
+
"\uD83D\uDCC8 Chart",
|
|
689
|
+
],
|
|
690
|
+
];
|
|
691
|
+
}
|
|
640
692
|
return [
|
|
641
693
|
"span",
|
|
642
694
|
{
|
|
643
695
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-blue-700 bg-blue-50 border border-blue-200",
|
|
644
696
|
"data-node-type": "chart_atom",
|
|
645
697
|
contenteditable: "false",
|
|
646
|
-
title:
|
|
698
|
+
title: detail,
|
|
647
699
|
},
|
|
648
700
|
"\uD83D\uDCC8 Chart",
|
|
649
701
|
];
|
|
@@ -657,16 +709,48 @@ export const editorSchema = new Schema({
|
|
|
657
709
|
selectable: false,
|
|
658
710
|
attrs: {
|
|
659
711
|
previewMediaId: { default: null },
|
|
712
|
+
previewSrc: { default: null },
|
|
660
713
|
detail: { default: null },
|
|
661
714
|
},
|
|
662
715
|
toDOM(node) {
|
|
716
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
717
|
+
const detail = (node.attrs.detail as string) ?? "SmartArt";
|
|
718
|
+
if (previewSrc) {
|
|
719
|
+
return [
|
|
720
|
+
"span",
|
|
721
|
+
{
|
|
722
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
723
|
+
"data-node-type": "smartart_atom",
|
|
724
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
725
|
+
contenteditable: "false",
|
|
726
|
+
title: detail,
|
|
727
|
+
},
|
|
728
|
+
[
|
|
729
|
+
"img",
|
|
730
|
+
{
|
|
731
|
+
src: previewSrc,
|
|
732
|
+
alt: detail,
|
|
733
|
+
class: "block max-w-full h-auto",
|
|
734
|
+
draggable: "false",
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
[
|
|
738
|
+
"span",
|
|
739
|
+
{
|
|
740
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-purple-200 bg-purple-50/90 px-1 py-0.5 text-[10px] text-purple-700",
|
|
741
|
+
"aria-hidden": "true",
|
|
742
|
+
},
|
|
743
|
+
"\uD83D\uDDFA SmartArt",
|
|
744
|
+
],
|
|
745
|
+
];
|
|
746
|
+
}
|
|
663
747
|
return [
|
|
664
748
|
"span",
|
|
665
749
|
{
|
|
666
750
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-purple-700 bg-purple-50 border border-purple-200",
|
|
667
751
|
"data-node-type": "smartart_atom",
|
|
668
752
|
contenteditable: "false",
|
|
669
|
-
title:
|
|
753
|
+
title: detail,
|
|
670
754
|
},
|
|
671
755
|
"\uD83D\uDDFA SmartArt",
|
|
672
756
|
];
|
|
@@ -707,17 +791,50 @@ export const editorSchema = new Schema({
|
|
|
707
791
|
attrs: {
|
|
708
792
|
text: { default: "" },
|
|
709
793
|
geometry: { default: null },
|
|
794
|
+
previewMediaId: { default: null },
|
|
795
|
+
previewSrc: { default: null },
|
|
710
796
|
detail: { default: null },
|
|
711
797
|
},
|
|
712
798
|
toDOM(node) {
|
|
713
799
|
const text = node.attrs.text as string;
|
|
800
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
801
|
+
const detail = (node.attrs.detail as string) ?? (text ? `WordArt: ${text}` : "WordArt");
|
|
802
|
+
if (previewSrc) {
|
|
803
|
+
return [
|
|
804
|
+
"span",
|
|
805
|
+
{
|
|
806
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
807
|
+
"data-node-type": "wordart_atom",
|
|
808
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
809
|
+
contenteditable: "false",
|
|
810
|
+
title: detail,
|
|
811
|
+
},
|
|
812
|
+
[
|
|
813
|
+
"img",
|
|
814
|
+
{
|
|
815
|
+
src: previewSrc,
|
|
816
|
+
alt: detail,
|
|
817
|
+
class: "block max-w-full h-auto",
|
|
818
|
+
draggable: "false",
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
[
|
|
822
|
+
"span",
|
|
823
|
+
{
|
|
824
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-orange-200 bg-orange-50/90 px-1 py-0.5 text-[10px] text-orange-700",
|
|
825
|
+
"aria-hidden": "true",
|
|
826
|
+
},
|
|
827
|
+
"\u2728 WordArt",
|
|
828
|
+
],
|
|
829
|
+
];
|
|
830
|
+
}
|
|
714
831
|
return [
|
|
715
832
|
"span",
|
|
716
833
|
{
|
|
717
834
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-orange-700 bg-orange-50 border border-orange-200 font-medium italic",
|
|
718
835
|
"data-node-type": "wordart_atom",
|
|
719
836
|
contenteditable: "false",
|
|
720
|
-
title:
|
|
837
|
+
title: detail,
|
|
721
838
|
},
|
|
722
839
|
"\u2728 " + (text || "WordArt"),
|
|
723
840
|
];
|
|
@@ -732,18 +849,51 @@ export const editorSchema = new Schema({
|
|
|
732
849
|
attrs: {
|
|
733
850
|
text: { default: null },
|
|
734
851
|
shapeType: { default: null },
|
|
852
|
+
previewMediaId: { default: null },
|
|
853
|
+
previewSrc: { default: null },
|
|
735
854
|
detail: { default: null },
|
|
736
855
|
},
|
|
737
856
|
toDOM(node) {
|
|
738
857
|
const text = node.attrs.text as string | null;
|
|
739
858
|
const label = text ? `VML: ${text}` : "VML shape";
|
|
859
|
+
const previewSrc = node.attrs.previewSrc as string | null;
|
|
860
|
+
const detail = (node.attrs.detail as string) ?? label;
|
|
861
|
+
if (previewSrc) {
|
|
862
|
+
return [
|
|
863
|
+
"span",
|
|
864
|
+
{
|
|
865
|
+
class: "relative inline-block align-baseline mx-0.5 max-w-full",
|
|
866
|
+
"data-node-type": "vml_atom",
|
|
867
|
+
"data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
|
|
868
|
+
contenteditable: "false",
|
|
869
|
+
title: detail,
|
|
870
|
+
},
|
|
871
|
+
[
|
|
872
|
+
"img",
|
|
873
|
+
{
|
|
874
|
+
src: previewSrc,
|
|
875
|
+
alt: detail,
|
|
876
|
+
class: "block max-w-full h-auto",
|
|
877
|
+
draggable: "false",
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
[
|
|
881
|
+
"span",
|
|
882
|
+
{
|
|
883
|
+
class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-gray-300 bg-gray-100/90 px-1 py-0.5 text-[10px] text-gray-600",
|
|
884
|
+
"aria-hidden": "true",
|
|
885
|
+
},
|
|
886
|
+
"\u25A6 VML",
|
|
887
|
+
],
|
|
888
|
+
];
|
|
889
|
+
}
|
|
740
890
|
return [
|
|
741
891
|
"span",
|
|
742
892
|
{
|
|
743
893
|
class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-gray-600 bg-gray-100 border border-gray-300",
|
|
744
894
|
"data-node-type": "vml_atom",
|
|
745
895
|
contenteditable: "false",
|
|
746
|
-
title:
|
|
896
|
+
title: detail,
|
|
747
897
|
},
|
|
748
898
|
"\u25A6 " + label,
|
|
749
899
|
];
|
|
@@ -759,10 +909,26 @@ export const editorSchema = new Schema({
|
|
|
759
909
|
warningId: { default: "" },
|
|
760
910
|
label: { default: "Locked" },
|
|
761
911
|
detail: { default: "" },
|
|
912
|
+
presentation: { default: "callout" },
|
|
913
|
+
placeholderSize: { default: null },
|
|
762
914
|
},
|
|
763
915
|
toDOM(node) {
|
|
764
916
|
const fragmentId = node.attrs.fragmentId as string;
|
|
765
917
|
const isPreview = fragmentId.startsWith("preview:");
|
|
918
|
+
const presentation = node.attrs.presentation as string;
|
|
919
|
+
if (presentation === "quiet-marker") {
|
|
920
|
+
return [
|
|
921
|
+
"div",
|
|
922
|
+
{
|
|
923
|
+
class: "block h-0 w-0 overflow-hidden",
|
|
924
|
+
contenteditable: "false",
|
|
925
|
+
"data-node-type": "opaque_block",
|
|
926
|
+
"data-block-presentation": "quiet-marker",
|
|
927
|
+
title: node.attrs.detail as string,
|
|
928
|
+
"aria-label": node.attrs.label as string,
|
|
929
|
+
},
|
|
930
|
+
];
|
|
931
|
+
}
|
|
766
932
|
return [
|
|
767
933
|
"div",
|
|
768
934
|
{
|