-
Notifications
You must be signed in to change notification settings - Fork 10.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Acroform checkbox and radio controls #7664
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good in general. I'm happy that you made this work with the new framework we're developing!
Aside from these review comments, I'd like to ask you to write unit tests for this. You can find these in test/unit/annotation_layer_spec.js
. When this is done, I may be able to create a reference test for this patch.
function ButtonWidgetAnnotation(params) { | ||
WidgetAnnotation.call(this, params); | ||
|
||
this.renderInteractiveForms = params.renderInteractiveForms; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you rebase to the current master branch, you'll have this automatically because of https://github.com/mozilla/pdf.js/blob/master/src/display/annotation_layer.js#L120, so you can remove this line.
AnnotationFieldFlag.NOTOGGLETOOFF); | ||
this.data.radio = this.hasFieldFlag(AnnotationFieldFlag.RADIO); | ||
this.data.pushbutton = this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); | ||
this.data.radiosInUnison = this.hasFieldFlag( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not appear to be used, so let's remove it for now to reduce the size of the patch.
@@ -76,6 +76,15 @@ AnnotationElementFactory.prototype = | |||
switch (fieldType) { | |||
case 'Tx': | |||
return new TextWidgetAnnotationElement(parameters); | |||
case 'Btn': | |||
if (!parameters.data.pushbutton) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of a pushbutton, we should add a warn
call about this being unsupported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this warning really necessary though, since it's going to be printed in the console regardless if forms are enabled or not?
Given that forms support is still some ways from being implemented, is it necessary to bother users about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps not really. I'm also fine with a comment such as // Push buttons are not implemented yet
so that we at least know in the code that something is still missing here.
* @alias CheckboxWidgetAnnotationElement | ||
*/ | ||
var CheckboxWidgetAnnotationElement = | ||
(function CheckboxWidgetAnnotationElementClosure() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent this with two more spaces and remove the newline on the next line.
|
||
Util.inherit(CheckboxWidgetAnnotationElement, WidgetAnnotationElement, { | ||
/** | ||
* Render the text widget annotation's HTML element in the empty container. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkbox widget
|
||
Util.inherit(RadioButtonWidgetAnnotationElement, WidgetAnnotationElement, { | ||
/** | ||
* Render the text widget annotation's HTML element in the empty container. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
radio button widget
*/ | ||
_toggle: function RadioButtonWidgetAnnotationElement_toggle(element) { | ||
element.className = element.className && | ||
!this.data.noToggleToOff ?'' : 'checked'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a space after ?
. I find it a bit hard to understand what this line does exactly. Can we break it up a bit?
var otherKids = this.layer.querySelectorAll( | ||
'[data-parent-name="' + this.data.fullName.split('`')[0] + '"]'); | ||
// Unselect other kids | ||
for(var i=0; i<otherKids.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change this to for (var i = 0, ii = otherKids.length; i < ii; i++) {
to cache the length.
// Unselect other kids | ||
for(var i=0; i<otherKids.length; i++) { | ||
if (otherKids[i] !== element) { | ||
otherKids[i].className = ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My previous comment about the class name applies here too. It's a bit confusing why we do that.
@@ -93,6 +93,60 @@ | |||
width: 115%; | |||
} | |||
|
|||
.annotationLayer .checkboxWidgetAnnotation div, | |||
.annotationLayer .radioButtonWidgetAnnotation div { | |||
background-color: rgba(0, 54, 255, 0.13); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this styling also how it looks in Adobe Reader? If not, I'd prefer we keep it the same as Adobe Reader for now as that's what most users will expect. That's also what we did for text widgets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the color used by you guys in .textWidgetAnnotation
. It looks pretty much like Adobe Reader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this patch using <div>
s and a lot of complicated CSS instead of just standard HTML form elements (with appropriate styling)?
One obvious downside to the current approach is that the elements added in the patch isn't accessible as the other ones. E.g. note how it's not possible to tab to them, which using proper HTML form elements would fix.
Also, before we start building upon it, we really ought to go through and attempt to clean-up/refactor this code:
Lines 640 to 670 in 6c263c1
// Building the full field name by collecting the field and | |
// its ancestors 'T' data and joining them using '.'. | |
var fieldName = []; | |
var namedItem = dict; | |
var ref = params.ref; | |
while (namedItem) { | |
var parent = namedItem.get('Parent'); | |
var parentRef = namedItem.getRaw('Parent'); | |
var name = namedItem.get('T'); | |
if (name) { | |
fieldName.unshift(stringToPDFString(name)); | |
} else if (parent && ref) { | |
// The field name is absent, that means more than one field | |
// with the same name may exist. Replacing the empty name | |
// with the '`' plus index in the parent's 'Kids' array. | |
// This is not in the PDF spec but necessary to id the | |
// the input controls. | |
var kids = parent.get('Kids'); | |
var j, jj; | |
for (j = 0, jj = kids.length; j < jj; j++) { | |
var kidRef = kids[j]; | |
if (kidRef.num === ref.num && kidRef.gen === ref.gen) { | |
break; | |
} | |
} | |
fieldName.unshift('`' + j); | |
} | |
namedItem = parent; | |
ref = parentRef; | |
} | |
data.fullName = fieldName.join('.'); |
Finally, this patch is currently missing both unit and reference tests!
@timvandermeij I'll take your remarks into account and add unit tests. |
I have been looking into this a bit more and I now realize why you chose |
My suspicion is that using hidden elements will unfortunately do nothing to alleviate the keyboard accessibility problems! And it'd also be great if the solution isn't unnecessarily complex. |
Right, it's a bit unfortunate, but the current state of the web platform won't really let us do what we want. Using @benweet Could you check if using |
943b31c
to
3453bbc
Compare
Here is a new commit with hidden inputs. Keyboard accessibility works fine, though I didn't manage to have the highlighting on focus. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[...] though I didn't manage to have the highlighting on focus.
I'm afraid that this is something that will need to be addressed somehow, if this current solution is to work, since it's not particularily useful to be able to tab to the element if the user cannot see that it has focus.
@@ -43,7 +43,7 @@ | |||
|
|||
.annotationLayer .textWidgetAnnotation input, | |||
.annotationLayer .textWidgetAnnotation textarea { | |||
background-color: rgba(0, 54, 255, 0.13); | |||
background-color: rgb(222, 229, 255); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The background colour needs to be transparent, please undo this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mistake, sorry about that.
@@ -93,6 +93,64 @@ | |||
width: 115%; | |||
} | |||
|
|||
.annotationLayer .checkboxWidgetAnnotation label, | |||
.annotationLayer .radioButtonWidgetAnnotation label { | |||
background-color: rgb(222, 229, 255); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be the same colour as above, i.e. background-color: rgba(0, 54, 255, 0.13);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the pdf is pre-filled, the checked box is drawn in the canvas. I changed the color to be opaque so that the checked box is hidden behind the annotation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot reproduce this, please provide a PDF file that demonstrates this problem!
Without having seen the file, it sounds like it may be an issue related to Appearance
streams support in conjunction with forms, which is a difficult issue that we'll have to somehow fix before we can enable forms by default.
However, simply hiding the background in this way isn't going to be a working solution, so please change this for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a sample pdf: pptc153filled.pdf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I suspected, that's an issue with Apperance
streams, so please change this as I originally suggested since the proper solution lie elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just use an opaque color for the time being and keep this as a future enhancement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not try to fix this properly instead, by adding getOperatorList
overrides to the new class (in src/core/annotation.js
)?
Similar to the existing TextWidgetAnnotation
, but perhaps a bit simpler for now:
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);
}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works pretty good! I'll merge this tomorrow. Thanks
this.data.pushbutton = this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); | ||
var fieldValue = Util.getInheritableProperty(params.dict, 'V'); | ||
if (fieldValue) { | ||
// Must be a Name object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition doesn't actually agree with the comment, so change it to:
if (isName(fieldValue)) {
instead (and remove the comment since it's then superfluous).
width: 60%; | ||
} | ||
|
||
.annotationLayer .radioButtonWidgetAnnotation input:checked + label:before { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be some overlap between these rules and the ones above, could that be simplified somewhat?
|
||
this.data.noToggleToOff = this.hasFieldFlag( | ||
AnnotationFieldFlag.NOTOGGLETOOFF); | ||
this.data.radio = this.hasFieldFlag(AnnotationFieldFlag.RADIO); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PDF specification specifically mentions that the RADIO
flag cannot be set at the same time as the PUSHBUTTON
flag, but that is missing from the code here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to check that the document complies with the PDF spec? What does it bring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, PDF.js is a PDF viewer after all, so we most certainly want to try and follow the specification!
If experience has thought us anything, it's that PDF files found in the wild breaks the specification in all sorts of "interesting" ways, which often leads to bugs. So of course we always want to try to ensure that data present in a PDF file is valid!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. What do you suggest?
Note that I've already put a bit of logic in annotation_layer.js
that does some kind of compliancy check:
if (!parameters.data.pushbutton) {
if (parameters.data.radio) {
return new RadioButtonWidgetAnnotationElement(parameters);
} else {
return new CheckboxWidgetAnnotationElement(parameters);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main validation should happen in the core
layer, so wouldn't e.g. this be enough:
this.data.radio = this.hasFieldFlag(AnnotationFieldFlag.RADIO) &&
!this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON);
Added unit tests and CSS outline on focus. |
Let's remove the |
14d5ec9
to
657229a
Compare
Done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This patch looks much better! Let's wait until the other PR is merged and then address these comments. Thanks!
this.data.pushbutton = this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); | ||
this.data.radio = !this.data.pushbutton && | ||
this.hasFieldFlag(AnnotationFieldFlag.RADIO); | ||
var fieldValue = Util.getInheritableProperty(params.dict, 'V'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use this.data.fieldValue
here (and thus remove this line) after the other PR is merged, but that's just a note for now.
this.data.radio = !this.data.pushbutton && | ||
this.hasFieldFlag(AnnotationFieldFlag.RADIO); | ||
var fieldValue = Util.getInheritableProperty(params.dict, 'V'); | ||
if (isName(fieldValue)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if it's not a Name
? It happens very often that PDF files do not adhere to the specification entirely, so if they do not provide a Name
here, then this.data.fieldValue
will be used as-is after the other PR is merged. Should we make it empty in such a case?
@@ -76,6 +76,15 @@ AnnotationElementFactory.prototype = | |||
switch (fieldType) { | |||
case 'Tx': | |||
return new TextWidgetAnnotationElement(parameters); | |||
case 'Btn': | |||
if (!parameters.data.pushbutton) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps not really. I'm also fine with a comment such as // Push buttons are not implemented yet
so that we at least know in the code that something is still missing here.
element.checked = true; | ||
} | ||
this.container.appendChild(element); | ||
element = document.createElement('label'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add an empty line before this line to make this a bit more readable as a new block of code.
element.checked = true; | ||
} | ||
this.container.appendChild(element); | ||
element = document.createElement('label'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, let's add a newline above this line for readability.
}); | ||
|
||
it('should have proper flags', | ||
function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all occurrences in this file: move this to the previous line. We only use this if it doesn't fit on one line anymore.
]); | ||
|
||
var checkboxWidgetAnnotation = | ||
annotationFactory.create(xref, checkboxWidgetRef); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all occurrences in this file: remove two spaces from the indentation here. We indent everything with two spaces.
Everything that blocked this PR has landed, so it's now good to rebase and fix up, @benweet. Thanks. |
657229a
to
f2450ac
Compare
Fixed. Cheers! |
f2450ac
to
9f10e09
Compare
Closing as replaced by #7898, which contains this commit and follow-up commits so it can be merged. |
Added fake checkboxes and radio buttons with a pinch of css.
Tested against http://www.cic.gc.ca/english/passport/forms/pdf/pptc153.pdf on Chrome and Firefox: