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
This commit is contained in:
Jarek Radosz 2022-10-20 13:28:09 +02:00 committed by GitHub
parent ce53152e53
commit 8304f40f84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 170 additions and 204 deletions

View File

@ -2,9 +2,8 @@ import Controller from "@ember/controller";
import I18n from "I18n"; import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment"; import { INPUT_DELAY } from "discourse-common/config/environment";
import { alias } from "@ember/object/computed"; import { alias } from "@ember/object/computed";
import discourseDebounce from "discourse-common/lib/debounce";
import { isEmpty } from "@ember/utils"; 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"; import { action } from "@ember/object";
export default Controller.extend({ export default Controller.extend({
@ -113,18 +112,13 @@ export default Controller.extend({
}, },
@observes("filter", "onlyOverridden", "model") @observes("filter", "onlyOverridden", "model")
@debounce(INPUT_DELAY)
filterContent() { filterContent() {
discourseDebounce( if (this._skipBounce) {
this, this.set("_skipBounce", false);
() => { } else {
if (this._skipBounce) { this.filterContentNow(this.categoryNameKey);
this.set("_skipBounce", false); }
} else {
this.filterContentNow(this.categoryNameKey);
}
},
INPUT_DELAY
);
}, },
@action @action

View File

@ -1,9 +1,8 @@
import Component from "@ember/component"; import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce";
import { action, get } from "@ember/object"; import { action, get } from "@ember/object";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { next } from "@ember/runloop"; 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"; import { searchForTerm } from "discourse/lib/search";
export default Component.extend({ export default Component.extend({
@ -30,37 +29,29 @@ export default Component.extend({
this.set("loading", false); this.set("loading", false);
}, },
@debounce(300)
search(title) { search(title) {
discourseDebounce( if (isEmpty(title)) {
this, this.setProperties({ messages: null, loading: false });
function () { return;
const currentTopicId = this.currentTopicId; }
if (isEmpty(title)) { searchForTerm(title, {
this.setProperties({ messages: null, loading: false }); typeFilter: "private_messages",
return; searchForId: true,
} restrictToArchetype: "private_message",
}).then((results) => {
searchForTerm(title, { if (results?.posts?.length) {
typeFilter: "private_messages", this.set(
searchForId: true, "messages",
restrictToArchetype: "private_message", results.posts
}).then((results) => { .mapBy("topic")
if (results && results.posts && results.posts.length > 0) { .filter((t) => t.get("id") !== this.currentTopicId)
this.set( );
"messages", } else {
results.posts this.setProperties({ messages: null, loading: false });
.mapBy("topic") }
.filter((t) => t.get("id") !== currentTopicId) });
);
} else {
this.setProperties({ messages: null, loading: false });
}
});
},
title,
300
);
}, },
@action @action

View File

@ -1,19 +1,32 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators";
export default Component.extend({ export default Component.extend({
tagName: "", tagName: "",
copyIcon: "copy", copyIcon: "copy",
copyClass: "btn-primary", copyClass: "btn-primary",
@bind
_restoreButton() {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.set("copyIcon", "copy");
this.set("copyClass", "btn-primary");
},
@action @action
copy() { copy() {
const target = document.querySelector(this.selector); const target = document.querySelector(this.selector);
target.select(); target.select();
target.setSelectionRange(0, target.value.length); target.setSelectionRange(0, target.value.length);
try { try {
document.execCommand("copy"); document.execCommand("copy");
if (this.copied) { if (this.copied) {
this.copied(); this.copied();
} }
@ -21,13 +34,7 @@ export default Component.extend({
this.set("copyIcon", "check"); this.set("copyIcon", "check");
this.set("copyClass", "btn-primary ok"); this.set("copyClass", "btn-primary ok");
discourseDebounce(() => { discourseDebounce(this._restoreButton, 3000);
if (this.isDestroying || this.isDestroyed) {
return;
}
this.set("copyIcon", "copy");
this.set("copyClass", "btn-primary");
}, 3000);
} catch (err) {} } catch (err) {}
}, },
}); });

View File

@ -1,8 +1,10 @@
import Controller, { inject as controller } from "@ember/controller"; 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 { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import discourseDebounce from "discourse-common/lib/debounce";
import { gt } from "@ember/object/computed"; import { gt } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
@ -23,14 +25,9 @@ export default Controller.extend({
bulkSelection: null, bulkSelection: null,
@observes("filterInput") @observes("filterInput")
@debounce(500)
_setFilter() { _setFilter() {
discourseDebounce( this.set("filter", this.filterInput);
this,
function () {
this.set("filter", this.filterInput);
},
500
);
}, },
@observes("order", "asc", "filter") @observes("order", "asc", "filter")

View File

@ -1,7 +1,9 @@
import Controller, { inject as controller } from "@ember/controller"; 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 { ajax } from "discourse/lib/ajax";
import discourseDebounce from "discourse-common/lib/debounce";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({ export default Controller.extend({
@ -17,14 +19,9 @@ export default Controller.extend({
loading: false, loading: false,
@observes("filterInput") @observes("filterInput")
@debounce(500)
_setFilter() { _setFilter() {
discourseDebounce( this.set("filter", this.filterInput);
this,
function () {
this.set("filter", this.filterInput);
},
500
);
}, },
@observes("order", "asc", "filter") @observes("order", "asc", "filter")

View File

@ -2,8 +2,10 @@ import Controller from "@ember/controller";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { equal, reads } from "@ember/object/computed"; import { equal, reads } from "@ember/object/computed";
import { INPUT_DELAY } from "discourse-common/config/environment"; import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseComputed, {
import discourseComputed, { observes } from "discourse-common/utils/decorators"; debounce,
observes,
} from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import Invite from "discourse/models/invite"; import Invite from "discourse/models/invite";
@ -28,15 +30,10 @@ export default Controller.extend({
}, },
@observes("searchTerm") @observes("searchTerm")
@debounce(INPUT_DELAY)
_searchTermChanged() { _searchTermChanged() {
discourseDebounce( Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
this, (invites) => this.set("model", invites)
function () {
Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
(invites) => this.set("model", invites)
);
},
INPUT_DELAY
); );
}, },

View File

@ -19,6 +19,32 @@ function updateCache(term, results) {
return 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) { function searchTags(term, categories, limit) {
return new Promise((resolve) => { return new Promise((resolve) => {
let clearPromise = isTesting() let clearPromise = isTesting()
@ -27,45 +53,18 @@ function searchTags(term, categories, limit) {
resolve(CANCELLED_STATUS); resolve(CANCELLED_STATUS);
}, 5000); }, 5000);
const debouncedSearch = (q, cats, resultFunc) => { discourseDebounce(
discourseDebounce( this,
this, searchFunc,
function () { term,
oldSearch = ajax("/tags/filter/search", { limit,
data: { limit, q }, categories,
}); (result) => {
cancel(clearPromise);
let returnVal = CANCELLED_STATUS; resolve(updateCache(term, result));
},
oldSearch 300
.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));
});
}); });
} }

View File

@ -131,28 +131,26 @@ function positioningWorkaround(fixedElement) {
} }
} }
const checkForInputs = function () { function checkForInputs() {
discourseDebounce( attachTouchStart(fixedElement, lastTouched);
this,
function () {
attachTouchStart(fixedElement, lastTouched);
fixedElement fixedElement
.querySelectorAll("input[type=text], textarea") .querySelectorAll("input[type=text], textarea")
.forEach((el) => { .forEach((el) => {
attachTouchStart(el, positioningHack); attachTouchStart(el, positioningHack);
}); });
}, }
100
); function debouncedCheckForInputs() {
}; discourseDebounce(checkForInputs, 100);
}
positioningWorkaround.touchstartEvent = function (element) { positioningWorkaround.touchstartEvent = function (element) {
let triggerHack = positioningHack.bind(element); let triggerHack = positioningHack.bind(element);
triggerHack(); triggerHack();
}; };
const observer = new MutationObserver(checkForInputs); const observer = new MutationObserver(debouncedCheckForInputs);
observer.observe(fixedElement, { observer.observe(fixedElement, {
childList: true, childList: true,
subtree: true, subtree: true,

View File

@ -2,34 +2,15 @@ import Mixin from "@ember/object/mixin";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { cancel } from "@ember/runloop"; import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; 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 INITIAL_DELAY_MS = 50;
const DEBOUNCE_MS = isTesting() ? 0 : 5; const DEBOUNCE_MS = 5;
export default Mixin.create({ export default Mixin.create({
queueDockCheck: null,
_initialTimer: null, _initialTimer: null,
_queuedTimer: 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() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
@ -57,4 +38,21 @@ export default Mixin.create({
window.removeEventListener("scroll", this.queueDockCheck); window.removeEventListener("scroll", this.queueDockCheck);
document.removeEventListener("touchmove", 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();
},
}); });

View File

@ -48,16 +48,14 @@ export default Mixin.create({
// If the user reaches the very bottom of the topic, we only want to reset // 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 // this scroll direction after a second scroll down. This is a nicer event
// similar to what Safari and Chrome do. // similar to what Safari and Chrome do.
discourseDebounce( discourseDebounce(this, this._setBottomHit, 1000);
this,
function () {
this._bottomHit = 1;
},
1000
);
if (this._bottomHit === 1) { if (this._bottomHit === 1) {
this.set("mobileScrollDirection", null); this.set("mobileScrollDirection", null);
} }
}, },
_setBottomHit() {
this._bottomHit = 1;
},
}); });

View File

@ -1,12 +1,14 @@
/* global Pikaday:true */ /* 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 Component from "@ember/component";
import EmberObject, { action } from "@ember/object"; import EmberObject, { action } from "@ember/object";
import I18n from "I18n"; import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment"; import { INPUT_DELAY } from "discourse-common/config/environment";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import { cookAsync } from "discourse/lib/text"; import { cookAsync } from "discourse/lib/text";
import discourseDebounce from "discourse-common/lib/debounce";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
import { notEmpty } from "@ember/object/computed"; import { notEmpty } from "@ember/object/computed";
@ -59,25 +61,19 @@ export default Component.extend({
}, },
@observes("computedConfig.{from,to,options}", "options", "isValid", "isRange") @observes("computedConfig.{from,to,options}", "options", "isValid", "isRange")
_renderPreview() { @debounce(INPUT_DELAY)
discourseDebounce( async _renderPreview() {
this, if (this.markup) {
function () { const result = await cookAsync(this.markup);
const markup = this.markup; this.set("currentPreview", result);
if (markup) {
cookAsync(markup).then((result) => { schedule("afterRender", () => {
this.set("currentPreview", result); applyLocalDates(
schedule("afterRender", () => { document.querySelectorAll(".preview .discourse-local-date"),
applyLocalDates( this.siteSettings
document.querySelectorAll(".preview .discourse-local-date"), );
this.siteSettings });
); }
});
});
}
},
INPUT_DELAY
);
}, },
@computed("date", "toDate", "toTime") @computed("date", "toDate", "toTime")

View File

@ -1,5 +1,5 @@
import { debounce } from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import discourseDebounce from "discourse-common/lib/debounce";
import { headerOffset } from "discourse/lib/offset-calculator"; import { headerOffset } from "discourse/lib/offset-calculator";
import isElementInViewport from "discourse/lib/is-element-in-viewport"; import isElementInViewport from "discourse/lib/is-element-in-viewport";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
@ -74,28 +74,22 @@ function initialize(api) {
// No need to unsubscribe, core unsubscribes /topic/* routes // No need to unsubscribe, core unsubscribes /topic/* routes
}, },
@debounce(500)
_scrollToDiscobotPost(postNumber) { _scrollToDiscobotPost(postNumber) {
discourseDebounce( const post = document.querySelector(
this, `.topic-post article#post_${postNumber}`
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
); );
if (!post || isElementInViewport(post)) {
return;
}
const viewportOffset = post.getBoundingClientRect();
window.scrollTo({
top: window.scrollY + viewportOffset.top - headerOffset(),
behavior: "smooth",
});
}, },
}); });