From 430b744bc4eb226657533e85a00f76ddaa16e25b Mon Sep 17 00:00:00 2001 From: Corey Ogburn Date: Thu, 9 Jan 2025 14:40:19 -0700 Subject: [PATCH 1/2] Update Selection When clicking on a config value, we hide the text-area and invisibly swap out to a different text-area. It's almost invisible except that the focus isn't maintained: the user focused the old element but not the new one. Simply adding a call to .focus() puts the cursor in a different place than where the user clicked. This isn't how text controls work and can break a user's flow. The selection cursor isn't updated to the click location in the focus event handler so it was refactored to be a click handler. In the edit event that fires, the scroll position and selection range are taken from the old input and on next tick applied to the new input. This makes the control swapping truly invisible. You still have to focus the text-area before the resize handle will be available though. --- html/index.html | 4 ++-- html/js/routes/config.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/html/index.html b/html/index.html index 36e09d19..a8f891dd 100644 --- a/html/index.html +++ b/html/index.html @@ -5581,11 +5581,11 @@

{{ i18n.settingsCustomized }} {{ settingsCustomized }}
- +
- +
diff --git a/html/js/routes/config.js b/html/js/routes/config.js index 42638d49..0bd29204 100644 --- a/html/js/routes/config.js +++ b/html/js/routes/config.js @@ -523,6 +523,21 @@ routes.push({ this.form.value = setting.value; this.$root.drawAttention('#setting-global-save'); } + + // transfer caret position from non-edit element to edit element + const before = document.getElementById('value-output'); + const scrollTop = before?.scrollTop || 0; + const selStart = before?.selectionStart || 0; + const selEnd = before?.selectionEnd || 0; + this.$nextTick(() => { + const after = document.getElementById('value-input'); + if (after && typeof scrollTop !== 'undefined' && + typeof selStart !== 'undefined' && typeof selEnd !== 'undefined') { + after.focus(); + after.scrollTop = scrollTop; + after.setSelectionRange(selStart, selEnd); + } + }); }, addNode(setting, nodeId) { if (this.cancel() && setting && nodeId) { From 6d125d9ace7917bb965d029d9fc5ff76e9332eab Mon Sep 17 00:00:00 2001 From: Corey Ogburn Date: Thu, 9 Jan 2025 15:41:39 -0700 Subject: [PATCH 2/2] Use @focus over @click Tabbing to the control wasn't giving it focus. Reverted the event back to focus and then wrapped the event body in a `setTimeout([event body], 1)` so it executes after the click event sets the selection range. Now it works for node specific values as well by adapting the selector to the nodeId parameter. Tabbing to the control puts the cursor at the end. Clicking on the control puts the cursor where the user clicks. Highlighting text in the control highlights the same text. Methodology used: We setTimeout(..., 1) to let the click event finish so the cursor location is accurate. After that, we change our form variables normally. Then we collect scroll and caret information. We wait one tick for the form changes we just made to propogate through the UI and then we set the scroll and caret information on the new text-area. --- html/index.html | 4 ++-- html/js/routes/config.js | 30 +++++++++++++++++------------- html/js/routes/config.test.js | 6 +++++- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/html/index.html b/html/index.html index a8f891dd..36e09d19 100644 --- a/html/index.html +++ b/html/index.html @@ -5581,11 +5581,11 @@

{{ i18n.settingsCustomized }} {{ settingsCustomized }}
- +
- +
diff --git a/html/js/routes/config.js b/html/js/routes/config.js index 0bd29204..f60a3478 100644 --- a/html/js/routes/config.js +++ b/html/js/routes/config.js @@ -512,6 +512,7 @@ routes.push({ this.$root.stopLoading(); }, edit(setting, nodeId) { + setTimeout(() => { if (nodeId) { if ((this.form.key == nodeId) || !this.cancel()) return; this.form.key = nodeId; @@ -524,20 +525,23 @@ routes.push({ this.$root.drawAttention('#setting-global-save'); } - // transfer caret position from non-edit element to edit element - const before = document.getElementById('value-output'); - const scrollTop = before?.scrollTop || 0; - const selStart = before?.selectionStart || 0; - const selEnd = before?.selectionEnd || 0; - this.$nextTick(() => { - const after = document.getElementById('value-input'); - if (after && typeof scrollTop !== 'undefined' && + // transfer caret position from non-edit element to edit element + let selector = nodeId ? 'node-value-output-' + nodeId : 'value-output'; + const before = document.getElementById(selector); + const scrollTop = before?.scrollTop || 0; + const selStart = before?.selectionStart || 0; + const selEnd = before?.selectionEnd || 0; + this.$nextTick(() => { + selector = nodeId ? 'node-value-input-' + nodeId : 'value-input'; + const after = document.getElementById(selector); + if (after && typeof scrollTop !== 'undefined' && typeof selStart !== 'undefined' && typeof selEnd !== 'undefined') { - after.focus(); - after.scrollTop = scrollTop; - after.setSelectionRange(selStart, selEnd); - } - }); + after.focus(); + after.scrollTop = scrollTop; + after.setSelectionRange(selStart, selEnd); + } + }); + }, 1); }, addNode(setting, nodeId) { if (this.cancel() && setting && nodeId) { diff --git a/html/js/routes/config.test.js b/html/js/routes/config.test.js index 7ef85afc..6f3563a7 100644 --- a/html/js/routes/config.test.js +++ b/html/js/routes/config.test.js @@ -508,13 +508,14 @@ test('saveRegexValidMultiline', async () => { expect(mock).toHaveBeenCalledWith('config/', {"id": "test.id", "nodeId": null, "value": "123\n456"}); }); -test('edit', () => { +test('edit', async () => { // Global edit, nothing pending setupSettings(); comp.cancelDialog = false; comp.form.value = null; comp.form.key = null; comp.edit(comp.settings[0], null); + await new Promise(resolve => setTimeout(resolve, 2)); expect(comp.form.key).toBe("s-id"); expect(comp.form.value).toBe("orig-value"); expect(comp.cancelDialog).toBe(false); @@ -526,6 +527,7 @@ test('edit', () => { comp.form.key = "s-id2"; comp.activeBackup = ["s-id2"]; comp.edit(comp.settings[0], null); + await new Promise(resolve => setTimeout(resolve, 2)); expect(comp.form.key).toBe("s-id2"); expect(comp.form.value).toBe("touched-value"); expect(comp.cancelDialog).toBe(true); @@ -535,6 +537,7 @@ test('edit', () => { comp.form.value = null; comp.form.key = null; comp.edit(comp.settings[0], "n1"); + await new Promise(resolve => setTimeout(resolve, 2)); expect(comp.form.key).toBe("n1"); expect(comp.form.value).toBe("123"); expect(comp.cancelDialog).toBe(false); @@ -544,6 +547,7 @@ test('edit', () => { comp.form.value = "touched-value"; comp.form.key = "n2"; comp.edit(comp.settings[0], "n1"); + await new Promise(resolve => setTimeout(resolve, 2)); expect(comp.form.key).toBe("n2"); expect(comp.form.value).toBe("touched-value"); expect(comp.cancelDialog).toBe(true);