Class: Dommy::HTMLSelectElement

Inherits:
HTMLElement show all
Defined in:
lib/dommy/html_elements.rb

Overview

<select> — exposes value (selected option's value), options, selectedIndex, and dispatches change events. Minimal compared to happy-dom's full HTMLSelectElement, but covers common test cases.

Constant Summary

Constants inherited from Element

Element::ADJACENT_POSITIONS, Element::APPROX_CHAR_PX, Element::APPROX_LINE_PX, Element::ATTRIBUTE_NODE, Element::CDATA_SECTION_NODE, Element::COMMENT_NODE, Element::DOCUMENT_FRAGMENT_NODE, Element::DOCUMENT_NODE, Element::DOCUMENT_POSITION_CONTAINED_BY, Element::DOCUMENT_POSITION_CONTAINS, Element::DOCUMENT_POSITION_DISCONNECTED, Element::DOCUMENT_POSITION_FOLLOWING, Element::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, Element::DOCUMENT_POSITION_PRECEDING, Element::DOCUMENT_TYPE_NODE, Element::ELEMENT_NODE, Element::HTML_NAMESPACE, Element::INLINE_TAGS, Element::PROCESSING_INSTRUCTION_NODE, Element::REFLECTED_TOKEN_LIST_HOSTS, Element::SHADOW_HOST_TAGS, Element::SVG_NAMESPACE, Element::TEXT_NODE

Constants included from Node

Node::ATTRIBUTE_NODE, Node::CDATA_SECTION_NODE, Node::COMMENT_NODE, Node::DOCUMENT_FRAGMENT_NODE, Node::DOCUMENT_NODE, Node::DOCUMENT_POSITION_CONTAINED_BY, Node::DOCUMENT_POSITION_CONTAINS, Node::DOCUMENT_POSITION_DISCONNECTED, Node::DOCUMENT_POSITION_FOLLOWING, Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, Node::DOCUMENT_POSITION_PRECEDING, Node::DOCUMENT_TYPE_NODE, Node::ELEMENT_NODE, Node::HTML_NAMESPACE, Node::PROCESSING_INSTRUCTION_NODE, Node::TEXT_NODE, Node::XMLNS_NAMESPACE, Node::XML_NAMESPACE

Instance Attribute Summary

Attributes inherited from Element

#document

Instance Method Summary collapse

Methods inherited from HTMLElement

#case_sensitive_attribute_names?, #disabled_by_ancestor_fieldset?, #labels_node_list, #parse_non_negative_reflected, #set_non_negative_reflected

Methods included from Internal::ReflectedAttributes

included

Methods inherited from Element

#[], #[]=, #__dommy_backend_node__, #__internal_approx_box, #__internal_case_sensitive_attribute_names__?, #__internal_created_namespace__, #__internal_normalize_attr_key__, #__internal_set_namespace__, #__internal_shadow_root__, #__js_attribute_snapshot__, #__run_click_activation_behavior__, #__test_scroll_log__, #access_key_label, #accessibility_tree, #activation_behavior, #activation_target, #activation_target?, #after, #anchor_href, #animate, #approximate_layout?, #aria_content_attr, #aria_element_attr, #aria_element_get, #aria_element_set, #aria_elements_attr, #aria_elements_current, #aria_elements_get, #aria_elements_set, #aria_find_in_root, #aria_get, #aria_ref_in_valid_scope?, #aria_set, #aria_snapshot, #assigned_slot, #at_xpath, #attach_shadow, #attributes, #base_uri, #before, #blur, #child_element_count, #child_nodes, #children, #class_list, #class_name, #class_name=, #clear_aria_element_ref_for, #click, #clone_node, #closest, #computed_description, #computed_label, #computed_role, #contains?, #dataset, #element_prefix, #equal_node?, #first_child, #first_element_child, #focus, #get_animations, #get_attribute, #get_attribute_names, #get_attribute_node, #get_attribute_node_ns, #get_attribute_ns, #get_bounding_client_rect, #get_client_rects, #get_elements_by_class_name, #get_elements_by_tag_name, #get_elements_by_tag_name_ns, #get_html, #get_inner_html, #has_attribute?, #has_attribute_ns?, #has_attributes?, #has_child_nodes?, #hide_popover, #id, #id=, #initialize, #inner_html, #inner_html=, #insert_adjacent_element, #insert_adjacent_html, #insert_adjacent_text, #insert_before, #insertion_parent!, #is_connected?, #last_child, #last_element_child, #live_child_nodes, #local_name, #mark_fragment_scripts_started, #matches?, #namespace_uri, #next_element_sibling, #next_sibling, #on, #outer_html, #outer_html=, #owner_document, #parent_element, #parent_node, #path, #pre_click_activation_state, #previous_element_sibling, #previous_sibling, #query_selector, #query_selector_all, #reflected_attr_name, #reflected_token_list, #remove, #remove_attribute, #remove_attribute_node, #remove_attribute_ns, #remove_child, #replace_child, #replace_with_nodes, #request_fullscreen, #restore_pre_click_activation, #role, #role=, #root_node, #run_post_click_activation, #same_node?, #set_attribute, #set_attribute_node, #set_attribute_ns, #shadow_root, #show_popover, #slot, #slot=, #style, #tag_name, #text_content, #text_content=, #to_s, #toggle_attribute, #toggle_popover, #translate_mode?, #validate_adjacent_document_insert!, #with_selector_errors, #xpath

Methods included from Bridge::Methods

included

Methods included from Internal::ParentNode

#append, #append_child, #normalize, #prepend, #replace_children

Methods included from Internal::ChildNode

#child_node_after, #child_node_before, #child_node_replace_with, #nullable_dom_string, #validate_insert_before_ref!

Methods included from Node

#compare_document_position, #get_root_node, #is_default_namespace, #is_equal_node, #is_same_node, #lookup_namespace_uri, #lookup_prefix

Methods included from EventTarget

#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_event_parent__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler

Constructor Details

This class inherits a constructor from Dommy::Element

Instance Method Details

#__display_selected__Object

The option(s) that display as selected, applying the selectedness rules at read time: a single-select shows the LAST option whose selectedness is true (last-selected wins), or — if none is — its first option ("ask for reset"); a multiple select shows every selected option (or none).



2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
# File 'lib/dommy/html_elements.rb', line 2811

def __display_selected__
  opts = options.to_a
  chosen = opts.select { |o| o.respond_to?(:selected) && o.selected }
  if multiple
    chosen
  elsif !chosen.empty?
    # Single-select: the most recently property-selected option wins over an
    # attribute-selected one; otherwise the last selected in document order.
    dirty = chosen.select { |o| o.respond_to?(:__selectedness_dirty__) && o.__selectedness_dirty__ }
    [(dirty.empty? ? chosen : dirty).last]
  elsif !opts.empty?
    [opts.first]
  else
    []
  end
end

#__js_call__(method, args) ⇒ Object



2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
# File 'lib/dommy/html_elements.rb', line 2979

def __js_call__(method, args)
  case method
  when "item"
    item(args[0])
  when "namedItem"
    named_item(args[0])
  when "add"
    add(args[0], args[1])
  when "remove"
    # HTMLSelectElement.remove(index) removes an option; with no argument it
    # is ChildNode.remove() (removes the <select> itself).
    args.empty? ? super : remove_option(args[0])
  when "checkValidity"
    check_validity
  when "reportValidity"
    report_validity
  when "setCustomValidity"
    set_custom_validity(args[0])
  else
    super
  end
end

#__js_get__(key) ⇒ Object



2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
# File 'lib/dommy/html_elements.rb', line 2926

def __js_get__(key)
  case key
  when "options"
    options
  when "length"
    length
  when "value"
    value
  when "size"
    size
  when "selectedIndex"
    selected_index
  when "selectedOptions"
    selected_options
  when "form"
    form
  when "labels"
    labels
  when "type"
    type
  when "validity"
    validity
  when "willValidate"
    will_validate
  when "validationMessage"
    validation_message
  else
    # Indexed getter: `select[i]` is the option at index i (WebIDL).
    return item(key) if key.is_a?(Integer)
    return item(key.to_i) if key.is_a?(String) && key.match?(/\A\d+\z/)

    super
  end
end

#__js_set__(key, val) ⇒ Object



2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
# File 'lib/dommy/html_elements.rb', line 2961

def __js_set__(key, val)
  case key
  when "value"
    self.value = val
  when "selectedIndex"
    self.selected_index = val
  when "length"
    self.length = val
  else
    # Indexed setter: `select[i] = option` delegates to the options
    # collection's WebIDL "set an indexed property" algorithm.
    return options.__set_indexed__(key.to_i, val) if key.is_a?(Integer) || (key.is_a?(String) && key.match?(/\A\d+\z/))

    super
  end
end

#add(option, before = nil) ⇒ Object

select.add(option, before) — appends or inserts before before.



2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
# File 'lib/dommy/html_elements.rb', line 2859

def add(option, before = nil)
  return nil unless option.respond_to?(:__dommy_backend_node__)

  if before.respond_to?(:__dommy_backend_node__)
    insert_before(option, before)
  else
    append_child(option)
  end

  nil
end

#check_validityObject



2911
2912
2913
2914
2915
# File 'lib/dommy/html_elements.rb', line 2911

def check_validity
  ok = !will_validate || validity.valid
  dispatch_event(Event.new("invalid", "bubbles" => false, "cancelable" => true)) unless ok
  ok
end

#formObject



2803
2804
2805
# File 'lib/dommy/html_elements.rb', line 2803

def form
  closest("form")
end

#item(i) ⇒ Object

select.item(i) — returns the option at index i.



2854
2855
2856
# File 'lib/dommy/html_elements.rb', line 2854

def item(i)
  options[i.to_i]
end

#labelsObject



2883
2884
2885
2886
2887
# File 'lib/dommy/html_elements.rb', line 2883

def labels
  return [] if id.empty?

  @document.query_selector_all("label[for='#{id}']")
end

#lengthObject



2788
2789
2790
# File 'lib/dommy/html_elements.rb', line 2788

def length
  options.size
end

#length=(n) ⇒ Object

select.length = n resizes the options list (delegates to the collection): shrinks by removing trailing options, grows by appending blank ones.



2794
2795
2796
# File 'lib/dommy/html_elements.rb', line 2794

def length=(n)
  options.length = n
end

#named_item(name) ⇒ Object

select.namedItem(name) — the first option whose id or name matches.



2799
2800
2801
# File 'lib/dommy/html_elements.rb', line 2799

def named_item(name)
  options.named_item(name)
end

#optionsObject

options — all

). Live HTMLOptionsCollection (HTMLCollection + add/remove/selectedIndex/length= helpers).


2773
2774
2775
2776
2777
2778
# File 'lib/dommy/html_elements.rb', line 2773

def options
  el = self
  HTMLOptionsCollection.new(self) do
    el.__dommy_backend_node__.css("option").map { |n| el.document.wrap_node(n) }.compact
  end
end

#remove_option(i) ⇒ Object

select.remove(i) — removes the option at index i. (Note: also inherits remove() from ChildNode for self-removal; spec lets both forms coexist via overloading.)



2874
2875
2876
2877
2878
2879
2880
2881
# File 'lib/dommy/html_elements.rb', line 2874

def remove_option(i)
  idx = i.to_i
  # An index out of range (including a negative one) is a no-op — NOT Ruby's
  # from-the-end negative indexing.
  return if idx.negative? || idx >= options.length

  options[idx]&.remove
end

#report_validityObject



2917
2918
2919
# File 'lib/dommy/html_elements.rb', line 2917

def report_validity
  check_validity
end

#selected_indexObject



2828
2829
2830
2831
2832
2833
2834
# File 'lib/dommy/html_elements.rb', line 2828

def selected_index
  opts = options.to_a
  sel = __display_selected__.first
  return -1 unless sel

  opts.find_index { |o| o.__dommy_backend_node__.equal?(sel.__dommy_backend_node__) } || -1
end

#selected_index=(i) ⇒ Object



2836
2837
2838
# File 'lib/dommy/html_elements.rb', line 2836

def selected_index=(i)
  options.to_a.each_with_index { |o, idx| o.selected = (idx == i.to_i) }
end

#selected_optionsObject

selectedOptions — live collection of options with selected attribute. When nothing is explicitly selected, browsers fall back to the first option for non-multiple selects.



2783
2784
2785
2786
# File 'lib/dommy/html_elements.rb', line 2783

def selected_options
  el = self
  @selected_options ||= HTMLCollection.new { el.__display_selected__ }
end

#set_custom_validity(msg) ⇒ Object



2921
2922
2923
2924
# File 'lib/dommy/html_elements.rb', line 2921

def set_custom_validity(msg)
  @custom_validity_message = msg.to_s
  nil
end

#sizeObject

Own js_call methods, on top of Element's.



2766
2767
2768
# File 'lib/dommy/html_elements.rb', line 2766

def size
  @__node__["size"].to_s.to_i
end

#typeObject



2889
2890
2891
# File 'lib/dommy/html_elements.rb', line 2889

def type
  multiple ? "select-multiple" : "select-one"
end

#validation_messageObject



2901
2902
2903
2904
2905
2906
2907
2908
2909
# File 'lib/dommy/html_elements.rb', line 2901

def validation_message
  return "" unless will_validate

  msg = (@custom_validity_message || "").to_s
  return msg unless msg.empty?
  return "Please select an item in the list." if validity.value_missing

  ""
end

#validityObject



2893
2894
2895
# File 'lib/dommy/html_elements.rb', line 2893

def validity
  @__validity ||= ValidityState.new(self)
end

#valueObject

value of the select = value of the (displayed) selected option, or "".



2841
2842
2843
2844
# File 'lib/dommy/html_elements.rb', line 2841

def value
  sel = __display_selected__.first
  sel ? sel.value.to_s : ""
end

#value=(new_value) ⇒ Object



2846
2847
2848
2849
2850
2851
# File 'lib/dommy/html_elements.rb', line 2846

def value=(new_value)
  opts = options.to_a
  target = opts.find { |o| o.value.to_s == new_value.to_s }
  opts.each { |o| o.selected = false }
  target.selected = true if target
end

#will_validateObject



2897
2898
2899
# File 'lib/dommy/html_elements.rb', line 2897

def will_validate
  !reflected_boolean("disabled") && !disabled_by_ancestor_fieldset? && closest("datalist").nil?
end