From 8304f40f84dec7f7c31e75403c5527f077cb1020 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Thu, 20 Oct 2022 13:28:09 +0200 Subject: [PATCH] FIX: Correctly debounce various functions (#18673) Debouncing inline anonymous functions does not work. This fixes all instances of that error by extracting the function or using the new `@debounce(delay)` decorator --- .../addon/controllers/admin-site-settings.js | 20 ++--- .../app/components/choose-message.js | 53 ++++++------- .../discourse/app/components/copy-button.js | 21 +++-- .../discourse/app/controllers/group-index.js | 15 ++-- .../app/controllers/group-requests.js | 15 ++-- .../app/controllers/user-invited-show.js | 17 ++-- .../discourse/app/lib/category-tag-search.js | 77 +++++++++---------- .../discourse/app/lib/safari-hacks.js | 28 ++++--- .../discourse/app/mixins/docking.js | 42 +++++----- .../app/mixins/mobile-scroll-direction.js | 12 ++- .../discourse-local-dates-create-form.js | 38 ++++----- .../initializers/new-user-narrative.js | 36 ++++----- 12 files changed, 170 insertions(+), 204 deletions(-) diff --git a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js index 3dbcc3742cf..4ee3b0fab92 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js @@ -2,9 +2,8 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import { INPUT_DELAY } from "discourse-common/config/environment"; import { alias } from "@ember/object/computed"; -import discourseDebounce from "discourse-common/lib/debounce"; import { isEmpty } from "@ember/utils"; -import { observes } from "discourse-common/utils/decorators"; +import { debounce, observes } from "discourse-common/utils/decorators"; import { action } from "@ember/object"; export default Controller.extend({ @@ -113,18 +112,13 @@ export default Controller.extend({ }, @observes("filter", "onlyOverridden", "model") + @debounce(INPUT_DELAY) filterContent() { - discourseDebounce( - this, - () => { - if (this._skipBounce) { - this.set("_skipBounce", false); - } else { - this.filterContentNow(this.categoryNameKey); - } - }, - INPUT_DELAY - ); + if (this._skipBounce) { + this.set("_skipBounce", false); + } else { + this.filterContentNow(this.categoryNameKey); + } }, @action diff --git a/app/assets/javascripts/discourse/app/components/choose-message.js b/app/assets/javascripts/discourse/app/components/choose-message.js index 15f0424f60e..af9a19598bf 100644 --- a/app/assets/javascripts/discourse/app/components/choose-message.js +++ b/app/assets/javascripts/discourse/app/components/choose-message.js @@ -1,9 +1,8 @@ import Component from "@ember/component"; -import discourseDebounce from "discourse-common/lib/debounce"; import { action, get } from "@ember/object"; import { isEmpty } from "@ember/utils"; import { next } from "@ember/runloop"; -import { observes } from "discourse-common/utils/decorators"; +import { debounce, observes } from "discourse-common/utils/decorators"; import { searchForTerm } from "discourse/lib/search"; export default Component.extend({ @@ -30,37 +29,29 @@ export default Component.extend({ this.set("loading", false); }, + @debounce(300) search(title) { - discourseDebounce( - this, - function () { - const currentTopicId = this.currentTopicId; + if (isEmpty(title)) { + this.setProperties({ messages: null, loading: false }); + return; + } - if (isEmpty(title)) { - this.setProperties({ messages: null, loading: false }); - return; - } - - searchForTerm(title, { - typeFilter: "private_messages", - searchForId: true, - restrictToArchetype: "private_message", - }).then((results) => { - if (results && results.posts && results.posts.length > 0) { - this.set( - "messages", - results.posts - .mapBy("topic") - .filter((t) => t.get("id") !== currentTopicId) - ); - } else { - this.setProperties({ messages: null, loading: false }); - } - }); - }, - title, - 300 - ); + searchForTerm(title, { + typeFilter: "private_messages", + searchForId: true, + restrictToArchetype: "private_message", + }).then((results) => { + if (results?.posts?.length) { + this.set( + "messages", + results.posts + .mapBy("topic") + .filter((t) => t.get("id") !== this.currentTopicId) + ); + } else { + this.setProperties({ messages: null, loading: false }); + } + }); }, @action diff --git a/app/assets/javascripts/discourse/app/components/copy-button.js b/app/assets/javascripts/discourse/app/components/copy-button.js index c1b7eb24c89..f447629f498 100644 --- a/app/assets/javascripts/discourse/app/components/copy-button.js +++ b/app/assets/javascripts/discourse/app/components/copy-button.js @@ -1,19 +1,32 @@ import Component from "@ember/component"; import { action } from "@ember/object"; import discourseDebounce from "discourse-common/lib/debounce"; +import { bind } from "discourse-common/utils/decorators"; export default Component.extend({ tagName: "", copyIcon: "copy", copyClass: "btn-primary", + @bind + _restoreButton() { + if (this.isDestroying || this.isDestroyed) { + return; + } + + this.set("copyIcon", "copy"); + this.set("copyClass", "btn-primary"); + }, + @action copy() { const target = document.querySelector(this.selector); target.select(); target.setSelectionRange(0, target.value.length); + try { document.execCommand("copy"); + if (this.copied) { this.copied(); } @@ -21,13 +34,7 @@ export default Component.extend({ this.set("copyIcon", "check"); this.set("copyClass", "btn-primary ok"); - discourseDebounce(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - this.set("copyIcon", "copy"); - this.set("copyClass", "btn-primary"); - }, 3000); + discourseDebounce(this._restoreButton, 3000); } catch (err) {} }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/group-index.js b/app/assets/javascripts/discourse/app/controllers/group-index.js index 6bcbf5dd1b8..fd6826cd958 100644 --- a/app/assets/javascripts/discourse/app/controllers/group-index.js +++ b/app/assets/javascripts/discourse/app/controllers/group-index.js @@ -1,8 +1,10 @@ import Controller, { inject as controller } from "@ember/controller"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + debounce, + observes, +} from "discourse-common/utils/decorators"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; -import discourseDebounce from "discourse-common/lib/debounce"; import { gt } from "@ember/object/computed"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -23,14 +25,9 @@ export default Controller.extend({ bulkSelection: null, @observes("filterInput") + @debounce(500) _setFilter() { - discourseDebounce( - this, - function () { - this.set("filter", this.filterInput); - }, - 500 - ); + this.set("filter", this.filterInput); }, @observes("order", "asc", "filter") diff --git a/app/assets/javascripts/discourse/app/controllers/group-requests.js b/app/assets/javascripts/discourse/app/controllers/group-requests.js index 9cb4dcf3a68..71f0d0c3271 100644 --- a/app/assets/javascripts/discourse/app/controllers/group-requests.js +++ b/app/assets/javascripts/discourse/app/controllers/group-requests.js @@ -1,7 +1,9 @@ import Controller, { inject as controller } from "@ember/controller"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + debounce, + observes, +} from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; -import discourseDebounce from "discourse-common/lib/debounce"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default Controller.extend({ @@ -17,14 +19,9 @@ export default Controller.extend({ loading: false, @observes("filterInput") + @debounce(500) _setFilter() { - discourseDebounce( - this, - function () { - this.set("filter", this.filterInput); - }, - 500 - ); + this.set("filter", this.filterInput); }, @observes("order", "asc", "filter") diff --git a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js index 7ec24665f57..14e8bc968f9 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js +++ b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js @@ -2,8 +2,10 @@ import Controller from "@ember/controller"; import { action } from "@ember/object"; import { equal, reads } from "@ember/object/computed"; import { INPUT_DELAY } from "discourse-common/config/environment"; -import discourseDebounce from "discourse-common/lib/debounce"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + debounce, + observes, +} from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; import Invite from "discourse/models/invite"; @@ -28,15 +30,10 @@ export default Controller.extend({ }, @observes("searchTerm") + @debounce(INPUT_DELAY) _searchTermChanged() { - discourseDebounce( - this, - function () { - Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then( - (invites) => this.set("model", invites) - ); - }, - INPUT_DELAY + Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then( + (invites) => this.set("model", invites) ); }, diff --git a/app/assets/javascripts/discourse/app/lib/category-tag-search.js b/app/assets/javascripts/discourse/app/lib/category-tag-search.js index c73b2f3cf42..890c00084f0 100644 --- a/app/assets/javascripts/discourse/app/lib/category-tag-search.js +++ b/app/assets/javascripts/discourse/app/lib/category-tag-search.js @@ -19,6 +19,32 @@ function updateCache(term, results) { return results; } +function searchFunc(q, limit, cats, resultFunc) { + oldSearch = ajax("/tags/filter/search", { + data: { limit, q }, + }); + + let returnVal = CANCELLED_STATUS; + + oldSearch + .then((r) => { + const categoryNames = cats.map((c) => c.model.get("name")); + + const tags = r.results.map((tag) => { + tag.text = categoryNames.includes(tag.text) + ? `${tag.text}${TAG_HASHTAG_POSTFIX}` + : tag.text; + return tag; + }); + + returnVal = cats.concat(tags); + }) + .finally(() => { + oldSearch = null; + resultFunc(returnVal); + }); +} + function searchTags(term, categories, limit) { return new Promise((resolve) => { let clearPromise = isTesting() @@ -27,45 +53,18 @@ function searchTags(term, categories, limit) { resolve(CANCELLED_STATUS); }, 5000); - const debouncedSearch = (q, cats, resultFunc) => { - discourseDebounce( - this, - function () { - oldSearch = ajax("/tags/filter/search", { - data: { limit, q }, - }); - - let returnVal = CANCELLED_STATUS; - - oldSearch - .then((r) => { - const categoryNames = cats.map((c) => c.model.get("name")); - - const tags = r.results.map((tag) => { - tag.text = categoryNames.includes(tag.text) - ? `${tag.text}${TAG_HASHTAG_POSTFIX}` - : tag.text; - return tag; - }); - - returnVal = cats.concat(tags); - }) - .finally(() => { - oldSearch = null; - resultFunc(returnVal); - }); - }, - q, - cats, - resultFunc, - 300 - ); - }; - - debouncedSearch(term, categories, (result) => { - cancel(clearPromise); - resolve(updateCache(term, result)); - }); + discourseDebounce( + this, + searchFunc, + term, + limit, + categories, + (result) => { + cancel(clearPromise); + resolve(updateCache(term, result)); + }, + 300 + ); }); } diff --git a/app/assets/javascripts/discourse/app/lib/safari-hacks.js b/app/assets/javascripts/discourse/app/lib/safari-hacks.js index 58eea112361..d77b8b10b83 100644 --- a/app/assets/javascripts/discourse/app/lib/safari-hacks.js +++ b/app/assets/javascripts/discourse/app/lib/safari-hacks.js @@ -131,28 +131,26 @@ function positioningWorkaround(fixedElement) { } } - const checkForInputs = function () { - discourseDebounce( - this, - function () { - attachTouchStart(fixedElement, lastTouched); + function checkForInputs() { + attachTouchStart(fixedElement, lastTouched); - fixedElement - .querySelectorAll("input[type=text], textarea") - .forEach((el) => { - attachTouchStart(el, positioningHack); - }); - }, - 100 - ); - }; + fixedElement + .querySelectorAll("input[type=text], textarea") + .forEach((el) => { + attachTouchStart(el, positioningHack); + }); + } + + function debouncedCheckForInputs() { + discourseDebounce(checkForInputs, 100); + } positioningWorkaround.touchstartEvent = function (element) { let triggerHack = positioningHack.bind(element); triggerHack(); }; - const observer = new MutationObserver(checkForInputs); + const observer = new MutationObserver(debouncedCheckForInputs); observer.observe(fixedElement, { childList: true, subtree: true, diff --git a/app/assets/javascripts/discourse/app/mixins/docking.js b/app/assets/javascripts/discourse/app/mixins/docking.js index 7503cee52a2..33aa731a49c 100644 --- a/app/assets/javascripts/discourse/app/mixins/docking.js +++ b/app/assets/javascripts/discourse/app/mixins/docking.js @@ -2,34 +2,15 @@ import Mixin from "@ember/object/mixin"; import discourseDebounce from "discourse-common/lib/debounce"; import { cancel } from "@ember/runloop"; import discourseLater from "discourse-common/lib/later"; -import { isTesting } from "discourse-common/config/environment"; +import { bind } from "discourse-common/utils/decorators"; -const INITIAL_DELAY_MS = isTesting() ? 0 : 50; -const DEBOUNCE_MS = isTesting() ? 0 : 5; +const INITIAL_DELAY_MS = 50; +const DEBOUNCE_MS = 5; export default Mixin.create({ - queueDockCheck: null, _initialTimer: null, _queuedTimer: null, - init() { - this._super(...arguments); - this.queueDockCheck = () => { - this._queuedTimer = discourseDebounce( - this, - this.safeDockCheck, - DEBOUNCE_MS - ); - }; - }, - - safeDockCheck() { - if (this.isDestroyed || this.isDestroying) { - return; - } - this.dockCheck(); - }, - didInsertElement() { this._super(...arguments); @@ -57,4 +38,21 @@ export default Mixin.create({ window.removeEventListener("scroll", this.queueDockCheck); document.removeEventListener("touchmove", this.queueDockCheck); }, + + @bind + queueDockCheck() { + this._queuedTimer = discourseDebounce( + this, + this.safeDockCheck, + DEBOUNCE_MS + ); + }, + + @bind + safeDockCheck() { + if (this.isDestroyed || this.isDestroying) { + return; + } + this.dockCheck(); + }, }); diff --git a/app/assets/javascripts/discourse/app/mixins/mobile-scroll-direction.js b/app/assets/javascripts/discourse/app/mixins/mobile-scroll-direction.js index 27a31a8c459..66d8d36aa37 100644 --- a/app/assets/javascripts/discourse/app/mixins/mobile-scroll-direction.js +++ b/app/assets/javascripts/discourse/app/mixins/mobile-scroll-direction.js @@ -48,16 +48,14 @@ export default Mixin.create({ // If the user reaches the very bottom of the topic, we only want to reset // this scroll direction after a second scroll down. This is a nicer event // similar to what Safari and Chrome do. - discourseDebounce( - this, - function () { - this._bottomHit = 1; - }, - 1000 - ); + discourseDebounce(this, this._setBottomHit, 1000); if (this._bottomHit === 1) { this.set("mobileScrollDirection", null); } }, + + _setBottomHit() { + this._bottomHit = 1; + }, }); diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js index 3a880d715d5..dfc52ccc7bc 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js @@ -1,12 +1,14 @@ /* global Pikaday:true */ -import computed, { observes } from "discourse-common/utils/decorators"; +import computed, { + debounce, + observes, +} from "discourse-common/utils/decorators"; import Component from "@ember/component"; import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import { INPUT_DELAY } from "discourse-common/config/environment"; import { Promise } from "rsvp"; import { cookAsync } from "discourse/lib/text"; -import discourseDebounce from "discourse-common/lib/debounce"; import { isEmpty } from "@ember/utils"; import loadScript from "discourse/lib/load-script"; import { notEmpty } from "@ember/object/computed"; @@ -59,25 +61,19 @@ export default Component.extend({ }, @observes("computedConfig.{from,to,options}", "options", "isValid", "isRange") - _renderPreview() { - discourseDebounce( - this, - function () { - const markup = this.markup; - if (markup) { - cookAsync(markup).then((result) => { - this.set("currentPreview", result); - schedule("afterRender", () => { - applyLocalDates( - document.querySelectorAll(".preview .discourse-local-date"), - this.siteSettings - ); - }); - }); - } - }, - INPUT_DELAY - ); + @debounce(INPUT_DELAY) + async _renderPreview() { + if (this.markup) { + const result = await cookAsync(this.markup); + this.set("currentPreview", result); + + schedule("afterRender", () => { + applyLocalDates( + document.querySelectorAll(".preview .discourse-local-date"), + this.siteSettings + ); + }); + } }, @computed("date", "toDate", "toTime") diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js index 267ed5e44a1..4b12e22bad2 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js @@ -1,5 +1,5 @@ +import { debounce } from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; -import discourseDebounce from "discourse-common/lib/debounce"; import { headerOffset } from "discourse/lib/offset-calculator"; import isElementInViewport from "discourse/lib/is-element-in-viewport"; import { withPluginApi } from "discourse/lib/plugin-api"; @@ -74,28 +74,22 @@ function initialize(api) { // No need to unsubscribe, core unsubscribes /topic/* routes }, + @debounce(500) _scrollToDiscobotPost(postNumber) { - discourseDebounce( - this, - function () { - const post = document.querySelector( - `.topic-post article#post_${postNumber}` - ); - - if (!post || isElementInViewport(post)) { - return; - } - - const viewportOffset = post.getBoundingClientRect(); - - window.scrollTo({ - top: window.scrollY + viewportOffset.top - headerOffset(), - behavior: "smooth", - }); - }, - postNumber, - 500 + const post = document.querySelector( + `.topic-post article#post_${postNumber}` ); + + if (!post || isElementInViewport(post)) { + return; + } + + const viewportOffset = post.getBoundingClientRect(); + + window.scrollTo({ + top: window.scrollY + viewportOffset.top - headerOffset(), + behavior: "smooth", + }); }, });