DEV: De-jQ post-cooked (#18328)
This commit is contained in:
parent
8bc16dea95
commit
f64e7233e5
|
@ -6,6 +6,8 @@ import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { isValidLink } from "discourse/lib/click-track";
|
import { isValidLink } from "discourse/lib/click-track";
|
||||||
import { number } from "discourse/lib/formatter";
|
import { number } from "discourse/lib/formatter";
|
||||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||||
|
import { escape } from "pretty-text/sanitizer";
|
||||||
|
import domFromString from "discourse-common/lib/dom-from-string";
|
||||||
|
|
||||||
let _beforeAdoptDecorators = [];
|
let _beforeAdoptDecorators = [];
|
||||||
let _afterAdoptDecorators = [];
|
let _afterAdoptDecorators = [];
|
||||||
|
@ -31,6 +33,8 @@ function createDetachedElement(nodeName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PostCooked {
|
export default class PostCooked {
|
||||||
|
originalQuoteContents = null;
|
||||||
|
|
||||||
constructor(attrs, decoratorHelper, currentUser) {
|
constructor(attrs, decoratorHelper, currentUser) {
|
||||||
this.attrs = attrs;
|
this.attrs = attrs;
|
||||||
this.expanding = false;
|
this.expanding = false;
|
||||||
|
@ -52,13 +56,12 @@ export default class PostCooked {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.originalQuoteContents = null;
|
||||||
const cookedDiv = this._computeCooked();
|
const cookedDiv = this._computeCooked();
|
||||||
const $cookedDiv = $(cookedDiv);
|
|
||||||
|
|
||||||
this._insertQuoteControls($cookedDiv);
|
|
||||||
this._showLinkCounts($cookedDiv);
|
|
||||||
this._applySearchHighlight($cookedDiv);
|
|
||||||
|
|
||||||
|
this._insertQuoteControls(cookedDiv);
|
||||||
|
this._showLinkCounts(cookedDiv);
|
||||||
|
this._applySearchHighlight(cookedDiv);
|
||||||
this._decorateAndAdopt(cookedDiv);
|
this._decorateAndAdopt(cookedDiv);
|
||||||
|
|
||||||
return cookedDiv;
|
return cookedDiv;
|
||||||
|
@ -72,8 +75,7 @@ export default class PostCooked {
|
||||||
_afterAdoptDecorators.forEach((d) => d(cooked, this.decoratorHelper));
|
_afterAdoptDecorators.forEach((d) => d(cooked, this.decoratorHelper));
|
||||||
}
|
}
|
||||||
|
|
||||||
_applySearchHighlight($html) {
|
_applySearchHighlight(html) {
|
||||||
const html = $html[0];
|
|
||||||
const highlight = this.attrs.highlightTerm;
|
const highlight = this.attrs.highlightTerm;
|
||||||
|
|
||||||
if (highlight && highlight.length > 2) {
|
if (highlight && highlight.length > 2) {
|
||||||
|
@ -89,7 +91,7 @@ export default class PostCooked {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showLinkCounts($html) {
|
_showLinkCounts(html) {
|
||||||
const linkCounts = this.attrs.linkCounts;
|
const linkCounts = this.attrs.linkCounts;
|
||||||
if (!linkCounts) {
|
if (!linkCounts) {
|
||||||
return;
|
return;
|
||||||
|
@ -99,7 +101,7 @@ export default class PostCooked {
|
||||||
// for that one (the best element is the most significant one to the
|
// for that one (the best element is the most significant one to the
|
||||||
// viewer)
|
// viewer)
|
||||||
const bestElements = new Map();
|
const bestElements = new Map();
|
||||||
$html[0].querySelectorAll("aside.onebox").forEach((onebox) => {
|
html.querySelectorAll("aside.onebox").forEach((onebox) => {
|
||||||
// look in headings first
|
// look in headings first
|
||||||
for (let i = 1; i <= 6; ++i) {
|
for (let i = 1; i <= 6; ++i) {
|
||||||
const hLinks = onebox.querySelectorAll(`h${i} a[href]`);
|
const hLinks = onebox.querySelectorAll(`h${i} a[href]`);
|
||||||
|
@ -121,10 +123,8 @@ export default class PostCooked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$html.find("a[href]").each((i, e) => {
|
html.querySelectorAll("a[href]").forEach((link) => {
|
||||||
const $link = $(e);
|
const href = link.getAttribute("href");
|
||||||
const href = $link.attr("href");
|
|
||||||
|
|
||||||
let valid = href === lc.url;
|
let valid = href === lc.url;
|
||||||
|
|
||||||
// this might be an attachment
|
// this might be an attachment
|
||||||
|
@ -132,24 +132,29 @@ export default class PostCooked {
|
||||||
valid = href.includes(lc.url);
|
valid = href.includes(lc.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match server-side behaviour for internal links with query params
|
// match server-side behavior for internal links with query params
|
||||||
if (lc.internal && /\?/.test(href)) {
|
if (lc.internal && /\?/.test(href)) {
|
||||||
valid = href.split("?")[0] === lc.url;
|
valid = href.split("?")[0] === lc.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't display badge counts on category badge & oneboxes (unless when explicitly stated)
|
// don't display badge counts on category badge & oneboxes (unless when explicitly stated)
|
||||||
if (valid && isValidLink($link[0])) {
|
if (valid && isValidLink(link)) {
|
||||||
const $onebox = $link.closest(".onebox");
|
const onebox = link.closest(".onebox");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$onebox.length === 0 ||
|
!onebox ||
|
||||||
!bestElements.has($onebox[0]) ||
|
!bestElements.has(onebox) ||
|
||||||
bestElements.get($onebox[0]) === $link[0]
|
bestElements.get(onebox) === link
|
||||||
) {
|
) {
|
||||||
const title = I18n.t("topic_map.clicks", { count: lc.clicks });
|
const title = I18n.t("topic_map.clicks", { count: lc.clicks });
|
||||||
$link.append(
|
|
||||||
` <span class='badge badge-notification clicks' title='${title}'>${number(
|
link.appendChild(document.createTextNode(" "));
|
||||||
lc.clicks
|
link.appendChild(
|
||||||
)}</span>`
|
domFromString(
|
||||||
|
`<span class='badge badge-notification clicks' title='${title}'>${number(
|
||||||
|
lc.clicks
|
||||||
|
)}</span>`
|
||||||
|
)[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,21 +162,31 @@ export default class PostCooked {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleQuote($aside) {
|
async _toggleQuote(aside) {
|
||||||
if (this.expanding) {
|
if (this.expanding) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expanding = true;
|
this.expanding = true;
|
||||||
const blockQuote = $aside[0].querySelector("blockquote");
|
const blockQuote = aside.querySelector("blockquote");
|
||||||
$aside.data("expanded", !$aside.data("expanded"));
|
|
||||||
|
|
||||||
const finished = () => (this.expanding = false);
|
if (!blockQuote) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aside.dataset.expanded) {
|
||||||
|
delete aside.dataset.expanded;
|
||||||
|
} else {
|
||||||
|
aside.dataset.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quoteId = blockQuote.id;
|
||||||
|
|
||||||
|
if (aside.dataset.expanded) {
|
||||||
|
this._updateQuoteElements(aside, "chevron-up");
|
||||||
|
|
||||||
if ($aside.data("expanded")) {
|
|
||||||
this._updateQuoteElements($aside, "chevron-up");
|
|
||||||
// Show expanded quote
|
// Show expanded quote
|
||||||
$aside.data("original-contents", blockQuote.innerHTML);
|
this.originalQuoteContents.set(quoteId, blockQuote.innerHTML);
|
||||||
|
|
||||||
const originalText =
|
const originalText =
|
||||||
blockQuote.textContent.trim() ||
|
blockQuote.textContent.trim() ||
|
||||||
|
@ -179,51 +194,45 @@ export default class PostCooked {
|
||||||
|
|
||||||
blockQuote.innerHTML = spinnerHTML;
|
blockQuote.innerHTML = spinnerHTML;
|
||||||
|
|
||||||
let topicId = this.attrs.topicId;
|
const topicId = parseInt(aside.dataset.topic || this.attrs.topicId, 10);
|
||||||
if ($aside.data("topic")) {
|
const postId = parseInt(aside.dataset.post, 10);
|
||||||
topicId = $aside.data("topic");
|
|
||||||
}
|
|
||||||
|
|
||||||
const postId = parseInt($aside.data("post"), 10);
|
try {
|
||||||
topicId = parseInt(topicId, 10);
|
const result = await ajax(`/posts/by_number/${topicId}/${postId}`);
|
||||||
|
|
||||||
ajax(`/posts/by_number/${topicId}/${postId}`)
|
const post = this.decoratorHelper.getModel();
|
||||||
.then((result) => {
|
const quotedPosts = post.quoted || {};
|
||||||
const post = this.decoratorHelper.getModel();
|
quotedPosts[result.id] = result;
|
||||||
const quotedPosts = post.quoted || {};
|
post.set("quoted", quotedPosts);
|
||||||
quotedPosts[result.id] = result;
|
|
||||||
post.set("quoted", quotedPosts);
|
|
||||||
|
|
||||||
const div = createDetachedElement("div");
|
const div = createDetachedElement("div");
|
||||||
div.classList.add("expanded-quote");
|
div.classList.add("expanded-quote");
|
||||||
div.dataset.postId = result.id;
|
div.dataset.postId = result.id;
|
||||||
div.innerHTML = result.cooked;
|
div.innerHTML = result.cooked;
|
||||||
|
|
||||||
this._decorateAndAdopt(div);
|
this._decorateAndAdopt(div);
|
||||||
|
|
||||||
highlightHTML(div, originalText, {
|
highlightHTML(div, originalText, {
|
||||||
matchCase: true,
|
matchCase: true,
|
||||||
});
|
|
||||||
|
|
||||||
blockQuote.innerHTML = "";
|
|
||||||
blockQuote.appendChild(div);
|
|
||||||
finished();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if ([403, 404].includes(e.jqXHR.status)) {
|
|
||||||
const icon = e.jqXHR.status === 403 ? "lock" : "far-trash-alt";
|
|
||||||
blockQuote.innerHTML = `<div class='expanded-quote icon-only'>${iconHTML(
|
|
||||||
icon
|
|
||||||
)}</div>`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
blockQuote.innerHTML = "";
|
||||||
|
blockQuote.appendChild(div);
|
||||||
|
} catch (e) {
|
||||||
|
if ([403, 404].includes(e.jqXHR.status)) {
|
||||||
|
const icon = e.jqXHR.status === 403 ? "lock" : "far-trash-alt";
|
||||||
|
blockQuote.innerHTML = `<div class='expanded-quote icon-only'>${iconHTML(
|
||||||
|
icon
|
||||||
|
)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Hide expanded quote
|
// Hide expanded quote
|
||||||
this._updateQuoteElements($aside, "chevron-down");
|
this._updateQuoteElements(aside, "chevron-down");
|
||||||
blockQuote.innerHTML = $aside.data("original-contents");
|
blockQuote.innerHTML = this.originalQuoteContents.get(blockQuote.id);
|
||||||
finished();
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
this.expanding = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_urlForPostNumber(postNumber) {
|
_urlForPostNumber(postNumber) {
|
||||||
|
@ -232,72 +241,103 @@ export default class PostCooked {
|
||||||
: this.attrs.topicUrl;
|
: this.attrs.topicUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateQuoteElements($aside, desc) {
|
_updateQuoteElements(aside, desc) {
|
||||||
let navLink = "";
|
|
||||||
const quoteTitle = I18n.t("post.follow_quote");
|
const quoteTitle = I18n.t("post.follow_quote");
|
||||||
let postNumber = $aside.data("post");
|
const postNumber = aside.dataset.post;
|
||||||
let topicNumber = $aside.data("topic");
|
const topicNumber = aside.dataset.topic;
|
||||||
|
|
||||||
// If we have a post reference
|
// If we have a post reference
|
||||||
if (topicNumber && topicNumber === this.attrs.topicId && postNumber) {
|
let navLink = "";
|
||||||
let icon = iconHTML("arrow-up");
|
if (
|
||||||
|
topicNumber &&
|
||||||
|
postNumber &&
|
||||||
|
topicNumber === this.attrs.topicId?.toString()
|
||||||
|
) {
|
||||||
|
const icon = iconHTML("arrow-up");
|
||||||
navLink = `<a href='${this._urlForPostNumber(
|
navLink = `<a href='${this._urlForPostNumber(
|
||||||
postNumber
|
postNumber
|
||||||
)}' title='${quoteTitle}' class='btn-flat back'>${icon}</a>`;
|
)}' title='${quoteTitle}' class='btn-flat back'>${icon}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add the expand/contract control if it's not a full post
|
// Only add the expand/contract control if it's not a full post
|
||||||
|
const titleElement = aside.querySelector(".title");
|
||||||
let expandContract = "";
|
let expandContract = "";
|
||||||
const isExpanded = $aside.data("expanded") === true;
|
|
||||||
if (!$aside.data("full")) {
|
if (!aside.dataset.full) {
|
||||||
let icon = iconHTML(desc, { title: "post.expand_collapse" });
|
const icon = iconHTML(desc, { title: "post.expand_collapse" });
|
||||||
const quoteId = $aside.find("blockquote").attr("id");
|
const quoteId = aside.querySelector("blockquote")?.id;
|
||||||
expandContract = `<button aria-controls="${quoteId}" aria-expanded="${isExpanded}" class="quote-toggle btn-flat">${icon}</button>`;
|
|
||||||
$(".title", $aside).css("cursor", "pointer");
|
if (quoteId) {
|
||||||
}
|
const isExpanded = aside.dataset.expanded === "true";
|
||||||
if (this.ignoredUsers && this.ignoredUsers.length > 0) {
|
expandContract = `<button aria-controls="${quoteId}" aria-expanded="${isExpanded}" class="quote-toggle btn-flat">${icon}</button>`;
|
||||||
const username = $aside.find(".title").text().trim().slice(0, -1);
|
|
||||||
if (username.length > 0 && this.ignoredUsers.includes(username)) {
|
if (titleElement) {
|
||||||
$aside.find("p").remove();
|
titleElement.style.cursor = "pointer";
|
||||||
$aside.addClass("ignored-user");
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$(".quote-controls", $aside).html(expandContract + navLink);
|
|
||||||
|
if (this.ignoredUsers?.length && titleElement) {
|
||||||
|
const username = titleElement.innerText.trim().slice(0, -1);
|
||||||
|
|
||||||
|
if (username.length > 0 && this.ignoredUsers.includes(username)) {
|
||||||
|
aside.querySelectorAll("p").forEach((el) => el.remove());
|
||||||
|
aside.classList.add("ignored-user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quoteControls = aside.querySelector(".quote-controls");
|
||||||
|
if (quoteControls) {
|
||||||
|
quoteControls.innerHTML = expandContract + navLink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_insertQuoteControls($html) {
|
_insertQuoteControls(html) {
|
||||||
const $quotes = $html.find("aside.quote");
|
const quotes = html.querySelectorAll("aside.quote");
|
||||||
if ($quotes.length === 0) {
|
if (quotes.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$quotes.each((index, e) => {
|
this.originalQuoteContents = new Map();
|
||||||
const $aside = $(e);
|
|
||||||
if ($aside.data("post")) {
|
|
||||||
const quoteId = `quote-id-${$aside.data("topic")}-${$aside.data(
|
|
||||||
"post"
|
|
||||||
)}-${index}`;
|
|
||||||
$aside.find("blockquote").attr("id", quoteId);
|
|
||||||
|
|
||||||
this._updateQuoteElements($aside, "chevron-down");
|
quotes.forEach((aside, index) => {
|
||||||
const $title = $(".title", $aside);
|
if (aside.dataset.post) {
|
||||||
|
const quoteId = `quote-id-${aside.dataset.topic}-${aside.dataset.post}-${index}`;
|
||||||
|
|
||||||
|
const blockquote = aside.querySelector("blockquote");
|
||||||
|
if (blockquote) {
|
||||||
|
blockquote.id = quoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateQuoteElements(aside, "chevron-down");
|
||||||
|
const title = aside.querySelector(".title");
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If post/topic is not found then display username, skip controls
|
// If post/topic is not found then display username, skip controls
|
||||||
if (e.classList.contains("quote-post-not-found") && $title.length) {
|
if (aside.classList.contains("quote-post-not-found")) {
|
||||||
e.querySelector(".title").innerHTML = e.dataset.username;
|
if (aside.dataset.username) {
|
||||||
|
title.innerHTML = escape(aside.dataset.username);
|
||||||
|
} else {
|
||||||
|
title.remove();
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unless it's a full quote, allow click to expand
|
// Unless it's a full quote, allow click to expand
|
||||||
if (!($aside.data("full") || $title.data("has-quote-controls"))) {
|
if (!aside.dataset.full && !title.dataset.hasQuoteControls) {
|
||||||
$title.on("click", (e2) => {
|
title.addEventListener("click", (e) => {
|
||||||
let $target = $(e2.target);
|
if (e.target.closest("a")) {
|
||||||
if ($target.closest("a").length) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this._toggleQuote($aside);
|
|
||||||
|
this._toggleQuote(aside);
|
||||||
});
|
});
|
||||||
$title.data("has-quote-controls", true);
|
|
||||||
|
title.dataset.hasQuoteControls = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue