JS - Add support for display property

- in annotation_layer, move common properties treatment in a common method instead having duplicated code in each widget.
This commit is contained in:
Calixte Denizet 2021-05-03 18:03:16 +02:00
parent afb8c4fd25
commit af125cd299
6 changed files with 197 additions and 141 deletions

@ -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 = const storageEntry =
annotationStorage && annotationStorage.get(this.data.id); annotationStorage && annotationStorage.get(this.data.id);
if (storageEntry && storageEntry.hidden !== undefined) { 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;
} }
/** /**

@ -68,13 +68,6 @@ import { XRef } from "./xref.js";
const DEFAULT_USER_UNIT = 1.0; const DEFAULT_USER_UNIT = 1.0;
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792]; const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
function isAnnotationRenderable(annotation, intent) {
return (
(intent === "display" && annotation.viewable) ||
(intent === "print" && annotation.printable)
);
}
class Page { class Page {
constructor({ constructor({
pdfManager, pdfManager,
@ -274,7 +267,7 @@ class Page {
return this._parsedAnnotations.then(function (annotations) { return this._parsedAnnotations.then(function (annotations) {
const newRefsPromises = []; const newRefsPromises = [];
for (const annotation of annotations) { for (const annotation of annotations) {
if (!isAnnotationRenderable(annotation, "print")) { if (!annotation.mustBePrinted(annotationStorage)) {
continue; continue;
} }
newRefsPromises.push( newRefsPromises.push(
@ -377,8 +370,9 @@ class Page {
const opListPromises = []; const opListPromises = [];
for (const annotation of annotations) { for (const annotation of annotations) {
if ( if (
isAnnotationRenderable(annotation, intent) && (intent === "display" &&
!annotation.isHidden(annotationStorage) annotation.mustBeViewed(annotationStorage)) ||
(intent === "print" && annotation.mustBePrinted(annotationStorage))
) { ) {
opListPromises.push( opListPromises.push(
annotation annotation
@ -482,7 +476,13 @@ class Page {
return this._parsedAnnotations.then(function (annotations) { return this._parsedAnnotations.then(function (annotations) {
const annotationsData = []; const annotationsData = [];
for (let i = 0, ii = annotations.length; i < ii; i++) { 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); annotationsData.push(annotations[i].data);
} }
} }

@ -583,35 +583,81 @@ class WidgetAnnotationElement extends AnnotationElement {
} }
} }
_setColor(event) { _dispatchEventFromSandbox(actions, jsEvent) {
const { detail, target } = event; const setColor = (jsName, styleName, event) => {
const { style } = target; const color = event.detail[jsName];
for (const name of [ event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`](
"bgColor", color.slice(1)
"fillColor", );
"fgColor", };
"textColor",
"borderColor", const commonActions = {
"strokeColor", display: event => {
]) { const hidden = event.detail.display % 2 === 1;
let color = detail[name]; event.target.style.visibility = hidden ? "hidden" : "visible";
if (!color) { this.annotationStorage.setValue(this.data.id, {
continue; hidden,
} print: event.detail.display === 0 || event.detail.display === 3,
color = ColorConverters[`${color[0]}_HTML`](color.slice(1)); });
switch (name) { },
case "bgColor": print: event => {
case "fillColor": this.annotationStorage.setValue(this.data.id, {
style.backgroundColor = color; print: event.detail.print,
break; });
case "fgColor": },
case "textColor": hidden: event => {
style.color = color; event.target.style.visibility = event.detail.hidden
break; ? "hidden"
case "borderColor": : "visible";
case "strokeColor": this.annotationStorage.setValue(this.data.id, {
style.borderColor = color; hidden: event.detail.hidden,
break; });
},
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 => { element.addEventListener("updatefromsandbox", jsEvent => {
const { detail } = event;
const actions = { const actions = {
value() { value(event) {
elementData.userValue = detail.value || ""; elementData.userValue = event.detail.value || "";
storage.setValue(id, { value: elementData.userValue.toString() }); storage.setValue(id, { value: elementData.userValue.toString() });
if (!elementData.formattedValue) { if (!elementData.formattedValue) {
event.target.value = elementData.userValue; event.target.value = elementData.userValue;
} }
}, },
valueAsString() { valueAsString(event) {
elementData.formattedValue = detail.valueAsString || ""; elementData.formattedValue = event.detail.valueAsString || "";
if (event.target !== document.activeElement) { if (event.target !== document.activeElement) {
// Input hasn't the focus so display formatted string // Input hasn't the focus so display formatted string
event.target.value = elementData.formattedValue; event.target.value = elementData.formattedValue;
@ -718,33 +763,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
formattedValue: elementData.formattedValue, formattedValue: elementData.formattedValue,
}); });
}, },
focus() { selRange(event) {
setTimeout(() => event.target.focus({ preventScroll: false }), 0); const [selStart, selEnd] = event.detail.selRange;
},
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;
if (selStart >= 0 && selEnd < event.target.value.length) { if (selStart >= 0 && selEnd < event.target.value.length) {
event.target.setSelectionRange(selStart, selEnd); event.target.setSelectionRange(selStart, selEnd);
} }
}, },
}; };
Object.keys(detail) this._dispatchEventFromSandbox(actions, jsEvent);
.filter(name => name in actions)
.forEach(name => actions[name]());
this._setColor(event);
}); });
// Even if the field hasn't any actions // Even if the field hasn't any actions
@ -960,30 +986,14 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
}); });
if (this.enableScripting && this.hasJSActions) { if (this.enableScripting && this.hasJSActions) {
element.addEventListener("updatefromsandbox", event => { element.addEventListener("updatefromsandbox", jsEvent => {
const { detail } = event;
const actions = { const actions = {
value() { value(event) {
event.target.checked = detail.value !== "Off"; event.target.checked = event.detail.value !== "Off";
storage.setValue(id, { value: event.target.checked }); 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) this._dispatchEventFromSandbox(actions, jsEvent);
.filter(name => name in actions)
.forEach(name => actions[name]());
this._setColor(event);
}); });
this._setEventListeners( this._setEventListeners(
@ -1047,34 +1057,18 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
if (this.enableScripting && this.hasJSActions) { if (this.enableScripting && this.hasJSActions) {
const pdfButtonValue = data.buttonValue; const pdfButtonValue = data.buttonValue;
element.addEventListener("updatefromsandbox", event => { element.addEventListener("updatefromsandbox", jsEvent => {
const { detail } = event;
const actions = { const actions = {
value() { value(event) {
const checked = pdfButtonValue === detail.value; const checked = pdfButtonValue === event.detail.value;
for (const radio of document.getElementsByName(event.target.name)) { for (const radio of document.getElementsByName(event.target.name)) {
const radioId = radio.getAttribute("id"); const radioId = radio.getAttribute("id");
radio.checked = radioId === id && checked; radio.checked = radioId === id && checked;
storage.setValue(radioId, { value: radio.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) this._dispatchEventFromSandbox(actions, jsEvent);
.filter(name => name in actions)
.forEach(name => actions[name]());
this._setColor(event);
}); });
this._setEventListeners( this._setEventListeners(
@ -1181,12 +1175,11 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
}; };
if (this.enableScripting && this.hasJSActions) { if (this.enableScripting && this.hasJSActions) {
selectElement.addEventListener("updatefromsandbox", event => { selectElement.addEventListener("updatefromsandbox", jsEvent => {
const { detail } = event;
const actions = { const actions = {
value() { value(event) {
const options = selectElement.options; const options = selectElement.options;
const value = detail.value; const value = event.detail.value;
const values = new Set(Array.isArray(value) ? value : [value]); const values = new Set(Array.isArray(value) ? value : [value]);
Array.prototype.forEach.call(options, option => { Array.prototype.forEach.call(options, option => {
option.selected = values.has(option.value); option.selected = values.has(option.value);
@ -1195,12 +1188,12 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
value: getValue(event, /* isExport */ true), value: getValue(event, /* isExport */ true),
}); });
}, },
multipleSelection() { multipleSelection(event) {
selectElement.multiple = true; selectElement.multiple = true;
}, },
remove() { remove(event) {
const options = selectElement.options; const options = selectElement.options;
const index = detail.remove; const index = event.detail.remove;
options[index].selected = false; options[index].selected = false;
selectElement.remove(index); selectElement.remove(index);
if (options.length > 0) { if (options.length > 0) {
@ -1217,14 +1210,14 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
items: getItems(event), items: getItems(event),
}); });
}, },
clear() { clear(event) {
while (selectElement.length !== 0) { while (selectElement.length !== 0) {
selectElement.remove(0); selectElement.remove(0);
} }
storage.setValue(id, { value: null, items: [] }); storage.setValue(id, { value: null, items: [] });
}, },
insert() { insert(event) {
const { index, displayValue, exportValue } = detail.insert; const { index, displayValue, exportValue } = event.detail.insert;
const optionElement = document.createElement("option"); const optionElement = document.createElement("option");
optionElement.textContent = displayValue; optionElement.textContent = displayValue;
optionElement.value = exportValue; optionElement.value = exportValue;
@ -1237,8 +1230,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
items: getItems(event), items: getItems(event),
}); });
}, },
items() { items(event) {
const { items } = detail; const { items } = event.detail;
while (selectElement.length !== 0) { while (selectElement.length !== 0) {
selectElement.remove(0); selectElement.remove(0);
} }
@ -1257,8 +1250,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
items: getItems(event), items: getItems(event),
}); });
}, },
indices() { indices(event) {
const indices = new Set(detail.indices); const indices = new Set(event.detail.indices);
const options = event.target.options; const options = event.target.options;
Array.prototype.forEach.call(options, (option, i) => { Array.prototype.forEach.call(options, (option, i) => {
option.selected = indices.has(i); option.selected = indices.has(i);
@ -1267,23 +1260,11 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
value: getValue(event, /* isExport */ true), value: getValue(event, /* isExport */ true),
}); });
}, },
focus() { editable(event) {
setTimeout(() => event.target.focus({ preventScroll: false }), 0); event.target.disabled = !event.detail.editable;
},
hidden() {
event.target.style.visibility = detail.hidden
? "hidden"
: "visible";
storage.setValue(id, { hidden: detail.hidden });
},
editable() {
event.target.disabled = !detail.editable;
}, },
}; };
Object.keys(detail) this._dispatchEventFromSandbox(actions, jsEvent);
.filter(name => name in actions)
.forEach(name => actions[name]());
this._setColor(event);
}); });
selectElement.addEventListener("input", event => { selectElement.addEventListener("input", event => {

@ -14,6 +14,13 @@
*/ */
class ProxyHandler { 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) { get(obj, prop) {
// script may add some properties to the object // script may add some properties to the object
if (prop in obj._expandos) { if (prop in obj._expandos) {
@ -49,7 +56,12 @@ class ProxyHandler {
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
const old = obj[prop]; const old = obj[prop];
obj[prop] = value; 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 }; const data = { id: obj._id };
data[prop] = obj[prop]; data[prop] = obj[prop];

@ -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", () => { describe("in issue13269.pdf", () => {

@ -310,6 +310,7 @@ class PDFScriptingManager {
} }
delete detail.id; delete detail.id;
delete detail.siblings;
const ids = siblings ? [id, ...siblings] : [id]; const ids = siblings ? [id, ...siblings] : [id];
for (const elementId of ids) { for (const elementId of ids) {