feat: drag-and-drop into groups/parent notes + fix ⋯ button styling

Drag-and-drop:
- 3-zone drop indicator for groups and parent notes (files with children):
  top 25% = drop above, middle 50% = drop into, bottom 25% = drop below
- 2-zone for regular files/separators/spacers (unchanged)
- 'Drop into' shows background highlight instead of border line
- Circular reference guard: can't drop a group into itself or descendants

⋯ button:
- No visible button chrome when not hovered (no border, no background)
- Subtle opacity (0.6) at rest, full opacity on hover
- Hover shows text color change only, no background highlight
This commit is contained in:
2026-06-10 21:53:22 -04:00
parent 2e8ac7047f
commit eeb6059fe5
3 changed files with 81 additions and 30 deletions
+63 -20
View File
@@ -390,7 +390,7 @@ export class WaypointView extends ItemView {
});
rowEl.addEventListener('dragend', () => {
this.dragId = null;
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => {
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into').forEach(el => {
el.removeClass('waypoint-bm-dragging');
el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below');
@@ -399,16 +399,17 @@ export class WaypointView extends ItemView {
rowEl.addEventListener('dragenter', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, false);
});
rowEl.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, false);
});
rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
});
rowEl.addEventListener('drop', (e) => {
e.preventDefault();
@@ -448,7 +449,7 @@ export class WaypointView extends ItemView {
});
rowEl.addEventListener('dragend', () => {
this.dragId = null;
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => {
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into').forEach(el => {
el.removeClass('waypoint-bm-dragging');
el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below');
@@ -457,16 +458,17 @@ export class WaypointView extends ItemView {
rowEl.addEventListener('dragenter', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, false);
});
rowEl.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, false);
});
rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
});
rowEl.addEventListener('drop', (e) => {
e.preventDefault();
@@ -507,30 +509,34 @@ export class WaypointView extends ItemView {
rowEl.addClass('waypoint-bm-dragging');
});
const canAcceptChildren = isGroup || (item.children && item.children.length > 0);
rowEl.addEventListener('dragend', () => {
this.dragId = null;
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => {
container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into').forEach(el => {
el.removeClass('waypoint-bm-dragging');
el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below');
el.removeClass('waypoint-bm-drop-into');
});
});
rowEl.addEventListener('dragenter', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, canAcceptChildren);
});
rowEl.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e);
this.showDropIndicator(rowEl, e, canAcceptChildren);
});
rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
});
rowEl.addEventListener('drop', (e) => {
@@ -538,12 +544,18 @@ export class WaypointView extends ItemView {
this.dragId = null;
rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
const draggedId = e.dataTransfer?.getData('text/plain');
if (!draggedId || draggedId === item.id) return;
const dropAbove = (rowEl as any).__dropAbove;
this.moveBookmarkToPosition(draggedId, item.id, dropAbove);
const dropInto = (rowEl as any).__dropInto;
if (dropInto && canAcceptChildren) {
this.moveBookmarkToGroup(draggedId, item.id);
} else {
const dropAbove = (rowEl as any).__dropAbove;
this.moveBookmarkToPosition(draggedId, item.id, dropAbove);
}
});
// ── Group: chevron + icon + label ──
@@ -808,25 +820,47 @@ export class WaypointView extends ItemView {
menu.showAtPosition({ x: event.clientX, y: event.clientY });
}
private showDropIndicator(el: HTMLElement, e: MouseEvent): void {
// Clear indicator from all siblings first
private showDropIndicator(el: HTMLElement, e: MouseEvent, isGroupLike: boolean): void {
// Clear all indicators
const parent = el.parentElement;
if (parent) {
parent.querySelectorAll('.waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el2 => {
parent.querySelectorAll('.waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into').forEach(el2 => {
el2.removeClass('waypoint-bm-drop-line');
el2.removeClass('waypoint-bm-drop-below');
el2.removeClass('waypoint-bm-drop-into');
});
}
const rect = el.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
const above = e.clientY < midY;
const y = e.clientY;
el.addClass('waypoint-bm-drop-line');
if (!above) {
el.addClass('waypoint-bm-drop-below');
if (isGroupLike) {
// 3-zone: top 25% = above, middle 50% = into, bottom 25% = below
const topThreshold = rect.top + rect.height * 0.25;
const bottomThreshold = rect.top + rect.height * 0.75;
if (y < topThreshold) {
el.addClass('waypoint-bm-drop-line');
(el as any).__dropAbove = true;
(el as any).__dropInto = false;
} else if (y > bottomThreshold) {
el.addClass('waypoint-bm-drop-line');
el.addClass('waypoint-bm-drop-below');
(el as any).__dropAbove = false;
(el as any).__dropInto = false;
} else {
el.addClass('waypoint-bm-drop-into');
(el as any).__dropAbove = false;
(el as any).__dropInto = true;
}
} else {
// 2-zone: top half = above, bottom half = below
const above = y < rect.top + rect.height / 2;
el.addClass('waypoint-bm-drop-line');
if (!above) el.addClass('waypoint-bm-drop-below');
(el as any).__dropAbove = above;
(el as any).__dropInto = false;
}
(el as any).__dropAbove = above;
}
private moveBookmarkToGroup(itemId: string, targetGroupId: string | null): void {
@@ -846,6 +880,15 @@ export class WaypointView extends ItemView {
const item = removeRecursive(this.plugin.waypointData.bookmarks);
if (!item) return;
// Prevent dropping into self or own descendant
if (targetGroupId) {
const isDescendant = (parent: BookmarkItem, targetId: string): boolean => {
if (parent.id === targetId) return true;
return parent.children.some(c => isDescendant(c, targetId));
};
if (item.id === targetGroupId || isDescendant(item, targetGroupId)) return;
}
if (targetGroupId) {
const findGroup = (items: BookmarkItem[]): BookmarkItem | null => {
for (const i of items) {