@storystick/preview 0.0.0-stage → 0.2.0-rc.1
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/LICENSE +112 -0
- package/PUBLIC-API.md +56 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +92 -0
- package/dist/authoring/runtime-contract.js +28 -0
- package/dist/channel-taxonomy.d.ts +17 -0
- package/dist/channel-taxonomy.js +46 -0
- package/dist/context-command.d.ts +18 -0
- package/dist/context-command.js +38 -0
- package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
- package/dist/dev/suspicious-codeword-detector.js +220 -0
- package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
- package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
- package/dist/framework/browser-context.d.ts +2 -0
- package/dist/framework/browser-context.js +2 -0
- package/dist/framework/generation-refresh.d.ts +23 -0
- package/dist/framework/generation-refresh.js +82 -0
- package/dist/framework/generation-signal.d.ts +8 -0
- package/dist/framework/generation-signal.js +17 -0
- package/dist/framework/projection-generation-watch.d.ts +5 -0
- package/dist/framework/projection-generation-watch.js +53 -0
- package/dist/framework/render-receipt.d.ts +8 -0
- package/dist/framework/render-receipt.js +10 -0
- package/dist/framework/runtime-adapter-store.d.ts +30 -0
- package/dist/framework/runtime-adapter-store.js +81 -0
- package/dist/grant-coordinator.d.ts +53 -0
- package/dist/grant-coordinator.js +329 -0
- package/dist/grant-lifecycle.d.ts +52 -0
- package/dist/grant-lifecycle.js +74 -0
- package/dist/identity/__test-adapter.d.ts +34 -0
- package/dist/identity/__test-adapter.js +59 -0
- package/dist/identity/browser-context-gate.d.ts +4 -0
- package/dist/identity/browser-context-gate.js +22 -0
- package/dist/identity/browser-context.d.ts +13 -0
- package/dist/identity/browser-context.js +22 -0
- package/dist/identity/carriers/postmessage.d.ts +84 -0
- package/dist/identity/carriers/postmessage.js +177 -0
- package/dist/identity/carriers/storage-state.d.ts +41 -0
- package/dist/identity/carriers/storage-state.js +99 -0
- package/dist/identity/carriers/storage.d.ts +39 -0
- package/dist/identity/carriers/storage.js +182 -0
- package/dist/identity/carriers/url.d.ts +19 -0
- package/dist/identity/carriers/url.js +38 -0
- package/dist/identity/identity-state-machine.d.ts +70 -0
- package/dist/identity/identity-state-machine.js +244 -0
- package/dist/identity/identity-types.d.ts +192 -0
- package/dist/identity/identity-types.js +22 -0
- package/dist/identity/index.d.ts +18 -0
- package/dist/identity/index.js +16 -0
- package/dist/identity/preview-cookie.d.ts +9 -0
- package/dist/identity/preview-cookie.js +12 -0
- package/dist/identity/preview-draft.d.ts +6 -0
- package/dist/identity/preview-draft.js +25 -0
- package/dist/identity/preview-identity.d.ts +28 -0
- package/dist/identity/preview-identity.js +84 -0
- package/dist/identity/preview-scope.d.ts +28 -0
- package/dist/identity/preview-scope.js +47 -0
- package/dist/identity/resolver.d.ts +110 -0
- package/dist/identity/resolver.js +334 -0
- package/dist/identity/session-query.d.ts +4 -0
- package/dist/identity/session-query.js +19 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +788 -0
- package/dist/interaction-mode.d.ts +58 -0
- package/dist/interaction-mode.js +93 -0
- package/dist/nav-handler.d.ts +95 -0
- package/dist/nav-handler.js +84 -0
- package/dist/overlay-label.d.ts +79 -0
- package/dist/overlay-label.js +73 -0
- package/dist/page-presentation.d.ts +24 -0
- package/dist/page-presentation.js +32 -0
- package/dist/provenance/anchor-map.d.ts +95 -0
- package/dist/provenance/anchor-map.js +177 -0
- package/dist/provenance/carrier-plan.d.ts +51 -0
- package/dist/provenance/carrier-plan.js +90 -0
- package/dist/provenance/coded-text.d.ts +20 -0
- package/dist/provenance/coded-text.js +302 -0
- package/dist/provenance/direct-edit-locator.d.ts +29 -0
- package/dist/provenance/direct-edit-locator.js +218 -0
- package/dist/provenance/editable-leaf.d.ts +134 -0
- package/dist/provenance/editable-leaf.js +483 -0
- package/dist/provenance/editor-target.d.ts +29 -0
- package/dist/provenance/editor-target.js +46 -0
- package/dist/provenance/field-leaf-walker.d.ts +21 -0
- package/dist/provenance/field-leaf-walker.js +98 -0
- package/dist/provenance/frame-dependencies.d.ts +212 -0
- package/dist/provenance/frame-dependencies.js +550 -0
- package/dist/provenance/index.d.ts +26 -0
- package/dist/provenance/index.js +26 -0
- package/dist/provenance/local-events.d.ts +1 -0
- package/dist/provenance/local-events.js +1 -0
- package/dist/provenance/markers.d.ts +21 -0
- package/dist/provenance/markers.js +70 -0
- package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
- package/dist/provenance/multi-frame-anchor-map.js +109 -0
- package/dist/provenance/normalize.d.ts +2 -0
- package/dist/provenance/normalize.js +19 -0
- package/dist/provenance/overlay-focus-echo.d.ts +17 -0
- package/dist/provenance/overlay-focus-echo.js +16 -0
- package/dist/provenance/overlay-projection.d.ts +65 -0
- package/dist/provenance/overlay-projection.js +201 -0
- package/dist/provenance/owner-map.d.ts +55 -0
- package/dist/provenance/owner-map.js +239 -0
- package/dist/provenance/read-carrier.d.ts +22 -0
- package/dist/provenance/read-carrier.js +92 -0
- package/dist/provenance/recorder.d.ts +38 -0
- package/dist/provenance/recorder.js +82 -0
- package/dist/provenance/render-context.d.ts +106 -0
- package/dist/provenance/render-context.js +338 -0
- package/dist/provenance/reveal-contract.d.ts +44 -0
- package/dist/provenance/reveal-contract.js +1 -0
- package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
- package/dist/provenance/richtext-slice-embed-address.js +35 -0
- package/dist/provenance/runtime-script.d.ts +1 -0
- package/dist/provenance/runtime-script.js +2217 -0
- package/dist/provenance/sidecar-store.d.ts +63 -0
- package/dist/provenance/sidecar-store.js +151 -0
- package/dist/provenance/sidecar.d.ts +34 -0
- package/dist/provenance/sidecar.js +227 -0
- package/dist/provenance/slice-model.d.ts +28 -0
- package/dist/provenance/slice-model.js +86 -0
- package/dist/provenance/source-identity.d.ts +3 -0
- package/dist/provenance/source-identity.js +7 -0
- package/dist/provenance/source-map-materializer.d.ts +76 -0
- package/dist/provenance/source-map-materializer.js +286 -0
- package/dist/provenance/source-registry.d.ts +103 -0
- package/dist/provenance/source-registry.js +562 -0
- package/dist/provenance/transformed-output-gate.d.ts +161 -0
- package/dist/provenance/transformed-output-gate.js +171 -0
- package/dist/provenance/types.d.ts +102 -0
- package/dist/provenance/types.js +1 -0
- package/dist/refresh-policy.d.ts +11 -0
- package/dist/refresh-policy.js +14 -0
- package/dist/reveal-target.d.ts +8 -0
- package/dist/reveal-target.js +47 -0
- package/dist/route-paths.d.ts +11 -0
- package/dist/route-paths.js +14 -0
- package/dist/router-subscriber.d.ts +92 -0
- package/dist/router-subscriber.js +44 -0
- package/dist/runtime/anchor-types.d.ts +5 -0
- package/dist/runtime/anchor-types.js +1 -0
- package/dist/runtime/authority-store.d.ts +31 -0
- package/dist/runtime/authority-store.js +56 -0
- package/dist/runtime/boot.d.ts +52 -0
- package/dist/runtime/boot.js +379 -0
- package/dist/runtime/content-refresh.d.ts +30 -0
- package/dist/runtime/content-refresh.js +68 -0
- package/dist/runtime/emit-runtime-error.d.ts +51 -0
- package/dist/runtime/emit-runtime-error.js +64 -0
- package/dist/runtime/field-proxy.d.ts +112 -0
- package/dist/runtime/field-proxy.js +471 -0
- package/dist/runtime/form-contract-reporter.d.ts +99 -0
- package/dist/runtime/form-contract-reporter.js +391 -0
- package/dist/runtime/invalidation-adapter.d.ts +17 -0
- package/dist/runtime/invalidation-adapter.js +42 -0
- package/dist/runtime/live-query-overlay.d.ts +39 -0
- package/dist/runtime/live-query-overlay.js +345 -0
- package/dist/runtime/momentary-navigate.d.ts +16 -0
- package/dist/runtime/momentary-navigate.js +106 -0
- package/dist/runtime/observation-scheduler.d.ts +15 -0
- package/dist/runtime/observation-scheduler.js +42 -0
- package/dist/runtime/perf-metrics.d.ts +28 -0
- package/dist/runtime/perf-metrics.js +90 -0
- package/dist/runtime/preview-interaction-policy.d.ts +1 -0
- package/dist/runtime/preview-interaction-policy.js +103 -0
- package/dist/runtime/preview-overlay-painter.d.ts +27 -0
- package/dist/runtime/preview-overlay-painter.js +1732 -0
- package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
- package/dist/runtime/preview-overlay-sidecar.js +45 -0
- package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
- package/dist/runtime/preview-perspective-alignment.js +40 -0
- package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
- package/dist/runtime/preview-perspective-intent-state.js +5 -0
- package/dist/runtime/preview-transition-state.d.ts +142 -0
- package/dist/runtime/preview-transition-state.js +257 -0
- package/dist/runtime/render-mode-state.d.ts +51 -0
- package/dist/runtime/render-mode-state.js +76 -0
- package/dist/runtime/request-id.d.ts +4 -0
- package/dist/runtime/request-id.js +8 -0
- package/dist/runtime/resolve-schema-field.d.ts +99 -0
- package/dist/runtime/resolve-schema-field.js +356 -0
- package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
- package/dist/runtime/runtime-error-dispatch.js +75 -0
- package/dist/runtime/schema-store.d.ts +121 -0
- package/dist/runtime/schema-store.js +667 -0
- package/dist/runtime/snapshot-assembly.d.ts +104 -0
- package/dist/runtime/snapshot-assembly.js +553 -0
- package/dist/runtime/snapshot-reporter.d.ts +55 -0
- package/dist/runtime/snapshot-reporter.js +375 -0
- package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
- package/dist/runtime/standalone-overlay-controller.js +281 -0
- package/dist/runtime/store.d.ts +307 -0
- package/dist/runtime/store.js +1026 -0
- package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
- package/dist/runtime/windowed-minimap-projection.js +189 -0
- package/dist/runtime/workspace-shortcuts.d.ts +30 -0
- package/dist/runtime/workspace-shortcuts.js +188 -0
- package/dist/sdk-error-copy.d.ts +48 -0
- package/dist/sdk-error-copy.js +182 -0
- package/dist/select-cursor.d.ts +2 -0
- package/dist/select-cursor.js +23 -0
- package/dist/server/admission-cookie.d.ts +38 -0
- package/dist/server/admission-cookie.js +122 -0
- package/dist/server/control-client.d.ts +29 -0
- package/dist/server/control-client.js +22 -0
- package/dist/server/control-errors.d.ts +22 -0
- package/dist/server/control-errors.js +182 -0
- package/dist/server/draft-route.d.ts +12 -0
- package/dist/server/draft-route.js +45 -0
- package/dist/server/extract-token.d.ts +30 -0
- package/dist/server/extract-token.js +98 -0
- package/dist/server/grant.d.ts +128 -0
- package/dist/server/grant.js +395 -0
- package/dist/server/ordinary-read-provenance.d.ts +11 -0
- package/dist/server/ordinary-read-provenance.js +124 -0
- package/dist/server/preview-read-metadata.d.ts +10 -0
- package/dist/server/preview-read-metadata.js +27 -0
- package/dist/server/provenance-frame-builder.d.ts +53 -0
- package/dist/server/provenance-frame-builder.js +102 -0
- package/dist/server/provenance-runtime.d.ts +70 -0
- package/dist/server/provenance-runtime.js +46 -0
- package/dist/server/read-context-command.d.ts +11 -0
- package/dist/server/read-context-command.js +53 -0
- package/dist/server/read-identity.d.ts +3 -0
- package/dist/server/read-identity.js +63 -0
- package/dist/server/read-lifecycle.d.ts +17 -0
- package/dist/server/read-lifecycle.js +52 -0
- package/dist/server/render-frame-provenance.d.ts +133 -0
- package/dist/server/render-frame-provenance.js +676 -0
- package/dist/server/render-id.d.ts +15 -0
- package/dist/server/render-id.js +22 -0
- package/dist/server/route-document-provenance.d.ts +77 -0
- package/dist/server/route-document-provenance.js +1327 -0
- package/dist/server/route-session-cookie.d.ts +33 -0
- package/dist/server/route-session-cookie.js +54 -0
- package/dist/session-projections.d.ts +33 -0
- package/dist/session-projections.js +43 -0
- package/dist/session-subscriber.d.ts +124 -0
- package/dist/session-subscriber.js +79 -0
- package/dist/sha256-fallback.d.ts +30 -0
- package/dist/sha256-fallback.js +126 -0
- package/dist/types.d.ts +446 -0
- package/dist/types.js +85 -0
- package/dist/validators.d.ts +61 -0
- package/dist/validators.js +110 -0
- package/package.json +210 -3
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
import { isPreviewTargetRendered, scrollPreviewTargetIntoView } from "../reveal-target.js";
|
|
2
|
+
import { getPreviewOverlayRegistryTuple, getPreviewOverlayTarget, } from "./preview-overlay-sidecar.js";
|
|
3
|
+
import { installPreviewInteractionPolicy } from "./preview-interaction-policy.js";
|
|
4
|
+
import { getInteractionMode } from "../interaction-mode.js";
|
|
5
|
+
import { parsePublicSidecar, readPreviewProvenanceSidecarStore, } from "../provenance/index.js";
|
|
6
|
+
import { isPreviewPerfEnabled, recordPreviewPerfMetric, previewPerfNow, } from "./perf-metrics.js";
|
|
7
|
+
import { applyPreviewOverlayLabelVisualStyle, PREVIEW_OVERLAY_LABEL_GAP_PX, PREVIEW_OVERLAY_LABEL_HEIGHT_PX, PREVIEW_OVERLAY_VISUAL_STYLES, resolvePreviewOverlayLabelHorizontalPosition, } from "../overlay-label.js";
|
|
8
|
+
const STYLE_ID = "storystick-preview-field-overlays-style";
|
|
9
|
+
const ATTR = "data-ss-preview-field-overlay";
|
|
10
|
+
const KEY_ATTR = "data-ss-preview-field-overlay-key";
|
|
11
|
+
const DISPLAY_ATTR = "data-ss-preview-field-overlay-display";
|
|
12
|
+
const REVEAL_ATTR = "data-ss-preview-v2-reveal-flash";
|
|
13
|
+
const SYNTHETIC_ROOT_ATTR = "data-ss-preview-field-synthetic-overlay-root";
|
|
14
|
+
const SYNTHETIC_BOX_ATTR = "data-ss-preview-field-synthetic-overlay-box";
|
|
15
|
+
const SYNTHETIC_BOX_KEY_ATTR = "data-ss-preview-field-synthetic-overlay-key";
|
|
16
|
+
const PREVIEW_CONTROLS_ATTR = "data-storystick-preview-controls";
|
|
17
|
+
const SYNTHETIC_BOX_STATE_ATTR = "data-state";
|
|
18
|
+
const SYNTHETIC_BOX_TONE_ATTR = "data-tone";
|
|
19
|
+
const PREVIEW_PAINT_TARGET_REGISTRY_KEY = "__ssPreviewPaintTargetRegistryV1";
|
|
20
|
+
const OVERLAY_STYLE = {
|
|
21
|
+
fillColor: "transparent",
|
|
22
|
+
activeFillColor: "transparent",
|
|
23
|
+
revealFillColor: "transparent",
|
|
24
|
+
borderWidth: 1.5,
|
|
25
|
+
radius: 1,
|
|
26
|
+
maxAdaptiveGeometryFrames: 12,
|
|
27
|
+
};
|
|
28
|
+
const SYNTHETIC_OVERLAY_STATE_TOKENS = {
|
|
29
|
+
default: buildSyntheticOverlayStateTokens("default"),
|
|
30
|
+
model: buildSyntheticOverlayStateTokens("model"),
|
|
31
|
+
};
|
|
32
|
+
function buildSyntheticOverlayStateTokens(tone) {
|
|
33
|
+
const visual = PREVIEW_OVERLAY_VISUAL_STYLES[tone];
|
|
34
|
+
return {
|
|
35
|
+
idle: { color: visual.color, fill: OVERLAY_STYLE.fillColor },
|
|
36
|
+
hover: { color: visual.color, fill: OVERLAY_STYLE.activeFillColor },
|
|
37
|
+
focus: { color: visual.color, fill: OVERLAY_STYLE.activeFillColor },
|
|
38
|
+
reveal: {
|
|
39
|
+
color: visual.revealColor,
|
|
40
|
+
fill: OVERLAY_STYLE.revealFillColor,
|
|
41
|
+
},
|
|
42
|
+
degraded: { color: visual.color, fill: "transparent" },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let decorated = [];
|
|
46
|
+
let interactiveTargets = new WeakMap();
|
|
47
|
+
let activeWindow = null;
|
|
48
|
+
let activeClickHandler = null;
|
|
49
|
+
let activeInteractionHandler = null;
|
|
50
|
+
let listenersInstalled = false;
|
|
51
|
+
let overlayDelegationActive = false;
|
|
52
|
+
let editInteractionGuardCount = 0;
|
|
53
|
+
let activeRevealFlashes = [];
|
|
54
|
+
let cancelRevealScroll;
|
|
55
|
+
let syntheticOverlayState = null;
|
|
56
|
+
export function applyPreviewOverlayMap(payload, win, options = {}) {
|
|
57
|
+
const tupleMatches = matchesPreviewOverlayRegistryTuple(payload);
|
|
58
|
+
if (!tupleMatches) {
|
|
59
|
+
clearPreviewOverlayPaint();
|
|
60
|
+
}
|
|
61
|
+
ensureStyle(win.document);
|
|
62
|
+
activeWindow = win;
|
|
63
|
+
activeClickHandler =
|
|
64
|
+
typeof options === "function" ? options : (options.onClick ?? null);
|
|
65
|
+
const provenanceContext = resolvePreviewProvenanceOverlayContext(payload, win.document);
|
|
66
|
+
if (!tupleMatches && provenanceContext === null) {
|
|
67
|
+
return {
|
|
68
|
+
generation: payload.generation,
|
|
69
|
+
paintedCount: 0,
|
|
70
|
+
missingSnapshotItemKeys: payload.items.map((item) => item.snapshotItemKey),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
let paintedCount = 0;
|
|
74
|
+
interactiveTargets = new WeakMap();
|
|
75
|
+
const missingSnapshotItemKeys = [];
|
|
76
|
+
const syntheticTargets = [];
|
|
77
|
+
const syntheticElements = new Map();
|
|
78
|
+
let provenanceTargets = null;
|
|
79
|
+
const hostDomMutationSafe = isPreviewPaintTargetDomMutationSafe(win);
|
|
80
|
+
for (const item of payload.items) {
|
|
81
|
+
const registryTarget = tupleMatches
|
|
82
|
+
? getPreviewOverlayTarget(item.snapshotItemKey)
|
|
83
|
+
: null;
|
|
84
|
+
const targetElements = registryTarget !== null
|
|
85
|
+
? [registryTarget]
|
|
86
|
+
: getPreviewProvenanceItemTargets(item, win.document, provenanceContext, () => {
|
|
87
|
+
provenanceTargets ??= collectProvenanceMarkerTargets(win.document, provenanceContext);
|
|
88
|
+
return provenanceTargets;
|
|
89
|
+
});
|
|
90
|
+
const connectedTargets = targetElements.filter((element) => win.document.contains(element));
|
|
91
|
+
if (connectedTargets.length === 0) {
|
|
92
|
+
missingSnapshotItemKeys.push(item.snapshotItemKey);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const element of connectedTargets) {
|
|
96
|
+
const interaction = {
|
|
97
|
+
generation: payload.generation,
|
|
98
|
+
snapshotItemKey: item.snapshotItemKey,
|
|
99
|
+
paintKey: item.paintKey,
|
|
100
|
+
routeHash: payload.routeHash,
|
|
101
|
+
codeVersion: payload.codeVersion,
|
|
102
|
+
renderId: payload.renderId,
|
|
103
|
+
};
|
|
104
|
+
interactiveTargets.set(element, interaction);
|
|
105
|
+
const visualTarget = richTextVisualTarget(element);
|
|
106
|
+
const existing = syntheticElements.get(visualTarget);
|
|
107
|
+
if (!existing) {
|
|
108
|
+
const target = { element: visualTarget, item };
|
|
109
|
+
syntheticElements.set(visualTarget, target);
|
|
110
|
+
syntheticTargets.push(target);
|
|
111
|
+
if (visualTarget !== element)
|
|
112
|
+
interactiveTargets.set(visualTarget, interaction);
|
|
113
|
+
}
|
|
114
|
+
else if (visualTarget === element) {
|
|
115
|
+
// A repeated element has one winner for both painting and interaction.
|
|
116
|
+
// Richtext descendants still share their first block-level visual target.
|
|
117
|
+
existing.item = item;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
paintedCount += 1;
|
|
121
|
+
}
|
|
122
|
+
if (hostDomMutationSafe) {
|
|
123
|
+
for (const target of syntheticTargets)
|
|
124
|
+
decorateElement(target.element, target.item, payload);
|
|
125
|
+
}
|
|
126
|
+
if (tupleMatches && syntheticOverlayState !== null) {
|
|
127
|
+
reconcileSyntheticOverlayRenderer(syntheticTargets, payload, win);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
installSyntheticOverlayRenderer(syntheticTargets, payload, win);
|
|
131
|
+
}
|
|
132
|
+
if (activeClickHandler !== null) {
|
|
133
|
+
overlayDelegationActive = true;
|
|
134
|
+
installEventDelegation(win);
|
|
135
|
+
}
|
|
136
|
+
// Synchronous target association, not a browser paint timestamp. Synthetic
|
|
137
|
+
// geometry is committed by the scheduled renderer below.
|
|
138
|
+
if (typeof win !== 'undefined') {
|
|
139
|
+
recordPreviewPerfMetric(win, {
|
|
140
|
+
name: 'preview.overlayPaintApplied',
|
|
141
|
+
ts: previewPerfNow(win),
|
|
142
|
+
detail: {
|
|
143
|
+
generation: payload.generation,
|
|
144
|
+
paintedCount,
|
|
145
|
+
routeHash: payload.routeHash,
|
|
146
|
+
renderId: payload.renderId,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
generation: payload.generation,
|
|
152
|
+
paintedCount,
|
|
153
|
+
missingSnapshotItemKeys,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function clearPreviewOverlayPaint() {
|
|
157
|
+
clearSyntheticOverlayRenderer();
|
|
158
|
+
clearActiveRevealFlashes(activeWindow?.document);
|
|
159
|
+
for (const entry of decorated) {
|
|
160
|
+
for (const [name, previous] of entry.attrs) {
|
|
161
|
+
if (previous === null)
|
|
162
|
+
entry.element.removeAttribute(name);
|
|
163
|
+
else
|
|
164
|
+
entry.element.setAttribute(name, previous);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
decorated = [];
|
|
168
|
+
interactiveTargets = new WeakMap();
|
|
169
|
+
overlayDelegationActive = false;
|
|
170
|
+
activeClickHandler = null;
|
|
171
|
+
maybeUninstallEventDelegation();
|
|
172
|
+
}
|
|
173
|
+
export function installPreviewEditInteractionGuard(win, options = {}) {
|
|
174
|
+
if (activeWindow !== null && activeWindow !== win) {
|
|
175
|
+
clearPreviewOverlayPaint();
|
|
176
|
+
uninstallEventDelegation();
|
|
177
|
+
activeWindow = null;
|
|
178
|
+
overlayDelegationActive = false;
|
|
179
|
+
editInteractionGuardCount = 0;
|
|
180
|
+
activeClickHandler = null;
|
|
181
|
+
interactiveTargets = new WeakMap();
|
|
182
|
+
activeInteractionHandler = null;
|
|
183
|
+
}
|
|
184
|
+
activeWindow = win;
|
|
185
|
+
activeInteractionHandler = options.onInteraction ?? null;
|
|
186
|
+
editInteractionGuardCount += 1;
|
|
187
|
+
installEventDelegation(win);
|
|
188
|
+
const releaseInteractionPolicy = installPreviewInteractionPolicy(win);
|
|
189
|
+
let released = false;
|
|
190
|
+
return () => {
|
|
191
|
+
if (released)
|
|
192
|
+
return;
|
|
193
|
+
released = true;
|
|
194
|
+
releaseInteractionPolicy();
|
|
195
|
+
if (activeWindow !== win)
|
|
196
|
+
return;
|
|
197
|
+
editInteractionGuardCount = Math.max(0, editInteractionGuardCount - 1);
|
|
198
|
+
if (editInteractionGuardCount === 0)
|
|
199
|
+
activeInteractionHandler = null;
|
|
200
|
+
maybeUninstallEventDelegation();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function revealPreviewV2SourceInIframe(payload, win) {
|
|
204
|
+
clearActiveRevealFlashes(win.document);
|
|
205
|
+
const context = resolvePreviewProvenanceOverlayContext(payload, win.document);
|
|
206
|
+
if (context === null) {
|
|
207
|
+
return { status: "routeMissing", revealedCount: 0 };
|
|
208
|
+
}
|
|
209
|
+
if (payload.targets.some((target) => !context.markerIds.has(target.markerId))) {
|
|
210
|
+
return { status: "unknownSource", revealedCount: 0 };
|
|
211
|
+
}
|
|
212
|
+
ensureStyle(win.document);
|
|
213
|
+
let hiddenCount = 0;
|
|
214
|
+
let mountedCount = 0;
|
|
215
|
+
const revealed = [];
|
|
216
|
+
for (const target of payload.targets) {
|
|
217
|
+
const element = getPreviewV2RevealTargetElement(target.renderTargetId, target.markerId, win.document, context);
|
|
218
|
+
if (element === null || !win.document.contains(element))
|
|
219
|
+
continue;
|
|
220
|
+
mountedCount += 1;
|
|
221
|
+
if (!isPreviewTargetRendered(element)) {
|
|
222
|
+
hiddenCount += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const visualTarget = richTextVisualTarget(element);
|
|
226
|
+
if (!revealed.includes(visualTarget))
|
|
227
|
+
revealed.push(visualTarget);
|
|
228
|
+
if (payload.behavior.flash) {
|
|
229
|
+
flashRevealElement(visualTarget, win, payload.behavior.scroll ? 5_000 : payload.behavior.durationMs);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// One scroll destination, all matching occurrences highlighted. Restart the
|
|
233
|
+
// flash lifetime at arrival so long smooth scrolls do not consume it.
|
|
234
|
+
if (payload.behavior.scroll && revealed[0]) {
|
|
235
|
+
cancelRevealScroll = scrollPreviewTargetIntoView(revealed[0], payload.behavior.flash ? () => {
|
|
236
|
+
for (const element of revealed) {
|
|
237
|
+
if (element.isConnected)
|
|
238
|
+
flashRevealElement(element, win, payload.behavior.durationMs);
|
|
239
|
+
}
|
|
240
|
+
} : undefined);
|
|
241
|
+
}
|
|
242
|
+
const revealedCount = revealed.length;
|
|
243
|
+
if (revealedCount > 1)
|
|
244
|
+
return { status: "multiple", revealedCount };
|
|
245
|
+
if (revealedCount === 1)
|
|
246
|
+
return { status: "found", revealedCount };
|
|
247
|
+
if (hiddenCount > 0)
|
|
248
|
+
return { status: "hidden", revealedCount: 0 };
|
|
249
|
+
if (mountedCount === 0)
|
|
250
|
+
return { status: "notMounted", revealedCount: 0 };
|
|
251
|
+
return { status: "notMounted", revealedCount: 0 };
|
|
252
|
+
}
|
|
253
|
+
function matchesPreviewOverlayRegistryTuple(payload) {
|
|
254
|
+
const tuple = getPreviewOverlayRegistryTuple();
|
|
255
|
+
return (tuple !== null &&
|
|
256
|
+
tuple.routeHash === payload.routeHash &&
|
|
257
|
+
tuple.codeVersion === payload.codeVersion &&
|
|
258
|
+
tuple.cacheVersion === payload.cacheVersion &&
|
|
259
|
+
tuple.renderId === payload.renderId);
|
|
260
|
+
}
|
|
261
|
+
function resolvePreviewProvenanceOverlayContext(payload, doc) {
|
|
262
|
+
let routeFrameMatches = false;
|
|
263
|
+
const markerIds = new Set();
|
|
264
|
+
const multiMarkerIds = new Set();
|
|
265
|
+
const store = readPreviewProvenanceSidecarStore(doc.defaultView);
|
|
266
|
+
const storedSnapshot = store?.readSnapshot() ?? null;
|
|
267
|
+
const storedFrames = store?.readFrames() ?? [];
|
|
268
|
+
const frames = storedFrames.length > 0
|
|
269
|
+
? storedFrames
|
|
270
|
+
: [...doc.querySelectorAll("script[data-ss-provenance-frame]")].map((script) => ({
|
|
271
|
+
frameId: script.getAttribute("data-ss-provenance-frame") ?? "",
|
|
272
|
+
sidecar: parsePublicSidecar(script.textContent),
|
|
273
|
+
}));
|
|
274
|
+
for (const { frameId, sidecar } of frames) {
|
|
275
|
+
if (sidecar.codeVersion !== payload.codeVersion)
|
|
276
|
+
continue;
|
|
277
|
+
if (sidecar.renderId.length === 0)
|
|
278
|
+
continue;
|
|
279
|
+
if (frameId === "route" &&
|
|
280
|
+
sidecar.routeHash === payload.routeHash &&
|
|
281
|
+
sidecar.renderId === payload.renderId) {
|
|
282
|
+
routeFrameMatches = true;
|
|
283
|
+
}
|
|
284
|
+
for (const marker of sidecar.markers) {
|
|
285
|
+
markerIds.add(marker.markerId);
|
|
286
|
+
if (marker.anchorMultiplicity === "multi") {
|
|
287
|
+
multiMarkerIds.add(marker.markerId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!routeFrameMatches)
|
|
292
|
+
return null;
|
|
293
|
+
return {
|
|
294
|
+
routeHash: payload.routeHash,
|
|
295
|
+
codeVersion: payload.codeVersion,
|
|
296
|
+
renderId: payload.renderId,
|
|
297
|
+
sessionBoundByStore: storedFrames.length > 0 &&
|
|
298
|
+
storedSnapshot !== null &&
|
|
299
|
+
storedSnapshot.sessionId.length > 0,
|
|
300
|
+
markerIds,
|
|
301
|
+
multiMarkerIds,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function getPreviewV2RevealTargetElement(renderTargetId, markerId, doc, context) {
|
|
305
|
+
if (!markerId.startsWith("p_"))
|
|
306
|
+
return null;
|
|
307
|
+
if (!context.markerIds.has(markerId))
|
|
308
|
+
return null;
|
|
309
|
+
const resolved = getUniquePreviewPaintTarget(renderTargetId, doc);
|
|
310
|
+
if (resolved === null)
|
|
311
|
+
return null;
|
|
312
|
+
return resolved.markerId === markerId ? resolved.element : null;
|
|
313
|
+
}
|
|
314
|
+
function getUniquePreviewPaintTarget(renderTargetId, doc) {
|
|
315
|
+
const runtimeTarget = resolvePreviewPaintTargetFromRegistry(renderTargetId, doc);
|
|
316
|
+
let stamped = null;
|
|
317
|
+
for (const element of doc.querySelectorAll("[data-ss-preview-paint-id]")) {
|
|
318
|
+
if (element.getAttribute("data-ss-preview-paint-id") !== renderTargetId) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (stamped !== null)
|
|
322
|
+
return null;
|
|
323
|
+
stamped = element;
|
|
324
|
+
}
|
|
325
|
+
if (runtimeTarget !== null) {
|
|
326
|
+
if (runtimeTarget.element.ownerDocument !== doc ||
|
|
327
|
+
!doc.contains(runtimeTarget.element) ||
|
|
328
|
+
(stamped !== null && stamped !== runtimeTarget.element)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return runtimeTarget;
|
|
332
|
+
}
|
|
333
|
+
if (stamped === null)
|
|
334
|
+
return null;
|
|
335
|
+
const markerId = stamped.getAttribute("data-ss-e") ?? stamped.getAttribute("data-ss-p");
|
|
336
|
+
return markerId === null || markerId.length === 0
|
|
337
|
+
? null
|
|
338
|
+
: { element: stamped, markerId };
|
|
339
|
+
}
|
|
340
|
+
function resolvePreviewPaintTargetFromRegistry(renderTargetId, doc) {
|
|
341
|
+
const registry = readPreviewPaintTargetRegistry(doc);
|
|
342
|
+
if (registry === null)
|
|
343
|
+
return null;
|
|
344
|
+
try {
|
|
345
|
+
const target = registry.resolve(renderTargetId);
|
|
346
|
+
if (target === null ||
|
|
347
|
+
!(target.element instanceof Element) ||
|
|
348
|
+
typeof target.markerId !== "string" ||
|
|
349
|
+
target.markerId.length === 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
return target;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function readPreviewPaintTargetRegistry(doc) {
|
|
359
|
+
const win = doc.defaultView;
|
|
360
|
+
const registry = win?.[PREVIEW_PAINT_TARGET_REGISTRY_KEY];
|
|
361
|
+
return registry !== null && typeof registry === "object" &&
|
|
362
|
+
typeof registry.resolve === "function"
|
|
363
|
+
? registry
|
|
364
|
+
: null;
|
|
365
|
+
}
|
|
366
|
+
function isPreviewPaintTargetDomMutationSafe(win) {
|
|
367
|
+
const registry = readPreviewPaintTargetRegistry(win.document);
|
|
368
|
+
if (registry?.isDomMutationSafe === undefined)
|
|
369
|
+
return true;
|
|
370
|
+
try {
|
|
371
|
+
return registry.isDomMutationSafe();
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function getPreviewProvenanceMarkerTargets(markerId, doc, context, targetsRef) {
|
|
378
|
+
if (!markerId.startsWith("p_"))
|
|
379
|
+
return [];
|
|
380
|
+
if (context === null || !context.markerIds.has(markerId))
|
|
381
|
+
return [];
|
|
382
|
+
if (!context.sessionBoundByStore &&
|
|
383
|
+
!doc.documentElement.hasAttribute("data-preview-message-session-id") &&
|
|
384
|
+
!doc.body?.hasAttribute("data-preview-message-session-id") &&
|
|
385
|
+
doc.querySelector("[data-preview-message-session-id]") === null) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
const targets = targetsRef();
|
|
389
|
+
return targets.get(markerId) ?? [];
|
|
390
|
+
}
|
|
391
|
+
function getPreviewProvenanceItemTargets(item, doc, context, targetsRef) {
|
|
392
|
+
const markerId = item.markerId ?? item.snapshotItemKey;
|
|
393
|
+
const targets = getPreviewProvenanceMarkerTargets(markerId, doc, context, targetsRef);
|
|
394
|
+
if (item.renderTargetId === undefined)
|
|
395
|
+
return targets;
|
|
396
|
+
const target = getUniquePreviewPaintTarget(item.renderTargetId, doc);
|
|
397
|
+
return target === null || target.markerId !== markerId
|
|
398
|
+
? []
|
|
399
|
+
: [target.element];
|
|
400
|
+
}
|
|
401
|
+
function collectProvenanceMarkerTargets(doc, context) {
|
|
402
|
+
const collected = new Map();
|
|
403
|
+
for (const element of doc.querySelectorAll("[data-ss-e],[data-ss-p]")) {
|
|
404
|
+
const markerId = element.getAttribute("data-ss-e") ?? element.getAttribute("data-ss-p");
|
|
405
|
+
if (markerId === null || markerId.length === 0)
|
|
406
|
+
continue;
|
|
407
|
+
const existing = collected.get(markerId);
|
|
408
|
+
if (existing !== undefined)
|
|
409
|
+
existing.push(element);
|
|
410
|
+
else
|
|
411
|
+
collected.set(markerId, [element]);
|
|
412
|
+
}
|
|
413
|
+
const targets = new Map();
|
|
414
|
+
for (const [markerId, elements] of collected) {
|
|
415
|
+
if (elements.length === 1 || context?.multiMarkerIds.has(markerId)) {
|
|
416
|
+
targets.set(markerId, elements);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
targets.set(markerId, []);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return targets;
|
|
423
|
+
}
|
|
424
|
+
function flashRevealElement(element, win, durationMs) {
|
|
425
|
+
activeRevealFlashes = activeRevealFlashes.filter((entry) => {
|
|
426
|
+
if (entry.element !== element)
|
|
427
|
+
return true;
|
|
428
|
+
entry.win.clearTimeout(entry.timeoutId);
|
|
429
|
+
return false;
|
|
430
|
+
});
|
|
431
|
+
element.setAttribute(REVEAL_ATTR, "active");
|
|
432
|
+
const timeout = Math.max(0, Math.min(durationMs, 5_000));
|
|
433
|
+
const timeoutId = win.setTimeout(() => {
|
|
434
|
+
if (element.getAttribute(REVEAL_ATTR) === "active") {
|
|
435
|
+
element.removeAttribute(REVEAL_ATTR);
|
|
436
|
+
}
|
|
437
|
+
activeRevealFlashes = activeRevealFlashes.filter((entry) => entry.timeoutId !== timeoutId);
|
|
438
|
+
}, timeout);
|
|
439
|
+
activeRevealFlashes.push({ element, timeoutId, win });
|
|
440
|
+
}
|
|
441
|
+
function clearActiveRevealFlashes(doc) {
|
|
442
|
+
cancelRevealScroll?.();
|
|
443
|
+
cancelRevealScroll = undefined;
|
|
444
|
+
const docs = new Set();
|
|
445
|
+
if (doc !== undefined)
|
|
446
|
+
docs.add(doc);
|
|
447
|
+
for (const entry of activeRevealFlashes) {
|
|
448
|
+
entry.win.clearTimeout(entry.timeoutId);
|
|
449
|
+
docs.add(entry.element.ownerDocument);
|
|
450
|
+
}
|
|
451
|
+
activeRevealFlashes = [];
|
|
452
|
+
for (const activeDoc of docs) {
|
|
453
|
+
for (const element of activeDoc.querySelectorAll(`[${REVEAL_ATTR}]`)) {
|
|
454
|
+
element.removeAttribute(REVEAL_ATTR);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function decorateElement(element, item, payload) {
|
|
459
|
+
if (decorated.every((entry) => entry.element !== element)) {
|
|
460
|
+
decorated.push({
|
|
461
|
+
element,
|
|
462
|
+
attrs: new Map([
|
|
463
|
+
[ATTR, element.getAttribute(ATTR)],
|
|
464
|
+
[KEY_ATTR, element.getAttribute(KEY_ATTR)],
|
|
465
|
+
[DISPLAY_ATTR, element.getAttribute(DISPLAY_ATTR)],
|
|
466
|
+
]),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
element.setAttribute(ATTR, "field");
|
|
470
|
+
element.setAttribute(KEY_ATTR, item.paintKey);
|
|
471
|
+
element.setAttribute(DISPLAY_ATTR, payload.display);
|
|
472
|
+
}
|
|
473
|
+
function restoreDecoratedElement(element) {
|
|
474
|
+
const index = decorated.findIndex((entry) => entry.element === element);
|
|
475
|
+
if (index === -1)
|
|
476
|
+
return;
|
|
477
|
+
const [entry] = decorated.splice(index, 1);
|
|
478
|
+
if (entry === undefined)
|
|
479
|
+
return;
|
|
480
|
+
for (const [name, previous] of entry.attrs) {
|
|
481
|
+
if (previous === null)
|
|
482
|
+
element.removeAttribute(name);
|
|
483
|
+
else
|
|
484
|
+
element.setAttribute(name, previous);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function installSyntheticOverlayRenderer(targets, payload, win) {
|
|
488
|
+
if (targets.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
const doc = win.document;
|
|
491
|
+
const root = doc.createElement("storystick-preview-field-overlays");
|
|
492
|
+
root.setAttribute(SYNTHETIC_ROOT_ATTR, "true");
|
|
493
|
+
root.setAttribute(DISPLAY_ATTR, payload.display);
|
|
494
|
+
root.setAttribute("aria-hidden", "true");
|
|
495
|
+
root.style.display = "block";
|
|
496
|
+
root.style.position = "absolute";
|
|
497
|
+
root.style.left = "0";
|
|
498
|
+
root.style.top = "0";
|
|
499
|
+
root.style.width = "0";
|
|
500
|
+
root.style.height = "0";
|
|
501
|
+
root.style.pointerEvents = "none";
|
|
502
|
+
root.style.zIndex = "2147483646";
|
|
503
|
+
root.style.isolation = "isolate";
|
|
504
|
+
root.style.overflow = "visible";
|
|
505
|
+
const entries = createSyntheticOverlayEntries(targets);
|
|
506
|
+
doc.documentElement.appendChild(root);
|
|
507
|
+
const targetElements = new Set(entries.map((entry) => entry.element));
|
|
508
|
+
const onSchedule = () => scheduleSyntheticOverlayUpdate("geometry");
|
|
509
|
+
const onResize = () => {
|
|
510
|
+
if (doc.visibilityState !== "hidden") {
|
|
511
|
+
invalidateSyntheticOverlayLabelWidths();
|
|
512
|
+
invalidateSyntheticOverlayPositioningModes();
|
|
513
|
+
}
|
|
514
|
+
onSchedule();
|
|
515
|
+
};
|
|
516
|
+
const onPointerMove = (event) => {
|
|
517
|
+
const state = syntheticOverlayState;
|
|
518
|
+
if (state === null)
|
|
519
|
+
return;
|
|
520
|
+
const target = findSyntheticOverlayTargetAtEventPoint(state, event);
|
|
521
|
+
const next = target === null
|
|
522
|
+
? new Set()
|
|
523
|
+
: resolveSyntheticOverlayHoverTargets(target);
|
|
524
|
+
scheduleSyntheticOverlayHoverUpdate(state, next);
|
|
525
|
+
};
|
|
526
|
+
const onPointerOver = (event) => {
|
|
527
|
+
const state = syntheticOverlayState;
|
|
528
|
+
const target = findSyntheticOverlayTargetFromNode(state, event.target);
|
|
529
|
+
if (state === null || target === null)
|
|
530
|
+
return;
|
|
531
|
+
scheduleSyntheticOverlayHoverUpdate(state, resolveSyntheticOverlayHoverTargets(target));
|
|
532
|
+
};
|
|
533
|
+
const onPointerOut = (event) => {
|
|
534
|
+
const state = syntheticOverlayState;
|
|
535
|
+
if (state === null || state.hoveredTargets.size === 0)
|
|
536
|
+
return;
|
|
537
|
+
const related = event.relatedTarget;
|
|
538
|
+
if (isNode(related)) {
|
|
539
|
+
for (const hoveredTarget of state.hoveredTargets) {
|
|
540
|
+
if (hoveredTarget.contains(related))
|
|
541
|
+
return;
|
|
542
|
+
const groupRoot = findSyntheticOverlayHoverGroupRoot(hoveredTarget);
|
|
543
|
+
if (groupRoot !== null &&
|
|
544
|
+
isElementNode(related) &&
|
|
545
|
+
(groupRoot === related || groupRoot.contains(related))) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const relatedTarget = findSyntheticOverlayTargetFromNode(state, related);
|
|
551
|
+
if (relatedTarget !== null && state.hoveredTargets.has(relatedTarget)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
scheduleSyntheticOverlayHoverUpdate(state, new Set());
|
|
555
|
+
};
|
|
556
|
+
const onFocusIn = (event) => {
|
|
557
|
+
const state = syntheticOverlayState;
|
|
558
|
+
const target = findSyntheticOverlayTargetFromNode(state, event.target);
|
|
559
|
+
if (state === null || target === null)
|
|
560
|
+
return;
|
|
561
|
+
scheduleSyntheticOverlayFocusUpdate(state, target);
|
|
562
|
+
};
|
|
563
|
+
const onFocusOut = () => {
|
|
564
|
+
const state = syntheticOverlayState;
|
|
565
|
+
if (state === null)
|
|
566
|
+
return;
|
|
567
|
+
scheduleSyntheticOverlayFocusUpdate(state, null);
|
|
568
|
+
};
|
|
569
|
+
const onScroll = () => {
|
|
570
|
+
const state = syntheticOverlayState;
|
|
571
|
+
if (state?.intersectingTargets == null) {
|
|
572
|
+
scheduleSyntheticOverlayUpdate("geometry");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const targets = new Set(state.intersectingTargets);
|
|
576
|
+
// Keep outgoing boxes in the pass that removes them. IO independently
|
|
577
|
+
// discovers incoming targets, including fixed/sticky elements.
|
|
578
|
+
for (const entry of state.entries) {
|
|
579
|
+
if (entry.boxes.length > 0)
|
|
580
|
+
targets.add(entry.element);
|
|
581
|
+
}
|
|
582
|
+
scheduleSyntheticOverlayUpdate("geometry", targets);
|
|
583
|
+
};
|
|
584
|
+
const onVisibilityChange = () => {
|
|
585
|
+
const state = syntheticOverlayState;
|
|
586
|
+
if (state === null)
|
|
587
|
+
return;
|
|
588
|
+
if (doc.visibilityState === "hidden") {
|
|
589
|
+
if (state.frameId !== null) {
|
|
590
|
+
cancelSyntheticAnimationFrame(win, state.frameId);
|
|
591
|
+
state.frameId = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// Layout may have changed while hidden; remeasure once on return.
|
|
596
|
+
onResize();
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const resizeObserver = createSyntheticResizeObserver(win, onSchedule);
|
|
600
|
+
if (resizeObserver !== null) {
|
|
601
|
+
for (const entry of entries)
|
|
602
|
+
resizeObserver.observe(entry.element);
|
|
603
|
+
resizeObserver.observe(doc.documentElement);
|
|
604
|
+
if (doc.body !== null)
|
|
605
|
+
resizeObserver.observe(doc.body);
|
|
606
|
+
}
|
|
607
|
+
const visibleTargets = new Set(targetElements);
|
|
608
|
+
const intersectingTargets = new Set(targetElements);
|
|
609
|
+
const intersectionObserver = createSyntheticIntersectionObserver(win, (records) => {
|
|
610
|
+
const changedTargets = new Set();
|
|
611
|
+
for (const record of records) {
|
|
612
|
+
if (!targetElements.has(record.target))
|
|
613
|
+
continue;
|
|
614
|
+
changedTargets.add(record.target);
|
|
615
|
+
if (record.isIntersecting)
|
|
616
|
+
intersectingTargets.add(record.target);
|
|
617
|
+
else
|
|
618
|
+
intersectingTargets.delete(record.target);
|
|
619
|
+
if (record.isIntersecting && record.intersectionRatio >= 0.3) {
|
|
620
|
+
visibleTargets.add(record.target);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
visibleTargets.delete(record.target);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (changedTargets.size > 0) {
|
|
627
|
+
scheduleSyntheticOverlayUpdate("geometry", changedTargets);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
if (intersectionObserver !== null) {
|
|
631
|
+
for (const entry of entries)
|
|
632
|
+
intersectionObserver.observe(entry.element);
|
|
633
|
+
}
|
|
634
|
+
const mutationObserver = createSyntheticMutationObserver(win, (records) => {
|
|
635
|
+
if (records.every(isSyntheticOverlayMutationRecord))
|
|
636
|
+
return;
|
|
637
|
+
onResize();
|
|
638
|
+
});
|
|
639
|
+
if (mutationObserver !== null) {
|
|
640
|
+
mutationObserver.observe(doc.documentElement, {
|
|
641
|
+
attributes: true,
|
|
642
|
+
childList: true,
|
|
643
|
+
subtree: true,
|
|
644
|
+
attributeFilter: [
|
|
645
|
+
"class",
|
|
646
|
+
"style",
|
|
647
|
+
"hidden",
|
|
648
|
+
ATTR,
|
|
649
|
+
KEY_ATTR,
|
|
650
|
+
DISPLAY_ATTR,
|
|
651
|
+
REVEAL_ATTR,
|
|
652
|
+
],
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
win.addEventListener("resize", onResize, true);
|
|
656
|
+
win.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
657
|
+
doc.addEventListener("pointermove", onPointerMove, true);
|
|
658
|
+
doc.addEventListener("pointerover", onPointerOver, true);
|
|
659
|
+
doc.addEventListener("pointerout", onPointerOut, true);
|
|
660
|
+
doc.addEventListener("focusin", onFocusIn, true);
|
|
661
|
+
doc.addEventListener("focusout", onFocusOut, true);
|
|
662
|
+
doc.addEventListener("visibilitychange", onVisibilityChange);
|
|
663
|
+
const fontsReady = doc.fonts?.ready;
|
|
664
|
+
if (fontsReady && typeof fontsReady.then === "function") {
|
|
665
|
+
void fontsReady.then(() => {
|
|
666
|
+
if (syntheticOverlayState?.root === root) {
|
|
667
|
+
invalidateSyntheticOverlayLabelWidths();
|
|
668
|
+
onSchedule();
|
|
669
|
+
}
|
|
670
|
+
}, () => undefined);
|
|
671
|
+
}
|
|
672
|
+
syntheticOverlayState = {
|
|
673
|
+
win,
|
|
674
|
+
root,
|
|
675
|
+
entries,
|
|
676
|
+
targetElements,
|
|
677
|
+
display: payload.display,
|
|
678
|
+
resizeObserver,
|
|
679
|
+
intersectionObserver,
|
|
680
|
+
mutationObserver,
|
|
681
|
+
visibleTargets: intersectionObserver === null ? null : visibleTargets,
|
|
682
|
+
intersectingTargets: intersectionObserver === null ? null : intersectingTargets,
|
|
683
|
+
frameId: null,
|
|
684
|
+
pendingUpdateKind: null,
|
|
685
|
+
pendingGeometryTargets: null,
|
|
686
|
+
geometryFollowUpsRemaining: 0,
|
|
687
|
+
pendingCommitMetricGeneration: payload.generation,
|
|
688
|
+
hoveredTargets: new Set(),
|
|
689
|
+
focusedTarget: null,
|
|
690
|
+
onSchedule,
|
|
691
|
+
onResize,
|
|
692
|
+
onPointerMove,
|
|
693
|
+
onPointerOver,
|
|
694
|
+
onPointerOut,
|
|
695
|
+
onFocusIn,
|
|
696
|
+
onFocusOut,
|
|
697
|
+
onScroll,
|
|
698
|
+
onVisibilityChange,
|
|
699
|
+
};
|
|
700
|
+
scheduleSyntheticOverlayUpdate("geometry");
|
|
701
|
+
}
|
|
702
|
+
function reconcileSyntheticOverlayRenderer(targets, payload, win) {
|
|
703
|
+
const state = syntheticOverlayState;
|
|
704
|
+
if (state === null)
|
|
705
|
+
return;
|
|
706
|
+
const previousByIdentity = new Map(state.entries.map((entry) => [entry.identity, entry]));
|
|
707
|
+
const nextEntries = createSyntheticOverlayEntries(targets, previousByIdentity);
|
|
708
|
+
const claimedIdentities = new Set(nextEntries.map((entry) => entry.identity));
|
|
709
|
+
const nextTargetElements = new Set(nextEntries.map((entry) => entry.element));
|
|
710
|
+
for (const entry of state.entries) {
|
|
711
|
+
if (claimedIdentities.has(entry.identity))
|
|
712
|
+
continue;
|
|
713
|
+
removeSyntheticOverlayEntryBoxes(entry);
|
|
714
|
+
}
|
|
715
|
+
for (const element of state.targetElements) {
|
|
716
|
+
if (nextTargetElements.has(element))
|
|
717
|
+
continue;
|
|
718
|
+
state.resizeObserver?.unobserve(element);
|
|
719
|
+
state.intersectionObserver?.unobserve(element);
|
|
720
|
+
state.visibleTargets?.delete(element);
|
|
721
|
+
state.intersectingTargets?.delete(element);
|
|
722
|
+
interactiveTargets.delete(element);
|
|
723
|
+
restoreDecoratedElement(element);
|
|
724
|
+
}
|
|
725
|
+
for (const element of nextTargetElements) {
|
|
726
|
+
if (state.targetElements.has(element))
|
|
727
|
+
continue;
|
|
728
|
+
state.resizeObserver?.observe(element);
|
|
729
|
+
state.intersectionObserver?.observe(element);
|
|
730
|
+
state.intersectingTargets?.add(element);
|
|
731
|
+
}
|
|
732
|
+
state.targetElements.clear();
|
|
733
|
+
for (const element of nextTargetElements)
|
|
734
|
+
state.targetElements.add(element);
|
|
735
|
+
if (state.visibleTargets !== null) {
|
|
736
|
+
state.visibleTargets.clear();
|
|
737
|
+
for (const element of nextTargetElements)
|
|
738
|
+
state.visibleTargets.add(element);
|
|
739
|
+
}
|
|
740
|
+
state.entries = nextEntries;
|
|
741
|
+
state.pendingCommitMetricGeneration = payload.generation;
|
|
742
|
+
state.display = payload.display;
|
|
743
|
+
state.root.setAttribute(DISPLAY_ATTR, payload.display);
|
|
744
|
+
state.hoveredTargets.clear();
|
|
745
|
+
state.focusedTarget = null;
|
|
746
|
+
clearActiveRevealFlashes(win.document);
|
|
747
|
+
if (nextEntries.length === 0) {
|
|
748
|
+
// Mirror the install path (which never creates a renderer for zero
|
|
749
|
+
// targets): an emptied reconcile tears the renderer down instead of
|
|
750
|
+
// keeping the root and document-level observers alive until a hard
|
|
751
|
+
// clear. Per-element bookkeeping already ran above.
|
|
752
|
+
clearSyntheticOverlayRenderer();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
scheduleSyntheticOverlayUpdate("geometry");
|
|
756
|
+
}
|
|
757
|
+
function createSyntheticOverlayEntries(targets, previousByIdentity = new Map()) {
|
|
758
|
+
const occurrenceBySnapshotItemKey = new Map();
|
|
759
|
+
return targets.map(({ element, item }) => {
|
|
760
|
+
const occurrence = occurrenceBySnapshotItemKey.get(item.snapshotItemKey) ?? 0;
|
|
761
|
+
occurrenceBySnapshotItemKey.set(item.snapshotItemKey, occurrence + 1);
|
|
762
|
+
const identity = syntheticOverlayTargetIdentity(item, occurrence);
|
|
763
|
+
const previous = previousByIdentity.get(identity);
|
|
764
|
+
return {
|
|
765
|
+
identity,
|
|
766
|
+
snapshotItemKey: item.snapshotItemKey,
|
|
767
|
+
renderTargetId: item.renderTargetId ?? null,
|
|
768
|
+
element,
|
|
769
|
+
paintKey: item.paintKey,
|
|
770
|
+
tone: item.tone === "model" ? "model" : "default",
|
|
771
|
+
crossDoc: item.crossDoc === true,
|
|
772
|
+
label: item.crossDoc === true && item.label !== undefined ? item.label : null,
|
|
773
|
+
boxes: previous?.boxes ?? [],
|
|
774
|
+
labelWidth: previous?.label === item.label &&
|
|
775
|
+
previous?.tone === (item.tone === "model" ? "model" : "default")
|
|
776
|
+
? previous.labelWidth
|
|
777
|
+
: null,
|
|
778
|
+
lastRects: previous?.lastRects ?? [],
|
|
779
|
+
anchoredToViewport: previous?.element === element
|
|
780
|
+
? previous.anchoredToViewport
|
|
781
|
+
: null,
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function syntheticOverlayTargetIdentity(item, occurrence) {
|
|
786
|
+
if (item.renderTargetId !== undefined && item.renderTargetId.length > 0) {
|
|
787
|
+
return `render-target:${item.renderTargetId}`;
|
|
788
|
+
}
|
|
789
|
+
return `snapshot:${item.snapshotItemKey}:${occurrence}`;
|
|
790
|
+
}
|
|
791
|
+
function removeSyntheticOverlayEntryBoxes(entry) {
|
|
792
|
+
for (const box of entry.boxes)
|
|
793
|
+
box.remove();
|
|
794
|
+
entry.boxes = [];
|
|
795
|
+
}
|
|
796
|
+
function clearSyntheticOverlayRenderer() {
|
|
797
|
+
const state = syntheticOverlayState;
|
|
798
|
+
if (state === null)
|
|
799
|
+
return;
|
|
800
|
+
syntheticOverlayState = null;
|
|
801
|
+
if (state.frameId !== null) {
|
|
802
|
+
cancelSyntheticAnimationFrame(state.win, state.frameId);
|
|
803
|
+
}
|
|
804
|
+
state.pendingUpdateKind = null;
|
|
805
|
+
state.pendingGeometryTargets = null;
|
|
806
|
+
state.geometryFollowUpsRemaining = 0;
|
|
807
|
+
state.resizeObserver?.disconnect();
|
|
808
|
+
state.intersectionObserver?.disconnect();
|
|
809
|
+
state.mutationObserver?.disconnect();
|
|
810
|
+
state.win.removeEventListener("resize", state.onResize, true);
|
|
811
|
+
state.win.removeEventListener("scroll", state.onScroll, true);
|
|
812
|
+
state.win.document.removeEventListener("pointermove", state.onPointerMove, true);
|
|
813
|
+
state.win.document.removeEventListener("pointerover", state.onPointerOver, true);
|
|
814
|
+
state.win.document.removeEventListener("pointerout", state.onPointerOut, true);
|
|
815
|
+
state.win.document.removeEventListener("focusin", state.onFocusIn, true);
|
|
816
|
+
state.win.document.removeEventListener("focusout", state.onFocusOut, true);
|
|
817
|
+
state.win.document.removeEventListener("visibilitychange", state.onVisibilityChange);
|
|
818
|
+
state.root.remove();
|
|
819
|
+
}
|
|
820
|
+
function createSyntheticResizeObserver(win, callback) {
|
|
821
|
+
const ResizeObserverCtor = win.ResizeObserver;
|
|
822
|
+
if (typeof ResizeObserverCtor !== "function")
|
|
823
|
+
return null;
|
|
824
|
+
return new ResizeObserverCtor(callback);
|
|
825
|
+
}
|
|
826
|
+
function createSyntheticMutationObserver(win, callback) {
|
|
827
|
+
const MutationObserverCtor = win
|
|
828
|
+
.MutationObserver;
|
|
829
|
+
if (typeof MutationObserverCtor !== "function")
|
|
830
|
+
return null;
|
|
831
|
+
return new MutationObserverCtor(callback);
|
|
832
|
+
}
|
|
833
|
+
function createSyntheticIntersectionObserver(win, callback) {
|
|
834
|
+
const IntersectionObserverCtor = win
|
|
835
|
+
.IntersectionObserver;
|
|
836
|
+
if (typeof IntersectionObserverCtor !== "function")
|
|
837
|
+
return null;
|
|
838
|
+
return new IntersectionObserverCtor(callback, { threshold: [0, 0.3] });
|
|
839
|
+
}
|
|
840
|
+
function isSyntheticOverlayMutationRecord(record) {
|
|
841
|
+
const target = record.target;
|
|
842
|
+
if (!isElementNode(target))
|
|
843
|
+
return false;
|
|
844
|
+
return target.closest(`[${SYNTHETIC_ROOT_ATTR}]`) !== null;
|
|
845
|
+
}
|
|
846
|
+
function scheduleSyntheticOverlayUpdate(kind, targetElements) {
|
|
847
|
+
const state = syntheticOverlayState;
|
|
848
|
+
if (state === null)
|
|
849
|
+
return;
|
|
850
|
+
if (kind === "geometry") {
|
|
851
|
+
const nextTargets = targetElements === undefined ? null : new Set(targetElements);
|
|
852
|
+
if (state.pendingUpdateKind !== "geometry") {
|
|
853
|
+
state.pendingGeometryTargets = nextTargets;
|
|
854
|
+
}
|
|
855
|
+
else if (state.pendingGeometryTargets !== null) {
|
|
856
|
+
if (nextTargets === null) {
|
|
857
|
+
state.pendingGeometryTargets = null;
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
for (const element of nextTargets) {
|
|
861
|
+
state.pendingGeometryTargets.add(element);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
state.pendingUpdateKind = "geometry";
|
|
866
|
+
state.geometryFollowUpsRemaining =
|
|
867
|
+
OVERLAY_STYLE.maxAdaptiveGeometryFrames - 1;
|
|
868
|
+
}
|
|
869
|
+
else if (state.pendingUpdateKind === null) {
|
|
870
|
+
state.pendingUpdateKind = "presentation";
|
|
871
|
+
}
|
|
872
|
+
if (state.frameId !== null)
|
|
873
|
+
return;
|
|
874
|
+
scheduleSyntheticOverlayFrame(state);
|
|
875
|
+
}
|
|
876
|
+
function scheduleSyntheticOverlayGeometryFollowUp(state, targetElements) {
|
|
877
|
+
if (state.geometryFollowUpsRemaining <= 0)
|
|
878
|
+
return;
|
|
879
|
+
state.geometryFollowUpsRemaining -= 1;
|
|
880
|
+
state.pendingUpdateKind = "geometry";
|
|
881
|
+
state.pendingGeometryTargets =
|
|
882
|
+
targetElements === null ? null : new Set(targetElements);
|
|
883
|
+
}
|
|
884
|
+
function scheduleSyntheticOverlayFrame(state) {
|
|
885
|
+
if (state.win.document.visibilityState === "hidden")
|
|
886
|
+
return;
|
|
887
|
+
let requestReturned = false;
|
|
888
|
+
let callbackRan = false;
|
|
889
|
+
const frameId = requestSyntheticAnimationFrame(state.win, () => {
|
|
890
|
+
callbackRan = true;
|
|
891
|
+
const current = syntheticOverlayState;
|
|
892
|
+
if (current === null)
|
|
893
|
+
return;
|
|
894
|
+
current.frameId = null;
|
|
895
|
+
if (current.win.document.visibilityState === "hidden")
|
|
896
|
+
return;
|
|
897
|
+
const kind = current.pendingUpdateKind ?? "presentation";
|
|
898
|
+
const geometryTargets = current.pendingGeometryTargets;
|
|
899
|
+
current.pendingUpdateKind = null;
|
|
900
|
+
current.pendingGeometryTargets = null;
|
|
901
|
+
const geometryChanged = kind === "geometry"
|
|
902
|
+
? updateSyntheticOverlayGeometry(current, geometryTargets)
|
|
903
|
+
: (updateSyntheticOverlayPresentation(current), false);
|
|
904
|
+
if (geometryChanged) {
|
|
905
|
+
scheduleSyntheticOverlayGeometryFollowUp(current, geometryTargets);
|
|
906
|
+
}
|
|
907
|
+
else if (kind === "geometry") {
|
|
908
|
+
current.geometryFollowUpsRemaining = 0;
|
|
909
|
+
}
|
|
910
|
+
if (requestReturned && current.pendingUpdateKind !== null) {
|
|
911
|
+
scheduleSyntheticOverlayFrame(current);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
requestReturned = true;
|
|
915
|
+
if (!callbackRan && syntheticOverlayState === state) {
|
|
916
|
+
state.frameId = frameId;
|
|
917
|
+
}
|
|
918
|
+
else if (callbackRan &&
|
|
919
|
+
syntheticOverlayState === state &&
|
|
920
|
+
state.pendingUpdateKind !== null) {
|
|
921
|
+
scheduleSyntheticOverlayFrame(state);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function requestSyntheticAnimationFrame(win, callback) {
|
|
925
|
+
if (typeof win.requestAnimationFrame === "function") {
|
|
926
|
+
return win.requestAnimationFrame(callback);
|
|
927
|
+
}
|
|
928
|
+
return win.setTimeout(callback, 16);
|
|
929
|
+
}
|
|
930
|
+
function cancelSyntheticAnimationFrame(win, frameId) {
|
|
931
|
+
if (typeof win.cancelAnimationFrame === "function") {
|
|
932
|
+
win.cancelAnimationFrame(frameId);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
win.clearTimeout(frameId);
|
|
936
|
+
}
|
|
937
|
+
function updateSyntheticOverlayGeometry(state, targetElements = null) {
|
|
938
|
+
const perfEnabled = isPreviewPerfEnabled(state.win);
|
|
939
|
+
const startedAt = perfEnabled ? previewPerfNow(state.win) : 0;
|
|
940
|
+
let geometryChanged = false;
|
|
941
|
+
let rectCount = 0;
|
|
942
|
+
let labelMeasurements = 0;
|
|
943
|
+
const entries = targetElements === null
|
|
944
|
+
? state.entries
|
|
945
|
+
: state.entries.filter((entry) => targetElements.has(entry.element));
|
|
946
|
+
const reads = entries.map((entry) => {
|
|
947
|
+
entry.anchoredToViewport ??= hasViewportAnchoredAncestor(entry.element, state.win);
|
|
948
|
+
const rects = getSyntheticOverlayRects(entry.element, state.win, entry.anchoredToViewport);
|
|
949
|
+
rectCount += rects.length;
|
|
950
|
+
if (!sameSyntheticOverlayRects(entry.lastRects, rects)) {
|
|
951
|
+
geometryChanged = true;
|
|
952
|
+
}
|
|
953
|
+
const states = rects.map((rect) => resolveSyntheticOverlayBoxState(state, entry.element, rect.viewport));
|
|
954
|
+
return { entry, rects, states };
|
|
955
|
+
});
|
|
956
|
+
// Write structure for every entry before measuring any label. This creates
|
|
957
|
+
// one layout boundary instead of alternating target reads, DOM writes and
|
|
958
|
+
// pill width reads for every box.
|
|
959
|
+
for (const { entry, rects } of reads) {
|
|
960
|
+
ensureSyntheticOverlayEntryBoxes(state.root, entry, rects.length);
|
|
961
|
+
for (const box of entry.boxes)
|
|
962
|
+
ensureSyntheticOverlayBoxPill(box, entry);
|
|
963
|
+
}
|
|
964
|
+
for (const { entry } of reads) {
|
|
965
|
+
if (entry.labelWidth !== null || !entry.crossDoc || entry.label === null) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const pill = entry.boxes[0]?.querySelector('[data-slot="ss-preview-cross-doc-pill"]');
|
|
969
|
+
if (pill === undefined || pill === null)
|
|
970
|
+
continue;
|
|
971
|
+
entry.labelWidth = Math.max(pill.getBoundingClientRect().width, pill.offsetWidth, pill.scrollWidth);
|
|
972
|
+
labelMeasurements += 1;
|
|
973
|
+
}
|
|
974
|
+
const viewportWidth = state.root.ownerDocument.documentElement.clientWidth ||
|
|
975
|
+
state.win.innerWidth;
|
|
976
|
+
let geometryWrites = 0;
|
|
977
|
+
for (const { entry, rects, states } of reads) {
|
|
978
|
+
for (let index = 0; index < entry.boxes.length; index++) {
|
|
979
|
+
const box = entry.boxes[index];
|
|
980
|
+
const rect = rects[index];
|
|
981
|
+
if (rect === undefined) {
|
|
982
|
+
if (setSyntheticOverlayBoxState(box, "degraded"))
|
|
983
|
+
geometryWrites += 1;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (setSyntheticOverlayBoxState(box, states[index] ?? "degraded")) {
|
|
987
|
+
geometryWrites += 1;
|
|
988
|
+
}
|
|
989
|
+
if (box.getAttribute(SYNTHETIC_BOX_KEY_ATTR) !== entry.paintKey) {
|
|
990
|
+
box.setAttribute(SYNTHETIC_BOX_KEY_ATTR, entry.paintKey); // C4: update paintKey in place
|
|
991
|
+
geometryWrites += 1;
|
|
992
|
+
}
|
|
993
|
+
const position = rect.anchoredToViewport ? "fixed" : "absolute";
|
|
994
|
+
if (box.style.position !== position) {
|
|
995
|
+
box.style.position = position;
|
|
996
|
+
geometryWrites += 1;
|
|
997
|
+
}
|
|
998
|
+
const width = `${rect.width}px`;
|
|
999
|
+
if (box.style.width !== width) {
|
|
1000
|
+
box.style.width = width;
|
|
1001
|
+
geometryWrites += 1;
|
|
1002
|
+
}
|
|
1003
|
+
const height = `${rect.height}px`;
|
|
1004
|
+
if (box.style.height !== height) {
|
|
1005
|
+
box.style.height = height;
|
|
1006
|
+
geometryWrites += 1;
|
|
1007
|
+
}
|
|
1008
|
+
const transform = rect.anchoredToViewport
|
|
1009
|
+
? `translate3d(${rect.viewport.left}px, ${rect.viewport.top}px, 0)`
|
|
1010
|
+
: `translate3d(${rect.documentX}px, ${rect.documentY}px, 0)`;
|
|
1011
|
+
if (box.style.transform !== transform) {
|
|
1012
|
+
box.style.transform = transform;
|
|
1013
|
+
geometryWrites += 1;
|
|
1014
|
+
}
|
|
1015
|
+
geometryWrites += updateSyntheticOverlayPillPlacement(box, rect.viewport, entry.labelWidth, viewportWidth);
|
|
1016
|
+
}
|
|
1017
|
+
entry.lastRects = rects;
|
|
1018
|
+
}
|
|
1019
|
+
const committedGeneration = state.pendingCommitMetricGeneration;
|
|
1020
|
+
const committedBoxCount = state.entries.reduce((count, entry) => count + entry.boxes.length, 0);
|
|
1021
|
+
if (committedGeneration !== null && committedBoxCount > 0) {
|
|
1022
|
+
state.pendingCommitMetricGeneration = null;
|
|
1023
|
+
recordPreviewPerfMetric(state.win, {
|
|
1024
|
+
name: "preview.overlayBoxesCommitted",
|
|
1025
|
+
ts: previewPerfNow(state.win),
|
|
1026
|
+
detail: {
|
|
1027
|
+
generation: committedGeneration,
|
|
1028
|
+
paintedCount: committedBoxCount,
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (perfEnabled) {
|
|
1033
|
+
recordPreviewPerfMetric(state.win, {
|
|
1034
|
+
name: "preview.overlayGeometryPass",
|
|
1035
|
+
durationMs: Math.max(0, previewPerfNow(state.win) - startedAt),
|
|
1036
|
+
detail: {
|
|
1037
|
+
entryCount: entries.length,
|
|
1038
|
+
rectCount,
|
|
1039
|
+
geometryWrites,
|
|
1040
|
+
labelMeasurements,
|
|
1041
|
+
geometryChanged,
|
|
1042
|
+
targeted: targetElements !== null,
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return geometryChanged;
|
|
1047
|
+
}
|
|
1048
|
+
function updateSyntheticOverlayPresentation(state) {
|
|
1049
|
+
const perfEnabled = isPreviewPerfEnabled(state.win);
|
|
1050
|
+
const startedAt = perfEnabled ? previewPerfNow(state.win) : 0;
|
|
1051
|
+
let stateWrites = 0;
|
|
1052
|
+
for (const entry of state.entries) {
|
|
1053
|
+
for (let index = 0; index < entry.boxes.length; index += 1) {
|
|
1054
|
+
const box = entry.boxes[index];
|
|
1055
|
+
const rect = entry.lastRects[index];
|
|
1056
|
+
if (setSyntheticOverlayBoxState(box, rect === undefined
|
|
1057
|
+
? "degraded"
|
|
1058
|
+
: resolveSyntheticOverlayBoxState(state, entry.element, rect.viewport))) {
|
|
1059
|
+
stateWrites += 1;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (perfEnabled) {
|
|
1064
|
+
recordPreviewPerfMetric(state.win, {
|
|
1065
|
+
name: "preview.overlayPresentationPass",
|
|
1066
|
+
durationMs: Math.max(0, previewPerfNow(state.win) - startedAt),
|
|
1067
|
+
detail: {
|
|
1068
|
+
entryCount: state.entries.length,
|
|
1069
|
+
stateWrites,
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function sameSyntheticOverlayRects(left, right) {
|
|
1075
|
+
if (left.length !== right.length)
|
|
1076
|
+
return false;
|
|
1077
|
+
return left.every((rect, index) => {
|
|
1078
|
+
const candidate = right[index];
|
|
1079
|
+
if (candidate === undefined)
|
|
1080
|
+
return false;
|
|
1081
|
+
return (Math.abs(rect.viewport.left - candidate.viewport.left) < 0.1 &&
|
|
1082
|
+
Math.abs(rect.viewport.top - candidate.viewport.top) < 0.1 &&
|
|
1083
|
+
Math.abs(rect.width - candidate.width) < 0.1 &&
|
|
1084
|
+
Math.abs(rect.height - candidate.height) < 0.1 &&
|
|
1085
|
+
rect.anchoredToViewport === candidate.anchoredToViewport);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
function invalidateSyntheticOverlayLabelWidths() {
|
|
1089
|
+
const state = syntheticOverlayState;
|
|
1090
|
+
if (state === null)
|
|
1091
|
+
return;
|
|
1092
|
+
for (const entry of state.entries)
|
|
1093
|
+
entry.labelWidth = null;
|
|
1094
|
+
}
|
|
1095
|
+
function invalidateSyntheticOverlayPositioningModes() {
|
|
1096
|
+
const state = syntheticOverlayState;
|
|
1097
|
+
if (state === null)
|
|
1098
|
+
return;
|
|
1099
|
+
for (const entry of state.entries)
|
|
1100
|
+
entry.anchoredToViewport = null;
|
|
1101
|
+
}
|
|
1102
|
+
function ensureSyntheticOverlayEntryBoxes(root, entry, count) {
|
|
1103
|
+
while (entry.boxes.length > count) {
|
|
1104
|
+
entry.boxes.pop()?.remove();
|
|
1105
|
+
}
|
|
1106
|
+
while (entry.boxes.length < count) {
|
|
1107
|
+
const box = root.ownerDocument.createElement("div");
|
|
1108
|
+
box.setAttribute(SYNTHETIC_BOX_ATTR, "true");
|
|
1109
|
+
box.setAttribute(SYNTHETIC_BOX_KEY_ATTR, entry.paintKey);
|
|
1110
|
+
box.setAttribute(SYNTHETIC_BOX_STATE_ATTR, "idle");
|
|
1111
|
+
box.setAttribute(SYNTHETIC_BOX_TONE_ATTR, entry.tone);
|
|
1112
|
+
box.style.position = "absolute";
|
|
1113
|
+
box.style.left = "0";
|
|
1114
|
+
box.style.top = "0";
|
|
1115
|
+
box.style.boxSizing = "border-box";
|
|
1116
|
+
box.style.border = `${OVERLAY_STYLE.borderWidth}px solid ${SYNTHETIC_OVERLAY_STATE_TOKENS[entry.tone].idle.color}`;
|
|
1117
|
+
box.style.borderRadius = `${OVERLAY_STYLE.radius}px`;
|
|
1118
|
+
box.style.backgroundColor =
|
|
1119
|
+
SYNTHETIC_OVERLAY_STATE_TOKENS[entry.tone].idle.fill;
|
|
1120
|
+
box.style.overflow = "visible";
|
|
1121
|
+
box.style.pointerEvents = "none";
|
|
1122
|
+
box.style.cursor = "inherit";
|
|
1123
|
+
box.style.transformOrigin = "0 0";
|
|
1124
|
+
ensureSyntheticOverlayBoxPill(box, entry);
|
|
1125
|
+
root.appendChild(box);
|
|
1126
|
+
entry.boxes.push(box);
|
|
1127
|
+
}
|
|
1128
|
+
for (const box of entry.boxes) {
|
|
1129
|
+
if (box.getAttribute(SYNTHETIC_BOX_TONE_ATTR) !== entry.tone) {
|
|
1130
|
+
box.setAttribute(SYNTHETIC_BOX_TONE_ATTR, entry.tone);
|
|
1131
|
+
const pill = box.querySelector('[data-slot="ss-preview-cross-doc-pill"]');
|
|
1132
|
+
if (pill !== null)
|
|
1133
|
+
applyPreviewOverlayLabelVisualStyle(pill, entry.tone);
|
|
1134
|
+
// Force the next state application to pick up the new tone tokens.
|
|
1135
|
+
box.removeAttribute(SYNTHETIC_BOX_STATE_ATTR);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function ensureSyntheticOverlayBoxPill(box, entry) {
|
|
1140
|
+
let pill = box.querySelector('[data-slot="ss-preview-cross-doc-pill"]');
|
|
1141
|
+
if (!entry.crossDoc || entry.label === null) {
|
|
1142
|
+
pill?.remove();
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (pill === null) {
|
|
1146
|
+
pill = box.ownerDocument.createElement("span");
|
|
1147
|
+
pill.setAttribute("data-slot", "ss-preview-cross-doc-pill");
|
|
1148
|
+
pill.style.position = "absolute";
|
|
1149
|
+
pill.style.top = `-${PREVIEW_OVERLAY_LABEL_HEIGHT_PX + PREVIEW_OVERLAY_LABEL_GAP_PX}px`;
|
|
1150
|
+
pill.style.right = "0";
|
|
1151
|
+
pill.style.maxWidth = "160px";
|
|
1152
|
+
pill.style.overflow = "hidden";
|
|
1153
|
+
pill.style.textOverflow = "ellipsis";
|
|
1154
|
+
pill.style.whiteSpace = "nowrap";
|
|
1155
|
+
pill.style.opacity = "0";
|
|
1156
|
+
pill.style.pointerEvents = "none";
|
|
1157
|
+
applyPreviewOverlayLabelVisualStyle(pill, entry.tone);
|
|
1158
|
+
box.appendChild(pill);
|
|
1159
|
+
}
|
|
1160
|
+
// Geometry passes only reposition the label. Its visual tokens change on
|
|
1161
|
+
// creation or tone reconciliation, not on every viewport resize frame.
|
|
1162
|
+
if (pill.textContent !== entry.label)
|
|
1163
|
+
pill.textContent = entry.label;
|
|
1164
|
+
}
|
|
1165
|
+
function setSyntheticOverlayBoxState(box, state) {
|
|
1166
|
+
if (box.getAttribute(SYNTHETIC_BOX_STATE_ATTR) === state)
|
|
1167
|
+
return false;
|
|
1168
|
+
box.setAttribute(SYNTHETIC_BOX_STATE_ATTR, state);
|
|
1169
|
+
const tone = box.getAttribute(SYNTHETIC_BOX_TONE_ATTR) === "model"
|
|
1170
|
+
? "model"
|
|
1171
|
+
: "default";
|
|
1172
|
+
const tokens = SYNTHETIC_OVERLAY_STATE_TOKENS[tone][state];
|
|
1173
|
+
box.style.borderColor = tokens.color;
|
|
1174
|
+
box.style.backgroundColor = tokens.fill;
|
|
1175
|
+
const pill = box.querySelector('[data-slot="ss-preview-cross-doc-pill"]');
|
|
1176
|
+
if (pill !== null) {
|
|
1177
|
+
pill.style.opacity =
|
|
1178
|
+
state === "hover" || state === "focus" || state === "reveal" ? "1" : "0";
|
|
1179
|
+
}
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
function updateSyntheticOverlayPillPlacement(box, rect, labelWidth, viewportWidth) {
|
|
1183
|
+
const pill = box.querySelector('[data-slot="ss-preview-cross-doc-pill"]');
|
|
1184
|
+
if (pill === null)
|
|
1185
|
+
return 0;
|
|
1186
|
+
let writes = 0;
|
|
1187
|
+
const labelOffset = PREVIEW_OVERLAY_LABEL_HEIGHT_PX + PREVIEW_OVERLAY_LABEL_GAP_PX;
|
|
1188
|
+
const horizontal = resolvePreviewOverlayLabelHorizontalPosition({
|
|
1189
|
+
targetLeft: rect.left,
|
|
1190
|
+
targetWidth: rect.width,
|
|
1191
|
+
labelWidth: labelWidth ?? 0,
|
|
1192
|
+
viewportWidth: viewportWidth || rect.right,
|
|
1193
|
+
});
|
|
1194
|
+
const collideTop = rect.top < labelOffset;
|
|
1195
|
+
const collisionTop = collideTop ? "true" : "false";
|
|
1196
|
+
if (box.getAttribute("data-collision-top") !== collisionTop) {
|
|
1197
|
+
box.setAttribute("data-collision-top", collisionTop);
|
|
1198
|
+
writes += 1;
|
|
1199
|
+
}
|
|
1200
|
+
if (pill.getAttribute("data-horizontal-anchor") !== horizontal.anchor) {
|
|
1201
|
+
pill.setAttribute("data-horizontal-anchor", horizontal.anchor);
|
|
1202
|
+
writes += 1;
|
|
1203
|
+
}
|
|
1204
|
+
const left = `${horizontal.left - rect.left}px`;
|
|
1205
|
+
if (pill.style.left !== left) {
|
|
1206
|
+
pill.style.left = left;
|
|
1207
|
+
writes += 1;
|
|
1208
|
+
}
|
|
1209
|
+
if (pill.style.right !== "auto") {
|
|
1210
|
+
pill.style.right = "auto";
|
|
1211
|
+
writes += 1;
|
|
1212
|
+
}
|
|
1213
|
+
const offset = `-${labelOffset}px`;
|
|
1214
|
+
const top = collideTop ? "auto" : offset;
|
|
1215
|
+
if (pill.style.top !== top) {
|
|
1216
|
+
pill.style.top = top;
|
|
1217
|
+
writes += 1;
|
|
1218
|
+
}
|
|
1219
|
+
const bottom = collideTop ? offset : "auto";
|
|
1220
|
+
if (pill.style.bottom !== bottom) {
|
|
1221
|
+
pill.style.bottom = bottom;
|
|
1222
|
+
writes += 1;
|
|
1223
|
+
}
|
|
1224
|
+
return writes;
|
|
1225
|
+
}
|
|
1226
|
+
function getSyntheticOverlayRects(element, win, anchoredToViewport = hasViewportAnchoredAncestor(element, win)) {
|
|
1227
|
+
let rects = Array.from(element.getClientRects());
|
|
1228
|
+
if (rects.length === 0)
|
|
1229
|
+
rects.push(element.getBoundingClientRect());
|
|
1230
|
+
rects = coalesceRichTextOverlayRects(element, rects, win);
|
|
1231
|
+
return rects.flatMap((rect) => isDrawableSyntheticRect(element, rect, win)
|
|
1232
|
+
? [
|
|
1233
|
+
{
|
|
1234
|
+
viewport: rect,
|
|
1235
|
+
documentX: rect.left + win.scrollX,
|
|
1236
|
+
documentY: rect.top + win.scrollY,
|
|
1237
|
+
width: rect.width,
|
|
1238
|
+
height: rect.height,
|
|
1239
|
+
anchoredToViewport,
|
|
1240
|
+
},
|
|
1241
|
+
]
|
|
1242
|
+
: []);
|
|
1243
|
+
}
|
|
1244
|
+
function coalesceRichTextOverlayRects(element, rects, win) {
|
|
1245
|
+
if (rects.length <= 1)
|
|
1246
|
+
return rects;
|
|
1247
|
+
if (!isRichTextInlineOverlayTarget(element, win))
|
|
1248
|
+
return rects;
|
|
1249
|
+
const drawable = rects.filter((rect) => isDrawableSyntheticRect(element, rect, win));
|
|
1250
|
+
if (drawable.length <= 1)
|
|
1251
|
+
return rects;
|
|
1252
|
+
return [unionDomRects(drawable)];
|
|
1253
|
+
}
|
|
1254
|
+
function isRichTextInlineOverlayTarget(element, win) {
|
|
1255
|
+
if (element.hasAttribute("data-ss-richtext-block"))
|
|
1256
|
+
return true;
|
|
1257
|
+
if (!element.hasAttribute("data-ss-p"))
|
|
1258
|
+
return false;
|
|
1259
|
+
const textBlock = findSyntheticOverlayHoverGroupRoot(element);
|
|
1260
|
+
if (textBlock === null || textBlock === element)
|
|
1261
|
+
return false;
|
|
1262
|
+
const display = win.getComputedStyle(element).display;
|
|
1263
|
+
return display === "inline" || display === "inline-block";
|
|
1264
|
+
}
|
|
1265
|
+
function richTextVisualTarget(element) {
|
|
1266
|
+
if (!element.hasAttribute("data-ss-p") || element.hasAttribute("data-ss-richtext-link")) {
|
|
1267
|
+
return element;
|
|
1268
|
+
}
|
|
1269
|
+
return element.closest("[data-ss-richtext-block]") ?? element;
|
|
1270
|
+
}
|
|
1271
|
+
function unionDomRects(rects) {
|
|
1272
|
+
let left = Number.POSITIVE_INFINITY;
|
|
1273
|
+
let top = Number.POSITIVE_INFINITY;
|
|
1274
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
1275
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
1276
|
+
for (const rect of rects) {
|
|
1277
|
+
left = Math.min(left, rect.left);
|
|
1278
|
+
top = Math.min(top, rect.top);
|
|
1279
|
+
right = Math.max(right, rect.right);
|
|
1280
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
1281
|
+
}
|
|
1282
|
+
const width = right - left;
|
|
1283
|
+
const height = bottom - top;
|
|
1284
|
+
return {
|
|
1285
|
+
x: left,
|
|
1286
|
+
y: top,
|
|
1287
|
+
left,
|
|
1288
|
+
top,
|
|
1289
|
+
right,
|
|
1290
|
+
bottom,
|
|
1291
|
+
width,
|
|
1292
|
+
height,
|
|
1293
|
+
toJSON: () => ({}),
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function hasViewportAnchoredAncestor(element, win) {
|
|
1297
|
+
let current = element;
|
|
1298
|
+
while (current !== null) {
|
|
1299
|
+
const position = win.getComputedStyle(current).position;
|
|
1300
|
+
if (position === "fixed" || position === "sticky")
|
|
1301
|
+
return true;
|
|
1302
|
+
current = current.parentElement;
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
function resolveSyntheticOverlayBoxState(state, element, rect) {
|
|
1307
|
+
if (!isSyntheticOverlayTargetIntersecting(state, element))
|
|
1308
|
+
return "degraded";
|
|
1309
|
+
if (!isSyntheticOverlayRectTopmost(element, rect, state.win)) {
|
|
1310
|
+
return "degraded";
|
|
1311
|
+
}
|
|
1312
|
+
if (element.getAttribute(REVEAL_ATTR) === "active")
|
|
1313
|
+
return "reveal";
|
|
1314
|
+
if (state.focusedTarget === element)
|
|
1315
|
+
return "focus";
|
|
1316
|
+
if (state.hoveredTargets.has(element))
|
|
1317
|
+
return "hover";
|
|
1318
|
+
return "idle";
|
|
1319
|
+
}
|
|
1320
|
+
function resolveSyntheticOverlayHoverTargets(target) {
|
|
1321
|
+
// Hover must identify the same single, most-specific DOM target that a
|
|
1322
|
+
// click would route. Expanding to every provenance target in the nearest
|
|
1323
|
+
// text block makes nested parent/inline overlays — and their cross-document
|
|
1324
|
+
// labels — appear simultaneously even though only one is under the pointer.
|
|
1325
|
+
return new Set([target]);
|
|
1326
|
+
}
|
|
1327
|
+
function scheduleSyntheticOverlayHoverUpdate(state, nextTargets) {
|
|
1328
|
+
if (sameElementSet(state.hoveredTargets, nextTargets))
|
|
1329
|
+
return;
|
|
1330
|
+
const geometryTargets = new Set(state.hoveredTargets);
|
|
1331
|
+
for (const element of nextTargets)
|
|
1332
|
+
geometryTargets.add(element);
|
|
1333
|
+
state.hoveredTargets = nextTargets;
|
|
1334
|
+
// CSS :hover may move a target without producing a DOM mutation, resize or
|
|
1335
|
+
// intersection event. Measure only the targets entering/leaving hover and
|
|
1336
|
+
// let the adaptive frame budget follow an in-flight transform until stable.
|
|
1337
|
+
scheduleSyntheticOverlayUpdate("geometry", geometryTargets);
|
|
1338
|
+
}
|
|
1339
|
+
function scheduleSyntheticOverlayFocusUpdate(state, nextTarget) {
|
|
1340
|
+
if (state.focusedTarget === nextTarget)
|
|
1341
|
+
return;
|
|
1342
|
+
const geometryTargets = new Set();
|
|
1343
|
+
if (state.focusedTarget !== null)
|
|
1344
|
+
geometryTargets.add(state.focusedTarget);
|
|
1345
|
+
if (nextTarget !== null)
|
|
1346
|
+
geometryTargets.add(nextTarget);
|
|
1347
|
+
state.focusedTarget = nextTarget;
|
|
1348
|
+
// Focus styles have the same geometry risk as hover styles, so reuse the
|
|
1349
|
+
// targeted and bounded animation follow-up instead of measuring every box.
|
|
1350
|
+
scheduleSyntheticOverlayUpdate("geometry", geometryTargets);
|
|
1351
|
+
}
|
|
1352
|
+
function sameElementSet(a, b) {
|
|
1353
|
+
if (a.size !== b.size)
|
|
1354
|
+
return false;
|
|
1355
|
+
for (const element of a) {
|
|
1356
|
+
if (!b.has(element))
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
function findSyntheticOverlayHoverGroupRoot(target) {
|
|
1362
|
+
return target.closest("p,li,blockquote,figcaption,caption,dt,dd,td,th,h1,h2,h3,h4,h5,h6");
|
|
1363
|
+
}
|
|
1364
|
+
function isSyntheticOverlayTargetIntersecting(state, element) {
|
|
1365
|
+
return state.visibleTargets === null || state.visibleTargets.has(element);
|
|
1366
|
+
}
|
|
1367
|
+
function isDrawableSyntheticRect(element, rect, win) {
|
|
1368
|
+
if (!element.ownerDocument.contains(element))
|
|
1369
|
+
return false;
|
|
1370
|
+
if (!Number.isFinite(rect.left) || !Number.isFinite(rect.top))
|
|
1371
|
+
return false;
|
|
1372
|
+
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height))
|
|
1373
|
+
return false;
|
|
1374
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
1375
|
+
return false;
|
|
1376
|
+
if (rect.right <= 0 || rect.bottom <= 0)
|
|
1377
|
+
return false;
|
|
1378
|
+
if (rect.left >= win.innerWidth || rect.top >= win.innerHeight)
|
|
1379
|
+
return false;
|
|
1380
|
+
const style = win.getComputedStyle(element);
|
|
1381
|
+
if (style.display === "none")
|
|
1382
|
+
return false;
|
|
1383
|
+
if (style.visibility === "hidden" || style.visibility === "collapse") {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
if (style.opacity === "0")
|
|
1387
|
+
return false;
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
function isSyntheticOverlayRectTopmost(element, rect, win) {
|
|
1391
|
+
const doc = element.ownerDocument;
|
|
1392
|
+
if (typeof doc.elementFromPoint !== "function" &&
|
|
1393
|
+
typeof doc.elementsFromPoint !== "function") {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
const points = getSyntheticOverlayHitTestPoints(rect, win);
|
|
1397
|
+
if (points.length === 0)
|
|
1398
|
+
return false;
|
|
1399
|
+
for (const point of points) {
|
|
1400
|
+
const topmost = elementFromPointIgnoringSyntheticOverlays(doc, point.x, point.y);
|
|
1401
|
+
if (topmost !== null && isCompatibleSyntheticTopmost(element, topmost)) {
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
function getSyntheticOverlayHitTestPoints(rect, win) {
|
|
1408
|
+
const left = Math.max(0, rect.left);
|
|
1409
|
+
const top = Math.max(0, rect.top);
|
|
1410
|
+
const right = Math.min(win.innerWidth, rect.right);
|
|
1411
|
+
const bottom = Math.min(win.innerHeight, rect.bottom);
|
|
1412
|
+
if (right <= left || bottom <= top)
|
|
1413
|
+
return [];
|
|
1414
|
+
const insetX = Math.min(2, Math.max(0, (right - left) / 2));
|
|
1415
|
+
const insetY = Math.min(2, Math.max(0, (bottom - top) / 2));
|
|
1416
|
+
const center = {
|
|
1417
|
+
x: left + (right - left) / 2,
|
|
1418
|
+
y: top + (bottom - top) / 2,
|
|
1419
|
+
};
|
|
1420
|
+
const points = [
|
|
1421
|
+
center,
|
|
1422
|
+
{ x: left + insetX, y: top + insetY },
|
|
1423
|
+
{ x: right - insetX, y: top + insetY },
|
|
1424
|
+
{ x: left + insetX, y: bottom - insetY },
|
|
1425
|
+
{ x: right - insetX, y: bottom - insetY },
|
|
1426
|
+
];
|
|
1427
|
+
const seen = new Set();
|
|
1428
|
+
return points.filter((point) => {
|
|
1429
|
+
const x = Math.min(Math.max(point.x, 0), win.innerWidth - 1);
|
|
1430
|
+
const y = Math.min(Math.max(point.y, 0), win.innerHeight - 1);
|
|
1431
|
+
const key = `${Math.round(x * 10) / 10}:${Math.round(y * 10) / 10}`;
|
|
1432
|
+
if (seen.has(key))
|
|
1433
|
+
return false;
|
|
1434
|
+
seen.add(key);
|
|
1435
|
+
point.x = x;
|
|
1436
|
+
point.y = y;
|
|
1437
|
+
return true;
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
function elementFromPointIgnoringSyntheticOverlays(doc, x, y) {
|
|
1441
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
1442
|
+
return (doc
|
|
1443
|
+
.elementsFromPoint(x, y)
|
|
1444
|
+
.find((element) => !isSyntheticOverlayNode(element)) ?? null);
|
|
1445
|
+
}
|
|
1446
|
+
if (typeof doc.elementFromPoint !== "function")
|
|
1447
|
+
return null;
|
|
1448
|
+
const element = doc.elementFromPoint(x, y);
|
|
1449
|
+
if (element === null || !isSyntheticOverlayNode(element))
|
|
1450
|
+
return element;
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
function isSyntheticOverlayNode(element) {
|
|
1454
|
+
return (element.hasAttribute(SYNTHETIC_ROOT_ATTR) ||
|
|
1455
|
+
element.hasAttribute(SYNTHETIC_BOX_ATTR));
|
|
1456
|
+
}
|
|
1457
|
+
function isCompatibleSyntheticTopmost(target, topmost) {
|
|
1458
|
+
if (topmost === target)
|
|
1459
|
+
return true;
|
|
1460
|
+
if (target.contains(topmost))
|
|
1461
|
+
return true;
|
|
1462
|
+
if (topmost !== target.ownerDocument.body &&
|
|
1463
|
+
topmost !== target.ownerDocument.documentElement &&
|
|
1464
|
+
topmost.contains(target)) {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
function findSyntheticOverlayTargetFromNode(state, node) {
|
|
1470
|
+
if (state === null)
|
|
1471
|
+
return null;
|
|
1472
|
+
let current = isNode(node) ? node : null;
|
|
1473
|
+
while (current !== null) {
|
|
1474
|
+
if (isElementNode(current)) {
|
|
1475
|
+
if (state.targetElements.has(current)) {
|
|
1476
|
+
return current;
|
|
1477
|
+
}
|
|
1478
|
+
const box = syntheticOverlayBoxForNode(current);
|
|
1479
|
+
if (box !== null) {
|
|
1480
|
+
// Boxes are entry-owned: membership is the unambiguous mapping.
|
|
1481
|
+
// paintKey alone collides for multi-occurrence targets of one item.
|
|
1482
|
+
const owner = state.entries.find((candidate) => candidate.boxes.includes(box));
|
|
1483
|
+
if (owner !== undefined)
|
|
1484
|
+
return owner.element;
|
|
1485
|
+
const paintKey = box.getAttribute(SYNTHETIC_BOX_KEY_ATTR);
|
|
1486
|
+
if (paintKey !== null) {
|
|
1487
|
+
const entry = state.entries.find((candidate) => candidate.paintKey === paintKey);
|
|
1488
|
+
if (entry !== undefined)
|
|
1489
|
+
return entry.element;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
current = current.parentNode;
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
function findSyntheticOverlayTargetAtEventPoint(state, event) {
|
|
1498
|
+
const mouseEvent = event;
|
|
1499
|
+
if (!Number.isFinite(mouseEvent.clientX) ||
|
|
1500
|
+
!Number.isFinite(mouseEvent.clientY)) {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
return findSyntheticOverlayTargetAtPoint(state, mouseEvent.clientX, mouseEvent.clientY);
|
|
1504
|
+
}
|
|
1505
|
+
function findSyntheticOverlayTargetAtPoint(state, x, y) {
|
|
1506
|
+
const topmost = elementFromPointIgnoringSyntheticOverlays(state.win.document, x, y);
|
|
1507
|
+
if (topmost === null)
|
|
1508
|
+
return null;
|
|
1509
|
+
const directTarget = findSyntheticOverlayTargetFromNode(state, topmost);
|
|
1510
|
+
if (directTarget !== null)
|
|
1511
|
+
return directTarget;
|
|
1512
|
+
for (const entry of state.entries) {
|
|
1513
|
+
if (!isSyntheticOverlayTargetIntersecting(state, entry.element))
|
|
1514
|
+
continue;
|
|
1515
|
+
if (!isCompatibleSyntheticTopmost(entry.element, topmost))
|
|
1516
|
+
continue;
|
|
1517
|
+
const rects = getSyntheticOverlayRects(entry.element, state.win);
|
|
1518
|
+
if (rects.some((rect) => pointInRect(x, y, rect.viewport))) {
|
|
1519
|
+
return entry.element;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
function pointInRect(x, y, rect) {
|
|
1525
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
1526
|
+
}
|
|
1527
|
+
function syntheticOverlayBoxForNode(element) {
|
|
1528
|
+
if (element.hasAttribute(SYNTHETIC_BOX_ATTR))
|
|
1529
|
+
return element;
|
|
1530
|
+
return element.closest(`[${SYNTHETIC_BOX_ATTR}]`);
|
|
1531
|
+
}
|
|
1532
|
+
function installEventDelegation(win) {
|
|
1533
|
+
if (listenersInstalled)
|
|
1534
|
+
return;
|
|
1535
|
+
listenersInstalled = true;
|
|
1536
|
+
activeWindow = win;
|
|
1537
|
+
win.document.addEventListener("pointerdown", onOverlayPointerDown, true);
|
|
1538
|
+
win.document.addEventListener("pointerup", onOverlaySuppressOnly, true);
|
|
1539
|
+
win.document.addEventListener("mousedown", onOverlaySuppressOnly, true);
|
|
1540
|
+
win.document.addEventListener("mouseup", onOverlaySuppressOnly, true);
|
|
1541
|
+
win.document.addEventListener("selectstart", onOverlaySuppressOnly, true);
|
|
1542
|
+
win.document.addEventListener("dragstart", onOverlaySuppressOnly, true);
|
|
1543
|
+
win.document.addEventListener("keydown", onOverlayKeyDown, true);
|
|
1544
|
+
win.document.addEventListener("click", onOverlayClick, true);
|
|
1545
|
+
win.document.addEventListener("dblclick", onOverlaySuppressOnly, true);
|
|
1546
|
+
win.document.addEventListener("auxclick", onOverlaySuppressOnly, true);
|
|
1547
|
+
win.document.addEventListener("contextmenu", onOverlaySuppressOnly, true);
|
|
1548
|
+
win.document.addEventListener("submit", onOverlaySuppressOnly, true);
|
|
1549
|
+
}
|
|
1550
|
+
function maybeUninstallEventDelegation() {
|
|
1551
|
+
if (overlayDelegationActive || editInteractionGuardCount > 0)
|
|
1552
|
+
return;
|
|
1553
|
+
uninstallEventDelegation();
|
|
1554
|
+
activeWindow = null;
|
|
1555
|
+
}
|
|
1556
|
+
function uninstallEventDelegation() {
|
|
1557
|
+
if (!listenersInstalled || activeWindow === null)
|
|
1558
|
+
return;
|
|
1559
|
+
listenersInstalled = false;
|
|
1560
|
+
const doc = activeWindow.document;
|
|
1561
|
+
doc.removeEventListener("pointerdown", onOverlayPointerDown, true);
|
|
1562
|
+
doc.removeEventListener("pointerup", onOverlaySuppressOnly, true);
|
|
1563
|
+
doc.removeEventListener("mousedown", onOverlaySuppressOnly, true);
|
|
1564
|
+
doc.removeEventListener("mouseup", onOverlaySuppressOnly, true);
|
|
1565
|
+
doc.removeEventListener("selectstart", onOverlaySuppressOnly, true);
|
|
1566
|
+
doc.removeEventListener("dragstart", onOverlaySuppressOnly, true);
|
|
1567
|
+
doc.removeEventListener("keydown", onOverlayKeyDown, true);
|
|
1568
|
+
doc.removeEventListener("click", onOverlayClick, true);
|
|
1569
|
+
doc.removeEventListener("dblclick", onOverlaySuppressOnly, true);
|
|
1570
|
+
doc.removeEventListener("auxclick", onOverlaySuppressOnly, true);
|
|
1571
|
+
doc.removeEventListener("contextmenu", onOverlaySuppressOnly, true);
|
|
1572
|
+
doc.removeEventListener("submit", onOverlaySuppressOnly, true);
|
|
1573
|
+
}
|
|
1574
|
+
function onOverlayPointerDown(event) {
|
|
1575
|
+
if (isPreviewControlsEvent(event))
|
|
1576
|
+
return;
|
|
1577
|
+
const button = normalizeMouseButton(event);
|
|
1578
|
+
if (button !== null)
|
|
1579
|
+
emitIframeInteraction(button);
|
|
1580
|
+
if (!shouldBlockNativeInteraction())
|
|
1581
|
+
return;
|
|
1582
|
+
const target = getInteractiveTarget(event);
|
|
1583
|
+
suppressNativeEvent(event);
|
|
1584
|
+
if (target === null || button === null)
|
|
1585
|
+
return;
|
|
1586
|
+
emitOverlayClick(target, button);
|
|
1587
|
+
}
|
|
1588
|
+
function onOverlayClick(event) {
|
|
1589
|
+
if (!shouldBlockNativeInteraction() || isPreviewControlsEvent(event))
|
|
1590
|
+
return;
|
|
1591
|
+
const target = getInteractiveTarget(event);
|
|
1592
|
+
suppressNativeEvent(event);
|
|
1593
|
+
if (target === null)
|
|
1594
|
+
return;
|
|
1595
|
+
const mouseEvent = event;
|
|
1596
|
+
// Pointer/mouse clicks already emitted on pointerdown. Keyboard-
|
|
1597
|
+
// generated click events have detail=0, so keep that path reachable.
|
|
1598
|
+
if (mouseEvent.detail !== 0)
|
|
1599
|
+
return;
|
|
1600
|
+
emitOverlayClick(target, 0);
|
|
1601
|
+
}
|
|
1602
|
+
function onOverlayKeyDown(event) {
|
|
1603
|
+
if (!shouldBlockNativeInteraction() || isPreviewControlsEvent(event))
|
|
1604
|
+
return;
|
|
1605
|
+
const target = getInteractiveTarget(event);
|
|
1606
|
+
const key = event.key;
|
|
1607
|
+
if (key !== "Enter" && key !== " ")
|
|
1608
|
+
return;
|
|
1609
|
+
suppressNativeEvent(event);
|
|
1610
|
+
if (target === null)
|
|
1611
|
+
return;
|
|
1612
|
+
emitOverlayClick(target, 0);
|
|
1613
|
+
}
|
|
1614
|
+
function onOverlaySuppressOnly(event) {
|
|
1615
|
+
if (!shouldBlockNativeInteraction() || isPreviewControlsEvent(event))
|
|
1616
|
+
return;
|
|
1617
|
+
suppressNativeEvent(event);
|
|
1618
|
+
}
|
|
1619
|
+
function shouldBlockNativeInteraction() {
|
|
1620
|
+
return getInteractionMode() === "edit";
|
|
1621
|
+
}
|
|
1622
|
+
/** The customer document is inert while selecting content, but Storystick's
|
|
1623
|
+
* own controls live in the same browsing realm. The composed path is the one
|
|
1624
|
+
* stable boundary that includes the Shadow DOM host without coupling the
|
|
1625
|
+
* preview package to any editor internals. */
|
|
1626
|
+
function isPreviewControlsEvent(event) {
|
|
1627
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
1628
|
+
return path.some((node) => isElementNode(node) && node.hasAttribute(PREVIEW_CONTROLS_ATTR));
|
|
1629
|
+
}
|
|
1630
|
+
function getInteractiveTarget(event) {
|
|
1631
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
1632
|
+
for (const node of path) {
|
|
1633
|
+
if (!isElementNode(node))
|
|
1634
|
+
continue;
|
|
1635
|
+
const target = interactiveTargets.get(node);
|
|
1636
|
+
if (target !== undefined)
|
|
1637
|
+
return target;
|
|
1638
|
+
}
|
|
1639
|
+
let node = event.target;
|
|
1640
|
+
while (node !== null) {
|
|
1641
|
+
if (isElementNode(node)) {
|
|
1642
|
+
const target = interactiveTargets.get(node);
|
|
1643
|
+
if (target !== undefined)
|
|
1644
|
+
return target;
|
|
1645
|
+
}
|
|
1646
|
+
node = isNode(node) ? node.parentNode : null;
|
|
1647
|
+
}
|
|
1648
|
+
const syntheticTarget = syntheticOverlayState === null
|
|
1649
|
+
? null
|
|
1650
|
+
: findSyntheticOverlayTargetAtEventPoint(syntheticOverlayState, event);
|
|
1651
|
+
if (syntheticTarget !== null) {
|
|
1652
|
+
return interactiveTargets.get(syntheticTarget) ?? null;
|
|
1653
|
+
}
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
function isElementNode(node) {
|
|
1657
|
+
const win = activeWindow;
|
|
1658
|
+
if (win !== null) {
|
|
1659
|
+
return node instanceof win.Element;
|
|
1660
|
+
}
|
|
1661
|
+
return typeof Element !== "undefined" && node instanceof Element;
|
|
1662
|
+
}
|
|
1663
|
+
function isNode(node) {
|
|
1664
|
+
const win = activeWindow;
|
|
1665
|
+
if (win !== null) {
|
|
1666
|
+
return node instanceof win.Node;
|
|
1667
|
+
}
|
|
1668
|
+
return typeof Node !== "undefined" && node instanceof Node;
|
|
1669
|
+
}
|
|
1670
|
+
function suppressNativeEvent(event) {
|
|
1671
|
+
event.preventDefault();
|
|
1672
|
+
event.stopPropagation();
|
|
1673
|
+
event.stopImmediatePropagation();
|
|
1674
|
+
}
|
|
1675
|
+
function normalizeMouseButton(event) {
|
|
1676
|
+
const button = event.button;
|
|
1677
|
+
return button === 0 || button === 1 || button === 2 ? button : null;
|
|
1678
|
+
}
|
|
1679
|
+
function emitOverlayClick(target, button) {
|
|
1680
|
+
activeClickHandler?.({
|
|
1681
|
+
generation: target.generation,
|
|
1682
|
+
snapshotItemKey: target.snapshotItemKey,
|
|
1683
|
+
paintKey: target.paintKey,
|
|
1684
|
+
routeHash: target.routeHash,
|
|
1685
|
+
codeVersion: target.codeVersion,
|
|
1686
|
+
renderId: target.renderId,
|
|
1687
|
+
button,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
function emitIframeInteraction(button) {
|
|
1691
|
+
activeInteractionHandler?.({
|
|
1692
|
+
kind: "pointerdown",
|
|
1693
|
+
button,
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
function ensureStyle(doc) {
|
|
1697
|
+
const css = `
|
|
1698
|
+
[${ATTR}="field"] {
|
|
1699
|
+
cursor: pointer;
|
|
1700
|
+
user-select: none;
|
|
1701
|
+
}
|
|
1702
|
+
[${SYNTHETIC_ROOT_ATTR}] {
|
|
1703
|
+
pointer-events: none;
|
|
1704
|
+
}
|
|
1705
|
+
[${SYNTHETIC_BOX_ATTR}] {
|
|
1706
|
+
display: none;
|
|
1707
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
1708
|
+
}
|
|
1709
|
+
[${SYNTHETIC_ROOT_ATTR}][${DISPLAY_ATTR}="always"] [${SYNTHETIC_BOX_ATTR}],
|
|
1710
|
+
[${SYNTHETIC_BOX_ATTR}][${SYNTHETIC_BOX_STATE_ATTR}="hover"],
|
|
1711
|
+
[${SYNTHETIC_BOX_ATTR}][${SYNTHETIC_BOX_STATE_ATTR}="focus"],
|
|
1712
|
+
[${SYNTHETIC_BOX_ATTR}][${SYNTHETIC_BOX_STATE_ATTR}="reveal"] {
|
|
1713
|
+
display: block;
|
|
1714
|
+
}
|
|
1715
|
+
${buildSyntheticOverlayStateCss()}
|
|
1716
|
+
`;
|
|
1717
|
+
const existing = doc.getElementById(STYLE_ID);
|
|
1718
|
+
if (existing !== null) {
|
|
1719
|
+
if (existing.textContent !== css)
|
|
1720
|
+
existing.textContent = css;
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const style = doc.createElement("style");
|
|
1724
|
+
style.id = STYLE_ID;
|
|
1725
|
+
style.textContent = css;
|
|
1726
|
+
doc.head?.appendChild(style);
|
|
1727
|
+
}
|
|
1728
|
+
function buildSyntheticOverlayStateCss() {
|
|
1729
|
+
return Object.entries(SYNTHETIC_OVERLAY_STATE_TOKENS)
|
|
1730
|
+
.flatMap(([tone, states]) => Object.entries(states).map(([state, tokens]) => ` [${SYNTHETIC_BOX_ATTR}][${SYNTHETIC_BOX_TONE_ATTR}="${tone}"][${SYNTHETIC_BOX_STATE_ATTR}="${state}"] { border-color: ${tokens.color}; background-color: ${tokens.fill}; }`))
|
|
1731
|
+
.join("\n");
|
|
1732
|
+
}
|