popup.js (4418 bytes)
1 // The popup: search as you type, Enter or a click opens, "Add this page" adds. 2 "use strict"; 3 4 const api = globalThis.browser ?? globalThis.chrome; 5 const $ = (id) => document.getElementById(id); 6 const MAX_ROWS = 50; 7 let all = []; // newest first, as bm adds at the end 8 let shown = []; 9 10 function send(msg) { 11 return api.runtime.sendMessage(msg).then((r) => { 12 if (r && r.error) throw new Error(r.error); 13 return r && r.value; 14 }); 15 } 16 17 function ago(ms) { 18 const min = Math.round((Date.now() - ms) / 60000); 19 if (min < 1) return "just now"; 20 if (min < 60) return min + " min ago"; 21 return Math.round(min / 60) + " h ago"; 22 } 23 24 function showStatus(s) { 25 const n = all.length + (all.length === 1 ? " bookmark" : " bookmarks"); 26 let text = n + " in this browser. Sign in to sync in the options."; 27 if (s.error) text = n + ". Sync: " + s.error; 28 else if (s.token) text = n + (s.synced ? ", synced " + ago(s.synced) : "") + "."; 29 $("status").textContent = text; 30 } 31 32 function render() { 33 shown = Fuzzy.filter(all, $("search").value, (b) => b.desc + " " + b.tags.join(" ") + " " + b.url); 34 const list = $("list"); 35 list.replaceChildren(); 36 for (const b of shown.slice(0, MAX_ROWS)) { 37 const li = document.createElement("li"); 38 const a = document.createElement("a"); 39 a.href = b.url; 40 a.textContent = b.desc || b.url; 41 const small = document.createElement("small"); 42 small.textContent = [Tsv.host(b.url), b.tags.join(" ")].filter((x) => x).join(" ยท "); 43 a.append(document.createElement("br"), small); 44 a.addEventListener("click", (e) => { 45 e.preventDefault(); 46 open(b.url, e.ctrlKey || e.metaKey || e.button === 1); 47 }); 48 li.append(a); 49 list.append(li); 50 } 51 const empty = $("empty"); 52 empty.hidden = shown.length > 0; 53 empty.textContent = all.length === 0 ? "No bookmarks yet. Sign in to sync in the options, or add this page." 54 : "No bookmark matches. Press Enter to search the web."; 55 } 56 57 async function load() { 58 const s = await api.storage.local.get(["text", "token", "synced", "error"]); 59 all = Tsv.parse(s.text || "").reverse(); 60 render(); 61 showStatus(s); 62 return s; 63 } 64 65 /** Open in this tab, or in a new tab with Ctrl. */ 66 function open(url, newTab) { 67 if (newTab) api.tabs.create({ url }); 68 else api.tabs.update({ url }); 69 window.close(); 70 } 71 72 $("search").addEventListener("input", render); 73 $("search").addEventListener("keydown", (e) => { 74 if (e.key !== "Enter") return; 75 const text = $("search").value; 76 if (shown.length > 0) open(shown[0].url, e.ctrlKey || e.metaKey); 77 else if (text.trim() !== "") open(Tsv.target(text), e.ctrlKey || e.metaKey); 78 }); 79 80 $("options").addEventListener("click", (e) => { 81 e.preventDefault(); 82 api.runtime.openOptionsPage(); 83 window.close(); 84 }); 85 86 /** The form takes the place of the list while it is open. */ 87 function showForm(on) { 88 for (const id of ["search", "list", "add-page"]) $(id).hidden = on; 89 $("add").hidden = !on; 90 if (on) $("empty").hidden = true; 91 else render(); 92 } 93 94 $("add-page").addEventListener("click", async () => { 95 const [tab] = await api.tabs.query({ active: true, currentWindow: true }); 96 $("url").value = tab && tab.url ? tab.url : ""; 97 $("desc").value = tab && tab.title ? tab.title : ""; 98 $("tags").value = ""; 99 const known = [...new Set(all.flatMap((b) => b.tags))].sort(); 100 $("known").textContent = known.length ? "Tags in your file: " + known.join(" ") : ""; 101 showForm(true); 102 $("tags").focus(); 103 }); 104 105 $("cancel").addEventListener("click", () => showForm(false)); 106 107 $("add").addEventListener("submit", async (e) => { 108 e.preventDefault(); 109 const bookmark = { 110 url: $("url").value.replace(/\s/g, ""), 111 desc: Tsv.clean($("desc").value).trim(), 112 tags: $("tags").value.split(/\s+/).filter((t) => t !== ""), 113 }; 114 try { 115 const r = await send({ type: "add", bookmark }); 116 showForm(false); 117 await load(); 118 $("status").textContent = r.existing ? "Already a bookmark: " + (r.existing.desc || r.existing.url) : "Bookmark added."; 119 } catch (err) { 120 $("status").textContent = "Cannot add: " + err.message; 121 } 122 }); 123 124 api.storage.onChanged.addListener((changes, area) => { 125 if (area === "local" && (changes.text || changes.synced || changes.error)) load(); 126 }); 127 128 load().then((s) => { 129 // Sync when the popup opens, unless a sync ran in the last minute. 130 if (s.token && (!s.synced || Date.now() - s.synced > 60000)) send({ type: "sync" }).catch(() => {}); 131 });