fix(builder): duplicate inserts+selects after source; select-parent/styles-sheet/canvas-pad mobile fixes
Ship-blocking fix (Fable consult): useNodeActions.duplicate() appended the regenerated tree at the end of the parent while leaving the ORIGINAL selected, so duplicating a top-level section landed the copy off-screen at the bottom of the page with no visible change -- shared by both the mobile selection toolbar and the desktop right-click ContextMenu. Now inserts the copy immediately after the source (actions.addNodeTree(tree, parentId, sourceIndex + 1)) and selects it (actions.selectNode(tree.rootNodeId)), falling back to append-at-end if the source's index can't be resolved. Mobile also scrolls the new node into view. Three cheap fast-follows: - canSelectParent on useNodeActions (false when the node's parent is ROOT or missing); MobileSelectionToolbar disables "Select Parent" instead of dead-ending on a page-wide ROOT outline with no toolbar of its own. - Opening the Styles sheet on mobile now scrolls the selected node above the 65dvh sheet; a temporary generous bottom-padding class handles the case where the node is the last thing on the page and there'd otherwise be no room left to scroll it into view. - Canvas gets bottom padding equal to the fixed selection toolbar's height while it's visible, so the last section of a short page isn't stuck permanently underneath it. Desktop duplicate behavior improves (inserts after + selects) via the shared hook; no toHtml changes. Verified live via Playwright at 375px and 1280px (screenshots in craft/scratchpad/mobileB2/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,12 @@ import { findDeletableTarget } from '../utils/craft-helpers';
|
||||
export interface NodeActions {
|
||||
moveUp: () => void;
|
||||
moveDown: () => void;
|
||||
duplicate: () => void;
|
||||
/** Duplicates `nodeId`, inserting the copy immediately after the source in
|
||||
* the parent's children and selecting it. Returns the new node's id (so
|
||||
* callers -- e.g. `MobileSelectionToolbar` -- can scroll it into view),
|
||||
* or null if the duplicate could not be performed (no-op for ROOT/null,
|
||||
* or a caught error). */
|
||||
duplicate: () => string | null;
|
||||
deleteNode: () => void;
|
||||
selectParent: () => void;
|
||||
/** True if `nodeId` has an earlier sibling under the same parent (so
|
||||
@@ -18,6 +23,12 @@ export interface NodeActions {
|
||||
* `nodeId` itself, or an ancestor when `nodeId` is an empty linked-node
|
||||
* slot whose siblings are all also empty -- see `craft-helpers.ts`). */
|
||||
canDelete: boolean;
|
||||
/** True if `nodeId` has a real parent to select -- i.e. the parent is
|
||||
* neither ROOT nor missing. `selectParent` on a top-level section (whose
|
||||
* parent IS 'ROOT') would only select the page-wide ROOT node, which has
|
||||
* no on-canvas outline and no toolbar of its own -- a dead end for a
|
||||
* mobile user with no way back. False for ROOT/null/no-parent too. */
|
||||
canSelectParent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +85,13 @@ export function useNodeActions(nodeId: string | null | undefined): NodeActions {
|
||||
|
||||
let canMoveUp = false;
|
||||
let canMoveDown = false;
|
||||
let canSelectParent = false;
|
||||
if (!isRootOrNull) {
|
||||
try {
|
||||
const node = query.node(nodeId).get();
|
||||
const parentId: string | null | undefined = node?.data?.parent;
|
||||
if (parentId) {
|
||||
canSelectParent = parentId !== 'ROOT';
|
||||
const siblings: string[] = query.node(parentId).get()?.data?.nodes || [];
|
||||
const idx = siblings.indexOf(nodeId);
|
||||
canMoveUp = idx > 0;
|
||||
@@ -102,16 +115,45 @@ export function useNodeActions(nodeId: string | null | undefined): NodeActions {
|
||||
}
|
||||
};
|
||||
|
||||
const duplicate = () => {
|
||||
if (!nodeId || nodeId === 'ROOT') return;
|
||||
const duplicate = (): string | null => {
|
||||
if (!nodeId || nodeId === 'ROOT') return null;
|
||||
try {
|
||||
const parentId = getParentId();
|
||||
if (!parentId) return;
|
||||
if (!parentId) return null;
|
||||
|
||||
const tree = regenerateTreeIds(query.node(nodeId).toNodeTree());
|
||||
actions.addNodeTree(tree, parentId);
|
||||
|
||||
// Insert the copy IMMEDIATELY AFTER the source node, not appended at
|
||||
// the end of the parent -- on a top-level section, "append at end"
|
||||
// landed the copy at the bottom of the page, off-screen, with the
|
||||
// ORIGINAL still selected, so a duplicate looked like nothing had
|
||||
// happened at all. Resolve the source's index among its siblings so
|
||||
// the copy lands right next to what it was copied from; if that can't
|
||||
// be resolved for any reason, fall back to the old append behavior
|
||||
// rather than guessing at an index (or throwing).
|
||||
let insertIndex: number | undefined;
|
||||
try {
|
||||
const siblings: string[] = query.node(parentId).get()?.data?.nodes || [];
|
||||
const sourceIndex = siblings.indexOf(nodeId);
|
||||
if (sourceIndex !== -1) insertIndex = sourceIndex + 1;
|
||||
} catch {
|
||||
// Leave insertIndex undefined -- addNodeTree appends when omitted.
|
||||
}
|
||||
|
||||
if (insertIndex !== undefined) {
|
||||
actions.addNodeTree(tree, parentId, insertIndex);
|
||||
} else {
|
||||
actions.addNodeTree(tree, parentId);
|
||||
}
|
||||
|
||||
// Select the new copy (not the original) so the toolbar/context menu
|
||||
// and any on-canvas outline immediately reflect what was just created.
|
||||
actions.selectNode(tree.rootNodeId);
|
||||
|
||||
return tree.rootNodeId;
|
||||
} catch (e) {
|
||||
console.error('Duplicate failed:', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,5 +207,15 @@ export function useNodeActions(nodeId: string | null | undefined): NodeActions {
|
||||
}
|
||||
};
|
||||
|
||||
return { moveUp, moveDown, duplicate, deleteNode, selectParent, canMoveUp, canMoveDown, canDelete };
|
||||
return {
|
||||
moveUp,
|
||||
moveDown,
|
||||
duplicate,
|
||||
deleteNode,
|
||||
selectParent,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
canDelete,
|
||||
canSelectParent,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user