diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index bb96688d9..144a28b0a 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1321,15 +1321,6 @@ class WidgetAnnotationElement extends AnnotationElement { return this.container; } - showElementAndHideCanvas(element) { - if (this.data.hasOwnCanvas) { - if (element.previousSibling?.nodeName === "CANVAS") { - element.previousSibling.hidden = true; - } - element.hidden = false; - } - } - _getKeyModifier(event) { return FeatureTest.platform.isMac ? event.metaKey : event.ctrlKey; } @@ -1547,7 +1538,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { } } if (this.data.hasOwnCanvas) { - element.hidden = true; + // The rendered appearance (a canvas) is shown instead of this element. + this.container.classList.add("hasOwnCanvas"); + if (storage.has(id)) { + // Once the field is modified, the `sandboxModified` class hides the + // (now outdated) canvas and shows this element instead. + // The field can already have been modified. + this.container.classList.add("sandboxModified"); + } } GetElementsByNameSet.add(element); this.contentElement = element; @@ -1637,7 +1635,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { }); element.addEventListener("updatefromsandbox", jsEvent => { - this.showElementAndHideCanvas(jsEvent.target); + this.container.classList.add("sandboxModified"); const actions = { value(event) { elementData.userValue = event.detail.value ?? ""; diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index 5314cf687..82d38af8e 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -1886,23 +1886,30 @@ describe("Interaction", () => { const selector = getAnnotationSelector("9R"); const hasVisibleCanvas = await page.$eval( `${selector} > canvas`, - elem => elem && !elem.hasAttribute("hidden") + elem => getComputedStyle(elem).display !== "none" ); expect(hasVisibleCanvas) .withContext(`In ${browserName}`) .toEqual(true); - const hasHiddenInput = await page.$eval(`${selector} > input`, elem => - elem?.hasAttribute("hidden") + const hasHiddenInput = await page.$eval( + `${selector} > input`, + elem => getComputedStyle(elem).display === "none" ); expect(hasHiddenInput).withContext(`In ${browserName}`).toEqual(true); await page.click(getSelector("12R")); - await page.waitForSelector(`${selector} > canvas[hidden]`); + await page.waitForFunction( + sel => + getComputedStyle(document.querySelector(`${sel} > canvas`)) + .display === "none", + {}, + selector + ); const hasHiddenCanvas = await page.$eval( `${selector} > canvas`, - elem => elem?.hasAttribute("hidden") + elem => getComputedStyle(elem).display === "none" ); expect(hasHiddenCanvas) .withContext(`In ${browserName}`) @@ -1910,7 +1917,7 @@ describe("Interaction", () => { const hasVisibleInput = await page.$eval( `${selector} > input`, - elem => elem && !elem.hasAttribute("hidden") + elem => getComputedStyle(elem).display !== "none" ); expect(hasVisibleInput) .withContext(`In ${browserName}`) @@ -2696,4 +2703,89 @@ describe("Interaction", () => { ); }); }); + + describe("in text_field_own_canvas_calc.pdf", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "text_field_own_canvas_calc.pdf", + getSelector("7R"), + "page-fit" + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must show the field instead of its canvas when it was calculated while its page wasn't rendered", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // The read-only "Mirror" field (8R) is on page 3, which hasn't been + // rendered yet. + expect(await page.$(getSelector("8R"))) + .withContext(`In ${browserName}`) + .toBeNull(); + + // Modifying the "Source" field (7R) on page 1 mirrors its value into + // the read-only field on page 3 through a Calculate action. + await page.type(getSelector("7R"), "Hello PDF.js"); + await page.keyboard.press("Enter"); + await waitForEntryInStorage( + page, + "8R", + { value: "Hello PDF.js" }, + (stored, expected) => + !!stored && + JSON.parse(stored).value === JSON.parse(expected).value + ); + + // The value has been mirrored into the storage while page 3, and + // hence its annotation layer, hasn't been rendered yet. + const page3AnnotationCount = await page.evaluate(() => { + const layer = document.querySelector( + '.page[data-page-number="3"] .annotationLayer' + ); + return layer ? layer.childElementCount : 0; + }); + expect(page3AnnotationCount) + .withContext(`In ${browserName}`) + .toEqual(0); + + // Render page 3. + await scrollIntoView(page, '.page[data-page-number="3"]'); + const inputPage3Selector = getSelector("8R"); + await page.waitForSelector( + `.sandboxModified:has(${inputPage3Selector})`, + { visible: true } + ); + + // The field must show its (calculated) value and the now-outdated + // canvas must be hidden. + const { value, isFieldVisible, isCanvasHidden } = await page.evaluate( + sel => { + const input = document.querySelector(sel); + const canvas = input + .closest("section") + .querySelector("canvas.annotationContent"); + return { + value: input.value, + isFieldVisible: getComputedStyle(input).display !== "none", + isCanvasHidden: + !!canvas && getComputedStyle(canvas).display === "none", + }; + }, + inputPage3Selector + ); + + expect(value) + .withContext(`In ${browserName}`) + .toEqual("Hello PDF.js"); + expect(isFieldVisible).withContext(`In ${browserName}`).toBe(true); + expect(isCanvasHidden).withContext(`In ${browserName}`).toBe(true); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 8bb7acc70..4d2c9f005 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -931,3 +931,4 @@ !issue21346.pdf !cidfont_cmap_overflow.pdf !jbig2_file_header.pdf +!text_field_own_canvas_calc.pdf diff --git a/test/pdfs/text_field_own_canvas_calc.pdf b/test/pdfs/text_field_own_canvas_calc.pdf new file mode 100644 index 000000000..66e723971 --- /dev/null +++ b/test/pdfs/text_field_own_canvas_calc.pdf @@ -0,0 +1,64 @@ +%PDF-1.7 +%ÿÿÿÿ +1 0 obj +<< /Type /Catalog /Pages 2 0 R /AcroForm 3 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [4 0 R 5 0 R 6 0 R] /Count 3 >> +endobj +3 0 obj +<< /Fields [7 0 R 8 0 R] /CO [8 0 R] /DR << /Font << /Helv 9 0 R >> >> /DA (/Helv 12 Tf 0 g) /NeedAppearances true >> +endobj +4 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> /Annots [7 0 R] >> +endobj +5 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> >> +endobj +6 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> /Annots [8 0 R] >> +endobj +7 0 obj +<< /Type /Annot /Subtype /Widget /FT /Tx /T (Source) /Rect [100 650 300 670] /P 4 0 R /F 4 /DA (/Helv 12 Tf 0 g) /AP << /N 10 0 R >> >> +endobj +8 0 obj +<< /Type /Annot /Subtype /Widget /FT /Tx /T (Mirror) /Ff 1 /Rect [100 650 300 670] /P 6 0 R /F 4 /DA (/Helv 12 Tf 0 g) /AA << /C 11 0 R >> /AP << /N 12 0 R >> >> +endobj +9 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Name /Helv >> +endobj +10 0 obj +<< /Type /XObject /Subtype /Form /BBox [0 0 200 20] /Resources << /Font << /Helv 9 0 R >> >> /Length 50 >> +stream +/Tx BMC q BT /Helv 12 Tf 0 g 2 5 Td () Tj ET Q EMC +endstream +endobj +11 0 obj +<< /Type /Action /S /JavaScript /JS (event.value = this.getField("Source").value;) >> +endobj +12 0 obj +<< /Type /XObject /Subtype /Form /BBox [0 0 200 20] /Resources << /Font << /Helv 9 0 R >> >> /Length 49 >> +stream +/Tx BMC q 0.85 0.85 0.85 rg 0 0 200 20 re f Q EMC +endstream +endobj +xref +0 13 +0000000000 65535 f +0000000015 00000 n +0000000080 00000 n +0000000149 00000 n +0000000282 00000 n +0000000386 00000 n +0000000474 00000 n +0000000578 00000 n +0000000729 00000 n +0000000906 00000 n +0000000988 00000 n +0000001179 00000 n +0000001281 00000 n +trailer +<< /Size 13 /Root 1 0 R >> +startxref +1471 +%%EOF diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index c1c2a8d54..f529108e8 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -138,6 +138,17 @@ } } + /* A field rendered to its own canvas shows that canvas and keeps its editable + element hidden. Once the field is modified the canvas is outdated, so it's + hidden and the element is shown instead. */ + .hasOwnCanvas:not(.sandboxModified) :is(input, textarea) { + display: none; + } + + .hasOwnCanvas.sandboxModified canvas.annotationContent { + display: none; + } + .textLayer.selecting ~ & section { pointer-events: none; }