The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

##----------------------------------------------------------------------------
## HTML Object - ~/lib/HTML/Object/DOM/Element/Select.pm
## Version v0.2.0
## Copyright(c) 2021 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2021/12/23
## Modified 2022/09/18
## All rights reserved
##
##
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
BEGIN
{
use strict;
use warnings;
use vars qw( $VERSION );
our $VERSION = 'v0.2.0';
};
use strict;
sub init
{
my $self = shift( @_ );
$self->{_init_strict_use_sub} = 1;
$self->SUPER::init( @_ ) || return( $self->pass_error );
$self->{tag} = 'select' if( !CORE::length( "$self->{tag}" ) );
$self->{options} = [];
$self->{_select_reset} = 1;
my $callback = sub
{
my $def = shift( @_ );
# Our children were modified from outside our package.
# We need to check if it affects our rows and reset the cache accordingly
unless( $def->{caller}->[0] eq ref( $self ) ||
$def->{caller}->[0] eq 'HTML::Object::DOM::Element::Select' )
{
$self->reset(1);
}
return(1);
};
$self->children->callback( add => $callback );
$self->children->callback( remove => $callback );
return( $self );
}
sub add
{
my $self = shift( @_ );
return( $self->error({
message => sprintf( "At least 1 argument is required, but only %d was provided.", scalar( @_ ) ),
class => 'HTML::Object::SyntaxError',
}) ) if( scalar( @_ ) < 1 );
my( $elem, $pos ) = @_;
return( $self->error({
message => "Element provided is neither a HTML::Object::DOM::Element::Option object nor a HTML::Object::DOM::Element::OptGroup object.",
class => 'HTML::Object::TypeError',
}) ) if( !$self->_is_a( $elem => 'HTML::Object::DOM::Element::Option' ) && !$self->_is_a( $elem => 'HTML::Object::DOM::Element::OptGroup' ) );
my $options = $self->options;
my $size = $options->size;
if( defined( $pos ) && CORE::length( $pos ) )
{
if( !$self->_is_a( $elem => 'HTML::Object::DOM::Element::Option' ) &&
!$self->_is_a( $elem => 'HTML::Object::DOM::Element::OptGroup' ) &&
!$self->_is_integer( $pos ) )
{
return( $self->error({
message => "The offset position before which to insert the element, if provided, must be an object or an integer, but I got '" . overload::StrVal( $pos ) . "'.",
class => 'HTML::Object::TypeError',
}) );
}
elsif( $self->_is_a( $elem => 'HTML::Object::DOM::Element::Option' ) ||
$self->_is_a( $elem => 'HTML::Object::DOM::Element::OptGroup' ) )
{
# Check if it is an ancestor, as per the specifications
my $lineage = $self->lineage;
my $lineagePos = $lineage->pos( $elem );
if( defined( $lineagePos ) )
{
return( $self->error({
message => "Element to be added to this select element is an ancestor.",
class => 'HTML::Object::HierarchyRequestError',
}) );
}
my $tmpPos = $options->pos( $elem );
if( defined( $tmpPos ) )
{
$pos = $tmpPos;
}
else
{
undef( $pos );
}
}
else
{
if( $pos < 0 )
{
$pos = $pos + $size;
}
elsif( $pos > $size )
{
undef( $pos );
}
}
}
$elem->detach;
$elem->parent( $self );
my $children = $self->children;
if( defined( $pos ) && CORE::length( $pos ) )
{
my $kid = $options->index( $pos );
my $real_pos = $children->pos( $kid );
$children->splice( $real_pos, 0, $elem );
}
else
{
$children->push( $elem );
}
$self->reset(1);
return( $self );
}
# Note: property autofocus
sub autofocus : lvalue { return( shift->_set_get_property( 'autofocus', @_ ) ); }
sub blur { return; }
# Note: method checkValidity is inherited
# Note: property disabled is inherited
sub focus { return; }
# Note: property form read-only is inherited
sub item { return( shift->options->index( @_ ) ); }
# Note: property labels read-only is inherited
# Note: property length
sub length : lvalue { return( shift->_set_get_property( 'length', @_ ) ); }
# Note: property multiple
sub multiple : lvalue { return( shift->_set_get_property( 'multiple', @_ ) ); }
# Note: property name is inherited
# Note: method namedItem is inherited from HTML::Object::DOM::Collection
sub namedItem { return( shift->options->namedItem( @_ ) ); }
sub onchange : lvalue { return( shift->on( 'change', @_ ) ); }
sub oninput : lvalue { return( shift->on( 'input', @_ ) ); }
# Note: property options read-only
# sub options { return( shift->_set_get_object( 'options', 'HTML::Object::DOM::Element::OptionsCollection', @_ ) ); }
sub options
{
my $self = shift( @_ );
return( $self->{_select_options} ) if( $self->{_select_options} && !$self->_is_select_reset );
my $list = $self->children->grep(sub{ $self->_is_a( $_ => 'HTML::Object::DOM::Element::Option' ) });
# The content of the collection is refreshed, but the collection object itself does not change, so the user can poll it
unless( $self->{_select_options} )
{
$self->_load_class( 'HTML::Object::DOM::Element::OptionsCollection' ) || return( $self->pass_error );
$self->{_select_options} = HTML::Object::DOM::Element::OptionsCollection->new ||
return( $self->pass_error( HTML::Object::DOM::Element::OptionsCollection->error ) );
}
$self->{_select_options}->set( $list );
$self->_remove_select_reset;
return( $self->{_select_options} );
}
sub remove
{
my $self = shift( @_ );
my $pos = shift( @_ );
return( $self->error({
message => "Index value provided ($pos) is not an integer",
class => 'HTML::Object::IndexSizeError',
}) ) if( !defined( $pos ) || !CORE::length( "$pos" ) || !$self->_is_integer( $pos ) );
my $options = $self->options;
return( $self->error({
message => "Index value provided ($pos) is greater than the zero-based total number of options.",
class => 'HTML::Object::IndexSizeError',
}) ) if( $pos > $options->size );
my $elem = $options->index( $pos );
my $real_pos = $self->children->pos( $elem );
return( $self->error({
message => "Could not find the real index position of element found at offset '$pos'.",
class => 'HTML::Object::HierarchyRequestError',
}) ) if( !defined( $real_pos ) );
$self->children->splice( $real_pos, 1 );
$elem->parent( undef );
$self->reset(1);
return( $elem );
}
# Note: method reportValidity is inherited
# Note: property required is inherited
sub reset
{
my $self = shift( @_ );
if( scalar( @_ ) )
{
$self->_reset_select;
# Force the rebuilding of the collection of selected options
CORE::delete( $self->{_selected_options} );
CORE::delete( $self->{selectedindex} );
return( $self->SUPER::reset( @_ ) );
}
return( $self );
}
# Note: property selectedIndex
# This is called by the HTML::Object::DOM::Element::Option when selected
# sub selectedIndex : lvalue { return( shift->_set_get_number( 'selectedindex', @_ ) ); }
sub selectedIndex : lvalue { return( shift->_lvalue({
set => sub
{
my( $self, $val ) = @_;
return( $self->error({
message => "Index value provided is not an integer.",
class => 'HTML::Object::TypeError',
}) ) if( !$self->_is_integer( $val ) );
my $options = $self->options;
return( $self->error({
message => "Index value provided is greater than the zero-based total number (" . $options->size . ") of options available.",
class => 'HTML::Object::IndexSizeError',
}) ) if( $val > $options->size );
$self->{selectedindex} = $val;
my $elem = $options->index( $val );
return( $self->error({
message => "Somehow, the element found at position $val is not an HTML::Object::DOM::Element::Option object.",
class => 'HTML::Object::HierarchyRequestError',
}) ) if( !$self->_is_a( $elem => 'HTML::Object::DOM::Element::Option' ) );
$elem->defaultSelected = 1;
return( $elem );
},
get => sub
{
my $self = shift( @_ );
return( $self->{selectedindex} ) if( $self->{selectedindex} && !$self->_is_select_reset );
# Get all options
my $options = $self->options;
# Get all selected ones; could be empty
my $selected = $self->selectedOptions;
return if( $selected->is_empty );
# Get the first selected one
my $elem = $selected->index(0);
# Find its index position among all options
my $pos = $options->pos( $elem );
# and return it.
return( $self->{selectedindex} = $pos );
},
}, @_ ) ); }
# Note: property selectedOptions read-only
sub selectedOptions
{
my $self = shift( @_ );
return( $self->{_selected_options} ) if( $self->{_selected_options} && !$self->_is_select_reset );
my $list = $self->options->filter(sub{ $_->defaultSelected });
$self->children->for(sub
{
my( $i, $elem ) = @_;
});
# The content of the collection is refreshed, but the collection object itself does not change, so the user can poll it
unless( $self->{_selected_options} )
{
$self->_load_class( 'HTML::Object::DOM::Collection' ) || return( $self->pass_error );
$self->{_selected_options} = HTML::Object::DOM::Collection->new ||
return( $self->pass_error( HTML::Object::DOM::Collection->error ) );
}
$self->{_selected_options}->set( $list );
$self->_remove_select_reset;
return( $self->{_selected_options} );
}
# Note: method setCustomValidity is inherited
# Note: property size
sub size : lvalue { return( shift->_set_get_property( 'size', @_ ) ); }
# Note: property type read-only is inherited
# Note: property validationMessage read-only is inherited
# Note: property validity read-only is inherited
# Note: property value is inherited
# Note: property willValidate read-only is inherited
sub _is_select_reset { return( CORE::length( shift->{_select_reset} ) ); }
sub _remove_select_reset { return( CORE::delete( shift->{_select_reset} ) ); }
sub _reset_select
{
my $self = shift( @_ );
$self->{_select_reset}++;
# Force it to recompute
$self->options;
return( $self );
}
1;
# NOTE: POD
__END__
=encoding utf-8
=head1 NAME
HTML::Object::DOM::Element::Select - HTML Object DOM Select Class
=head1 SYNOPSIS
use HTML::Object::DOM::Element::Select;
my $select = HTML::Object::DOM::Element::Select->new ||
die( HTML::Object::DOM::Element::Select->error, "\n" );
=head1 VERSION
v0.2.0
=head1 DESCRIPTION
This interface represents a C<<select>> HTML Element. These elements also share all of the properties and methods of other HTML elements via the L<HTML::Object::DOM::Element> interface.
=head1 INHERITANCE
+-----------------------+ +---------------------------+ +-------------------------+ +----------------------------+ +------------------------------------+
| HTML::Object::Element | --> | HTML::Object::EventTarget | --> | HTML::Object::DOM::Node | --> | HTML::Object::DOM::Element | --> | HTML::Object::DOM::Element::Select |
+-----------------------+ +---------------------------+ +-------------------------+ +----------------------------+ +------------------------------------+
=head1 PROPERTIES
Inherits properties from its parent L<HTML::Object::DOM::Element>
=head2 autofocus
A boolean value reflecting the autofocus L<HTML attribute|HTML::Object::DOM::Attribute>, which indicates whether the control should have input focus when the page loads, unless the user overrides it, for example by typing in a different control. Only one form-associated element in a document can have this attribute specified.
Example:
<select id="mySelect" autofocus>
<option>Option 1</option>
<option>Option 2</option>
</select>
# Check if the autofocus attribute on the <select>
my $hasAutofocus = $doc->getElementById('mySelect')->autofocus;
=head2 disabled
A boolean value reflecting the disabled L<HTML attribute|HTML::Object::DOM::Attribute>, which indicates whether the control is disabled. If it is disabled, it does not accept clicks.
Example:
<label>
Allow drinks?
<input id="allow-drinks" type="checkbox" />
</label>
<label for="drink-select">Drink selection:</label>
<select id="drink-select" disabled>
<option value="1">Water</option>
<option value="2">Beer</option>
<option value="3">Pepsi</option>
<option value="4">Whisky</option>
</select>
my $allowDrinksCheckbox = $doc->getElementById( 'allow-drinks' );
my $drinkSelect = $doc->getElementById( 'drink-select' );
$allowDrinksCheckbox->addEventListener( change => sub
{
if( $event->target->checked )
{
$drinkSelect->disabled = 0; # false
}
else
{
$drinkSelect->disabled = 1; # true
}
}, { capture => 0});
=head2 form
Read-only.
An L<HTML::Object::DOM::Element::Form> referencing the form that this element is associated with. If the element is not associated with of a <form> element, then it returns C<undef>.
Example:
<form id="pet-form">
<label for="pet-select">Choose a pet</label>
<select name="pets" id="pet-select">
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="parrot">Parrot</option>
</select>
<button type="submit">Submit</button>
</form>
<label for="lunch-select">Choose your lunch</label>
<select name="lunch" id="lunch-select">
<option value="salad">Salad</option>
<option value="sandwich">Sandwich</option>
</select>
my $petSelect = $doc->getElementById( 'pet-select' );
my $petForm = $petSelect->form; # <form id="pet-form">
my $lunchSelect = $doc->getElementById( 'lunch-select' );
my $lunchForm = $lunchSelect->form; # undef
=head2 labels
Read-only.
A L<HTML::Object::DOM::NodeList> of L<label elements|HTML::Object::DOM::Element::Label> associated with the element.
Example:
<label id="label1" for="test">Label 1</label>
<select id="test">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
<label id="label2" for="test">Label 2</label>
window->addEventListener( DOMContentLoaded => sub
{
my $select = $doc->getElementById( 'test' );
for( my $i = 0; $i < $select->labels->length; $i++ )
{
say( $select->labels->[$i]->textContent ); # "Label 1" and "Label 2"
}
});
=head2 length
An unsigned long that reflects the number of L<option elements|HTML::Object::DOM::Element::Option> in this select element.
=head2 multiple
A boolean value reflecting the multiple L<HTML attribute|HTML::Object::DOM::Attribute>, which indicates whether multiple items can be selected.
=head2 name
A string reflecting the name L<HTML attribute|HTML::Object::DOM::Attribute>, containing the name of this control used by servers and DOM search functions.
=head2 options
Read-only.
An L<OptionsCollection|HTML::Object::DOM::Element::OptionsCollection> representing the set of L<option elements|HTML::Object::DOM::Element::Option> contained by this element.
Example:
<label for="test">Label</label>
<select id="test">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
window->addEventListener( DOMContentLoaded => sub
{
my $select = $doc->getElementById( 'test' );
for( my $i = 0; $i < $select->options->length; $i++ )
{
say( $select->options->[$i]->label ); # "Option 1" and "Option 2"
}
});
=head2 required
A boolean value reflecting the required L<HTML attribute|HTML::Object::DOM::Attribute>, which indicates whether the user is required to select a value before submitting the form.
=head2 selectedIndex
A long reflecting the index of the first selected L<option element|HTML::Object::DOM::Element::Option>. The value C<undef> indicates no element is selected.
Example:
<p id="p">selectedIndex: 0</p>
<select id="select">
<option selected>Option A</option>
<option>Option B</option>
<option>Option C</option>
<option>Option D</option>
<option>Option E</option>
</select>
my $selectElem = $doc->getElementById('select');
my $pElem = $doc->getElementById('p');
# When a new <option> is selected
$selectElem->addEventListener( change => sub
{
my $index = $selectElem->selectedIndex;
# Add that data to the <p>
$pElem->innerHTML = 'selectedIndex: ' . $index;
})
=head2 selectedOptions
Read-only.
An L<Collection|HTML::Object::DOM::Collection> representing the set of L<option elements|HTML::Object::DOM::Element::Option> that are selected.
=head2 size
A long reflecting the size L<HTML attribute|HTML::Object::DOM::Attribute>, which contains the number of visible items in the control. The default is 1, unless multiple is true, in which case it is 4.
=head2 type
Read-only.
A string represeting the form control's type. When multiple is true, it returns C<select-multiple>; otherwise, it returns C<select-one>.
=head2 validationMessage
Read-only.
A string representing a localized message that describes the validation constraints that the control does not satisfy (if any). This attribute is the empty string if the control is not a candidate for constraint validation (willValidate is false), or it satisfies its constraints.
=head2 validity
Read-only.
A L<ValidityState|HTML::Object::DOM::ValidityState> object reflecting the validity state that this control is in.
=head2 value
A string reflecting the value of the form control. Returns the value property of the first selected option element if there is one, otherwise the empty string.
=head2 willValidate
Read-only.
A boolean value that indicates whether the button is a candidate for constraint validation. It is false if any conditions bar it from constraint validation.
=head1 METHODS
Inherits methods from its parent L<HTML::Object::DOM::Element>
=head2 add
Provided with an C<item> and optionally an position offset C<before> which to insert the item and this adds an element to the collection of option elements for this select element.
=over 4
=item C<item> is an L<Option element|HTML::Object::DOM::Element::Option> or L<OptGroup element|HTML::Object::DOM::Element::OptGroup>
=item C<before> is optional and an element of the collection, or an index of type long, representing the item should be inserted before. If this parameter is C<undef> (or the index does not exist), the new element is appended to the end of the collection.
=back
Example:
my $sel = $doc->createElement( 'select' );
my $opt1 = $doc->createElement( 'option' );
my $opt2 = $doc->createElement( 'option' );
$opt1->value = 1;
$opt1->text = "Option: Value 1";
$opt2->value = 2;
$opt2->text = "Option: Value 2";
# No second argument; no 'before' argument
$sel->add( $opt1, undef );
# Equivalent to above
$sel->add( $opt2 );
Produces the following, conceptually:
<select>
<option value="1">Option: Value 1</option>
<option value="2">Option: Value 2</option>
</select>
=head2 blur
Under perl, of course, this does nothing.
Under JavaScript, this removes the input focus from this element. This method is now implemented on L<HTML::Object::DOM::Element>.
=head2 checkValidity
Checks whether the element has any constraints and whether it satisfies them. If the element fails its constraints, the browser fires a cancelable invalid event at the element (and returns false).
=head2 focus
Under perl, of course, this does nothing.
Under JavaScript, this gives input focus to this element. This method is now implemented on L<HTML::Object::DOM::Element>.
=head2 item
Gets an item from the options collection for this L<select element|HTML::Object::DOM::Select> by providing a zero-based index position.
Example:
<select id="myFormControl">
<option id="o1">Opt 1</option>
<option id="o2">Opt 2</option>
</select>
# Returns the OptionElement representing #o2
my $sel = $doc->getElementById( 'myFormControl' );
my $elem1 = $sel->item(1); # Opt 2
=head2 namedItem
Gets the item in the options collection with the specified name. The name string can match either the id or the name attribute of an option node.
Example:
<select id="myFormControl">
<option id="o1">Opt 1</option>
<option id="o2">Opt 2</option>
</select>
my $elem1 = $doc->getElementById( 'myFormControl' )->namedItem( 'o1' ); # Returns the OptionElement representing #o1
This is, in effect, a shortcut for C<$select->options->namedItem>
=head2 remove
Removes the element at the specified index (zero-based) from the options collection for this select element and return it.
Example:
<select id="existingList" name="existingList">
<option value="1">Option: Value 1</option>
<option value="2">Option: Value 2</option>
<option value="3">Option: Value 3</option>
</select>
my $sel = $doc->getElementById( 'existingList' );
my $removed = $sel->remove(1);
HTML is now:
<select id="existingList" name="existingList">
<option value="1">Option: Value 1</option>
<option value="3">Option: Value 3</option>
</select>
=head2 reportValidity
This method reports the problems with the constraints on the element, if any, to the user. If there are problems, it fires a cancelable invalid event at the element, and returns false; if there are no problems, it returns true.
=head2 setCustomValidity
Sets the custom validity message for the selection element to the specified message. Use the empty string to indicate that the element does not have a custom validity error.
=head1 EVENTS
Event listeners for those events can also be found by prepending C<on> before the event type:
C<click> event listeners can be set also with C<onclick> method:
$e->onclick(sub{ # do something });
# or as an lvalue method
$e->onclick = sub{ # do something };
Note that, under perl, almost no event are fired, but you can trigger them yourself.
=head2 change
Fires when the user selects an option, but since there is no user interaction, this event is fired when the C<selectedIndex> value changes, which you can change yourself.
Example:
<label>Choose an ice cream flavor:
<select class="ice-cream" name="ice-cream">
<option value="">Select One …</option>
<option value="chocolate">Chocolate</option>
<option value="sardine">Sardine</option>
<option value="vanilla">Vanilla</option>
</select>
</label>
<div class="result"></div>
my $selectElement = $doc->querySelector('.ice-cream');
$selectElement->addEventListener( change => sub
{
my $result = $doc->querySelector( '.$result' );
$result->textContent = "You like " . $event->target->value;
});
=head2 input
Fires when the value of an <input>, <select>, or <textarea> element has been changed.
=head2 reset
Reset the cache flag so that some data will be recomputed. The cache is design to avoid doing useless computing repeatedly when there is no change of data.
=head1 AUTHOR
Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
=head1 SEE ALSO
=head1 COPYRIGHT & LICENSE
Copyright(c) 2021 DEGUEST Pte. Ltd.
All rights reserved
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut