From af125cd2995c4cac1a80e4720e685d85699d1d3b Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 3 May 2021 18:03:16 +0200 Subject: [PATCH] JS - Add support for display property - in annotation_layer, move common properties treatment in a common method instead having duplicated code in each widget. --- src/core/annotation.js | 33 +++- src/core/document.js | 22 +-- src/display/annotation_layer.js | 233 +++++++++++++---------------- src/scripting_api/proxy.js | 14 +- test/integration/scripting_spec.js | 35 +++++ web/pdf_scripting_manager.js | 1 + 6 files changed, 197 insertions(+), 141 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 38b43e4e5..ea047d8c3 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -439,13 +439,40 @@ class Annotation { ); } - isHidden(annotationStorage) { + /** + * Check if the annotation must be displayed by taking into account + * the value found in the annotationStorage which may have been set + * through JS. + * + * @public + * @memberof Annotation + * @param {AnnotationStorage} [annotationStorage] - Storage for annotation + */ + mustBeViewed(annotationStorage) { const storageEntry = annotationStorage && annotationStorage.get(this.data.id); if (storageEntry && storageEntry.hidden !== undefined) { - return storageEntry.hidden; + return !storageEntry.hidden; } - return this._hasFlag(this.flags, AnnotationFlag.HIDDEN); + return this.viewable && !this._hasFlag(this.flags, AnnotationFlag.HIDDEN); + } + + /** + * Check if the annotation must be printed by taking into account + * the value found in the annotationStorage which may have been set + * through JS. + * + * @public + * @memberof Annotation + * @param {AnnotationStorage} [annotationStorage] - Storage for annotation + */ + mustBePrinted(annotationStorage) { + const storageEntry = + annotationStorage && annotationStorage.get(this.data.id); + if (storageEntry && storageEntry.print !== undefined) { + return storageEntry.print; + } + return this.printable; } /** diff --git a/src/core/document.js b/src/core/document.js index b5f9b596e..79f0ef448 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -68,13 +68,6 @@ import { XRef } from "./xref.js"; const DEFAULT_USER_UNIT = 1.0; const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792]; -function isAnnotationRenderable(annotation, intent) { - return ( - (intent === "display" && annotation.viewable) || - (intent === "print" && annotation.printable) - ); -} - class Page { constructor({ pdfManager, @@ -274,7 +267,7 @@ class Page { return this._parsedAnnotations.then(function (annotations) { const newRefsPromises = []; for (const annotation of annotations) { - if (!isAnnotationRenderable(annotation, "print")) { + if (!annotation.mustBePrinted(annotationStorage)) { continue; } newRefsPromises.push( @@ -377,8 +370,9 @@ class Page { const opListPromises = []; for (const annotation of annotations) { if ( - isAnnotationRenderable(annotation, intent) && - !annotation.isHidden(annotationStorage) + (intent === "display" && + annotation.mustBeViewed(annotationStorage)) || + (intent === "print" && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( annotation @@ -482,7 +476,13 @@ class Page { return this._parsedAnnotations.then(function (annotations) { const annotationsData = []; for (let i = 0, ii = annotations.length; i < ii; i++) { - if (!intent || isAnnotationRenderable(annotations[i], intent)) { + // Get the annotation even if it's hidden because + // JS can change its display. + if ( + !intent || + (intent === "display" && annotations[i].viewable) || + (intent === "print" && annotations[i].printable) + ) { annotationsData.push(annotations[i].data); } } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index b8fdc21b7..7706c850b 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -583,35 +583,81 @@ class WidgetAnnotationElement extends AnnotationElement { } } - _setColor(event) { - const { detail, target } = event; - const { style } = target; - for (const name of [ - "bgColor", - "fillColor", - "fgColor", - "textColor", - "borderColor", - "strokeColor", - ]) { - let color = detail[name]; - if (!color) { - continue; - } - color = ColorConverters[`${color[0]}_HTML`](color.slice(1)); - switch (name) { - case "bgColor": - case "fillColor": - style.backgroundColor = color; - break; - case "fgColor": - case "textColor": - style.color = color; - break; - case "borderColor": - case "strokeColor": - style.borderColor = color; - break; + _dispatchEventFromSandbox(actions, jsEvent) { + const setColor = (jsName, styleName, event) => { + const color = event.detail[jsName]; + event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`]( + color.slice(1) + ); + }; + + const commonActions = { + display: event => { + const hidden = event.detail.display % 2 === 1; + event.target.style.visibility = hidden ? "hidden" : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden, + print: event.detail.display === 0 || event.detail.display === 3, + }); + }, + print: event => { + this.annotationStorage.setValue(this.data.id, { + print: event.detail.print, + }); + }, + hidden: event => { + event.target.style.visibility = event.detail.hidden + ? "hidden" + : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden: event.detail.hidden, + }); + }, + focus: event => { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + userName: event => { + // tooltip + event.target.title = event.detail.userName; + }, + readonly: event => { + if (event.detail.readonly) { + event.target.setAttribute("readonly", ""); + } else { + event.target.removeAttribute("readonly"); + } + }, + required: event => { + if (event.detail.required) { + event.target.setAttribute("required", ""); + } else { + event.target.removeAttribute("required"); + } + }, + bgColor: event => { + setColor("bgColor", "backgroundColor", event); + }, + fillColor: event => { + setColor("fillColor", "backgroundColor", event); + }, + fgColor: event => { + setColor("fgColor", "color", event); + }, + textColor: event => { + setColor("textColor", "color", event); + }, + borderColor: event => { + setColor("borderColor", "borderColor", event); + }, + strokeColor: event => { + setColor("strokeColor", "borderColor", event); + }, + }; + + for (const name of Object.keys(jsEvent.detail)) { + const action = actions[name] || commonActions[name]; + if (action) { + action(jsEvent); } } } @@ -698,18 +744,17 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { } }); - element.addEventListener("updatefromsandbox", event => { - const { detail } = event; + element.addEventListener("updatefromsandbox", jsEvent => { const actions = { - value() { - elementData.userValue = detail.value || ""; + value(event) { + elementData.userValue = event.detail.value || ""; storage.setValue(id, { value: elementData.userValue.toString() }); if (!elementData.formattedValue) { event.target.value = elementData.userValue; } }, - valueAsString() { - elementData.formattedValue = detail.valueAsString || ""; + valueAsString(event) { + elementData.formattedValue = event.detail.valueAsString || ""; if (event.target !== document.activeElement) { // Input hasn't the focus so display formatted string event.target.value = elementData.formattedValue; @@ -718,33 +763,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { formattedValue: elementData.formattedValue, }); }, - focus() { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - userName() { - // tooltip - event.target.title = detail.userName; - }, - hidden() { - event.target.style.visibility = detail.hidden - ? "hidden" - : "visible"; - storage.setValue(id, { hidden: detail.hidden }); - }, - editable() { - event.target.disabled = !detail.editable; - }, - selRange() { - const [selStart, selEnd] = detail.selRange; + selRange(event) { + const [selStart, selEnd] = event.detail.selRange; if (selStart >= 0 && selEnd < event.target.value.length) { event.target.setSelectionRange(selStart, selEnd); } }, }; - Object.keys(detail) - .filter(name => name in actions) - .forEach(name => actions[name]()); - this._setColor(event); + this._dispatchEventFromSandbox(actions, jsEvent); }); // Even if the field hasn't any actions @@ -960,30 +986,14 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { }); if (this.enableScripting && this.hasJSActions) { - element.addEventListener("updatefromsandbox", event => { - const { detail } = event; + element.addEventListener("updatefromsandbox", jsEvent => { const actions = { - value() { - event.target.checked = detail.value !== "Off"; + value(event) { + event.target.checked = event.detail.value !== "Off"; storage.setValue(id, { value: event.target.checked }); }, - focus() { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - hidden() { - event.target.style.visibility = detail.hidden - ? "hidden" - : "visible"; - storage.setValue(id, { hidden: detail.hidden }); - }, - editable() { - event.target.disabled = !detail.editable; - }, }; - Object.keys(detail) - .filter(name => name in actions) - .forEach(name => actions[name]()); - this._setColor(event); + this._dispatchEventFromSandbox(actions, jsEvent); }); this._setEventListeners( @@ -1047,34 +1057,18 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { if (this.enableScripting && this.hasJSActions) { const pdfButtonValue = data.buttonValue; - element.addEventListener("updatefromsandbox", event => { - const { detail } = event; + element.addEventListener("updatefromsandbox", jsEvent => { const actions = { - value() { - const checked = pdfButtonValue === detail.value; + value(event) { + const checked = pdfButtonValue === event.detail.value; for (const radio of document.getElementsByName(event.target.name)) { const radioId = radio.getAttribute("id"); radio.checked = radioId === id && checked; storage.setValue(radioId, { value: radio.checked }); } }, - focus() { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - hidden() { - event.target.style.visibility = detail.hidden - ? "hidden" - : "visible"; - storage.setValue(id, { hidden: detail.hidden }); - }, - editable() { - event.target.disabled = !detail.editable; - }, }; - Object.keys(detail) - .filter(name => name in actions) - .forEach(name => actions[name]()); - this._setColor(event); + this._dispatchEventFromSandbox(actions, jsEvent); }); this._setEventListeners( @@ -1181,12 +1175,11 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { }; if (this.enableScripting && this.hasJSActions) { - selectElement.addEventListener("updatefromsandbox", event => { - const { detail } = event; + selectElement.addEventListener("updatefromsandbox", jsEvent => { const actions = { - value() { + value(event) { const options = selectElement.options; - const value = detail.value; + const value = event.detail.value; const values = new Set(Array.isArray(value) ? value : [value]); Array.prototype.forEach.call(options, option => { option.selected = values.has(option.value); @@ -1195,12 +1188,12 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { value: getValue(event, /* isExport */ true), }); }, - multipleSelection() { + multipleSelection(event) { selectElement.multiple = true; }, - remove() { + remove(event) { const options = selectElement.options; - const index = detail.remove; + const index = event.detail.remove; options[index].selected = false; selectElement.remove(index); if (options.length > 0) { @@ -1217,14 +1210,14 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { items: getItems(event), }); }, - clear() { + clear(event) { while (selectElement.length !== 0) { selectElement.remove(0); } storage.setValue(id, { value: null, items: [] }); }, - insert() { - const { index, displayValue, exportValue } = detail.insert; + insert(event) { + const { index, displayValue, exportValue } = event.detail.insert; const optionElement = document.createElement("option"); optionElement.textContent = displayValue; optionElement.value = exportValue; @@ -1237,8 +1230,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { items: getItems(event), }); }, - items() { - const { items } = detail; + items(event) { + const { items } = event.detail; while (selectElement.length !== 0) { selectElement.remove(0); } @@ -1257,8 +1250,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { items: getItems(event), }); }, - indices() { - const indices = new Set(detail.indices); + indices(event) { + const indices = new Set(event.detail.indices); const options = event.target.options; Array.prototype.forEach.call(options, (option, i) => { option.selected = indices.has(i); @@ -1267,23 +1260,11 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { value: getValue(event, /* isExport */ true), }); }, - focus() { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - hidden() { - event.target.style.visibility = detail.hidden - ? "hidden" - : "visible"; - storage.setValue(id, { hidden: detail.hidden }); - }, - editable() { - event.target.disabled = !detail.editable; + editable(event) { + event.target.disabled = !event.detail.editable; }, }; - Object.keys(detail) - .filter(name => name in actions) - .forEach(name => actions[name]()); - this._setColor(event); + this._dispatchEventFromSandbox(actions, jsEvent); }); selectElement.addEventListener("input", event => { diff --git a/src/scripting_api/proxy.js b/src/scripting_api/proxy.js index 41e6fb336..a1de00278 100644 --- a/src/scripting_api/proxy.js +++ b/src/scripting_api/proxy.js @@ -14,6 +14,13 @@ */ class ProxyHandler { + constructor() { + // Don't dispatch an event for those properties. + // - delay: allow to delay field redraw until delay is set to false. + // Likely it's useless to implement that stuff. + this.nosend = new Set(["delay"]); + } + get(obj, prop) { // script may add some properties to the object if (prop in obj._expandos) { @@ -49,7 +56,12 @@ class ProxyHandler { if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { const old = obj[prop]; obj[prop] = value; - if (obj._send && obj._id !== null && typeof old !== "function") { + if ( + !this.nosend.has(prop) && + obj._send && + obj._id !== null && + typeof old !== "function" + ) { const data = { id: obj._id }; data[prop] = obj[prop]; diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index de950357a..3c3ad3f70 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -809,6 +809,41 @@ describe("Interaction", () => { }) ); }); + + it("must check display", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + for (const [type, vis] of [ + ["hidden", "hidden"], + ["noPrint", "visible"], + ["noView", "hidden"], + ["visible", "visible"], + ]) { + let visibility = await page.$eval( + "#\\35 6R", + el => getComputedStyle(el).visibility + ); + + await clearInput(page, "#\\35 5R"); + await page.type( + "#\\35 5R", + `this.getField("Text2").display = display.${type};` + ); + + await page.click("[data-annotation-id='57R']"); + await page.waitForFunction( + `getComputedStyle(document.querySelector("#\\\\35 6R")).visibility !== "${visibility}"` + ); + + visibility = await page.$eval( + "#\\35 6R", + el => getComputedStyle(el).visibility + ); + expect(visibility).withContext(`In ${browserName}`).toEqual(vis); + } + }) + ); + }); }); describe("in issue13269.pdf", () => { diff --git a/web/pdf_scripting_manager.js b/web/pdf_scripting_manager.js index 01c6949be..aff5926a0 100644 --- a/web/pdf_scripting_manager.js +++ b/web/pdf_scripting_manager.js @@ -310,6 +310,7 @@ class PDFScriptingManager { } delete detail.id; + delete detail.siblings; const ids = siblings ? [id, ...siblings] : [id]; for (const elementId of ids) {