diff --git a/src/core/annotation.js b/src/core/annotation.js index 97f728621..75d030535 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -106,6 +106,8 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { switch (fieldType) { case 'Tx': return new TextWidgetAnnotation(parameters); + case 'Btn': + return new ButtonWidgetAnnotation(parameters); case 'Ch': return new ChoiceWidgetAnnotation(parameters); } @@ -767,6 +769,78 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { return TextWidgetAnnotation; })(); +var ButtonWidgetAnnotation = (function ButtonWidgetAnnotationClosure() { + function ButtonWidgetAnnotation(params) { + WidgetAnnotation.call(this, params); + + this.data.checkBox = !this.hasFieldFlag(AnnotationFieldFlag.RADIO) && + !this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); + if (this.data.checkBox) { + if (!isName(this.data.fieldValue)) { + return; + } + this.data.fieldValue = this.data.fieldValue.name; + } + + this.data.radioButton = this.hasFieldFlag(AnnotationFieldFlag.RADIO) && + !this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); + if (this.data.radioButton) { + this.data.fieldValue = this.data.buttonValue = null; + + // The parent field's `V` entry holds a `Name` object with the appearance + // state of whichever child field is currently in the "on" state. + var fieldParent = params.dict.get('Parent'); + if (!isDict(fieldParent) || !fieldParent.has('V')) { + return; + } + var fieldParentValue = fieldParent.get('V'); + if (!isName(fieldParentValue)) { + return; + } + this.data.fieldValue = fieldParentValue.name; + + // The button's value corresponds to its appearance state. + var appearanceStates = params.dict.get('AP'); + if (!isDict(appearanceStates)) { + return; + } + var normalAppearanceState = appearanceStates.get('N'); + if (!isDict(normalAppearanceState)) { + return; + } + var keys = normalAppearanceState.getKeys(); + for (var i = 0, ii = keys.length; i < ii; i++) { + if (keys[i] !== 'Off') { + this.data.buttonValue = keys[i]; + break; + } + } + } + } + + Util.inherit(ButtonWidgetAnnotation, WidgetAnnotation, { + getOperatorList: + function ButtonWidgetAnnotation_getOperatorList(evaluator, task, + renderForms) { + var operatorList = new OperatorList(); + + // Do not render form elements on the canvas when interactive forms are + // enabled. The display layer is responsible for rendering them instead. + if (renderForms) { + return Promise.resolve(operatorList); + } + + if (this.appearance) { + return Annotation.prototype.getOperatorList.call(this, evaluator, task, + renderForms); + } + return Promise.resolve(operatorList); + } + }); + + return ButtonWidgetAnnotation; +})(); + var ChoiceWidgetAnnotation = (function ChoiceWidgetAnnotationClosure() { function ChoiceWidgetAnnotation(params) { WidgetAnnotation.call(this, params); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 0a57128c2..b70992adf 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -76,6 +76,15 @@ AnnotationElementFactory.prototype = switch (fieldType) { case 'Tx': return new TextWidgetAnnotationElement(parameters); + case 'Btn': + if (parameters.data.radioButton) { + return new RadioButtonWidgetAnnotationElement(parameters); + } else if (parameters.data.checkBox) { + return new CheckboxWidgetAnnotationElement(parameters); + } else { + warn('Unimplemented button widget annotation: pushbutton'); + } + break; case 'Ch': return new ChoiceWidgetAnnotationElement(parameters); } @@ -531,6 +540,83 @@ var TextWidgetAnnotationElement = ( })(); /** + * @class + * @alias CheckboxWidgetAnnotationElement + */ +var CheckboxWidgetAnnotationElement = + (function CheckboxWidgetAnnotationElementClosure() { + function CheckboxWidgetAnnotationElement(parameters) { + WidgetAnnotationElement.call(this, parameters, + parameters.renderInteractiveForms); + } + + Util.inherit(CheckboxWidgetAnnotationElement, WidgetAnnotationElement, { + /** + * Render the checkbox widget annotation's HTML element + * in the empty container. + * + * @public + * @memberof CheckboxWidgetAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function CheckboxWidgetAnnotationElement_render() { + this.container.className = 'buttonWidgetAnnotation checkBox'; + + var element = document.createElement('input'); + element.disabled = this.data.readOnly; + element.type = 'checkbox'; + if (this.data.fieldValue && this.data.fieldValue !== 'Off') { + element.setAttribute('checked', true); + } + + this.container.appendChild(element); + return this.container; + } + }); + + return CheckboxWidgetAnnotationElement; +})(); + +/** + * @class + * @alias RadioButtonWidgetAnnotationElement + */ +var RadioButtonWidgetAnnotationElement = + (function RadioButtonWidgetAnnotationElementClosure() { + function RadioButtonWidgetAnnotationElement(parameters) { + WidgetAnnotationElement.call(this, parameters, + parameters.renderInteractiveForms); + } + + Util.inherit(RadioButtonWidgetAnnotationElement, WidgetAnnotationElement, { + /** + * Render the radio button widget annotation's HTML element + * in the empty container. + * + * @public + * @memberof RadioButtonWidgetAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function RadioButtonWidgetAnnotationElement_render() { + this.container.className = 'buttonWidgetAnnotation radioButton'; + + var element = document.createElement('input'); + element.disabled = this.data.readOnly; + element.type = 'radio'; + element.name = this.data.fieldName; + if (this.data.fieldValue === this.data.buttonValue) { + element.setAttribute('checked', true); + } + + this.container.appendChild(element); + return this.container; + } + }); + + return RadioButtonWidgetAnnotationElement; +})(); + + /** * @class * @alias ChoiceWidgetAnnotationElement */ diff --git a/test/annotation_layer_test.css b/test/annotation_layer_test.css index 5c6a87be8..a18dd77ec 100644 --- a/test/annotation_layer_test.css +++ b/test/annotation_layer_test.css @@ -15,6 +15,11 @@ /* Used for annotation layer tests */ +* { + padding: 0; + margin: 0; +} + .annotationLayer { position: absolute; left: 0; @@ -45,7 +50,9 @@ .annotationLayer .textWidgetAnnotation input, .annotationLayer .textWidgetAnnotation textarea, -.annotationLayer .choiceWidgetAnnotation select { +.annotationLayer .choiceWidgetAnnotation select, +.annotationLayer .buttonWidgetAnnotation.checkBox input, +.annotationLayer .buttonWidgetAnnotation.radioButton input { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -64,7 +71,9 @@ .annotationLayer .textWidgetAnnotation input[disabled], .annotationLayer .textWidgetAnnotation textarea[disabled], -.annotationLayer .choiceWidgetAnnotation select[disabled] { +.annotationLayer .choiceWidgetAnnotation select[disabled], +.annotationLayer .buttonWidgetAnnotation.checkBox input[disabled], +.annotationLayer .buttonWidgetAnnotation.radioButton input[disabled] { background: none; border: 1px solid transparent; } @@ -75,6 +84,14 @@ padding-right: 0; } +.annotationLayer .buttonWidgetAnnotation.checkBox input, +.annotationLayer .buttonWidgetAnnotation.radioButton input { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; +} + .annotationLayer .popupAnnotation { display: block !important; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 90f561d3a..ee6015144 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -266,5 +266,6 @@ !annotation-fileattachment.pdf !annotation-text-widget.pdf !annotation-choice-widget.pdf +!annotation-button-widget.pdf !zero_descent.pdf !operator-in-TJ-array.pdf diff --git a/test/pdfs/annotation-button-widget.pdf b/test/pdfs/annotation-button-widget.pdf new file mode 100644 index 000000000..f1fa2394c Binary files /dev/null and b/test/pdfs/annotation-button-widget.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 5f7fd9b97..60bc5f8ad 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -3312,6 +3312,20 @@ "type": "eq", "forms": true }, + { "id": "annotation-button-widget-annotations", + "file": "pdfs/annotation-button-widget.pdf", + "md5": "5cf23adfff84256d9cfe261bea96dade", + "rounds": 1, + "type": "eq", + "annotations": true + }, + { "id": "annotation-button-widget-forms", + "file": "pdfs/annotation-button-widget.pdf", + "md5": "5cf23adfff84256d9cfe261bea96dade", + "rounds": 1, + "type": "eq", + "forms": true + }, { "id": "issue6108", "file": "pdfs/issue6108.pdf", "md5": "8961cb55149495989a80bf0487e0f076", diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js index f5f3a1bb4..ba10db204 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -869,6 +869,65 @@ describe('Annotation layer', function() { }); }); + describe('ButtonWidgetAnnotation', function() { + var buttonWidgetDict; + + beforeEach(function (done) { + buttonWidgetDict = new Dict(); + buttonWidgetDict.set('Type', Name.get('Annot')); + buttonWidgetDict.set('Subtype', Name.get('Widget')); + buttonWidgetDict.set('FT', Name.get('Btn')); + + done(); + }); + + afterEach(function () { + buttonWidgetDict = null; + }); + + it('should handle checkboxes', function() { + buttonWidgetDict.set('V', Name.get('1')); + + var buttonWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: buttonWidgetRef, data: buttonWidgetDict, } + ]); + + var buttonWidgetAnnotation = + annotationFactory.create(xref, buttonWidgetRef); + expect(buttonWidgetAnnotation.data.checkBox).toEqual(true); + expect(buttonWidgetAnnotation.data.fieldValue).toEqual('1'); + expect(buttonWidgetAnnotation.data.radioButton).toEqual(false); + }); + + it('should handle radio buttons', function() { + var parentDict = new Dict(); + parentDict.set('V', Name.get('1')); + + var normalAppearanceStateDict = new Dict(); + normalAppearanceStateDict.set('2', null); + + var appearanceStatesDict = new Dict(); + appearanceStatesDict.set('N', normalAppearanceStateDict); + + buttonWidgetDict.set('Ff', AnnotationFieldFlag.RADIO); + buttonWidgetDict.set('Parent', parentDict); + buttonWidgetDict.set('AP', appearanceStatesDict); + + var buttonWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: buttonWidgetRef, data: buttonWidgetDict, } + ]); + + var buttonWidgetAnnotation = + annotationFactory.create(xref, buttonWidgetRef); + expect(buttonWidgetAnnotation.data.checkBox).toEqual(false); + expect(buttonWidgetAnnotation.data.radioButton).toEqual(true); + expect(buttonWidgetAnnotation.data.fieldValue).toEqual('1'); + expect(buttonWidgetAnnotation.data.buttonValue).toEqual('2'); + }); + }); + describe('ChoiceWidgetAnnotation', function() { var choiceWidgetDict; diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 8cff630f9..982f66134 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -43,7 +43,9 @@ .annotationLayer .textWidgetAnnotation input, .annotationLayer .textWidgetAnnotation textarea, -.annotationLayer .choiceWidgetAnnotation select { +.annotationLayer .choiceWidgetAnnotation select, +.annotationLayer .buttonWidgetAnnotation.checkBox input, +.annotationLayer .buttonWidgetAnnotation.radioButton input { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -62,7 +64,9 @@ .annotationLayer .textWidgetAnnotation input[disabled], .annotationLayer .textWidgetAnnotation textarea[disabled], -.annotationLayer .choiceWidgetAnnotation select[disabled] { +.annotationLayer .choiceWidgetAnnotation select[disabled], +.annotationLayer .buttonWidgetAnnotation.checkBox input[disabled], +.annotationLayer .buttonWidgetAnnotation.radioButton input[disabled] { background: none; border: 1px solid transparent; cursor: not-allowed; @@ -70,7 +74,9 @@ .annotationLayer .textWidgetAnnotation input:hover, .annotationLayer .textWidgetAnnotation textarea:hover, -.annotationLayer .choiceWidgetAnnotation select:hover { +.annotationLayer .choiceWidgetAnnotation select:hover, +.annotationLayer .buttonWidgetAnnotation.checkBox input:hover, +.annotationLayer .buttonWidgetAnnotation.radioButton input:hover { border: 1px solid #000; } @@ -97,6 +103,14 @@ width: 115%; } +.annotationLayer .buttonWidgetAnnotation.checkBox input, +.annotationLayer .buttonWidgetAnnotation.radioButton input { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; +} + .annotationLayer .popupWrapper { position: absolute; width: 20em;