FormAssociatedElement.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /*
  2. * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <LibWeb/DOM/Document.h>
  8. #include <LibWeb/DOM/Event.h>
  9. #include <LibWeb/HTML/FormAssociatedElement.h>
  10. #include <LibWeb/HTML/HTMLButtonElement.h>
  11. #include <LibWeb/HTML/HTMLFieldSetElement.h>
  12. #include <LibWeb/HTML/HTMLFormElement.h>
  13. #include <LibWeb/HTML/HTMLInputElement.h>
  14. #include <LibWeb/HTML/HTMLLegendElement.h>
  15. #include <LibWeb/HTML/HTMLSelectElement.h>
  16. #include <LibWeb/HTML/HTMLTextAreaElement.h>
  17. #include <LibWeb/HTML/Parser/HTMLParser.h>
  18. namespace Web::HTML {
  19. static SelectionDirection string_to_selection_direction(Optional<String> value)
  20. {
  21. if (!value.has_value())
  22. return SelectionDirection::None;
  23. if (value.value() == "forward"sv)
  24. return SelectionDirection::Forward;
  25. if (value.value() == "backward"sv)
  26. return SelectionDirection::Backward;
  27. return SelectionDirection::None;
  28. }
  29. void FormAssociatedElement::set_form(HTMLFormElement* form)
  30. {
  31. if (m_form)
  32. m_form->remove_associated_element({}, form_associated_element_to_html_element());
  33. m_form = form;
  34. if (m_form)
  35. m_form->add_associated_element({}, form_associated_element_to_html_element());
  36. }
  37. bool FormAssociatedElement::enabled() const
  38. {
  39. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled
  40. auto const& html_element = form_associated_element_to_html_element();
  41. // A form control is disabled if any of the following conditions are met:
  42. // 1. The element is a button, input, select, textarea, or form-associated custom element, and the disabled attribute is specified on this element (regardless of its value).
  43. // FIXME: This doesn't check for form-associated custom elements.
  44. if ((is<HTMLButtonElement>(html_element) || is<HTMLInputElement>(html_element) || is<HTMLSelectElement>(html_element) || is<HTMLTextAreaElement>(html_element)) && html_element.has_attribute(HTML::AttributeNames::disabled))
  45. return false;
  46. // 2. The element is a descendant of a fieldset element whose disabled attribute is specified, and is not a descendant of that fieldset element's first legend element child, if any.
  47. for (auto* fieldset_ancestor = html_element.first_ancestor_of_type<HTMLFieldSetElement>(); fieldset_ancestor; fieldset_ancestor = fieldset_ancestor->first_ancestor_of_type<HTMLFieldSetElement>()) {
  48. if (fieldset_ancestor->has_attribute(HTML::AttributeNames::disabled)) {
  49. auto* first_legend_element_child = fieldset_ancestor->first_child_of_type<HTMLLegendElement>();
  50. if (!first_legend_element_child || !html_element.is_descendant_of(*first_legend_element_child))
  51. return false;
  52. }
  53. }
  54. return true;
  55. }
  56. void FormAssociatedElement::set_parser_inserted(Badge<HTMLParser>)
  57. {
  58. m_parser_inserted = true;
  59. }
  60. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-inserted
  61. void FormAssociatedElement::form_node_was_inserted()
  62. {
  63. // 1. If the form-associated element's parser inserted flag is set, then return.
  64. if (m_parser_inserted)
  65. return;
  66. // 2. Reset the form owner of the form-associated element.
  67. reset_form_owner();
  68. }
  69. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-removed
  70. void FormAssociatedElement::form_node_was_removed()
  71. {
  72. // 1. If the form-associated element has a form owner and the form-associated element and its form owner are no longer in the same tree, then reset the form owner of the form-associated element.
  73. if (m_form && &form_associated_element_to_html_element().root() != &m_form->root())
  74. reset_form_owner();
  75. }
  76. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3
  77. void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional<String> const& value)
  78. {
  79. // When a listed form-associated element's form attribute is set, changed, or removed, then the user agent must
  80. // reset the form owner of that element.
  81. if (name == HTML::AttributeNames::form) {
  82. auto& html_element = form_associated_element_to_html_element();
  83. if (value.has_value())
  84. html_element.document().add_form_associated_element_with_form_attribute(*this);
  85. else
  86. html_element.document().remove_form_associated_element_with_form_attribute(*this);
  87. reset_form_owner();
  88. }
  89. }
  90. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-4
  91. void FormAssociatedElement::element_id_changed(Badge<DOM::Document>)
  92. {
  93. // When a listed form-associated element has a form attribute and the ID of any of the elements in the tree changes,
  94. // then the user agent must reset the form owner of that form-associated element.
  95. VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
  96. reset_form_owner();
  97. }
  98. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-5
  99. void FormAssociatedElement::element_with_id_was_added_or_removed(Badge<DOM::Document>)
  100. {
  101. // When a listed form-associated element has a form attribute and an element with an ID is inserted into or removed
  102. // from the Document, then the user agent must reset the form owner of that form-associated element.
  103. VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
  104. reset_form_owner();
  105. }
  106. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#reset-the-form-owner
  107. void FormAssociatedElement::reset_form_owner()
  108. {
  109. auto& html_element = form_associated_element_to_html_element();
  110. // 1. Unset element's parser inserted flag.
  111. m_parser_inserted = false;
  112. // 2. If all of the following conditions are true
  113. // - element's form owner is not null
  114. // - element is not listed or its form content attribute is not present
  115. // - element's form owner is its nearest form element ancestor after the change to the ancestor chain
  116. // then do nothing, and return.
  117. if (m_form
  118. && (!is_listed() || !html_element.has_attribute(HTML::AttributeNames::form))
  119. && html_element.first_ancestor_of_type<HTMLFormElement>() == m_form.ptr()) {
  120. return;
  121. }
  122. // 3. Set element's form owner to null.
  123. set_form(nullptr);
  124. // 4. If element is listed, has a form content attribute, and is connected, then:
  125. if (is_listed() && html_element.has_attribute(HTML::AttributeNames::form) && html_element.is_connected()) {
  126. // 1. If the first element in element's tree, in tree order, to have an ID that is identical to element's form content attribute's value, is a form element, then associate the element with that form element.
  127. auto form_value = html_element.attribute(HTML::AttributeNames::form);
  128. html_element.root().for_each_in_inclusive_subtree_of_type<HTMLFormElement>([this, &form_value](HTMLFormElement& form_element) {
  129. if (form_element.id() == form_value) {
  130. set_form(&form_element);
  131. return TraversalDecision::Break;
  132. }
  133. return TraversalDecision::Continue;
  134. });
  135. }
  136. // 5. Otherwise, if element has an ancestor form element, then associate element with the nearest such ancestor form element.
  137. else {
  138. auto* form_ancestor = html_element.first_ancestor_of_type<HTMLFormElement>();
  139. if (form_ancestor)
  140. set_form(form_ancestor);
  141. }
  142. }
  143. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
  144. String FormAssociatedElement::relevant_value() const
  145. {
  146. auto const& html_element = form_associated_element_to_html_element();
  147. if (is<HTMLInputElement>(html_element))
  148. return static_cast<HTMLInputElement const&>(html_element).value();
  149. if (is<HTMLTextAreaElement>(html_element))
  150. return static_cast<HTMLTextAreaElement const&>(html_element).api_value();
  151. VERIFY_NOT_REACHED();
  152. }
  153. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
  154. void FormAssociatedElement::relevant_value_was_changed(JS::GCPtr<DOM::Text> text_node)
  155. {
  156. auto the_relevant_value = relevant_value();
  157. auto relevant_value_length = the_relevant_value.code_points().length();
  158. // 1. If the element has a selection:
  159. if (m_selection_start < m_selection_end) {
  160. // 1. If the start of the selection is now past the end of the relevant value, set it to
  161. // the end of the relevant value.
  162. if (m_selection_start > relevant_value_length)
  163. m_selection_start = relevant_value_length;
  164. // 2. If the end of the selection is now past the end of the relevant value, set it to the
  165. // end of the relevant value.
  166. if (m_selection_end > relevant_value_length)
  167. m_selection_end = relevant_value_length;
  168. // 3. If the user agent does not support empty selection, and both the start and end of the
  169. // selection are now pointing to the end of the relevant value, then instead set the
  170. // element's text entry cursor position to the end of the relevant value, removing any
  171. // selection.
  172. // NOTE: We support empty selections.
  173. return;
  174. }
  175. // 2. Otherwise, the element must have a text entry cursor position position. If it is now past
  176. // the end of the relevant value, set it to the end of the relevant value.
  177. auto& document = form_associated_element_to_html_element().document();
  178. auto const current_cursor_position = document.cursor_position();
  179. if (current_cursor_position && text_node
  180. && current_cursor_position->node() == text_node
  181. && current_cursor_position->offset() > relevant_value_length) {
  182. document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length));
  183. }
  184. }
  185. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
  186. WebIDL::ExceptionOr<void> FormAssociatedElement::select()
  187. {
  188. // 1. If this element is an input element, and either select() does not apply to this element
  189. // or the corresponding control has no selectable text, return.
  190. auto& html_element = form_associated_element_to_html_element();
  191. if (is<HTMLInputElement>(html_element)) {
  192. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  193. // FIXME: implement "or the corresponding control has no selectable text"
  194. if (!input_element.select_applies())
  195. return {};
  196. }
  197. // 2. Set the selection range with 0 and infinity.
  198. set_the_selection_range(0, NumericLimits<WebIDL::UnsignedLong>::max());
  199. return {};
  200. }
  201. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
  202. Optional<WebIDL::UnsignedLong> FormAssociatedElement::selection_start() const
  203. {
  204. // 1. If this element is an input element, and selectionStart does not apply to this element, return null.
  205. auto const& html_element = form_associated_element_to_html_element();
  206. if (is<HTMLInputElement>(html_element)) {
  207. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  208. if (!input_element.selection_or_range_applies())
  209. return {};
  210. }
  211. // 2. If there is no selection, return the code unit offset within the relevant value to the character that
  212. // immediately follows the text entry cursor.
  213. if (m_selection_start == m_selection_end) {
  214. if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
  215. return cursor->offset();
  216. }
  217. // 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
  218. // the selection.
  219. return m_selection_start;
  220. }
  221. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
  222. WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_start(Optional<WebIDL::UnsignedLong> const& value)
  223. {
  224. // 1. If this element is an input element, and selectionStart does not apply to this element,
  225. // throw an "InvalidStateError" DOMException.
  226. auto& html_element = form_associated_element_to_html_element();
  227. if (is<HTMLInputElement>(html_element)) {
  228. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  229. if (!input_element.selection_or_range_applies())
  230. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionStart does not apply to this input type"_fly_string);
  231. }
  232. // 2. Let end be the value of this element's selectionEnd attribute.
  233. auto end = m_selection_end;
  234. // 3. If end is less than the given value, set end to the given value.
  235. if (value.has_value() && end < value.value())
  236. end = value.value();
  237. // 4. Set the selection range with the given value, end, and the value of this element's
  238. // selectionDirection attribute.
  239. set_the_selection_range(value, end, selection_direction_state());
  240. return {};
  241. }
  242. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend
  243. Optional<WebIDL::UnsignedLong> FormAssociatedElement::selection_end() const
  244. {
  245. // 1. If this element is an input element, and selectionEnd does not apply to this element, return
  246. // null.
  247. auto const& html_element = form_associated_element_to_html_element();
  248. if (is<HTMLInputElement>(html_element)) {
  249. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  250. if (!input_element.selection_or_range_applies())
  251. return {};
  252. }
  253. // 2. If there is no selection, return the code unit offset within the relevant value to the
  254. // character that immediately follows the text entry cursor.
  255. if (m_selection_start == m_selection_end) {
  256. if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
  257. return cursor->offset();
  258. }
  259. // 3. Return the code unit offset within the relevant value to the character that immediately
  260. // follows the end of the selection.
  261. return m_selection_end;
  262. }
  263. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
  264. WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_end(Optional<WebIDL::UnsignedLong> const& value)
  265. {
  266. // 1. If this element is an input element, and selectionEnd does not apply to this element,
  267. // throw an "InvalidStateError" DOMException.
  268. auto& html_element = form_associated_element_to_html_element();
  269. if (is<HTMLInputElement>(html_element)) {
  270. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  271. if (!input_element.selection_or_range_applies())
  272. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionEnd does not apply to this input type"_fly_string);
  273. }
  274. // 2. Set the selection range with the value of this element's selectionStart attribute, the
  275. // given value, and the value of this element's selectionDirection attribute.
  276. set_the_selection_range(m_selection_start, value, selection_direction_state());
  277. return {};
  278. }
  279. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
  280. Optional<String> FormAssociatedElement::selection_direction() const
  281. {
  282. // 1. If this element is an input element, and selectionDirection does not apply to this
  283. // element, return null.
  284. auto const& html_element = form_associated_element_to_html_element();
  285. if (is<HTMLInputElement>(html_element)) {
  286. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  287. if (!input_element.selection_or_range_applies())
  288. return {};
  289. }
  290. // 2. Return this element's selection direction.
  291. switch (m_selection_direction) {
  292. case SelectionDirection::Forward:
  293. return "forward"_string;
  294. case SelectionDirection::Backward:
  295. return "backward"_string;
  296. case SelectionDirection::None:
  297. return "none"_string;
  298. default:
  299. VERIFY_NOT_REACHED();
  300. }
  301. }
  302. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-direction
  303. void FormAssociatedElement::set_selection_direction(Optional<String> direction)
  304. {
  305. // To set the selection direction of an element to a given direction, update the element's
  306. // selection direction to the given direction, unless the direction is "none" and the
  307. // platform does not support that direction; in that case, update the element's selection
  308. // direction to "forward".
  309. m_selection_direction = string_to_selection_direction(direction);
  310. }
  311. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
  312. WebIDL::ExceptionOr<void> FormAssociatedElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
  313. {
  314. // 1. If this element is an input element, and setSelectionRange() does not apply to this
  315. // element, throw an "InvalidStateError" DOMException.
  316. auto& html_element = form_associated_element_to_html_element();
  317. if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
  318. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionRange does not apply to this input type"_fly_string);
  319. // 2. Set the selection range with start, end, and direction.
  320. set_the_selection_range(start, end, string_to_selection_direction(direction));
  321. return {};
  322. }
  323. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-range
  324. void FormAssociatedElement::set_the_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction)
  325. {
  326. // 1. If start is null, let start be zero.
  327. start = start.value_or(0);
  328. // 2. If end is null, let end be zero.
  329. end = end.value_or(0);
  330. // 3. Set the selection of the text control to the sequence of code units within the relevant
  331. // value starting with the code unit at the startth position (in logical order) and ending
  332. // with the code unit at the (end-1)th position. Arguments greater than the length of the
  333. // relevant value of the text control (including the special value infinity) must be treated
  334. // as pointing at the end of the text control.
  335. auto the_relevant_value = relevant_value();
  336. auto relevant_value_length = the_relevant_value.code_points().length();
  337. auto new_selection_start = AK::min(start.value(), relevant_value_length);
  338. auto new_selection_end = AK::min(end.value(), relevant_value_length);
  339. // If end is less than or equal to start then the start of the selection and the end of the
  340. // selection must both be placed immediately before the character with offset end. In UAs
  341. // where there is no concept of an empty selection, this must set the cursor to be just
  342. // before the character with offset end.
  343. new_selection_start = AK::min(new_selection_start, new_selection_end);
  344. bool was_modified = m_selection_start != new_selection_start || m_selection_end != new_selection_end;
  345. m_selection_start = new_selection_start;
  346. m_selection_end = new_selection_end;
  347. // 4. If direction is not identical to either "backward" or "forward", or if the direction
  348. // argument was not given, set direction to "none".
  349. // NOTE: This is handled by the argument's default value and ::string_to_selection_direction().
  350. // 5. Set the selection direction of the text control to direction.
  351. was_modified |= m_selection_direction != direction;
  352. m_selection_direction = direction;
  353. // 6. If the previous steps caused the selection of the text control to be modified (in either
  354. // extent or direction), then queue an element task on the user interaction task source
  355. // given the element to fire an event named select at the element, with the bubbles attribute
  356. // initialized to true.
  357. // AD-HOC: If there is no selection, we do not fire the event. This seems to correspond to how
  358. // other browsers behave.
  359. if (was_modified && m_selection_start != m_selection_end) {
  360. auto& html_element = form_associated_element_to_html_element();
  361. html_element.queue_an_element_task(Task::Source::UserInteraction, [&html_element] {
  362. auto select_event = DOM::Event::create(html_element.realm(), EventNames::select, { .bubbles = true });
  363. static_cast<DOM::EventTarget*>(&html_element)->dispatch_event(select_event);
  364. });
  365. }
  366. }
  367. }