File Coverage

blib/lib/Mojo/DOM/Role/Form.pm
Criterion Covered Total %
statement 29 29 100.0
branch 14 14 100.0
condition 21 21 100.0
subroutine 10 10 100.0
pod 1 1 100.0
total 75 75 100.0


line stmt bran cond sub pod time code
1             package Mojo::DOM::Role::Form;
2              
3 2     2   2513 use Mojo::Base -role;
  2         4  
  2         14  
4              
5             requires qw{ancestors at attr find matches selector tag val};
6              
7             sub target {
8 21     21 1 2339 my ($self, $submit) = (shift, shift);
9 21 100 100     62 return () if ($self->tag // '') ne 'form';
10             return ()
11 20 100 100     306 unless defined($submit = $self->at($submit || _form_default_submit($self)));
12 19 100       13483 return () if $submit->matches('[disabled]');
13 17   100     1915 my $method = uc($submit->attr('formmethod') || $self->attr('method') || 'GET');
14 17   100     499 my $action = $submit->attr('formaction') || $self->attr('action') || '#';
15 17   100     402 my $enctyp = $submit->attr('formenctype') || $self->attr('enctype') ||
16             'url-encoded';
17 17         470 return $method, $action, $enctyp;
18             }
19              
20             around val => sub {
21             my ($orig, $self, @args) = @_;
22             # "form"
23             return
24             $self->find('button, checkbox, input, radio, select, textarea')
25             ->map(sub {
26             my $is_image = !!$_->matches('input[type=image]');
27             # ignore disabled nodes
28             return () if _form_element_disabled($_);
29             # ignore those without name, unless image type
30             return () if !defined(my $name = $_->attr("name")) && !$is_image;
31             # only continue if the clickable element matches (synthesize click)
32             return () if _form_element_submits($_) && !$_->matches($_[1]);
33             # client only buttons ignored
34             return () if _form_element_client_only_button($_);
35             # simply return name => value for all but image types
36             return [$name => $_->val()] unless $is_image;
37             # synthesize image click
38             return _form_image_click($_, $name);
39             }, $args[0] || _form_default_submit($self))
40             ->reduce(sub {
41             my ($key, $value) = @$b;
42             $a->{$key} = defined $a->{$key} && defined($value)
43             ? [ ref($a->{$key}) ? (@{$a->{$key}}, $value) : ($a->{$key}, $value) ]
44             : $value;
45             $a
46             }, {})
47             if (my $tag = $self->tag) eq 'form';
48              
49             # "option"
50             return $self->{value} // $self->text if $tag eq 'option';
51              
52             # "input" ("type=checkbox" and "type=radio")
53             my $type = $self->{type} // '';
54             return $self->{value} // 'on'
55             if $tag eq 'input' && ($type eq 'radio' || $type eq 'checkbox');
56              
57             # "textarea", "input" or "button". Give input[type=submit] default value
58             return (
59             $tag eq 'textarea'
60             ? $self->text
61             : ($self->matches('input[type=submit]')
62             ? $self->{value} || 'Submit'
63             : $self->{value})) if $tag ne 'select';
64              
65             # "select"
66             my $v = $self->find('option:checked:not([disabled])')
67             ->grep(sub { !$_->ancestors('optgroup[disabled]')->size })->map('val');
68             return exists $self->{multiple} ? $v->size ? $v->to_array : undef : $v->last;
69             };
70              
71             #
72             # internal
73             #
74              
75             sub _form_default_submit {
76             # filter for those submittable nodes
77 454     454   22501 return shift->find('*')->grep(sub { !!$_->_form_element_submits; })
78             # only the first continues, save some cycles
79 30     30   660 ->tap(sub { splice @$_, 1; })
80             ->map(sub {
81             # $_->selector;
82             # get the selector and relativise to form
83 23     23   337 (my $s = $_->selector) =~ s/^.*form[^>]*>\s//;
84 23         3857 return $s;
85 30   100 30   28531 })->first || '';
86             }
87              
88             sub _form_element_client_only_button {
89 222     222   358 my $s = 'input[type=button], button:matches([type=button], [type=reset])';
90 222         570 return !!$_[0]->matches($s);
91             }
92              
93             sub _form_element_disabled {
94 441 100   441   75331 return 1 if $_[0]->matches('[disabled]');
95 399 100 100     40830 return 1 if $_[0]->ancestors('fieldset[disabled]')->size &&
96             !$_[0]->ancestors('fieldset legend:first-child')->size;
97 396         207154 return 0;
98             }
99              
100             sub _form_element_submits {
101 719     719   1065 my $s = join ', ', 'button:not([type=button], [type=reset])',
102             'button', # submit is the default
103             'input:matches([type=submit], [type=image])';
104 719 100 100     1398 return 1 if $_[0]->matches($s) && !_form_element_disabled($_[0]);
105 588         288537 return 0;
106             }
107              
108             sub _form_image_click {
109 6     6   18 my ($self, $name) = (shift, shift);
110 6   100     14 my ($x, $y) = map { int(rand($self->attr($_) || 1)) + 1 } qw{width height};
  12         199  
111             # x and y if no name
112 6 100       105 return ([x => $x], [y => $y]) unless $name;
113             # named x and y, with name
114 3         20 return (["$name.x" => $x], ["$name.y" => $y]);
115             }
116              
117             1;
118              
119             =encoding utf8
120              
121             =head1 NAME
122              
123             Mojo::DOM::Role::Form - Form data extraction
124              
125             =head1 SYNOPSIS
126              
127             # description
128             my $obj = Mojo::DOM::Role::Form->new();
129             $obj->target('#submit-id');
130              
131             =head1 DESCRIPTION
132              
133             L based role to compose additional form data extraction methods into
134             L.
135              
136             =head1 METHODS
137              
138             L implements the following methods.
139              
140             =head2 target
141              
142             # result
143             $obj->target
144              
145             Explain what the L does.
146              
147             =head1 AUTHOR
148              
149             =cut