Firefox WebExtension (Desktop and Mobile) that lets you share the current tab to Margit.at, frontpage.fyi, etc. with minimal effort.
1const api = globalThis.browser ?? globalThis.chrome;
2const MAX_TITLE = 120;
3
4const form = document.getElementById("submit-form");
5const titleInput = document.getElementById("title");
6const urlInput = document.getElementById("url");
7const statusEl = document.getElementById("status");
8const titleCountEl = document.getElementById("title-count");
9const submitBtn = document.getElementById("submit-btn");
10const openOptionsBtn = document.getElementById("open-options");
11const openFrontpageLink = document.getElementById("open-frontpage");
12
13// Tabs
14const tabFrontpage = document.getElementById("tab-frontpage");
15const tabMargin = document.getElementById("tab-margin");
16const panelFrontpage = document.getElementById("panel-frontpage");
17const panelMargin = document.getElementById("panel-margin");
18
19// Margin panel
20const marginForm = document.getElementById("margin-form");
21const marginSelectionEl = document.getElementById("margin-selection");
22const marginCommentEl = document.getElementById("margin-comment");
23const marginSubmitBtn = document.getElementById("margin-submit-btn");
24const marginStatusEl = document.getElementById("margin-status");
25const openMarginLink = document.getElementById("open-margin");
26
27let activeTab = null; // the browser tab object
28
29function showStatus(message, isError = false) {
30 statusEl.textContent = message;
31 statusEl.className = isError ? "status error" : "status success";
32}
33
34function updateTitleCounter() {
35 const value = titleInput.value ?? "";
36 titleCountEl.textContent = `${value.length}/${MAX_TITLE}`;
37 if (value.length > MAX_TITLE) {
38 titleCountEl.classList.add("over-limit");
39 } else {
40 titleCountEl.classList.remove("over-limit");
41 }
42}
43
44async function populateFromTab() {
45 try {
46 const tabs = await api.tabs.query({ active: true, currentWindow: true });
47 const tab = tabs?.[0];
48 if (!tab) return;
49 activeTab = tab;
50 if (tab.title) {
51 titleInput.value = tab.title.trim().slice(0, MAX_TITLE);
52 }
53 if (tab.url && /^https?:/i.test(tab.url)) {
54 urlInput.value = tab.url;
55 }
56 updateTitleCounter();
57 } catch (error) {
58 console.error("Unable to read active tab", error);
59 }
60}
61
62function showMarginStatus(message, isError = false) {
63 marginStatusEl.textContent = message;
64 marginStatusEl.className = isError ? "status error" : "status success";
65}
66
67async function loadMarginSelection() {
68 marginSelectionEl.value = "";
69 showMarginStatus("");
70 if (!activeTab?.id) return;
71 if (!/^https?:/i.test(activeTab.url ?? "")) {
72 showMarginStatus("Margin annotations require an http/https page.", true);
73 return;
74 }
75 try {
76 const response = await api.tabs.sendMessage(activeTab.id, { type: "margin-get-selection" });
77 if (response?.selection?.exact) {
78 marginSelectionEl.value = response.selection.exact;
79 } else {
80 showMarginStatus("No text selected. Select text on the page and reopen.", true);
81 }
82 } catch {
83 showMarginStatus("Could not read selection. Try reloading the page.", true);
84 }
85}
86
87function switchTab(tab) {
88 if (tab === "margin") {
89 tabFrontpage.classList.remove("active");
90 tabFrontpage.setAttribute("aria-selected", "false");
91 tabMargin.classList.add("active");
92 tabMargin.setAttribute("aria-selected", "true");
93 panelFrontpage.hidden = true;
94 panelMargin.hidden = false;
95 loadMarginSelection();
96 } else {
97 tabMargin.classList.remove("active");
98 tabMargin.setAttribute("aria-selected", "false");
99 tabFrontpage.classList.add("active");
100 tabFrontpage.setAttribute("aria-selected", "true");
101 panelMargin.hidden = true;
102 panelFrontpage.hidden = false;
103 }
104}
105
106let hasAuth = false;
107
108async function checkAuth() {
109 try {
110 const response = await api.runtime.sendMessage({ type: "frontpage-get-auth" });
111 hasAuth = Boolean(response?.auth);
112 if (!hasAuth) {
113 showStatus("Configure your Frontpage credentials in the options page.", true);
114 } else {
115 showStatus("");
116 }
117 } catch (error) {
118 console.error("Failed to query auth state", error);
119 showStatus("Unable to read authentication state.", true);
120 }
121}
122
123form.addEventListener("submit", async (event) => {
124 event.preventDefault();
125 if (!hasAuth) {
126 showStatus("Configure your Frontpage credentials in the options page.", true);
127 return;
128 }
129 showStatus("");
130 submitBtn.disabled = true;
131 try {
132 const payload = {
133 title: titleInput.value,
134 url: urlInput.value
135 };
136 const response = await api.runtime.sendMessage({
137 type: "frontpage-submit",
138 payload
139 });
140 if (!response?.ok) {
141 throw new Error(response?.error ?? "Unknown error");
142 }
143 const uri = response?.result?.uri;
144 showStatus(uri ? `Posted! ${uri}` : "Posted to Frontpage!", false);
145 } catch (error) {
146 console.error("Submission failed", error);
147 showStatus(error.message, true);
148 } finally {
149 submitBtn.disabled = false;
150 }
151});
152
153titleInput.addEventListener("input", updateTitleCounter);
154
155openOptionsBtn.addEventListener("click", () => {
156 api.runtime.openOptionsPage();
157});
158
159openFrontpageLink.addEventListener("click", (event) => {
160 event.preventDefault();
161 api.tabs.create({ url: "https://frontpage.fyi" });
162});
163
164tabFrontpage.addEventListener("click", () => switchTab("frontpage"));
165tabMargin.addEventListener("click", () => switchTab("margin"));
166
167marginCommentEl.addEventListener("input", () => {
168 marginSubmitBtn.textContent = marginCommentEl.value.trim() ? "Annotate on Margin" : "Highlight on Margin";
169});
170
171marginForm.addEventListener("submit", async (event) => {
172 event.preventDefault();
173 if (!hasAuth) {
174 showMarginStatus("Configure your credentials in the options page.", true);
175 return;
176 }
177 const exact = marginSelectionEl.value.trim();
178 if (!exact) {
179 showMarginStatus("No text selected. Select text on the page and reopen.", true);
180 return;
181 }
182 showMarginStatus("");
183 marginSubmitBtn.disabled = true;
184 try {
185 const tabs = await api.tabs.query({ active: true, currentWindow: true });
186 const tab = tabs?.[0];
187 let selectionData = { exact, prefix: "", suffix: "" };
188 try {
189 const res = await api.tabs.sendMessage(tab.id, { type: "margin-get-selection" });
190 if (res?.selection) selectionData = res.selection;
191 } catch {
192 // use what we have
193 }
194 const payload = {
195 url: tab?.url ?? "",
196 title: tab?.title ?? "",
197 exact: selectionData.exact,
198 prefix: selectionData.prefix,
199 suffix: selectionData.suffix,
200 comment: marginCommentEl.value
201 };
202 const response = await api.runtime.sendMessage({ type: "margin-submit", payload });
203 if (!response?.ok) {
204 throw new Error(response?.error ?? "Unknown error");
205 }
206 const type = payload.comment.trim() ? "Annotation" : "Highlight";
207 showMarginStatus(`${type} published to Margin!`);
208 marginCommentEl.value = "";
209 } catch (error) {
210 console.error("Margin submission failed", error);
211 showMarginStatus(error.message, true);
212 } finally {
213 marginSubmitBtn.disabled = false;
214 }
215});
216
217openMarginLink.addEventListener("click", (event) => {
218 event.preventDefault();
219 api.tabs.create({ url: "https://margin.at" });
220});
221
222populateFromTab().then(() => switchTab("margin"));
223checkAuth();