DEV: De-jQ post-cooked (#18328)

This commit is contained in:
Jarek Radosz 2022-09-26 14:26:38 +02:00 committed by GitHub
parent 8bc16dea95
commit f64e7233e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 145 additions and 105 deletions

View File

@ -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;
} }
} }
}); });