Update app.js
Browse files
app.js
CHANGED
|
@@ -46,6 +46,13 @@ const els = {
|
|
| 46 |
cancelPassword: document.querySelector("#cancelPassword"),
|
| 47 |
submitPassword: document.querySelector("#submitPassword"),
|
| 48 |
passwordMessage: document.querySelector("#passwordMessage"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
folderList: document.querySelector("#folderList"),
|
| 50 |
newFolder: document.querySelector("#newFolder"),
|
| 51 |
listTitle: document.querySelector("#listTitle"),
|
|
@@ -242,35 +249,11 @@ document.addEventListener("click", (event) => {
|
|
| 242 |
els.mobileActionMenu.hidden = true;
|
| 243 |
});
|
| 244 |
|
| 245 |
-
els.newFolder.addEventListener("click",
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
createdAt: Date.now(),
|
| 251 |
-
updatedAt: Date.now(),
|
| 252 |
-
version: 1
|
| 253 |
-
};
|
| 254 |
-
|
| 255 |
-
state.folders.push(folder);
|
| 256 |
-
state.activeFolder = folder.id;
|
| 257 |
-
state.selectedId = null;
|
| 258 |
-
state.mobileView = "list";
|
| 259 |
-
render();
|
| 260 |
-
|
| 261 |
-
try {
|
| 262 |
-
const saved = await createFolder(folder);
|
| 263 |
-
Object.assign(folder, saved);
|
| 264 |
-
setSaveStatus("文件夹已创建", "ok");
|
| 265 |
-
renderFolders();
|
| 266 |
-
renderEditor();
|
| 267 |
-
} catch (error) {
|
| 268 |
-
state.folders = state.folders.filter((entry) => entry.id !== folder.id);
|
| 269 |
-
state.activeFolder = "all";
|
| 270 |
-
setSaveStatus("创建失败", "error");
|
| 271 |
-
render();
|
| 272 |
-
console.error("Failed to create folder.", error);
|
| 273 |
-
}
|
| 274 |
});
|
| 275 |
|
| 276 |
els.editor.addEventListener("input", (event) => {
|
|
@@ -318,6 +301,31 @@ els.changePassword.addEventListener("click", async () => {
|
|
| 318 |
|
| 319 |
els.cancelPassword.addEventListener("click", hidePasswordGate);
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
els.passwordForm.addEventListener("submit", async (event) => {
|
| 322 |
event.preventDefault();
|
| 323 |
const currentPassword = els.currentPassword.value;
|
|
@@ -444,6 +452,28 @@ function hidePasswordGate() {
|
|
| 444 |
els.passwordMessage.textContent = "";
|
| 445 |
}
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
function saveSelectedNote({ immediate = false } = {}) {
|
| 448 |
saveUiState();
|
| 449 |
const note = getSelectedNote();
|
|
@@ -536,6 +566,22 @@ async function createFolder(folder) {
|
|
| 536 |
return await response.json();
|
| 537 |
}
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
async function removeFolder(folderId) {
|
| 540 |
const response = await fetch(`/api/folders/${encodeURIComponent(folderId)}`, { method: "DELETE" });
|
| 541 |
if (!response.ok) throw new Error(`Delete folder API returned ${response.status}`);
|
|
@@ -645,7 +691,7 @@ function renderFolders() {
|
|
| 645 |
name: folder.name,
|
| 646 |
icon: "folder",
|
| 647 |
count: state.notes.filter((note) => note.folder === folder.id).length,
|
| 648 |
-
|
| 649 |
}));
|
| 650 |
|
| 651 |
const smartFolders = [allFolder, recentFolder].filter((folder) => (
|
|
@@ -695,7 +741,7 @@ function renderGroupedNotes(notes) {
|
|
| 695 |
|
| 696 |
function renderFolderItem(folder) {
|
| 697 |
const item = document.createElement("div");
|
| 698 |
-
item.className = `folder-item${folder.
|
| 699 |
|
| 700 |
const button = document.createElement("button");
|
| 701 |
button.className = `folder-row${folder.id === state.activeFolder ? " active" : ""}`;
|
|
@@ -708,6 +754,7 @@ function renderFolderItem(folder) {
|
|
| 708 |
icon.setAttribute("aria-hidden", "true");
|
| 709 |
|
| 710 |
const name = document.createElement("span");
|
|
|
|
| 711 |
name.textContent = folder.name;
|
| 712 |
|
| 713 |
const count = document.createElement("span");
|
|
@@ -726,7 +773,23 @@ function renderFolderItem(folder) {
|
|
| 726 |
|
| 727 |
item.append(button);
|
| 728 |
|
| 729 |
-
if (folder.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
const deleteButton = document.createElement("button");
|
| 731 |
deleteButton.className = "folder-delete";
|
| 732 |
deleteButton.type = "button";
|
|
@@ -928,6 +991,77 @@ async function deleteFolder(folderId) {
|
|
| 928 |
}
|
| 929 |
}
|
| 930 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
function getActiveFolderLabel() {
|
| 932 |
if (state.activeFolder === "all") return "所有笔记";
|
| 933 |
if (state.activeFolder === "recent") return "最近编辑";
|
|
|
|
| 46 |
cancelPassword: document.querySelector("#cancelPassword"),
|
| 47 |
submitPassword: document.querySelector("#submitPassword"),
|
| 48 |
passwordMessage: document.querySelector("#passwordMessage"),
|
| 49 |
+
folderGate: document.querySelector("#folderGate"),
|
| 50 |
+
folderForm: document.querySelector("#folderForm"),
|
| 51 |
+
folderTitle: document.querySelector("#folderTitle"),
|
| 52 |
+
folderName: document.querySelector("#folderName"),
|
| 53 |
+
cancelFolder: document.querySelector("#cancelFolder"),
|
| 54 |
+
submitFolder: document.querySelector("#submitFolder"),
|
| 55 |
+
folderMessage: document.querySelector("#folderMessage"),
|
| 56 |
folderList: document.querySelector("#folderList"),
|
| 57 |
newFolder: document.querySelector("#newFolder"),
|
| 58 |
listTitle: document.querySelector("#listTitle"),
|
|
|
|
| 249 |
els.mobileActionMenu.hidden = true;
|
| 250 |
});
|
| 251 |
|
| 252 |
+
els.newFolder.addEventListener("click", () => {
|
| 253 |
+
openFolderGate({
|
| 254 |
+
mode: "create",
|
| 255 |
+
name: getUniqueFolderName("新建文件夹")
|
| 256 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
});
|
| 258 |
|
| 259 |
els.editor.addEventListener("input", (event) => {
|
|
|
|
| 301 |
|
| 302 |
els.cancelPassword.addEventListener("click", hidePasswordGate);
|
| 303 |
|
| 304 |
+
els.cancelFolder.addEventListener("click", hideFolderGate);
|
| 305 |
+
|
| 306 |
+
els.folderForm.addEventListener("submit", async (event) => {
|
| 307 |
+
event.preventDefault();
|
| 308 |
+
const mode = els.folderForm.dataset.mode;
|
| 309 |
+
const folderId = els.folderForm.dataset.folderId;
|
| 310 |
+
const name = els.folderName.value.trim();
|
| 311 |
+
if (!name) {
|
| 312 |
+
els.folderMessage.textContent = "请输入文件夹名称";
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
els.submitFolder.disabled = true;
|
| 317 |
+
els.folderMessage.textContent = "";
|
| 318 |
+
try {
|
| 319 |
+
if (mode === "rename" && folderId) {
|
| 320 |
+
await submitFolderRename(folderId, name);
|
| 321 |
+
} else {
|
| 322 |
+
await submitFolderCreate(name);
|
| 323 |
+
}
|
| 324 |
+
} finally {
|
| 325 |
+
els.submitFolder.disabled = false;
|
| 326 |
+
}
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
els.passwordForm.addEventListener("submit", async (event) => {
|
| 330 |
event.preventDefault();
|
| 331 |
const currentPassword = els.currentPassword.value;
|
|
|
|
| 452 |
els.passwordMessage.textContent = "";
|
| 453 |
}
|
| 454 |
|
| 455 |
+
function openFolderGate({ mode, name, folderId = "" }) {
|
| 456 |
+
els.folderForm.dataset.mode = mode;
|
| 457 |
+
els.folderForm.dataset.folderId = folderId;
|
| 458 |
+
els.folderTitle.textContent = mode === "rename" ? "重命名文件夹" : "新建文件夹";
|
| 459 |
+
els.submitFolder.textContent = mode === "rename" ? "保存" : "创建";
|
| 460 |
+
els.folderName.value = name || "";
|
| 461 |
+
els.folderMessage.textContent = "";
|
| 462 |
+
els.folderGate.hidden = false;
|
| 463 |
+
requestAnimationFrame(() => {
|
| 464 |
+
els.folderName.focus();
|
| 465 |
+
els.folderName.select();
|
| 466 |
+
});
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
function hideFolderGate() {
|
| 470 |
+
els.folderGate.hidden = true;
|
| 471 |
+
els.folderForm.reset();
|
| 472 |
+
els.folderForm.dataset.mode = "";
|
| 473 |
+
els.folderForm.dataset.folderId = "";
|
| 474 |
+
els.folderMessage.textContent = "";
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
function saveSelectedNote({ immediate = false } = {}) {
|
| 478 |
saveUiState();
|
| 479 |
const note = getSelectedNote();
|
|
|
|
| 566 |
return await response.json();
|
| 567 |
}
|
| 568 |
|
| 569 |
+
async function updateFolder(folderId, patch) {
|
| 570 |
+
const response = await fetch(`/api/folders/${encodeURIComponent(folderId)}`, {
|
| 571 |
+
method: "PATCH",
|
| 572 |
+
headers: {
|
| 573 |
+
"content-type": "application/json"
|
| 574 |
+
},
|
| 575 |
+
body: JSON.stringify(patch)
|
| 576 |
+
});
|
| 577 |
+
const payload = await response.json().catch(() => ({}));
|
| 578 |
+
if (response.status === 409 && payload.folder) {
|
| 579 |
+
return { status: "conflict", folder: payload.folder };
|
| 580 |
+
}
|
| 581 |
+
if (!response.ok) throw new Error(`Update folder API returned ${response.status}`);
|
| 582 |
+
return payload;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
async function removeFolder(folderId) {
|
| 586 |
const response = await fetch(`/api/folders/${encodeURIComponent(folderId)}`, { method: "DELETE" });
|
| 587 |
if (!response.ok) throw new Error(`Delete folder API returned ${response.status}`);
|
|
|
|
| 691 |
name: folder.name,
|
| 692 |
icon: "folder",
|
| 693 |
count: state.notes.filter((note) => note.folder === folder.id).length,
|
| 694 |
+
manageable: folder.id !== "notes"
|
| 695 |
}));
|
| 696 |
|
| 697 |
const smartFolders = [allFolder, recentFolder].filter((folder) => (
|
|
|
|
| 741 |
|
| 742 |
function renderFolderItem(folder) {
|
| 743 |
const item = document.createElement("div");
|
| 744 |
+
item.className = `folder-item${folder.manageable ? " can-manage" : ""}`;
|
| 745 |
|
| 746 |
const button = document.createElement("button");
|
| 747 |
button.className = `folder-row${folder.id === state.activeFolder ? " active" : ""}`;
|
|
|
|
| 754 |
icon.setAttribute("aria-hidden", "true");
|
| 755 |
|
| 756 |
const name = document.createElement("span");
|
| 757 |
+
name.className = "folder-name";
|
| 758 |
name.textContent = folder.name;
|
| 759 |
|
| 760 |
const count = document.createElement("span");
|
|
|
|
| 773 |
|
| 774 |
item.append(button);
|
| 775 |
|
| 776 |
+
if (folder.manageable) {
|
| 777 |
+
const editButton = document.createElement("button");
|
| 778 |
+
editButton.className = "folder-edit";
|
| 779 |
+
editButton.type = "button";
|
| 780 |
+
editButton.setAttribute("aria-label", `重命名${folder.name}`);
|
| 781 |
+
editButton.title = "重命名文件夹";
|
| 782 |
+
editButton.innerHTML = '<i data-lucide="pencil" aria-hidden="true"></i>';
|
| 783 |
+
editButton.addEventListener("click", (event) => {
|
| 784 |
+
event.stopPropagation();
|
| 785 |
+
openFolderGate({
|
| 786 |
+
mode: "rename",
|
| 787 |
+
folderId: folder.id,
|
| 788 |
+
name: folder.name
|
| 789 |
+
});
|
| 790 |
+
});
|
| 791 |
+
item.append(editButton);
|
| 792 |
+
|
| 793 |
const deleteButton = document.createElement("button");
|
| 794 |
deleteButton.className = "folder-delete";
|
| 795 |
deleteButton.type = "button";
|
|
|
|
| 991 |
}
|
| 992 |
}
|
| 993 |
|
| 994 |
+
async function submitFolderCreate(name) {
|
| 995 |
+
const now = Date.now();
|
| 996 |
+
const previousActiveFolder = state.activeFolder;
|
| 997 |
+
const previousSelectedId = state.selectedId;
|
| 998 |
+
const folder = {
|
| 999 |
+
id: crypto.randomUUID(),
|
| 1000 |
+
name,
|
| 1001 |
+
createdAt: now,
|
| 1002 |
+
updatedAt: now,
|
| 1003 |
+
version: 1
|
| 1004 |
+
};
|
| 1005 |
+
|
| 1006 |
+
state.folders.push(folder);
|
| 1007 |
+
state.activeFolder = folder.id;
|
| 1008 |
+
state.selectedId = null;
|
| 1009 |
+
state.mobileView = "list";
|
| 1010 |
+
render();
|
| 1011 |
+
hideFolderGate();
|
| 1012 |
+
|
| 1013 |
+
try {
|
| 1014 |
+
const saved = await createFolder(folder);
|
| 1015 |
+
Object.assign(folder, saved);
|
| 1016 |
+
setSaveStatus("文件夹已创建", "ok");
|
| 1017 |
+
render();
|
| 1018 |
+
} catch (error) {
|
| 1019 |
+
state.folders = state.folders.filter((entry) => entry.id !== folder.id);
|
| 1020 |
+
state.activeFolder = previousActiveFolder;
|
| 1021 |
+
state.selectedId = previousSelectedId;
|
| 1022 |
+
setSaveStatus("创建失败", "error");
|
| 1023 |
+
render();
|
| 1024 |
+
console.error("Failed to create folder.", error);
|
| 1025 |
+
}
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
async function submitFolderRename(folderId, name) {
|
| 1029 |
+
const folder = state.folders.find((entry) => entry.id === folderId);
|
| 1030 |
+
if (!folder || folder.id === "notes") return;
|
| 1031 |
+
|
| 1032 |
+
const previousName = folder.name;
|
| 1033 |
+
const previousUpdatedAt = folder.updatedAt;
|
| 1034 |
+
const previousVersion = folder.version || 1;
|
| 1035 |
+
folder.name = name;
|
| 1036 |
+
folder.updatedAt = Date.now();
|
| 1037 |
+
folder.version = previousVersion + 1;
|
| 1038 |
+
render();
|
| 1039 |
+
hideFolderGate();
|
| 1040 |
+
|
| 1041 |
+
try {
|
| 1042 |
+
const result = await updateFolder(folder.id, {
|
| 1043 |
+
name: folder.name,
|
| 1044 |
+
updatedAt: folder.updatedAt,
|
| 1045 |
+
version: previousVersion
|
| 1046 |
+
});
|
| 1047 |
+
if (result.status === "conflict" && result.folder) {
|
| 1048 |
+
Object.assign(folder, result.folder);
|
| 1049 |
+
setSaveStatus("文件夹已被其他设备修改", "conflict");
|
| 1050 |
+
} else {
|
| 1051 |
+
Object.assign(folder, result);
|
| 1052 |
+
setSaveStatus("名称已更新", "ok");
|
| 1053 |
+
}
|
| 1054 |
+
render();
|
| 1055 |
+
} catch (error) {
|
| 1056 |
+
folder.name = previousName;
|
| 1057 |
+
folder.updatedAt = previousUpdatedAt;
|
| 1058 |
+
folder.version = previousVersion;
|
| 1059 |
+
setSaveStatus("重命名失败", "error");
|
| 1060 |
+
render();
|
| 1061 |
+
console.error("Failed to rename folder.", error);
|
| 1062 |
+
}
|
| 1063 |
+
}
|
| 1064 |
+
|
| 1065 |
function getActiveFolderLabel() {
|
| 1066 |
if (state.activeFolder === "all") return "所有笔记";
|
| 1067 |
if (state.activeFolder === "recent") return "最近编辑";
|