JS -- add support for choice widget (#12826)

This commit is contained in:
calixteman 2021-01-25 14:40:57 -08:00 committed by GitHub
parent 6249ef517d
commit a3f6882b06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 516 additions and 114 deletions

View File

@ -2091,6 +2091,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
multipleSelection: this.data.multiSelect,
hidden: this.data.hidden,
actions: this.data.actions,
items: this.data.options,
type,
};
}

View File

@ -698,25 +698,43 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
.forEach(name => actions[name]());
});
if (this.data.actions) {
// Even if the field hasn't any actions
// leaving it can still trigger some actions with Calculate
element.addEventListener("keydown", event => {
elementData.beforeInputValue = event.target.value;
// if the key is one of Escape, Enter or Tab
// then the data are committed
let commitKey = -1;
if (event.key === "Escape") {
commitKey = 0;
} else if (event.key === "Enter") {
commitKey = 2;
} else if (event.key === "Tab") {
commitKey = 3;
}
if (commitKey === -1) {
return;
}
// Save the entered value
// Even if the field hasn't any actions
// leaving it can still trigger some actions with Calculate
element.addEventListener("keydown", event => {
elementData.beforeInputValue = event.target.value;
// if the key is one of Escape, Enter or Tab
// then the data are committed
let commitKey = -1;
if (event.key === "Escape") {
commitKey = 0;
} else if (event.key === "Enter") {
commitKey = 2;
} else if (event.key === "Tab") {
commitKey = 3;
}
if (commitKey === -1) {
return;
}
// Save the entered value
elementData.userValue = event.target.value;
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id,
name: "Keystroke",
value: event.target.value,
willCommit: true,
commitKey,
selStart: event.target.selectionStart,
selEnd: event.target.selectionEnd,
},
});
});
const _blurListener = blurListener;
blurListener = null;
element.addEventListener("blur", event => {
if (this._mouseState.isDown) {
// Focus out using the mouse: data are committed
elementData.userValue = event.target.value;
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
@ -725,87 +743,67 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
name: "Keystroke",
value: event.target.value,
willCommit: true,
commitKey,
commitKey: 1,
selStart: event.target.selectionStart,
selEnd: event.target.selectionEnd,
},
});
});
const _blurListener = blurListener;
blurListener = null;
element.addEventListener("blur", event => {
if (this._mouseState.isDown) {
// Focus out using the mouse: data are committed
elementData.userValue = event.target.value;
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id,
name: "Keystroke",
value: event.target.value,
willCommit: true,
commitKey: 1,
selStart: event.target.selectionStart,
selEnd: event.target.selectionEnd,
},
});
}
_blurListener(event);
});
element.addEventListener("mousedown", event => {
elementData.beforeInputValue = event.target.value;
elementData.beforeInputSelectionRange = null;
});
element.addEventListener("keyup", event => {
// keyup is triggered after input
if (event.target.selectionStart === event.target.selectionEnd) {
elementData.beforeInputSelectionRange = null;
}
});
element.addEventListener("select", event => {
elementData.beforeInputSelectionRange = [
event.target.selectionStart,
event.target.selectionEnd,
];
});
if ("Keystroke" in this.data.actions) {
// We should use beforeinput but this
// event isn't available in Firefox
element.addEventListener("input", event => {
let selStart = -1;
let selEnd = -1;
if (elementData.beforeInputSelectionRange) {
[selStart, selEnd] = elementData.beforeInputSelectionRange;
}
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id,
name: "Keystroke",
value: elementData.beforeInputValue,
change: event.data,
willCommit: false,
selStart,
selEnd,
},
});
});
}
_blurListener(event);
});
element.addEventListener("mousedown", event => {
elementData.beforeInputValue = event.target.value;
elementData.beforeInputSelectionRange = null;
});
element.addEventListener("keyup", event => {
// keyup is triggered after input
if (event.target.selectionStart === event.target.selectionEnd) {
elementData.beforeInputSelectionRange = null;
}
});
element.addEventListener("select", event => {
elementData.beforeInputSelectionRange = [
event.target.selectionStart,
event.target.selectionEnd,
];
});
this._setEventListeners(
element,
[
["focus", "Focus"],
["blur", "Blur"],
["mousedown", "Mouse Down"],
["mouseenter", "Mouse Enter"],
["mouseleave", "Mouse Exit"],
["mouseup", "Mouse Up"],
],
event => event.target.value
);
if (this.data.actions?.Keystroke) {
// We should use beforeinput but this
// event isn't available in Firefox
element.addEventListener("input", event => {
let selStart = -1;
let selEnd = -1;
if (elementData.beforeInputSelectionRange) {
[selStart, selEnd] = elementData.beforeInputSelectionRange;
}
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id,
name: "Keystroke",
value: elementData.beforeInputValue,
change: event.data,
willCommit: false,
selStart,
selEnd,
},
});
});
}
this._setEventListeners(
element,
[
["focus", "Focus"],
["blur", "Blur"],
["mousedown", "Mouse Down"],
["mouseenter", "Mouse Enter"],
["mouseleave", "Mouse Exit"],
["mouseup", "Mouse Up"],
],
event => event.target.value
);
}
if (blurListener) {
@ -1102,23 +1100,112 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
selectElement.appendChild(optionElement);
}
function getValue(event) {
const getValue = (event, isExport) => {
const name = isExport ? "value" : "textContent";
const options = event.target.options;
return options[options.selectedIndex].value;
}
if (!event.target.multiple) {
return options.selectedIndex === -1
? null
: options[options.selectedIndex][name];
}
return Array.prototype.filter
.call(options, option => option.selected)
.map(option => option[name]);
};
const getItems = event => {
const options = event.target.options;
return Array.prototype.map.call(options, option => {
return { displayValue: option.textContent, exportValue: option.value };
});
};
if (this.enableScripting && this.hasJSActions) {
selectElement.addEventListener("updatefromsandbox", event => {
const { detail } = event;
const actions = {
value() {
const options = event.target.options;
const options = selectElement.options;
const value = detail.value;
const i = options.indexOf(value);
if (i !== -1) {
options.selectedIndex = i;
storage.setValue(id, { value });
const values = new Set(Array.isArray(value) ? value : [value]);
Array.prototype.forEach.call(options, option => {
option.selected = values.has(option.value);
});
storage.setValue(id, {
value: getValue(event, /* isExport */ true),
});
},
multipleSelection() {
selectElement.multiple = true;
},
remove() {
const options = selectElement.options;
const index = detail.remove;
options[index].selected = false;
selectElement.remove(index);
if (options.length > 0) {
const i = Array.prototype.findIndex.call(
options,
option => option.selected
);
if (i === -1) {
options[0].selected = true;
}
}
storage.setValue(id, {
value: getValue(event, /* isExport */ true),
items: getItems(event),
});
},
clear() {
while (selectElement.length !== 0) {
selectElement.remove(0);
}
storage.setValue(id, { value: null, items: [] });
},
insert() {
const { index, displayValue, exportValue } = detail.insert;
const optionElement = document.createElement("option");
optionElement.textContent = displayValue;
optionElement.value = exportValue;
selectElement.insertBefore(
optionElement,
selectElement.children[index]
);
storage.setValue(id, {
value: getValue(event, /* isExport */ true),
items: getItems(event),
});
},
items() {
const { items } = detail;
while (selectElement.length !== 0) {
selectElement.remove(0);
}
for (const item of items) {
const { displayValue, exportValue } = item;
const optionElement = document.createElement("option");
optionElement.textContent = displayValue;
optionElement.value = exportValue;
selectElement.appendChild(optionElement);
}
if (selectElement.options.length > 0) {
selectElement.options[0].selected = true;
}
storage.setValue(id, {
value: getValue(event, /* isExport */ true),
items: getItems(event),
});
},
indices() {
const indices = new Set(detail.indices);
const options = event.target.options;
Array.prototype.forEach.call(options, (option, i) => {
option.selected = indices.has(i);
});
storage.setValue(id, {
value: getValue(event, /* isExport */ true),
});
},
focus() {
setTimeout(() => event.target.focus({ preventScroll: false }), 0);
@ -1139,15 +1226,17 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
});
selectElement.addEventListener("input", event => {
const value = getValue(event);
storage.setValue(id, { value });
const exportValue = getValue(event, /* isExport */ true);
const value = getValue(event, /* isExport */ false);
storage.setValue(id, { value: exportValue });
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id,
name: "Keystroke",
changeEx: value,
value,
changeEx: exportValue,
willCommit: true,
commitKey: 1,
keyDown: false,
@ -1164,6 +1253,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
["mouseenter", "Mouse Enter"],
["mouseleave", "Mouse Exit"],
["mouseup", "Mouse Up"],
["input", "Action"],
],
event => event.target.checked
);

View File

@ -49,7 +49,6 @@ class Field extends PDFObject {
this.multiline = data.multiline;
this.multipleSelection = !!data.multipleSelection;
this.name = data.name;
this.numItems = data.numItems;
this.page = data.page;
this.password = data.password;
this.print = data.print;
@ -68,17 +67,64 @@ class Field extends PDFObject {
this.userName = data.userName;
// Private
this._document = data.doc;
this._value = data.value || "";
this._valueAsString = data.valueAsString;
this._actions = createActionsMap(data.actions);
this._currentValueIndices = data.currentValueIndices || 0;
this._document = data.doc;
this._fillColor = data.fillColor || ["T"];
this._isChoice = Array.isArray(data.items);
this._items = data.items || [];
this._strokeColor = data.strokeColor || ["G", 0];
this._textColor = data.textColor || ["G", 0];
this._value = data.value || "";
this._valueAsString = data.valueAsString;
this._globalEval = data.globalEval;
}
get currentValueIndices() {
if (!this._isChoice) {
return 0;
}
return this._currentValueIndices;
}
set currentValueIndices(indices) {
if (!this._isChoice) {
return;
}
if (!Array.isArray(indices)) {
indices = [indices];
}
if (
!indices.every(
i =>
typeof i === "number" &&
Number.isInteger(i) &&
i >= 0 &&
i < this.numItems
)
) {
return;
}
indices.sort();
if (this.multipleSelection) {
this._currentValueIndices = indices;
this._value = [];
indices.forEach(i => {
this._value.push(this._items[i].displayValue);
});
} else {
if (indices.length > 0) {
indices = indices.splice(1, indices.length - 1);
this._currentValueIndices = indices[0];
this._value = this._items[this._currentValueIndices];
}
}
this._send({ id: this._id, indices });
}
get fillColor() {
return this._fillColor;
}
@ -89,6 +135,17 @@ class Field extends PDFObject {
}
}
get numItems() {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
return this._items.length;
}
set numItems(_) {
throw new Error("field.numItems is read-only");
}
get strokeColor() {
return this._strokeColor;
}
@ -114,8 +171,21 @@ class Field extends PDFObject {
}
set value(value) {
if (!this.multipleSelection) {
this._value = value;
this._value = value;
if (this._isChoice) {
if (this.multipleSelection) {
const values = new Set(value);
this._currentValueIndices.length = 0;
this._items.forEach(({ displayValue }, i) => {
if (values.has(displayValue)) {
this._currentValueIndices.push(i);
}
});
} else {
this._currentValueIndices = this._items.findIndex(
({ displayValue }) => value === displayValue
);
}
}
}
@ -129,6 +199,67 @@ class Field extends PDFObject {
checkThisBox(nWidget, bCheckIt = true) {}
clearItems() {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
this._items = [];
this._send({ id: this._id, clear: null });
}
deleteItemAt(nIdx = null) {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
if (!this.numItems) {
return;
}
if (nIdx === null) {
// Current selected item.
nIdx = Array.isArray(this._currentValueIndices)
? this._currentValueIndices[0]
: this._currentValueIndices;
nIdx = nIdx || 0;
}
if (nIdx < 0 || nIdx >= this.numItems) {
nIdx = this.numItems - 1;
}
this._items.splice(nIdx, 1);
if (Array.isArray(this._currentValueIndices)) {
let index = this._currentValueIndices.findIndex(i => i >= nIdx);
if (index !== -1) {
if (this._currentValueIndices[index] === nIdx) {
this._currentValueIndices.splice(index, 1);
}
for (const ii = this._currentValueIndices.length; index < ii; index++) {
--this._currentValueIndices[index];
}
}
} else {
if (this._currentValueIndices === nIdx) {
this._currentValueIndices = this.numItems > 0 ? 0 : -1;
} else if (this._currentValueIndices > nIdx) {
--this._currentValueIndices;
}
}
this._send({ id: this._id, remove: nIdx });
}
getItemAt(nIdx = -1, bExportValue = false) {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
if (nIdx < 0 || nIdx >= this.numItems) {
nIdx = this.numItems - 1;
}
const item = this._items[nIdx];
return bExportValue ? item.exportValue : item.displayValue;
}
isBoxChecked(nWidget) {
return false;
}
@ -137,6 +268,41 @@ class Field extends PDFObject {
return false;
}
insertItemAt(cName, cExport = undefined, nIdx = 0) {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
if (!cName) {
return;
}
if (nIdx < 0 || nIdx > this.numItems) {
nIdx = this.numItems;
}
if (this._items.some(({ displayValue }) => displayValue === cName)) {
return;
}
if (cExport === undefined) {
cExport = cName;
}
const data = { displayValue: cName, exportValue: cExport };
this._items.splice(nIdx, 0, data);
if (Array.isArray(this._currentValueIndices)) {
let index = this._currentValueIndices.findIndex(i => i >= nIdx);
if (index !== -1) {
for (const ii = this._currentValueIndices.length; index < ii; index++) {
++this._currentValueIndices[index];
}
}
} else if (this._currentValueIndices >= nIdx) {
++this._currentValueIndices;
}
this._send({ id: this._id, insert: { index: nIdx, ...data } });
}
setAction(cTrigger, cScript) {
if (typeof cTrigger !== "string" || typeof cScript !== "string") {
return;
@ -151,6 +317,26 @@ class Field extends PDFObject {
this._send({ id: this._id, focus: true });
}
setItems(oArray) {
if (!this._isChoice) {
throw new Error("Not a choice widget");
}
this._items.length = 0;
for (const element of oArray) {
let displayValue, exportValue;
if (Array.isArray(element)) {
displayValue = element[0]?.toString() || "";
exportValue = element[1]?.toString() || "";
} else {
displayValue = exportValue = element?.toString() || "";
}
this._items.push({ displayValue, exportValue });
}
this._currentValueIndices = 0;
this._send({ id: this._id, items: this._items });
}
_isButton() {
return false;
}

View File

@ -489,10 +489,6 @@ describe("Interaction", () => {
pages = await loadAndWait("js-authors.pdf", "#\\32 5R");
});
afterAll(async () => {
await closePages(pages);
});
it("must print authors in a text field", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
@ -506,4 +502,132 @@ describe("Interaction", () => {
);
});
});
describe("in listbox_actions.pdf", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("listbox_actions.pdf", "#\\33 3R");
});
afterAll(async () => {
await closePages(pages);
});
it("must print selected value in a text field", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (const num of [7, 6, 4, 3, 2, 1]) {
await page.click(`option[value=Export${num}]`);
await page.waitForFunction(
`document.querySelector("#\\\\33 3R").value !== ""`
);
const text = await page.$eval("#\\33 3R", el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`Item${num},Export${num}`);
}
})
);
});
it("must clear and restore list elements", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Click on ClearItems button.
await page.click("[data-annotation-id='34R']");
await page.waitForFunction(
`document.querySelector("#\\\\33 0R").children.length === 0`
);
// Click on Restore button.
await page.click("[data-annotation-id='37R']");
await page.waitForFunction(
`document.querySelector("#\\\\33 0R").children.length !== 0`
);
for (const num of [7, 6, 4, 3, 2, 1]) {
await page.click(`option[value=Export${num}]`);
await page.waitForFunction(
`document.querySelector("#\\\\33 3R").value !== ""`
);
const text = await page.$eval("#\\33 3R", el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`Item${num},Export${num}`);
}
})
);
});
it("must insert new elements", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
let len = 6;
for (const num of [1, 3, 5, 6, 431, -1, 0]) {
++len;
await clearInput(page, "#\\33 9R");
await page.type("#\\33 9R", `${num},Insert${num},Tresni${num}`, {
delay: 10,
});
// Click on AddItem button.
await page.click("[data-annotation-id='38R']");
await page.waitForFunction(
`document.querySelector("#\\\\33 0R").children.length === ${len}`
);
// Click on newly added option.
await page.select("#\\33 0R", `Tresni${num}`);
await page.waitForFunction(
`document.querySelector("#\\\\33 3R").value !== ""`
);
const text = await page.$eval("#\\33 3R", el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`Insert${num},Tresni${num}`);
}
})
);
});
it("must delete some element", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
let len = 6;
// Click on Restore button.
await page.click("[data-annotation-id='37R']");
await page.waitForFunction(
`document.querySelector("#\\\\33 0R").children.length === ${len}`
);
for (const num of [2, 5]) {
--len;
await clearInput(page, "#\\33 9R");
await page.type("#\\33 9R", `${num}`);
// Click on DeleteItem button.
await page.click("[data-annotation-id='36R']");
await page.waitForFunction(
`document.querySelector("#\\\\33 0R").children.length === ${len}`
);
}
for (const num of [6, 4, 2, 1]) {
await page.click(`option[value=Export${num}]`);
await page.waitForFunction(
`document.querySelector("#\\\\33 3R").value !== ""`
);
const text = await page.$eval("#\\33 3R", el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`Item${num},Export${num}`);
}
})
);
});
});
});

View File

@ -403,6 +403,7 @@
!operator-in-TJ-array.pdf
!issue7878.pdf
!font_ascent_descent.pdf
!listbox_actions.pdf
!issue11442_reduced.pdf
!issue11549_reduced.pdf
!issue8097_reduced.pdf

Binary file not shown.