mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 20:21:58 +03:00
sheet organization: pin and reorder with up/down arrows
This commit is contained in:
parent
f8f9095fca
commit
4b5381d7e7
4 changed files with 71 additions and 6 deletions
|
|
@ -58,6 +58,10 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
closeSheet: "Close this sheet?",
|
closeSheet: "Close this sheet?",
|
||||||
close: "Close",
|
close: "Close",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
|
pin: "Pin",
|
||||||
|
unpin: "Unpin",
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
settings: "Настройки",
|
settings: "Настройки",
|
||||||
|
|
@ -116,6 +120,10 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
closeSheet: "Закрыть этот лист?",
|
closeSheet: "Закрыть этот лист?",
|
||||||
close: "Закрыть",
|
close: "Закрыть",
|
||||||
cancel: "Отмена",
|
cancel: "Отмена",
|
||||||
|
pin: "Закрепить",
|
||||||
|
unpin: "Открепить",
|
||||||
|
moveUp: "Выше",
|
||||||
|
moveDown: "Ниже",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
51
src/main.ts
51
src/main.ts
|
|
@ -54,19 +54,65 @@ function syncTitleField(): void {
|
||||||
if (document.activeElement !== field) field.value = activeDoc()?.title ?? "";
|
if (document.activeElement !== field) field.value = activeDoc()?.title ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pinDoc(id: string): void {
|
||||||
|
const doc = data.docs.find((d) => d.id === id);
|
||||||
|
if (!doc) return;
|
||||||
|
doc.pinned = !doc.pinned;
|
||||||
|
if (doc.pinned) {
|
||||||
|
// move to end of pinned group
|
||||||
|
data.docs = data.docs.filter((d) => d.id !== id);
|
||||||
|
let lastPinned = -1;
|
||||||
|
for (let i = data.docs.length - 1; i >= 0; i--) { if (data.docs[i].pinned) { lastPinned = i; break; } }
|
||||||
|
data.docs.splice(lastPinned + 1, 0, doc);
|
||||||
|
} else {
|
||||||
|
// move to start of non-pinned group
|
||||||
|
data.docs = data.docs.filter((d) => d.id !== id);
|
||||||
|
const firstNonPinned = data.docs.findIndex((d) => !d.pinned);
|
||||||
|
data.docs.splice(firstNonPinned === -1 ? data.docs.length : firstNonPinned, 0, doc);
|
||||||
|
}
|
||||||
|
saveAppData(data);
|
||||||
|
renderDocList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDoc(id: string, dir: -1 | 1): void {
|
||||||
|
const idx = data.docs.findIndex((d) => d.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
const doc = data.docs[idx];
|
||||||
|
const target = data.docs[idx + dir];
|
||||||
|
if (!target || !!target.pinned !== !!doc.pinned) return; // can't cross pin boundary
|
||||||
|
data.docs.splice(idx, 1);
|
||||||
|
data.docs.splice(idx + dir, 0, doc);
|
||||||
|
saveAppData(data);
|
||||||
|
renderDocList();
|
||||||
|
}
|
||||||
|
|
||||||
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" : "");
|
el.className = "doc-item" + (doc.id === data.activeId ? " active" : "") + (doc.pinned ? " pinned" : "");
|
||||||
const name = document.createElement("span");
|
const name = document.createElement("span");
|
||||||
|
name.className = "doc-name";
|
||||||
name.textContent = doc.title || t("untitled");
|
name.textContent = doc.title || t("untitled");
|
||||||
el.appendChild(name);
|
el.appendChild(name);
|
||||||
|
|
||||||
|
const mkBtn = (cls: string, text: string, title: string, cb: () => void): HTMLButtonElement => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.className = cls;
|
||||||
|
b.textContent = text;
|
||||||
|
b.title = title;
|
||||||
|
b.addEventListener("click", (e) => { e.stopPropagation(); cb(); });
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
const del = document.createElement("button");
|
const del = document.createElement("button");
|
||||||
del.className = "del";
|
del.className = "del";
|
||||||
del.textContent = "✕";
|
del.textContent = "✕";
|
||||||
// two-step inline confirmation instead of the native dialog
|
|
||||||
let confirmTimer: ReturnType<typeof setTimeout> | null = null;
|
let confirmTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
del.addEventListener("click", (e) => {
|
del.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -81,7 +127,6 @@ function renderDocList(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (confirmTimer) clearTimeout(confirmTimer);
|
if (confirmTimer) clearTimeout(confirmTimer);
|
||||||
// the sheet goes to backups/deleted/ — restore by dragging it back
|
|
||||||
void backupDeletedSheet(settings.dataDir, doc.title, data.contents[doc.id] ?? "");
|
void backupDeletedSheet(settings.dataDir, doc.title, data.contents[doc.id] ?? "");
|
||||||
delete data.contents[doc.id];
|
delete data.contents[doc.id];
|
||||||
data.docs = data.docs.filter((d) => d.id !== doc.id);
|
data.docs = data.docs.filter((d) => d.id !== doc.id);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface DocMeta {
|
||||||
title: string;
|
title: string;
|
||||||
/** user renamed the document — stop deriving the title from the first line */
|
/** user renamed the document — stop deriving the title from the first line */
|
||||||
customTitle?: boolean;
|
customTitle?: boolean;
|
||||||
|
pinned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppData {
|
export interface AppData {
|
||||||
|
|
|
||||||
|
|
@ -83,23 +83,34 @@ body {
|
||||||
}
|
}
|
||||||
.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-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.doc-item .pin-btn,
|
||||||
|
.doc-item .move-btn,
|
||||||
.doc-item .del {
|
.doc-item .del {
|
||||||
margin-left: auto;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px 6px;
|
padding: 2px 4px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
.doc-item:hover .pin-btn,
|
||||||
|
.doc-item:hover .move-btn,
|
||||||
|
.doc-item:hover .del { opacity: 0.7; }
|
||||||
|
.doc-item .pin-btn.active { opacity: 1; }
|
||||||
|
.doc-item:hover .pin-btn.active { opacity: 1; }
|
||||||
|
.doc-item .del { font-size: 12px; padding: 2px 6px; }
|
||||||
.doc-item:hover .del { opacity: 1; }
|
.doc-item:hover .del { opacity: 1; }
|
||||||
.doc-item .del.confirm {
|
.doc-item .del.confirm {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: #d63a3a;
|
background: #d63a3a;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.doc-item.pinned .doc-name { font-weight: 500; }
|
||||||
#new-doc {
|
#new-doc {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
background: none;
|
background: none;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue