PERF: Refactor decorateCooked to run in a detached DOM (#9517)
This means that decorateCooked can be used to modify HTML without triggering the download of remote resources (e.g. images) In some rare cases (e.g. IntersectionObservers in Chromium), decorating needs to happen in the real DOM. For this, pass `afterAdopt: true` to `decorateCooked`
This commit is contained in:
parent
8dccf0521c
commit
8e28ccb2ea
|
@ -84,6 +84,14 @@ function show(image) {
|
|||
}
|
||||
}
|
||||
|
||||
function forEachImage($post, callback) {
|
||||
$post[0].querySelectorAll("img").forEach(img => {
|
||||
if (img.width >= MINIMUM_SIZE && img.height >= MINIMUM_SIZE) {
|
||||
callback(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function setupLazyLoading(api) {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
|
@ -96,15 +104,20 @@ export function setupLazyLoading(api) {
|
|||
});
|
||||
}, OBSERVER_OPTIONS);
|
||||
|
||||
api.decorateCooked(
|
||||
$post => {
|
||||
$("img", $post).each((_, img) => {
|
||||
if (img.width >= MINIMUM_SIZE && img.height >= MINIMUM_SIZE) {
|
||||
hide(img);
|
||||
observer.observe(img);
|
||||
}
|
||||
api.decorateCooked($post => forEachImage($post, img => hide(img)), {
|
||||
onlyStream: true,
|
||||
id: "discourse-lazy-load"
|
||||
});
|
||||
},
|
||||
{ onlyStream: true, id: "discourse-lazy-load" }
|
||||
|
||||
// IntersectionObserver.observe must be called after the cooked
|
||||
// content is adopted by the document element in chrome
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1073469
|
||||
api.decorateCooked(
|
||||
$post => forEachImage($post, img => observer.observe(img)),
|
||||
{
|
||||
onlyStream: true,
|
||||
id: "discourse-lazy-load-after-adopt",
|
||||
afterAdopt: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ import { on } from "@ember/object/evented";
|
|||
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = "0.8.40";
|
||||
const PLUGIN_API_VERSION = "0.8.41";
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
|
@ -200,6 +200,9 @@ class PluginApi {
|
|||
* Use `options.onlyStream` if you only want to decorate posts within a topic,
|
||||
* and not in other places like the user stream.
|
||||
*
|
||||
* Decoration normally happens in a detached DOM. Use `options.afterAdopt`
|
||||
* to decorate html content after it is adopted by the main `document`.
|
||||
*
|
||||
* For example, to add a yellow background to all posts you could do this:
|
||||
*
|
||||
* ```
|
||||
|
@ -215,7 +218,7 @@ class PluginApi {
|
|||
decorateCooked(callback, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
addDecorator(callback);
|
||||
addDecorator(callback, { afterAdopt: !!opts.afterAdopt });
|
||||
|
||||
if (!opts.onlyStream) {
|
||||
decorate(ComposerEditor, "previewRefreshed", callback, opts.id);
|
||||
|
|
|
@ -8,15 +8,27 @@ import {
|
|||
unhighlightHTML
|
||||
} from "discourse/lib/highlight-html";
|
||||
|
||||
let _decorators = [];
|
||||
let _beforeAdoptDecorators = [];
|
||||
let _afterAdoptDecorators = [];
|
||||
|
||||
// Don't call this directly: use `plugin-api/decorateCooked`
|
||||
export function addDecorator(cb) {
|
||||
_decorators.push(cb);
|
||||
export function addDecorator(callback, { afterAdopt = false } = {}) {
|
||||
if (afterAdopt) {
|
||||
_afterAdoptDecorators.push(callback);
|
||||
} else {
|
||||
_beforeAdoptDecorators.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetDecorators() {
|
||||
_decorators = [];
|
||||
_beforeAdoptDecorators = [];
|
||||
_afterAdoptDecorators = [];
|
||||
}
|
||||
|
||||
let detachedDocument = document.implementation.createHTMLDocument("detached");
|
||||
|
||||
function createDetachedElement(nodeName) {
|
||||
return detachedDocument.createElement(nodeName);
|
||||
}
|
||||
|
||||
export default class PostCooked {
|
||||
|
@ -41,14 +53,27 @@ export default class PostCooked {
|
|||
}
|
||||
|
||||
init() {
|
||||
const $html = this._computeCooked();
|
||||
this._insertQuoteControls($html);
|
||||
this._showLinkCounts($html);
|
||||
this._fixImageSizes($html);
|
||||
this._applySearchHighlight($html);
|
||||
const cookedDiv = this._computeCooked();
|
||||
const $cookedDiv = $(cookedDiv);
|
||||
|
||||
_decorators.forEach(cb => cb($html, this.decoratorHelper));
|
||||
return $html[0];
|
||||
this._insertQuoteControls($cookedDiv);
|
||||
this._showLinkCounts($cookedDiv);
|
||||
this._fixImageSizes($cookedDiv);
|
||||
this._applySearchHighlight($cookedDiv);
|
||||
|
||||
this._decorateAndAdopt(cookedDiv);
|
||||
|
||||
return cookedDiv;
|
||||
}
|
||||
|
||||
_decorateAndAdopt(cooked) {
|
||||
const $cooked = $(cooked);
|
||||
|
||||
_beforeAdoptDecorators.forEach(d => d($cooked, this.decoratorHelper));
|
||||
|
||||
document.adoptNode(cooked);
|
||||
|
||||
_afterAdoptDecorators.forEach(d => d($cooked, this.decoratorHelper));
|
||||
}
|
||||
|
||||
_applySearchHighlight($html) {
|
||||
|
@ -175,12 +200,14 @@ export default class PostCooked {
|
|||
quotedPosts[result.id] = result;
|
||||
post.set("quoted", quotedPosts);
|
||||
|
||||
const div = $("<div class='expanded-quote'></div>");
|
||||
div.data("post-id", result.id);
|
||||
div.html(result.cooked);
|
||||
_decorators.forEach(cb => cb(div, this.decoratorHelper));
|
||||
const div = createDetachedElement("div");
|
||||
div.classList.add("expanded-quote");
|
||||
div.dataset.postId = result.id;
|
||||
div.innerHTML = result.cooked;
|
||||
|
||||
highlightHTML(div[0], originalText, {
|
||||
this._decorateAndAdopt(div);
|
||||
|
||||
highlightHTML(div, originalText, {
|
||||
matchCase: true
|
||||
});
|
||||
$blockQuote.showHtml(div, "fast", finished);
|
||||
|
@ -275,18 +302,22 @@ export default class PostCooked {
|
|||
}
|
||||
|
||||
_computeCooked() {
|
||||
const cookedDiv = createDetachedElement("div");
|
||||
cookedDiv.classList.add("cooked");
|
||||
|
||||
if (
|
||||
(this.attrs.firstPost || this.attrs.embeddedPost) &&
|
||||
this.ignoredUsers &&
|
||||
this.ignoredUsers.length > 0 &&
|
||||
this.ignoredUsers.includes(this.attrs.username)
|
||||
) {
|
||||
return $(
|
||||
`<div class='cooked post-ignored'>${I18n.t("post.ignored")}</div>`
|
||||
);
|
||||
cookedDiv.classList.add("post-ignored");
|
||||
cookedDiv.innerHTML = I18n.t("post.ignored");
|
||||
} else {
|
||||
cookedDiv.innerHTML = this.attrs.cooked;
|
||||
}
|
||||
|
||||
return $(`<div class='cooked'>${this.attrs.cooked}</div>`);
|
||||
return cookedDiv;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue