Recently Written · git

sbm-extension

sbm bookmarks for Firefox and Chrome: fuzzy search, add the page, sync with bm and the sbm app

git clone https://github.com/equwal/sbm-extension

Log | Files | Refs


commit c29ea5cc96c7b8daca7cbccf6555110a311b3cf7
equwal <truex@equwal.com>
2026-09-24 00:43:33 -0700

Popup: remove the live preview

Most sites do not let a page of another site show them in a frame: they
send X-Frame-Options or a frame-ancestors policy. YouTube is one of them.
For these sites the frame of the preview stayed empty, so the preview did
not work for most bookmarks. An add-on can change this only with access
to all sites, which is too much for a bookmark add-on.

This reverts c915c68. Enter opens the first match again, and the options
have no preview setting. The new test test/popup.test.js checks that the
popup has no frame.

 README.md            |  13 ++-----
 lib/preview.js       |  31 ---------------
 options.html         |   7 ----
 options.js           |   6 ---
 popup.html           |   6 ---
 popup.js             | 105 +++++----------------------------------------------
 store/listing.md     |   3 +-
 style.css            |  16 --------
 test/popup.test.js   |  17 +++++++++
 test/preview.test.js |  56 ---------------------------
 10 files changed, 30 insertions(+), 230 deletions(-)
diff --git a/README.md b/README.md
index 0477313..4ad4d18 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,8 @@ Your [sbm](https://github.com/equwal/sbm) bookmarks in the browser.
 ([MP4](https://github.com/equwal/sbm-sync/raw/master/demo/sbm-demo.mp4), 80 seconds.)
 
 - **Search as you type** in the popup (Alt+Shift+M), with the fuzzy match of
-  fzf. Enter opens the highlighted match, first the top one. Text that is no
-  bookmark opens as an address, or as a web search, as in bm.
-- **Live preview:** the page of the highlighted bookmark shows beside the
-  list while you search. The arrow keys and the mouse move the highlight.
-  An http page shows with https. Some sites do not let other pages show
-  them, so their preview stays empty. Turn the preview off in the options.
+  fzf. Enter opens the first match. Text that is no bookmark opens as an
+  address, or as a web search, as in bm.
 - **Address bar:** type `bm`, a space, then words.
 - **Add this page** with a description and tags.
 - **Sync** with bm on your computers, the
@@ -54,16 +50,13 @@ your email address and your password to the server. The
 [privacy policy](https://sbmsync.com/privacy) tells what the server
 keeps.
 
-The live preview needs no permission. It loads the page from its site, as
-a visit does, without a referrer. It sends nothing to the sync server.
-
 ## Develop
 
 The add-on needs no build: load this directory as an unpacked extension
 (Chrome: chrome://extensions, developer mode; Firefox: about:debugging).
 
     npm install
-    npm test          # property tests of the file format, the search and the preview
+    npm test          # property tests of the file format and the search
     npm run lint      # the checks of addons.mozilla.org, on the Firefox package
     npm run build     # the zip for the Chrome Web Store, in web-ext-artifacts/
     npm run build:firefox  # the zip for addons.mozilla.org
diff --git a/lib/preview.js b/lib/preview.js
deleted file mode 100644
index 68241dc..0000000
--- a/lib/preview.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// The live preview of the popup: the page of the highlighted bookmark shows
-// in a frame beside the list. This file has no browser code, so the tests
-// run in node.
-"use strict";
-
-const Preview = {
-  /**
-   * The address that the frame loads for the URL of a bookmark, or null when
-   * the page cannot show in a frame. Only web pages can show. An http:// page
-   * loads as https://, because the popup is a secure page, and a secure page
-   * cannot show an insecure frame.
-   */
-  address(url) {
-    let u;
-    try {
-      u = new URL(url);
-    } catch (e) {
-      return null;
-    }
-    if (u.protocol !== "http:" && u.protocol !== "https:") return null;
-    u.protocol = "https:";
-    return u.href;
-  },
-
-  /** The row that is highlighted after a move of step rows from row i, in a list of n rows. */
-  move(i, step, n) {
-    return Math.max(0, Math.min(n - 1, i + step));
-  },
-};
-
-if (typeof module !== "undefined") module.exports = Preview;
diff --git a/options.html b/options.html
index 8723c2f..64ce1cb 100644
--- a/options.html
+++ b/options.html
@@ -37,13 +37,6 @@ other browsers.</p>
   <p class="note"><a id="account" href="https://sbmsync.com/account" target="_blank" rel="noopener">Your account</a></p>
 </section>
 
-<section>
-  <h2>Preview</h2>
-  <label class="check"><input id="preview" type="checkbox"> Show the page of the highlighted bookmark in the popup</label>
-  <p class="note">The preview loads the page from its site, as a visit does. Some
-  sites do not let other pages show them, so their preview stays empty.</p>
-</section>
-
 <p id="message" role="status"></p>
 
 <footer>
diff --git a/options.js b/options.js
index d8e5d3f..0220df8 100644
--- a/options.js
+++ b/options.js
@@ -150,10 +150,4 @@ $("sign-out").addEventListener("click", async () => {
   show();
 });
 
-// The preview in the popup is on until the user turns it off.
-api.storage.local.get("preview").then((s) => {
-  $("preview").checked = s.preview !== false;
-});
-$("preview").addEventListener("change", () => api.storage.local.set({ preview: $("preview").checked }));
-
 show();
diff --git a/popup.html b/popup.html
index bc83da7..1c570a7 100644
--- a/popup.html
+++ b/popup.html
@@ -23,14 +23,8 @@
   <span id="status" class="note"></span>
   <nav><a href="#" id="options">Options</a> ยท <a href="https://ko-fi.com/truex" target="_blank" rel="noopener">Support sbm on Ko-fi</a></nav>
 </footer>
-
-<section id="preview" hidden>
-  <p id="caption" class="note">Type a search, or point at a bookmark, to see its page here.</p>
-  <iframe id="frame" title="Preview of the page" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"></iframe>
-</section>
 <script src="lib/tsv.js"></script>
 <script src="lib/fuzzy.js"></script>
-<script src="lib/preview.js"></script>
 <script src="popup.js"></script>
 </body>
 </html>
diff --git a/popup.js b/popup.js
index 1ddf98b..de33da9 100644
--- a/popup.js
+++ b/popup.js
@@ -1,21 +1,11 @@
 // The popup: search as you type, Enter or a click opens, "Add this page" adds.
-// The page of the highlighted bookmark shows live in a preview beside the list.
 "use strict";
 
 const api = globalThis.browser ?? globalThis.chrome;
 const $ = (id) => document.getElementById(id);
 const MAX_ROWS = 50;
-// The preview loads a page only after the highlight stays on it this long
-// (ms), so that fast typing does not load a page for each letter.
-const PREVIEW_WAIT = 300;
 let all = []; // newest first, as bm adds at the end
 let shown = [];
-let sel = 0; // the highlighted row: Enter opens it
-let preview = true; // the options can turn the preview off
-let started = false; // the preview starts when the user types or points at a bookmark
-let framed = null; // the address in the frame
-let timer;
-let pointer = ""; // where the mouse was last
 
 function send(msg) {
   return api.runtime.sendMessage(msg).then((r) => {
@@ -39,12 +29,11 @@ function showStatus(s) {
   $("status").textContent = text;
 }
 
-/** Show the bookmarks that match the search. The highlight goes to the row of keep, or else to the first row. */
-function render(keep) {
+function render() {
   shown = Fuzzy.filter(all, $("search").value, (b) => b.desc + " " + b.tags.join(" ") + " " + b.url);
   const list = $("list");
   list.replaceChildren();
-  shown.slice(0, MAX_ROWS).forEach((b, i) => {
+  for (const b of shown.slice(0, MAX_ROWS)) {
     const li = document.createElement("li");
     const a = document.createElement("a");
     a.href = b.url;
@@ -56,76 +45,19 @@ function render(keep) {
       e.preventDefault();
       open(b.url, e.ctrlKey || e.metaKey || e.button === 1);
     });
-    a.addEventListener("mousemove", (e) => {
-      // The browser also sends mousemove when the list moves under a mouse
-      // that stays still. Only a move of the mouse moves the highlight.
-      const at = e.screenX + "," + e.screenY;
-      if (at === pointer) return;
-      pointer = at;
-      point(i);
-    });
-    a.addEventListener("focus", () => point(i));
     li.append(a);
     list.append(li);
-  });
+  }
   const empty = $("empty");
   empty.hidden = shown.length > 0;
   empty.textContent = all.length === 0 ? "No bookmarks yet. Sign in to sync in the options, or add this page."
     : "No bookmark matches. Press Enter to search the web.";
-  const again = keep ? shown.findIndex((b) => b.url === keep.url) : -1;
-  highlight(again >= 0 && again < MAX_ROWS ? again : 0);
-}
-
-/** The user points at row i: with the mouse, the Tab key or the arrow keys. */
-function point(i) {
-  started = true;
-  highlight(i);
-}
-
-/** Highlight row i, and show its page in the preview. */
-function highlight(i) {
-  const rows = $("list").children;
-  sel = Preview.move(i, 0, rows.length);
-  for (let j = 0; j < rows.length; j++) rows[j].classList.toggle("sel", j === sel);
-  if (started) showPage(rows.length > 0 ? shown[sel] : undefined);
-}
-
-/** Show the page of bookmark b in the preview, when the highlight stays on it. */
-function showPage(b) {
-  clearTimeout(timer);
-  if (!preview) return;
-  const address = b ? Preview.address(b.url) : null;
-  $("caption").textContent = !b ? "No bookmark to preview."
-    : address ? address : "No preview: " + b.url + " is not a web page.";
-  if (address) timer = setTimeout(() => frame(address), PREVIEW_WAIT);
-  else frame(null);
-}
-
-/** Load address in the frame, or an empty page for null. */
-function frame(address) {
-  if (address === framed) return;
-  framed = address;
-  $("frame").src = address || "about:blank";
-}
-
-/**
- * Show or hide the preview. On a computer it goes beside the list. On
- * Android the popup fills the narrow screen, so it goes under the search.
- */
-function showPreview(on) {
-  const android = /Android/.test(navigator.userAgent);
-  $("preview").hidden = !on;
-  document.body.classList.toggle("wide", on && !android);
-  document.body.classList.toggle("stack", on && android);
-  if (!on) frame(null);
 }
 
 async function load() {
-  const s = await api.storage.local.get(["text", "token", "synced", "error", "preview"]);
+  const s = await api.storage.local.get(["text", "token", "synced", "error"]);
   all = Tsv.parse(s.text || "").reverse();
-  preview = s.preview !== false;
-  showPreview(preview && $("add").hidden);
-  render(shown[sel]);
+  render();
   showStatus(s);
   return s;
 }
@@ -137,43 +69,24 @@ function open(url, newTab) {
   window.close();
 }
 
-$("search").addEventListener("input", () => {
-  started = true;
-  render();
-});
+$("search").addEventListener("input", render);
 $("search").addEventListener("keydown", (e) => {
-  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
-    e.preventDefault();
-    point(sel + (e.key === "ArrowDown" ? 1 : -1));
-    const row = $("list").children[sel];
-    if (row) row.scrollIntoView({ block: "nearest" });
-    return;
-  }
   if (e.key !== "Enter") return;
   const text = $("search").value;
-  if (shown.length > 0) open(shown[sel].url, e.ctrlKey || e.metaKey);
+  if (shown.length > 0) open(shown[0].url, e.ctrlKey || e.metaKey);
   else if (text.trim() !== "") open(Tsv.target(text), e.ctrlKey || e.metaKey);
 });
 
-// A page in the preview can take the focus with a script, and then the
-// typing goes into that page. The preview is only to look at, so the focus
-// goes back to the search at once. The mouse wheel and clicks on links still
-// work in the preview.
-window.addEventListener("blur", () => setTimeout(() => {
-  if (document.activeElement === $("frame") && !$("search").hidden) $("search").focus();
-}));
-
 $("options").addEventListener("click", (e) => {
   e.preventDefault();
   api.runtime.openOptionsPage();
   window.close();
 });
 
-/** The form takes the place of the list and the preview while it is open. */
+/** The form takes the place of the list while it is open. */
 function showForm(on) {
   for (const id of ["search", "list", "add-page"]) $(id).hidden = on;
   $("add").hidden = !on;
-  showPreview(preview && !on);
   if (on) $("empty").hidden = true;
   else render();
 }
@@ -209,7 +122,7 @@ $("add").addEventListener("submit", async (e) => {
 });
 
 api.storage.onChanged.addListener((changes, area) => {
-  if (area === "local" && (changes.text || changes.synced || changes.error || changes.preview)) load();
+  if (area === "local" && (changes.text || changes.synced || changes.error)) load();
 });
 
 load().then((s) => {
diff --git a/store/listing.md b/store/listing.md
index 21899af..a4a8e0c 100644
--- a/store/listing.md
+++ b/store/listing.md
@@ -21,8 +21,7 @@ Your sbm bookmarks in the browser: search as you type, add the page, sync with b
 
 sbm keeps your bookmarks in one plain text file: one bookmark per line, with the address, a description and tags. This add-on brings that file to your browser.
 
-- Search as you type: press Alt+Shift+M or click the icon. The search is fuzzy, as in fzf. Enter opens the highlighted match. Text that is no bookmark opens as an address, or as a web search.
-- Live preview: the page of the highlighted bookmark shows beside the list while you search. Move the highlight with the arrow keys or the mouse. Some sites do not let other pages show them. Turn the preview off in the options.
+- Search as you type: press Alt+Shift+M or click the icon. The search is fuzzy, as in fzf. Enter opens the first match. Text that is no bookmark opens as an address, or as a web search.
 - In the address bar, type bm, a space, then words.
 - Add the page that you look at, with a description and tags.
 - Sync with bm on your computers, the sbm app for Android and your other browsers, through an sbm-sync server. The server is free software: use the default one, sbmsync.com, or run your own. Sync on sbmsync.com is free.
diff --git a/style.css b/style.css
index ff06b2f..50adfe8 100644
--- a/style.css
+++ b/style.css
@@ -7,31 +7,15 @@ a { color: var(--accent); }
 input { font: inherit; box-sizing: border-box; width: 100%; padding: 6px; margin: 2px 0 8px; }
 button { font: inherit; padding: 4px 12px; }
 label { display: block; }
-label.check input { width: auto; margin: 0 6px 0 0; }
 
 .popup { width: 380px; padding: 8px; }
 .popup #list { list-style: none; margin: 0; padding: 0; max-height: 380px; overflow-y: auto; }
 .popup #list a { display: block; padding: 6px 4px; border-bottom: 1px solid var(--line); text-decoration: none; color: inherit; }
 .popup #list a:hover, .popup #list a:focus { background: color-mix(in srgb, var(--accent) 15%, transparent); }
 .popup #list small { color: var(--muted); }
-.popup #list .sel a { background: color-mix(in srgb, var(--accent) 15%, transparent); }
 .popup footer { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
 .popup nav { font-size: 12px; }
 
-/* The preview of the highlighted page: beside the list on a computer, under
-   the search on Android. The frame is white, as a browser shows a page. */
-.popup #preview[hidden] { display: none; }
-.popup #caption { margin: 0 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.popup #frame { flex: 1; width: 100%; box-sizing: border-box; border: 1px solid var(--line); background: #fff; }
-.popup.wide { width: 780px; display: grid; grid-template-columns: 380px minmax(0, 1fr); column-gap: 12px; align-items: start; }
-.popup.wide > * { grid-column: 1; }
-.popup.wide #preview { grid-column: 2; grid-row: 1 / span 5; align-self: stretch; display: flex; flex-direction: column; }
-.popup.wide #frame { min-height: 460px; }
-.popup.stack { width: auto; display: flex; flex-direction: column; }
-.popup.stack #search { order: -2; }
-.popup.stack #preview { order: -1; margin-bottom: 8px; }
-.popup.stack #frame { height: 50vh; }
-
 .options { max-width: 36em; margin: 2em auto; padding: 0 1em; font-size: 15px; }
 .options h1 { color: var(--accent); }
 .options section { margin: 1.5em 0; }
diff --git a/test/popup.test.js b/test/popup.test.js
new file mode 100644
index 0000000..799294c
--- /dev/null
+++ b/test/popup.test.js
@@ -0,0 +1,17 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const fs = require("node:fs");
+const path = require("node:path");
+
+const read = (file) => fs.readFileSync(path.join(__dirname, "..", file), "utf8");
+
+// Most sites do not let a page of another site show them in a frame
+// (X-Frame-Options or frame-ancestors), YouTube among them. A preview of a
+// bookmark in a frame of the popup stays empty for them, so the popup has
+// no frame.
+test("the popup shows no page of another site in a frame", () => {
+  for (const file of ["popup.html", "popup.js"]) {
+    assert.doesNotMatch(read(file), /iframe/i, file);
+  }
+});
diff --git a/test/preview.test.js b/test/preview.test.js
deleted file mode 100644
index a469c9a..0000000
--- a/test/preview.test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-"use strict";
-const test = require("node:test");
-const assert = require("node:assert/strict");
-const fc = require("fast-check");
-const Preview = require("../lib/preview.js");
-
-const https = fc.webUrl({ validSchemes: ["https"], withQueryParameters: true, withFragments: true });
-const http = fc.webUrl({ validSchemes: ["http"], withQueryParameters: true, withFragments: true });
-
-test("an https page shows at its own address", () => {
-  fc.assert(fc.property(https, (u) => {
-    assert.equal(Preview.address(u), new URL(u).href);
-  }), { numRuns: 1000 });
-});
-
-test("an http page shows at the same address with https", () => {
-  fc.assert(fc.property(http, (u) => {
-    const shown = new URL(Preview.address(u));
-    const asked = new URL(u);
-    assert.equal(shown.protocol, "https:");
-    assert.equal(shown.host, asked.host);
-    assert.equal(shown.pathname + shown.search + shown.hash, asked.pathname + asked.search + asked.hash);
-  }), { numRuns: 1000 });
-});
-
-test("the frame only ever loads https addresses", () => {
-  fc.assert(fc.property(fc.oneof(https, http, fc.string(), fc.webUrl({ validSchemes: ["ftp", "file", "data"] })), (u) => {
-    const a = Preview.address(u);
-    if (a !== null) {
-      assert.equal(new URL(a).protocol, "https:");
-      assert.equal(Preview.address(a), a);
-    }
-  }), { numRuns: 2000 });
-});
-
-test("addresses that are no web page have no preview", () => {
-  for (const u of ["javascript:alert(1)", "JavaScript:alert(1)", "data:text/html,<b>x</b>", "file:///etc/passwd",
-    "ftp://example.org/", "about:blank", "chrome://settings", "example.org", "", "https://", "http://"]) {
-    assert.equal(Preview.address(u), null, u);
-  }
-});
-
-test("the highlight stays on a row of the list", () => {
-  fc.assert(fc.property(fc.integer({ min: 1, max: 60 }), fc.integer({ min: -5, max: 70 }), fc.integer({ min: -100, max: 100 }),
-    (n, i, step) => {
-      const j = Preview.move(i, step, n);
-      assert.ok(j >= 0 && j < n, `${i} + ${step} in ${n} rows gave ${j}`);
-    }), { numRuns: 1000 });
-});
-
-test("a move inside the list goes exactly that many rows", () => {
-  fc.assert(fc.property(fc.integer({ min: 1, max: 60 }).chain((n) => fc.tuple(fc.constant(n), fc.nat(n - 1), fc.nat(n - 1))),
-    ([n, i, j]) => {
-      assert.equal(Preview.move(i, j - i, n), j);
-    }), { numRuns: 1000 });
-});