From ba012c7a683bef9117ca3e5206c8949432d82113 Mon Sep 17 00:00:00 2001 From: benweet Date: Fri, 4 Nov 2016 12:01:42 +0000 Subject: [PATCH] Button widget annotations: implement checkboxes and radio buttons --- src/core/annotation.js | 55 +++++++++++++++++ src/display/annotation_layer.js | 97 ++++++++++++++++++++++++++++++ test/unit/annotation_layer_spec.js | 91 ++++++++++++++++++++++++++++ web/annotation_layer_builder.css | 62 +++++++++++++++++++ 4 files changed, 305 insertions(+) diff --git a/src/core/annotation.js b/src/core/annotation.js index 97f728621..615bc3f96 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,59 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { return TextWidgetAnnotation; })(); +var ButtonWidgetAnnotation = (function ButtonWidgetAnnotationClosure() { + function ButtonWidgetAnnotation(params) { + WidgetAnnotation.call(this, params); + + this.data.pushbutton = this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); + this.data.radio = !this.data.pushbutton && + this.hasFieldFlag(AnnotationFieldFlag.RADIO); + if (isName(this.data.fieldValue)) { + this.data.fieldValue = this.data.fieldValue.name; + } + // Get the value of the radio button + if (this.data.radio) { + // Generate a unique ID in case no value is found + this.data.buttonValue = Math.random().toString(16).slice(2); + var appearanceState = params.dict.get('AP'); + if (isDict(appearanceState)) { + var appearances = appearanceState.get('N'); + if (isDict(appearances) && appearances.has('Off')) { + var keys = appearances.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..b72c4d944 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -76,6 +76,17 @@ AnnotationElementFactory.prototype = switch (fieldType) { case 'Tx': return new TextWidgetAnnotationElement(parameters); + case 'Btn': + if (!parameters.data.pushbutton) { + if (parameters.data.radio) { + return new RadioButtonWidgetAnnotationElement(parameters); + } else { + return new CheckboxWidgetAnnotationElement(parameters); + } + } else { + warn('Unimplemented push button'); + } + break; case 'Ch': return new ChoiceWidgetAnnotationElement(parameters); } @@ -141,6 +152,7 @@ var AnnotationElement = (function AnnotationElementClosure() { var height = data.rect[3] - data.rect[1]; container.setAttribute('data-annotation-id', data.id); + container.setAttribute('data-annotation-name', data.fieldName); // Do *not* modify `data.rect`, since that will corrupt the annotation // position on subsequent calls to `_createContainer` (see issue 6804). @@ -531,6 +543,91 @@ 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 = 'checkboxWidgetAnnotation'; + + var element = document.createElement('input'); + element.type = 'checkbox'; + element.id = this.data.fieldName; + if (this.data.fieldValue && this.data.fieldValue !== 'Off') { + element.checked = true; + } + this.container.appendChild(element); + element = document.createElement('label'); + element.htmlFor = this.data.fieldName; + 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 = 'radioButtonWidgetAnnotation'; + + var element = document.createElement('input'); + var id = this.data.fieldName + '.' + this.data.buttonValue; + element.type = 'radio'; + element.id = id; + element.name = this.data.fieldName; + element.value = this.data.buttonValue; + if (this.data.fieldValue === this.data.buttonValue) { + element.checked = true; + } + this.container.appendChild(element); + element = document.createElement('label'); + element.htmlFor = id; + this.container.appendChild(element); + + return this.container; + }, + }); + + return RadioButtonWidgetAnnotationElement; +})(); + + /** * @class * @alias ChoiceWidgetAnnotationElement */ diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js index f5f3a1bb4..5bdec7f9f 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -869,6 +869,97 @@ describe('Annotation layer', function() { }); }); + describe('CheckboxWidgetAnnotation', function() { + var checkboxWidgetDict; + + beforeEach(function (done) { + checkboxWidgetDict = new Dict(); + checkboxWidgetDict.set('Type', Name.get('Annot')); + checkboxWidgetDict.set('Subtype', Name.get('Widget')); + checkboxWidgetDict.set('FT', Name.get('Btn')); + + done(); + }); + + afterEach(function () { + checkboxWidgetDict = null; + }); + + it('should have proper flags', + function() { + var checkboxWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: checkboxWidgetRef, data: checkboxWidgetDict, } + ]); + + var checkboxWidgetAnnotation = + annotationFactory.create(xref, checkboxWidgetRef); + expect(checkboxWidgetAnnotation.data.radio).toEqual(false); + expect(checkboxWidgetAnnotation.data.pushbutton).toEqual(false); + expect(checkboxWidgetAnnotation.data.fieldValue).toEqual(null); + }); + + it('should have a proper value', + function() { + checkboxWidgetDict.set('V', Name.get('1')); + + var checkboxWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: checkboxWidgetRef, data: checkboxWidgetDict, } + ]); + + var checkboxWidgetAnnotation = + annotationFactory.create(xref, checkboxWidgetRef); + expect(checkboxWidgetAnnotation.data.fieldValue).toEqual('1'); + }); + }); + + describe('RadioButtonWidgetAnnotation', function() { + var radioButtonWidgetDict; + + beforeEach(function (done) { + radioButtonWidgetDict = new Dict(); + radioButtonWidgetDict.set('Type', Name.get('Annot')); + radioButtonWidgetDict.set('Subtype', Name.get('Widget')); + radioButtonWidgetDict.set('FT', Name.get('Btn')); + radioButtonWidgetDict.set('Ff', AnnotationFieldFlag.RADIO); + + done(); + }); + + afterEach(function () { + radioButtonWidgetDict = null; + }); + + it('should have proper flags', + function() { + var radioButtonWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: radioButtonWidgetRef, data: radioButtonWidgetDict, } + ]); + + var radioButtonWidgetAnnotation = + annotationFactory.create(xref, radioButtonWidgetRef); + expect(radioButtonWidgetAnnotation.data.radio).toEqual(true); + expect(radioButtonWidgetAnnotation.data.pushbutton).toEqual(false); + expect(radioButtonWidgetAnnotation.data.fieldValue).toEqual(null); + }); + + it('should have a proper value', + function() { + radioButtonWidgetDict.set('V', Name.get('1')); + + var radioButtonWidgetRef = new Ref(124, 0); + var xref = new XRefMock([ + { ref: radioButtonWidgetRef, data: radioButtonWidgetDict, } + ]); + + var radioButtonWidgetAnnotation = + annotationFactory.create(xref, radioButtonWidgetRef); + expect(radioButtonWidgetAnnotation.data.fieldValue).toEqual('1'); + }); + }); + describe('ChoiceWidgetAnnotation', function() { var choiceWidgetDict; diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 8cff630f9..58afd7e3b 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -97,6 +97,68 @@ width: 115%; } +.annotationLayer .checkboxWidgetAnnotation label, +.annotationLayer .radioButtonWidgetAnnotation label { + background-color: rgba(0, 54, 255, 0.13); + border: 1px solid transparent; + box-sizing: border-box; + cursor: pointer; + height: 100%; + position: absolute; + width: 100%; +} + +.annotationLayer .checkboxWidgetAnnotation input, +.annotationLayer .radioButtonWidgetAnnotation input { + position: absolute; + left: -9999px; +} + +.annotationLayer .radioButtonWidgetAnnotation label { + border-radius: 50%; +} + +.annotationLayer .checkboxWidgetAnnotation label:hover, +.annotationLayer .radioButtonWidgetAnnotation label:hover, +.annotationLayer .checkboxWidgetAnnotation label:focus, +.annotationLayer .radioButtonWidgetAnnotation label:focus, +.annotationLayer .checkboxWidgetAnnotation input:focus + label, +.annotationLayer .radioButtonWidgetAnnotation input:focus + label { + border: 1px solid #000; +} + +.annotationLayer .checkboxWidgetAnnotation input:checked + label:before, +.annotationLayer .radioButtonWidgetAnnotation input:checked + label:before { + content: ''; + left: 50%; + top: 50%; + position: absolute; +} + +.annotationLayer .checkboxWidgetAnnotation input:checked + label:before { + border-bottom: 1px solid #000; + border-left: 1px solid #000; + height: 25%; + -webkit-transform: translate(-50%, -65%) rotateZ(-45deg); + -moz-transform: translate(-50%, -65%) rotateZ(-45deg); + -o-transform: translate(-50%, -65%) rotateZ(-45deg); + -ms-transform: translate(-50%, -65%) rotateZ(-45deg); + transform: translate(-50%, -65%) rotateZ(-45deg); + width: 60%; +} + +.annotationLayer .radioButtonWidgetAnnotation input:checked + label:before { + background-color: #000; + border-radius: 50%; + height: 50%; + -webkit-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + -o-transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + width: 50%; +} + .annotationLayer .popupWrapper { position: absolute; width: 20em;