DEV: refactor (composer|d)-editor.js

This started as a way to prevent "previewUpdated" from doing the same work twice when morphing.

Ended up refactoring "previewUpdated" and extracted into 5 distinct methods for clearer understanding and more consistent debouncing (using the "@debounce" decorator instead of the "discourseDebounce" method).

No "feature" was changed, other than not doing the "decorateCookedElement" when morphing is enabled, since we already did it _before_ morphing.
This commit is contained in:
Régis Hanol 2024-04-09 23:07:12 +02:00
parent 4b043a2a82
commit 8509fc2ebc
3 changed files with 110 additions and 110 deletions

View File

@ -33,7 +33,6 @@ import ComposerUploadUppy from "discourse/mixins/composer-upload-uppy";
import Composer from "discourse/models/composer";
import { isTesting } from "discourse-common/config/environment";
import { tinyAvatar } from "discourse-common/lib/avatar-utils";
import discourseDebounce from "discourse-common/lib/debounce";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseLater from "discourse-common/lib/later";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
@ -107,6 +106,9 @@ export function addApiImageWrapperButtonClickEvent(fn) {
apiImageWrapperBtnEvents.push(fn);
}
const DEBOUNCE_FETCH_MS = 450;
const DEBOUNCE_JIT_MS = 2000;
export default Component.extend(ComposerUploadUppy, {
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
@ -218,10 +220,11 @@ export default Component.extend(ComposerUploadUppy, {
@on("didInsertElement")
_composerEditorInit() {
const $input = $(this.element.querySelector(".d-editor-input"));
const input = this.element.querySelector(".d-editor-input");
const preview = this.element.querySelector(".d-editor-preview-wrapper");
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
$(input).autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
dataSource: (term) => {
destroyUserStatuses();
@ -235,9 +238,7 @@ export default Component.extend(ComposerUploadUppy, {
return result;
});
},
onRender: (options) => {
renderUserStatusHtml(options);
},
onRender: (options) => renderUserStatusHtml(options),
key: "@",
transformComplete: (v) => v.username || v.name,
afterComplete: this._afterMentionComplete,
@ -247,13 +248,16 @@ export default Component.extend(ComposerUploadUppy, {
});
}
this.element
.querySelector(".d-editor-input")
?.addEventListener("scroll", this._throttledSyncEditorAndPreviewScroll);
input?.addEventListener(
"scroll",
this._throttledSyncEditorAndPreviewScroll
);
this._registerImageAltTextButtonClick(preview);
// Focus on the body unless we have a title
if (!this.get("composer.canEditTitle")) {
putCursorAtEnd(this.element.querySelector(".d-editor-input"));
putCursorAtEnd(input);
}
if (this.allowUpload) {
@ -488,6 +492,17 @@ export default Component.extend(ComposerUploadUppy, {
$preview.scrollTop(desired + 50);
},
_renderMentions(preview, unseen) {
unseen ||= linkSeenMentions(preview, this.siteSettings);
if (unseen.length > 0) {
this._renderUnseenMentions(preview, unseen);
} else {
this._warnMentionedGroups(preview);
this._warnCannotSeeMention(preview);
}
},
@debounce(DEBOUNCE_FETCH_MS)
_renderUnseenMentions(preview, unseen) {
fetchUnseenMentions({
names: unseen,
@ -501,17 +516,50 @@ export default Component.extend(ComposerUploadUppy, {
});
},
_renderUnseenHashtags(preview) {
const hashtagContext = this.site.hashtag_configurations["topic-composer"];
const unseen = linkSeenHashtagsInContext(hashtagContext, preview);
_renderHashtags(preview, unseen) {
const context = this.site.hashtag_configurations["topic-composer"];
unseen ||= linkSeenHashtagsInContext(context, preview);
if (unseen.length > 0) {
fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => {
linkSeenHashtagsInContext(hashtagContext, preview);
});
this._renderUnseenHashtags(preview, unseen, context);
}
},
@debounce(2000)
@debounce(DEBOUNCE_FETCH_MS)
_renderUnseenHashtags(preview, unseen, context) {
fetchUnseenHashtagsInContext(context, unseen).then(() =>
linkSeenHashtagsInContext(context, preview)
);
},
@debounce(DEBOUNCE_FETCH_MS)
_refreshOneboxes(preview) {
const post = this.get("composer.post");
// If we are editing a post, we'll refresh its contents once.
const refresh = post && !post.get("refreshedPost");
const loaded = loadOneboxes(
preview,
ajax,
this.get("composer.topic.id"),
this.get("composer.category.id"),
this.siteSettings.max_oneboxes_per_post,
refresh
);
if (refresh && loaded > 0) {
post.set("refreshedPost", true);
}
},
_expandShortUrls(preview) {
resolveAllShortUrls(ajax, this.siteSettings, preview);
},
_decorateCookedElement(preview) {
this.appEvents.trigger("decorate-non-stream-cooked-element", preview);
},
@debounce(DEBOUNCE_JIT_MS)
_warnMentionedGroups(preview) {
schedule("afterRender", () => {
preview
@ -537,7 +585,7 @@ export default Component.extend(ComposerUploadUppy, {
// add a delay to allow for typing, so you don't open the warning right away
// previously we would warn after @bob even if you were about to mention @bob2
@debounce(2000)
@debounce(DEBOUNCE_JIT_MS)
_warnCannotSeeMention(preview) {
if (this.composer.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) {
return;
@ -773,24 +821,31 @@ export default Component.extend(ComposerUploadUppy, {
},
_registerImageAltTextButtonClick(preview) {
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
preview.addEventListener("click", this._handleAltTextEditButtonClick);
preview.addEventListener("click", this._handleAltTextOkButtonClick);
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
preview.addEventListener("click", this._handleImageDeleteButtonClick);
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
preview.addEventListener("click", this._handleImageGridButtonClick);
preview.addEventListener("click", this._handleImageScaleButtonClick);
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
if (apiImageWrapperBtnEvents.length > 0) {
apiImageWrapperBtnEvents.forEach((fn) => {
preview.addEventListener("click", fn);
});
}
apiImageWrapperBtnEvents.forEach((fn) =>
preview.addEventListener("click", fn)
);
},
@on("willDestroyElement")
_composerClosed() {
this._unbindMobileUploadButton();
const input = this.element.querySelector(".d-editor-input");
const preview = this.element.querySelector(".d-editor-preview-wrapper");
if (this.allowUpload) {
this._unbindUploadTarget();
this._unbindMobileUploadButton();
}
this.appEvents.trigger(`${this.composerEventPrefix}:will-close`);
next(() => {
// need to wait a bit for the "slide down" transition of the composer
discourseLater(
@ -799,27 +854,22 @@ export default Component.extend(ComposerUploadUppy, {
);
});
this.element
.querySelector(".d-editor-input")
?.removeEventListener(
"scroll",
this._throttledSyncEditorAndPreviewScroll
);
input?.removeEventListener(
"scroll",
this._throttledSyncEditorAndPreviewScroll
);
const preview = this.element.querySelector(".d-editor-preview-wrapper");
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
preview?.removeEventListener("click", this._handleImageDeleteButtonClick);
preview?.removeEventListener("click", this._handleImageGridButtonClick);
preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
preview?.removeEventListener("keypress", this._handleAltTextInputKeypress);
if (apiImageWrapperBtnEvents.length > 0) {
apiImageWrapperBtnEvents.forEach((fn) => {
preview?.removeEventListener("click", fn);
});
}
apiImageWrapperBtnEvents.forEach((fn) =>
preview?.removeEventListener("click", fn)
);
},
onExpandPopupMenuOptions(toolbarEvent) {
@ -919,65 +969,17 @@ export default Component.extend(ComposerUploadUppy, {
});
},
previewUpdated(preview) {
// cache jquery objects for functions still using jquery
const $preview = $(preview);
previewUpdated(preview, unseenMentions, unseenHashtags) {
this._renderMentions(preview, unseenMentions);
this._renderHashtags(preview, unseenHashtags);
this._refreshOneboxes(preview);
this._expandShortUrls(preview);
// Paint mentions
const unseenMentions = linkSeenMentions(preview, this.siteSettings);
if (unseenMentions.length) {
discourseDebounce(
this,
this._renderUnseenMentions,
preview,
unseenMentions,
450
);
if (!this.siteSettings.enable_diffhtml_preview) {
this._decorateCookedElement(preview);
}
this._warnMentionedGroups(preview);
this._warnCannotSeeMention(preview);
// Paint category, tag, and other data source hashtags
const hashtagContext = this.site.hashtag_configurations["topic-composer"];
if (linkSeenHashtagsInContext(hashtagContext, preview).length > 0) {
discourseDebounce(this, this._renderUnseenHashtags, preview, 450);
}
// Paint oneboxes
const paintFunc = () => {
const post = this.get("composer.post");
let refresh = false;
//If we are editing a post, we'll refresh its contents once.
if (post && !post.get("refreshedPost")) {
refresh = true;
}
const paintedCount = loadOneboxes(
preview,
ajax,
this.get("composer.topic.id"),
this.get("composer.category.id"),
this.siteSettings.max_oneboxes_per_post,
refresh
);
if (refresh && paintedCount > 0) {
post.set("refreshedPost", true);
}
};
discourseDebounce(this, paintFunc, 450);
// Short upload urls need resolution
resolveAllShortUrls(ajax, this.siteSettings, preview);
preview.addEventListener("click", this._handleImageScaleButtonClick);
this._registerImageAltTextButtonClick(preview);
this.appEvents.trigger("decorate-non-stream-cooked-element", preview);
this.afterRefresh($preview);
this.afterRefresh(preview);
},
},
});

View File

@ -6,7 +6,7 @@ import ItsATrap from "@discourse/itsatrap";
import $ from "jquery";
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
import { translations } from "pretty-text/emoji/data";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
import { resolveCachedShortUrls } from "pretty-text/upload-short-url";
import { Promise } from "rsvp";
import InsertHyperlink from "discourse/components/modal/insert-hyperlink";
import { ajax } from "discourse/lib/ajax";
@ -445,14 +445,16 @@ export default Component.extend(TextareaTextManipulation, {
this.set("preview", cooked);
let unseenMentions, unseenHashtags;
if (this.siteSettings.enable_diffhtml_preview) {
const previewElement = this.element.querySelector(".d-editor-preview");
const cookedElement = previewElement.cloneNode(false);
cookedElement.innerHTML = cooked;
linkSeenMentions(cookedElement, this.siteSettings);
unseenMentions = linkSeenMentions(cookedElement, this.siteSettings);
linkSeenHashtagsInContext(
unseenHashtags = linkSeenHashtagsInContext(
this.site.hashtag_configurations["topic-composer"],
cookedElement
);
@ -467,7 +469,7 @@ export default Component.extend(TextareaTextManipulation, {
/* offline */ true
);
resolveAllShortUrls(ajax, this.siteSettings, cookedElement);
resolveCachedShortUrls(this.siteSettings, cookedElement);
// trigger all the "api.decorateCookedElement"
this.appEvents.trigger(
@ -495,7 +497,7 @@ export default Component.extend(TextareaTextManipulation, {
const previewElement = this.element.querySelector(".d-editor-preview");
if (previewElement && this.previewUpdated) {
this.previewUpdated(previewElement);
this.previewUpdated(previewElement, unseenMentions, unseenHashtags);
}
});
},

View File

@ -698,7 +698,7 @@ export default class ComposerService extends Service {
}
@action
afterRefresh($preview) {
afterRefresh(preview) {
const topic = this.get("model.topic");
const linkLookup = this.linkLookup;
@ -712,13 +712,12 @@ export default class ComposerService extends Service {
}
const post = this.get("model.post");
const $links = $("a[href]", $preview);
$links.each((idx, l) => {
preview.querySelectorAll("a[href]").forEach((l) => {
const href = l.href;
if (href && href.length) {
// skip links added by watched words
if (l.dataset.word !== undefined) {
return true;
return;
}
// skip links in quotes and oneboxes
@ -734,7 +733,7 @@ export default class ComposerService extends Service {
element.tagName === "ASIDE" &&
element.classList.contains("quote")
) {
return true;
return;
}
if (
@ -742,7 +741,7 @@ export default class ComposerService extends Service {
element.classList.contains("onebox") &&
href !== element.dataset["onebox-src"]
) {
return true;
return;
}
}
@ -771,11 +770,8 @@ export default class ComposerService extends Service {
}),
});
}
return false;
}
}
return true;
});
}