Compare commits

..

1 Commits

Author SHA1 Message Date
ea61c91774 Removing id:discourse.resolver-resolutions notice. 2026-03-23 16:22:23 -04:00

View File

@@ -17,197 +17,204 @@ const STRINGS = {
}; };
export default apiInitializer("1.8.0", (api) => { 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; return;
} }
api.modifyClass("component:composer-editor", { let titleInputEl = null;
pluginId: "bookmark-url", let titlePasteHandler = null;
let pendingUrl = null;
didInsertElement() { // ---- Bar UI -------------------------------------------------------
this._super(...arguments);
this._attachTitlePasteListener();
},
willDestroyElement() { function showArticleBar(url) {
this._super(...arguments); hideArticleBar();
if (this._titleInputEl && this._titlePasteHandler) {
this._titleInputEl.removeEventListener(
"paste",
this._titlePasteHandler,
true
);
}
},
// ---- Title paste interception ------------------------------------- const bar = document.createElement("div");
// capture:true so our handler runs before Discourse's, then bar.className = "bookmark-url-bar";
// preventDefault + stopImmediatePropagation so Discourse never sees bar.innerHTML = `
// the paste event — no title lookup, no onebox race. <span class="bookmark-url-icon">📄</span>
<span class="bookmark-url-label">${STRINGS.bar_label}</span>
<button class="btn btn-small btn-primary bookmark-url-btn">
${STRINGS.fetch_button}
</button>
<button class="btn btn-small btn-default bookmark-url-onebox-btn">
${STRINGS.onebox_button}
</button>
<button class="btn btn-small btn-flat bookmark-url-dismiss"
aria-label="${STRINGS.dismiss}">✕</button>
`;
_attachTitlePasteListener(attempts = 0) { bar.querySelector(".bookmark-url-btn").addEventListener("click", () => {
const input = document.querySelector("#reply-title"); fetchAndPopulate(url);
if (input) { });
this._titleInputEl = input;
this._titlePasteHandler = (e) => {
const text = (e.clipboardData || window.clipboardData)
.getData("text/plain")
.trim();
if (!URL_REGEX.test(text)) return;
e.preventDefault(); bar.querySelector(".bookmark-url-onebox-btn").addEventListener("click", () => {
e.stopImmediatePropagation(); hideArticleBar();
commitUrlToModel();
});
// Show URL in the field visually without going through Ember binding bar.querySelector(".bookmark-url-dismiss").addEventListener("click", () => {
input.value = text; hideArticleBar();
this._pendingUrl = text; pendingUrl = null;
this._showArticleBar(text); });
};
input.addEventListener("paste", this._titlePasteHandler, true);
} else if (attempts < 15) {
setTimeout(() => this._attachTitlePasteListener(attempts + 1), 200);
}
},
// ---- 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) { function hideArticleBar() {
this._hideArticleBar(); document
.querySelectorAll(".bookmark-url-bar")
.forEach((el) => el.remove());
}
const bar = document.createElement("div"); function setStatus(message, type = "info") {
bar.className = "bookmark-url-bar"; const bar = document.querySelector(".bookmark-url-bar");
bar.innerHTML = ` if (!bar) return;
<span class="bookmark-url-icon">📄</span> let status = bar.querySelector(".bookmark-url-status");
<span class="bookmark-url-label">${STRINGS.bar_label}</span> if (!status) {
<button class="btn btn-small btn-primary bookmark-url-btn"> status = document.createElement("span");
${STRINGS.fetch_button} status.className = "bookmark-url-status";
</button> bar.appendChild(status);
<button class="btn btn-small btn-default bookmark-url-onebox-btn"> }
${STRINGS.onebox_button} status.textContent = message;
</button> status.className = `bookmark-url-status bookmark-url-status--${type}`;
<button class="btn btn-small btn-flat bookmark-url-dismiss" }
aria-label="${STRINGS.dismiss}">✕</button>
`;
bar.querySelector(".bookmark-url-btn").addEventListener("click", () => { // Release the URL into Ember's data-binding so Discourse handles it normally
this._fetchAndPopulate(url); 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", () => { if (data.error) throw new Error(data.error);
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");
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) { if (btn) {
btn.disabled = true; btn.disabled = false;
btn.textContent = STRINGS.fetching; btn.textContent = STRINGS.retry_button;
} }
this._setStatus(STRINGS.fetching, "info"); }
}
try { function populateComposer(data) {
const data = await ajax("/bookmark-url/extract", { const composerService = api.container.lookup("service:composer");
type: "POST", const composerModel = composerService?.model;
data: { url }, 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); if (data.description) {
this._setStatus(STRINGS.success, "success"); lines.push(`*${data.description}*`);
setTimeout(() => this._hideArticleBar(), 3000); lines.push("");
} catch (err) { lines.push("---");
const msg = lines.push("");
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;
}
}
},
_populateComposer(data) { lines.push(data.markdown || "");
const composerModel = this.get("composer.model");
if (!composerModel) return;
const lines = []; composerModel.set("title", data.title || pendingUrl || "");
const siteName = data.site_name ? `**${data.site_name}**` : ""; composerModel.set("reply", lines.join("\n"));
const byline = data.byline ? ` — *${data.byline}*` : ""; pendingUrl = null;
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("");
}
if (data.description) { if (titleInputEl) {
lines.push(`*${data.description}*`); titleInputEl.value = composerModel.get("title");
lines.push(""); }
lines.push("---"); }
lines.push("");
}
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 || ""); function attachTitlePasteListener(attempts = 0) {
composerModel.set("reply", lines.join("\n")); const input = document.querySelector("#reply-title");
this._pendingUrl = null; 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) { e.preventDefault();
this._titleInputEl.value = composerModel.get("title"); 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();
}); });
}); });