From d03494eeff496547bfd9001c3ccacd7a89ecae6f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 5 Sep 2023 14:15:01 +0200 Subject: [PATCH] Only call the focus/blur callbacks when it's necessary (bug 1851517) Focus callback must be called only when the element has been blurred. For example, blur callback (which implies some potential validation) is not called because the newly focused element is an other tab, an alert dialog, ... so consequently the focus callback mustn't be called when the element gets its focus back. --- src/display/annotation_layer.js | 51 +++++++++++++++++++++++++++--- test/integration/scripting_spec.js | 46 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index cba633a0e..fd9c67023 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1013,7 +1013,7 @@ class WidgetAnnotationElement extends AnnotationElement { return (isWin && event.ctrlKey) || (isMac && event.metaKey); } - _setEventListener(element, baseName, eventName, valueGetter) { + _setEventListener(element, elementData, baseName, eventName, valueGetter) { if (baseName.includes("mouse")) { // Mouse events element.addEventListener(baseName, event => { @@ -1029,8 +1029,24 @@ class WidgetAnnotationElement extends AnnotationElement { }); }); } else { - // Non mouse event + // Non-mouse events element.addEventListener(baseName, event => { + if (baseName === "blur") { + if (!elementData.focused || !event.relatedTarget) { + return; + } + elementData.focused = false; + } else if (baseName === "focus") { + if (elementData.focused) { + return; + } + elementData.focused = true; + } + + if (!valueGetter) { + return; + } + this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { @@ -1043,10 +1059,25 @@ class WidgetAnnotationElement extends AnnotationElement { } } - _setEventListeners(element, names, getter) { + _setEventListeners(element, elementData, names, getter) { for (const [baseName, eventName] of names) { if (eventName === "Action" || this.data.actions?.[eventName]) { - this._setEventListener(element, baseName, eventName, getter); + if (eventName === "Focus" || eventName === "Blur") { + elementData ||= { focused: false }; + } + this._setEventListener( + element, + elementData, + baseName, + eventName, + getter + ); + if (eventName === "Focus" && !this.data.actions?.Blur) { + // Ensure that elementData will have the correct value. + this._setEventListener(element, elementData, "blur", "Blur", null); + } else if (eventName === "Blur" && !this.data.actions?.Focus) { + this._setEventListener(element, elementData, "focus", "Focus", null); + } } } } @@ -1178,6 +1209,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { formattedValue: fieldFormattedValues, lastCommittedValue: null, commitKey: 1, + focused: false, }; if (this.data.multiLine) { @@ -1238,12 +1270,16 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (this.enableScripting && this.hasJSActions) { element.addEventListener("focus", event => { + if (elementData.focused) { + return; + } const { target } = event; if (elementData.userValue) { target.value = elementData.userValue; } elementData.lastCommittedValue = target.value; elementData.commitKey = 1; + elementData.focused = true; }); element.addEventListener("updatefromsandbox", jsEvent => { @@ -1349,9 +1385,10 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { const _blurListener = blurListener; blurListener = null; element.addEventListener("blur", event => { - if (!event.relatedTarget) { + if (!elementData.focused || !event.relatedTarget) { return; } + elementData.focused = false; const { value } = event.target; elementData.userValue = value; if (elementData.lastCommittedValue !== value) { @@ -1431,6 +1468,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { this._setEventListeners( element, + elementData, [ ["focus", "Focus"], ["blur", "Blur"], @@ -1540,6 +1578,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { this._setEventListeners( element, + null, [ ["change", "Validate"], ["change", "Action"], @@ -1630,6 +1669,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { this._setEventListeners( element, + null, [ ["change", "Validate"], ["change", "Action"], @@ -1894,6 +1934,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { this._setEventListeners( selectElement, + null, [ ["focus", "Focus"], ["blur", "Blur"], diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index cfaef62e2..7680ec9de 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -2154,4 +2154,50 @@ describe("Interaction", () => { ); }); }); + + describe("Textfields and focus", () => { + let pages; + let otherPages; + + beforeAll(async () => { + otherPages = await Promise.all( + global.integrationSessions.map(async session => + session.browser.newPage() + ) + ); + pages = await loadAndWait("evaljs.pdf", getSelector("55R")); + }); + + afterAll(async () => { + await closePages(pages); + await Promise.all(otherPages.map(page => page.close())); + }); + + it("must check that focus/blur callbacks aren't called", async () => { + await Promise.all( + pages.map(async ([browserName, page], i) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.click(getSelector("55R")); + await page.type(getSelector("55R"), "Hello", { delay: 10 }); + await page.click(getSelector("56R")); + await page.waitForTimeout(10); + + await page.click(getSelector("55R")); + await page.type(getSelector("55R"), " World", { delay: 10 }); + await page.waitForTimeout(10); + + await otherPages[i].bringToFront(); + await otherPages[i].waitForTimeout(100); + await page.bringToFront(); + await page.waitForTimeout(100); + + const text = await page.$eval(getSelector("55R"), el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("Hello World"); + }) + ); + }); + }); });