diff --git a/src/core/annotation.js b/src/core/annotation.js index fff00fa8d..6aa8b4d20 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -2255,7 +2255,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { // Determine the maximum length of text in the field. let maximumLength = getInheritableProperty({ dict, key: "MaxLen" }); if (!Number.isInteger(maximumLength) || maximumLength < 0) { - maximumLength = null; + maximumLength = 0; } this.data.maxLen = maximumLength; @@ -2266,7 +2266,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { !this.hasFieldFlag(AnnotationFieldFlag.MULTILINE) && !this.hasFieldFlag(AnnotationFieldFlag.PASSWORD) && !this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) && - this.data.maxLen !== null; + this.data.maxLen !== 0; this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 1e272ac89..052210367 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1000,7 +1000,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { const storedData = storage.getValue(id, { value: this.data.fieldValue, }); - const textContent = storedData.formattedValue || storedData.value || ""; + let textContent = storedData.formattedValue || storedData.value || ""; + const maxLen = storage.getValue(id, { + charLimit: this.data.maxLen, + }).charLimit; + if (maxLen && textContent.length > maxLen) { + textContent = textContent.slice(0, maxLen); + } + const elementData = { userValue: textContent, formattedValue: null, @@ -1030,6 +1037,10 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { this._setRequired(element, this.data.required); + if (maxLen) { + element.maxLength = maxLen; + } + element.addEventListener("input", event => { storage.setValue(id, { value: event.target.value }); this.setPropertyOnSiblings( @@ -1088,6 +1099,36 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { selRange(event) { event.target.setSelectionRange(...event.detail.selRange); }, + charLimit: event => { + const { charLimit } = event.detail; + const { target } = event; + if (charLimit === 0) { + target.removeAttribute("maxLength"); + return; + } + + target.setAttribute("maxLength", charLimit); + let value = elementData.userValue; + if (!value || value.length <= charLimit) { + return; + } + value = value.slice(0, charLimit); + target.value = elementData.userValue = value; + storage.setValue(id, { value }); + + this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { + source: this, + detail: { + id, + name: "Keystroke", + value, + willCommit: true, + commitKey: 1, + selStart: target.selectionStart, + selEnd: target.selectionEnd, + }, + }); + }, }; this._dispatchEventFromSandbox(actions, jsEvent); }); @@ -1225,13 +1266,9 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { element.addEventListener("blur", blurListener); } - if (this.data.maxLen !== null) { - element.maxLength = this.data.maxLen; - } - if (this.data.comb) { const fieldWidth = this.data.rect[2] - this.data.rect[0]; - const combWidth = fieldWidth / this.data.maxLen; + const combWidth = fieldWidth / maxLen; element.classList.add("comb"); element.style.letterSpacing = `calc(${combWidth}px * var(--scale-factor) - 1ch)`; diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index cf7b99044..49dc39450 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -29,7 +29,6 @@ class Field extends PDFObject { this.buttonScaleHow = data.buttonScaleHow; this.ButtonScaleWhen = data.buttonScaleWhen; this.calcOrderIndex = data.calcOrderIndex; - this.charLimit = data.charLimit; this.comb = data.comb; this.commitOnSelChange = data.commitOnSelChange; this.currentValueIndices = data.currentValueIndices; @@ -69,6 +68,7 @@ class Field extends PDFObject { this._browseForFileToSubmit = data.browseForFileToSubmit || null; this._buttonCaption = null; this._buttonIcon = null; + this._charLimit = data.charLimit; this._children = null; this._currentValueIndices = data.currentValueIndices || 0; this._document = data.doc; @@ -151,6 +151,17 @@ class Field extends PDFObject { this.fillColor = color; } + get charLimit() { + return this._charLimit; + } + + set charLimit(limit) { + if (typeof limit !== "number") { + throw new Error("Invalid argument value"); + } + this._charLimit = Math.max(0, Math.floor(limit)); + } + get numItems() { if (!this._isChoice) { throw new Error("Not a choice widget"); diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index e5dd59c39..dffef9f76 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -1492,4 +1492,65 @@ describe("Interaction", () => { ); }); }); + + describe("in bug1782564.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("bug1782564.pdf", getSelector("7R")); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that charLimit is correctly set", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await clearInput(page, getSelector("7R")); + // By default the charLimit is 0 which means that the input + // length is unlimited. + await page.type(getSelector("7R"), "abcdefghijklmnopq", { + delay: 10, + }); + + let value = await page.$eval(getSelector("7R"), el => el.value); + expect(value) + .withContext(`In ${browserName}`) + .toEqual("abcdefghijklmnopq"); + + // charLimit is set to 1 + await page.click(getSelector("9R")); + + await page.waitForFunction( + `document.querySelector('${getSelector( + "7R" + )}').value !== "abcdefgh"` + ); + + value = await page.$eval(getSelector("7R"), el => el.value); + expect(value).withContext(`In ${browserName}`).toEqual("a"); + + await clearInput(page, getSelector("7R")); + await page.type(getSelector("7R"), "xyz", { delay: 10 }); + + value = await page.$eval(getSelector("7R"), el => el.value); + expect(value).withContext(`In ${browserName}`).toEqual("x"); + + // charLimit is set to 2 + await page.click(getSelector("9R")); + + await clearInput(page, getSelector("7R")); + await page.type(getSelector("7R"), "xyz", { delay: 10 }); + + value = await page.$eval(getSelector("7R"), el => el.value); + expect(value).withContext(`In ${browserName}`).toEqual("xy"); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 6df734f40..b3f167b00 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -535,3 +535,4 @@ !issue15092.pdf !bug1782186.pdf !tracemonkey_a11y.pdf +!bug1782564.pdf diff --git a/test/pdfs/bug1782564.pdf b/test/pdfs/bug1782564.pdf new file mode 100755 index 000000000..a64589a3b Binary files /dev/null and b/test/pdfs/bug1782564.pdf differ diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 2530d315e..dd9510626 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1454,7 +1454,7 @@ describe("annotation", function () { ); expect(data.annotationType).toEqual(AnnotationType.WIDGET); expect(data.textAlignment).toEqual(null); - expect(data.maxLen).toEqual(null); + expect(data.maxLen).toEqual(0); expect(data.readOnly).toEqual(false); expect(data.hidden).toEqual(false); expect(data.multiLine).toEqual(false); @@ -1478,7 +1478,7 @@ describe("annotation", function () { ); expect(data.annotationType).toEqual(AnnotationType.WIDGET); expect(data.textAlignment).toEqual(null); - expect(data.maxLen).toEqual(null); + expect(data.maxLen).toEqual(0); expect(data.readOnly).toEqual(false); expect(data.hidden).toEqual(false); expect(data.multiLine).toEqual(false); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 0caf0c2e1..bdcfb05d1 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1390,7 +1390,7 @@ describe("api", function () { defaultValue: "", multiline: false, password: false, - charLimit: null, + charLimit: 0, comb: false, editable: true, hidden: false,