diff --git a/assets/javascripts/discourse/initializers/bookmark-url.js b/assets/javascripts/discourse/initializers/bookmark-url.js
index 711df23..dae6979 100644
--- a/assets/javascripts/discourse/initializers/bookmark-url.js
+++ b/assets/javascripts/discourse/initializers/bookmark-url.js
@@ -17,197 +17,204 @@ const STRINGS = {
};
export default apiInitializer("1.8.0", (api) => {
- if (!api.container.lookup("site-settings:main").bookmark_url_enabled) {
+ if (!api.container.lookup("service:site-settings").bookmark_url_enabled) {
return;
}
- api.modifyClass("component:composer-editor", {
- pluginId: "bookmark-url",
+ let titleInputEl = null;
+ let titlePasteHandler = null;
+ let pendingUrl = null;
- didInsertElement() {
- this._super(...arguments);
- this._attachTitlePasteListener();
- },
+ // ---- Bar UI -------------------------------------------------------
- willDestroyElement() {
- this._super(...arguments);
- if (this._titleInputEl && this._titlePasteHandler) {
- this._titleInputEl.removeEventListener(
- "paste",
- this._titlePasteHandler,
- true
- );
- }
- },
+ function showArticleBar(url) {
+ hideArticleBar();
- // ---- Title paste interception -------------------------------------
- // capture:true so our handler runs before Discourse's, then
- // preventDefault + stopImmediatePropagation so Discourse never sees
- // the paste event — no title lookup, no onebox race.
+ const bar = document.createElement("div");
+ bar.className = "bookmark-url-bar";
+ bar.innerHTML = `
+ 📄
+ ${STRINGS.bar_label}
+
+
+
+ `;
- _attachTitlePasteListener(attempts = 0) {
- const input = document.querySelector("#reply-title");
- if (input) {
- this._titleInputEl = input;
- this._titlePasteHandler = (e) => {
- const text = (e.clipboardData || window.clipboardData)
- .getData("text/plain")
- .trim();
- if (!URL_REGEX.test(text)) return;
+ bar.querySelector(".bookmark-url-btn").addEventListener("click", () => {
+ fetchAndPopulate(url);
+ });
- e.preventDefault();
- e.stopImmediatePropagation();
+ bar.querySelector(".bookmark-url-onebox-btn").addEventListener("click", () => {
+ hideArticleBar();
+ commitUrlToModel();
+ });
- // Show URL in the field visually without going through Ember binding
- input.value = text;
- this._pendingUrl = text;
- this._showArticleBar(text);
- };
- input.addEventListener("paste", this._titlePasteHandler, true);
- } else if (attempts < 15) {
- setTimeout(() => this._attachTitlePasteListener(attempts + 1), 200);
- }
- },
+ bar.querySelector(".bookmark-url-dismiss").addEventListener("click", () => {
+ hideArticleBar();
+ pendingUrl = null;
+ });
- // ---- Bar UI -------------------------------------------------------
+ const container = document.querySelector(".d-editor-container");
+ if (container) {
+ container.parentElement.insertBefore(bar, container);
+ } else {
+ document
+ .querySelector(".composer-fields")
+ ?.insertAdjacentElement("afterbegin", bar);
+ }
+ }
- _showArticleBar(url) {
- this._hideArticleBar();
+ function hideArticleBar() {
+ document
+ .querySelectorAll(".bookmark-url-bar")
+ .forEach((el) => el.remove());
+ }
- const bar = document.createElement("div");
- bar.className = "bookmark-url-bar";
- bar.innerHTML = `
- 📄
- ${STRINGS.bar_label}
-
-
-
- `;
+ function setStatus(message, type = "info") {
+ const bar = document.querySelector(".bookmark-url-bar");
+ if (!bar) return;
+ let status = bar.querySelector(".bookmark-url-status");
+ if (!status) {
+ status = document.createElement("span");
+ status.className = "bookmark-url-status";
+ bar.appendChild(status);
+ }
+ status.textContent = message;
+ status.className = `bookmark-url-status bookmark-url-status--${type}`;
+ }
- bar.querySelector(".bookmark-url-btn").addEventListener("click", () => {
- this._fetchAndPopulate(url);
+ // Release the URL into Ember's data-binding so Discourse handles it normally
+ function commitUrlToModel() {
+ const input = titleInputEl;
+ const url = pendingUrl;
+ if (!input || !url) return;
+ input.value = url;
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ pendingUrl = null;
+ }
+
+ // ---- Fetch & populate ---------------------------------------------
+
+ async function fetchAndPopulate(url) {
+ const bar = document.querySelector(".bookmark-url-bar");
+ const btn = bar?.querySelector(".bookmark-url-btn");
+
+ if (btn) {
+ btn.disabled = true;
+ btn.textContent = STRINGS.fetching;
+ }
+ setStatus(STRINGS.fetching, "info");
+
+ try {
+ const data = await ajax("/bookmark-url/extract", {
+ type: "POST",
+ data: { url },
});
- bar.querySelector(".bookmark-url-onebox-btn").addEventListener("click", () => {
- this._hideArticleBar();
- this._commitUrlToModel();
- });
-
- bar.querySelector(".bookmark-url-dismiss").addEventListener("click", () => {
- this._hideArticleBar();
- this._pendingUrl = null;
- });
-
- const container = this.element.querySelector(".d-editor-container");
- if (container) {
- container.parentElement.insertBefore(bar, container);
- } else {
- this.element.insertAdjacentElement("afterbegin", bar);
- }
- },
-
- _hideArticleBar() {
- this.element
- ?.querySelectorAll(".bookmark-url-bar")
- .forEach((el) => el.remove());
- },
-
- _setStatus(message, type = "info") {
- const bar = this.element?.querySelector(".bookmark-url-bar");
- if (!bar) return;
- let status = bar.querySelector(".bookmark-url-status");
- if (!status) {
- status = document.createElement("span");
- status.className = "bookmark-url-status";
- bar.appendChild(status);
- }
- status.textContent = message;
- status.className = `bookmark-url-status bookmark-url-status--${type}`;
- },
-
- // Release the URL into Ember's data-binding so Discourse handles it normally
- _commitUrlToModel() {
- const input = this._titleInputEl;
- const url = this._pendingUrl;
- if (!input || !url) return;
- input.value = url;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- this._pendingUrl = null;
- },
-
- // ---- Fetch & populate ---------------------------------------------
-
- async _fetchAndPopulate(url) {
- const bar = this.element?.querySelector(".bookmark-url-bar");
- const btn = bar?.querySelector(".bookmark-url-btn");
+ if (data.error) throw new Error(data.error);
+ populateComposer(data);
+ setStatus(STRINGS.success, "success");
+ setTimeout(() => hideArticleBar(), 3000);
+ } catch (err) {
+ const msg =
+ err.jqXHR?.responseJSON?.error ||
+ err.message ||
+ STRINGS.error_generic;
+ setStatus(`${STRINGS.error_prefix} ${msg}`, "error");
if (btn) {
- btn.disabled = true;
- btn.textContent = STRINGS.fetching;
+ btn.disabled = false;
+ btn.textContent = STRINGS.retry_button;
}
- this._setStatus(STRINGS.fetching, "info");
+ }
+ }
- try {
- const data = await ajax("/bookmark-url/extract", {
- type: "POST",
- data: { url },
- });
+ function populateComposer(data) {
+ const composerService = api.container.lookup("service:composer");
+ const composerModel = composerService?.model;
+ if (!composerModel) return;
- if (data.error) throw new Error(data.error);
+ const lines = [];
+ const siteName = data.site_name ? `**${data.site_name}**` : "";
+ const byline = data.byline ? ` — *${data.byline}*` : "";
+ if (siteName || byline) {
+ lines.push(`> ${siteName}${byline}`);
+ lines.push(`> ${STRINGS.source_label}: <${data.url}>`);
+ lines.push("");
+ } else {
+ lines.push(`> ${STRINGS.source_label}: <${data.url}>`);
+ lines.push("");
+ }
- this._populateComposer(data);
- this._setStatus(STRINGS.success, "success");
- setTimeout(() => this._hideArticleBar(), 3000);
- } catch (err) {
- const msg =
- err.jqXHR?.responseJSON?.error ||
- err.message ||
- STRINGS.error_generic;
- this._setStatus(`${STRINGS.error_prefix} ${msg}`, "error");
- if (btn) {
- btn.disabled = false;
- btn.textContent = STRINGS.retry_button;
- }
- }
- },
+ if (data.description) {
+ lines.push(`*${data.description}*`);
+ lines.push("");
+ lines.push("---");
+ lines.push("");
+ }
- _populateComposer(data) {
- const composerModel = this.get("composer.model");
- if (!composerModel) return;
+ lines.push(data.markdown || "");
- const lines = [];
- const siteName = data.site_name ? `**${data.site_name}**` : "";
- const byline = data.byline ? ` — *${data.byline}*` : "";
- if (siteName || byline) {
- lines.push(`> ${siteName}${byline}`);
- lines.push(`> ${STRINGS.source_label}: <${data.url}>`);
- lines.push("");
- } else {
- lines.push(`> ${STRINGS.source_label}: <${data.url}>`);
- lines.push("");
- }
+ composerModel.set("title", data.title || pendingUrl || "");
+ composerModel.set("reply", lines.join("\n"));
+ pendingUrl = null;
- if (data.description) {
- lines.push(`*${data.description}*`);
- lines.push("");
- lines.push("---");
- lines.push("");
- }
+ if (titleInputEl) {
+ titleInputEl.value = composerModel.get("title");
+ }
+ }
- lines.push(data.markdown || "");
+ // ---- Title paste interception -------------------------------------
+ // capture:true so our handler runs before Discourse's, then
+ // preventDefault + stopImmediatePropagation so Discourse never sees
+ // the paste event — no title lookup, no onebox race.
- composerModel.set("title", data.title || this._pendingUrl || "");
- composerModel.set("reply", lines.join("\n"));
- this._pendingUrl = null;
+ function attachTitlePasteListener(attempts = 0) {
+ const input = document.querySelector("#reply-title");
+ if (input) {
+ titleInputEl = input;
+ titlePasteHandler = (e) => {
+ const text = (e.clipboardData || window.clipboardData)
+ .getData("text/plain")
+ .trim();
+ if (!URL_REGEX.test(text)) return;
- if (this._titleInputEl) {
- this._titleInputEl.value = composerModel.get("title");
- }
- },
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ // Show URL in the field visually without going through Ember binding
+ input.value = text;
+ pendingUrl = text;
+ showArticleBar(text);
+ };
+ input.addEventListener("paste", titlePasteHandler, true);
+ } else if (attempts < 15) {
+ setTimeout(() => attachTitlePasteListener(attempts + 1), 200);
+ }
+ }
+
+ function detachTitlePasteListener() {
+ if (titleInputEl && titlePasteHandler) {
+ titleInputEl.removeEventListener("paste", titlePasteHandler, true);
+ }
+ titleInputEl = null;
+ titlePasteHandler = null;
+ pendingUrl = null;
+ hideArticleBar();
+ }
+
+ // ---- Composer lifecycle -------------------------------------------
+
+ api.onAppEvent("composer:opened", () => {
+ attachTitlePasteListener();
+ });
+
+ api.onAppEvent("composer:closed", () => {
+ detachTitlePasteListener();
});
});