Class: Dommy::HTMLInputElement

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

Overview

<input> — covers the most-used form control surface.

Constant Summary collapse

SELECTION_TYPES =

Only these input types expose a variable-length text selection; the rest return null for the selection attributes and throw on the setters/methods.

%w[text search url tel password].freeze
VALID_FLOAT_RE =

WHATWG "valid floating-point number": no surrounding whitespace (unlike Ruby's Float()), optional sign, digits with optional fraction, optional exponent. Anything else — including " 1 " or "1e" — yields NaN.

/\A-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\z/

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 included from SubmitButtonActivation

#activation_behavior, #activation_target?

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, #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, #role, #role=, #root_node, #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

#__apply_cloning_state__(state) ⇒ Object



1281
1282
1283
1284
1285
1286
# File 'lib/dommy/html_elements.rb', line 1281

def __apply_cloning_state__(state)
  @__value = state[:value] if state.key?(:value)
  @__raw_value = state[:raw_value] if state.key?(:raw_value)
  @__checked = state[:checked] if state.key?(:checked)
  @__indeterminate = state[:indeterminate] if state.key?(:indeterminate)
end

#__cloning_state__Object

HTML "cloning steps" for input: the dirty value flag + value and the dirty checkedness flag + checkedness (plus indeterminate) — the user-modified state a clone must retain beyond the default* content attributes. Returns nil when the control is still pristine, so the walk skips it.



1272
1273
1274
1275
1276
1277
1278
1279
# File 'lib/dommy/html_elements.rb', line 1272

def __cloning_state__
  state = {}
  state[:value] = @__value unless @__value.nil?
  state[:raw_value] = @__raw_value unless @__raw_value.nil?
  state[:checked] = @__checked unless @__checked.nil?
  state[:indeterminate] = @__indeterminate unless @__indeterminate.nil?
  state.empty? ? nil : state
end

#__driver_set_files__(files_input) ⇒ Object

Test-only seam: set the input's file list directly. Accepts an array (wrapped in a FileList) or a FileList itself.



540
541
542
# File 'lib/dommy/html_elements.rb', line 540

def __driver_set_files__(files_input)
  @__files = files_input.is_a?(FileList) ? files_input : FileList.new(Array(files_input))
end

#__internal_set_checked_silently__(value) ⇒ Object

Set checkedness without re-running the group cascade (used while unchecking peers).



692
693
694
695
# File 'lib/dommy/html_elements.rb', line 692

def __internal_set_checked_silently__(value)
  @__checked = !!value
  @document&.__internal_bump_style_generation__
end

#__js_call__(method, args) ⇒ Object



1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
# File 'lib/dommy/html_elements.rb', line 1333

def __js_call__(method, args)
  case method
  when "select"
    select
  when "setSelectionRange"
    set_selection_range(args[0], args[1], args[2])
  when "setRangeText"
    set_range_text(args[0])
  when "stepUp"
    step_up(args[0])
  when "stepDown"
    step_down(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



1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
# File 'lib/dommy/html_elements.rb', line 1225

def __js_get__(key)
  case key
  when "type"
    type
  when "value"
    value
  when "checked"
    checked
  when "indeterminate"
    indeterminate
  when "readonly", "readOnly"
    readonly
  when "labels"
    labels
  when "form"
    form
  when "validity"
    validity
  when "willValidate"
    will_validate
  when "validationMessage"
    validation_message
  when "files"
    files
  when "selectionStart"
    selection_start
  when "selectionEnd"
    selection_end
  when "selectionDirection"
    selection_direction
  when "valueAsNumber"
    value_as_number
  when "maxLength"
    max_length
  when "minLength"
    min_length
  when "list"
    list
  else
    super
  end
end

#__js_set__(key, value) ⇒ Object



1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
# File 'lib/dommy/html_elements.rb', line 1300

def __js_set__(key, value)
  case key
  when "type"
    set_reflected_string("type", value)
  when "value"
    self.value = value
  when "valueAsNumber"
    self.value_as_number = value
  when "selectionStart"
    self.selection_start = value
  when "selectionEnd"
    self.selection_end = value
  when "selectionDirection"
    self.selection_direction = value
  when "checked"
    self.checked = value
  when "indeterminate"
    self.indeterminate = value
  when "readonly", "readOnly"
    self.readonly = value
  when "maxLength"
    self.max_length = value
  when "minLength"
    self.min_length = value
  else
    super
  end
end

#__submit_button__?Boolean

Returns:

  • (Boolean)


499
# File 'lib/dommy/html_elements.rb', line 499

def __submit_button__? = %w[submit image].include?(type) && !disabled

#allowed_value_stepObject

The allowed value step in valueAsNumber units, or nil for step="any" / a type with no stepping.



1153
1154
1155
1156
1157
1158
# File 'lib/dommy/html_elements.rb', line 1153

def allowed_value_step
  return nil unless numeric_value_type?

  step = step_base_value
  step.nil? ? nil : step * step_scale_factor
end

#apply_step(count) ⇒ Object

stepUp/stepDown throw when the type has no allowed value step: a type with no number representation, or step="any". Otherwise the value moves by count steps (in valueAsNumber units), clamped/aligned to the min & max.



872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
# File 'lib/dommy/html_elements.rb', line 872

def apply_step(count)
  unless numeric_value_type?
    raise DOMException::InvalidStateError, "stepUp/stepDown is not applicable to input type '#{type}'"
  end

  step = step_base_value
  if step.nil?
    raise DOMException::InvalidStateError, "stepUp/stepDown is not allowed when step is 'any'"
  end
  return if count.zero?

  allowed = step * step_scale_factor
  mn = step_boundary("min")
  mx = step_boundary("max")
  # A min above the max means no in-range value exists — do nothing.
  return if mn && mx && mn > mx

  before = value_as_number
  base = before.nan? ? (mn || 0.0) : before
  result = base + count * allowed

  step_base = mn || 0.0
  result = mx - (mx - step_base) % allowed if mx && result > mx
  result = mn + (step_base - mn) % allowed if mn && result < mn

  # Clamping must never move the value against the step direction (e.g. a
  # stepDown on a value already below min must not jump UP to min).
  unless before.nan?
    return if count.positive? && result < before
    return if count.negative? && result > before
  end

  self.value_as_number = result
  nil
end

#check_validityObject



1210
1211
1212
1213
1214
# File 'lib/dommy/html_elements.rb', line 1210

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

#checkedObject



606
607
608
# File 'lib/dommy/html_elements.rb', line 606

def checked
  @__checked.nil? ? default_checked : @__checked
end

#checked=(v) ⇒ Object



610
611
612
613
614
615
616
617
# File 'lib/dommy/html_elements.rb', line 610

def checked=(v)
  @__checked = !!v
  # A radio becoming checked unchecks the rest of its radio button group.
  uncheck_radio_group if @__checked && type == "radio"
  # Checkedness is property state (no attribute mutation fires), yet it
  # is selector-observable via :checked — invalidate computed styles.
  @document&.__internal_bump_style_generation__
end

#clamp_selection_index(v, len = value.to_s.length) ⇒ Object



829
830
831
832
# File 'lib/dommy/html_elements.rb', line 829

def clamp_selection_index(v, len = value.to_s.length)
  n = v.to_i
  n.negative? ? 0 : [n, len].min
end

#currently_checked_in_radio_groupObject

The currently-checked radio in this element's group (or nil).



677
678
679
# File 'lib/dommy/html_elements.rb', line 677

def currently_checked_in_radio_group
  radio_group_members.find { |radio| radio.checked && !radio.equal?(self) }
end

#date_string_to_ms(s) ⇒ Object

--- Date/time "convert a string to a number" algorithms (all UTC) --------



963
964
965
966
967
968
969
970
971
# File 'lib/dommy/html_elements.rb', line 963

def date_string_to_ms(s)
  m = /\A(\d{4,})-(\d{2})-(\d{2})\z/.match(s.strip)
  return ::Float::NAN unless m

  y, mo, d = m[1].to_i, m[2].to_i, m[3].to_i
  return ::Float::NAN if y < 1 || !::Date.valid_date?(y, mo, d)

  ::Time.utc(y, mo, d).to_i * 1000.0
end

#datetime_local_string_to_ms(s) ⇒ Object



984
985
986
987
988
989
990
991
992
993
994
995
# File 'lib/dommy/html_elements.rb', line 984

def datetime_local_string_to_ms(s)
  # The date/time separator may be "T" or a space (the "parse a local date
  # and time string" algorithm accepts both).
  m = /\A(\d{4,})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\z/.match(s.strip)
  return ::Float::NAN unless m

  y, mo, d, h, mi, se = m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i, m[6].to_i
  return ::Float::NAN if y < 1 || !::Date.valid_date?(y, mo, d) || h > 23 || mi > 59 || se > 59

  frac = m[7] ? m[7].ljust(3, "0").to_i : 0
  (::Time.utc(y, mo, d, h, mi, se).to_i * 1000 + frac).to_f
end

#default_range_value(lo, hi) ⇒ Object

A range with no (valid) value defaults to the midpoint of its range, or the minimum when the maximum is below it.



855
856
857
# File 'lib/dommy/html_elements.rb', line 855

def default_range_value(lo, hi)
  hi < lo ? lo : lo + (hi - lo) / 2.0
end

#default_stepObject

The default allowed step (in the type's own step units) when step is absent or invalid: 60 (seconds) for time/datetime-local, 1 otherwise.



921
922
923
# File 'lib/dommy/html_elements.rb', line 921

def default_step
  %w[time datetime-local].include?(type) ? 60.0 : 1.0
end

#filesObject

files — for <input type="file">. Browsers populate this via user interaction; in tests, code uses __driver_set_files__ to seed it.



531
532
533
534
535
536
# File 'lib/dommy/html_elements.rb', line 531

def files
  # `files` is null for every type other than file (WHATWG).
  return nil unless type == "file"

  @__files ||= FileList.new
end

#formObject

The form owner (WebIDL input.form): the form referenced by a form= content attribute (when it resolves to a form element), otherwise the nearest ancestor form.



739
740
741
# File 'lib/dommy/html_elements.rb', line 739

def form
  form_owner
end

#form_ownerObject



743
744
745
746
747
748
749
750
751
# File 'lib/dommy/html_elements.rb', line 743

def form_owner
  form_id = get_attribute("form").to_s
  unless form_id.empty?
    target = @document.get_element_by_id(form_id)
    return (target && target.tag_name.to_s.casecmp?("form")) ? target : nil
  end

  closest("form")
end

#indeterminateObject

indeterminate is pure property state (no content attribute), default false. Selector-observable via :indeterminate, so bump style generation.



621
622
623
# File 'lib/dommy/html_elements.rb', line 621

def indeterminate
  @__indeterminate || false
end

#indeterminate=(v) ⇒ Object



625
626
627
628
# File 'lib/dommy/html_elements.rb', line 625

def indeterminate=(v)
  @__indeterminate = !!v
  @document&.__internal_bump_style_generation__
end

#labelsObject



729
730
731
732
733
734
# File 'lib/dommy/html_elements.rb', line 729

def labels
  # A hidden input is not a labelable element, so it has no labels list.
  return nil if type == "hidden"

  labels_node_list
end

#listObject

HTMLInputElement.list — the referenced by the list content attribute (resolved by id, first element in tree order). Null when there is no list attribute, no element with that id, or the referenced element is not a .



1292
1293
1294
1295
1296
1297
1298
# File 'lib/dommy/html_elements.rb', line 1292

def list
  id = get_attribute("list")
  return nil if id.nil? || id.empty?

  element = @document.get_element_by_id(id)
  element.is_a?(HTMLDataListElement) ? element : nil
end

#max_as_numberObject



1147
1148
1149
# File 'lib/dommy/html_elements.rb', line 1147

def max_as_number
  step_boundary("max")
end

#max_lengthObject

maxLength / minLength reflect a "limited to only non-negative numbers" long: a missing / negative / non-numeric content attribute reads as -1.



546
# File 'lib/dommy/html_elements.rb', line 546

def max_length = parse_non_negative_reflected("maxlength")

#max_length=(value) ⇒ Object



549
550
551
# File 'lib/dommy/html_elements.rb', line 549

def max_length=(value)
  set_non_negative_reflected("maxlength", value)
end

#min_as_numberObject

Numeric-domain accessors shared with constraint validation (rangeUnderflow / rangeOverflow / stepMismatch), all in valueAsNumber units.



1143
1144
1145
# File 'lib/dommy/html_elements.rb', line 1143

def min_as_number
  step_boundary("min")
end

#min_lengthObject



547
# File 'lib/dommy/html_elements.rb', line 547

def min_length = parse_non_negative_reflected("minlength")

#min_length=(value) ⇒ Object



553
554
555
# File 'lib/dommy/html_elements.rb', line 553

def min_length=(value)
  set_non_negative_reflected("minlength", value)
end

#month_string_to_number(s) ⇒ Object



997
998
999
1000
1001
1002
1003
1004
1005
# File 'lib/dommy/html_elements.rb', line 997

def month_string_to_number(s)
  m = /\A(\d{4,})-(\d{2})\z/.match(s.strip)
  return ::Float::NAN unless m

  y, mo = m[1].to_i, m[2].to_i
  return ::Float::NAN if y < 1 || mo < 1 || mo > 12

  ((y - 1970) * 12 + (mo - 1)).to_f
end

#ms_to_date_string(ms) ⇒ Object



1027
1028
1029
1030
1031
1032
1033
1034
# File 'lib/dommy/html_elements.rb', line 1027

def ms_to_date_string(ms)
  return "" if ms.nan?

  t = utc_time_from_ms(ms)
  format("%04d-%02d-%02d", t.year, t.month, t.day)
rescue ::RangeError, ::ArgumentError, ::FloatDomainError
  ""
end

#ms_to_datetime_local_string(ms) ⇒ Object



1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
# File 'lib/dommy/html_elements.rb', line 1053

def ms_to_datetime_local_string(ms)
  return "" if ms.nan?

  t = utc_time_from_ms(ms)
  return "" if t.year < 1 || t.year > 9999

  base = format("%04d-%02d-%02dT%02d:%02d", t.year, t.month, t.day, t.hour, t.min)
  frac = (ms % 1000).to_i
  if t.sec.zero? && frac.zero?
    base
  elsif frac.zero?
    base + format(":%02d", t.sec)
  else
    base + format(":%02d.%03d", t.sec, frac)
  end
rescue ::RangeError, ::ArgumentError, ::FloatDomainError
  ""
end

#ms_to_time_string(ms) ⇒ Object



1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
# File 'lib/dommy/html_elements.rb', line 1036

def ms_to_time_string(ms)
  return "" if ms.nan?

  v = (ms % 86_400_000).to_i
  h = v / 3_600_000
  mi = (v % 3_600_000) / 60_000
  se = (v % 60_000) / 1000
  frac = v % 1000
  if se.zero? && frac.zero?
    format("%02d:%02d", h, mi)
  elsif frac.zero?
    format("%02d:%02d:%02d", h, mi, se)
  else
    format("%02d:%02d:%02d.%03d", h, mi, se, frac)
  end
end

#ms_to_week_string(ms) ⇒ Object



1081
1082
1083
1084
1085
1086
1087
1088
1089
# File 'lib/dommy/html_elements.rb', line 1081

def ms_to_week_string(ms)
  return "" if ms.nan?

  t = utc_time_from_ms(ms)
  d = ::Date.new(t.year, t.month, t.day)
  format("%04d-W%02d", d.cwyear, d.cweek)
rescue ::RangeError, ::ArgumentError, ::FloatDomainError
  ""
end

#nan_to_nil(n) ⇒ Object



940
941
942
# File 'lib/dommy/html_elements.rb', line 940

def nan_to_nil(n)
  n.nan? ? nil : n
end

#normalize_selection_direction(v) ⇒ Object



834
835
836
837
# File 'lib/dommy/html_elements.rb', line 834

def normalize_selection_direction(v)
  d = v.to_s
  %w[forward backward].include?(d) ? d : "none"
end

#number_to_month_string(n) ⇒ Object



1072
1073
1074
1075
1076
1077
1078
1079
# File 'lib/dommy/html_elements.rb', line 1072

def number_to_month_string(n)
  return "" if n.nan?

  months = n.to_i
  y = 1970 + (months.fdiv(12).floor)
  mo = months % 12
  format("%04d-%02d", y, mo + 1)
end

#number_to_string(n) ⇒ Object

JS Number-to-string: an integral value drops the trailing ".0".



945
946
947
948
949
# File 'lib/dommy/html_elements.rb', line 945

def number_to_string(n)
  return "" if n.nan?

  n == n.to_i ? n.to_i.to_s : n.to_s
end

#numeric_value_type?Boolean

Input types whose value has a numeric representation (valueAsNumber / stepUp / stepDown apply).

Returns:

  • (Boolean)


841
842
843
# File 'lib/dommy/html_elements.rb', line 841

def numeric_value_type?
  %w[number range date month week time datetime-local].include?(type)
end

#parse_valid_float(str) ⇒ Object



956
957
958
959
# File 'lib/dommy/html_elements.rb', line 956

def parse_valid_float(str)
  s = str.to_s
  VALID_FLOAT_RE.match?(s) ? (Float(s) rescue ::Float::NAN) : ::Float::NAN
end

#pre_click_activation_stateObject

HTML pre-click activation: a checkbox toggles; a radio becomes checked (which unchecks its group). Returns the state needed to undo this if the click is canceled, or nil for inputs with no activation behavior.



635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/dommy/html_elements.rb', line 635

def pre_click_activation_state
  # Only a mutable (enabled) checkbox/radio has activation behavior.
  return nil if disabled

  case type
  when "checkbox"
    old = checked
    old_indeterminate = indeterminate
    # Pre-click activation clears indeterminate, then toggles checkedness.
    self.indeterminate = false
    self.checked = !old
    { kind: :checkbox, old_checked: old, old_indeterminate: old_indeterminate }
  when "radio"
    old_checked = checked
    previously_checked = old_checked ? nil : currently_checked_in_radio_group
    self.checked = true
    { kind: :radio, old_checked: old_checked, previously_checked: previously_checked }
  end
end

#radio_group_membersObject

The radio button group: radios in the SAME tree (root node — so an orphan subtree groups too) that share this element's non-empty name and form owner (two radios with no form owner still group, as long as they share a tree and name).



710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
# File 'lib/dommy/html_elements.rb', line 710

def radio_group_members
  group_name = get_attribute("name").to_s
  return [self] if group_name.empty?

  owner = form_owner
  root = get_root_node
  return [self] unless root.respond_to?(:query_selector_all)

  members = root.query_selector_all("input[type='radio']").to_a.select do |radio|
    next false unless radio.respond_to?(:form_owner)
    next false unless radio.get_attribute("name").to_s == group_name

    same_form_owner?(owner, radio.form_owner)
  end
  # `query_selector_all` searches descendants, so a detached radio (whose
  # root node is itself) isn't returned — a radio is always in its own group.
  members.any? { |m| m.__dommy_backend_node__.equal?(__dommy_backend_node__) } ? members : members + [self]
end

#range_maxObject



849
850
851
# File 'lib/dommy/html_elements.rb', line 849

def range_max
  Float(@__node__["max"].to_s) rescue 100.0
end

#range_minObject



845
846
847
# File 'lib/dommy/html_elements.rb', line 845

def range_min
  Float(@__node__["min"].to_s) rescue 0.0
end

#raw_valueObject

Underlying string the user supplied to value=, before any sanitization. Used by ValidityState.badInput so a non-parseable number still trips constraint validation.



602
603
604
# File 'lib/dommy/html_elements.rb', line 602

def raw_value
  @__raw_value || @__value || reflected_string("value")
end

#report_validityObject



1216
1217
1218
# File 'lib/dommy/html_elements.rb', line 1216

def report_validity
  check_validity
end

#require_selection!Object

Raise on the selection setters/methods for a type that has no text selection (email, number, checkbox, …).



823
824
825
826
827
# File 'lib/dommy/html_elements.rb', line 823

def require_selection!
  return if supports_selection?

  raise DOMException::InvalidStateError, "The input element's type ('#{type}') does not support selection."
end

#restore_pre_click_activation(state) ⇒ Object

Canceled (default prevented): restore the pre-click checkedness. For a radio, also re-check whichever member was checked before.



664
665
666
667
668
669
670
671
672
673
674
# File 'lib/dommy/html_elements.rb', line 664

def restore_pre_click_activation(state)
  case state[:kind]
  when :checkbox
    self.checked = state[:old_checked]
    self.indeterminate = state[:old_indeterminate]
  when :radio
    self.checked = state[:old_checked]
    prev = state[:previously_checked]
    prev.checked = true if prev && !prev.equal?(self)
  end
end

#run_post_click_activation(_state) ⇒ Object

Not canceled: fire input then change (HTML "input activation behavior"). Both are UA-generated, so trusted and non-cancelable.



657
658
659
660
# File 'lib/dommy/html_elements.rb', line 657

def run_post_click_activation(_state)
  dispatch_event(Event.new("input", "bubbles" => true).__internal_mark_trusted__)
  dispatch_event(Event.new("change", "bubbles" => true).__internal_mark_trusted__)
end

#same_form_owner?(a, b) ⇒ Boolean

Two controls share a form owner when both are formless, or both point at the same form element (compared by backend node identity).

Returns:

  • (Boolean)


699
700
701
702
703
704
# File 'lib/dommy/html_elements.rb', line 699

def same_form_owner?(a, b)
  return b.nil? if a.nil?
  return false if b.nil?

  a.__dommy_backend_node__.equal?(b.__dommy_backend_node__)
end

#sanitize_number(raw) ⇒ Object



591
592
593
594
595
596
597
# File 'lib/dommy/html_elements.rb', line 591

def sanitize_number(raw)
  s = raw.to_s
  Float(s)
  s
rescue ArgumentError, TypeError
  ""
end

#sanitize_value(raw) ⇒ Object

Spec: the "value sanitization algorithm" runs lazily on read. type=email/url trim leading/trailing ASCII whitespace; type=number rejects non-finite floats by returning "" (badInput stays true so validity surfaces the original raw value).



561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/dommy/html_elements.rb', line 561

def sanitize_value(raw)
  case type
  # The one-line text types "strip newlines from the value" (WHATWG value
  # sanitization) — a pasted multi-line string collapses to one line.
  when "text", "search", "tel", "password"
    strip_newlines(raw.to_s)
  when "email"
    stripped = strip_newlines(raw.to_s)
    if @__node__.key?("multiple")
      stripped.split(",").map(&:strip).join(",")
    else
      stripped.strip
    end
  when "url"
    # Strip newlines, then leading/trailing whitespace.
    strip_newlines(raw.to_s).strip
  when "number", "range"
    sanitize_number(raw)
  when "color"
    s = raw.to_s.strip.downcase
    s.match?(/\A#[0-9a-f]{6}\z/) ? s : "#000000"
  else
    raw.to_s
  end
end

#selectObject

select() selects the whole control on a text control; on any other type it is a silent no-op (it does NOT throw).



796
797
798
799
800
801
802
803
# File 'lib/dommy/html_elements.rb', line 796

def select
  return nil unless supports_selection?

  @__selection_start = 0
  @__selection_end = value.to_s.length
  @__selection_direction = "none"
  nil
end

#selection_directionObject



783
784
785
786
787
# File 'lib/dommy/html_elements.rb', line 783

def selection_direction
  return nil unless supports_selection?

  @__selection_direction || "none"
end

#selection_direction=(v) ⇒ Object



789
790
791
792
# File 'lib/dommy/html_elements.rb', line 789

def selection_direction=(v)
  require_selection!
  @__selection_direction = normalize_selection_direction(v)
end

#selection_endObject



772
773
774
775
776
# File 'lib/dommy/html_elements.rb', line 772

def selection_end
  return nil unless supports_selection?

  @__selection_end ||= value.to_s.length
end

#selection_end=(v) ⇒ Object



778
779
780
781
# File 'lib/dommy/html_elements.rb', line 778

def selection_end=(v)
  require_selection!
  @__selection_end = clamp_selection_index(v)
end

#selection_startObject



761
762
763
764
765
# File 'lib/dommy/html_elements.rb', line 761

def selection_start
  return nil unless supports_selection?

  @__selection_start ||= value.to_s.length
end

#selection_start=(v) ⇒ Object



767
768
769
770
# File 'lib/dommy/html_elements.rb', line 767

def selection_start=(v)
  require_selection!
  @__selection_start = clamp_selection_index(v)
end

#set_custom_validity(msg) ⇒ Object



1220
1221
1222
1223
# File 'lib/dommy/html_elements.rb', line 1220

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

#set_range_text(_replacement, *_) ⇒ Object



816
817
818
819
# File 'lib/dommy/html_elements.rb', line 816

def set_range_text(_replacement, *_)
  require_selection!
  nil
end

#set_selection_range(start, finish, direction = nil) ⇒ Object



805
806
807
808
809
810
811
812
813
814
# File 'lib/dommy/html_elements.rb', line 805

def set_selection_range(start, finish, direction = nil)
  require_selection!
  len = value.to_s.length
  e = clamp_selection_index(finish, len)
  s = [clamp_selection_index(start, len), e].min
  @__selection_start = s
  @__selection_end = e
  @__selection_direction = normalize_selection_direction(direction)
  nil
end

#step_base_valueObject

The declared step (default 1 for number, 1 for range); "any" disables stepping (returns nil).



861
862
863
864
865
866
867
# File 'lib/dommy/html_elements.rb', line 861

def step_base_value
  raw = @__node__["step"].to_s.strip
  return nil if raw.casecmp?("any")

  s = (Float(raw) rescue nil)
  s && s > 0 ? s : default_step
end

#step_boundary(attr) ⇒ Object

The min/max boundary as a valueAsNumber, or nil when absent/unparseable.



926
927
928
929
930
931
932
933
934
935
936
937
938
# File 'lib/dommy/html_elements.rb', line 926

def step_boundary(attr)
  raw = @__node__[attr].to_s.strip
  return nil if raw.empty?

  case type
  when "number", "range" then (Float(raw) rescue nil)
  when "date" then nan_to_nil(date_string_to_ms(raw))
  when "time" then nan_to_nil(time_string_to_ms(raw))
  when "datetime-local" then nan_to_nil(datetime_local_string_to_ms(raw))
  when "month" then nan_to_nil(month_string_to_number(raw))
  when "week" then nan_to_nil(week_string_to_ms(raw))
  end
end

#step_down(n = 1) ⇒ Object



1176
1177
1178
# File 'lib/dommy/html_elements.rb', line 1176

def step_down(n = 1)
  apply_step(-(n || 1).to_i)
end

#step_scale_factorObject

The scale that turns one declared step into valueAsNumber units (ms for the date/time types, natural units for number/range/month).



910
911
912
913
914
915
916
917
# File 'lib/dommy/html_elements.rb', line 910

def step_scale_factor
  case type
  when "date" then 86_400_000
  when "week" then 604_800_000
  when "time", "datetime-local" then 1000
  else 1
  end
end

#step_up(n = 1) ⇒ Object

stepUp(n) / stepDown(n) add/subtract n steps to the current number. The WebIDL default for n is 1 (a missing/undefined arg crosses as nil).



1172
1173
1174
# File 'lib/dommy/html_elements.rb', line 1172

def step_up(n = 1)
  apply_step((n || 1).to_i)
end

#strip_newlines(str) ⇒ Object



587
588
589
# File 'lib/dommy/html_elements.rb', line 587

def strip_newlines(str)
  str.gsub(/[\r\n]/, "")
end

#supports_selection?Boolean

Returns:

  • (Boolean)


757
758
759
# File 'lib/dommy/html_elements.rb', line 757

def supports_selection?
  SELECTION_TYPES.include?(type)
end

#time_string_to_ms(s) ⇒ Object



973
974
975
976
977
978
979
980
981
982
# File 'lib/dommy/html_elements.rb', line 973

def time_string_to_ms(s)
  m = /\A(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\z/.match(s.strip)
  return ::Float::NAN unless m

  h, mi, se = m[1].to_i, m[2].to_i, m[3].to_i
  return ::Float::NAN if h > 23 || mi > 59 || se > 59

  frac = m[4] ? m[4].ljust(3, "0").to_i : 0
  ((h * 3600 + mi * 60 + se) * 1000 + frac).to_f
end

#typeObject

Own js_call methods, on top of Element's.



494
495
496
497
# File 'lib/dommy/html_elements.rb', line 494

def type
  raw = @__node__["type"].to_s
  raw.empty? ? "text" : raw.downcase
end

#type=(v) ⇒ Object



501
502
503
# File 'lib/dommy/html_elements.rb', line 501

def type=(v)
  set_reflected_string("type", v)
end

#uncheck_radio_groupObject

Uncheck every other radio in this element's group.



682
683
684
685
686
687
688
# File 'lib/dommy/html_elements.rb', line 682

def uncheck_radio_group
  radio_group_members.each do |radio|
    next if radio.equal?(self)

    radio.__internal_set_checked_silently__(false)
  end
end

#utc_time_from_ms(ms) ⇒ Object

--- Inverse: "convert a number to a string" for the date/time types -------



1023
1024
1025
# File 'lib/dommy/html_elements.rb', line 1023

def utc_time_from_ms(ms)
  ::Time.at(ms / 1000.0).utc
end

#validation_messageObject



1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
# File 'lib/dommy/html_elements.rb', line 1197

def validation_message
  return "" unless will_validate

  msg = (@custom_validity_message || "").to_s
  return msg unless msg.empty?
  return "Please fill out this field." if validity.value_missing
  return "Please match the requested format." if validity.pattern_mismatch
  return "Please enter a valid email address." if validity.type_mismatch && type == "email"
  return "Please enter a URL." if validity.type_mismatch && type == "url"

  ""
end

#validation_step_baseObject

The step base for validation: the min boundary if present, else 0.



1161
1162
1163
1164
1165
1166
1167
1168
# File 'lib/dommy/html_elements.rb', line 1161

def validation_step_base
  return min_as_number if min_as_number

  # The default step base is 0 for most types, but a `week` control aligns to
  # the Monday of 1970-W01 (the epoch is mid-week), so an unmatched default
  # base would report every whole week as a step mismatch.
  type == "week" ? week_string_to_ms("1970-W01") : 0.0
end

#validityObject



1180
1181
1182
# File 'lib/dommy/html_elements.rb', line 1180

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

#valueObject

Runtime value/checked. Dommy has no UI, so the runtime state is initialized from the attribute on first access and tracked separately thereafter — matching browser semantics where the value IDL attribute can drift from the value content attr.



509
510
511
512
513
514
515
516
# File 'lib/dommy/html_elements.rb', line 509

def value
  raw = @__value.nil? ? reflected_string("value") : @__value
  # checkbox/radio use the "default/on" value mode: with no value content
  # attribute (and no assigned value) the IDL value is "on".
  return "on" if raw.to_s.empty? && !@__node__.key?("value") && %w[checkbox radio].include?(type)

  sanitize_value(raw)
end

#value=(v) ⇒ Object



518
519
520
521
522
523
524
525
526
527
# File 'lib/dommy/html_elements.rb', line 518

def value=(v)
  raw = v.to_s
  # WHATWG: a file input's value IDL setter throws unless set to the empty
  # string (which clears the selection).
  if type == "file" && !raw.empty?
    raise DOMException::InvalidStateError, "a file input's value may only be set to the empty string"
  end
  @__raw_value = raw
  @__value = raw
end

#value_as_numberObject

valueAsNumber — the control's value as a number, per the type's "convert a string to a number" algorithm (NaN when the type has no number representation or the value doesn't parse). number/range are plain floats; range additionally defaults to its midpoint and clamps to [min, max].



1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
# File 'lib/dommy/html_elements.rb', line 1095

def value_as_number
  case type
  when "number"
    parse_valid_float(value.to_s)
  when "range"
    n = parse_valid_float(value.to_s)
    n = nil if n.nan?
    lo = range_min
    hi = range_max
    n = default_range_value(lo, hi) if n.nil?
    n.clamp(lo, hi)
  when "date"
    date_string_to_ms(value.to_s)
  when "time"
    time_string_to_ms(value.to_s)
  when "datetime-local"
    datetime_local_string_to_ms(value.to_s)
  when "month"
    month_string_to_number(value.to_s)
  when "week"
    week_string_to_ms(value.to_s)
  else
    ::Float::NAN
  end
end

#value_as_number=(n) ⇒ Object



1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
# File 'lib/dommy/html_elements.rb', line 1121

def value_as_number=(n)
  f = n.to_f
  case type
  when "number", "range"
    self.value = f.nan? ? "" : number_to_string(f)
  when "date"
    self.value = ms_to_date_string(f)
  when "time"
    self.value = ms_to_time_string(f)
  when "datetime-local"
    self.value = ms_to_datetime_local_string(f)
  when "month"
    self.value = number_to_month_string(f)
  when "week"
    self.value = ms_to_week_string(f)
  else
    raise DOMException::InvalidStateError, "valueAsNumber is not applicable to input type '#{type}'"
  end
end

#week_string_to_ms(s) ⇒ Object



1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/dommy/html_elements.rb', line 1007

def week_string_to_ms(s)
  m = /\A(\d{4,})-W(\d{2})\z/.match(s.strip)
  return ::Float::NAN unless m

  y, w = m[1].to_i, m[2].to_i
  return ::Float::NAN if y < 1 || w < 1

  # Date.commercial raises for a week beyond the ISO year's 52/53 weeks.
  d = ::Date.commercial(y, w, 1)
  ::Time.utc(d.year, d.month, d.day).to_i * 1000.0
rescue ::ArgumentError
  ::Float::NAN
end

#will_validateObject

Whether this control participates in constraint validation. Disabled / hidden / button-type inputs return false.



1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
# File 'lib/dommy/html_elements.rb', line 1186

def will_validate
  return false if reflected_boolean("disabled")
  return false if disabled_by_ancestor_fieldset?
  return false if reflected_boolean("readonly")
  return false if %w[hidden button submit reset image].include?(type)
  # A control with a datalist ancestor is barred from constraint validation.
  return false unless closest("datalist").nil?

  true
end