init commit
This commit is contained in:
197
assets/javascripts/discourse/initializers/url-to-article.js
Normal file
197
assets/javascripts/discourse/initializers/url-to-article.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Inject a helper button + status banner into the composer
|
||||
// -----------------------------------------------------------------------
|
||||
api.modifyClass("component:composer-editor", {
|
||||
pluginId: "url-to-article",
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._setupUrlToArticle();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this._teardownUrlToArticle();
|
||||
},
|
||||
|
||||
_setupUrlToArticle() {
|
||||
// Watch the title field — it lives outside the composer-editor DOM,
|
||||
// so we observe via the composer model's `title` property.
|
||||
const composer = this.get("composer");
|
||||
if (!composer) return;
|
||||
|
||||
this._titleObserver = () => this._onTitleChanged();
|
||||
composer.addObserver("model.title", this, "_titleObserver");
|
||||
},
|
||||
|
||||
_teardownUrlToArticle() {
|
||||
const composer = this.get("composer");
|
||||
if (!composer) return;
|
||||
composer.removeObserver("model.title", this, "_titleObserver");
|
||||
},
|
||||
|
||||
_onTitleChanged() {
|
||||
const title = this.get("composer.model.title") || "";
|
||||
const match = title.trim().match(URL_REGEX);
|
||||
|
||||
if (!match) {
|
||||
this._hideArticleBar();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = match[1];
|
||||
|
||||
if (this._lastDetectedUrl === url) return; // Same URL — no-op
|
||||
this._lastDetectedUrl = url;
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
// ---- Bar UI -------------------------------------------------------
|
||||
|
||||
_showArticleBar(url) {
|
||||
this._hideArticleBar(); // remove any existing bar first
|
||||
|
||||
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>
|
||||
<button class="btn btn-small btn-primary url-to-article-btn">
|
||||
${I18n.t("url_to_article.fetch_button")}
|
||||
</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);
|
||||
});
|
||||
|
||||
bar.querySelector(".url-to-article-dismiss").addEventListener("click", () => {
|
||||
this._hideArticleBar();
|
||||
this._lastDetectedUrl = null; // Allow re-detection if title changes
|
||||
});
|
||||
|
||||
const toolbarEl = this.element.querySelector(".d-editor-container");
|
||||
if (toolbarEl) {
|
||||
toolbarEl.insertAdjacentElement("afterbegin", bar);
|
||||
}
|
||||
},
|
||||
|
||||
_hideArticleBar() {
|
||||
this.element?.querySelectorAll(".url-to-article-bar").forEach((el) => el.remove());
|
||||
},
|
||||
|
||||
_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");
|
||||
status.className = "url-to-article-status";
|
||||
bar.appendChild(status);
|
||||
}
|
||||
status.textContent = message;
|
||||
status.className = `url-to-article-status url-to-article-status--${type}`;
|
||||
},
|
||||
|
||||
// ---- Fetch & populate ---------------------------------------------
|
||||
|
||||
async _fetchAndPopulate(url) {
|
||||
const bar = this.element?.querySelector(".url-to-article-bar");
|
||||
const btn = bar?.querySelector(".url-to-article-btn");
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = I18n.t("url_to_article.fetching");
|
||||
}
|
||||
this._setStatus(I18n.t("url_to_article.fetching"), "info");
|
||||
|
||||
try {
|
||||
const data = await ajax("/url-to-article/extract", {
|
||||
type: "POST",
|
||||
data: { url },
|
||||
});
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
this._populateComposer(data);
|
||||
this._setStatus(I18n.t("url_to_article.success"), "success");
|
||||
|
||||
// Auto-hide bar after 3 seconds on success
|
||||
setTimeout(() => this._hideArticleBar(), 3000);
|
||||
} catch (err) {
|
||||
const msg = err.jqXHR?.responseJSON?.error || err.message || I18n.t("url_to_article.error_generic");
|
||||
this._setStatus(`${I18n.t("url_to_article.error_prefix")} ${msg}`, "error");
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = I18n.t("url_to_article.retry_button");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_populateComposer(data) {
|
||||
const composerModel = this.get("composer.model");
|
||||
if (!composerModel) return;
|
||||
|
||||
// Build the article body in Markdown
|
||||
const lines = [];
|
||||
|
||||
// Attribution header
|
||||
const siteName = data.site_name ? `**${data.site_name}**` : "";
|
||||
const byline = data.byline ? ` — *${data.byline}*` : "";
|
||||
if (siteName || byline) {
|
||||
lines.push(`> ${siteName}${byline}`);
|
||||
lines.push(`> ${I18n.t("url_to_article.source_label")}: <${data.url}>`);
|
||||
lines.push("");
|
||||
} else {
|
||||
lines.push(`> ${I18n.t("url_to_article.source_label")}: <${data.url}>`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (data.description) {
|
||||
lines.push(`*${data.description}*`);
|
||||
lines.push("");
|
||||
lines.push("---");
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push(data.markdown || "");
|
||||
|
||||
const body = lines.join("\n");
|
||||
|
||||
// Only set title if it's still the raw URL (avoid overwriting edited titles)
|
||||
const currentTitle = composerModel.get("title") || "";
|
||||
if (currentTitle.trim() === data.url || currentTitle.trim() === "") {
|
||||
composerModel.set("title", data.title || data.url);
|
||||
}
|
||||
|
||||
composerModel.set("reply", body);
|
||||
},
|
||||
});
|
||||
});
|
||||
51
assets/stylesheets/url-to-article.scss
Normal file
51
assets/stylesheets/url-to-article.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
/* URL-to-Article plugin styles */
|
||||
|
||||
.url-to-article-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--tertiary-low, #e8f4ff);
|
||||
border: 1px solid var(--tertiary-medium, #8bc2f0);
|
||||
border-radius: var(--d-border-radius, 4px);
|
||||
font-size: var(--font-down-1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.url-to-article-icon {
|
||||
font-size: 1.1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-to-article-label {
|
||||
flex: 1;
|
||||
min-width: 8rem;
|
||||
color: var(--primary-medium);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.url-to-article-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-to-article-dismiss {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
color: var(--primary-medium) !important;
|
||||
}
|
||||
|
||||
.url-to-article-status {
|
||||
font-style: italic;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
&.url-to-article-status--info {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
&.url-to-article-status--success {
|
||||
color: var(--success);
|
||||
}
|
||||
&.url-to-article-status--error {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user