diff --git a/tests/test_dom_patchers.mjs b/tests/test_dom_patchers.mjs index d5e2de7..c9a5538 100644 --- a/tests/test_dom_patchers.mjs +++ b/tests/test_dom_patchers.mjs @@ -9,6 +9,7 @@ import { setTextIfChanged, syncElementAttributes, } from '../web/resolver/utils/dom_patch_utils.js'; +import * as domPatchUtils from '../web/resolver/utils/dom_patch_utils.js'; const projectRoot = path.resolve(import.meta.dirname, '..'); const resolveDownloadMethodsSource = fs.readFileSync( @@ -92,6 +93,29 @@ test('shared DOM helpers synchronize attributes, text, and one-time listeners', assert.equal(clicks, 1); }); +test('instant actions handle pointer and click as one idempotent action', () => { + assert.equal(typeof domPatchUtils.bindInstantAction, 'function'); + + const window = new Window(); + const button = window.document.createElement('button'); + let calls = 0; + const events = []; + + assert.equal(domPatchUtils.bindInstantAction(button, event => { + calls += 1; + events.push(event.type); + }), true); + assert.equal(domPatchUtils.bindInstantAction(button, () => { + calls += 100; + }), false); + + button.dispatchEvent(new window.Event('pointerdown', { bubbles: true, cancelable: true })); + button.dispatchEvent(new window.Event('click', { bubbles: true, cancelable: true })); + + assert.equal(calls, 1); + assert.deepEqual(events, ['pointerdown']); +}); + function createDialog(window) { return { syncSearchElementAttributes, diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index b7e137a..29cf94f 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -2,7 +2,7 @@ import { app } from "../../../../../scripts/app.js"; import { $el } from "../../../../../scripts/ui.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; import { startSplitterDrag } from "../utils/splitter_drag.js"; -import { syncElementAttributes } from "../utils/dom_patch_utils.js"; +import { bindInstantAction, syncElementAttributes } from "../utils/dom_patch_utils.js"; const FOOTER_VERSION_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; const FOOTER_VERSION_FAILURE_REFRESH_INTERVAL_MS = 5 * 60 * 1000; @@ -1115,26 +1115,6 @@ export const queueMethods = { }, wireDownloadsPanelControls() { - const bindInstantAction = (button, handler) => { - if (!button || button._hasListener) return; - button._hasListener = true; - const run = (event) => { - event?.preventDefault?.(); - event?.stopPropagation?.(); - if (button.disabled) return; - if (event?.type === 'click' && button._handledPointerAction) { - button._handledPointerAction = false; - return; - } - if (event?.type === 'pointerdown') { - button._handledPointerAction = true; - } - handler(); - }; - button.addEventListener('pointerdown', run); - button.addEventListener('click', run); - }; - const getDownloadContext = (button) => { const downloadId = button?.dataset?.downloadId || ''; const info = this.activeDownloads?.[downloadId]; diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index d93b474..36cb9b9 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -4,7 +4,7 @@ import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js" import { getCivitaiModelUrl } from "../globals.js"; import { normalizeSha256 } from "../utils/hash_utils.js"; import { normalizePathIdentity } from "../utils/html_utils.js"; -import { bindEventOnce, syncElementAttributes } from "../utils/dom_patch_utils.js"; +import { bindEventOnce, bindInstantAction, syncElementAttributes } from "../utils/dom_patch_utils.js"; const log = createModuleLogger('resolve_download_methods'); export const resolveDownloadMethods = { @@ -1108,25 +1108,6 @@ export const resolveDownloadMethods = { attachDownloadActionHandlers(progressDiv, downloadId) { if (!progressDiv) return; - const bindInstantAction = (button, handler) => { - if (!button || button._hasListener) return; - button._hasListener = true; - const run = (event) => { - event?.preventDefault?.(); - event?.stopPropagation?.(); - if (button.disabled) return; - if (event?.type === 'click' && button._handledPointerAction) { - button._handledPointerAction = false; - return; - } - if (event?.type === 'pointerdown') { - button._handledPointerAction = true; - } - handler(event); - }; - button.addEventListener('pointerdown', run); - button.addEventListener('click', run); - }; progressDiv.querySelectorAll('.cancel-download-btn, .cancel-download-btn-pending, .mr-download-queue-cancel').forEach(cancelBtn => { bindInstantAction(cancelBtn, () => { const targetDownloadId = cancelBtn.dataset.downloadId || downloadId; diff --git a/web/resolver/utils/dom_patch_utils.js b/web/resolver/utils/dom_patch_utils.js index 3c5a348..e12048c 100644 --- a/web/resolver/utils/dom_patch_utils.js +++ b/web/resolver/utils/dom_patch_utils.js @@ -42,3 +42,24 @@ export function bindEventOnce(element, eventName, handler, key = eventName) { element.addEventListener(eventName, handler); return true; } + +export function bindInstantAction(button, handler) { + if (!button || button._hasListener) return false; + button._hasListener = true; + const run = (event) => { + event?.preventDefault?.(); + event?.stopPropagation?.(); + if (button.disabled) return; + if (event?.type === 'click' && button._handledPointerAction) { + button._handledPointerAction = false; + return; + } + if (event?.type === 'pointerdown') { + button._handledPointerAction = true; + } + handler(event); + }; + button.addEventListener('pointerdown', run); + button.addEventListener('click', run); + return true; +}