still chasing my dreams
This commit is contained in:
@@ -1,86 +1,24 @@
|
||||
import { apiInitializer } from "discourse/lib/api";
|
||||
import { debounce } from "@ember/runloop";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import I18n from "I18n";
|
||||
|
||||
const URL_REGEX = /^(https?:\/\/[^\s/$.?#][^\s]*)$/i;
|
||||
const DEBOUNCE_MS = 600;
|
||||
|
||||
export default apiInitializer("1.8.0", (api) => {
|
||||
if (!api.container.lookup("site-settings:main").url_to_article_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Intercept Discourse's native title URL lookup at the CLASS level.
|
||||
// Instance-patching is too late — Discourse's own titleChanged observer
|
||||
// may fire before ours. This wraps the prototype method so the flag wins
|
||||
// regardless of observer order.
|
||||
// -----------------------------------------------------------------------
|
||||
api.modifyClass("model:composer", {
|
||||
pluginId: "url-to-article",
|
||||
|
||||
_titleLookup() {
|
||||
if (this._urlToArticleSuppressLookup) return;
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Inject helper button + status banner into the composer
|
||||
// -----------------------------------------------------------------------
|
||||
api.modifyClass("component:composer-editor", {
|
||||
pluginId: "url-to-article",
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._setupUrlToArticle();
|
||||
this._attachTitlePasteListener();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this._teardownUrlToArticle();
|
||||
},
|
||||
|
||||
_setupUrlToArticle() {
|
||||
const composer = this.get("composer");
|
||||
if (!composer) return;
|
||||
|
||||
this._titleObserver = () => this._onTitleChanged();
|
||||
composer.addObserver("model.title", this, "_titleObserver");
|
||||
|
||||
// Intercept paste on the title input BEFORE Ember's data-binding fires,
|
||||
// so the suppression flag is set before Discourse's titleChanged observer runs.
|
||||
this._attachTitlePasteListener();
|
||||
},
|
||||
|
||||
_attachTitlePasteListener() {
|
||||
const tryAttach = (attempts = 0) => {
|
||||
const input = document.querySelector("#reply-title");
|
||||
if (input) {
|
||||
this._titlePasteHandler = (e) => {
|
||||
const text = (
|
||||
e.clipboardData || window.clipboardData
|
||||
).getData("text/plain").trim();
|
||||
if (URL_REGEX.test(text)) {
|
||||
const model = this.get("composer.model");
|
||||
if (model) model._urlToArticleSuppressLookup = true;
|
||||
}
|
||||
};
|
||||
input.addEventListener("paste", this._titlePasteHandler, true);
|
||||
this._titleInputEl = input;
|
||||
} else if (attempts < 10) {
|
||||
setTimeout(() => tryAttach(attempts + 1), 200);
|
||||
}
|
||||
};
|
||||
tryAttach();
|
||||
},
|
||||
|
||||
_teardownUrlToArticle() {
|
||||
const composer = this.get("composer");
|
||||
if (!composer) return;
|
||||
composer.removeObserver("model.title", this, "_titleObserver");
|
||||
this._clearSuppression();
|
||||
if (this._titleInputEl && this._titlePasteHandler) {
|
||||
this._titleInputEl.removeEventListener(
|
||||
"paste",
|
||||
@@ -90,48 +28,32 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
}
|
||||
},
|
||||
|
||||
_onTitleChanged() {
|
||||
const title = this.get("composer.model.title") || "";
|
||||
const match = title.trim().match(URL_REGEX);
|
||||
// ---- Title paste interception -------------------------------------
|
||||
// We use capture:true so our handler runs before Discourse's, then
|
||||
// preventDefault + stopImmediatePropagation so Discourse never sees
|
||||
// the paste event at all — no title lookup, no onebox race.
|
||||
|
||||
if (!match) {
|
||||
this._clearSuppression();
|
||||
this._hideArticleBar();
|
||||
return;
|
||||
}
|
||||
_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;
|
||||
|
||||
const url = match[1];
|
||||
if (this._lastDetectedUrl === url) return;
|
||||
this._lastDetectedUrl = url;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Belt-and-suspenders: ensure suppression is set even if paste listener
|
||||
// missed it (e.g. URL typed manually rather than pasted).
|
||||
const model = this.get("composer.model");
|
||||
if (model) model._urlToArticleSuppressLookup = true;
|
||||
|
||||
const autoPopulate = api.container
|
||||
.lookup("site-settings:main")
|
||||
.url_to_article_auto_populate;
|
||||
|
||||
if (autoPopulate) {
|
||||
debounce(this, "_fetchAndPopulate", url, DEBOUNCE_MS);
|
||||
} else {
|
||||
this._showArticleBar(url);
|
||||
}
|
||||
},
|
||||
|
||||
// ---- Suppression helpers ------------------------------------------
|
||||
|
||||
_clearSuppression() {
|
||||
const model = this.get("composer.model");
|
||||
if (model) delete model._urlToArticleSuppressLookup;
|
||||
},
|
||||
|
||||
_triggerNativeLookup() {
|
||||
this._clearSuppression();
|
||||
const model = this.get("composer.model");
|
||||
if (model && typeof model._titleLookup === "function") {
|
||||
model._titleLookup();
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,7 +64,6 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "url-to-article-bar";
|
||||
bar.dataset.url = url;
|
||||
bar.innerHTML = `
|
||||
<span class="url-to-article-icon">📄</span>
|
||||
<span class="url-to-article-label">${I18n.t("url_to_article.bar_label")}</span>
|
||||
@@ -152,28 +73,28 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
<button class="btn btn-small btn-default url-to-article-onebox-btn">
|
||||
${I18n.t("url_to_article.onebox_button")}
|
||||
</button>
|
||||
<button class="btn btn-small btn-flat url-to-article-dismiss" aria-label="${I18n.t("url_to_article.dismiss")}">✕</button>
|
||||
<button class="btn btn-small btn-flat url-to-article-dismiss"
|
||||
aria-label="${I18n.t("url_to_article.dismiss")}">✕</button>
|
||||
`;
|
||||
|
||||
bar.querySelector(".url-to-article-btn").addEventListener("click", () => {
|
||||
this._fetchAndPopulate(url);
|
||||
});
|
||||
|
||||
// "Use Onebox" — commit URL into Ember model normally so Discourse takes over
|
||||
bar.querySelector(".url-to-article-onebox-btn").addEventListener("click", () => {
|
||||
this._hideArticleBar();
|
||||
this._lastDetectedUrl = null;
|
||||
this._triggerNativeLookup();
|
||||
this._commitUrlToModel();
|
||||
});
|
||||
|
||||
bar.querySelector(".url-to-article-dismiss").addEventListener("click", () => {
|
||||
this._hideArticleBar();
|
||||
this._clearSuppression();
|
||||
this._lastDetectedUrl = null;
|
||||
this._pendingUrl = null;
|
||||
});
|
||||
|
||||
const toolbarEl = this.element.querySelector(".d-editor-container");
|
||||
if (toolbarEl) {
|
||||
toolbarEl.insertAdjacentElement("afterbegin", bar);
|
||||
const container = this.element.querySelector(".d-editor-container");
|
||||
if (container) {
|
||||
container.insertAdjacentElement("afterbegin", bar);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -186,7 +107,6 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
_setStatus(message, type = "info") {
|
||||
const bar = this.element?.querySelector(".url-to-article-bar");
|
||||
if (!bar) return;
|
||||
|
||||
let status = bar.querySelector(".url-to-article-status");
|
||||
if (!status) {
|
||||
status = document.createElement("span");
|
||||
@@ -197,6 +117,16 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
status.className = `url-to-article-status url-to-article-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) {
|
||||
@@ -215,14 +145,10 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
data: { url },
|
||||
});
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
this._populateComposer(data);
|
||||
this._clearSuppression();
|
||||
this._setStatus(I18n.t("url_to_article.success"), "success");
|
||||
|
||||
setTimeout(() => this._hideArticleBar(), 3000);
|
||||
} catch (err) {
|
||||
const msg =
|
||||
@@ -245,7 +171,6 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
if (!composerModel) return;
|
||||
|
||||
const lines = [];
|
||||
|
||||
const siteName = data.site_name ? `**${data.site_name}**` : "";
|
||||
const byline = data.byline ? ` — *${data.byline}*` : "";
|
||||
if (siteName || byline) {
|
||||
@@ -266,14 +191,15 @@ export default apiInitializer("1.8.0", (api) => {
|
||||
|
||||
lines.push(data.markdown || "");
|
||||
|
||||
const body = lines.join("\n");
|
||||
// Set title directly on the model (bypasses the title-lookup trigger)
|
||||
composerModel.set("title", data.title || this._pendingUrl || "");
|
||||
composerModel.set("reply", lines.join("\n"));
|
||||
this._pendingUrl = null;
|
||||
|
||||
const currentTitle = composerModel.get("title") || "";
|
||||
if (currentTitle.trim() === data.url || currentTitle.trim() === "") {
|
||||
composerModel.set("title", data.title || data.url);
|
||||
// Sync the title input visually
|
||||
if (this._titleInputEl) {
|
||||
this._titleInputEl.value = composerModel.get("title");
|
||||
}
|
||||
|
||||
composerModel.set("reply", body);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user