refactor: replace debounce/throttle deps with first-party code (#38610)
Adds `web_src/js/utils/func.ts` with `debounce` and `throttle`, dropping
`throttle-debounce` and `perfect-debounce` dependencies. Both were
needed before: the former has no promise-returning debounce, which
`TextExpander` requires, and the latter no `throttle`.
Signature follows lodash and es-toolkit: `(func, wait, {leading,
trailing})` plus `cancel`, so argument order flips at the migrated call
sites.
This commit is contained in:
@@ -55,13 +55,11 @@
|
|||||||
"mermaid": "11.16.0",
|
"mermaid": "11.16.0",
|
||||||
"online-3d-viewer": "0.18.0",
|
"online-3d-viewer": "0.18.0",
|
||||||
"pdfobject": "2.3.1",
|
"pdfobject": "2.3.1",
|
||||||
"perfect-debounce": "2.1.0",
|
|
||||||
"postcss": "8.5.19",
|
"postcss": "8.5.19",
|
||||||
"rolldown-license-plugin": "3.0.9",
|
"rolldown-license-plugin": "3.0.9",
|
||||||
"sortablejs": "1.15.7",
|
"sortablejs": "1.15.7",
|
||||||
"swagger-ui-dist": "5.32.8",
|
"swagger-ui-dist": "5.32.8",
|
||||||
"tailwindcss": "3.4.19",
|
"tailwindcss": "3.4.19",
|
||||||
"throttle-debounce": "5.0.2",
|
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"toastify-js": "1.12.0",
|
"toastify-js": "1.12.0",
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
@@ -84,7 +82,6 @@
|
|||||||
"@types/pdfobject": "2.2.5",
|
"@types/pdfobject": "2.2.5",
|
||||||
"@types/sortablejs": "1.15.9",
|
"@types/sortablejs": "1.15.9",
|
||||||
"@types/swagger-ui-dist": "3.30.6",
|
"@types/swagger-ui-dist": "3.30.6",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
|
||||||
"@types/toastify-js": "1.12.4",
|
"@types/toastify-js": "1.12.4",
|
||||||
"@typescript-eslint/parser": "8.64.0",
|
"@typescript-eslint/parser": "8.64.0",
|
||||||
"@vitejs/plugin-vue": "6.0.8",
|
"@vitejs/plugin-vue": "6.0.8",
|
||||||
|
|||||||
Generated
-25
@@ -155,9 +155,6 @@ importers:
|
|||||||
pdfobject:
|
pdfobject:
|
||||||
specifier: 2.3.1
|
specifier: 2.3.1
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
perfect-debounce:
|
|
||||||
specifier: 2.1.0
|
|
||||||
version: 2.1.0
|
|
||||||
postcss:
|
postcss:
|
||||||
specifier: 8.5.19
|
specifier: 8.5.19
|
||||||
version: 8.5.19
|
version: 8.5.19
|
||||||
@@ -173,9 +170,6 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 3.4.19
|
specifier: 3.4.19
|
||||||
version: 3.4.19(yaml@2.9.0)
|
version: 3.4.19(yaml@2.9.0)
|
||||||
throttle-debounce:
|
|
||||||
specifier: 5.0.2
|
|
||||||
version: 5.0.2
|
|
||||||
tippy.js:
|
tippy.js:
|
||||||
specifier: 6.3.7
|
specifier: 6.3.7
|
||||||
version: 6.3.7
|
version: 6.3.7
|
||||||
@@ -237,9 +231,6 @@ importers:
|
|||||||
'@types/swagger-ui-dist':
|
'@types/swagger-ui-dist':
|
||||||
specifier: 3.30.6
|
specifier: 3.30.6
|
||||||
version: 3.30.6
|
version: 3.30.6
|
||||||
'@types/throttle-debounce':
|
|
||||||
specifier: 5.0.2
|
|
||||||
version: 5.0.2
|
|
||||||
'@types/toastify-js':
|
'@types/toastify-js':
|
||||||
specifier: 1.12.4
|
specifier: 1.12.4
|
||||||
version: 1.12.4
|
version: 1.12.4
|
||||||
@@ -1335,9 +1326,6 @@ packages:
|
|||||||
'@types/tern@0.23.9':
|
'@types/tern@0.23.9':
|
||||||
resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==}
|
resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==}
|
||||||
|
|
||||||
'@types/throttle-debounce@5.0.2':
|
|
||||||
resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==}
|
|
||||||
|
|
||||||
'@types/toastify-js@1.12.4':
|
'@types/toastify-js@1.12.4':
|
||||||
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
|
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
|
||||||
|
|
||||||
@@ -3546,9 +3534,6 @@ packages:
|
|||||||
pdfobject@2.3.1:
|
pdfobject@2.3.1:
|
||||||
resolution: {integrity: sha512-vluuGiSDmMGpOvWFGiUY4trNB8aGKLDVxIXuuGHjX0kK3bMxCANUVtLivctE7uejLBScWCnbVarKatFVvdwXaQ==}
|
resolution: {integrity: sha512-vluuGiSDmMGpOvWFGiUY4trNB8aGKLDVxIXuuGHjX0kK3bMxCANUVtLivctE7uejLBScWCnbVarKatFVvdwXaQ==}
|
||||||
|
|
||||||
perfect-debounce@2.1.0:
|
|
||||||
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -4068,10 +4053,6 @@ packages:
|
|||||||
three@0.176.0:
|
three@0.176.0:
|
||||||
resolution: {integrity: sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==}
|
resolution: {integrity: sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==}
|
||||||
|
|
||||||
throttle-debounce@5.0.2:
|
|
||||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
|
||||||
engines: {node: '>=12.22'}
|
|
||||||
|
|
||||||
time-span@5.1.0:
|
time-span@5.1.0:
|
||||||
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
|
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -5579,8 +5560,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.9
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
'@types/throttle-debounce@5.0.2': {}
|
|
||||||
|
|
||||||
'@types/toastify-js@1.12.4': {}
|
'@types/toastify-js@1.12.4': {}
|
||||||
|
|
||||||
'@types/tough-cookie@4.0.5': {}
|
'@types/tough-cookie@4.0.5': {}
|
||||||
@@ -8155,8 +8134,6 @@ snapshots:
|
|||||||
|
|
||||||
pdfobject@2.3.1: {}
|
pdfobject@2.3.1: {}
|
||||||
|
|
||||||
perfect-debounce@2.1.0: {}
|
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.2: {}
|
picomatch@2.3.2: {}
|
||||||
@@ -8774,8 +8751,6 @@ snapshots:
|
|||||||
|
|
||||||
three@0.176.0: {}
|
three@0.176.0: {}
|
||||||
|
|
||||||
throttle-debounce@5.0.2: {}
|
|
||||||
|
|
||||||
time-span@5.1.0:
|
time-span@5.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
convert-hrtime: 5.0.0
|
convert-hrtime: 5.0.0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {GET} from '../modules/fetch.ts';
|
|||||||
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
|
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
|
||||||
import {pathEscapeSegments} from '../utils/url.ts';
|
import {pathEscapeSegments} from '../utils/url.ts';
|
||||||
import {SvgIcon} from '../svg.ts';
|
import {SvgIcon} from '../svg.ts';
|
||||||
import {throttle} from 'throttle-debounce';
|
import {throttle} from '../utils/func.ts';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
repoLink: { type: String, required: true },
|
repoLink: { type: String, required: true },
|
||||||
@@ -31,10 +31,10 @@ const filteredFiles = computed(() => {
|
|||||||
return filterRepoFilesWeighted(allFiles.value, searchQuery.value);
|
return filterRepoFilesWeighted(allFiles.value, searchQuery.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const applySearchQuery = throttle(300, () => {
|
const applySearchQuery = throttle(() => {
|
||||||
searchQuery.value = refElemInput.value.value;
|
searchQuery.value = refElemInput.value.value;
|
||||||
selectedIndex.value = 0;
|
selectedIndex.value = 0;
|
||||||
});
|
}, 300);
|
||||||
|
|
||||||
const handleSearchInput = () => {
|
const handleSearchInput = () => {
|
||||||
loadFileListForSearch();
|
loadFileListForSearch();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ActionStatusIcon from './ActionStatusIcon.vue';
|
|||||||
import {localUserSettings} from '../modules/user-settings.ts';
|
import {localUserSettings} from '../modules/user-settings.ts';
|
||||||
import {isPlainClick} from '../utils/dom.ts';
|
import {isPlainClick} from '../utils/dom.ts';
|
||||||
import {trN} from '../modules/i18n.ts';
|
import {trN} from '../modules/i18n.ts';
|
||||||
import {debounce} from 'throttle-debounce';
|
import {debounce} from '../utils/func.ts';
|
||||||
import type {ActionsJob} from '../modules/gitea-actions.ts';
|
import type {ActionsJob} from '../modules/gitea-actions.ts';
|
||||||
import type {ActionRunViewStore} from './ActionRunView.ts';
|
import type {ActionRunViewStore} from './ActionRunView.ts';
|
||||||
import {
|
import {
|
||||||
@@ -178,7 +178,7 @@ function handleWheel(event: WheelEvent) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSavedState();
|
loadSavedState();
|
||||||
watch([translateX, translateY, scale], debounce(500, saveState));
|
watch([translateX, translateY, scale], debounce(saveState, 500));
|
||||||
document.addEventListener('mousemove', handleMouseMoveOnDocument);
|
document.addEventListener('mousemove', handleMouseMoveOnDocument);
|
||||||
document.addEventListener('mouseup', handleMouseUpOnDocument);
|
document.addEventListener('mouseup', handleMouseUpOnDocument);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {svg} from '../../svg.ts';
|
|||||||
import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts';
|
import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts';
|
||||||
import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
|
import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
|
||||||
import {getIssueColorClass, getIssueIcon} from '../issue.ts';
|
import {getIssueColorClass, getIssueIcon} from '../issue.ts';
|
||||||
import {debounce} from 'perfect-debounce';
|
import {debounce} from '../../utils/func.ts';
|
||||||
import type TextExpanderElement from '@github/text-expander-element';
|
import type TextExpanderElement from '@github/text-expander-element';
|
||||||
import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element';
|
import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {GET} from '../modules/fetch.ts';
|
|||||||
import {createApp} from 'vue';
|
import {createApp} from 'vue';
|
||||||
import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts';
|
import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts';
|
||||||
import {addDelegatedEventListener} from '../utils/dom.ts';
|
import {addDelegatedEventListener} from '../utils/dom.ts';
|
||||||
import type {Issue} from '../types.ts';
|
import type {Issue, TimeoutId} from '../types.ts';
|
||||||
|
|
||||||
type IssueInfo = {
|
type IssueInfo = {
|
||||||
convertedIssue: Issue,
|
convertedIssue: Issue,
|
||||||
@@ -55,7 +55,7 @@ export function initRefIssueContextPopup() {
|
|||||||
link.setAttribute('data-ref-issue-popup', '');
|
link.setAttribute('data-ref-issue-popup', '');
|
||||||
|
|
||||||
// delay so a mouse passing over the link doesn't fire a fetch
|
// delay so a mouse passing over the link doesn't fire a fetch
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
let timer: TimeoutId;
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
link.removeAttribute('data-ref-issue-popup');
|
link.removeAttribute('data-ref-issue-popup');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {debounce} from 'throttle-debounce';
|
import {debounce} from '../utils/func.ts';
|
||||||
import {GET} from './fetch.ts';
|
import {GET} from './fetch.ts';
|
||||||
import {errorName} from './errors.ts';
|
import {errorName} from './errors.ts';
|
||||||
import {html, htmlRaw} from '../utils/html.ts';
|
import {html, htmlRaw} from '../utils/html.ts';
|
||||||
@@ -69,7 +69,7 @@ export function attachSearchBox<T = unknown>(container: HTMLElement, url: string
|
|||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = debounce(200, async (query: string) => {
|
const search = debounce(async (query: string) => {
|
||||||
fetchController?.abort();
|
fetchController?.abort();
|
||||||
if (query.length < minCharacters) return hide();
|
if (query.length < minCharacters) return hide();
|
||||||
const ctrl = (fetchController = new AbortController());
|
const ctrl = (fetchController = new AbortController());
|
||||||
@@ -82,7 +82,7 @@ export function attachSearchBox<T = unknown>(container: HTMLElement, url: string
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (errorName(err) !== 'AbortError') hide();
|
if (errorName(err) !== 'AbortError') hide();
|
||||||
}
|
}
|
||||||
});
|
}, 200);
|
||||||
// cancel + hide ensures a debounced fetch scheduled before any of these can't fire afterwards
|
// cancel + hide ensures a debounced fetch scheduled before any of these can't fire afterwards
|
||||||
const dismiss = () => { search.cancel(); hide() };
|
const dismiss = () => { search.cancel(); hide() };
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export type TimeoutId = ReturnType<typeof setTimeout>;
|
||||||
export type IntervalId = ReturnType<typeof setInterval>;
|
export type IntervalId = ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
export type Intent = 'error' | 'warning' | 'info';
|
export type Intent = 'error' | 'warning' | 'info';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {debounce} from 'throttle-debounce';
|
import {debounce} from './func.ts';
|
||||||
import type {Promisable} from '../types.ts';
|
import type {Promisable} from '../types.ts';
|
||||||
import type $ from 'jquery';
|
import type $ from 'jquery';
|
||||||
import {isInFrontendUnitTest} from './testhelper.ts';
|
import {isInFrontendUnitTest} from './testhelper.ts';
|
||||||
@@ -242,7 +242,7 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function onInputDebounce(fn: () => Promisable<any>) {
|
export function onInputDebounce(fn: () => Promisable<any>) {
|
||||||
return debounce(300, fn);
|
return debounce(fn, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement;
|
type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {debounce, throttle} from './func.ts';
|
||||||
|
|
||||||
|
test('debounce', {concurrent: false}, () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const spy = vi.fn();
|
||||||
|
const fn = debounce(spy, 10);
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
|
vi.advanceTimersByTime(30);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('debounce leading', {concurrent: false}, () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const spy = vi.fn();
|
||||||
|
const fn = debounce(spy, 10, {leading: true, trailing: false});
|
||||||
|
fn();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
fn();
|
||||||
|
vi.advanceTimersByTime(30);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('debounce result', {concurrent: false}, async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const fn = debounce((value: number) => value * 2, 10);
|
||||||
|
const first = fn(1);
|
||||||
|
const second = fn(2);
|
||||||
|
vi.advanceTimersByTime(10);
|
||||||
|
expect(await first).toEqual(4); // both calls collapse into the last one
|
||||||
|
expect(await second).toEqual(4);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('debounce cancel', {concurrent: false}, () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const spy = vi.fn();
|
||||||
|
const fn = debounce(spy, 10);
|
||||||
|
fn();
|
||||||
|
fn.cancel();
|
||||||
|
vi.advanceTimersByTime(30);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throttle', {concurrent: false}, () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const spy = vi.fn();
|
||||||
|
const fn = throttle(spy, 10);
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1); // leading
|
||||||
|
vi.advanceTimersByTime(30);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(2); // plus one trailing for the collapsed rest
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throttle trailing only', {concurrent: false}, () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const spy = vi.fn();
|
||||||
|
const fn = throttle(spy, 10, {leading: false});
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
|
vi.advanceTimersByTime(30);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type {TimeoutId} from '../types.ts';
|
||||||
|
|
||||||
|
/** Options for `debounce` */
|
||||||
|
export type DebounceOpts = {
|
||||||
|
/** Invoke on the leading edge of the wait period. Default: `false` */
|
||||||
|
leading?: boolean,
|
||||||
|
/** Invoke on the trailing edge of the wait period. Default: `true` */
|
||||||
|
trailing?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Options for `throttle` */
|
||||||
|
export type ThrottleOpts = {
|
||||||
|
/** Invoke on the leading edge of the interval. Default: `true` */
|
||||||
|
leading?: boolean,
|
||||||
|
/** Invoke on the trailing edge of the interval. Default: `true` */
|
||||||
|
trailing?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A debounced or throttled function. Calls collapsed into one invocation settle with its result, dropped calls never settle. */
|
||||||
|
export type TimedFunction<T extends (...args: Array<any>) => any> = ((...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>>) & {
|
||||||
|
/** Drop the pending invocation */
|
||||||
|
cancel: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTimed<T extends (...args: Array<any>) => any>(func: T, wait: number, leading: boolean, trailing: boolean, isThrottle: boolean): TimedFunction<T> {
|
||||||
|
let timer: TimeoutId | null = null;
|
||||||
|
let pendingArgs: Parameters<T> | null = null;
|
||||||
|
let resolvers: Array<{resolve: (value: any) => void, reject: (reason: any) => void}> = [];
|
||||||
|
|
||||||
|
const invoke = async (args: Parameters<T>): Promise<void> => {
|
||||||
|
const settling = resolvers;
|
||||||
|
resolvers = [];
|
||||||
|
try {
|
||||||
|
const value = await func(...args);
|
||||||
|
for (const {resolve} of settling) resolve(value);
|
||||||
|
} catch (err) {
|
||||||
|
for (const {reject} of settling) reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimer = (): void => {
|
||||||
|
timer = null;
|
||||||
|
const args = pendingArgs;
|
||||||
|
pendingArgs = null;
|
||||||
|
if (trailing && args) {
|
||||||
|
invoke(args);
|
||||||
|
if (isThrottle) timer = setTimeout(onTimer, wait); // keep the window open so a burst stays rate-limited
|
||||||
|
} else {
|
||||||
|
resolvers = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = (): void => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
pendingArgs = null;
|
||||||
|
resolvers = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> => {
|
||||||
|
const promise = new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
|
||||||
|
resolvers.push({resolve, reject});
|
||||||
|
});
|
||||||
|
const isLeadingCall = leading && timer === null;
|
||||||
|
if (!isThrottle && timer) { clearTimeout(timer); timer = null } // debounce restarts the wait on every call, throttle does not
|
||||||
|
if (isLeadingCall) invoke(args); else pendingArgs = args;
|
||||||
|
if (timer === null) timer = setTimeout(onTimer, wait);
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.assign(wrapper, {cancel});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debounce a function, delaying invocation until `wait` milliseconds have passed without another call */
|
||||||
|
export function debounce<T extends (...args: Array<any>) => any>(func: T, wait: number, {leading = false, trailing = true}: DebounceOpts = {}): TimedFunction<T> {
|
||||||
|
return createTimed(func, wait, leading, trailing, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throttle a function to invoke at most once per `interval` milliseconds */
|
||||||
|
export function throttle<T extends (...args: Array<any>) => any>(func: T, interval: number, {leading = true, trailing = true}: ThrottleOpts = {}): TimedFunction<T> {
|
||||||
|
return createTimed(func, interval, leading, trailing, true);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {throttle} from 'throttle-debounce';
|
import {throttle} from '../utils/func.ts';
|
||||||
import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||||
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
|
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateItems = throttle(100, () => {
|
updateItems = throttle(() => {
|
||||||
if (!this.popup) {
|
if (!this.popup) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.classList.add('overflow-menu-popup');
|
div.classList.add('overflow-menu-popup');
|
||||||
@@ -183,7 +183,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
|||||||
this.append(this.button);
|
this.append(this.button);
|
||||||
this.append(this.popup);
|
this.append(this.popup);
|
||||||
this.updateButtonActivationState();
|
this.updateButtonActivationState();
|
||||||
});
|
}, 100);
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// for horizontal menus where fomantic boldens active items, prevent this bold text from
|
// for horizontal menus where fomantic boldens active items, prevent this bold text from
|
||||||
|
|||||||
Reference in New Issue
Block a user