feat: drag-and-drop reorder for daily focus items
Add HTML5 drag-and-drop within domain groups on the focus page. Items can be dragged to reorder within their domain; cross-domain drag is prevented. Uses hidden form POST (no fetch/XHR). Arrow buttons kept as fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,20 @@ async def reorder_focus(
|
||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/reorder-all")
|
||||
async def reorder_all_focus(
|
||||
request: Request,
|
||||
item_ids: str = Form(...),
|
||||
focus_date: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("daily_focus", db)
|
||||
ids = [i.strip() for i in item_ids.split(",") if i.strip()]
|
||||
if ids:
|
||||
await repo.reorder(ids)
|
||||
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{focus_id}/toggle")
|
||||
async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("daily_focus", db)
|
||||
|
||||
@@ -1107,6 +1107,11 @@ a:hover { color: var(--accent-hover); }
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* ---- Focus Drag-and-Drop ---- */
|
||||
.focus-drag-row { cursor: grab; }
|
||||
.focus-drag-row.dragging { opacity: 0.4; background: var(--accent-soft); }
|
||||
.focus-drag-row.drag-over { box-shadow: 0 -2px 0 0 var(--accent) inset; }
|
||||
|
||||
/* ---- Reorder Grip Handle ---- */
|
||||
.reorder-grip {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -15,20 +15,23 @@
|
||||
<!-- Focus items grouped by domain -->
|
||||
{% if items %}
|
||||
<div class="card">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;">
|
||||
<table id="focus-table" style="width:100%;border-collapse:collapse;font-size:0.8rem;">
|
||||
<colgroup>
|
||||
<col style="width:24px"><col style="width:20px"><col style="width:74px"><col style="width:10px">
|
||||
<col><col style="width:110px"><col style="width:120px"><col style="width:50px"><col style="width:24px">
|
||||
</colgroup>
|
||||
{% for domain in hierarchy %}
|
||||
<tbody>
|
||||
<tr><td colspan="9" style="padding:2px 8px;border-bottom:1px solid var(--border);background:var(--surface2);">
|
||||
<span style="display:inline-flex;align-items:center;gap:6px;">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:{{ domain.color or 'var(--accent)' }};flex-shrink:0;"></span>
|
||||
<span style="font-weight:700;font-size:0.8rem;letter-spacing:0.03em;text-transform:uppercase;color:var(--text)">{{ domain.label }}</span>
|
||||
</span>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
<tbody class="focus-drag-group">
|
||||
{% for item in domain.rows %}
|
||||
<tr style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
|
||||
<tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
|
||||
<td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td>
|
||||
<td style="padding:1px 1px;vertical-align:middle;">
|
||||
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
|
||||
@@ -48,8 +51,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<form id="focus-reorder-form" action="/focus/reorder-all" method="post" style="display:none;">
|
||||
<input type="hidden" name="item_ids" id="focus-reorder-ids">
|
||||
<input type="hidden" name="focus_date" value="{{ focus_date }}">
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
|
||||
@@ -182,6 +190,71 @@
|
||||
document.getElementById('focus-show-more-wrap').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Drag-and-drop reorder within domain groups
|
||||
var dragRow = null;
|
||||
var dragGroup = null;
|
||||
|
||||
document.querySelectorAll('.focus-drag-row').forEach(function(row) {
|
||||
row.addEventListener('dragstart', function(e) {
|
||||
dragRow = row;
|
||||
dragGroup = row.closest('.focus-drag-group');
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', row.dataset.id);
|
||||
});
|
||||
|
||||
row.addEventListener('dragend', function() {
|
||||
row.classList.remove('dragging');
|
||||
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
|
||||
el.classList.remove('drag-over');
|
||||
});
|
||||
// Collect all IDs across all groups in DOM order and submit
|
||||
if (dragRow) {
|
||||
var allIds = [];
|
||||
document.querySelectorAll('#focus-table .focus-drag-row').forEach(function(r) {
|
||||
allIds.push(r.dataset.id);
|
||||
});
|
||||
document.getElementById('focus-reorder-ids').value = allIds.join(',');
|
||||
document.getElementById('focus-reorder-form').submit();
|
||||
}
|
||||
dragRow = null;
|
||||
dragGroup = null;
|
||||
});
|
||||
|
||||
row.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
if (!dragRow || row === dragRow) return;
|
||||
// Constrain to same domain group
|
||||
if (row.closest('.focus-drag-group') !== dragGroup) return;
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
|
||||
el.classList.remove('drag-over');
|
||||
});
|
||||
row.classList.add('drag-over');
|
||||
});
|
||||
|
||||
row.addEventListener('dragleave', function() {
|
||||
row.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
row.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
if (!dragRow || row === dragRow) return;
|
||||
if (row.closest('.focus-drag-group') !== dragGroup) return;
|
||||
row.classList.remove('drag-over');
|
||||
// Insert dragged row before or after target based on position
|
||||
var tbody = dragGroup;
|
||||
var rows = Array.from(tbody.querySelectorAll('.focus-drag-row'));
|
||||
var dragIdx = rows.indexOf(dragRow);
|
||||
var targetIdx = rows.indexOf(row);
|
||||
if (dragIdx < targetIdx) {
|
||||
tbody.insertBefore(dragRow, row.nextSibling);
|
||||
} else {
|
||||
tbody.insertBefore(dragRow, row);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user