Class: Dommy::ValidityState

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

Overview

ValidityState — computes constraint-validation flags from the host control's current attributes and value. Bound to a single host control; reads dynamically on every access so attribute changes between calls are reflected.

Flags follow the HTML spec; badInput is always false (we'd need the browser's number parser to detect "12abc" in a type=number).

Constant Summary collapse

FLAGS =
%w[
valueMissing
typeMismatch
patternMismatch
tooLong
tooShort
rangeUnderflow
rangeOverflow
stepMismatch
badInput
customError
    ]
.freeze
EMAIL_RE =

The exact WHATWG "valid email address" production.

%r{\A[a-zA-Z0-9.!\#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z}
URL_SCHEMES =
%w[http:// https:// ftp://].freeze

Instance Method Summary collapse

Constructor Details

#initialize(host = nil) ⇒ ValidityState

Returns a new instance of ValidityState.



1720
1721
1722
# File 'lib/dommy/html_elements.rb', line 1720

def initialize(host = nil)
  @host = host
end

Instance Method Details

#__js_get__(key) ⇒ Object

---- Bridge protocol ----



1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
# File 'lib/dommy/html_elements.rb', line 1923

def __js_get__(key)
  case key
  when "valueMissing"
    value_missing
  when "typeMismatch"
    type_mismatch
  when "patternMismatch"
    pattern_mismatch
  when "tooLong"
    too_long
  when "tooShort"
    too_short
  when "rangeUnderflow"
    range_underflow
  when "rangeOverflow"
    range_overflow
  when "stepMismatch"
    step_mismatch
  when "badInput"
    bad_input
  when "customError"
    custom_error
  when "valid"
    valid
  else
    Bridge::ABSENT
  end
end

#bad_inputObject

badInput flags input that the user agent couldn't convert to the host control's expected type. For Dommy this is meaningful for type=number/range (raw string not a finite float) and type=color (not a #rrggbb literal).



1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
# File 'lib/dommy/html_elements.rb', line 1880

def bad_input
  return false unless @host

  raw = @host.respond_to?(:raw_value) ? @host.raw_value : host_value
  raw = raw.to_s
  return false if raw.empty?

  case host_type
  when "number", "range"
    !valid_float?(raw)
  when "color"
    !raw.strip.downcase.match?(/\A#[0-9a-f]{6}\z/)
  else
    false
  end
end

#custom_errorObject



1904
1905
1906
# File 'lib/dommy/html_elements.rb', line 1904

def custom_error
  !custom_message.empty?
end

#host_barred?Boolean

A control that is disabled or readonly is barred from constraint validation — none of the "suffering from" flags apply.

Returns:

  • (Boolean)


1728
1729
1730
1731
1732
1733
1734
# File 'lib/dommy/html_elements.rb', line 1728

def host_barred?
  return false unless @host

  disabled = host_attr_present?("disabled")
  readonly = @host.respond_to?(:readonly) ? @host.readonly : host_attr_present?("readonly")
  disabled || readonly
end

#pattern_mismatchObject



1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
# File 'lib/dommy/html_elements.rb', line 1797

def pattern_mismatch
  return false unless @host

  pat = host_attr_value("pattern").to_s
  return false if pat.empty?

  v = host_value.to_s
  return false if v.empty?

  # The pattern must be a valid regex ON ITS OWN — validate it before
  # anchoring, so an unbalanced `a)(b` (which the `(?:…)` wrapper would
  # otherwise balance) is correctly discarded rather than silently matched.
  Regexp.new(pat)
  !Regexp.new("\\A(?:#{pat})\\z").match?(v)
rescue RegexpError
  false
end

#range_overflowObject



1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
# File 'lib/dommy/html_elements.rb', line 1844

def range_overflow
  return false unless numeric_host?

  max = @host.max_as_number
  return false if max.nil?

  num = @host.value_as_number
  return false if num.nan?
  min = @host.min_as_number
  return num > max && num < min if reversed_range?(min, max)

  num > max
end

#range_underflowObject



1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
# File 'lib/dommy/html_elements.rb', line 1827

def range_underflow
  return false unless numeric_host?

  min = @host.min_as_number
  return false if min.nil?

  num = @host.value_as_number
  return false if num.nan?
  # A `time` control has a periodic domain: min > max means a REVERSED range
  # whose accepted values are `>= min` OR `<= max`, so both underflow and
  # overflow hold for a value in the excluded gap (max, min).
  max = @host.max_as_number
  return num > max && num < min if reversed_range?(min, max)

  num < min
end

#reversed_range?(min, max) ⇒ Boolean

A reversed range only exists for the periodic time domain with min > max.

Returns:

  • (Boolean)


1859
1860
1861
# File 'lib/dommy/html_elements.rb', line 1859

def reversed_range?(min, max)
  host_type == "time" && min && max && min > max
end

#step_mismatchObject



1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
# File 'lib/dommy/html_elements.rb', line 1863

def step_mismatch
  return false unless numeric_host?

  step = @host.allowed_value_step
  return false if step.nil?

  num = @host.value_as_number
  return false if num.nan?

  ratio = (num - @host.validation_step_base) / step
  (ratio - ratio.round).abs > 1e-7
end

#too_longObject

tooLong / tooShort apply ONLY when the value was last changed by a USER EDIT (not a script assignment), per the WHATWG "suffering from being too long/short" definitions. Dommy has no interactive text entry, so a value is never user-edited and these constraints never fire.



1819
1820
1821
# File 'lib/dommy/html_elements.rb', line 1819

def too_long
  false
end

#too_shortObject



1823
1824
1825
# File 'lib/dommy/html_elements.rb', line 1823

def too_short
  false
end

#type_mismatchObject



1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
# File 'lib/dommy/html_elements.rb', line 1776

def type_mismatch
  return false unless @host

  v = host_value.to_s
  return false if v.empty?

  case host_type
  when "email"
    # A `multiple` email is a comma-separated list; every part must be valid.
    if host_attr_present?("multiple")
      v.split(",", -1).any? { |part| !part.strip.match?(EMAIL_RE) }
    else
      !v.match?(EMAIL_RE)
    end
  when "url"
    URL_SCHEMES.none? { |s| v.start_with?(s) }
  else
    false
  end
end

#validObject



1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
# File 'lib/dommy/html_elements.rb', line 1908

def valid
  !(value_missing ||
    type_mismatch ||
    pattern_mismatch ||
    too_long ||
    too_short ||
    range_underflow ||
    range_overflow ||
    step_mismatch ||
    bad_input ||
    custom_error)
end

#valid_float?(s) ⇒ Boolean

Returns:

  • (Boolean)


1897
1898
1899
1900
1901
1902
# File 'lib/dommy/html_elements.rb', line 1897

def valid_float?(s)
  Float(s)
  true
rescue ArgumentError, TypeError
  false
end

#value_missingObject



1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
# File 'lib/dommy/html_elements.rb', line 1736

def value_missing
  return false unless @host && host_attr_present?("required")

  case host_type
  when "checkbox"
    # The checkbox/radio "being missing" flag reflects checkedness even when
    # the control is barred (only willValidate gates participation).
    !host_checked?
  when "radio"
    # An unnamed radio is not part of a group and is never missing.
    return false if @host.respond_to?(:get_attribute) && @host.get_attribute("name").to_s.empty?

    # A required radio is missing only when NO member of its group (same
    # name/form owner/tree) is checked — using runtime checkedness.
    if @host.respond_to?(:radio_group_members)
      @host.radio_group_members.none? { |r| r.respond_to?(:checked) ? r.checked : false }
    else
      !host_checked?
    end
  when "file"
    files = @host.respond_to?(:files) ? @host.files : nil
    files.nil? || files.length.zero?
  when "select-one", "select-multiple"
    # A required select is missing when its selected option has an empty
    # value (the placeholder label option); the flag isn't barred by disabled.
    @host.respond_to?(:value) && @host.value.to_s.empty?
  else
    # Text-like controls only "suffer from being missing" when mutable.
    return false if host_barred?

    # A date/number type with an unparseable value has no value (its
    # sanitized value is empty), so it counts as missing.
    if @host.respond_to?(:numeric_value_type?) && @host.send(:numeric_value_type?)
      @host.value_as_number.nan?
    else
      host_value.to_s.empty?
    end
  end
end