@agent-native/core 0.90.5 → 0.90.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +156 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +181 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/deploy/build.d.ts +2 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +96 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- package/package.json +1 -1
|
@@ -242,6 +242,43 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
242
242
|
return !!(el && !isDocumentRootElement(el) && getSourceId(el));
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// Detects an Alpine `<template x-for>` runtime clone: Alpine keeps the
|
|
246
|
+
// `<template>` element itself in the live DOM (as a hidden, zero-size
|
|
247
|
+
// marker) and inserts every rendered instance as a DIRECT SIBLING of that
|
|
248
|
+
// template, all still children of the same parent — so `ul > template,
|
|
249
|
+
// li, li, li` is the live shape for `<ul><template x-for>...</template>
|
|
250
|
+
// rendering 3 items</ul>`. The static SOURCE HTML the host resolves moves
|
|
251
|
+
// against only ever contains the single template child, never the N
|
|
252
|
+
// runtime clones, so structural moves (reorder/reparent) targeting a
|
|
253
|
+
// clone — or targeting another clone as the anchor — can never resolve on
|
|
254
|
+
// the host and always come back `applied:false`. Detected once per drag
|
|
255
|
+
// via an ancestor walk (not just the immediate parent) so nested x-for
|
|
256
|
+
// clones (e.g. a subtask `<li>` inside a per-task `<ul>` that is itself
|
|
257
|
+
// x-for'd) are also caught, stopping at the first stable-id ancestor
|
|
258
|
+
// (anything inside a stamped subtree has a real anchor and is fine).
|
|
259
|
+
function isTemplateCloneElement(el: Element | null): boolean {
|
|
260
|
+
var node: Element | null = el;
|
|
261
|
+
while (node && !isDocumentRootElement(node)) {
|
|
262
|
+
if (hasStableOwnSource(node)) return false;
|
|
263
|
+
var parent = node.parentElement;
|
|
264
|
+
if (!parent) return false;
|
|
265
|
+
var siblings = parent.children;
|
|
266
|
+
for (var i = 0; i < siblings.length; i += 1) {
|
|
267
|
+
var sib = siblings[i];
|
|
268
|
+
if (
|
|
269
|
+
sib !== node &&
|
|
270
|
+
sib.tagName &&
|
|
271
|
+
sib.tagName.toLowerCase() === "template" &&
|
|
272
|
+
sib.hasAttribute("x-for")
|
|
273
|
+
) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
node = parent;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
function selectionTargetForHit(hit: Element | null): Element | null {
|
|
246
283
|
if (!hit || isDocumentRootElement(hit)) return hit;
|
|
247
284
|
if (selectedEl && hit !== selectedEl && selectedEl.contains(hit))
|
|
@@ -520,6 +557,30 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
520
557
|
return path;
|
|
521
558
|
}
|
|
522
559
|
|
|
560
|
+
// Editor-internal CSS custom-property prefixes — selection chrome colors,
|
|
561
|
+
// editor-chrome scale compensation, framework clipboard/surface tokens.
|
|
562
|
+
// These have no meaning outside this editor session and must never leak
|
|
563
|
+
// into persisted user HTML/exports. DesignEditor.tsx's
|
|
564
|
+
// applyPortableStyleSnapshotToHtml (isEditorInternalCssVar /
|
|
565
|
+
// EDITOR_INTERNAL_CSS_VAR_PREFIXES) already filters them back out on the
|
|
566
|
+
// apply side; filtering here too at COLLECTION time is pure bloat
|
|
567
|
+
// reduction (skips carrying them across the postMessage boundary at all)
|
|
568
|
+
// and changes no observable behavior on the apply side.
|
|
569
|
+
//
|
|
570
|
+
// keep in sync with DesignEditor.tsx's EDITOR_INTERNAL_CSS_VAR_PREFIXES
|
|
571
|
+
var EDITOR_INTERNAL_CSS_VAR_PREFIXES = [
|
|
572
|
+
"--design-editor-",
|
|
573
|
+
"--agent-native-editor-chrome-",
|
|
574
|
+
"--agent-native-",
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
function isEditorInternalCssVarName(name: string): boolean {
|
|
578
|
+
for (var i = 0; i < EDITOR_INTERNAL_CSS_VAR_PREFIXES.length; i += 1) {
|
|
579
|
+
if (name.indexOf(EDITOR_INTERNAL_CSS_VAR_PREFIXES[i]) === 0) return true;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
523
584
|
function collectPortableComputedStyles(
|
|
524
585
|
el: Element | null,
|
|
525
586
|
): Record<string, string> {
|
|
@@ -534,7 +595,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
534
595
|
});
|
|
535
596
|
for (var index = 0; index < cs.length; index += 1) {
|
|
536
597
|
var name = cs.item(index);
|
|
537
|
-
if (
|
|
598
|
+
if (
|
|
599
|
+
name &&
|
|
600
|
+
name.indexOf("--") === 0 &&
|
|
601
|
+
!isEditorInternalCssVarName(name)
|
|
602
|
+
) {
|
|
538
603
|
var customValue = cs.getPropertyValue(name);
|
|
539
604
|
if (customValue && customValue.trim()) {
|
|
540
605
|
styles[name] = customValue.trim();
|
|
@@ -677,7 +742,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
677
742
|
el !== document.body &&
|
|
678
743
|
el !== document.documentElement &&
|
|
679
744
|
el.getAttribute &&
|
|
680
|
-
el.setAttribute
|
|
745
|
+
el.setAttribute &&
|
|
746
|
+
// Defensive guard (mirrors hit-test.bridge.ts's getOrMintPendingNodeId):
|
|
747
|
+
// a template clone has no counterpart in source HTML, so no host
|
|
748
|
+
// persist call could ever durably write data-agent-native-node-id for
|
|
749
|
+
// it, and Alpine re-renders the clone from scratch on the next data
|
|
750
|
+
// change anyway (the stamped attribute would vanish). Fail closed
|
|
751
|
+
// instead of minting a pending id that can never be persisted.
|
|
752
|
+
!isTemplateCloneElement(el)
|
|
681
753
|
) {
|
|
682
754
|
pendingNodeId = el.getAttribute("data-an-pending-node-id") || "";
|
|
683
755
|
if (!pendingNodeId) {
|
|
@@ -1449,13 +1521,108 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
1449
1521
|
selection: string;
|
|
1450
1522
|
highlight: string;
|
|
1451
1523
|
} | null = null;
|
|
1524
|
+
// T22: deferred begin-text-edit command for a node that hasn't landed in
|
|
1525
|
+
// this document yet. The host posts begin-text-edit immediately after
|
|
1526
|
+
// creating a text primitive, but the node itself arrives via the
|
|
1527
|
+
// replace-document-content persist round-trip — when the command wins that
|
|
1528
|
+
// race the old behavior silently dropped it, leaving the user typing into
|
|
1529
|
+
// nothing (worst case: Delete/arrow keystrokes fell through to host layer
|
|
1530
|
+
// shortcuts). Instead, poll briefly (bounded ~2s, rAF cadence) for the
|
|
1531
|
+
// nodeId and activate the edit the moment it appears. Only the newest
|
|
1532
|
+
// command is kept; any user pointerdown or a user-initiated text edit
|
|
1533
|
+
// cancels it (the user has moved on — never yank focus later).
|
|
1534
|
+
var pendingBeginTextEdit: {
|
|
1535
|
+
nodeId: string;
|
|
1536
|
+
force: boolean;
|
|
1537
|
+
deadline: number;
|
|
1538
|
+
raf: number;
|
|
1539
|
+
// Keystrokes typed INTO THIS IFRAME while the command waits for its node
|
|
1540
|
+
// (see the pending-window branch of the document keydown handler) —
|
|
1541
|
+
// replayed into the editable the moment it activates.
|
|
1542
|
+
buffer: string;
|
|
1543
|
+
} | null = null;
|
|
1544
|
+
function cancelPendingBeginTextEdit(): void {
|
|
1545
|
+
if (!pendingBeginTextEdit) return;
|
|
1546
|
+
if (pendingBeginTextEdit.raf) {
|
|
1547
|
+
window.cancelAnimationFrame(pendingBeginTextEdit.raf);
|
|
1548
|
+
}
|
|
1549
|
+
pendingBeginTextEdit = null;
|
|
1550
|
+
}
|
|
1551
|
+
// T25: tell the HOST (DesignCanvas) that a begin-text-edit command is
|
|
1552
|
+
// waiting for its node, so the host arms its own keystroke buffer for keys
|
|
1553
|
+
// that land on the HOST document during the window (the host cannot see
|
|
1554
|
+
// the begin-text-edit post itself — DesignEditor sends it straight to this
|
|
1555
|
+
// iframe). pending:false stands the host down when the wait is abandoned;
|
|
1556
|
+
// successful activation instead flows through text-editing-state(active),
|
|
1557
|
+
// which both flushes the host buffer and clears its pending flag.
|
|
1558
|
+
function postTextEditPending(nodeId: string, pending: boolean): void {
|
|
1559
|
+
(window.parent as Window).postMessage(
|
|
1560
|
+
{ type: "text-edit-pending", nodeId: nodeId, pending: pending },
|
|
1561
|
+
"*",
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
// T23: is the active text-edit element still part of this document? A
|
|
1565
|
+
// document patch (replaceRuntimeDocument subtree/body swap), a
|
|
1566
|
+
// delete-element command, or in-page reactivity (Alpine x-if) can detach
|
|
1567
|
+
// the edited node while its session is live — its blur/keydown listeners
|
|
1568
|
+
// then never fire again, so nothing ever runs the Escape/blur cleanup
|
|
1569
|
+
// path. The leaked activeTextEditEl blocked ALL drags/marquees
|
|
1570
|
+
// (beginPotentialShieldDrag/beginMarqueeSelection bail on it), kept the
|
|
1571
|
+
// shield pointer-passthrough disabled, swallowed every design hotkey, and
|
|
1572
|
+
// buffered every future runtime content update until a full reload.
|
|
1573
|
+
function isTextEditElConnected(): boolean {
|
|
1574
|
+
return !!(
|
|
1575
|
+
activeTextEditEl &&
|
|
1576
|
+
activeTextEditEl.isConnected &&
|
|
1577
|
+
document.documentElement.contains(activeTextEditEl)
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
// T23: exit a stale (detached-element) text edit session through the SAME
|
|
1581
|
+
// cleanup path a user Escape/blur takes. Returns true when a stale session
|
|
1582
|
+
// was cleaned up. Callers that previously hard-bailed on activeTextEditEl
|
|
1583
|
+
// should call this first so a leaked session self-heals on the next
|
|
1584
|
+
// interaction instead of wedging the surface until reload.
|
|
1585
|
+
function exitStaleTextEditSession(): boolean {
|
|
1586
|
+
if (!activeTextEditEl || isTextEditElConnected()) return false;
|
|
1587
|
+
var staleEl = activeTextEditEl;
|
|
1588
|
+
if (finishActiveTextEdit) {
|
|
1589
|
+
finishActiveTextEdit(true);
|
|
1590
|
+
} else {
|
|
1591
|
+
postTextEditingState(staleEl, false);
|
|
1592
|
+
activeTextEditEl = null;
|
|
1593
|
+
setTextEditingPointerPassthrough(false);
|
|
1594
|
+
setSelectionOverlayResizeChromeVisible(true);
|
|
1595
|
+
}
|
|
1596
|
+
// Defensive: finish() only clears activeTextEditEl when it still points
|
|
1597
|
+
// at that session's own target. If overlapping sessions ever left a
|
|
1598
|
+
// different detached element behind, force-clear so the surface can't
|
|
1599
|
+
// stay wedged.
|
|
1600
|
+
if (activeTextEditEl === staleEl) {
|
|
1601
|
+
activeTextEditEl = null;
|
|
1602
|
+
setTextEditingPointerPassthrough(false);
|
|
1603
|
+
setSelectionOverlayResizeChromeVisible(true);
|
|
1604
|
+
}
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1452
1607
|
var pendingStructureMoves: Record<
|
|
1453
1608
|
string,
|
|
1454
1609
|
{
|
|
1455
1610
|
requestId: string;
|
|
1456
1611
|
el: Element;
|
|
1457
1612
|
target: { anchor: Element; placement: string; axis?: string };
|
|
1458
|
-
origin: {
|
|
1613
|
+
origin: {
|
|
1614
|
+
prevParent: Element;
|
|
1615
|
+
prevNextSibling: Node | null;
|
|
1616
|
+
// Inline position/left/top/right/bottom VALUES captured right before
|
|
1617
|
+
// the optimistic reorder's stripAbsolutePositioningForFlowInsert ran
|
|
1618
|
+
// (absent/undefined when that strip did not apply — e.g. an
|
|
1619
|
+
// absolute-container drop, or a flow-reorder of an already-flow
|
|
1620
|
+
// element with nothing to strip). Restored by the visual-structure-ack
|
|
1621
|
+
// failure branch alongside the parent/sibling revert so a rejected
|
|
1622
|
+
// move-node round-trip cannot leave the element stripped of its
|
|
1623
|
+
// absolute positioning while stuck in the wrong parent.
|
|
1624
|
+
prevInlinePositionStyles?: Record<string, string> | null;
|
|
1625
|
+
} | null;
|
|
1459
1626
|
}
|
|
1460
1627
|
> = {};
|
|
1461
1628
|
var pendingShieldDrag: {
|
|
@@ -1713,6 +1880,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
1713
1880
|
forceFullDocument?: boolean,
|
|
1714
1881
|
): void {
|
|
1715
1882
|
if (typeof html !== "string") return;
|
|
1883
|
+
// T23: a session whose element was already detached (earlier patch,
|
|
1884
|
+
// delete-element, in-page reactivity) can never end via blur/Escape —
|
|
1885
|
+
// buffering behind it would freeze this surface's content forever. Exit
|
|
1886
|
+
// it through the canonical cleanup first, then treat this update
|
|
1887
|
+
// normally. (The nested pendingRuntimeDocumentUpdate replay inside
|
|
1888
|
+
// finish() runs before we continue; this newer payload then supersedes
|
|
1889
|
+
// its result, preserving ordering.)
|
|
1890
|
+
exitStaleTextEditSession();
|
|
1716
1891
|
if (activeTextEditEl && !forceFullDocument) {
|
|
1717
1892
|
// Don't yank a runtime content update out from under an in-progress
|
|
1718
1893
|
// text edit — but don't silently lose it either (T13). Buffer only the
|
|
@@ -2849,48 +3024,133 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2849
3024
|
return false;
|
|
2850
3025
|
}
|
|
2851
3026
|
|
|
2852
|
-
|
|
2853
|
-
|
|
3027
|
+
// ── Selection-handle hit-zone inward clamp ────────────────────────────
|
|
3028
|
+
// Keep in sync with multi-screen/handle-hit-zones.ts (the host-side
|
|
3029
|
+
// selection chrome applies the same clamp rule to its screen-frame/board
|
|
3030
|
+
// handles; nominal sizes differ — bridge edge bars are 10px thick centered
|
|
3031
|
+
// on the edge, corner squares 7px with a 4px outward offset — but the
|
|
3032
|
+
// inward-reach clamp and its 0.25 fraction must stay identical).
|
|
3033
|
+
//
|
|
3034
|
+
// The edge/corner handles multiply by the chrome scale so they keep a
|
|
3035
|
+
// constant on-screen size, which makes their HIT zones grow without bound
|
|
3036
|
+
// in iframe-local px as the host zooms out: at 19% zoom the nominal 10px
|
|
3037
|
+
// edge bar is ~52.6 local px thick, reaching ~26.3 local px into the
|
|
3038
|
+
// element from each edge. Any element smaller than twice that reach has
|
|
3039
|
+
// its ENTIRE body covered by the two opposing bars — every press resolves
|
|
3040
|
+
// to a resize, so the element can never be grabbed for a move drag (or
|
|
3041
|
+
// clicked in its interior) at low zoom. Clamp only the INWARD reach of
|
|
3042
|
+
// each handle hit zone to a fraction of the element's own dimension on
|
|
3043
|
+
// that axis; the outward reach (which can never occlude the body) and the
|
|
3044
|
+
// corner handles' VISUAL size stay untouched. With 0.25, two opposing
|
|
3045
|
+
// handles consume at most half the dimension, so the central 50% band of
|
|
3046
|
+
// each axis always stays body-grabbable.
|
|
3047
|
+
var HANDLE_MAX_INWARD_FRACTION = 0.25;
|
|
3048
|
+
|
|
3049
|
+
// Mirror of clampHandleInwardReach in multi-screen/handle-hit-zones.ts.
|
|
3050
|
+
// Non-finite or non-positive dimensions (no overlaid element, degenerate
|
|
3051
|
+
// zero-size elements mid-creation) return the nominal reach unchanged —
|
|
3052
|
+
// exactly the pre-clamp behavior, and a zero-size element has no body to
|
|
3053
|
+
// protect.
|
|
3054
|
+
function clampHandleInwardReach(nominalInward, elementDimension) {
|
|
3055
|
+
if (!Number.isFinite(elementDimension) || elementDimension <= 0) {
|
|
3056
|
+
return nominalInward;
|
|
3057
|
+
}
|
|
3058
|
+
return Math.min(
|
|
3059
|
+
nominalInward,
|
|
3060
|
+
elementDimension * HANDLE_MAX_INWARD_FRACTION,
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// Sizes the selection overlay's edge/corner handles for the current chrome
|
|
3065
|
+
// scale, clamping each handle's inward reach against the overlaid
|
|
3066
|
+
// element's own rect. Called from applyEditorChromeScale (scale changes)
|
|
3067
|
+
// AND from positionOverlay (selection/element changes), because the
|
|
3068
|
+
// clamped geometry depends on the element's dimensions, not just the
|
|
3069
|
+
// scale. On large elements this reproduces the historical geometry
|
|
3070
|
+
// exactly: edge bars 10*scale thick centered on the edge, corner squares
|
|
3071
|
+
// 7*scale offset -4*scale.
|
|
3072
|
+
function applySelectionHandleHitGeometry(el) {
|
|
2854
3073
|
var sx = chromeScaleX();
|
|
2855
3074
|
var sy = chromeScaleY();
|
|
2856
3075
|
var line = chromeLineScale();
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
marqueeSelectionOverlay.style.borderWidth = 1 * line + "px";
|
|
2865
|
-
passiveSelectionOverlays.forEach(scalePassiveSelectionOverlay);
|
|
2866
|
-
if (selectedEl) updateSpacingOverlay(selectedEl);
|
|
3076
|
+
var elWidth = NaN;
|
|
3077
|
+
var elHeight = NaN;
|
|
3078
|
+
if (el && document.documentElement.contains(el)) {
|
|
3079
|
+
var elRect = el.getBoundingClientRect();
|
|
3080
|
+
elWidth = elRect.width;
|
|
3081
|
+
elHeight = elRect.height;
|
|
3082
|
+
}
|
|
2867
3083
|
|
|
3084
|
+
// Invisible edge-resize hit bars: nominal 5*scale outward + 5*scale
|
|
3085
|
+
// inward. Only the inward half is clamped; the bar's outward side stays
|
|
3086
|
+
// anchored at -5*scale from the edge.
|
|
2868
3087
|
selectionOverlay
|
|
2869
3088
|
.querySelectorAll("[data-agent-native-edge-handle]")
|
|
2870
3089
|
.forEach(function (edge) {
|
|
2871
3090
|
var pos = edge.getAttribute("data-agent-native-edge-handle");
|
|
2872
3091
|
if (pos === "n" || pos === "s") {
|
|
2873
|
-
|
|
2874
|
-
|
|
3092
|
+
var outwardY = 5 * sy;
|
|
3093
|
+
var inwardY = clampHandleInwardReach(5 * sy, elHeight);
|
|
3094
|
+
edge.style.height = outwardY + inwardY + "px";
|
|
3095
|
+
edge.style[pos === "n" ? "top" : "bottom"] = -outwardY + "px";
|
|
2875
3096
|
}
|
|
2876
3097
|
if (pos === "e" || pos === "w") {
|
|
2877
|
-
|
|
2878
|
-
|
|
3098
|
+
var outwardX = 5 * sx;
|
|
3099
|
+
var inwardX = clampHandleInwardReach(5 * sx, elWidth);
|
|
3100
|
+
edge.style.width = outwardX + inwardX + "px";
|
|
3101
|
+
edge.style[pos === "w" ? "left" : "right"] = -outwardX + "px";
|
|
2879
3102
|
}
|
|
2880
3103
|
});
|
|
2881
3104
|
|
|
3105
|
+
// Visible corner squares: the square keeps its constant on-screen size
|
|
3106
|
+
// (7*scale); when its nominal 3*scale inward overlap would exceed the
|
|
3107
|
+
// per-axis clamp, the square shifts outward so only the clamped reach
|
|
3108
|
+
// overlaps the body. Corners clamp per-axis independently.
|
|
2882
3109
|
selectionOverlay
|
|
2883
3110
|
.querySelectorAll("[data-agent-native-edit-handle]")
|
|
2884
3111
|
.forEach(function (handle) {
|
|
2885
3112
|
var pos = handle.getAttribute("data-agent-native-edit-handle") || "";
|
|
2886
|
-
|
|
2887
|
-
|
|
3113
|
+
var sizeX = 7 * sx;
|
|
3114
|
+
var sizeY = 7 * sy;
|
|
3115
|
+
// sizeY - 4*sy is exact (Sterbenz), so the unclamped offset below
|
|
3116
|
+
// reproduces the historical -4*scale bit-for-bit.
|
|
3117
|
+
var inwardX = clampHandleInwardReach(sizeX - 4 * sx, elWidth);
|
|
3118
|
+
var inwardY = clampHandleInwardReach(sizeY - 4 * sy, elHeight);
|
|
3119
|
+
handle.style.width = sizeX + "px";
|
|
3120
|
+
handle.style.height = sizeY + "px";
|
|
2888
3121
|
handle.style.borderWidth = 1 * line + "px";
|
|
2889
|
-
if (pos.indexOf("n") !== -1)
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
if (pos.indexOf("
|
|
3122
|
+
if (pos.indexOf("n") !== -1) {
|
|
3123
|
+
handle.style.top = inwardY - sizeY + "px";
|
|
3124
|
+
}
|
|
3125
|
+
if (pos.indexOf("s") !== -1) {
|
|
3126
|
+
handle.style.bottom = inwardY - sizeY + "px";
|
|
3127
|
+
}
|
|
3128
|
+
if (pos.indexOf("w") !== -1) {
|
|
3129
|
+
handle.style.left = inwardX - sizeX + "px";
|
|
3130
|
+
}
|
|
3131
|
+
if (pos.indexOf("e") !== -1) {
|
|
3132
|
+
handle.style.right = inwardX - sizeX + "px";
|
|
3133
|
+
}
|
|
2893
3134
|
});
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
function applyEditorChromeScale() {
|
|
3138
|
+
syncEditorChromeScaleVars();
|
|
3139
|
+
var sx = chromeScaleX();
|
|
3140
|
+
var sy = chromeScaleY();
|
|
3141
|
+
var line = chromeLineScale();
|
|
3142
|
+
// Figma parity: the hover outline is visibly thinner than the selection
|
|
3143
|
+
// outline — hover is a light "you could select this" hint, selection is
|
|
3144
|
+
// the stronger confirmed-state chrome. Matches the overview canvas's
|
|
3145
|
+
// hover (1 * chromeScale) vs selection (1.5 * chromeScale) ratio.
|
|
3146
|
+
highlightOverlay.style.borderWidth = 1 * line + "px";
|
|
3147
|
+
parentAutoLayoutOverlay.style.borderWidth = 1 * line + "px";
|
|
3148
|
+
selectionOverlay.style.borderWidth = 1.5 * line + "px";
|
|
3149
|
+
marqueeSelectionOverlay.style.borderWidth = 1 * line + "px";
|
|
3150
|
+
passiveSelectionOverlays.forEach(scalePassiveSelectionOverlay);
|
|
3151
|
+
if (selectedEl) updateSpacingOverlay(selectedEl);
|
|
3152
|
+
|
|
3153
|
+
applySelectionHandleHitGeometry(selectedEl);
|
|
2894
3154
|
|
|
2895
3155
|
selectionOverlay
|
|
2896
3156
|
.querySelectorAll("[data-agent-native-rotate-handle]")
|
|
@@ -2961,6 +3221,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2961
3221
|
}
|
|
2962
3222
|
if (overlay === selectionOverlay) {
|
|
2963
3223
|
applySelectionChrome(el);
|
|
3224
|
+
// Re-clamp handle hit zones for THIS element's dimensions — the
|
|
3225
|
+
// clamped geometry is element-dependent, not just scale-dependent
|
|
3226
|
+
// (see applySelectionHandleHitGeometry).
|
|
3227
|
+
applySelectionHandleHitGeometry(el);
|
|
2964
3228
|
updateSpacingOverlay(el);
|
|
2965
3229
|
// `rect` is undefined on the rotated-local-box path; updateComponentTag
|
|
2966
3230
|
// falls back to its own read in that case.
|
|
@@ -3836,7 +4100,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
3836
4100
|
}
|
|
3837
4101
|
|
|
3838
4102
|
function beginMarqueeSelection(e): void {
|
|
3839
|
-
if (e.button !== 0
|
|
4103
|
+
if (e.button !== 0) return;
|
|
4104
|
+
// T23: a stale session self-heals and the marquee proceeds; only a LIVE
|
|
4105
|
+
// session (connected element) blocks marquee starts.
|
|
4106
|
+
if (activeTextEditEl && !exitStaleTextEditSession()) return;
|
|
3840
4107
|
clearActiveMarqueeSelection();
|
|
3841
4108
|
var events = dragEventNames(e);
|
|
3842
4109
|
var additive = Boolean(e && (e.metaKey || e.ctrlKey || e.shiftKey));
|
|
@@ -3959,6 +4226,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
3959
4226
|
)
|
|
3960
4227
|
return false;
|
|
3961
4228
|
if (target.parentElement) target.parentElement.removeChild(target);
|
|
4229
|
+
// T23: the removed subtree may contain the active text-edit element —
|
|
4230
|
+
// its blur/keydown listeners are gone with it, so exit the session
|
|
4231
|
+
// through the canonical cleanup instead of leaking it.
|
|
4232
|
+
exitStaleTextEditSession();
|
|
3962
4233
|
if (
|
|
3963
4234
|
selectedEl === target ||
|
|
3964
4235
|
!document.documentElement.contains(selectedEl)
|
|
@@ -4927,6 +5198,26 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
4927
5198
|
|
|
4928
5199
|
function hideTransformBadge(): void {
|
|
4929
5200
|
transformBadge.style.display = "none";
|
|
5201
|
+
transformBadge.style.removeProperty("background");
|
|
5202
|
+
transformBadge.style.removeProperty("color");
|
|
5203
|
+
transformBadge.style.removeProperty("border-color");
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
// Rejection feedback for a drag that can never resolve on the host (see
|
|
5207
|
+
// isTemplateCloneElement): reuses transformBadge with a red-tinted style
|
|
5208
|
+
// instead of adding another chrome element, and a "not-allowed" cursor on
|
|
5209
|
+
// shieldOverlay for the duration of the gesture — clear, immediate signal
|
|
5210
|
+
// instead of an optimistic reorder that silently reverts ~1 frame later.
|
|
5211
|
+
function showRejectedDragBadge(
|
|
5212
|
+
text: string,
|
|
5213
|
+
clientX: number,
|
|
5214
|
+
clientY: number,
|
|
5215
|
+
): void {
|
|
5216
|
+
showTransformBadge(text, clientX, clientY);
|
|
5217
|
+
transformBadge.style.background =
|
|
5218
|
+
"color-mix(in srgb, #dc2626 92%, transparent)";
|
|
5219
|
+
transformBadge.style.color = "#fff";
|
|
5220
|
+
transformBadge.style.borderColor = "#dc2626";
|
|
4930
5221
|
}
|
|
4931
5222
|
|
|
4932
5223
|
function hideInsertionGuide(): void {
|
|
@@ -4944,12 +5235,25 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
4944
5235
|
);
|
|
4945
5236
|
}
|
|
4946
5237
|
|
|
5238
|
+
// Anchor-candidate gate (companion to the dragged-element
|
|
5239
|
+
// isTemplateCloneElement rejection below): a template clone can never be
|
|
5240
|
+
// used as an insertion ANCHOR either — it has no counterpart in the static
|
|
5241
|
+
// source HTML, so before/after placement against it can never resolve on
|
|
5242
|
+
// the host any more than dragging the clone itself could. Filtering clones
|
|
5243
|
+
// out of the candidate list here (rather than only checking the dragged
|
|
5244
|
+
// element) is what fixes drops into a container whose ONLY children are
|
|
5245
|
+
// x-for clones: without this, nearestChildInsertionTarget's "nearest
|
|
5246
|
+
// child" search and reorderTargetForPoint's sibling-scan fallback would
|
|
5247
|
+
// both happily pick a clone as the anchor, and the resulting moveNode
|
|
5248
|
+
// against source HTML would silently fail (layerMoveFailed toast) even
|
|
5249
|
+
// though the drop gesture itself was completely valid.
|
|
4947
5250
|
function draggableElementChildren(parent: Element): Element[] {
|
|
4948
5251
|
return Array.prototype.slice.call(parent.children).filter(function (child) {
|
|
4949
5252
|
return (
|
|
4950
5253
|
child.nodeType === 1 &&
|
|
4951
5254
|
!isOverlayElement(child) &&
|
|
4952
|
-
!isLayerInteractionBlocked(child)
|
|
5255
|
+
!isLayerInteractionBlocked(child) &&
|
|
5256
|
+
!isTemplateCloneElement(child)
|
|
4953
5257
|
);
|
|
4954
5258
|
});
|
|
4955
5259
|
}
|
|
@@ -5189,12 +5493,20 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5189
5493
|
// pre-marker content — exactly the default white AND the destination is
|
|
5190
5494
|
// light), switch it to `color:inherit` so it picks up the container's
|
|
5191
5495
|
// effective text color. A color the user explicitly set is NEVER touched:
|
|
5192
|
-
// DesignEditor
|
|
5193
|
-
// the color at creation
|
|
5194
|
-
//
|
|
5195
|
-
//
|
|
5196
|
-
//
|
|
5197
|
-
//
|
|
5496
|
+
// DesignEditor's appendCanvasPrimitiveToHtml stamps `data-an-auto-text-color`
|
|
5497
|
+
// when IT auto-picks the color at creation (BOARD_TEXT_AUTO_COLOR_MARKER
|
|
5498
|
+
// export in DesignEditor.tsx) and any explicit color edit removes the
|
|
5499
|
+
// marker; when the marker is present the color is definitely auto (always
|
|
5500
|
+
// safe to adapt), and when absent the conservative default-white +
|
|
5501
|
+
// light-target heuristic below only fires in the exact case where the text
|
|
5502
|
+
// would be invisible anyway.
|
|
5503
|
+
//
|
|
5504
|
+
// keep in sync with DesignEditor.tsx's
|
|
5505
|
+
// adaptAutoTextColorForCrossScreenNode / shouldAdaptAutoTextColorForCrossScreenMove
|
|
5506
|
+
// — the cross-screen mirror of this same decision, applied host-side (HTML
|
|
5507
|
+
// string, post-reparent) after handleCrossScreenElementDrop moves a text
|
|
5508
|
+
// node between documents, since this in-iframe bridge only ever sees
|
|
5509
|
+
// same-document re-parents.
|
|
5198
5510
|
var BOARD_TEXT_AUTO_COLOR_MARKER = "data-an-auto-text-color";
|
|
5199
5511
|
|
|
5200
5512
|
function parseCssRgb(
|
|
@@ -5397,6 +5709,38 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5397
5709
|
"li",
|
|
5398
5710
|
];
|
|
5399
5711
|
|
|
5712
|
+
// keep in sync with hit-test.bridge.ts BRIDGE_INTERACTIVE_LEAF_TAGS
|
|
5713
|
+
var BRIDGE_INTERACTIVE_LEAF_TAGS = ["button", "summary"];
|
|
5714
|
+
|
|
5715
|
+
// Drop-on-leaf fix: a `<button>` (or similar interactive leaf control) is
|
|
5716
|
+
// frequently styled `display:flex` purely to align its own icon + label —
|
|
5717
|
+
// that is NOT the same thing as a Figma "frame" a user expects to drop
|
|
5718
|
+
// items into. Neither the tag denylist (BRIDGE_LEAF_TAGS/TEXT_TAGS) nor the
|
|
5719
|
+
// flex/grid computed-display check alone can tell these apart (button is
|
|
5720
|
+
// in neither tag list, and it genuinely has display:flex), so this walks
|
|
5721
|
+
// the element's own children: if every child is itself a leaf/text tag
|
|
5722
|
+
// with no further container/flex descendant of its own, the element is
|
|
5723
|
+
// "leaf content" (an icon+label control) and must not accept nested
|
|
5724
|
+
// drops — only a container that itself hosts a real sub-layout (a nested
|
|
5725
|
+
// container/flex child) qualifies.
|
|
5726
|
+
function hasOnlyLeafContent(el: Element): boolean {
|
|
5727
|
+
var children = el.children;
|
|
5728
|
+
if (!children.length) return true;
|
|
5729
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
5730
|
+
var child = children[i] as Element;
|
|
5731
|
+
var childTag = (child.tagName || "").toLowerCase();
|
|
5732
|
+
if (
|
|
5733
|
+
BRIDGE_LEAF_TAGS.indexOf(childTag) === -1 &&
|
|
5734
|
+
BRIDGE_TEXT_TAGS.indexOf(childTag) === -1 &&
|
|
5735
|
+
BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(childTag) === -1
|
|
5736
|
+
) {
|
|
5737
|
+
return false;
|
|
5738
|
+
}
|
|
5739
|
+
if (child.children.length && !hasOnlyLeafContent(child)) return false;
|
|
5740
|
+
}
|
|
5741
|
+
return true;
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5400
5744
|
function isContainerDropTarget(el: Element | null): boolean {
|
|
5401
5745
|
if (!el || el === document.documentElement) return false;
|
|
5402
5746
|
if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
|
|
@@ -5408,6 +5752,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5408
5752
|
BRIDGE_TEXT_TAGS.indexOf(tag) !== -1
|
|
5409
5753
|
)
|
|
5410
5754
|
return false;
|
|
5755
|
+
// Reject interactive leaf controls (button, summary) whose children are
|
|
5756
|
+
// all leaf/text content — see hasOnlyLeafContent above.
|
|
5757
|
+
if (
|
|
5758
|
+
BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(tag) !== -1 &&
|
|
5759
|
+
hasOnlyLeafContent(el)
|
|
5760
|
+
) {
|
|
5761
|
+
return false;
|
|
5762
|
+
}
|
|
5411
5763
|
var cs = window.getComputedStyle(el);
|
|
5412
5764
|
if (
|
|
5413
5765
|
cs.display === "flex" ||
|
|
@@ -5466,6 +5818,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5466
5818
|
// with the container-fill affordance instead of an insertion line). Both
|
|
5467
5819
|
// in-screen drag paths now route container-background hits through this
|
|
5468
5820
|
// helper so dropping between children works and shows the line.
|
|
5821
|
+
//
|
|
5822
|
+
// keep in sync with hit-test.bridge.ts's own nearestChildInsertionTarget
|
|
5823
|
+
// (finding 6's cross-screen/canvas-to-screen mirror of this same fix —
|
|
5824
|
+
// that copy omits the `excludeEls` param since hit-test.bridge.ts never
|
|
5825
|
+
// has a dragged element of its own).
|
|
5469
5826
|
function nearestChildInsertionTarget(
|
|
5470
5827
|
container: Element,
|
|
5471
5828
|
clientX: number,
|
|
@@ -5538,11 +5895,21 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5538
5895
|
return false;
|
|
5539
5896
|
}
|
|
5540
5897
|
var hit = elementFromEditorPoint(clientX, clientY);
|
|
5898
|
+
// Anchor-candidate gate: a hit that resolves directly onto a template
|
|
5899
|
+
// clone (e.g. hovering over one of the rendered `<li>` items inside a
|
|
5900
|
+
// container whose ONLY children are x-for clones) can never anchor a
|
|
5901
|
+
// structural move — see draggableElementChildren's comment above. Falling
|
|
5902
|
+
// through here (instead of using `hit` as the anchor) routes to the
|
|
5903
|
+
// sibling-scan fallback below, which already filters clones out via
|
|
5904
|
+
// draggableElementChildren, so it correctly resolves to either a
|
|
5905
|
+
// non-clone sibling or, when there are none, no anchor at all (caller
|
|
5906
|
+
// falls back to the container itself with "inside" placement).
|
|
5541
5907
|
if (
|
|
5542
5908
|
hit &&
|
|
5543
5909
|
hit !== document.documentElement &&
|
|
5544
5910
|
!isDraggedOrInsideDragged(hit) &&
|
|
5545
|
-
!isOverlayElement(hit)
|
|
5911
|
+
!isOverlayElement(hit) &&
|
|
5912
|
+
!isTemplateCloneElement(hit)
|
|
5546
5913
|
) {
|
|
5547
5914
|
if (isContainerDropTarget(hit)) {
|
|
5548
5915
|
var containerRect = hit.getBoundingClientRect();
|
|
@@ -5607,7 +5974,25 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5607
5974
|
var siblings = draggableElementChildren(parent).filter(function (child) {
|
|
5608
5975
|
return !isDraggedOrInsideDragged(child);
|
|
5609
5976
|
});
|
|
5610
|
-
if (!siblings.length)
|
|
5977
|
+
if (!siblings.length) {
|
|
5978
|
+
// No non-clone, non-dragged sibling to anchor against — e.g. `parent`'s
|
|
5979
|
+
// only other children are x-for clones (draggableElementChildren
|
|
5980
|
+
// already filtered those out above). Fall back to the parent container
|
|
5981
|
+
// itself with "inside" placement instead of returning null: null here
|
|
5982
|
+
// would make onReorderUp treat this as "no valid drop target" and
|
|
5983
|
+
// silently no-op the whole gesture, even though moving into `parent`
|
|
5984
|
+
// (landing after the rendered clones, which in source HTML is simply
|
|
5985
|
+
// inside the container since clones don't exist there) is a completely
|
|
5986
|
+
// valid and expected drop.
|
|
5987
|
+
return {
|
|
5988
|
+
anchor: parent,
|
|
5989
|
+
placement: "inside",
|
|
5990
|
+
axis: axis,
|
|
5991
|
+
dropMode: isAbsolutePrimitiveContainer(parent)
|
|
5992
|
+
? "absolute-container"
|
|
5993
|
+
: "flow-insert",
|
|
5994
|
+
};
|
|
5995
|
+
}
|
|
5611
5996
|
var beforeTarget = null;
|
|
5612
5997
|
for (var i = 0; i < siblings.length; i += 1) {
|
|
5613
5998
|
var rect = siblings[i].getBoundingClientRect();
|
|
@@ -5721,29 +6106,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5721
6106
|
// node whose parent happens to be body — must fall through to a plain
|
|
5722
6107
|
// absolute placement instead of silently wrapping body in auto-layout.
|
|
5723
6108
|
var parent = cursor.parentElement;
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
6109
|
+
// Absolute-primitive-container target (a canvas rectangle marked
|
|
6110
|
+
// data-an-primitive="rectangle"/"rect"): this is a dedicated
|
|
6111
|
+
// free-placement container, not a Figma-style auto-layout frame — the
|
|
6112
|
+
// matching reorderTargetForPoint (flow-reorder) branch already
|
|
6113
|
+
// recognizes it via this same helper and assigns dropMode
|
|
6114
|
+
// "absolute-container" so onUp skips the auto-layout conversion and
|
|
6115
|
+
// keeps the moved element's position:absolute.
|
|
6116
|
+
if (cursor !== document.body && isAbsolutePrimitiveContainer(cursor)) {
|
|
5732
6117
|
return {
|
|
5733
6118
|
anchor: cursor,
|
|
5734
|
-
placement:
|
|
5735
|
-
axis:
|
|
5736
|
-
dropMode: "
|
|
5737
|
-
needsAutoLayoutConversion: !isAutoLayoutElement(parent),
|
|
5738
|
-
conversionTarget: parent,
|
|
6119
|
+
placement: "inside",
|
|
6120
|
+
axis: "y",
|
|
6121
|
+
dropMode: "absolute-container",
|
|
5739
6122
|
};
|
|
5740
6123
|
}
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
6124
|
+
// Nest-inside-what-you're-hovering takes priority over sibling-insert
|
|
6125
|
+
// UNLESS cursor is already a managed flex-item of an ESTABLISHED
|
|
6126
|
+
// auto-layout parent (isAutoLayoutElement — genuinely display:flex/grid,
|
|
6127
|
+
// not just an isContainerDropTarget tag match). That parent-is-already-
|
|
6128
|
+
// auto-layout signal is what distinguishes "cursor is a list item being
|
|
6129
|
+
// reordered within its existing list" (sibling-insert is correct: a
|
|
6130
|
+
// plain-block <div> chip that is itself a flex-item of #frame/#col)
|
|
6131
|
+
// from "cursor is genuinely being hovered as a nesting target" (a
|
|
6132
|
+
// pristine/empty container, or a real container whose own parent isn't
|
|
6133
|
+
// already running auto-layout — e.g. a top-level or newly-adjacent
|
|
6134
|
+
// rectangle, matching reorderTargetForPoint's isContainerDropTarget(hit)
|
|
6135
|
+
// priority for the flow-reorder gesture).
|
|
6136
|
+
//
|
|
6137
|
+
// This check used to run AFTER the sibling-insert branch below with no
|
|
6138
|
+
// such carve-out, so hovering directly over a pristine/empty auto-layout
|
|
6139
|
+
// container nested under a NON-auto-layout ancestor (e.g. a plain
|
|
6140
|
+
// <main>) — whose own parent still satisfies the old unconditional
|
|
6141
|
+
// "isContainerDropTarget(parent)" check — matched the sibling-insert
|
|
6142
|
+
// branch first and resolved the drop one level too high (anchoring
|
|
6143
|
+
// before/after the hovered container inside ITS parent, instead of
|
|
6144
|
+
// nesting inside the hovered container). Fixed by promoting this
|
|
6145
|
+
// nest-inside check ahead of the sibling-insert fallback, gated on the
|
|
6146
|
+
// parent NOT already being an established auto-layout list (so genuine
|
|
6147
|
+
// flex-item reordering inside an existing list is unaffected).
|
|
6148
|
+
if (
|
|
6149
|
+
cursor !== document.body &&
|
|
6150
|
+
isContainerDropTarget(cursor) &&
|
|
6151
|
+
!(parent && parent !== document.body && isAutoLayoutElement(parent))
|
|
6152
|
+
) {
|
|
6153
|
+
// B5-4 (absolute path): pointer over the container's own
|
|
6154
|
+
// background — its padding or a gap between children. Prefer the
|
|
6155
|
+
// nearest child slot (insertion line at the pointer index) over
|
|
6156
|
+
// plain "inside" (which appends after the last child); fall back
|
|
6157
|
+
// to "inside" only for containers with no visible children.
|
|
5747
6158
|
var betweenContainerChildren = nearestChildInsertionTarget(
|
|
5748
6159
|
cursor,
|
|
5749
6160
|
clientX,
|
|
@@ -5769,6 +6180,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5769
6180
|
conversionTarget: cursor,
|
|
5770
6181
|
};
|
|
5771
6182
|
}
|
|
6183
|
+
if (parent && parent !== document.body && isContainerDropTarget(parent)) {
|
|
6184
|
+
// Anchor-candidate gate: cursor is a plain sibling under `parent`
|
|
6185
|
+
// being used as a before/after anchor — but if it's a template
|
|
6186
|
+
// clone (no counterpart in source HTML), fall back to the nearest
|
|
6187
|
+
// non-clone sibling via nearestChildInsertionTarget, else the
|
|
6188
|
+
// container itself with "inside" placement, exactly like
|
|
6189
|
+
// reorderTargetForPoint's equivalent fallback above.
|
|
6190
|
+
if (isTemplateCloneElement(cursor)) {
|
|
6191
|
+
var cloneFallback = nearestChildInsertionTarget(
|
|
6192
|
+
parent,
|
|
6193
|
+
clientX,
|
|
6194
|
+
clientY,
|
|
6195
|
+
dragged,
|
|
6196
|
+
);
|
|
6197
|
+
if (cloneFallback) {
|
|
6198
|
+
return {
|
|
6199
|
+
anchor: cloneFallback.anchor,
|
|
6200
|
+
placement: cloneFallback.placement,
|
|
6201
|
+
axis: cloneFallback.axis,
|
|
6202
|
+
dropMode: "flow-insert",
|
|
6203
|
+
needsAutoLayoutConversion: !isAutoLayoutElement(parent),
|
|
6204
|
+
conversionTarget: parent,
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
return {
|
|
6208
|
+
anchor: parent,
|
|
6209
|
+
placement: "inside",
|
|
6210
|
+
axis: parentFlowAxis(parent),
|
|
6211
|
+
dropMode: "flow-insert",
|
|
6212
|
+
needsAutoLayoutConversion: !isAutoLayoutElement(parent),
|
|
6213
|
+
conversionTarget: parent,
|
|
6214
|
+
};
|
|
6215
|
+
}
|
|
6216
|
+
var parentAxis = parentFlowAxis(parent);
|
|
6217
|
+
var childRect = cursor.getBoundingClientRect();
|
|
6218
|
+
var childCenter =
|
|
6219
|
+
parentAxis === "x"
|
|
6220
|
+
? childRect.left + childRect.width / 2
|
|
6221
|
+
: childRect.top + childRect.height / 2;
|
|
6222
|
+
var childPointer = parentAxis === "x" ? clientX : clientY;
|
|
6223
|
+
return {
|
|
6224
|
+
anchor: cursor,
|
|
6225
|
+
placement: childPointer < childCenter ? "before" : "after",
|
|
6226
|
+
axis: parentAxis,
|
|
6227
|
+
dropMode: "flow-insert",
|
|
6228
|
+
needsAutoLayoutConversion: !isAutoLayoutElement(parent),
|
|
6229
|
+
conversionTarget: parent,
|
|
6230
|
+
};
|
|
6231
|
+
}
|
|
5772
6232
|
cursor = parent;
|
|
5773
6233
|
}
|
|
5774
6234
|
return null;
|
|
@@ -5824,9 +6284,171 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5824
6284
|
}
|
|
5825
6285
|
}
|
|
5826
6286
|
|
|
6287
|
+
// Absolute-into-flow teleport fix: a flow-insert reparent (the ONLY
|
|
6288
|
+
// dropMode applyRuntimeReorder ever nests an absolute-positioned member
|
|
6289
|
+
// through — "absolute-container" placements keep position:absolute by
|
|
6290
|
+
// design) must strip the leftover position/left/top/right/bottom the
|
|
6291
|
+
// absolute-drag onMove loop wrote onto the element throughout the drag.
|
|
6292
|
+
// Without this the element reparents into the flow container correctly
|
|
6293
|
+
// but stays absolutely positioned at its last drag offset — rendering
|
|
6294
|
+
// hundreds of px away from the slot the insertion guide indicated, since
|
|
6295
|
+
// position:absolute measures from the nearest positioned ancestor, not
|
|
6296
|
+
// flow layout. DesignEditor.tsx does the equivalent strip on the
|
|
6297
|
+
// PERSISTED source string once the host round-trips (see
|
|
6298
|
+
// removeAbsolutePositioningFromNodeInHtml); this mirrors it on the LIVE
|
|
6299
|
+
// runtime DOM so the optimistic in-iframe result is correct immediately,
|
|
6300
|
+
// not just after the host ack.
|
|
6301
|
+
var ABS_POSITION_INLINE_PROPS = [
|
|
6302
|
+
"position",
|
|
6303
|
+
"left",
|
|
6304
|
+
"top",
|
|
6305
|
+
"right",
|
|
6306
|
+
"bottom",
|
|
6307
|
+
];
|
|
6308
|
+
// Snapshot of the inline position/left/top/right/bottom VALUES (not just
|
|
6309
|
+
// whether they existed) taken right before stripAbsolutePositioningForFlowInsert
|
|
6310
|
+
// runs, so a failed move-node round-trip can restore exactly what was
|
|
6311
|
+
// there — including "" for a property that had no inline value at all,
|
|
6312
|
+
// which style.removeProperty already treats correctly as "unset". Reused
|
|
6313
|
+
// by the visual-structure-ack failure branch below to undo the optimistic
|
|
6314
|
+
// strip together with the parent/sibling DOM revert.
|
|
6315
|
+
function snapshotInlinePositionStyles(el: Element): Record<string, string> {
|
|
6316
|
+
var htmlEl = el as HTMLElement;
|
|
6317
|
+
var snapshot: Record<string, string> = {};
|
|
6318
|
+
for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
|
|
6319
|
+
var prop = ABS_POSITION_INLINE_PROPS[i];
|
|
6320
|
+
snapshot[prop] = htmlEl.style.getPropertyValue(prop);
|
|
6321
|
+
}
|
|
6322
|
+
return snapshot;
|
|
6323
|
+
}
|
|
6324
|
+
function restoreInlinePositionStyles(
|
|
6325
|
+
el: Element,
|
|
6326
|
+
snapshot: Record<string, string> | null | undefined,
|
|
6327
|
+
): void {
|
|
6328
|
+
if (!snapshot) return;
|
|
6329
|
+
var htmlEl = el as HTMLElement;
|
|
6330
|
+
for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
|
|
6331
|
+
var prop = ABS_POSITION_INLINE_PROPS[i];
|
|
6332
|
+
var value = snapshot[prop];
|
|
6333
|
+
if (value) {
|
|
6334
|
+
htmlEl.style.setProperty(prop, value);
|
|
6335
|
+
} else {
|
|
6336
|
+
htmlEl.style.removeProperty(prop);
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
// Builds the same snapshot shape as snapshotInlinePositionStyles, but from
|
|
6341
|
+
// a startMove memberState's TRUE pre-drag inline values (captured once at
|
|
6342
|
+
// gesture start — see memberStates below) rather than the live element's
|
|
6343
|
+
// CURRENT inline styles. Used at the startMove/applyGroupStructureDrop
|
|
6344
|
+
// call sites: those paths continuously rewrite the dragged element's
|
|
6345
|
+
// left/top to follow the pointer throughout the free-drag phase, so a
|
|
6346
|
+
// snapshot taken right before the strip would only capture the LAST
|
|
6347
|
+
// dragged-to position, not the position the element should return to when
|
|
6348
|
+
// the whole move-node round-trip is rejected and it goes back to its
|
|
6349
|
+
// original parent.
|
|
6350
|
+
function dragOriginInlinePositionStyles(state: {
|
|
6351
|
+
originalPosition: string;
|
|
6352
|
+
originalLeft: string;
|
|
6353
|
+
originalTop: string;
|
|
6354
|
+
}): Record<string, string> {
|
|
6355
|
+
return {
|
|
6356
|
+
position: state.originalPosition,
|
|
6357
|
+
left: state.originalLeft,
|
|
6358
|
+
top: state.originalTop,
|
|
6359
|
+
right: "",
|
|
6360
|
+
bottom: "",
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
function stripAbsolutePositioningForFlowInsert(el: Element, target): void {
|
|
6364
|
+
if (!target || target.dropMode !== "flow-insert") return;
|
|
6365
|
+
var htmlEl = el as HTMLElement;
|
|
6366
|
+
var cs = window.getComputedStyle(htmlEl);
|
|
6367
|
+
if (cs.position !== "absolute" && cs.position !== "fixed") return;
|
|
6368
|
+
for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
|
|
6369
|
+
htmlEl.style.removeProperty(ABS_POSITION_INLINE_PROPS[i]);
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
// Absolute-container nest rebase: an "absolute-container" drop keeps the
|
|
6374
|
+
// member position:absolute BY DESIGN (no flow-insert strip), but the drag
|
|
6375
|
+
// loop wrote its left/top in the member's ORIGINAL containing-block space
|
|
6376
|
+
// (typically the screen root). After reparenting into the drop container —
|
|
6377
|
+
// itself a positioned element — those same numbers re-resolve against the
|
|
6378
|
+
// NEW containing block, displacing the child by exactly the container's
|
|
6379
|
+
// origin: it renders outside the container's (unclipped) box and visually
|
|
6380
|
+
// "vanishes", and that corrupt geometry then persists. Convert left/top
|
|
6381
|
+
// into the new parent's coordinate space BEFORE the DOM move so (a) the
|
|
6382
|
+
// optimistic in-iframe render is correct immediately and (b)
|
|
6383
|
+
// postVisualStructureChange's sourceRect — measured AFTER the move — now
|
|
6384
|
+
// reflects the true on-screen position, which the host's
|
|
6385
|
+
// absoluteContainerOffset persistence math (sourceRect − anchorRect)
|
|
6386
|
+
// depends on. Delta math (old CB origin − new CB origin) keeps the
|
|
6387
|
+
// member's on-screen position identical through the reparent and stays
|
|
6388
|
+
// exact under margins/rotation, unlike re-deriving from the member's own
|
|
6389
|
+
// (transform-inflated) bounding box.
|
|
6390
|
+
function rebaseAbsoluteMemberForContainerDrop(el, target): void {
|
|
6391
|
+
if (!el || !target || target.dropMode !== "absolute-container") return;
|
|
6392
|
+
var container = dropContainerForTarget(target);
|
|
6393
|
+
if (!container || container === document.body || container === el) return;
|
|
6394
|
+
if (el.contains && el.contains(container)) return;
|
|
6395
|
+
var htmlEl = el as HTMLElement;
|
|
6396
|
+
var cs = window.getComputedStyle(htmlEl);
|
|
6397
|
+
if (cs.position !== "absolute" && cs.position !== "fixed") return;
|
|
6398
|
+
// New containing block origin: the container's padding edge, in client
|
|
6399
|
+
// coordinates (border box + border widths − its own scroll offsets).
|
|
6400
|
+
var containerRect = container.getBoundingClientRect();
|
|
6401
|
+
var containerCS = window.getComputedStyle(container);
|
|
6402
|
+
var newOriginX =
|
|
6403
|
+
containerRect.left +
|
|
6404
|
+
readPx(containerCS.borderLeftWidth) -
|
|
6405
|
+
container.scrollLeft;
|
|
6406
|
+
var newOriginY =
|
|
6407
|
+
containerRect.top +
|
|
6408
|
+
readPx(containerCS.borderTopWidth) -
|
|
6409
|
+
container.scrollTop;
|
|
6410
|
+
// Current containing block origin: the member's offsetParent when it is
|
|
6411
|
+
// a real containing block, else the initial containing block (client
|
|
6412
|
+
// 0,0 minus page scroll). offsetParent falls back to <body> even when
|
|
6413
|
+
// body is NOT positioned/transformed — detect that and use the ICB.
|
|
6414
|
+
var oldOriginX = -(window.scrollX || 0);
|
|
6415
|
+
var oldOriginY = -(window.scrollY || 0);
|
|
6416
|
+
var offsetParent = htmlEl.offsetParent as HTMLElement | null;
|
|
6417
|
+
if (offsetParent && offsetParent !== document.documentElement) {
|
|
6418
|
+
var offsetParentIsRealContainingBlock = true;
|
|
6419
|
+
if (offsetParent === document.body) {
|
|
6420
|
+
var bodyCS = window.getComputedStyle(document.body);
|
|
6421
|
+
offsetParentIsRealContainingBlock =
|
|
6422
|
+
bodyCS.position !== "static" ||
|
|
6423
|
+
bodyCS.transform !== "none" ||
|
|
6424
|
+
(bodyCS.getPropertyValue("translate") || "none") !== "none";
|
|
6425
|
+
}
|
|
6426
|
+
if (offsetParentIsRealContainingBlock) {
|
|
6427
|
+
var opRect = offsetParent.getBoundingClientRect();
|
|
6428
|
+
var opCS = window.getComputedStyle(offsetParent);
|
|
6429
|
+
oldOriginX =
|
|
6430
|
+
opRect.left + readPx(opCS.borderLeftWidth) - offsetParent.scrollLeft;
|
|
6431
|
+
oldOriginY =
|
|
6432
|
+
opRect.top + readPx(opCS.borderTopWidth) - offsetParent.scrollTop;
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
var currentLeft = readPx(htmlEl.style.left || cs.left);
|
|
6436
|
+
var currentTop = readPx(htmlEl.style.top || cs.top);
|
|
6437
|
+
htmlEl.style.left = currentLeft + (oldOriginX - newOriginX) + "px";
|
|
6438
|
+
htmlEl.style.top = currentTop + (oldOriginY - newOriginY) + "px";
|
|
6439
|
+
}
|
|
6440
|
+
|
|
5827
6441
|
function applyRuntimeReorder(el, target) {
|
|
5828
6442
|
if (!el || !target || !target.anchor || !target.anchor.parentElement)
|
|
5829
6443
|
return;
|
|
6444
|
+
stripAbsolutePositioningForFlowInsert(el, target);
|
|
6445
|
+
// Must run BEFORE the DOM move below: the delta math reads the member's
|
|
6446
|
+
// CURRENT containing block via offsetParent. Called here (the single
|
|
6447
|
+
// choke point for drop reparenting) so the single-drag, group-drag,
|
|
6448
|
+
// flow-reorder, and alt-drag-duplicate paths all rebase consistently.
|
|
6449
|
+
// Idempotent for already-nested members (old CB === new CB → delta 0),
|
|
6450
|
+
// so the visual-structure-ack replay path is safe too.
|
|
6451
|
+
rebaseAbsoluteMemberForContainerDrop(el, target);
|
|
5830
6452
|
if (target.placement === "inside") {
|
|
5831
6453
|
target.anchor.appendChild(el);
|
|
5832
6454
|
return;
|
|
@@ -5900,7 +6522,20 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5900
6522
|
// selection to each moved node as it processes each message, so a final
|
|
5901
6523
|
// marquee-selection message restores the full multi-selection afterwards
|
|
5902
6524
|
// (requirement: selection stays intact after a group drop).
|
|
5903
|
-
|
|
6525
|
+
// `originInlineStylesFor` (optional): resolves a member's TRUE pre-drag
|
|
6526
|
+
// position/left/top snapshot (see dragOriginInlinePositionStyles) when the
|
|
6527
|
+
// caller has that gesture-start state available (the startMove auto-layout
|
|
6528
|
+
// branch, whose onMove continuously rewrites left/top to follow the
|
|
6529
|
+
// pointer). Falls back to a live snapshot taken here for the flow-reorder
|
|
6530
|
+
// branch's group call site, which never mutates left/top during its drag
|
|
6531
|
+
// (flow-reorder has no free left/top phase), so the live value IS the
|
|
6532
|
+
// pre-strip/pre-move value there.
|
|
6533
|
+
function applyGroupStructureDrop(
|
|
6534
|
+
members: Element[],
|
|
6535
|
+
target,
|
|
6536
|
+
ev,
|
|
6537
|
+
originInlineStylesFor?: (member: Element) => Record<string, string>,
|
|
6538
|
+
): void {
|
|
5904
6539
|
var container = dropContainerForTarget(target);
|
|
5905
6540
|
var previous: Element | null = null;
|
|
5906
6541
|
for (var i = 0; i < members.length; i += 1) {
|
|
@@ -5918,6 +6553,13 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5918
6553
|
};
|
|
5919
6554
|
var prevParent = member.parentElement;
|
|
5920
6555
|
var prevNextSibling = member.nextSibling;
|
|
6556
|
+
// Captured BEFORE applyRuntimeReorder so a rejected move-node
|
|
6557
|
+
// round-trip can restore the exact pre-strip inline values (see
|
|
6558
|
+
// snapshotInlinePositionStyles / dragOriginInlinePositionStyles doc
|
|
6559
|
+
// comments).
|
|
6560
|
+
var prevInlinePositionStyles = originInlineStylesFor
|
|
6561
|
+
? originInlineStylesFor(member)
|
|
6562
|
+
: snapshotInlinePositionStyles(member);
|
|
5921
6563
|
// Board-text auto-color: adapt before the DOM move so the re-parent
|
|
5922
6564
|
// check sees the ORIGINAL parent (see adaptAutoTextColorForNest).
|
|
5923
6565
|
adaptAutoTextColorForNest(member, container);
|
|
@@ -5925,6 +6567,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5925
6567
|
postVisualStructureChange(member, memberTarget, {
|
|
5926
6568
|
prevParent: prevParent,
|
|
5927
6569
|
prevNextSibling: prevNextSibling,
|
|
6570
|
+
prevInlinePositionStyles: prevInlinePositionStyles,
|
|
5928
6571
|
});
|
|
5929
6572
|
previous = member;
|
|
5930
6573
|
}
|
|
@@ -6151,6 +6794,72 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6151
6794
|
// move one element, which would tear the group apart.
|
|
6152
6795
|
postCrossScreenDrag("cancel");
|
|
6153
6796
|
}
|
|
6797
|
+
// Template-clone reorder rejection (CRITICAL fix): a runtime clone of an
|
|
6798
|
+
// Alpine `<template x-for>` item has no counterpart in the static source
|
|
6799
|
+
// HTML the host resolves structural moves against (only the single
|
|
6800
|
+
// template child exists there), so a reorder/reparent targeting one can
|
|
6801
|
+
// never succeed — the old behavior optimistically reordered the live DOM
|
|
6802
|
+
// then silently reverted it ~1 frame later with zero feedback. Reject up
|
|
6803
|
+
// front instead: no DOM mutation, no doomed host round-trip, clear
|
|
6804
|
+
// "can't reorder" cursor + badge feedback for the whole gesture. Scoped
|
|
6805
|
+
// to single-element, non-duplicate drags of a flow-reorder candidate —
|
|
6806
|
+
// group drags and alt-duplicates build a real, source-backed clone/copy
|
|
6807
|
+
// first (resetRuntimeStableIds), so they are unaffected.
|
|
6808
|
+
if (
|
|
6809
|
+
!isGroupDrag &&
|
|
6810
|
+
!duplicatedForDrag &&
|
|
6811
|
+
isFlowReorderCandidate(gestureEl) &&
|
|
6812
|
+
isTemplateCloneElement(gestureEl)
|
|
6813
|
+
) {
|
|
6814
|
+
postCrossScreenDrag("cancel");
|
|
6815
|
+
var rejectedEl = gestureEl;
|
|
6816
|
+
function onRejectedMove(ev) {
|
|
6817
|
+
showRejectedDragBadge(
|
|
6818
|
+
"Can't reorder repeated items",
|
|
6819
|
+
ev.clientX,
|
|
6820
|
+
ev.clientY,
|
|
6821
|
+
);
|
|
6822
|
+
}
|
|
6823
|
+
function cleanupRejectedDrag() {
|
|
6824
|
+
document.removeEventListener(events.move, onRejectedMove, true);
|
|
6825
|
+
document.removeEventListener(events.up, onRejectedUp, true);
|
|
6826
|
+
document.removeEventListener("keydown", onRejectedKeyDown, true);
|
|
6827
|
+
clearActiveDragCancel(onRejectedEscape);
|
|
6828
|
+
shieldOverlay.style.cursor = "default";
|
|
6829
|
+
}
|
|
6830
|
+
function onRejectedEscape() {
|
|
6831
|
+
cleanupRejectedDrag();
|
|
6832
|
+
hideTransformBadge();
|
|
6833
|
+
suppressNextShieldClickBriefly();
|
|
6834
|
+
return true;
|
|
6835
|
+
}
|
|
6836
|
+
function onRejectedKeyDown(ev) {
|
|
6837
|
+
if (ev.key === "Escape") {
|
|
6838
|
+
stopNativeInteraction(ev);
|
|
6839
|
+
onRejectedEscape();
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
function onRejectedUp() {
|
|
6843
|
+
cleanupRejectedDrag();
|
|
6844
|
+
hideTransformBadge();
|
|
6845
|
+
selectTargetAfterRejectedDrag();
|
|
6846
|
+
}
|
|
6847
|
+
function selectTargetAfterRejectedDrag(): void {
|
|
6848
|
+
selectedEl = rejectedEl;
|
|
6849
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
6850
|
+
}
|
|
6851
|
+
shieldOverlay.style.cursor = "not-allowed";
|
|
6852
|
+
showRejectedDragBadge(
|
|
6853
|
+
"Can't reorder repeated items",
|
|
6854
|
+
e.clientX,
|
|
6855
|
+
e.clientY,
|
|
6856
|
+
);
|
|
6857
|
+
document.addEventListener(events.move, onRejectedMove, true);
|
|
6858
|
+
document.addEventListener(events.up, onRejectedUp, true);
|
|
6859
|
+
document.addEventListener("keydown", onRejectedKeyDown, true);
|
|
6860
|
+
setActiveDragCancel(onRejectedEscape);
|
|
6861
|
+
return;
|
|
6862
|
+
}
|
|
6154
6863
|
if (isFlowReorderCandidate(gestureEl)) {
|
|
6155
6864
|
// Snapshot the element being reordered so a concurrent select-element or
|
|
6156
6865
|
// clear-selection postMessage cannot mutate the wrong element mid-drag.
|
|
@@ -6318,6 +7027,13 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6318
7027
|
// reports applied===false on the structure-ack.
|
|
6319
7028
|
var prevParent = reorderEl.parentElement;
|
|
6320
7029
|
var prevNextSibling = reorderEl.nextSibling;
|
|
7030
|
+
// Usually a no-op here (flow-reorder drags a member that's
|
|
7031
|
+
// already in flow, so there's nothing to strip), but captured for
|
|
7032
|
+
// consistency so an absolute-positioned element reordered through
|
|
7033
|
+
// this gesture still rolls back its inline styles correctly on a
|
|
7034
|
+
// rejected move-node round-trip.
|
|
7035
|
+
var prevInlinePositionStyles =
|
|
7036
|
+
snapshotInlinePositionStyles(reorderEl);
|
|
6321
7037
|
adaptAutoTextColorForNest(
|
|
6322
7038
|
reorderEl,
|
|
6323
7039
|
dropContainerForTarget(currentTarget),
|
|
@@ -6329,6 +7045,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6329
7045
|
postVisualStructureChange(reorderEl, currentTarget, {
|
|
6330
7046
|
prevParent: prevParent,
|
|
6331
7047
|
prevNextSibling: prevNextSibling,
|
|
7048
|
+
prevInlinePositionStyles: prevInlinePositionStyles,
|
|
6332
7049
|
});
|
|
6333
7050
|
}
|
|
6334
7051
|
}
|
|
@@ -6606,10 +7323,31 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6606
7323
|
);
|
|
6607
7324
|
}
|
|
6608
7325
|
if (isGroupDrag) {
|
|
6609
|
-
applyGroupStructureDrop(
|
|
7326
|
+
applyGroupStructureDrop(
|
|
7327
|
+
groupEls,
|
|
7328
|
+
currentAutoLayoutTarget,
|
|
7329
|
+
ev,
|
|
7330
|
+
function (member) {
|
|
7331
|
+
var state = memberStates.filter(function (s) {
|
|
7332
|
+
return s.el === member;
|
|
7333
|
+
})[0];
|
|
7334
|
+
return state
|
|
7335
|
+
? dragOriginInlinePositionStyles(state)
|
|
7336
|
+
: snapshotInlinePositionStyles(member);
|
|
7337
|
+
},
|
|
7338
|
+
);
|
|
6610
7339
|
} else {
|
|
6611
7340
|
var prevParent = dragEl.parentElement;
|
|
6612
7341
|
var prevNextSibling = dragEl.nextSibling;
|
|
7342
|
+
// The element's TRUE pre-drag inline position/left/top (gestureState
|
|
7343
|
+
// is captured once at drag start, before onMove's continuous
|
|
7344
|
+
// pointer-follow rewrites left/top) — not a snapshot taken here,
|
|
7345
|
+
// which would only capture the LAST dragged-to position. On a
|
|
7346
|
+
// rejected move-node round-trip the element goes back to its
|
|
7347
|
+
// ORIGINAL parent, so it must also go back to the position it had
|
|
7348
|
+
// in that original parent, not a mid-drag coordinate.
|
|
7349
|
+
var prevInlinePositionStyles =
|
|
7350
|
+
dragOriginInlinePositionStyles(gestureState);
|
|
6613
7351
|
adaptAutoTextColorForNest(
|
|
6614
7352
|
dragEl,
|
|
6615
7353
|
dropContainerForTarget(currentAutoLayoutTarget),
|
|
@@ -6618,6 +7356,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6618
7356
|
postVisualStructureChange(dragEl, currentAutoLayoutTarget, {
|
|
6619
7357
|
prevParent: prevParent,
|
|
6620
7358
|
prevNextSibling: prevNextSibling,
|
|
7359
|
+
prevInlinePositionStyles: prevInlinePositionStyles,
|
|
6621
7360
|
});
|
|
6622
7361
|
}
|
|
6623
7362
|
} else {
|
|
@@ -6984,7 +7723,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
6984
7723
|
|
|
6985
7724
|
function beginPotentialShieldDrag(e) {
|
|
6986
7725
|
stopNativeInteraction(e);
|
|
6987
|
-
if (e.button !== 0
|
|
7726
|
+
if (e.button !== 0) return;
|
|
7727
|
+
// T23: a stale session self-heals and the drag proceeds; only a LIVE
|
|
7728
|
+
// session (connected element) blocks shield drags.
|
|
7729
|
+
if (activeTextEditEl && !exitStaleTextEditSession()) return;
|
|
6988
7730
|
var events = dragEventNames(e);
|
|
6989
7731
|
var hit = elementFromEditorPoint(e.clientX, e.clientY);
|
|
6990
7732
|
var hitTarget = selectionTargetForHit(hit);
|
|
@@ -7193,6 +7935,106 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7193
7935
|
document.addEventListener(
|
|
7194
7936
|
"keydown",
|
|
7195
7937
|
function (e) {
|
|
7938
|
+
// T25: pending-window keydown routing — a begin-text-edit is still
|
|
7939
|
+
// waiting for its node. Keystrokes that land in THIS document during
|
|
7940
|
+
// the wait belong to the upcoming text session: buffer printable
|
|
7941
|
+
// characters (replayed on activation), let Backspace edit the buffer,
|
|
7942
|
+
// and swallow Delete/Enter/Tab/arrows so they can never be forwarded
|
|
7943
|
+
// into host layer-deletion/navigation. IME composition and Cmd/Ctrl
|
|
7944
|
+
// chords pass through untouched.
|
|
7945
|
+
if (!activeTextEditEl && pendingBeginTextEdit) {
|
|
7946
|
+
if (!(e.isComposing || e.keyCode === 229) && !e.metaKey && !e.ctrlKey) {
|
|
7947
|
+
var pendingKey = e.key || "";
|
|
7948
|
+
if (pendingKey === "Escape") {
|
|
7949
|
+
var abandonedPendingNodeId = pendingBeginTextEdit.nodeId;
|
|
7950
|
+
cancelPendingBeginTextEdit();
|
|
7951
|
+
postTextEditPending(abandonedPendingNodeId, false);
|
|
7952
|
+
stopNativeInteraction(e);
|
|
7953
|
+
return;
|
|
7954
|
+
}
|
|
7955
|
+
if (pendingKey === "Backspace") {
|
|
7956
|
+
pendingBeginTextEdit.buffer = pendingBeginTextEdit.buffer.slice(
|
|
7957
|
+
0,
|
|
7958
|
+
-1,
|
|
7959
|
+
);
|
|
7960
|
+
stopNativeInteraction(e);
|
|
7961
|
+
return;
|
|
7962
|
+
}
|
|
7963
|
+
if (pendingKey.length === 1) {
|
|
7964
|
+
pendingBeginTextEdit.buffer += pendingKey;
|
|
7965
|
+
stopNativeInteraction(e);
|
|
7966
|
+
return;
|
|
7967
|
+
}
|
|
7968
|
+
if (
|
|
7969
|
+
pendingKey === "Delete" ||
|
|
7970
|
+
pendingKey === "Enter" ||
|
|
7971
|
+
pendingKey === "Tab" ||
|
|
7972
|
+
pendingKey.indexOf("Arrow") === 0
|
|
7973
|
+
) {
|
|
7974
|
+
stopNativeInteraction(e);
|
|
7975
|
+
return;
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
}
|
|
7979
|
+
// T23/T24: text-edit keydown routing runs BEFORE hotkey forwarding so
|
|
7980
|
+
// a nominally-active session can never lose keys to host shortcuts.
|
|
7981
|
+
if (activeTextEditEl) {
|
|
7982
|
+
if (exitStaleTextEditSession()) {
|
|
7983
|
+
// The session was stale (element detached by a patch). Swallow
|
|
7984
|
+
// this keystroke entirely — letting it fall through in the same
|
|
7985
|
+
// event would forward Delete/Backspace straight into host
|
|
7986
|
+
// layer-deletion while the user believes they are typing text.
|
|
7987
|
+
stopNativeInteraction(e);
|
|
7988
|
+
return;
|
|
7989
|
+
}
|
|
7990
|
+
// Respect IME composition exactly like the session's own onKeyDown.
|
|
7991
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
7992
|
+
var activeNow = document.activeElement;
|
|
7993
|
+
var focusInsideEdit = !!(
|
|
7994
|
+
activeNow &&
|
|
7995
|
+
(activeNow === activeTextEditEl ||
|
|
7996
|
+
activeTextEditEl.contains(activeNow))
|
|
7997
|
+
);
|
|
7998
|
+
// T24: Escape must ALWAYS exit the session deterministically, even
|
|
7999
|
+
// when focus fell outside the editable (where the session's own
|
|
8000
|
+
// target-scoped keydown listener can never fire).
|
|
8001
|
+
if (e.key === "Escape") {
|
|
8002
|
+
if (!focusInsideEdit) {
|
|
8003
|
+
stopNativeInteraction(e);
|
|
8004
|
+
if (finishActiveTextEdit) finishActiveTextEdit(true);
|
|
8005
|
+
return;
|
|
8006
|
+
}
|
|
8007
|
+
// Focus is inside: fall through — the session's own capture
|
|
8008
|
+
// onKeyDown on the target handles Escape (commit + blur) next.
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
8011
|
+
if (!focusInsideEdit) {
|
|
8012
|
+
// Race window: the session is active but focus sits elsewhere
|
|
8013
|
+
// (creation focus race, transient focus steal). If the user is
|
|
8014
|
+
// legitimately typing in a real form control, leave it alone;
|
|
8015
|
+
// otherwise pull focus back into the editable so the keystroke
|
|
8016
|
+
// lands as text — and never reaches host shortcuts.
|
|
8017
|
+
if (!isEditorTypingTarget(activeNow)) {
|
|
8018
|
+
try {
|
|
8019
|
+
activeTextEditEl.focus();
|
|
8020
|
+
var refocusRange = document.createRange();
|
|
8021
|
+
refocusRange.selectNodeContents(activeTextEditEl);
|
|
8022
|
+
refocusRange.collapse(false);
|
|
8023
|
+
var refocusSelection = window.getSelection();
|
|
8024
|
+
if (refocusSelection) {
|
|
8025
|
+
refocusSelection.removeAllRanges();
|
|
8026
|
+
refocusSelection.addRange(refocusRange);
|
|
8027
|
+
}
|
|
8028
|
+
} catch (_err) {
|
|
8029
|
+
/* focus/selection APIs unavailable — key is still swallowed */
|
|
8030
|
+
}
|
|
8031
|
+
e.stopPropagation();
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
// While a live session exists, never forward hotkeys to the host
|
|
8035
|
+
// (matches shouldForwardDesignHotkey's activeTextEditEl guard).
|
|
8036
|
+
return;
|
|
8037
|
+
}
|
|
7196
8038
|
if (!shouldForwardDesignHotkey(e)) return;
|
|
7197
8039
|
var key = e.key;
|
|
7198
8040
|
var normalized = key && key.length === 1 ? key.toLowerCase() : key;
|
|
@@ -7248,6 +8090,53 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7248
8090
|
true,
|
|
7249
8091
|
);
|
|
7250
8092
|
|
|
8093
|
+
// T23/T24: pointerdown-level text-edit session hygiene. Runs on DOCUMENT
|
|
8094
|
+
// capture (not the shield) because an active session sets the shield to
|
|
8095
|
+
// pointer-events:none — and a LEAKED session leaves it that way, so shield
|
|
8096
|
+
// handlers can never observe the pointerdown that should recover from it.
|
|
8097
|
+
// 1. A pointerdown means the user moved on: drop any deferred
|
|
8098
|
+
// begin-text-edit command so it can't yank focus later.
|
|
8099
|
+
// 2. A stale (detached-element) session self-heals on the next click.
|
|
8100
|
+
// 3. Click-away must exit the session even when the editable is NOT
|
|
8101
|
+
// focused (the blur-based commit can never fire in that state).
|
|
8102
|
+
document.addEventListener(
|
|
8103
|
+
"pointerdown",
|
|
8104
|
+
function (e) {
|
|
8105
|
+
if (pendingBeginTextEdit) {
|
|
8106
|
+
var canceledPendingNodeId = pendingBeginTextEdit.nodeId;
|
|
8107
|
+
cancelPendingBeginTextEdit();
|
|
8108
|
+
postTextEditPending(canceledPendingNodeId, false);
|
|
8109
|
+
}
|
|
8110
|
+
if (!activeTextEditEl) return;
|
|
8111
|
+
if (exitStaleTextEditSession()) return;
|
|
8112
|
+
var pointerTarget =
|
|
8113
|
+
e.target && (e.target as Element).nodeType === 1
|
|
8114
|
+
? (e.target as Element)
|
|
8115
|
+
: null;
|
|
8116
|
+
if (
|
|
8117
|
+
pointerTarget &&
|
|
8118
|
+
(pointerTarget === activeTextEditEl ||
|
|
8119
|
+
activeTextEditEl.contains(pointerTarget))
|
|
8120
|
+
) {
|
|
8121
|
+
return;
|
|
8122
|
+
}
|
|
8123
|
+
// Editor chrome (overlays) never hosts text content — clicking it
|
|
8124
|
+
// shouldn't force-commit here; the session's own blur handling decides.
|
|
8125
|
+
if (pointerTarget && isOverlayElement(pointerTarget)) return;
|
|
8126
|
+
// A real user pointerdown outside the editable is a deterministic
|
|
8127
|
+
// click-away: commit and exit NOW. This covers both broken states the
|
|
8128
|
+
// blur path can't reach — (a) focus already fell outside the editable
|
|
8129
|
+
// (blur will never fire), and (b) the programmatic empty-text session,
|
|
8130
|
+
// whose blur handler deliberately re-focuses on transient focus steals
|
|
8131
|
+
// but must NOT fight a real click elsewhere. For a healthy focused
|
|
8132
|
+
// session this simply commits a few ms before blur would have.
|
|
8133
|
+
if (finishActiveTextEdit) {
|
|
8134
|
+
finishActiveTextEdit(true);
|
|
8135
|
+
}
|
|
8136
|
+
},
|
|
8137
|
+
true,
|
|
8138
|
+
);
|
|
8139
|
+
|
|
7251
8140
|
function hasFigmaClipboardPayload(value) {
|
|
7252
8141
|
return /<[^>]+\sdata-(metadata|buffer)=["'][^"']*\((figmeta|figma)\)[^"']*["']/i.test(
|
|
7253
8142
|
String(value || ""),
|
|
@@ -7338,6 +8227,23 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7338
8227
|
return;
|
|
7339
8228
|
}
|
|
7340
8229
|
stopNativeInteraction(e);
|
|
8230
|
+
// A new edit supersedes any deferred begin-text-edit command. This is a
|
|
8231
|
+
// USER-initiated path (dblclick) whenever pending is still set here —
|
|
8232
|
+
// the programmatic activation paths clear pending BEFORE calling in —
|
|
8233
|
+
// so also stand the host keystroke buffer down: it must never flush
|
|
8234
|
+
// into this unrelated session.
|
|
8235
|
+
if (pendingBeginTextEdit) {
|
|
8236
|
+
var supersededPendingNodeId = pendingBeginTextEdit.nodeId;
|
|
8237
|
+
cancelPendingBeginTextEdit();
|
|
8238
|
+
postTextEditPending(supersededPendingNodeId, false);
|
|
8239
|
+
}
|
|
8240
|
+
// T23: a live session on a DIFFERENT element must end through the
|
|
8241
|
+
// canonical cleanup BEFORE the new one starts. Previously the new
|
|
8242
|
+
// session simply overwrote activeTextEditEl/finishActiveTextEdit, and
|
|
8243
|
+
// the old session's eventual blur-driven finish() then restored the
|
|
8244
|
+
// shield pointer-passthrough state and posted text-editing-state(false)
|
|
8245
|
+
// UNDERNEATH the new session, corrupting both.
|
|
8246
|
+
if (activeTextEditEl && finishActiveTextEdit) finishActiveTextEdit(true);
|
|
7341
8247
|
var eventTarget =
|
|
7342
8248
|
e && e.target && e.target.nodeType === 1 ? e.target : null;
|
|
7343
8249
|
// The raw `eventTarget` fallback (no findTextEditTarget resolution at
|
|
@@ -7501,7 +8407,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7501
8407
|
var next = target.textContent || "";
|
|
7502
8408
|
var nextHtml = target.innerHTML || "";
|
|
7503
8409
|
refreshOverlays();
|
|
7504
|
-
|
|
8410
|
+
// T23: never post a content change from a DETACHED node — a document
|
|
8411
|
+
// patch already replaced it, so the in-document copy is the source of
|
|
8412
|
+
// truth and a selector computed from the orphan would target the wrong
|
|
8413
|
+
// (or no) element host-side.
|
|
8414
|
+
if (
|
|
8415
|
+
target.isConnected &&
|
|
8416
|
+
(next !== originalText || nextHtml !== originalHtml)
|
|
8417
|
+
) {
|
|
7505
8418
|
postTextContentChange(
|
|
7506
8419
|
target,
|
|
7507
8420
|
next,
|
|
@@ -7632,6 +8545,88 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7632
8545
|
}
|
|
7633
8546
|
}
|
|
7634
8547
|
|
|
8548
|
+
// T22: shared programmatic activation used by the begin-text-edit message
|
|
8549
|
+
// handler — both for an immediately-resolvable node and for one that lands
|
|
8550
|
+
// later via the deferred-retry window below.
|
|
8551
|
+
function queryBeginTextEditNode(nodeId: string): HTMLElement | null {
|
|
8552
|
+
var node: Element | null = document.querySelector(
|
|
8553
|
+
'[data-agent-native-node-id="' +
|
|
8554
|
+
nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
|
|
8555
|
+
'"]',
|
|
8556
|
+
);
|
|
8557
|
+
return node && node.nodeType === 1 ? (node as HTMLElement) : null;
|
|
8558
|
+
}
|
|
8559
|
+
function activateProgrammaticTextEdit(
|
|
8560
|
+
textTarget: HTMLElement,
|
|
8561
|
+
force: boolean,
|
|
8562
|
+
): void {
|
|
8563
|
+
// If we are already editing this element, do nothing.
|
|
8564
|
+
if (activeTextEditEl && activeTextEditEl === textTarget) return;
|
|
8565
|
+
// Synthesise coordinates at the end of the element content so the caret
|
|
8566
|
+
// lands at the insertion point (right after any placeholder text).
|
|
8567
|
+
var bteRect = textTarget.getBoundingClientRect();
|
|
8568
|
+
var bteCenterX = bteRect.right - 2;
|
|
8569
|
+
var bteCenterY = bteRect.top + bteRect.height / 2;
|
|
8570
|
+
// Delegate to the canonical path so all state, events, and postMessages
|
|
8571
|
+
// stay consistent with a normal double-click text edit.
|
|
8572
|
+
beginTextEditingFromEvent(
|
|
8573
|
+
{
|
|
8574
|
+
clientX: bteCenterX,
|
|
8575
|
+
clientY: bteCenterY,
|
|
8576
|
+
target: textTarget,
|
|
8577
|
+
agentNativeProgrammaticTextEdit: true,
|
|
8578
|
+
preventDefault: function () {},
|
|
8579
|
+
stopPropagation: function () {},
|
|
8580
|
+
stopImmediatePropagation: function () {},
|
|
8581
|
+
} as unknown as MouseEvent,
|
|
8582
|
+
force,
|
|
8583
|
+
);
|
|
8584
|
+
}
|
|
8585
|
+
function pumpPendingBeginTextEdit(): void {
|
|
8586
|
+
if (!pendingBeginTextEdit) return;
|
|
8587
|
+
var entry = pendingBeginTextEdit;
|
|
8588
|
+
var node = queryBeginTextEditNode(entry.nodeId);
|
|
8589
|
+
if (node) {
|
|
8590
|
+
pendingBeginTextEdit = null;
|
|
8591
|
+
// The user may have started their own edit meanwhile — never steal it.
|
|
8592
|
+
if (!activeTextEditEl) {
|
|
8593
|
+
activateProgrammaticTextEdit(node, entry.force);
|
|
8594
|
+
// Replay keystrokes typed into this iframe during the wait — the
|
|
8595
|
+
// session is focused with the caret at the content end, so this
|
|
8596
|
+
// lands exactly where the user expects their first characters.
|
|
8597
|
+
if (entry.buffer && activeTextEditEl === node) {
|
|
8598
|
+
insertPlainTextAtSelection(entry.buffer);
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
if (Date.now() > entry.deadline) {
|
|
8604
|
+
pendingBeginTextEdit = null;
|
|
8605
|
+
postTextEditPending(entry.nodeId, false);
|
|
8606
|
+
return;
|
|
8607
|
+
}
|
|
8608
|
+
entry.raf = window.requestAnimationFrame(pumpPendingBeginTextEdit);
|
|
8609
|
+
}
|
|
8610
|
+
function scheduleBeginTextEditRetry(nodeId: string, force: boolean): void {
|
|
8611
|
+
// DesignEditor's own T6 loop re-posts begin-text-edit for the SAME node
|
|
8612
|
+
// every few hundred ms until it activates — those re-posts must extend
|
|
8613
|
+
// the wait, not reset it (a reset would drop keystrokes already buffered
|
|
8614
|
+
// for this node).
|
|
8615
|
+
if (pendingBeginTextEdit && pendingBeginTextEdit.nodeId === nodeId) {
|
|
8616
|
+
pendingBeginTextEdit.force = pendingBeginTextEdit.force || force;
|
|
8617
|
+
pendingBeginTextEdit.deadline = Date.now() + 2000;
|
|
8618
|
+
return;
|
|
8619
|
+
}
|
|
8620
|
+
cancelPendingBeginTextEdit();
|
|
8621
|
+
pendingBeginTextEdit = {
|
|
8622
|
+
nodeId: nodeId,
|
|
8623
|
+
force: force,
|
|
8624
|
+
deadline: Date.now() + 2000,
|
|
8625
|
+
raf: window.requestAnimationFrame(pumpPendingBeginTextEdit),
|
|
8626
|
+
buffer: "",
|
|
8627
|
+
};
|
|
8628
|
+
}
|
|
8629
|
+
|
|
7635
8630
|
shieldOverlay.addEventListener("dblclick", beginTextEditingFromEvent, true);
|
|
7636
8631
|
selectionOverlay.addEventListener(
|
|
7637
8632
|
"dblclick",
|
|
@@ -7826,40 +8821,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
7826
8821
|
var nodeId: string =
|
|
7827
8822
|
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
7828
8823
|
if (!nodeId) return;
|
|
7829
|
-
var nodeTarget: Element | null = document.querySelector(
|
|
7830
|
-
'[data-agent-native-node-id="' +
|
|
7831
|
-
nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
|
|
7832
|
-
'"]',
|
|
7833
|
-
);
|
|
7834
|
-
if (!nodeTarget || nodeTarget.nodeType !== 1) return;
|
|
7835
8824
|
// Edit the EXACT node identified by nodeId. Do NOT run it through
|
|
7836
8825
|
// findTextEditTarget here — that helper climbs UP to the highest
|
|
7837
8826
|
// inline-editable ancestor, which for a text node inside a text-heavy
|
|
7838
8827
|
// screen resolves all the way to <main>, putting the ENTIRE screen into
|
|
7839
8828
|
// edit mode instead of this node (keystrokes land in the wrong element).
|
|
7840
|
-
var textTarget
|
|
7841
|
-
if (!textTarget
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
8829
|
+
var textTarget = queryBeginTextEditNode(nodeId);
|
|
8830
|
+
if (!textTarget) {
|
|
8831
|
+
// T22: the node hasn't landed in this document yet — the command won
|
|
8832
|
+
// the race against the replace-document-content round trip that
|
|
8833
|
+
// carries the freshly-created element. Defer instead of dropping,
|
|
8834
|
+
// so the caret is live the moment the node appears. Tell the host so
|
|
8835
|
+
// it buffers HOST-focused keystrokes for the same window (T25).
|
|
8836
|
+
scheduleBeginTextEditRetry(nodeId, forceBeginTextEdit);
|
|
8837
|
+
postTextEditPending(nodeId, true);
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
cancelPendingBeginTextEdit();
|
|
8841
|
+
activateProgrammaticTextEdit(textTarget, forceBeginTextEdit);
|
|
8842
|
+
return;
|
|
8843
|
+
}
|
|
8844
|
+
// T25: replay keystrokes the HOST buffered during the creation→activation
|
|
8845
|
+
// race window (DesignCanvas suppresses host shortcuts and stashes
|
|
8846
|
+
// printable keys while its begin-text-edit is pending, then flushes them
|
|
8847
|
+
// here once the session reports active). Inserted at the caret through
|
|
8848
|
+
// the same execCommand path paste uses, so the session's own input
|
|
8849
|
+
// listener updates chrome/state naturally.
|
|
8850
|
+
if (e.data.type === "text-edit-insert-text") {
|
|
8851
|
+
var bufferedText = typeof e.data.text === "string" ? e.data.text : "";
|
|
8852
|
+
if (!bufferedText || !activeTextEditEl || !isTextEditElConnected())
|
|
8853
|
+
return;
|
|
8854
|
+
var bufferedActive = document.activeElement;
|
|
8855
|
+
if (
|
|
8856
|
+
!bufferedActive ||
|
|
8857
|
+
(bufferedActive !== activeTextEditEl &&
|
|
8858
|
+
!activeTextEditEl.contains(bufferedActive))
|
|
8859
|
+
) {
|
|
8860
|
+
try {
|
|
8861
|
+
activeTextEditEl.focus();
|
|
8862
|
+
var bufferedRange = document.createRange();
|
|
8863
|
+
bufferedRange.selectNodeContents(activeTextEditEl);
|
|
8864
|
+
bufferedRange.collapse(false);
|
|
8865
|
+
var bufferedSelection = window.getSelection();
|
|
8866
|
+
if (bufferedSelection) {
|
|
8867
|
+
bufferedSelection.removeAllRanges();
|
|
8868
|
+
bufferedSelection.addRange(bufferedRange);
|
|
8869
|
+
}
|
|
8870
|
+
} catch (_err) {}
|
|
8871
|
+
}
|
|
8872
|
+
insertPlainTextAtSelection(bufferedText);
|
|
7863
8873
|
return;
|
|
7864
8874
|
}
|
|
7865
8875
|
if (e.data.type === "set-editor-chrome-scale") {
|
|
@@ -8192,7 +9202,15 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
8192
9202
|
postElementSelect(selectedEl);
|
|
8193
9203
|
}
|
|
8194
9204
|
} else {
|
|
8195
|
-
// Revert the optimistic reorder to its pre-drag position
|
|
9205
|
+
// Revert the optimistic reorder to its pre-drag position AND
|
|
9206
|
+
// restore any inline position/left/top/right/bottom the optimistic
|
|
9207
|
+
// reorder stripped (stripAbsolutePositioningForFlowInsert runs
|
|
9208
|
+
// inside applyRuntimeReorder for flow-insert drops). Without this
|
|
9209
|
+
// second half a rejected move-node round-trip left the element
|
|
9210
|
+
// re-parented back to its original container but permanently
|
|
9211
|
+
// stripped of its absolute positioning — worse than doing nothing,
|
|
9212
|
+
// since it now renders at the flow position of a detached style
|
|
9213
|
+
// instead of either its original spot or the intended drop slot.
|
|
8196
9214
|
if (
|
|
8197
9215
|
move.el &&
|
|
8198
9216
|
move.el.isConnected &&
|
|
@@ -8204,6 +9222,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
8204
9222
|
move.el,
|
|
8205
9223
|
move.origin.prevNextSibling,
|
|
8206
9224
|
);
|
|
9225
|
+
restoreInlinePositionStyles(
|
|
9226
|
+
move.el,
|
|
9227
|
+
move.origin.prevInlinePositionStyles,
|
|
9228
|
+
);
|
|
8207
9229
|
selectedEl = move.el;
|
|
8208
9230
|
positionOverlay(selectionOverlay, selectedEl);
|
|
8209
9231
|
postElementSelect(selectedEl);
|