@archie/devtools 0.0.36 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/inspector.standalone.js +829 -73
- package/package.json +1 -1
|
@@ -1527,80 +1527,172 @@
|
|
|
1527
1527
|
}
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1531
|
+
// THE HANDSHAKE PAYLOAD — ONE BUILDER, THREE EMISSIONS.
|
|
1532
|
+
//
|
|
1533
|
+
// `INSPECTOR_SCRIPT_LOADED` is emitted exactly ONCE per execution of
|
|
1534
|
+
// this script and is never replayed. A host whose `message` listener
|
|
1535
|
+
// is not attached yet when it fires loses it IRRECOVERABLY, and with
|
|
1536
|
+
// it every `capabilities` bit — including `bindingContract`, the one
|
|
1537
|
+
// that decides whether a text edit on a data row is writable or gets
|
|
1538
|
+
// the "Regenerate this prototype" refusal. That race is real and
|
|
1539
|
+
// routinely lost after a host page refresh: this page is served warm
|
|
1540
|
+
// from the CDN while the host still resolves the async probe that
|
|
1541
|
+
// decides whether to mount its bridge at all.
|
|
1542
|
+
//
|
|
1543
|
+
// The cure is to make the announcement RECOVERABLE, by two
|
|
1544
|
+
// independent routes that both reuse THIS builder (never a
|
|
1545
|
+
// second, drifting literal):
|
|
1546
|
+
// 1. PULL — the host asks (`QUERY_CAPABILITIES`) and gets
|
|
1547
|
+
// `CAPABILITIES_RESULT` back, correlated by `requestId`.
|
|
1548
|
+
// 2. PUSH — every `SET_EDITOR_MODE`/`SYNC_EDITOR_STATE` re-announces
|
|
1549
|
+
// `CAPABILITIES_RESULT` (unsolicited, `requestId: null`) BEFORE
|
|
1550
|
+
// its `EDITOR_MODE_CHANGED`, so a host that lost the handshake
|
|
1551
|
+
// is healed by the message it was already going to send, without
|
|
1552
|
+
// knowing the new message exists.
|
|
1553
|
+
// Both are idempotent: they report state, they never mutate it.
|
|
1554
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1555
|
+
function buildInspectorHandshakePayload() {
|
|
1556
|
+
return {
|
|
1557
|
+
// 2.2.0 (ARCH-1065): additive only — `bindingChain` on element data,
|
|
1558
|
+
// `QUERY_SLICE_FIELD` in the message protocol, and
|
|
1559
|
+
// `capabilities.bindingContract` below. No existing field changed
|
|
1560
|
+
// shape, so `protocolVersion` stays 1.
|
|
1561
|
+
// 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
|
|
1562
|
+
// in the message protocol and `capabilities.collectionOrderQuery`
|
|
1563
|
+
// below. No existing field changed shape, so `protocolVersion` stays 1.
|
|
1564
|
+
// 2.4.0 (ARCH-883-KOI, CREW round): additive only — `origin` on
|
|
1565
|
+
// `EditorMeta`, the usage-projected `ownMetaNodeId`/`inspectorSource`/
|
|
1566
|
+
// `editorInstance` resolution, and `capabilities.kitOriginIdentity`
|
|
1567
|
+
// below. No existing field changed shape, so `protocolVersion` stays 1.
|
|
1568
|
+
// 2.5.0: additive only — `CONFIGURE_INSPECTOR` inbound (labels +
|
|
1569
|
+
// permissions), and the `CHAT_CONTEXT_REQUESTED` /
|
|
1570
|
+
// `ADD_COMMENT_REQUESTED` outbound pair behind the right-click menu
|
|
1571
|
+
// (`capabilities.contextMenuActions` below). No existing field
|
|
1572
|
+
// changed shape, so `protocolVersion` stays 1.
|
|
1573
|
+
// 2.6.0: additive only — `QUERY_CAPABILITIES` inbound, the
|
|
1574
|
+
// `CAPABILITIES_RESULT` outbound it answers with (also pushed
|
|
1575
|
+
// unsolicited on every editor-mode command), and
|
|
1576
|
+
// `capabilities.capabilitiesQuery` below. No existing field
|
|
1577
|
+
// changed shape, so `protocolVersion` stays 1.
|
|
1578
|
+
version: "2.6.0",
|
|
1579
|
+
protocolVersion: 1,
|
|
1580
|
+
capabilities: {
|
|
1581
|
+
// 2.1.1: function-valued __editorMeta props are invoked before any
|
|
1582
|
+
// postMessage, so meta payloads stay structured-cloneable. Hosts can
|
|
1583
|
+
// gate on this bit to confirm the deployed CDN artifact carries the
|
|
1584
|
+
// fix (the 2.1.0 URL is overwritten in place on publish).
|
|
1585
|
+
functionValuedEditorMeta: true,
|
|
1586
|
+
inspect: true,
|
|
1587
|
+
dndMove: true,
|
|
1588
|
+
dndResize: true,
|
|
1589
|
+
fiberInspectorSource: true,
|
|
1590
|
+
hostControlledHistory: true,
|
|
1591
|
+
arrangeStructuredHistory: true,
|
|
1592
|
+
arrangeStrongNodeIdentity: true,
|
|
1593
|
+
// Unified mode: SET_EDITOR_MODE "inspect" also enables the arrange
|
|
1594
|
+
// (design) runtime — selection and drag/resize coexist. Hosts MUST
|
|
1595
|
+
// feature-detect on this flag: a frozen 2.0.x inspector disables
|
|
1596
|
+
// design mode on "inspect" and needs the legacy "design" mode.
|
|
1597
|
+
unifiedInspectArrange: true,
|
|
1598
|
+
// ARCH-1065: the M2 codemod's data-agent-* grammar is readable
|
|
1599
|
+
// (`bindingChain` on element data) and slice-field-queryable
|
|
1600
|
+
// (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
|
|
1601
|
+
// before relying on either — a frozen pre-1065 inspector has
|
|
1602
|
+
// neither.
|
|
1603
|
+
collectionBinding: true,
|
|
1604
|
+
// ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
|
|
1605
|
+
// the enclosing collection's order ("stable"/"derived") and row
|
|
1606
|
+
// ids/count for a target node, off the same data-agent-* grammar
|
|
1607
|
+
// as `collectionBinding`. Hosts MUST feature-detect on this flag —
|
|
1608
|
+
// a frozen pre-2.3.0 inspector has no handler for the message.
|
|
1609
|
+
collectionOrderQuery: true,
|
|
1610
|
+
// Host draw mode: the inspector can map a drawn rect to the
|
|
1611
|
+
// elements under it (`QUERY_ELEMENTS_IN_RECT` →
|
|
1612
|
+
// `ELEMENTS_IN_RECT_RESULT`) and rasterize its own viewport
|
|
1613
|
+
// (`CAPTURE_VIEWPORT` → `VIEWPORT_CAPTURED`). Hosts MUST
|
|
1614
|
+
// feature-detect: a frozen earlier inspector has no handler for
|
|
1615
|
+
// either message and would never reply.
|
|
1616
|
+
rectElementQuery: true,
|
|
1617
|
+
// Best-effort by construction — the foreignObject round-trip drops
|
|
1618
|
+
// web fonts, cross-origin images and <canvas> pixels, and answers
|
|
1619
|
+
// VIEWPORT_CAPTURE_FAILED when the rasterization throws.
|
|
1620
|
+
viewportCapture: true,
|
|
1621
|
+
// The BUILD-TIME contract version this specific page was stamped
|
|
1622
|
+
// with, forwarded verbatim from `<meta name="archie-binding-contract">`
|
|
1623
|
+
// when the kit build emitted one — null on any page built before
|
|
1624
|
+
// that meta tag existed, or that never carried collection-bound
|
|
1625
|
+
// data. Opaque to this script: validating/interpreting the value is
|
|
1626
|
+
// the host's job (mirrors how `slot` and `bindingContract` — plural
|
|
1627
|
+
// concept, singular mechanism — are treated as opaque strings
|
|
1628
|
+
// elsewhere in this file).
|
|
1629
|
+
bindingContract: readBindingContractMeta(),
|
|
1630
|
+
// ARCH-883-KOI F4 (v2.4.0) — this SCRIPT resolves `EditorMeta.origin`
|
|
1631
|
+
// and projects identity across an instrumented kit boundary
|
|
1632
|
+
// (`ownMetaNodeId`, `projectUsageMetaFromElement`,
|
|
1633
|
+
// `getInspectorInfoFromElement`, `resolveInstanceIdentity`). Hosts
|
|
1634
|
+
// MUST feature-detect on this flag before trusting `origin`
|
|
1635
|
+
// (absent or `editorMeta.origin === 'kit'`) as meaningful: a frozen
|
|
1636
|
+
// pre-2.4.0 inspector never reads the field at all, so its
|
|
1637
|
+
// `inspectorSource`/`editorInstance` on an instrumented-kit page
|
|
1638
|
+
// reproduce the exact F1 bug this version fixes.
|
|
1639
|
+
//
|
|
1640
|
+
// What this bit CANNOT tell the host: whether the PAGE's own build
|
|
1641
|
+
// emitted `origin` on its metas in the first place — that is the
|
|
1642
|
+
// `@archie/devtools` version the acg/kitgen esbuild path had
|
|
1643
|
+
// installed when the screen (or the kit) was built, a completely
|
|
1644
|
+
// separate axis from this CDN script's version (see the `origin`
|
|
1645
|
+
// field's docstring in `types/editor-meta.ts` for the full
|
|
1646
|
+
// reasoning, and why NO per-node signal can close that gap).
|
|
1647
|
+
kitOriginIdentity: true,
|
|
1648
|
+
// v2.5.0 — right-click menu in navigate AND inspect mode, emitting
|
|
1649
|
+
// `CHAT_CONTEXT_REQUESTED` (with the node's full metadata) and
|
|
1650
|
+
// `ADD_COMMENT_REQUESTED` (with the anchor target). Labels and the
|
|
1651
|
+
// chat-entry permission gate arrive via `CONFIGURE_INSPECTOR`.
|
|
1652
|
+
// Hosts MUST feature-detect: a frozen pre-2.5.0 inspector ignores
|
|
1653
|
+
// `CONFIGURE_INSPECTOR` and never emits either message.
|
|
1654
|
+
contextMenuActions: true,
|
|
1655
|
+
// v2.6.0 — the handshake is RECOVERABLE: `QUERY_CAPABILITIES`
|
|
1656
|
+
// answers with `CAPABILITIES_RESULT` carrying this exact payload,
|
|
1657
|
+
// and every `SET_EDITOR_MODE`/`SYNC_EDITOR_STATE` re-announces it
|
|
1658
|
+
// unsolicited. Hosts MUST feature-detect before WAITING on a
|
|
1659
|
+
// reply — a frozen pre-2.6.0 inspector has no handler for
|
|
1660
|
+
// `QUERY_CAPABILITIES` and never answers — but a host that lost
|
|
1661
|
+
// the one-shot handshake has no capabilities to detect ON, so
|
|
1662
|
+
// asking blindly (and treating silence as "still unknown", never
|
|
1663
|
+
// as "reported empty") is the correct posture.
|
|
1664
|
+
capabilitiesQuery: true,
|
|
1665
|
+
},
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Emit the capabilities payload as `CAPABILITIES_RESULT`.
|
|
1671
|
+
*
|
|
1672
|
+
* `requestId` echoes the pull's correlation id, or is `null` for the
|
|
1673
|
+
* unsolicited push. `reason` names which route produced it, purely
|
|
1674
|
+
* for host-side tracing.
|
|
1675
|
+
*/
|
|
1676
|
+
function announceInspectorCapabilities(requestId, reason) {
|
|
1677
|
+
var handshake = buildInspectorHandshakePayload()
|
|
1678
|
+
postMessageToParent({
|
|
1679
|
+
type: "CAPABILITIES_RESULT",
|
|
1680
|
+
requestId:
|
|
1681
|
+
typeof requestId === "string" && requestId !== "" ? requestId : null,
|
|
1682
|
+
reason: typeof reason === "string" && reason !== "" ? reason : null,
|
|
1683
|
+
version: handshake.version,
|
|
1684
|
+
protocolVersion: handshake.protocolVersion,
|
|
1685
|
+
capabilities: handshake.capabilities,
|
|
1686
|
+
})
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
var initialHandshake = buildInspectorHandshakePayload()
|
|
1530
1690
|
postMessageToParent({
|
|
1531
1691
|
type: "INSPECTOR_SCRIPT_LOADED",
|
|
1532
1692
|
timestamp: Date.now(),
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
// shape, so `protocolVersion` stays 1.
|
|
1537
|
-
// 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
|
|
1538
|
-
// in the message protocol and `capabilities.collectionOrderQuery`
|
|
1539
|
-
// below. No existing field changed shape, so `protocolVersion` stays 1.
|
|
1540
|
-
// 2.4.0 (ARCH-883-KOI, CREW round): additive only — `origin` on
|
|
1541
|
-
// `EditorMeta`, the usage-projected `ownMetaNodeId`/`inspectorSource`/
|
|
1542
|
-
// `editorInstance` resolution, and `capabilities.kitOriginIdentity`
|
|
1543
|
-
// below. No existing field changed shape, so `protocolVersion` stays 1.
|
|
1544
|
-
version: "2.4.0",
|
|
1545
|
-
protocolVersion: 1,
|
|
1546
|
-
capabilities: {
|
|
1547
|
-
// 2.1.1: function-valued __editorMeta props are invoked before any
|
|
1548
|
-
// postMessage, so meta payloads stay structured-cloneable. Hosts can
|
|
1549
|
-
// gate on this bit to confirm the deployed CDN artifact carries the
|
|
1550
|
-
// fix (the 2.1.0 URL is overwritten in place on publish).
|
|
1551
|
-
functionValuedEditorMeta: true,
|
|
1552
|
-
inspect: true,
|
|
1553
|
-
dndMove: true,
|
|
1554
|
-
dndResize: true,
|
|
1555
|
-
fiberInspectorSource: true,
|
|
1556
|
-
hostControlledHistory: true,
|
|
1557
|
-
arrangeStructuredHistory: true,
|
|
1558
|
-
arrangeStrongNodeIdentity: true,
|
|
1559
|
-
// Unified mode: SET_EDITOR_MODE "inspect" also enables the arrange
|
|
1560
|
-
// (design) runtime — selection and drag/resize coexist. Hosts MUST
|
|
1561
|
-
// feature-detect on this flag: a frozen 2.0.x inspector disables
|
|
1562
|
-
// design mode on "inspect" and needs the legacy "design" mode.
|
|
1563
|
-
unifiedInspectArrange: true,
|
|
1564
|
-
// ARCH-1065: the M2 codemod's data-agent-* grammar is readable
|
|
1565
|
-
// (`bindingChain` on element data) and slice-field-queryable
|
|
1566
|
-
// (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
|
|
1567
|
-
// before relying on either — a frozen pre-1065 inspector has
|
|
1568
|
-
// neither.
|
|
1569
|
-
collectionBinding: true,
|
|
1570
|
-
// ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
|
|
1571
|
-
// the enclosing collection's order ("stable"/"derived") and row
|
|
1572
|
-
// ids/count for a target node, off the same data-agent-* grammar
|
|
1573
|
-
// as `collectionBinding`. Hosts MUST feature-detect on this flag —
|
|
1574
|
-
// a frozen pre-2.3.0 inspector has no handler for the message.
|
|
1575
|
-
collectionOrderQuery: true,
|
|
1576
|
-
// The BUILD-TIME contract version this specific page was stamped
|
|
1577
|
-
// with, forwarded verbatim from `<meta name="archie-binding-contract">`
|
|
1578
|
-
// when the kit build emitted one — null on any page built before
|
|
1579
|
-
// that meta tag existed, or that never carried collection-bound
|
|
1580
|
-
// data. Opaque to this script: validating/interpreting the value is
|
|
1581
|
-
// the host's job (mirrors how `slot` and `bindingContract` — plural
|
|
1582
|
-
// concept, singular mechanism — are treated as opaque strings
|
|
1583
|
-
// elsewhere in this file).
|
|
1584
|
-
bindingContract: readBindingContractMeta(),
|
|
1585
|
-
// ARCH-883-KOI F4 (v2.4.0) — this SCRIPT resolves `EditorMeta.origin`
|
|
1586
|
-
// and projects identity across an instrumented kit boundary
|
|
1587
|
-
// (`ownMetaNodeId`, `projectUsageMetaFromElement`,
|
|
1588
|
-
// `getInspectorInfoFromElement`, `resolveInstanceIdentity`). Hosts
|
|
1589
|
-
// MUST feature-detect on this flag before trusting `origin`
|
|
1590
|
-
// (absent or `editorMeta.origin === 'kit'`) as meaningful: a frozen
|
|
1591
|
-
// pre-2.4.0 inspector never reads the field at all, so its
|
|
1592
|
-
// `inspectorSource`/`editorInstance` on an instrumented-kit page
|
|
1593
|
-
// reproduce the exact F1 bug this version fixes.
|
|
1594
|
-
//
|
|
1595
|
-
// What this bit CANNOT tell the host: whether the PAGE's own build
|
|
1596
|
-
// emitted `origin` on its metas in the first place — that is the
|
|
1597
|
-
// `@archie/devtools` version the acg/kitgen esbuild path had
|
|
1598
|
-
// installed when the screen (or the kit) was built, a completely
|
|
1599
|
-
// separate axis from this CDN script's version (see the `origin`
|
|
1600
|
-
// field's docstring in `types/editor-meta.ts` for the full
|
|
1601
|
-
// reasoning, and why NO per-node signal can close that gap).
|
|
1602
|
-
kitOriginIdentity: true,
|
|
1603
|
-
},
|
|
1693
|
+
version: initialHandshake.version,
|
|
1694
|
+
protocolVersion: initialHandshake.protocolVersion,
|
|
1695
|
+
capabilities: initialHandshake.capabilities,
|
|
1604
1696
|
})
|
|
1605
1697
|
|
|
1606
1698
|
if (window._inspectorMessageHandler) {
|
|
@@ -1690,6 +1782,10 @@
|
|
|
1690
1782
|
element === selectedElementBadge ||
|
|
1691
1783
|
element.id === "inspector-ping-effect" ||
|
|
1692
1784
|
(element.closest && element.closest("[data-inspector-badge]")) ||
|
|
1785
|
+
// Right-click menu: its buttons must survive setupEventBlockers'
|
|
1786
|
+
// "click" blocker and never be treated as an inspect target.
|
|
1787
|
+
(element.closest &&
|
|
1788
|
+
element.closest("[data-inspector-context-menu]")) ||
|
|
1693
1789
|
// Design-mode (arrange) overlay elements: HitArea, resize handles,
|
|
1694
1790
|
// drop-line, etc. These belong to the unified visual editor and must
|
|
1695
1791
|
// pass mousedown/click events through to React (dnd-kit) instead of
|
|
@@ -2224,6 +2320,15 @@
|
|
|
2224
2320
|
}
|
|
2225
2321
|
|
|
2226
2322
|
function setEditorModeFromHost(mode, options) {
|
|
2323
|
+
// PUSH route of the recoverable handshake (see
|
|
2324
|
+
// `buildInspectorHandshakePayload`). Emitted BEFORE the
|
|
2325
|
+
// `EDITOR_MODE_CHANGED` echo below — deliberately, not incidentally:
|
|
2326
|
+
// a host that lost the one-shot `INSPECTOR_SCRIPT_LOADED` refuses
|
|
2327
|
+
// every inbound message until it knows who it is talking to, so the
|
|
2328
|
+
// capabilities have to land FIRST or the very `EDITOR_MODE_CHANGED`
|
|
2329
|
+
// this command exists to confirm would be the message dropped.
|
|
2330
|
+
announceInspectorCapabilities(null, "editor-mode")
|
|
2331
|
+
|
|
2227
2332
|
var historyMode =
|
|
2228
2333
|
options && options.hostControlledHistory ? "host" : "local"
|
|
2229
2334
|
|
|
@@ -2758,6 +2863,310 @@
|
|
|
2758
2863
|
return element
|
|
2759
2864
|
}
|
|
2760
2865
|
|
|
2866
|
+
// ── Annotation support (host draw mode) ──────────────────────────────
|
|
2867
|
+
// Two OPTIONAL queries the host uses to turn a drawing over the preview
|
|
2868
|
+
// into actionable context. Both are advertised as capabilities
|
|
2869
|
+
// (`rectElementQuery`, `viewportCapture`); a host that sees neither
|
|
2870
|
+
// degrades to sending the raw drawing, so failures here must never
|
|
2871
|
+
// throw into the shared message handler.
|
|
2872
|
+
|
|
2873
|
+
var ANNOTATION_MAX_ELEMENTS = 8
|
|
2874
|
+
var ANNOTATION_CAPTURE_TIMEOUT_MS = 5000
|
|
2875
|
+
var ANNOTATION_MIN_SIDE_PX = 4
|
|
2876
|
+
var ANNOTATION_SKIP_TAGS = {
|
|
2877
|
+
HTML: true,
|
|
2878
|
+
HEAD: true,
|
|
2879
|
+
SCRIPT: true,
|
|
2880
|
+
STYLE: true,
|
|
2881
|
+
META: true,
|
|
2882
|
+
LINK: true,
|
|
2883
|
+
TITLE: true,
|
|
2884
|
+
BR: true,
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function annotationIntersection(a, b) {
|
|
2888
|
+
var x = Math.max(a.left, b.x)
|
|
2889
|
+
var y = Math.max(a.top, b.y)
|
|
2890
|
+
var right = Math.min(a.right, b.x + b.width)
|
|
2891
|
+
var bottom = Math.min(a.bottom, b.y + b.height)
|
|
2892
|
+
if (right <= x || bottom <= y) return 0
|
|
2893
|
+
return (right - x) * (bottom - y)
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* Elements intersecting a viewport-relative rect, most specific first.
|
|
2898
|
+
*
|
|
2899
|
+
* "Most specific" is deliberately NOT smallest-area: a mark drawn around
|
|
2900
|
+
* a card should report the card, not the innermost text node inside it.
|
|
2901
|
+
* Ranking therefore puts nodes carrying an inspector source (i.e. ones
|
|
2902
|
+
* the agent can actually edit) ahead of anonymous wrappers, then favours
|
|
2903
|
+
* higher coverage of the drawn rect.
|
|
2904
|
+
*/
|
|
2905
|
+
function collectElementsInRect(rect) {
|
|
2906
|
+
if (
|
|
2907
|
+
!rect ||
|
|
2908
|
+
typeof rect.x !== "number" ||
|
|
2909
|
+
typeof rect.y !== "number" ||
|
|
2910
|
+
!(rect.width > 0) ||
|
|
2911
|
+
!(rect.height > 0)
|
|
2912
|
+
) {
|
|
2913
|
+
return []
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
var all = _rawDocumentQuerySelectorAll.call(document, "body *")
|
|
2917
|
+
var scored = []
|
|
2918
|
+
|
|
2919
|
+
for (var i = 0; i < all.length; i++) {
|
|
2920
|
+
var el = all[i]
|
|
2921
|
+
if (ANNOTATION_SKIP_TAGS[el.tagName]) continue
|
|
2922
|
+
if (el.closest && el.closest("[data-inspector-badge]")) continue
|
|
2923
|
+
|
|
2924
|
+
var box = el.getBoundingClientRect()
|
|
2925
|
+
if (
|
|
2926
|
+
box.width < ANNOTATION_MIN_SIDE_PX ||
|
|
2927
|
+
box.height < ANNOTATION_MIN_SIDE_PX
|
|
2928
|
+
) {
|
|
2929
|
+
continue
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
var overlap = annotationIntersection(box, rect)
|
|
2933
|
+
if (overlap <= 0) continue
|
|
2934
|
+
|
|
2935
|
+
var style = window.getComputedStyle(el)
|
|
2936
|
+
if (
|
|
2937
|
+
style.visibility === "hidden" ||
|
|
2938
|
+
style.display === "none" ||
|
|
2939
|
+
style.opacity === "0"
|
|
2940
|
+
) {
|
|
2941
|
+
continue
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
var elementArea = box.width * box.height
|
|
2945
|
+
var rectArea = rect.width * rect.height
|
|
2946
|
+
// Skip nodes the mark barely grazes, and page-sized wrappers the
|
|
2947
|
+
// mark covers a negligible fraction of.
|
|
2948
|
+
if (overlap / elementArea < 0.25 && overlap / rectArea < 0.5) continue
|
|
2949
|
+
|
|
2950
|
+
var info = null
|
|
2951
|
+
try {
|
|
2952
|
+
info = getInspectorInfoFromElement(el)
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
info = null
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
scored.push({
|
|
2958
|
+
element: el,
|
|
2959
|
+
info: info,
|
|
2960
|
+
hasSource: info && info.file ? 1 : 0,
|
|
2961
|
+
coverage: overlap / rectArea,
|
|
2962
|
+
})
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
scored.sort(function (a, b) {
|
|
2966
|
+
if (a.hasSource !== b.hasSource) return b.hasSource - a.hasSource
|
|
2967
|
+
return b.coverage - a.coverage
|
|
2968
|
+
})
|
|
2969
|
+
|
|
2970
|
+
var result = []
|
|
2971
|
+
var seenSelectors = {}
|
|
2972
|
+
for (var j = 0; j < scored.length; j++) {
|
|
2973
|
+
if (result.length >= ANNOTATION_MAX_ELEMENTS) break
|
|
2974
|
+
var entry = scored[j]
|
|
2975
|
+
var selector = getStableSelector(entry.element)
|
|
2976
|
+
if (!selector || seenSelectors[selector]) continue
|
|
2977
|
+
seenSelectors[selector] = true
|
|
2978
|
+
|
|
2979
|
+
var box2 = entry.element.getBoundingClientRect()
|
|
2980
|
+
var text = entry.element.textContent
|
|
2981
|
+
result.push({
|
|
2982
|
+
cssSelector: selector,
|
|
2983
|
+
tagName: String(entry.element.tagName || "").toLowerCase(),
|
|
2984
|
+
textPreview: text ? text.trim().slice(0, 120) : "",
|
|
2985
|
+
sourceRef:
|
|
2986
|
+
entry.info && entry.info.file
|
|
2987
|
+
? entry.info.file +
|
|
2988
|
+
":" +
|
|
2989
|
+
(entry.info.line || 0) +
|
|
2990
|
+
":" +
|
|
2991
|
+
(entry.info.column || 0)
|
|
2992
|
+
: undefined,
|
|
2993
|
+
componentName:
|
|
2994
|
+
entry.info && entry.info.componentName
|
|
2995
|
+
? entry.info.componentName
|
|
2996
|
+
: undefined,
|
|
2997
|
+
boundingRect: {
|
|
2998
|
+
x: box2.x,
|
|
2999
|
+
y: box2.y,
|
|
3000
|
+
width: box2.width,
|
|
3001
|
+
height: box2.height,
|
|
3002
|
+
},
|
|
3003
|
+
})
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
return result
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/** Same-origin CSS text, concatenated. Cross-origin sheets throw on
|
|
3010
|
+
* `cssRules` and are skipped — their styling is simply absent from the
|
|
3011
|
+
* capture, which is why the host treats it as best-effort. */
|
|
3012
|
+
function collectSameOriginCss() {
|
|
3013
|
+
var chunks = []
|
|
3014
|
+
for (var i = 0; i < document.styleSheets.length; i++) {
|
|
3015
|
+
var sheet = document.styleSheets[i]
|
|
3016
|
+
var rules = null
|
|
3017
|
+
try {
|
|
3018
|
+
rules = sheet.cssRules
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
continue
|
|
3021
|
+
}
|
|
3022
|
+
if (!rules) continue
|
|
3023
|
+
for (var j = 0; j < rules.length; j++) {
|
|
3024
|
+
chunks.push(rules[j].cssText)
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
return chunks.join("\n")
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function fetchAsDataUrl(url) {
|
|
3031
|
+
return fetch(url, { mode: "same-origin" })
|
|
3032
|
+
.then(function (response) {
|
|
3033
|
+
if (!response.ok) throw new Error("bad status")
|
|
3034
|
+
return response.blob()
|
|
3035
|
+
})
|
|
3036
|
+
.then(function (blob) {
|
|
3037
|
+
return new Promise(function (resolve, reject) {
|
|
3038
|
+
var reader = new FileReader()
|
|
3039
|
+
reader.onload = function () {
|
|
3040
|
+
resolve(String(reader.result))
|
|
3041
|
+
}
|
|
3042
|
+
reader.onerror = reject
|
|
3043
|
+
reader.readAsDataURL(blob)
|
|
3044
|
+
})
|
|
3045
|
+
})
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/** Rasterizing an SVG cannot fetch subresources, so same-origin <img>
|
|
3049
|
+
* sources are inlined first. Failures leave the image blank rather
|
|
3050
|
+
* than aborting the whole capture. */
|
|
3051
|
+
function inlineImages(root) {
|
|
3052
|
+
var images = root.querySelectorAll("img")
|
|
3053
|
+
var jobs = []
|
|
3054
|
+
for (var i = 0; i < images.length; i++) {
|
|
3055
|
+
;(function (img) {
|
|
3056
|
+
var src = img.getAttribute("src")
|
|
3057
|
+
if (!src || src.indexOf("data:") === 0) return
|
|
3058
|
+
var absolute
|
|
3059
|
+
try {
|
|
3060
|
+
absolute = new URL(src, document.baseURI)
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
return
|
|
3063
|
+
}
|
|
3064
|
+
if (absolute.origin !== window.location.origin) return
|
|
3065
|
+
jobs.push(
|
|
3066
|
+
fetchAsDataUrl(absolute.href)
|
|
3067
|
+
.then(function (dataUrl) {
|
|
3068
|
+
img.setAttribute("src", dataUrl)
|
|
3069
|
+
})
|
|
3070
|
+
.catch(function () {
|
|
3071
|
+
img.removeAttribute("src")
|
|
3072
|
+
})
|
|
3073
|
+
)
|
|
3074
|
+
})(images[i])
|
|
3075
|
+
}
|
|
3076
|
+
return Promise.all(jobs)
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* Rasterizes the CURRENT viewport into a PNG data URL via an
|
|
3081
|
+
* `<svg><foreignObject>` round-trip. Best-effort by construction: web
|
|
3082
|
+
* fonts, cross-origin images and <canvas> pixels do not survive it. The
|
|
3083
|
+
* host composes the result with the user's drawing, and falls back to
|
|
3084
|
+
* the drawing alone when this rejects.
|
|
3085
|
+
*/
|
|
3086
|
+
function captureViewportDataUrl(pixelRatio) {
|
|
3087
|
+
var width = window.innerWidth
|
|
3088
|
+
var height = window.innerHeight
|
|
3089
|
+
var ratio = Math.min(Math.max(Number(pixelRatio) || 1, 1), 2)
|
|
3090
|
+
|
|
3091
|
+
var clone = document.documentElement.cloneNode(true)
|
|
3092
|
+
var strip = clone.querySelectorAll(
|
|
3093
|
+
"script, noscript, [data-inspector-badge], #archie-inspector-styles"
|
|
3094
|
+
)
|
|
3095
|
+
for (var i = 0; i < strip.length; i++) {
|
|
3096
|
+
if (strip[i].parentNode) strip[i].parentNode.removeChild(strip[i])
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
var styleEl = document.createElement("style")
|
|
3100
|
+
styleEl.textContent = collectSameOriginCss()
|
|
3101
|
+
var head = clone.querySelector("head")
|
|
3102
|
+
if (head) head.appendChild(styleEl)
|
|
3103
|
+
else clone.insertBefore(styleEl, clone.firstChild)
|
|
3104
|
+
|
|
3105
|
+
return inlineImages(clone).then(function () {
|
|
3106
|
+
var markup = new XMLSerializer().serializeToString(clone)
|
|
3107
|
+
var svg =
|
|
3108
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
|
3109
|
+
width +
|
|
3110
|
+
'" height="' +
|
|
3111
|
+
height +
|
|
3112
|
+
'">' +
|
|
3113
|
+
'<foreignObject x="0" y="0" width="' +
|
|
3114
|
+
width +
|
|
3115
|
+
'" height="' +
|
|
3116
|
+
height +
|
|
3117
|
+
'">' +
|
|
3118
|
+
'<div xmlns="http://www.w3.org/1999/xhtml" style="width:' +
|
|
3119
|
+
width +
|
|
3120
|
+
"px;transform:translate(" +
|
|
3121
|
+
-window.scrollX +
|
|
3122
|
+
"px," +
|
|
3123
|
+
-window.scrollY +
|
|
3124
|
+
'px)">' +
|
|
3125
|
+
markup +
|
|
3126
|
+
"</div></foreignObject></svg>"
|
|
3127
|
+
|
|
3128
|
+
return new Promise(function (resolve, reject) {
|
|
3129
|
+
var image = new Image()
|
|
3130
|
+
// An SVG the browser refuses to decode can fire NEITHER onload nor
|
|
3131
|
+
// onerror (malformed serialization, blocked subresource). Without
|
|
3132
|
+
// this guard the host would sit on the request until its own
|
|
3133
|
+
// timeout with no diagnostic; the deadline turns that into an
|
|
3134
|
+
// explicit VIEWPORT_CAPTURE_FAILED.
|
|
3135
|
+
var settled = false
|
|
3136
|
+
var deadline = setTimeout(function () {
|
|
3137
|
+
if (settled) return
|
|
3138
|
+
settled = true
|
|
3139
|
+
reject(new Error("svg rasterization timed out"))
|
|
3140
|
+
}, ANNOTATION_CAPTURE_TIMEOUT_MS)
|
|
3141
|
+
var finish = function (fn, value) {
|
|
3142
|
+
if (settled) return
|
|
3143
|
+
settled = true
|
|
3144
|
+
clearTimeout(deadline)
|
|
3145
|
+
fn(value)
|
|
3146
|
+
}
|
|
3147
|
+
image.onload = function () {
|
|
3148
|
+
try {
|
|
3149
|
+
var canvas = document.createElement("canvas")
|
|
3150
|
+
canvas.width = Math.round(width * ratio)
|
|
3151
|
+
canvas.height = Math.round(height * ratio)
|
|
3152
|
+
var context = canvas.getContext("2d")
|
|
3153
|
+
context.fillStyle = "#ffffff"
|
|
3154
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
3155
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
|
3156
|
+
finish(resolve, canvas.toDataURL("image/png"))
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
finish(reject, error)
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
image.onerror = function () {
|
|
3162
|
+
finish(reject, new Error("svg rasterization failed"))
|
|
3163
|
+
}
|
|
3164
|
+
image.src =
|
|
3165
|
+
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
|
|
3166
|
+
})
|
|
3167
|
+
})
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2761
3170
|
function extractMetadata(element) {
|
|
2762
3171
|
if (!element) return null
|
|
2763
3172
|
|
|
@@ -3416,7 +3825,20 @@
|
|
|
3416
3825
|
}
|
|
3417
3826
|
}
|
|
3418
3827
|
|
|
3419
|
-
if (event.data && event.data.type === "
|
|
3828
|
+
if (event.data && event.data.type === "CONFIGURE_INSPECTOR") {
|
|
3829
|
+
// Origin adoption already happened above; this carries the
|
|
3830
|
+
// right-click menu's i18n labels and permission gate.
|
|
3831
|
+
applyInspectorConfiguration(event.data)
|
|
3832
|
+
} else if (event.data && event.data.type === "QUERY_CAPABILITIES") {
|
|
3833
|
+
// PULL route of the recoverable handshake (v2.6.0). Idempotent and
|
|
3834
|
+
// side-effect free: it reports the SAME payload
|
|
3835
|
+
// `INSPECTOR_SCRIPT_LOADED` carried, rebuilt from the one builder,
|
|
3836
|
+
// so a host that lost the one-shot announcement can recover it on
|
|
3837
|
+
// demand instead of living forever with "capabilities unknown".
|
|
3838
|
+
// Mirrors QUERY_SLICE_FIELD / QUERY_COLLECTION_ORDER's standalone
|
|
3839
|
+
// request/response envelope (`requestId` echoed on the reply).
|
|
3840
|
+
announceInspectorCapabilities(event.data.requestId, "query")
|
|
3841
|
+
} else if (event.data && event.data.type === "SET_EDITOR_MODE") {
|
|
3420
3842
|
setEditorModeFromHost(event.data.mode || "off", {
|
|
3421
3843
|
hostControlledHistory: event.data.hostControlledHistory !== false,
|
|
3422
3844
|
theme: event.data.theme || null,
|
|
@@ -3589,6 +4011,59 @@
|
|
|
3589
4011
|
},
|
|
3590
4012
|
})
|
|
3591
4013
|
}
|
|
4014
|
+
} else if (
|
|
4015
|
+
event.data &&
|
|
4016
|
+
event.data.type === "QUERY_ELEMENTS_IN_RECT"
|
|
4017
|
+
) {
|
|
4018
|
+
var rectRequestId = event.data.requestId
|
|
4019
|
+
var queryElements = []
|
|
4020
|
+
try {
|
|
4021
|
+
queryElements = collectElementsInRect(event.data.rect)
|
|
4022
|
+
var rectLimit = Number(event.data.limit)
|
|
4023
|
+
if (rectLimit > 0 && queryElements.length > rectLimit) {
|
|
4024
|
+
queryElements = queryElements.slice(0, rectLimit)
|
|
4025
|
+
}
|
|
4026
|
+
} catch (error) {
|
|
4027
|
+
inspectorWarn("[Inspector] QUERY_ELEMENTS_IN_RECT failed", error)
|
|
4028
|
+
queryElements = []
|
|
4029
|
+
}
|
|
4030
|
+
// Always answers — an empty list is a valid result, and the host's
|
|
4031
|
+
// pending request would otherwise sit until its timeout.
|
|
4032
|
+
postMessageToParent({
|
|
4033
|
+
type: "ELEMENTS_IN_RECT_RESULT",
|
|
4034
|
+
requestId: rectRequestId,
|
|
4035
|
+
elements: queryElements,
|
|
4036
|
+
})
|
|
4037
|
+
} else if (event.data && event.data.type === "CAPTURE_VIEWPORT") {
|
|
4038
|
+
var captureRequestId = event.data.requestId
|
|
4039
|
+
try {
|
|
4040
|
+
captureViewportDataUrl(event.data.pixelRatio)
|
|
4041
|
+
.then(function (dataUrl) {
|
|
4042
|
+
postMessageToParent({
|
|
4043
|
+
type: "VIEWPORT_CAPTURED",
|
|
4044
|
+
requestId: captureRequestId,
|
|
4045
|
+
dataUrl: dataUrl,
|
|
4046
|
+
width: window.innerWidth,
|
|
4047
|
+
height: window.innerHeight,
|
|
4048
|
+
scrollX: window.scrollX,
|
|
4049
|
+
scrollY: window.scrollY,
|
|
4050
|
+
})
|
|
4051
|
+
})
|
|
4052
|
+
.catch(function (error) {
|
|
4053
|
+
inspectorWarn("[Inspector] CAPTURE_VIEWPORT failed", error)
|
|
4054
|
+
postMessageToParent({
|
|
4055
|
+
type: "VIEWPORT_CAPTURE_FAILED",
|
|
4056
|
+
requestId: captureRequestId,
|
|
4057
|
+
error: String((error && error.message) || error),
|
|
4058
|
+
})
|
|
4059
|
+
})
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
postMessageToParent({
|
|
4062
|
+
type: "VIEWPORT_CAPTURE_FAILED",
|
|
4063
|
+
requestId: captureRequestId,
|
|
4064
|
+
error: String((error && error.message) || error),
|
|
4065
|
+
})
|
|
4066
|
+
}
|
|
3592
4067
|
} else if (event.data && event.data.type === "APPLY_STYLE_CHANGE") {
|
|
3593
4068
|
const { selector, property, value } = event.data
|
|
3594
4069
|
|
|
@@ -4503,8 +4978,289 @@
|
|
|
4503
4978
|
}
|
|
4504
4979
|
}
|
|
4505
4980
|
|
|
4981
|
+
// ── Right-click context menu ────────────────────────────────────────
|
|
4982
|
+
// Two entries — pin the node as chat context, or open a comment
|
|
4983
|
+
// anchored to it. Active in plain navigate mode as well as inspect, so
|
|
4984
|
+
// the listeners are registered here at load time: setupEventBlockers()
|
|
4985
|
+
// blocks "contextmenu" with stopImmediatePropagation once inspection
|
|
4986
|
+
// starts, and only a listener registered earlier in the capture phase
|
|
4987
|
+
// still runs.
|
|
4988
|
+
|
|
4989
|
+
var inspectorLabels = null
|
|
4990
|
+
var inspectorPermissions = { sendToChat: true }
|
|
4991
|
+
var contextMenuEl = null
|
|
4992
|
+
var contextMenuChatButton = null
|
|
4993
|
+
var contextMenuCommentButton = null
|
|
4994
|
+
var contextMenuTarget = null
|
|
4995
|
+
// Snapshotted when the menu opens: the comment anchor needs the
|
|
4996
|
+
// right-click POINT, which is gone by the time the entry is activated.
|
|
4997
|
+
var contextMenuTargetPayload = null
|
|
4998
|
+
|
|
4999
|
+
function getSendToChatLabel() {
|
|
5000
|
+
var configured =
|
|
5001
|
+
inspectorLabels &&
|
|
5002
|
+
inspectorLabels.badge &&
|
|
5003
|
+
typeof inspectorLabels.badge.sendToChat === "string"
|
|
5004
|
+
? inspectorLabels.badge.sendToChat.trim()
|
|
5005
|
+
: ""
|
|
5006
|
+
return configured || "Send to chat"
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
function getAddCommentLabel() {
|
|
5010
|
+
var configured =
|
|
5011
|
+
inspectorLabels &&
|
|
5012
|
+
inspectorLabels.contextMenu &&
|
|
5013
|
+
typeof inspectorLabels.contextMenu.addComment === "string"
|
|
5014
|
+
? inspectorLabels.contextMenu.addComment.trim()
|
|
5015
|
+
: ""
|
|
5016
|
+
return configured || "Add a comment"
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
// CONFIGURE_INSPECTOR payload — i18n labels plus the host's permission
|
|
5020
|
+
// gate for the chat entry (a viewer who can't send chat messages gets
|
|
5021
|
+
// the comment entry only).
|
|
5022
|
+
function applyInspectorConfiguration(data) {
|
|
5023
|
+
if (data && data.labels && typeof data.labels === "object") {
|
|
5024
|
+
inspectorLabels = data.labels
|
|
5025
|
+
}
|
|
5026
|
+
if (data && data.permissions && typeof data.permissions === "object") {
|
|
5027
|
+
inspectorPermissions = {
|
|
5028
|
+
sendToChat: data.permissions.sendToChat !== false,
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
syncContextMenuLabels()
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
function syncContextMenuLabels() {
|
|
5035
|
+
if (contextMenuChatButton) {
|
|
5036
|
+
contextMenuChatButton.textContent = getSendToChatLabel()
|
|
5037
|
+
contextMenuChatButton.style.display = inspectorPermissions.sendToChat
|
|
5038
|
+
? "block"
|
|
5039
|
+
: "none"
|
|
5040
|
+
}
|
|
5041
|
+
if (contextMenuCommentButton) {
|
|
5042
|
+
contextMenuCommentButton.textContent = getAddCommentLabel()
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
function hideSelectionContextMenu() {
|
|
5047
|
+
if (contextMenuEl) contextMenuEl.style.display = "none"
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5050
|
+
function ensureSelectionContextMenu() {
|
|
5051
|
+
if (contextMenuEl && document.contains(contextMenuEl)) {
|
|
5052
|
+
return contextMenuEl
|
|
5053
|
+
}
|
|
5054
|
+
// A previous run's node (or one orphaned by a body wipe) would
|
|
5055
|
+
// otherwise linger with dead click handlers.
|
|
5056
|
+
var orphan = document.getElementById("__archie-inspector-context-menu__")
|
|
5057
|
+
if (orphan && orphan.parentNode) orphan.parentNode.removeChild(orphan)
|
|
5058
|
+
|
|
5059
|
+
var menu = document.createElement("div")
|
|
5060
|
+
menu.id = "__archie-inspector-context-menu__"
|
|
5061
|
+
menu.setAttribute("data-inspector-context-menu", "true")
|
|
5062
|
+
menu.style.cssText =
|
|
5063
|
+
"position:fixed;display:none;z-index:2147483647;box-sizing:border-box;min-width:136px;padding:4px;border:1px solid rgba(15,23,42,0.16);border-radius:6px;background:#fff;box-shadow:0 10px 30px rgba(15,23,42,0.18);font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
|
|
5064
|
+
|
|
5065
|
+
function createMenuButton(labelText, onActivate) {
|
|
5066
|
+
var button = document.createElement("button")
|
|
5067
|
+
button.type = "button"
|
|
5068
|
+
button.textContent = labelText
|
|
5069
|
+
button.style.cssText =
|
|
5070
|
+
"display:block;width:100%;border:0;border-radius:4px;background:transparent;padding:6px 8px;text-align:left;color:#0f172a;font-size:12px;line-height:16px;cursor:pointer;"
|
|
5071
|
+
button.addEventListener("mouseenter", function () {
|
|
5072
|
+
button.style.background = inspectorTheme.accentSoft
|
|
5073
|
+
})
|
|
5074
|
+
button.addEventListener("mouseleave", function () {
|
|
5075
|
+
button.style.background = "transparent"
|
|
5076
|
+
})
|
|
5077
|
+
button.addEventListener("click", function (event) {
|
|
5078
|
+
event.preventDefault()
|
|
5079
|
+
event.stopPropagation()
|
|
5080
|
+
event.stopImmediatePropagation()
|
|
5081
|
+
try {
|
|
5082
|
+
onActivate()
|
|
5083
|
+
} catch (err) {
|
|
5084
|
+
inspectorWarn("[archie-inspector] context menu action failed", err)
|
|
5085
|
+
}
|
|
5086
|
+
hideSelectionContextMenu()
|
|
5087
|
+
})
|
|
5088
|
+
return button
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
contextMenuChatButton = createMenuButton(
|
|
5092
|
+
getSendToChatLabel(),
|
|
5093
|
+
function () {
|
|
5094
|
+
if (!contextMenuTarget) return
|
|
5095
|
+
// TWO messages on purpose — the host splits this request in two
|
|
5096
|
+
// and neither half alone is visible:
|
|
5097
|
+
//
|
|
5098
|
+
// bare -> arms the editor surface ("inspect"), which is
|
|
5099
|
+
// what makes the chat's source bar render at all.
|
|
5100
|
+
// + metadata-> pins the node as the chat context and opens the
|
|
5101
|
+
// chat, but leaves the surface untouched.
|
|
5102
|
+
//
|
|
5103
|
+
// Sending only the metadata one pinned an element behind a bar
|
|
5104
|
+
// that was never rendered, which is why the entry looked dead.
|
|
5105
|
+
// The bare half is skipped while inspecting so it can't yank an
|
|
5106
|
+
// open visual-editor session into source-selection mode.
|
|
5107
|
+
if (!isInspecting) {
|
|
5108
|
+
postMessageToParent({ type: "CHAT_CONTEXT_REQUESTED" })
|
|
5109
|
+
}
|
|
5110
|
+
// `extractMetadata` carries the node's whole attribute map (the
|
|
5111
|
+
// `data-archie-id` the plain-render prototype stamps included) plus
|
|
5112
|
+
// `inspectorSource`; the host derives the chip's file from the
|
|
5113
|
+
// latter and its componentId from the former, falling back to
|
|
5114
|
+
// `data-dnd-node-id` on kit pages that stamp no archie id.
|
|
5115
|
+
postMessageToParent({
|
|
5116
|
+
type: "CHAT_CONTEXT_REQUESTED",
|
|
5117
|
+
metadata: extractMetadata(contextMenuTarget),
|
|
5118
|
+
})
|
|
5119
|
+
}
|
|
5120
|
+
)
|
|
5121
|
+
|
|
5122
|
+
contextMenuCommentButton = createMenuButton(
|
|
5123
|
+
getAddCommentLabel(),
|
|
5124
|
+
function () {
|
|
5125
|
+
postMessageToParent({
|
|
5126
|
+
type: "ADD_COMMENT_REQUESTED",
|
|
5127
|
+
target: contextMenuTargetPayload,
|
|
5128
|
+
})
|
|
5129
|
+
}
|
|
5130
|
+
)
|
|
5131
|
+
|
|
5132
|
+
menu.appendChild(contextMenuChatButton)
|
|
5133
|
+
menu.appendChild(contextMenuCommentButton)
|
|
5134
|
+
;(document.body || document.documentElement).appendChild(menu)
|
|
5135
|
+
contextMenuEl = menu
|
|
5136
|
+
syncContextMenuLabels()
|
|
5137
|
+
return menu
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
function buildCommentTarget(element, event) {
|
|
5141
|
+
if (!element) return null
|
|
5142
|
+
try {
|
|
5143
|
+
var rect = element.getBoundingClientRect()
|
|
5144
|
+
var viewportWidth =
|
|
5145
|
+
window.innerWidth || document.documentElement.clientWidth || 0
|
|
5146
|
+
var viewportHeight =
|
|
5147
|
+
window.innerHeight || document.documentElement.clientHeight || 0
|
|
5148
|
+
var preview = element.textContent
|
|
5149
|
+
? element.textContent.trim().slice(0, 120)
|
|
5150
|
+
: ""
|
|
5151
|
+
return {
|
|
5152
|
+
cssSelector: getStableSelector(element),
|
|
5153
|
+
elementTag: String(element.tagName || "").toLowerCase(),
|
|
5154
|
+
elementPreview: preview || null,
|
|
5155
|
+
rect: {
|
|
5156
|
+
x: rect.left,
|
|
5157
|
+
y: rect.top,
|
|
5158
|
+
width: rect.width,
|
|
5159
|
+
height: rect.height,
|
|
5160
|
+
},
|
|
5161
|
+
// Fractions of the iframe viewport — the host re-projects them
|
|
5162
|
+
// against the iframe's own rect (see AddCommentRequestTarget).
|
|
5163
|
+
relativeX: viewportWidth ? event.clientX / viewportWidth : 0,
|
|
5164
|
+
relativeY: viewportHeight ? event.clientY / viewportHeight : 0,
|
|
5165
|
+
viewportWidth: viewportWidth,
|
|
5166
|
+
viewportHeight: viewportHeight,
|
|
5167
|
+
scrollTop:
|
|
5168
|
+
window.scrollY || document.documentElement.scrollTop || 0,
|
|
5169
|
+
}
|
|
5170
|
+
} catch (err) {
|
|
5171
|
+
return null
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
function showSelectionContextMenu(target, event) {
|
|
5176
|
+
var menu = ensureSelectionContextMenu()
|
|
5177
|
+
contextMenuTarget = target
|
|
5178
|
+
contextMenuTargetPayload = buildCommentTarget(target, event)
|
|
5179
|
+
menu.style.left =
|
|
5180
|
+
Math.min(event.clientX, window.innerWidth - 150) + "px"
|
|
5181
|
+
menu.style.top = Math.min(event.clientY, window.innerHeight - 60) + "px"
|
|
5182
|
+
menu.style.display = "block"
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
var onContextMenuOpen = function (e) {
|
|
5186
|
+
var target = e.target
|
|
5187
|
+
if (!target || target === document.documentElement) return
|
|
5188
|
+
if (contextMenuEl && contextMenuEl.contains(target)) return
|
|
5189
|
+
if (isInspectorElement(target)) return
|
|
5190
|
+
|
|
5191
|
+
var resolved = resolveExactInspectTarget(target)
|
|
5192
|
+
if (!isValidContentElement(resolved)) {
|
|
5193
|
+
resolved = getElementFromPoint(e.clientX, e.clientY, target)
|
|
5194
|
+
}
|
|
5195
|
+
if (!isValidContentElement(resolved)) return
|
|
5196
|
+
|
|
5197
|
+
e.preventDefault()
|
|
5198
|
+
e.stopPropagation()
|
|
5199
|
+
// Beat setupEventBlockers' "contextmenu" blocker, which would
|
|
5200
|
+
// otherwise swallow the event before it reaches us again.
|
|
5201
|
+
e.stopImmediatePropagation()
|
|
5202
|
+
showSelectionContextMenu(resolved, e)
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
var onContextMenuDismissPointer = function (e) {
|
|
5206
|
+
if (contextMenuEl && contextMenuEl.contains(e.target)) return
|
|
5207
|
+
hideSelectionContextMenu()
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
var onContextMenuDismissKey = function (e) {
|
|
5211
|
+
if (e.key === "Escape") hideSelectionContextMenu()
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
// Each runInspector() call builds a fresh closure, so drop the previous
|
|
5215
|
+
// run's listeners (and its orphaned menu node) the same way the message
|
|
5216
|
+
// handler above is deduped — otherwise a re-run stacks handlers that
|
|
5217
|
+
// each open their own menu.
|
|
5218
|
+
var previousContextMenuHandlers =
|
|
5219
|
+
window.__ARCHIE_INSPECTOR_CONTEXT_MENU_HANDLERS__
|
|
5220
|
+
if (previousContextMenuHandlers) {
|
|
5221
|
+
try {
|
|
5222
|
+
document.removeEventListener(
|
|
5223
|
+
"contextmenu",
|
|
5224
|
+
previousContextMenuHandlers.contextmenu,
|
|
5225
|
+
true
|
|
5226
|
+
)
|
|
5227
|
+
document.removeEventListener(
|
|
5228
|
+
"mousedown",
|
|
5229
|
+
previousContextMenuHandlers.mousedown,
|
|
5230
|
+
true
|
|
5231
|
+
)
|
|
5232
|
+
window.removeEventListener(
|
|
5233
|
+
"scroll",
|
|
5234
|
+
previousContextMenuHandlers.scroll,
|
|
5235
|
+
true
|
|
5236
|
+
)
|
|
5237
|
+
document.removeEventListener(
|
|
5238
|
+
"keydown",
|
|
5239
|
+
previousContextMenuHandlers.keydown,
|
|
5240
|
+
true
|
|
5241
|
+
)
|
|
5242
|
+
} catch (err) {
|
|
5243
|
+
// Ignore cleanup errors
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
document.addEventListener("contextmenu", onContextMenuOpen, true)
|
|
5248
|
+
document.addEventListener("mousedown", onContextMenuDismissPointer, true)
|
|
5249
|
+
window.addEventListener("scroll", hideSelectionContextMenu, true)
|
|
5250
|
+
document.addEventListener("keydown", onContextMenuDismissKey, true)
|
|
5251
|
+
|
|
5252
|
+
window.__ARCHIE_INSPECTOR_CONTEXT_MENU_HANDLERS__ = {
|
|
5253
|
+
contextmenu: onContextMenuOpen,
|
|
5254
|
+
mousedown: onContextMenuDismissPointer,
|
|
5255
|
+
scroll: hideSelectionContextMenu,
|
|
5256
|
+
keydown: onContextMenuDismissKey,
|
|
5257
|
+
}
|
|
5258
|
+
|
|
4506
5259
|
// Register inspector click handler BEFORE event blockers
|
|
4507
|
-
// This ensures it runs first and can mark events as handled
|
|
5260
|
+
// This ensures it runs first and can mark events as handled.
|
|
5261
|
+
// It stays AFTER the context-menu mousedown listener above, which must
|
|
5262
|
+
// still run (to dismiss the menu) even when a selection click calls
|
|
5263
|
+
// stopImmediatePropagation.
|
|
4508
5264
|
document.addEventListener("mousedown", inspectorClickHandler, true)
|
|
4509
5265
|
|
|
4510
5266
|
// Block clicks when element is selected but not inspecting
|