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
+8 -8
View File
File diff suppressed because one or more lines are too long
+63 -20
View File
@@ -390,7 +390,7 @@ export class WaypointView extends ItemView {
}); });
rowEl.addEventListener('dragend', () => { rowEl.addEventListener('dragend', () => {
this.dragId = null; 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-dragging');
el.removeClass('waypoint-bm-drop-line'); el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below'); el.removeClass('waypoint-bm-drop-below');
@@ -399,16 +399,17 @@ export class WaypointView extends ItemView {
rowEl.addEventListener('dragenter', (e) => { rowEl.addEventListener('dragenter', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, false);
}); });
rowEl.addEventListener('dragover', (e) => { rowEl.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, false);
}); });
rowEl.addEventListener('dragleave', () => { rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line'); rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below'); rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
}); });
rowEl.addEventListener('drop', (e) => { rowEl.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
@@ -448,7 +449,7 @@ export class WaypointView extends ItemView {
}); });
rowEl.addEventListener('dragend', () => { rowEl.addEventListener('dragend', () => {
this.dragId = null; 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-dragging');
el.removeClass('waypoint-bm-drop-line'); el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below'); el.removeClass('waypoint-bm-drop-below');
@@ -457,16 +458,17 @@ export class WaypointView extends ItemView {
rowEl.addEventListener('dragenter', (e) => { rowEl.addEventListener('dragenter', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, false);
}); });
rowEl.addEventListener('dragover', (e) => { rowEl.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, false);
}); });
rowEl.addEventListener('dragleave', () => { rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line'); rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below'); rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
}); });
rowEl.addEventListener('drop', (e) => { rowEl.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
@@ -507,30 +509,34 @@ export class WaypointView extends ItemView {
rowEl.addClass('waypoint-bm-dragging'); rowEl.addClass('waypoint-bm-dragging');
}); });
const canAcceptChildren = isGroup || (item.children && item.children.length > 0);
rowEl.addEventListener('dragend', () => { rowEl.addEventListener('dragend', () => {
this.dragId = null; 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-dragging');
el.removeClass('waypoint-bm-drop-line'); el.removeClass('waypoint-bm-drop-line');
el.removeClass('waypoint-bm-drop-below'); el.removeClass('waypoint-bm-drop-below');
el.removeClass('waypoint-bm-drop-into');
}); });
}); });
rowEl.addEventListener('dragenter', (e) => { rowEl.addEventListener('dragenter', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, canAcceptChildren);
}); });
rowEl.addEventListener('dragover', (e) => { rowEl.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
if (!this.dragId || this.dragId === item.id) return; if (!this.dragId || this.dragId === item.id) return;
this.showDropIndicator(rowEl, e); this.showDropIndicator(rowEl, e, canAcceptChildren);
}); });
rowEl.addEventListener('dragleave', () => { rowEl.addEventListener('dragleave', () => {
rowEl.removeClass('waypoint-bm-drop-line'); rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below'); rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
}); });
rowEl.addEventListener('drop', (e) => { rowEl.addEventListener('drop', (e) => {
@@ -538,12 +544,18 @@ export class WaypointView extends ItemView {
this.dragId = null; this.dragId = null;
rowEl.removeClass('waypoint-bm-drop-line'); rowEl.removeClass('waypoint-bm-drop-line');
rowEl.removeClass('waypoint-bm-drop-below'); rowEl.removeClass('waypoint-bm-drop-below');
rowEl.removeClass('waypoint-bm-drop-into');
const draggedId = e.dataTransfer?.getData('text/plain'); const draggedId = e.dataTransfer?.getData('text/plain');
if (!draggedId || draggedId === item.id) return; if (!draggedId || draggedId === item.id) return;
const dropAbove = (rowEl as any).__dropAbove; const dropInto = (rowEl as any).__dropInto;
this.moveBookmarkToPosition(draggedId, item.id, dropAbove); 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 ── // ── Group: chevron + icon + label ──
@@ -808,25 +820,47 @@ export class WaypointView extends ItemView {
menu.showAtPosition({ x: event.clientX, y: event.clientY }); menu.showAtPosition({ x: event.clientX, y: event.clientY });
} }
private showDropIndicator(el: HTMLElement, e: MouseEvent): void { private showDropIndicator(el: HTMLElement, e: MouseEvent, isGroupLike: boolean): void {
// Clear indicator from all siblings first // Clear all indicators
const parent = el.parentElement; const parent = el.parentElement;
if (parent) { 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-line');
el2.removeClass('waypoint-bm-drop-below'); el2.removeClass('waypoint-bm-drop-below');
el2.removeClass('waypoint-bm-drop-into');
}); });
} }
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const midY = rect.top + rect.height / 2; const y = e.clientY;
const above = e.clientY < midY;
el.addClass('waypoint-bm-drop-line'); if (isGroupLike) {
if (!above) { // 3-zone: top 25% = above, middle 50% = into, bottom 25% = below
el.addClass('waypoint-bm-drop-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 { private moveBookmarkToGroup(itemId: string, targetGroupId: string | null): void {
@@ -846,6 +880,15 @@ export class WaypointView extends ItemView {
const item = removeRecursive(this.plugin.waypointData.bookmarks); const item = removeRecursive(this.plugin.waypointData.bookmarks);
if (!item) return; 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) { if (targetGroupId) {
const findGroup = (items: BookmarkItem[]): BookmarkItem | null => { const findGroup = (items: BookmarkItem[]): BookmarkItem | null => {
for (const i of items) { for (const i of items) {
+10 -2
View File
@@ -44,11 +44,14 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: color 80ms, background 80ms; transition: color 80ms, background 80ms;
box-shadow: none;
opacity: 0.6;
} }
.waypoint-header-more:hover { .waypoint-header-more:hover {
color: var(--text-accent); color: var(--text-normal);
background-color: var(--background-modifier-active-hover); background: none;
opacity: 1;
} }
/* ── Calendar ── */ /* ── Calendar ── */
@@ -287,6 +290,11 @@
border-bottom: 3px solid var(--text-accent) !important; border-bottom: 3px solid var(--text-accent) !important;
} }
.waypoint-favorites .waypoint-bm-drop-into {
background-color: var(--background-modifier-active-hover) !important;
border-radius: 4px;
}
/* ── Separator & Spacer ── */ /* ── Separator & Spacer ── */
.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator { .waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator {