Fix structure tree drag feedback: stable rows, on-row child drops

Rework the site structure tree's drag-and-drop to match Sortable's native
model instead of mid-drag drop zones:

- Drop the dashed empty-list drop zones that appeared on drag start
  (flicker, space reservation, and mis-targeted drops: the last item's
  child zone sat exactly where 'end of parent list' was).
- Every non-empty list (and the root) ends with a non-draggable  footer
  row (vuedraggable #footer slot): click to start a new page at that
  level, and while dragging it is the 'end of this list' drop target.
  It renders last even mid-drag via flex order, since Sortable appends
  end-of-list previews after it in the DOM.
- Dropping ON the lower part of a row makes the page that row's first
  child: the child list's container invisibly overlaps its own row's
  bottom (negative margin + equal padding, no layout effect) and becomes
  hit-testable only while dragging. A row's exposed top strip remains
  the 'sibling before' target. Leaf rows work too, creating a sublist.
- Indentation is structural (each nested list margin-indents itself), so
  a dragged row previews its whole subtree at the target list's depth.
  Rows get a little vertical padding to widen the drop zones.
This commit is contained in:
2026-08-16 18:05:33 +00:00
parent e38063a522
commit 3dc438f8df
5 changed files with 121 additions and 61 deletions
+7 -28
View File
@@ -157,12 +157,13 @@ function followMove(oldPath, newPath) {
}
// --- New page flow -------------------------------------------------------
// The in the pages header adds a *pending* row to the tree: a local-only
// item that can be dragged into place before anything is filled in. It is
// persisted only on commit (✓/Enter), at wherever it currently sits.
// The row at the end of any list adds a *pending* row there: a
// local-only item that can be dragged into place before anything is
// filled in. It is persisted only on commit (✓/Enter), at wherever it
// currently sits.
const pending = ref(null)
function newPage() {
function newPage(list) {
if (pending.value) return // one at a time
pending.value = {
slug: '',
@@ -174,7 +175,7 @@ function newPage() {
children: [],
pending: true,
}
tree.value.push(pending.value)
list.push(pending.value)
}
// Where does the pending row currently sit? -> {parentPath, list, index}.
@@ -431,6 +432,7 @@ provide('structureHandlers', {
addContent,
commitPending,
discardPending,
newPage,
})
// --- Banner editing ------------------------------------------------------
@@ -644,12 +646,6 @@ onUnmounted(() => {
<section class="block structure">
<StructureTree :nodes="tree" />
<button
type="button"
class="add"
title="new page — drag the new row into place, then fill in title and slug"
@click="newPage"
></button>
</section>
</div>
</template>
@@ -774,21 +770,4 @@ onUnmounted(() => {
overflow-y: auto;
min-height: 0;
}
/* Icon buttons () keep the emoji's own color, no button chrome. */
.structure .add {
align-self: flex-start; /* don't stretch to the block's full width */
margin-top: 0.3rem;
margin-left: 1.2em; /* align with the row titles, past the drag handle */
padding: 0.1rem 0.3rem;
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
opacity: 0.7;
}
.structure .add:hover {
opacity: 1;
}
</style>