Shared drag-reorder primitive: createDragReorder owning DOM drag plumbing #124

Closed
opened 2026-07-19 01:13:49 +02:00 by dominik.polakovics · 1 comment

This was generated by AI during triage.

Split out of #117 during triage (the other two primitives — a shared errorMessage and createAsyncAction — stayed there and have since merged). This half needed its own design pass before it was agent-ready.

Problem: the drop verdicts are already pure, shared, and unit-tested (elementcrud.classifyDrop, pagemanage.resolveDrop), but the DOM event plumbing around them — dragover → compute zone → set a dropSpot signal → drop → resolve → clear — is hand-wired separately per feature.

Rescoped during triage (2026-07-19) after a code survey resolved the open design questions:

  • The genuine reorder pattern exists in three sites, not five: PagePane (four target kinds: row, top-level/end gap, empty cell, slot gap), TreePane (three-position grammar with auto-expand-on-hover), and FormPane (repeatable-item reorder that explicitly mirrors PagePane's zone idiom). MediaModule is a whole-target folder move with no zone math and MediaPicker is a plain file-upload sink — both are out of scope for the primitive.
  • Zone model: the sites already compute zones with pure functions; the primitive owns the event skeleton and the dropSpot signal, and takes a per-target zoneFor-style callback. It does not own geometry.
  • Signal ownership: the primitive owns dropSpot and its keyed clearing (dragleave/drop/dragend), which is exactly the ~15–25 lines each handler trio repeats today.
  • Adoption is proven against the two most dissimilar sites first (PagePane + TreePane), then FormPane.
  • Cleanups folded in: unify the two near-identical pure zone-geometry helpers (rowDropZone in the element-crud module vs dropZone in the page-manage module), and deduplicate the per-test-file jsdom fireDrag drag-simulation helper into one shared test util.

See the agent brief in the comments for the full contract.

Related: #117 (sibling primitives, merged).

> *This was generated by AI during triage.* Split out of #117 during triage (the other two primitives — a shared `errorMessage` and `createAsyncAction` — stayed there and have since merged). This half needed its own design pass before it was agent-ready. **Problem**: the drop *verdicts* are already pure, shared, and unit-tested (`elementcrud.classifyDrop`, `pagemanage.resolveDrop`), but the DOM event plumbing around them — dragover → compute zone → set a `dropSpot` signal → drop → resolve → clear — is hand-wired separately per feature. **Rescoped during triage (2026-07-19)** after a code survey resolved the open design questions: - The genuine reorder pattern exists in **three** sites, not five: PagePane (four target kinds: row, top-level/end gap, empty cell, slot gap), TreePane (three-position grammar with auto-expand-on-hover), and FormPane (repeatable-item reorder that explicitly mirrors PagePane's zone idiom). MediaModule is a whole-target folder move with no zone math and MediaPicker is a plain file-upload sink — both are **out of scope** for the primitive. - Zone model: the sites already compute zones with pure functions; the primitive owns the event skeleton and the `dropSpot` signal, and takes a per-target `zoneFor`-style callback. It does not own geometry. - Signal ownership: the primitive owns `dropSpot` and its keyed clearing (dragleave/drop/dragend), which is exactly the ~15–25 lines each handler trio repeats today. - Adoption is proven against the two most dissimilar sites first (PagePane + TreePane), then FormPane. - **Cleanups folded in**: unify the two near-identical pure zone-geometry helpers (`rowDropZone` in the element-crud module vs `dropZone` in the page-manage module), and deduplicate the per-test-file jsdom `fireDrag` drag-simulation helper into one shared test util. See the agent brief in the comments for the full contract. Related: #117 (sibling primitives, merged).
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Extract a shared createDragReorder primitive that owns the DOM drag-and-drop plumbing (the dropSpot signal plus the dragover/dragleave/drop/dragend skeleton) and adopt it in the three panes that hand-wire it today — PagePane, TreePane, FormPane — while unifying the duplicated zone-geometry math and the duplicated jsdom drag test helper.

Current behavior:
The web editor (Solid.js, under web/) already has pure, unit-tested drop-verdict helpers: classifyDrop (element tree, in the element-crud module) and resolveDrop with dropZone/dropZoneMode (page tree, in the page-manage module). The DOM plumbing around them is hand-wired per pane, repeating the same skeleton each time: a drag-source signal plus a dropSpot-like signal; a dragover that guards on an active drag, calls preventDefault, sets dataTransfer.dropEffect = "move", computes a zone, and sets the spot; a keyed dragleave that clears the spot only if it belongs to this target; a drop that clears state and invokes a move action; a dragend that clears everything. The effectAllowed/dropEffect pairing and its rationale is re-commented at every site.

  • PagePane wires this skeleton four times over — row targets (midpoint or 25/50/25 zones), top-level gaps plus an end gap, whole empty cells (drop-into-slot), and slot gaps inside populated cells — all sharing one dropSpot signal with per-kind keyed clearing. It also carries a duplicate verdict-recompute helper that exists only because drop handlers clear the drag-source signal before recomputing.
  • TreePane wires it once for row targets with a three-position grammar (before/into/after with mode degradation), plus two quirks: an auto-expand-on-hover timer (~600ms, cancelled on leave/drop/cleanup) and a zone recompute at drop time.
  • FormPane wires it for repeatable-item reorder, mirroring PagePane's idiom with an inlined midpoint check, an inline no-op guard, and a relatedTarget-containment check in dragleave to suppress indicator flicker when the pointer crosses into child elements (a real-browser behavior jsdom never exercises).

Two additional duplications ride along:

  • Two near-identical pure zone-geometry helpers exist — rowDropZone (element-crud module) and dropZone (page-manage module) — both implementing midpoint and 25/50/25 band math.
  • Each drag-wiring test file defines its own fireDrag-style jsdom helper (one also stubs a FakeDataTransfer emulating browser effectAllowed/dropEffect arbitration), because jsdom lacks a real DragEvent. The workaround is copy-pasted four times.

Desired behavior:

  1. A createDragReorder primitive owns the shared skeleton exactly once: the dropSpot signal (generic in the spot type), the dragover guard/preventDefault/dropEffect invariant, keyed clearing on dragleave, and full clearing on drop/dragend. Zone geometry and drop verdicts stay in the existing pure helpers — the primitive receives per-target callbacks (a zoneFor-style function returning a spot or null, and an on-drop action) rather than owning geometry. It must support several target kinds in one pane sharing a single dropSpot signal (PagePane needs four), and it must stay a thin Solid primitive — signals plus handler factories, not a framework.
  2. The three panes adopt it, in this order: PagePane and TreePane first (the two most dissimilar consumers — if the contract fits both without leaking, it's right), then FormPane. Pane-specific behavior must survive adoption: TreePane's auto-expand timer and drop-time zone recompute, FormPane's relatedTarget anti-flicker dragleave (either absorbed into the primitive or still expressible around it), PagePane's read-only gating and per-kind keyed clears.
  3. Exactly one implementation of the shared zone band math (midpoint, 25/50/25, mode degradation) remains, and both the element-crud and page-manage verdict paths use it. Verdict semantics and thresholds are unchanged.
  4. Exactly one jsdom drag-simulation test helper (including the fake dataTransfer) lives in a shared test util, and the wiring test files use it instead of their local copies.
  5. MediaModule (whole-target folder move plus OS-file upload grid) and MediaPicker (file-upload sink) are not converted — they don't fit the reorder pattern and forcing them under the primitive would make it leak.

Key interfaces:

  • createDragReorder — new primitive. Suggested shape: generic over the spot type, returning the dropSpot accessor plus per-target handler factories (dragover/dragleave/drop) and a dragend/clear hook; exact signature is the agent's call. Lives alongside the other shared primitives (createAsyncAction sets the precedent for a flat module under the web source root).
  • classifyDrop, resolveDrop — behavior unchanged. The geometry consolidation may move rowDropZone/dropZone into a shared module, but every existing verdict test must still pass with at most mechanical import updates.
  • The three panes' rendered UI, drop indicators, verdict outcomes, and move actions must be behaviorally unchanged.

Acceptance criteria:

  • PagePane, TreePane, and FormPane no longer declare their own drop-spot signals or hand-written dragover/dragleave/dragend skeletons; the shared primitive is the only place that logic exists.
  • createDragReorder has unit tests (vitest, under the web test directory) covering at minimum: spot set on dragover with a valid zone and not set when the zone callback returns null or no drag is active; dropEffect set only when valid; keyed dragleave clears only the matching spot; drop and dragend clear all state.
  • Exactly one implementation of the midpoint/25-50-25 zone band math exists in the web source; all existing classifyDrop/resolveDrop unit tests pass unchanged apart from imports.
  • Exactly one jsdom drag-simulation helper exists under the web test tree; the per-file copies are gone and all existing wiring suites (element CRUD, layout grid, page manage, form items) pass with unchanged assertions.
  • TreePane's auto-expand-on-hover and drop-time recompute, FormPane's child-element dragleave anti-flicker case, and PagePane's read-only gating each still have a passing test.
  • The full web test suite and go test ./... pass (build web/dist first; toolchains via nix shell nixpkgs#go / nixpkgs#nodejs per the repo's dev-env notes).
  • A real-browser smoke check of drag reorder in all three panes (the repo's playwright-core + nix chromium recipe against harness serve) confirms drag indicators appear and moves land — jsdom cannot catch native drag-and-drop regressions.

Out of scope:

  • Converting MediaModule's folder-move/upload wiring or MediaPicker's upload sink.
  • Keyboard-accessible reorder, drag auto-scroll, or custom drag images — none exist today; don't add them.
  • Any change to drop verdict semantics, zone thresholds, indicator visuals, or move behavior.
  • The PagePane/TreePane controller extraction proposed by the original architecture review.
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Extract a shared `createDragReorder` primitive that owns the DOM drag-and-drop plumbing (the `dropSpot` signal plus the dragover/dragleave/drop/dragend skeleton) and adopt it in the three panes that hand-wire it today — PagePane, TreePane, FormPane — while unifying the duplicated zone-geometry math and the duplicated jsdom drag test helper. **Current behavior:** The web editor (Solid.js, under `web/`) already has pure, unit-tested drop-*verdict* helpers: `classifyDrop` (element tree, in the element-crud module) and `resolveDrop` with `dropZone`/`dropZoneMode` (page tree, in the page-manage module). The DOM *plumbing* around them is hand-wired per pane, repeating the same skeleton each time: a drag-source signal plus a `dropSpot`-like signal; a dragover that guards on an active drag, calls `preventDefault`, sets `dataTransfer.dropEffect = "move"`, computes a zone, and sets the spot; a keyed dragleave that clears the spot only if it belongs to this target; a drop that clears state and invokes a move action; a dragend that clears everything. The `effectAllowed`/`dropEffect` pairing and its rationale is re-commented at every site. - **PagePane** wires this skeleton four times over — row targets (midpoint or 25/50/25 zones), top-level gaps plus an end gap, whole empty cells (drop-into-slot), and slot gaps inside populated cells — all sharing one `dropSpot` signal with per-kind keyed clearing. It also carries a duplicate verdict-recompute helper that exists only because drop handlers clear the drag-source signal before recomputing. - **TreePane** wires it once for row targets with a three-position grammar (before/into/after with mode degradation), plus two quirks: an auto-expand-on-hover timer (~600ms, cancelled on leave/drop/cleanup) and a zone recompute at drop time. - **FormPane** wires it for repeatable-item reorder, mirroring PagePane's idiom with an inlined midpoint check, an inline no-op guard, and a `relatedTarget`-containment check in dragleave to suppress indicator flicker when the pointer crosses into child elements (a real-browser behavior jsdom never exercises). Two additional duplications ride along: - Two near-identical pure zone-geometry helpers exist — `rowDropZone` (element-crud module) and `dropZone` (page-manage module) — both implementing midpoint and 25/50/25 band math. - Each drag-wiring test file defines its own `fireDrag`-style jsdom helper (one also stubs a `FakeDataTransfer` emulating browser `effectAllowed`/`dropEffect` arbitration), because jsdom lacks a real `DragEvent`. The workaround is copy-pasted four times. **Desired behavior:** 1. A `createDragReorder` primitive owns the shared skeleton exactly once: the `dropSpot` signal (generic in the spot type), the dragover guard/`preventDefault`/`dropEffect` invariant, keyed clearing on dragleave, and full clearing on drop/dragend. Zone geometry and drop verdicts stay in the existing pure helpers — the primitive receives per-target callbacks (a `zoneFor`-style function returning a spot or null, and an on-drop action) rather than owning geometry. It must support several target kinds in one pane sharing a single `dropSpot` signal (PagePane needs four), and it must stay a thin Solid primitive — signals plus handler factories, not a framework. 2. The three panes adopt it, in this order: PagePane and TreePane first (the two most dissimilar consumers — if the contract fits both without leaking, it's right), then FormPane. Pane-specific behavior must survive adoption: TreePane's auto-expand timer and drop-time zone recompute, FormPane's `relatedTarget` anti-flicker dragleave (either absorbed into the primitive or still expressible around it), PagePane's read-only gating and per-kind keyed clears. 3. Exactly one implementation of the shared zone band math (midpoint, 25/50/25, mode degradation) remains, and both the element-crud and page-manage verdict paths use it. Verdict semantics and thresholds are unchanged. 4. Exactly one jsdom drag-simulation test helper (including the fake `dataTransfer`) lives in a shared test util, and the wiring test files use it instead of their local copies. 5. MediaModule (whole-target folder move plus OS-file upload grid) and MediaPicker (file-upload sink) are **not** converted — they don't fit the reorder pattern and forcing them under the primitive would make it leak. **Key interfaces:** - `createDragReorder` — new primitive. Suggested shape: generic over the spot type, returning the `dropSpot` accessor plus per-target handler factories (dragover/dragleave/drop) and a dragend/clear hook; exact signature is the agent's call. Lives alongside the other shared primitives (`createAsyncAction` sets the precedent for a flat module under the web source root). - `classifyDrop`, `resolveDrop` — behavior unchanged. The geometry consolidation may move `rowDropZone`/`dropZone` into a shared module, but every existing verdict test must still pass with at most mechanical import updates. - The three panes' rendered UI, drop indicators, verdict outcomes, and move actions must be behaviorally unchanged. **Acceptance criteria:** - [ ] PagePane, TreePane, and FormPane no longer declare their own drop-spot signals or hand-written dragover/dragleave/dragend skeletons; the shared primitive is the only place that logic exists. - [ ] `createDragReorder` has unit tests (vitest, under the web test directory) covering at minimum: spot set on dragover with a valid zone and not set when the zone callback returns null or no drag is active; `dropEffect` set only when valid; keyed dragleave clears only the matching spot; drop and dragend clear all state. - [ ] Exactly one implementation of the midpoint/25-50-25 zone band math exists in the web source; all existing `classifyDrop`/`resolveDrop` unit tests pass unchanged apart from imports. - [ ] Exactly one jsdom drag-simulation helper exists under the web test tree; the per-file copies are gone and all existing wiring suites (element CRUD, layout grid, page manage, form items) pass with unchanged assertions. - [ ] TreePane's auto-expand-on-hover and drop-time recompute, FormPane's child-element dragleave anti-flicker case, and PagePane's read-only gating each still have a passing test. - [ ] The full web test suite and `go test ./...` pass (build `web/dist` first; toolchains via `nix shell nixpkgs#go` / `nixpkgs#nodejs` per the repo's dev-env notes). - [ ] A real-browser smoke check of drag reorder in all three panes (the repo's playwright-core + nix chromium recipe against `harness serve`) confirms drag indicators appear and moves land — jsdom cannot catch native drag-and-drop regressions. **Out of scope:** - Converting MediaModule's folder-move/upload wiring or MediaPicker's upload sink. - Keyboard-accessible reorder, drag auto-scroll, or custom drag images — none exist today; don't add them. - Any change to drop verdict semantics, zone thresholds, indicator visuals, or move behavior. - The PagePane/TreePane controller extraction proposed by the original architecture review.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#124
No description provided.