DEV: reduces jquery usage and memory leaks in composer (#14924)

Removes more than 60 jquery function leaks in one `Acceptance: Composer` run.
This commit is contained in:
Joffrey JAFFEUX 2021-11-16 10:27:05 +01:00 committed by GitHub
parent ef881fdedc
commit ae16b0a9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 315 additions and 213 deletions

View File

@ -11,9 +11,9 @@ import discourseDebounce from "discourse-common/lib/debounce";
import { headerHeight } from "discourse/components/site-header";
import positioningWorkaround from "discourse/lib/safari-hacks";
const START_EVENTS = "touchstart mousedown";
const DRAG_EVENTS = "touchmove mousemove";
const END_EVENTS = "touchend mouseup";
const START_DRAG_EVENTS = ["touchstart", "mousedown"];
const DRAG_EVENTS = ["touchmove", "mousemove"];
const END_DRAG_EVENTS = ["touchend", "mouseup"];
const THROTTLE_RATE = 20;
@ -54,17 +54,15 @@ export default Component.extend(KeyEnterEscape, {
},
movePanels(size) {
$("#main-outlet").css("padding-bottom", size ? size : "");
document.querySelector("#main-outlet").style.paddingBottom = size
? `${size}px`
: "";
// signal the progress bar it should move!
this.appEvents.trigger("composer:resized");
},
@observes(
"composeState",
"composer.action",
"composer.canEditTopicFeaturedLink"
)
@observes("composeState", "composer.{action,canEditTopicFeaturedLink}")
resize() {
schedule("afterRender", () => {
if (!this.element || this.isDestroying || this.isDestroyed) {
@ -76,8 +74,11 @@ export default Component.extend(KeyEnterEscape, {
},
debounceMove() {
const h = $("#reply-control:not(.saving)").height() || 0;
this.movePanels(h);
let height = 0;
if (!this.element.classList.contains("saving")) {
height = this.element.offsetHeight;
}
this.movePanels(height);
},
keyUp() {
@ -105,45 +106,13 @@ export default Component.extend(KeyEnterEscape, {
},
setupComposerResizeEvents() {
const $composer = $(this.element);
const $grippie = $(this.element.querySelector(".grippie"));
const $document = $(document);
let origComposerSize = 0;
let lastMousePos = 0;
this.origComposerSize = 0;
this.lastMousePos = 0;
const performDrag = (event) => {
$composer.trigger("div-resizing");
this.appEvents.trigger("composer:div-resizing");
$composer.addClass("clear-transitions");
const currentMousePos = mouseYPos(event);
let size = origComposerSize + (lastMousePos - currentMousePos);
const winHeight = $(window).height();
size = Math.min(size, winHeight - headerHeight());
this.movePanels(size);
$composer.height(size);
};
const throttledPerformDrag = ((event) => {
event.preventDefault();
throttle(this, performDrag, event, THROTTLE_RATE);
}).bind(this);
const endDrag = (() => {
this.appEvents.trigger("composer:resize-ended");
$document.off(DRAG_EVENTS, throttledPerformDrag);
$document.off(END_EVENTS, endDrag);
$composer.removeClass("clear-transitions");
$composer.focus();
}).bind(this);
$grippie.on(START_EVENTS, (event) => {
event.preventDefault();
origComposerSize = $composer.height();
lastMousePos = mouseYPos(event);
$document.on(DRAG_EVENTS, throttledPerformDrag);
$document.on(END_EVENTS, endDrag);
this.appEvents.trigger("composer:resize-started");
START_DRAG_EVENTS.forEach((startDragEvent) => {
this.element
.querySelector(".grippie")
?.addEventListener(startDragEvent, this.startDragHandler);
});
if (this._visualViewportResizing()) {
@ -152,6 +121,58 @@ export default Component.extend(KeyEnterEscape, {
}
},
@bind
performDragHandler() {
this.appEvents.trigger("composer:div-resizing");
this.element.classList.add("clear-transitions");
const currentMousePos = mouseYPos(event);
let size = this.origComposerSize + (this.lastMousePos - currentMousePos);
size = Math.min(size, window.innerHeight - headerHeight());
this.movePanels(size);
this.element.style.height = size ? `${size}px` : "";
},
@bind
startDragHandler(event) {
event.preventDefault();
this.origComposerSize = this.element.offsetHeight;
this.lastMousePos = mouseYPos(event);
DRAG_EVENTS.forEach((dragEvent) => {
document.addEventListener(dragEvent, this.throttledPerformDrag);
});
END_DRAG_EVENTS.forEach((endDragEvent) => {
document.addEventListener(endDragEvent, this.endDragHandler);
});
this.appEvents.trigger("composer:resize-started");
},
@bind
endDragHandler() {
this.appEvents.trigger("composer:resize-ended");
DRAG_EVENTS.forEach((dragEvent) => {
document.removeEventListener(dragEvent, this.throttledPerformDrag);
});
END_DRAG_EVENTS.forEach((endDragEvent) => {
document.removeEventListener(endDragEvent, this.endDragHandler);
});
this.element.classList.remove("clear-transitions");
this.element.focus();
},
@bind
throttledPerformDrag(event) {
event.preventDefault();
throttle(this, this.performDragHandler, event, THROTTLE_RATE);
},
@bind
viewportResize() {
const composerVH = window.visualViewport.height * 0.01,
@ -207,10 +228,17 @@ export default Component.extend(KeyEnterEscape, {
willDestroyElement() {
this._super(...arguments);
if (this._visualViewportResizing()) {
window.visualViewport.removeEventListener("resize", this.viewportResize);
}
START_DRAG_EVENTS.forEach((startDragEvent) => {
this.element
.querySelector(".grippie")
?.removeEventListener(startDragEvent, this.startDragHandler);
});
cancel(this._lastKeyTimeout);
},

View File

@ -12,6 +12,7 @@ import {
tinyAvatar,
} from "discourse/lib/utilities";
import discourseComputed, {
bind,
observes,
on,
} from "discourse-common/utils/decorators";
@ -138,9 +139,7 @@ export default Component.extend(ComposerUpload, {
@discourseComputed
showLink() {
return (
this.currentUser && this.currentUser.get("link_posting_access") !== "none"
);
return this.currentUser && this.currentUser.link_posting_access !== "none";
},
@observes("focusTarget")
@ -189,7 +188,8 @@ export default Component.extend(ComposerUpload, {
};
},
userSearchTerm(term) {
@bind
_userSearchTerm(term) {
const topicId = this.get("topic.id");
// maybe this is a brand new topic, so grab category from composer
const categoryId =
@ -218,34 +218,42 @@ export default Component.extend(ComposerUpload, {
return extensions.map((ext) => `.${ext}`).join();
},
@bind
_afterMentionComplete(value) {
this.composer.set("reply", value);
// ensures textarea scroll position is correct
schedule("afterRender", () => {
const input = this.element.querySelector(".d-editor-input");
input?.blur();
input?.focus();
});
},
@on("didInsertElement")
_composerEditorInit() {
const $input = $(this.element.querySelector(".d-editor-input"));
const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
dataSource: (term) => this.userSearchTerm.call(this, term),
dataSource: this._userSearchTerm,
key: "@",
transformComplete: (v) => v.username || v.name,
afterComplete: (value) => {
this.composer.set("reply", value);
// ensures textarea scroll position is correct
schedule("afterRender", () => $input.blur().focus());
},
afterComplete: this._afterMentionComplete,
triggerRule: (textarea) =>
!inCodeBlock(textarea.value, caretPosition(textarea)),
});
}
if (this._enableAdvancedEditorPreviewSync()) {
this._initInputPreviewSync($input, $preview);
const input = this.element.querySelector(".d-editor-input");
const preview = this.element.querySelector(".d-editor-preview-wrapper");
this._initInputPreviewSync(input, preview);
} else {
$input.on("scroll", () =>
throttle(this, this._syncEditorAndPreviewScroll, $input, $preview, 20)
);
this.element
.querySelector(".d-editor-input")
?.addEventListener("scroll", this._throttledSyncEditorAndPreviewScroll);
}
// Focus on the body unless we have a title
@ -316,30 +324,47 @@ export default Component.extend(ComposerUpload, {
this.set("shouldBuildScrollMap", true);
},
_initInputPreviewSync($input, $preview) {
@bind
_handleInputInteraction(event) {
const preview = this.element.querySelector(".d-editor-preview-wrapper");
if (!$(preview).is(":visible")) {
return;
}
preview.removeEventListener("scroll", this._handleInputOrPreviewScroll);
event.target.addEventListener("scroll", this._handleInputOrPreviewScroll);
},
@bind
_handleInputOrPreviewScroll(event) {
this._syncScroll(
this._syncEditorAndPreviewScroll,
$(event.target),
$(this.element.querySelector(".d-editor-preview-wrapper"))
);
},
@bind
_handlePreviewInteraction(event) {
this.element
.querySelector(".d-editor-input")
?.removeEventListener("scroll", this._handleInputOrPreviewScroll);
event.target?.addEventListener("scroll", this._handleInputOrPreviewScroll);
},
_initInputPreviewSync(input, preview) {
REBUILD_SCROLL_MAP_EVENTS.forEach((event) => {
this.appEvents.on(event, this, this._resetShouldBuildScrollMap);
});
schedule("afterRender", () => {
$input.on("touchstart mouseenter", () => {
if (!$preview.is(":visible")) {
return;
}
$preview.off("scroll");
input?.addEventListener("touchstart", this._handleInputInteraction);
input?.addEventListener("mouseenter", this._handleInputInteraction);
$input.on("scroll", () => {
this._syncScroll(this._syncEditorAndPreviewScroll, $input, $preview);
});
});
$preview.on("touchstart mouseenter", () => {
$input.off("scroll");
$preview.on("scroll", () => {
this._syncScroll(this._syncPreviewAndEditorScroll, $input, $preview);
});
});
preview?.addEventListener("touchstart", this._handlePreviewInteraction);
preview?.addEventListener("mouseenter", this._handlePreviewInteraction);
});
},
@ -353,13 +378,15 @@ export default Component.extend(ComposerUpload, {
},
_teardownInputPreviewSync() {
[
$(this.element.querySelector(".d-editor-input")),
$(this.element.querySelector(".d-editor-preview-wrapper")),
].forEach(($element) => {
$element.off("mouseenter touchstart");
$element.off("scroll");
});
const input = this.element.querySelector(".d-editor-input");
input?.removeEventListener("mouseEnter", this._handleInputInteraction);
input?.removeEventListener("touchstart", this._handleInputInteraction);
input?.removeEventListener("scroll", this._handleInputOrPreviewScroll);
const preview = this.element.querySelector(".d-editor-preview-wrapper");
preview?.removeEventListener("mouseEnter", this._handlePreviewInteraction);
preview?.removeEventListener("touchstart", this._handlePreviewInteraction);
preview?.removeEventListener("scroll", this._handleInputOrPreviewScroll);
REBUILD_SCROLL_MAP_EVENTS.forEach((event) => {
this.appEvents.off(event, this, this._resetShouldBuildScrollMap);
@ -453,6 +480,19 @@ export default Component.extend(ComposerUpload, {
return scrollMap;
},
@bind
_throttledSyncEditorAndPreviewScroll(event) {
const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
throttle(
this,
this._syncEditorAndPreviewScroll,
$(event.target),
$preview,
20
);
},
_syncEditorAndPreviewScroll($input, $preview, scrollMap) {
if (this._enableAdvancedEditorPreviewSync()) {
let scrollTop;
@ -599,91 +639,103 @@ export default Component.extend(ComposerUpload, {
});
},
_registerImageScaleButtonClick($preview) {
$preview.off("click", ".scale-btn").on("click", ".scale-btn", (e) => {
@bind
_handleImageScaleButtonClick(event) {
if (!event.target.classList.contains("scale-btn")) {
return;
}
const index = parseInt(
event.target.closest(".button-wrapper").dataset.imageIndex,
10
);
const scale = event.target.dataset.scale;
const matchingPlaceholder = this.get("composer.reply").match(
IMAGE_MARKDOWN_REGEX
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
if (match) {
const replacement = match.replace(
IMAGE_MARKDOWN_REGEX,
`![$1|$2, ${scale}%$4]($5)`
);
this.appEvents.trigger(
"composer:replace-text",
matchingPlaceholder[index],
replacement,
{ regex: IMAGE_MARKDOWN_REGEX, index }
);
}
}
event.preventDefault();
return;
},
@bind
_handleAltTextInputKeypress(event) {
if (!event.target.classList.contains("alt-text-input")) {
return;
}
if (event.key === "[" || event.key === "]") {
event.preventDefault();
}
if (event.key === "Enter") {
const index = parseInt(
$(e.target).closest(".button-wrapper").attr("data-image-index"),
$(event.target).closest(".button-wrapper").attr("data-image-index"),
10
);
const scale = e.target.attributes["data-scale"].value;
const matchingPlaceholder = this.get("composer.reply").match(
IMAGE_MARKDOWN_REGEX
);
const match = matchingPlaceholder[index];
const replacement = match.replace(
IMAGE_MARKDOWN_REGEX,
`![${$(event.target).val()}|$2$3$4]($5)`
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
this.appEvents.trigger("composer:replace-text", match, replacement);
if (match) {
const replacement = match.replace(
IMAGE_MARKDOWN_REGEX,
`![$1|$2, ${scale}%$4]($5)`
);
this.appEvents.trigger(
"composer:replace-text",
matchingPlaceholder[index],
replacement,
{ regex: IMAGE_MARKDOWN_REGEX, index }
);
}
}
e.preventDefault();
return;
});
const parentContainer = $(event.target).closest(
".alt-text-readonly-container"
);
const altText = parentContainer.find(".alt-text");
const altTextButton = parentContainer.find(".alt-text-edit-btn");
altText.show();
altTextButton.show();
$(event.target).hide();
}
},
_registerImageAltTextButtonClick($preview) {
$preview
.off("click", ".alt-text-edit-btn")
.on("click", ".alt-text-edit-btn", (e) => {
const parentContainer = $(e.target).closest(
".alt-text-readonly-container"
);
const altText = parentContainer.find(".alt-text");
const correspondingInput = parentContainer.find(".alt-text-input");
@bind
_handleAltTextEditButtonClick(event) {
if (!event.target.classList.contains("alt-text-edit-btn")) {
return;
}
$(e.target).hide();
altText.hide();
correspondingInput.val(altText.text());
correspondingInput.show();
e.preventDefault();
});
const parentContainer = $(event.target).closest(
".alt-text-readonly-container"
);
const altText = parentContainer.find(".alt-text");
const correspondingInput = parentContainer.find(".alt-text-input");
$preview
.off("keypress", ".alt-text-input")
.on("keypress", ".alt-text-input", (e) => {
if (e.key === "[" || e.key === "]") {
e.preventDefault();
}
$(event.target).hide();
altText.hide();
correspondingInput.val(altText.text());
correspondingInput.show();
event.preventDefault();
},
if (e.key === "Enter") {
const index = parseInt(
$(e.target).closest(".button-wrapper").attr("data-image-index"),
10
);
const matchingPlaceholder = this.get("composer.reply").match(
IMAGE_MARKDOWN_REGEX
);
const match = matchingPlaceholder[index];
const replacement = match.replace(
IMAGE_MARKDOWN_REGEX,
`![${$(e.target).val()}|$2$3$4]($5)`
);
this.appEvents.trigger("composer:replace-text", match, replacement);
const parentContainer = $(e.target).closest(
".alt-text-readonly-container"
);
const altText = parentContainer.find(".alt-text");
const altTextButton = parentContainer.find(".alt-text-edit-btn");
altText.show();
altTextButton.show();
$(e.target).hide();
}
});
_registerImageAltTextButtonClick(preview) {
preview.addEventListener("click", this._handleAltTextEditButtonClick);
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
},
@on("willDestroyElement")
@ -701,6 +753,20 @@ export default Component.extend(ComposerUpload, {
if (this._enableAdvancedEditorPreviewSync()) {
this._teardownInputPreviewSync();
}
if (!this._enableAdvancedEditorPreviewSync()) {
this.element
.querySelector(".d-editor-input")
?.removeEventListener(
"scroll",
this._throttledSyncEditorAndPreviewScroll
);
}
const preview = this.element.querySelector(".d-editor-preview-wrapper");
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
preview?.removeEventListener("keypress", this._handleAltTextInputKeypress);
},
onExpandPopupMenuOptions(toolbarEvent) {
@ -863,8 +929,8 @@ export default Component.extend(ComposerUpload, {
);
}
this._registerImageScaleButtonClick($preview);
this._registerImageAltTextButtonClick($preview);
preview.addEventListener("click", this._handleImageScaleButtonClick);
this._registerImageAltTextButtonClick(preview);
this.trigger("previewRefreshed", preview);
this.afterRefresh($preview);

View File

@ -5,6 +5,7 @@ import {
translateModKey,
} from "discourse/lib/utilities";
import discourseComputed, {
bind,
observes,
on,
} from "discourse-common/utils/decorators";
@ -286,31 +287,9 @@ export default Component.extend(TextareaTextManipulation, {
});
// disable clicking on links in the preview
$(this.element.querySelector(".d-editor-preview")).on(
"click.preview",
(e) => {
if (wantsNewWindow(e)) {
return;
}
const $target = $(e.target);
if ($target.is("a.mention")) {
this.appEvents.trigger(
"click.discourse-preview-user-card-mention",
$target
);
}
if ($target.is("a.mention-group")) {
this.appEvents.trigger(
"click.discourse-preview-group-card-mention-group",
$target
);
}
if ($target.is("a")) {
e.preventDefault();
return false;
}
}
);
this.element
.querySelector(".d-editor-preview")
.addEventListener("click", this._handlePreviewLinkClick);
if (this.composerEvents) {
this.appEvents.on("composer:insert-block", this, "_insertBlock");
@ -323,6 +302,32 @@ export default Component.extend(TextareaTextManipulation, {
}
},
@bind
_handlePreviewLinkClick(event) {
if (wantsNewWindow(event)) {
return;
}
if (event.target.tagName === "A") {
if (event.target.classList.contains("mention")) {
this.appEvents.trigger(
"click.discourse-preview-user-card-mention",
$(event.target)
);
}
if (event.target.classList.contains("mention-group")) {
this.appEvents.trigger(
"click.discourse-preview-group-card-mention-group",
$(event.target)
);
}
event.preventDefault();
return false;
}
},
@on("willDestroyElement")
_shutDown() {
if (this.composerEvents) {
@ -334,7 +339,9 @@ export default Component.extend(TextareaTextManipulation, {
this._itsatrap?.destroy();
this._itsatrap = null;
$(this.element.querySelector(".d-editor-preview")).off("click.preview");
this.element
.querySelector(".d-editor-preview")
?.removeEventListener("click", this._handlePreviewLinkClick);
this._previewMutationObserver?.disconnect();

View File

@ -46,8 +46,6 @@ const keys = {
let inputTimeout;
export default function (options) {
const autocompletePlugin = this;
if (this.length === 0) {
return;
}
@ -55,13 +53,11 @@ export default function (options) {
if (options === "destroy" || options.updateData) {
cancel(inputTimeout);
$(this)
.off("keyup.autocomplete")
.off("keydown.autocomplete")
.off("paste.autocomplete")
.off("click.autocomplete");
$(window).off("click.autocomplete");
this[0].removeEventListener("keydown", handleKeyDown);
this[0].removeEventListener("keyup", handleKeyUp);
this[0].removeEventListener("paste", handlePaste);
this[0].removeEventListener("click", closeAutocomplete);
window.removeEventListener("click", closeAutocomplete);
if (options === "destroy") {
return;
@ -116,8 +112,12 @@ export default function (options) {
const isInput = me[0].tagName === "INPUT" && !options.treatAsTextarea;
let inputSelectedItems = [];
function handlePaste() {
later(() => me.trigger("keydown"), 50);
}
function closeAutocomplete() {
_autoCompletePopper && _autoCompletePopper.destroy();
_autoCompletePopper?.destroy();
if (div) {
div.hide().remove();
@ -276,7 +276,7 @@ export default function (options) {
this.val("");
completeStart = 0;
wrap.click(function () {
autocompletePlugin.focus();
this.focus();
return true;
});
}
@ -447,24 +447,17 @@ export default function (options) {
closeAutocomplete();
});
$(window).on("click.autocomplete", () => closeAutocomplete());
$(this).on("click.autocomplete", () => closeAutocomplete());
$(this).on("paste.autocomplete", () => {
later(() => me.trigger("keydown"), 50);
});
function checkTriggerRule(opts) {
return options.triggerRule ? options.triggerRule(me[0], opts) : true;
}
$(this).on("keyup.autocomplete", function (e) {
function handleKeyUp(e) {
if (options.debounced) {
discourseDebounce(this, performAutocomplete, e, INPUT_DELAY);
} else {
performAutocomplete(e);
}
});
}
function performAutocomplete(e) {
if ([keys.esc, keys.enter].indexOf(e.which) !== -1) {
@ -503,7 +496,7 @@ export default function (options) {
}
}
$(this).on("keydown.autocomplete", function (e) {
function handleKeyDown(e) {
let c, i, initial, prev, prevIsGood, stopFound, term, total, userToComplete;
let cp;
@ -602,7 +595,9 @@ export default function (options) {
// We're cancelling it, really.
return true;
}
e.stopImmediatePropagation();
e.preventDefault();
return false;
case keys.upArrow:
selectedOption = selectedOption - 1;
@ -652,7 +647,13 @@ export default function (options) {
return true;
}
}
});
}
window.addEventListener("click", closeAutocomplete);
this[0].addEventListener("click", closeAutocomplete);
this[0].addEventListener("paste", handlePaste);
this[0].addEventListener("keyup", handleKeyUp);
this[0].addEventListener("keydown", handleKeyDown);
return this;
}

View File

@ -1,3 +1,4 @@
import { bind } from "discourse-common/utils/decorators";
import Mixin from "@ember/object/mixin";
import toMarkdown from "discourse/lib/to-markdown";
import { isTesting } from "discourse-common/config/environment";
@ -6,7 +7,6 @@ import {
determinePostReplaceSelection,
safariHacksDisabled,
} from "discourse/lib/utilities";
import { bind } from "discourse-common/utils/decorators";
import { next, schedule } from "@ember/runloop";
const isInside = (text, regex) => {