From 8509fc2ebcdab32f617fabf4c0ac8c1c002c15d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 9 Apr 2024 23:07:12 +0200 Subject: [PATCH] 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. --- .../app/components/composer-editor.js | 194 +++++++++--------- .../discourse/app/components/d-editor.js | 12 +- .../discourse/app/services/composer.js | 14 +- 3 files changed, 110 insertions(+), 110 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index a1b3a08743a..5e290a20611 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -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); }, }, }); diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index b4e2ee2bc43..a41efddf50b 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -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); } }); }, diff --git a/app/assets/javascripts/discourse/app/services/composer.js b/app/assets/javascripts/discourse/app/services/composer.js index 03fc5da0abb..a6d7cc3d2cc 100644 --- a/app/assets/javascripts/discourse/app/services/composer.js +++ b/app/assets/javascripts/discourse/app/services/composer.js @@ -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; }); }