Skip to content

Commit ecd43bc

Browse files
committed
Fix form fields with their own canvas updated on non-rendered pages
When a read-only field (which has its own canvas) is updated by the sandbox while its page isn't rendered, showElementAndHideCanvas isn't called, so once the page is finally rendered the field still shows its outdated canvas instead of the new value. Replace the imperative canvas/element toggling with a `sandboxModified` class, set from the annotation storage both at render time and on sandbox updates, and let the CSS show the element and hide the canvas.
1 parent 52a44a6 commit ecd43bc

5 files changed

Lines changed: 119 additions & 17 deletions

File tree

src/display/annotation_layer.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,15 +1321,6 @@ class WidgetAnnotationElement extends AnnotationElement {
13211321
return this.container;
13221322
}
13231323

1324-
showElementAndHideCanvas(element) {
1325-
if (this.data.hasOwnCanvas) {
1326-
if (element.previousSibling?.nodeName === "CANVAS") {
1327-
element.previousSibling.hidden = true;
1328-
}
1329-
element.hidden = false;
1330-
}
1331-
}
1332-
13331324
_getKeyModifier(event) {
13341325
return FeatureTest.platform.isMac ? event.metaKey : event.ctrlKey;
13351326
}
@@ -1547,7 +1538,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
15471538
}
15481539
}
15491540
if (this.data.hasOwnCanvas) {
1550-
element.hidden = true;
1541+
// The rendered appearance (a canvas) is shown instead of this element.
1542+
this.container.classList.add("hasOwnCanvas");
1543+
if (storage.has(id)) {
1544+
// Once the field is modified, the `sandboxModified` class hides the
1545+
// (now outdated) canvas and shows this element instead.
1546+
// The field can already have been modified.
1547+
this.container.classList.add("sandboxModified");
1548+
}
15511549
}
15521550
GetElementsByNameSet.add(element);
15531551
this.contentElement = element;
@@ -1637,7 +1635,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
16371635
});
16381636

16391637
element.addEventListener("updatefromsandbox", jsEvent => {
1640-
this.showElementAndHideCanvas(jsEvent.target);
1638+
this.container.classList.add("sandboxModified");
16411639
const actions = {
16421640
value(event) {
16431641
elementData.userValue = event.detail.value ?? "";

test/integration/scripting_spec.mjs

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,31 +1886,38 @@ describe("Interaction", () => {
18861886
const selector = getAnnotationSelector("9R");
18871887
const hasVisibleCanvas = await page.$eval(
18881888
`${selector} > canvas`,
1889-
elem => elem && !elem.hasAttribute("hidden")
1889+
elem => getComputedStyle(elem).display !== "none"
18901890
);
18911891
expect(hasVisibleCanvas)
18921892
.withContext(`In ${browserName}`)
18931893
.toEqual(true);
18941894

1895-
const hasHiddenInput = await page.$eval(`${selector} > input`, elem =>
1896-
elem?.hasAttribute("hidden")
1895+
const hasHiddenInput = await page.$eval(
1896+
`${selector} > input`,
1897+
elem => getComputedStyle(elem).display === "none"
18971898
);
18981899
expect(hasHiddenInput).withContext(`In ${browserName}`).toEqual(true);
18991900

19001901
await page.click(getSelector("12R"));
1901-
await page.waitForSelector(`${selector} > canvas[hidden]`);
1902+
await page.waitForFunction(
1903+
sel =>
1904+
getComputedStyle(document.querySelector(`${sel} > canvas`))
1905+
.display === "none",
1906+
{},
1907+
selector
1908+
);
19021909

19031910
const hasHiddenCanvas = await page.$eval(
19041911
`${selector} > canvas`,
1905-
elem => elem?.hasAttribute("hidden")
1912+
elem => getComputedStyle(elem).display === "none"
19061913
);
19071914
expect(hasHiddenCanvas)
19081915
.withContext(`In ${browserName}`)
19091916
.toEqual(true);
19101917

19111918
const hasVisibleInput = await page.$eval(
19121919
`${selector} > input`,
1913-
elem => elem && !elem.hasAttribute("hidden")
1920+
elem => getComputedStyle(elem).display !== "none"
19141921
);
19151922
expect(hasVisibleInput)
19161923
.withContext(`In ${browserName}`)
@@ -2696,4 +2703,89 @@ describe("Interaction", () => {
26962703
);
26972704
});
26982705
});
2706+
2707+
describe("in text_field_own_canvas_calc.pdf", () => {
2708+
let pages;
2709+
2710+
beforeEach(async () => {
2711+
pages = await loadAndWait(
2712+
"text_field_own_canvas_calc.pdf",
2713+
getSelector("7R"),
2714+
"page-fit"
2715+
);
2716+
});
2717+
2718+
afterEach(async () => {
2719+
await closePages(pages);
2720+
});
2721+
2722+
it("must show the field instead of its canvas when it was calculated while its page wasn't rendered", async () => {
2723+
await Promise.all(
2724+
pages.map(async ([browserName, page]) => {
2725+
// The read-only "Mirror" field (8R) is on page 3, which hasn't been
2726+
// rendered yet.
2727+
expect(await page.$(getSelector("8R")))
2728+
.withContext(`In ${browserName}`)
2729+
.toBeNull();
2730+
2731+
// Modifying the "Source" field (7R) on page 1 mirrors its value into
2732+
// the read-only field on page 3 through a Calculate action.
2733+
await page.type(getSelector("7R"), "Hello PDF.js");
2734+
await page.keyboard.press("Enter");
2735+
await waitForEntryInStorage(
2736+
page,
2737+
"8R",
2738+
{ value: "Hello PDF.js" },
2739+
(stored, expected) =>
2740+
!!stored &&
2741+
JSON.parse(stored).value === JSON.parse(expected).value
2742+
);
2743+
2744+
// The value has been mirrored into the storage while page 3, and
2745+
// hence its annotation layer, hasn't been rendered yet.
2746+
const page3AnnotationCount = await page.evaluate(() => {
2747+
const layer = document.querySelector(
2748+
'.page[data-page-number="3"] .annotationLayer'
2749+
);
2750+
return layer ? layer.childElementCount : 0;
2751+
});
2752+
expect(page3AnnotationCount)
2753+
.withContext(`In ${browserName}`)
2754+
.toEqual(0);
2755+
2756+
// Render page 3.
2757+
await scrollIntoView(page, '.page[data-page-number="3"]');
2758+
const inputPage3Selector = getSelector("8R");
2759+
await page.waitForSelector(
2760+
`.sandboxModified:has(${inputPage3Selector})`,
2761+
{ visible: true }
2762+
);
2763+
2764+
// The field must show its (calculated) value and the now-outdated
2765+
// canvas must be hidden.
2766+
const { value, isFieldVisible, isCanvasHidden } = await page.evaluate(
2767+
sel => {
2768+
const input = document.querySelector(sel);
2769+
const canvas = input
2770+
.closest("section")
2771+
.querySelector("canvas.annotationContent");
2772+
return {
2773+
value: input.value,
2774+
isFieldVisible: getComputedStyle(input).display !== "none",
2775+
isCanvasHidden:
2776+
!!canvas && getComputedStyle(canvas).display === "none",
2777+
};
2778+
},
2779+
inputPage3Selector
2780+
);
2781+
2782+
expect(value)
2783+
.withContext(`In ${browserName}`)
2784+
.toEqual("Hello PDF.js");
2785+
expect(isFieldVisible).withContext(`In ${browserName}`).toBe(true);
2786+
expect(isCanvasHidden).withContext(`In ${browserName}`).toBe(true);
2787+
})
2788+
);
2789+
});
2790+
});
26992791
});

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,3 +931,4 @@
931931
!issue21346.pdf
932932
!cidfont_cmap_overflow.pdf
933933
!jbig2_file_header.pdf
934+
!text_field_own_canvas_calc.pdf
1.75 KB
Binary file not shown.

web/annotation_layer_builder.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@
138138
}
139139
}
140140

141+
/* A field rendered to its own canvas shows that canvas and keeps its editable
142+
element hidden. Once the field is modified the canvas is outdated, so it's
143+
hidden and the element is shown instead. */
144+
.hasOwnCanvas:not(.sandboxModified) :is(input, textarea) {
145+
display: none;
146+
}
147+
148+
.hasOwnCanvas.sandboxModified canvas.annotationContent {
149+
display: none;
150+
}
151+
141152
.textLayer.selecting ~ & section {
142153
pointer-events: none;
143154
}

0 commit comments

Comments
 (0)