From 49ea7ed4a57bfec4786816ed3e58fd73166a8906 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 24 Jan 2025 12:01:25 +0100 Subject: [PATCH] feat(forms): add a debounce for Input and Textarea fields fix #2415 --- umap/static/umap/js/modules/form/fields.js | 18 ++++++++++++------ umap/static/umap/js/modules/utils.js | 13 +++++++++++++ umap/tests/integration/test_import.py | 1 + .../tests/integration/test_optimistic_merge.py | 1 + umap/tests/integration/test_save.py | 1 + umap/tests/integration/test_tableeditor.py | 1 + umap/tests/integration/test_websocket_sync.py | 2 ++ 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js index 1ff8b5b0..082ba2d1 100644 --- a/umap/static/umap/js/modules/form/fields.js +++ b/umap/static/umap/js/modules/form/fields.js @@ -132,7 +132,10 @@ Fields.Textarea = class extends BaseElement { super.build() this.textarea = this.elements.textarea this.fetch() - this.textarea.addEventListener('input', () => this.sync()) + this.textarea.addEventListener( + 'input', + Utils.debounce(() => this.sync(), 300) + ) this.textarea.addEventListener('keypress', (event) => this.onKeyPress(event)) } @@ -179,7 +182,7 @@ Fields.Input = class extends BaseElement { this.input.step = this.properties.step } this.fetch() - this.input.addEventListener(this.getSyncEvent(), () => this.sync()) + this.listenForSync() this.input.addEventListener('keydown', (event) => this.onKeyDown(event)) } @@ -189,8 +192,11 @@ Fields.Input = class extends BaseElement { this.input.value = value } - getSyncEvent() { - return 'input' + listenForSync() { + this.input.addEventListener( + 'input', + Utils.debounce(() => this.sync(), 300) + ) } type() { @@ -212,8 +218,8 @@ Fields.Input = class extends BaseElement { } Fields.BlurInput = class extends Fields.Input { - getSyncEvent() { - return 'blur' + listenForSync() { + this.input.addEventListener('blur', () => this.sync()) } getTemplate() { diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index bf4b3e3c..3f9fdf0e 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -471,6 +471,19 @@ export function isWritable(element) { return false } +// From https://www.joshwcomeau.com/snippets/javascript/debounce/ +export const debounce = (callback, wait) => { + let timeoutId = null + + return (...args) => { + window.clearTimeout(timeoutId) + + timeoutId = window.setTimeout(() => { + callback.apply(null, args) + }, wait) + } +} + export const COLORS = [ 'Black', 'Navy', diff --git a/umap/tests/integration/test_import.py b/umap/tests/integration/test_import.py index 9cd42646..f51d0967 100644 --- a/umap/tests/integration/test_import.py +++ b/umap/tests/integration/test_import.py @@ -24,6 +24,7 @@ def test_layers_list_is_updated(live_server, tilelayer, page): page.get_by_role("button", name="Add a layer").click() page.locator('input[name="name"]').click() page.locator('input[name="name"]').fill("foobar") + page.wait_for_timeout(300) # Time for the input debounce. page.get_by_role("link", name=f"Import data ({modifier}+I)").click() # Should still work page.locator("[name=layer-id]").select_option(label="Import in a new layer") diff --git a/umap/tests/integration/test_optimistic_merge.py b/umap/tests/integration/test_optimistic_merge.py index 3c718ec1..997b71fa 100644 --- a/umap/tests/integration/test_optimistic_merge.py +++ b/umap/tests/integration/test_optimistic_merge.py @@ -285,6 +285,7 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm # Change name on page one and save page_one.locator(".leaflet-marker-icon").click(modifiers=["Shift"]) page_one.locator('input[name="name"]').fill("name from page one") + page_one.wait_for_timeout(300) # Time for the input debounce. with page_one.expect_response(re.compile(r".*/datalayer/update/.*")): page_one.get_by_role("button", name="Save").click() diff --git a/umap/tests/integration/test_save.py b/umap/tests/integration/test_save.py index b1366bd9..2ffdaa0e 100644 --- a/umap/tests/integration/test_save.py +++ b/umap/tests/integration/test_save.py @@ -24,6 +24,7 @@ def test_reseting_map_would_remove_from_save_queue( page.get_by_role("button", name="Edit", exact=True).click() page.locator('input[name="name"]').click() page.locator('input[name="name"]').fill("new datalayer name") + page.wait_for_timeout(300) # Time of the Input debounce with page.expect_response(re.compile(".*/datalayer/update/.*")): page.get_by_role("button", name="Save").click() assert len(requests) == 1 diff --git a/umap/tests/integration/test_tableeditor.py b/umap/tests/integration/test_tableeditor.py index 7ce41722..003a7504 100644 --- a/umap/tests/integration/test_tableeditor.py +++ b/umap/tests/integration/test_tableeditor.py @@ -74,6 +74,7 @@ def test_table_editor(live_server, openmap, datalayer, page): page.locator("dialog").get_by_role("button", name="OK").click() page.locator("td").nth(2).dblclick() page.locator('input[name="newprop"]').fill("newvalue") + page.wait_for_timeout(300) # Time for the input debounce. page.keyboard.press("Enter") page.locator("thead button[data-property=name]").click() page.get_by_role("button", name="Delete this column").click() diff --git a/umap/tests/integration/test_websocket_sync.py b/umap/tests/integration/test_websocket_sync.py index 96946ce8..9e0b22f3 100644 --- a/umap/tests/integration/test_websocket_sync.py +++ b/umap/tests/integration/test_websocket_sync.py @@ -44,6 +44,7 @@ def test_websocket_connection_can_sync_markers( expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden() peerA.locator("body").type("Synced name") peerA.locator("body").press("Escape") + peerA.wait_for_timeout(300) peerB.locator(".leaflet-marker-icon").first.click() peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click() @@ -310,6 +311,7 @@ def test_websocket_connection_can_sync_late_joining_peer( a_map_el.click(position={"x": 220, "y": 220}) peerA.locator("body").type("First marker") peerA.locator("body").press("Escape") + peerA.wait_for_timeout(300) # Add a polygon from peer A create_polygon = peerA.locator(".leaflet-control-toolbar ").get_by_title(