@bpmsoftwaresolutions/renderx-plugins
Version:
RenderX plugins meta-package with unit tests and build + manifest generation
297 lines (283 loc) • 10.3 kB
JavaScript
// Pointer-driven drag lifecycle for Canvas UI elements
// attachDragHandlers returns an object of event handlers to spread into element props
import { ensureCursorStylesInjected } from "../styles/cursors.js";
import { updateInstancePositionCSS } from "../styles/instanceCss.js";
import { DragCoordinator } from "../utils/DragCoordinator.js";
import { overlayArrangement } from "../features/overlay/overlay.arrangement.js";
export function attachDragHandlers(node, deps = {}) {
ensureCursorStylesInjected();
const getPromptBook = () => {
try {
const w = (typeof window !== "undefined" && window) || {};
return w.__rx_prompt_book__ || null;
} catch {
return null;
}
};
const getStartPos = () => {
// Prefer position from Prompt Book ONLY if the node exists in the store
try {
const pb = getPromptBook();
const exists = pb?.selectors?.nodeById?.(node.id);
if (exists) {
const p = pb?.selectors?.positionOf?.(node.id);
if (p && typeof p.x === "number" && typeof p.y === "number") {
return { x: p.x, y: p.y };
}
}
} catch {}
// Fallback to any persisted baseline (to be removed after full migration)
try {
const w = (typeof window !== "undefined" && window) || {};
const p = w.__rx_canvas_ui__?.positions?.[node.id];
if (p && typeof p.x === "number" && typeof p.y === "number") {
return { x: p.x, y: p.y };
}
} catch {}
// Default to node's own position
return {
x: node?.position?.x || 0,
y: node?.position?.y || 0,
};
};
const playLegacy = (id, payload) => {
try {
const system = (window && window.renderxCommunicationSystem) || null;
const conductor = system && system.conductor;
if (conductor && typeof conductor.play === "function") {
conductor.play(id, id, payload);
}
} catch {}
};
const playPhase = (channel, action, payload) => {
try {
const system = (window && window.renderxCommunicationSystem) || null;
const conductor = system && system.conductor;
if (conductor && typeof conductor.play === "function") {
conductor.play(channel, action, payload);
}
} catch {}
};
const getRec = () => {
try {
const w = (typeof window !== "undefined" && window) || {};
w.__rx_drag = w.__rx_drag || {};
return w.__rx_drag[node.id] || null;
} catch {
return null;
}
};
const setRec = (rec) => {
try {
const w = (typeof window !== "undefined" && window) || {};
w.__rx_drag = w.__rx_drag || {};
w.__rx_drag[node.id] = rec;
} catch {}
};
return {
onPointerEnter: (e) => {
try {
const rec = getRec();
if (!rec || !rec.active) {
// Ensure only hover affordance is present
e.currentTarget?.classList?.remove("rx-comp-grabbing");
e.currentTarget?.classList?.add("rx-comp-draggable");
}
} catch {}
},
onPointerLeave: (e) => {
try {
const rec = getRec();
if (!rec || !rec.active) {
e.currentTarget?.classList?.remove("rx-comp-draggable");
e.currentTarget?.classList?.remove("rx-comp-grabbing");
}
} catch {}
},
onPointerDown: (e) => {
try {
e?.stopPropagation?.();
try {
// On drag start: switch cursor to grabbing
e.currentTarget?.classList?.remove("rx-comp-draggable");
e.currentTarget?.classList?.add("rx-comp-grabbing");
if (e.currentTarget && e.currentTarget.style) {
try {
e.currentTarget.style.touchAction = "none";
e.currentTarget.style.willChange = "transform";
} catch {}
}
} catch {}
try {
e.target?.setPointerCapture?.(e.pointerId);
} catch {}
const origin = { x: e.clientX || 0, y: e.clientY || 0 };
DragCoordinator.start({
id: node.id,
start: getStartPos(),
origin,
el: e.currentTarget || null,
});
setRec({
origin,
start: getStartPos(),
active: true,
lastCursor: origin,
rafScheduled: false,
el: e.currentTarget || null,
});
// Orchestration: broadcast drag start; overlay will react via conductor
// Emit both namespaced and base events to support mixed listeners during migration
playPhase("Canvas.component-drag-symphony", "start", {
elementId: node.id,
origin,
});
playLegacy("Canvas.component-drag-symphony", {
elementId: node.id,
origin,
});
// Safety: ensure overlay hides even if concertmasters aren't bootstrapped in this test
try {
const css = overlayArrangement.hideRule(node.id);
const id = `overlay-visibility-${node.id}`;
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement("style");
tag.id = id;
document.head.appendChild(tag);
}
tag.textContent = css;
} catch {}
} catch {}
},
onPointerMove: (e) => {
try {
try {
// Maintain grabbing cursor while dragging; ignore hover-only moves
if (e && e.buttons === 1) {
e.currentTarget?.classList?.remove("rx-comp-draggable");
e.currentTarget?.classList?.add("rx-comp-grabbing");
}
} catch {}
const cur = { x: e.clientX || 0, y: e.clientY || 0 };
const rec = getRec();
if (!rec || !rec.active) return;
DragCoordinator.move({
id: node.id,
cursor: cur,
onFrame: ({ dx, dy }) => {
// Legacy overlay callback (back-compat) + Orchestration move per frame
try {
const system =
(window && window.renderxCommunicationSystem) || null;
const conductor = system && system.conductor;
// Also update overlay transform directly for UI tests that assert DOM styles without concertmaster bootstrapping
try {
const css = overlayArrangement.transformRule(node.id, dx, dy);
const id = `overlay-transform-${node.id}`;
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement("style");
tag.id = id;
document.head.appendChild(tag);
}
tag.textContent = css;
} catch {}
playPhase("Canvas.component-drag-symphony", "update", {
elementId: node.id,
delta: { dx, dy },
});
playLegacy("Canvas.component-drag-symphony", {
elementId: node.id,
delta: { dx, dy },
});
} catch {}
},
});
} catch {}
},
onPointerUp: (e) => {
try {
try {
e.currentTarget?.classList?.remove("rx-comp-grabbing");
e.currentTarget?.classList?.add("rx-comp-draggable");
if (e.currentTarget && e.currentTarget.style) {
try {
e.currentTarget.style.willChange = "";
e.currentTarget.style.touchAction = "";
e.currentTarget.style.transform = "";
} catch {}
}
} catch {}
try {
e.target?.releasePointerCapture?.(e.pointerId);
} catch {}
const upClient = { x: e.clientX || 0, y: e.clientY || 0 };
const finalPos = DragCoordinator.end({
id: node.id,
upClient,
onCommit: (pos) => {
try {
updateInstancePositionCSS(
node.id,
String(node.cssClass || node.id || ""),
pos.x,
pos.y
);
} catch {}
try {
const w = (typeof window !== "undefined" && window) || {};
const pb = w.__rx_prompt_book__ || null;
try {
pb?.actions?.move?.(node.id, { x: pos.x, y: pos.y });
} catch {}
// After store commit, read the canonical position and update per-instance CSS
try {
const committed = pb?.selectors?.positionOf?.(node.id) || {
x: pos.x,
y: pos.y,
};
updateInstancePositionCSS(
node.id,
String(node.cssClass || node.id || ""),
committed.x,
committed.y
);
// Also commit overlay base instance CSS for tests that assert DOM state without full concertmaster wiring
try {
const css = overlayArrangement.instanceRule(
node.id,
committed,
{}
);
const id = `overlay-instance-${node.id}`;
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement("style");
tag.id = id;
document.head.appendChild(tag);
}
tag.textContent = css;
} catch {}
} catch {}
} catch {}
},
});
playPhase("Canvas.component-drag-symphony", "end", {
elementId: node.id,
end: true,
});
playLegacy("Canvas.component-drag-symphony", {
elementId: node.id,
end: true,
});
// Clean up overlay visibility tag (show overlay)
try {
const id = `overlay-visibility-${node.id}`;
const tag = document.getElementById(id);
if (tag && tag.parentNode) tag.parentNode.removeChild(tag);
} catch {}
} catch {}
},
};
}