sheets: reorder by dragging instead of up/down buttons

Move-up/move-down felt disconnected from the result — clicking a
button and having two rows silently swap position doesn't read as
motion. Dropped both buttons for native HTML5 drag-and-drop: drag a
sheet, an accent line shows where it'll land, drop to reorder. Still
respects the pinned/unpinned grouping — dragging across that boundary
is a no-op, same as the old up/down buttons.
This commit is contained in:
Horizontal42 2026-07-03 12:57:14 +03:00
parent 5205c9fb63
commit 7b94f6a5b0
3 changed files with 42 additions and 17 deletions

View file

@ -61,8 +61,6 @@ const dict: Record<string, Record<string, string>> = {
cancel: "Cancel", cancel: "Cancel",
pin: "Pin", pin: "Pin",
unpin: "Unpin", unpin: "Unpin",
moveUp: "Move up",
moveDown: "Move down",
marketUpdated: "Market data updated", marketUpdated: "Market data updated",
exportImage: "Copy as image", exportImage: "Copy as image",
imageCopied: "Image copied", imageCopied: "Image copied",
@ -139,8 +137,6 @@ const dict: Record<string, Record<string, string>> = {
cancel: "Отмена", cancel: "Отмена",
pin: "Закрепить", pin: "Закрепить",
unpin: "Открепить", unpin: "Открепить",
moveUp: "Выше",
moveDown: "Ниже",
marketUpdated: "Рыночные данные обновлены", marketUpdated: "Рыночные данные обновлены",
exportImage: "Скопировать картинкой", exportImage: "Скопировать картинкой",
imageCopied: "Картинка скопирована", imageCopied: "Картинка скопирована",

View file

@ -81,24 +81,28 @@ function pinDoc(id: string): void {
renderDocList(); renderDocList();
} }
function moveDoc(id: string, dir: -1 | 1): void { /** drag id -> drop-target id; no-op across the pinned/unpinned boundary */
const idx = data.docs.findIndex((d) => d.id === id); function reorderDoc(srcId: string, targetId: string): void {
if (idx < 0) return; if (srcId === targetId) return;
const doc = data.docs[idx]; const src = data.docs.find((d) => d.id === srcId);
const target = data.docs[idx + dir]; const target = data.docs.find((d) => d.id === targetId);
if (!target || !!target.pinned !== !!doc.pinned) return; // can't cross pin boundary if (!src || !target || !!src.pinned !== !!target.pinned) return;
data.docs.splice(idx, 1); data.docs = data.docs.filter((d) => d.id !== srcId);
data.docs.splice(idx + dir, 0, doc); const targetIdx = data.docs.findIndex((d) => d.id === targetId);
data.docs.splice(targetIdx, 0, src);
saveAppData(data); saveAppData(data);
renderDocList(); renderDocList();
} }
let dragSrcId: string | null = null;
function renderDocList(): void { function renderDocList(): void {
const list = $("#doc-list"); const list = $("#doc-list");
list.replaceChildren(); list.replaceChildren();
for (const doc of data.docs) { for (const doc of data.docs) {
const el = document.createElement("div"); const el = document.createElement("div");
el.className = "doc-item" + (doc.id === data.activeId ? " active" : "") + (doc.pinned ? " pinned" : ""); el.className = "doc-item" + (doc.id === data.activeId ? " active" : "") + (doc.pinned ? " pinned" : "");
el.draggable = true;
const name = document.createElement("span"); const name = document.createElement("span");
name.className = "doc-name"; name.className = "doc-name";
name.textContent = doc.title || t("untitled"); name.textContent = doc.title || t("untitled");
@ -114,8 +118,31 @@ function renderDocList(): void {
}; };
el.appendChild(mkBtn("pin-btn" + (doc.pinned ? " active" : ""), "📌", doc.pinned ? t("unpin") : t("pin"), () => pinDoc(doc.id))); el.appendChild(mkBtn("pin-btn" + (doc.pinned ? " active" : ""), "📌", doc.pinned ? t("unpin") : t("pin"), () => pinDoc(doc.id)));
el.appendChild(mkBtn("move-btn", "↑", t("moveUp"), () => moveDoc(doc.id, -1)));
el.appendChild(mkBtn("move-btn", "↓", t("moveDown"), () => moveDoc(doc.id, 1))); el.addEventListener("dragstart", (e) => {
dragSrcId = doc.id;
el.classList.add("dragging");
e.dataTransfer!.effectAllowed = "move";
e.dataTransfer?.setData("text/plain", doc.id);
});
el.addEventListener("dragend", () => {
el.classList.remove("dragging");
dragSrcId = null;
for (const other of list.querySelectorAll(".doc-item")) other.classList.remove("drag-over");
});
el.addEventListener("dragover", (e) => {
if (!dragSrcId || dragSrcId === doc.id) return;
const src = data.docs.find((d) => d.id === dragSrcId);
if (!src || !!src.pinned !== !!doc.pinned) return; // can't cross the pin boundary
e.preventDefault(); // allow drop
for (const other of list.querySelectorAll(".doc-item")) other.classList.remove("drag-over");
el.classList.add("drag-over");
});
el.addEventListener("drop", (e) => {
e.preventDefault();
el.classList.remove("drag-over");
if (dragSrcId) reorderDoc(dragSrcId, doc.id);
});
const del = document.createElement("button"); const del = document.createElement("button");
del.className = "del"; del.className = "del";

View file

@ -102,7 +102,7 @@ body {
.doc-item { .doc-item {
padding: 7px 10px; padding: 7px 10px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: grab;
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -110,12 +110,15 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
border-top: 2px solid transparent;
transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease, border-top-color 0.15s ease;
} }
.doc-item:hover { background: var(--hover); } .doc-item:hover { background: var(--hover); }
.doc-item.active { background: var(--sel); } .doc-item.active { background: var(--sel); }
.doc-item.dragging { opacity: 0.4; }
.doc-item.drag-over { border-top-color: var(--accent); }
.doc-name { flex: 1; overflow: hidden; text-overflow: ellipsis; } .doc-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.doc-item .pin-btn, .doc-item .pin-btn,
.doc-item .move-btn,
.doc-item .del { .doc-item .del {
flex-shrink: 0; flex-shrink: 0;
opacity: 0; opacity: 0;
@ -130,7 +133,6 @@ body {
transition: opacity 0.15s ease, background-color 0.15s ease; transition: opacity 0.15s ease, background-color 0.15s ease;
} }
.doc-item:hover .pin-btn, .doc-item:hover .pin-btn,
.doc-item:hover .move-btn,
.doc-item:hover .del { opacity: 0.7; } .doc-item:hover .del { opacity: 0.7; }
.doc-item .pin-btn.active { opacity: 1; } .doc-item .pin-btn.active { opacity: 1; }
.doc-item:hover .pin-btn.active { opacity: 1; } .doc-item:hover .pin-btn.active { opacity: 1; }