FormAssociatedElement.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. /*
  2. * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2024, Jelle Raaijmakers <jelle@gmta.nl>
  4. * Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include <LibWeb/DOM/Document.h>
  9. #include <LibWeb/DOM/Event.h>
  10. #include <LibWeb/HTML/FormAssociatedElement.h>
  11. #include <LibWeb/HTML/HTMLButtonElement.h>
  12. #include <LibWeb/HTML/HTMLFieldSetElement.h>
  13. #include <LibWeb/HTML/HTMLFormElement.h>
  14. #include <LibWeb/HTML/HTMLInputElement.h>
  15. #include <LibWeb/HTML/HTMLLegendElement.h>
  16. #include <LibWeb/HTML/HTMLSelectElement.h>
  17. #include <LibWeb/HTML/HTMLTextAreaElement.h>
  18. #include <LibWeb/HTML/Parser/HTMLParser.h>
  19. namespace Web::HTML {
  20. static SelectionDirection string_to_selection_direction(Optional<String> value)
  21. {
  22. if (!value.has_value())
  23. return SelectionDirection::None;
  24. if (value.value() == "forward"sv)
  25. return SelectionDirection::Forward;
  26. if (value.value() == "backward"sv)
  27. return SelectionDirection::Backward;
  28. return SelectionDirection::None;
  29. }
  30. void FormAssociatedElement::set_form(HTMLFormElement* form)
  31. {
  32. if (m_form)
  33. m_form->remove_associated_element({}, form_associated_element_to_html_element());
  34. m_form = form;
  35. if (m_form)
  36. m_form->add_associated_element({}, form_associated_element_to_html_element());
  37. }
  38. bool FormAssociatedElement::enabled() const
  39. {
  40. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled
  41. auto const& html_element = form_associated_element_to_html_element();
  42. // A form control is disabled if any of the following conditions are met:
  43. // 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).
  44. // FIXME: This doesn't check for form-associated custom elements.
  45. if ((is<HTMLButtonElement>(html_element) || is<HTMLInputElement>(html_element) || is<HTMLSelectElement>(html_element) || is<HTMLTextAreaElement>(html_element)) && html_element.has_attribute(HTML::AttributeNames::disabled))
  46. return false;
  47. // 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.
  48. for (auto* fieldset_ancestor = html_element.first_ancestor_of_type<HTMLFieldSetElement>(); fieldset_ancestor; fieldset_ancestor = fieldset_ancestor->first_ancestor_of_type<HTMLFieldSetElement>()) {
  49. if (fieldset_ancestor->has_attribute(HTML::AttributeNames::disabled)) {
  50. auto* first_legend_element_child = fieldset_ancestor->first_child_of_type<HTMLLegendElement>();
  51. if (!first_legend_element_child || !html_element.is_descendant_of(*first_legend_element_child))
  52. return false;
  53. }
  54. }
  55. return true;
  56. }
  57. void FormAssociatedElement::set_parser_inserted(Badge<HTMLParser>)
  58. {
  59. m_parser_inserted = true;
  60. }
  61. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-inserted
  62. void FormAssociatedElement::form_node_was_inserted()
  63. {
  64. // 1. If the form-associated element's parser inserted flag is set, then return.
  65. if (m_parser_inserted)
  66. return;
  67. // 2. Reset the form owner of the form-associated element.
  68. reset_form_owner();
  69. }
  70. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:nodes-are-removed
  71. void FormAssociatedElement::form_node_was_removed()
  72. {
  73. // 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.
  74. if (m_form && &form_associated_element_to_html_element().root() != &m_form->root())
  75. reset_form_owner();
  76. }
  77. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3
  78. void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional<String> const& value)
  79. {
  80. // When a listed form-associated element's form attribute is set, changed, or removed, then the user agent must
  81. // reset the form owner of that element.
  82. if (name == HTML::AttributeNames::form) {
  83. auto& html_element = form_associated_element_to_html_element();
  84. if (value.has_value())
  85. html_element.document().add_form_associated_element_with_form_attribute(*this);
  86. else
  87. html_element.document().remove_form_associated_element_with_form_attribute(*this);
  88. reset_form_owner();
  89. }
  90. }
  91. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-4
  92. void FormAssociatedElement::element_id_changed(Badge<DOM::Document>)
  93. {
  94. // When a listed form-associated element has a form attribute and the ID of any of the elements in the tree changes,
  95. // then the user agent must reset the form owner of that form-associated element.
  96. VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
  97. reset_form_owner();
  98. }
  99. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-5
  100. void FormAssociatedElement::element_with_id_was_added_or_removed(Badge<DOM::Document>)
  101. {
  102. // When a listed form-associated element has a form attribute and an element with an ID is inserted into or removed
  103. // from the Document, then the user agent must reset the form owner of that form-associated element.
  104. VERIFY(form_associated_element_to_html_element().has_attribute(HTML::AttributeNames::form));
  105. reset_form_owner();
  106. }
  107. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#reset-the-form-owner
  108. void FormAssociatedElement::reset_form_owner()
  109. {
  110. auto& html_element = form_associated_element_to_html_element();
  111. // 1. Unset element's parser inserted flag.
  112. m_parser_inserted = false;
  113. // 2. If all of the following conditions are true
  114. // - element's form owner is not null
  115. // - element is not listed or its form content attribute is not present
  116. // - element's form owner is its nearest form element ancestor after the change to the ancestor chain
  117. // then do nothing, and return.
  118. if (m_form
  119. && (!is_listed() || !html_element.has_attribute(HTML::AttributeNames::form))
  120. && html_element.first_ancestor_of_type<HTMLFormElement>() == m_form.ptr()) {
  121. return;
  122. }
  123. // 3. Set element's form owner to null.
  124. set_form(nullptr);
  125. // 4. If element is listed, has a form content attribute, and is connected, then:
  126. if (is_listed() && html_element.has_attribute(HTML::AttributeNames::form) && html_element.is_connected()) {
  127. // 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.
  128. auto form_value = html_element.attribute(HTML::AttributeNames::form);
  129. html_element.root().for_each_in_inclusive_subtree_of_type<HTMLFormElement>([this, &form_value](HTMLFormElement& form_element) {
  130. if (form_element.id() == form_value) {
  131. set_form(&form_element);
  132. return TraversalDecision::Break;
  133. }
  134. return TraversalDecision::Continue;
  135. });
  136. }
  137. // 5. Otherwise, if element has an ancestor form element, then associate element with the nearest such ancestor form element.
  138. else {
  139. auto* form_ancestor = html_element.first_ancestor_of_type<HTMLFormElement>();
  140. if (form_ancestor)
  141. set_form(form_ancestor);
  142. }
  143. }
  144. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-formaction
  145. String FormAssociatedElement::form_action() const
  146. {
  147. // The formAction IDL attribute must reflect the formaction content attribute, except that on getting, when the content attribute is missing or its value is the empty string,
  148. // the element's node document's URL must be returned instead.
  149. auto& html_element = form_associated_element_to_html_element();
  150. auto form_action_attribute = html_element.attribute(HTML::AttributeNames::formaction);
  151. if (!form_action_attribute.has_value() || form_action_attribute.value().is_empty()) {
  152. return html_element.document().url_string();
  153. }
  154. auto document_base_url = html_element.document().base_url();
  155. return MUST(document_base_url.complete_url(form_action_attribute.value()).to_string());
  156. }
  157. WebIDL::ExceptionOr<void> FormAssociatedElement::set_form_action(String const& value)
  158. {
  159. auto& html_element = form_associated_element_to_html_element();
  160. return html_element.set_attribute(HTML::AttributeNames::formaction, value);
  161. }
  162. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
  163. void FormAssociatedTextControlElement::relevant_value_was_changed(JS::GCPtr<DOM::Text> text_node)
  164. {
  165. auto the_relevant_value = relevant_value();
  166. auto relevant_value_length = the_relevant_value.code_points().length();
  167. // 1. If the element has a selection:
  168. if (m_selection_start < m_selection_end) {
  169. // 1. If the start of the selection is now past the end of the relevant value, set it to
  170. // the end of the relevant value.
  171. if (m_selection_start > relevant_value_length)
  172. m_selection_start = relevant_value_length;
  173. // 2. If the end of the selection is now past the end of the relevant value, set it to the
  174. // end of the relevant value.
  175. if (m_selection_end > relevant_value_length)
  176. m_selection_end = relevant_value_length;
  177. // 3. If the user agent does not support empty selection, and both the start and end of the
  178. // selection are now pointing to the end of the relevant value, then instead set the
  179. // element's text entry cursor position to the end of the relevant value, removing any
  180. // selection.
  181. // NOTE: We support empty selections.
  182. return;
  183. }
  184. // 2. Otherwise, the element must have a text entry cursor position position. If it is now past
  185. // the end of the relevant value, set it to the end of the relevant value.
  186. auto& document = form_associated_element_to_html_element().document();
  187. auto const current_cursor_position = document.cursor_position();
  188. if (current_cursor_position && text_node
  189. && current_cursor_position->node() == text_node
  190. && current_cursor_position->offset() > relevant_value_length) {
  191. document.set_cursor_position(DOM::Position::create(document.realm(), *text_node, relevant_value_length));
  192. }
  193. }
  194. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-select
  195. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::select()
  196. {
  197. // 1. If this element is an input element, and either select() does not apply to this element
  198. // or the corresponding control has no selectable text, return.
  199. auto& html_element = form_associated_element_to_html_element();
  200. if (is<HTMLInputElement>(html_element)) {
  201. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  202. // FIXME: implement "or the corresponding control has no selectable text"
  203. if (!input_element.select_applies())
  204. return {};
  205. }
  206. // 2. Set the selection range with 0 and infinity.
  207. set_the_selection_range(0, NumericLimits<WebIDL::UnsignedLong>::max());
  208. return {};
  209. }
  210. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart
  211. Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_start() const
  212. {
  213. // 1. If this element is an input element, and selectionStart does not apply to this element, return null.
  214. auto const& html_element = form_associated_element_to_html_element();
  215. if (is<HTMLInputElement>(html_element)) {
  216. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  217. if (!input_element.selection_or_range_applies())
  218. return {};
  219. }
  220. // 2. If there is no selection, return the code unit offset within the relevant value to the character that
  221. // immediately follows the text entry cursor.
  222. if (m_selection_start == m_selection_end) {
  223. if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
  224. return cursor->offset();
  225. }
  226. // 3. Return the code unit offset within the relevant value to the character that immediately follows the start of
  227. // the selection.
  228. return m_selection_start;
  229. }
  230. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionstart-2
  231. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_start(Optional<WebIDL::UnsignedLong> const& value)
  232. {
  233. // 1. If this element is an input element, and selectionStart does not apply to this element,
  234. // throw an "InvalidStateError" DOMException.
  235. auto& html_element = form_associated_element_to_html_element();
  236. if (is<HTMLInputElement>(html_element)) {
  237. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  238. if (!input_element.selection_or_range_applies())
  239. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionStart does not apply to this input type"_fly_string);
  240. }
  241. // 2. Let end be the value of this element's selectionEnd attribute.
  242. auto end = m_selection_end;
  243. // 3. If end is less than the given value, set end to the given value.
  244. if (value.has_value() && end < value.value())
  245. end = value.value();
  246. // 4. Set the selection range with the given value, end, and the value of this element's
  247. // selectionDirection attribute.
  248. set_the_selection_range(value, end, selection_direction_state());
  249. return {};
  250. }
  251. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionend
  252. Optional<WebIDL::UnsignedLong> FormAssociatedTextControlElement::selection_end() const
  253. {
  254. // 1. If this element is an input element, and selectionEnd does not apply to this element, return
  255. // null.
  256. auto const& html_element = form_associated_element_to_html_element();
  257. if (is<HTMLInputElement>(html_element)) {
  258. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  259. if (!input_element.selection_or_range_applies())
  260. return {};
  261. }
  262. // 2. If there is no selection, return the code unit offset within the relevant value to the
  263. // character that immediately follows the text entry cursor.
  264. if (m_selection_start == m_selection_end) {
  265. if (auto cursor = form_associated_element_to_html_element().document().cursor_position())
  266. return cursor->offset();
  267. }
  268. // 3. Return the code unit offset within the relevant value to the character that immediately
  269. // follows the end of the selection.
  270. return m_selection_end;
  271. }
  272. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection:dom-textarea/input-selectionend-3
  273. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_end(Optional<WebIDL::UnsignedLong> const& value)
  274. {
  275. // 1. If this element is an input element, and selectionEnd does not apply to this element,
  276. // throw an "InvalidStateError" DOMException.
  277. auto& html_element = form_associated_element_to_html_element();
  278. if (is<HTMLInputElement>(html_element)) {
  279. auto& input_element = static_cast<HTMLInputElement&>(html_element);
  280. if (!input_element.selection_or_range_applies())
  281. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionEnd does not apply to this input type"_fly_string);
  282. }
  283. // 2. Set the selection range with the value of this element's selectionStart attribute, the
  284. // given value, and the value of this element's selectionDirection attribute.
  285. set_the_selection_range(m_selection_start, value, selection_direction_state());
  286. return {};
  287. }
  288. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
  289. Optional<String> FormAssociatedTextControlElement::selection_direction() const
  290. {
  291. // 1. If this element is an input element, and selectionDirection does not apply to this
  292. // element, return null.
  293. auto const& html_element = form_associated_element_to_html_element();
  294. if (is<HTMLInputElement>(html_element)) {
  295. auto const& input_element = static_cast<HTMLInputElement const&>(html_element);
  296. if (!input_element.selection_or_range_applies())
  297. return {};
  298. }
  299. // 2. Return this element's selection direction.
  300. switch (m_selection_direction) {
  301. case SelectionDirection::Forward:
  302. return "forward"_string;
  303. case SelectionDirection::Backward:
  304. return "backward"_string;
  305. case SelectionDirection::None:
  306. return "none"_string;
  307. default:
  308. VERIFY_NOT_REACHED();
  309. }
  310. }
  311. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-direction
  312. void FormAssociatedTextControlElement::set_selection_direction(Optional<String> direction)
  313. {
  314. // To set the selection direction of an element to a given direction, update the element's
  315. // selection direction to the given direction, unless the direction is "none" and the
  316. // platform does not support that direction; in that case, update the element's selection
  317. // direction to "forward".
  318. m_selection_direction = string_to_selection_direction(direction);
  319. }
  320. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
  321. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement)
  322. {
  323. return set_range_text(replacement, m_selection_start, m_selection_end);
  324. }
  325. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setrangetext
  326. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_range_text(String const& replacement, WebIDL::UnsignedLong start, WebIDL::UnsignedLong end, Bindings::SelectionMode selection_mode)
  327. {
  328. // 1. If this element is an input element, and setRangeText() does not apply to this element,
  329. // throw an "InvalidStateError" DOMException.
  330. auto& html_element = form_associated_element_to_html_element();
  331. if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
  332. return WebIDL::InvalidStateError::create(html_element.realm(), "setRangeText does not apply to this input type"_fly_string);
  333. // 2. Set this element's dirty value flag to true.
  334. set_dirty_value_flag(true);
  335. // 3. If the method has only one argument, then let start and end have the values of the selectionStart attribute and the selectionEnd attribute respectively.
  336. // Otherwise, let start, end have the values of the second and third arguments respectively.
  337. // NOTE: This is handled by the caller.
  338. // 4. If start is greater than end, then throw an "IndexSizeError" DOMException.
  339. if (start > end)
  340. return WebIDL::IndexSizeError::create(html_element.realm(), "The start argument must be less than or equal to the end argument"_fly_string);
  341. // 5. If start is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
  342. auto the_relevant_value = relevant_value();
  343. auto relevant_value_length = the_relevant_value.code_points().length();
  344. if (start > relevant_value_length)
  345. start = relevant_value_length;
  346. // 6. If end is greater than the length of the relevant value of the text control, then set it to the length of the relevant value of the text control.
  347. if (end > relevant_value_length)
  348. end = relevant_value_length;
  349. // 7. Let selection start be the current value of the selectionStart attribute.
  350. auto selection_start = m_selection_start;
  351. // 8. Let selection end be the current value of the selectionEnd attribute.
  352. auto selection_end = m_selection_end;
  353. // 9. If start is less than end, delete the sequence of code units within the element's relevant value starting with
  354. // the code unit at the startth position and ending with the code unit at the (end-1)th position.
  355. if (start < end) {
  356. StringBuilder builder;
  357. auto before_removal_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
  358. builder.append(before_removal_point_view.as_string());
  359. auto after_removal_point_view = the_relevant_value.code_points().unicode_substring_view(end);
  360. builder.append(after_removal_point_view.as_string());
  361. the_relevant_value = MUST(builder.to_string());
  362. }
  363. // 10. Insert the value of the first argument into the text of the relevant value of the text control, immediately before the startth code unit.
  364. StringBuilder builder;
  365. auto before_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(0, start);
  366. builder.append(before_insertion_point_view.as_string());
  367. builder.append(replacement);
  368. auto after_insertion_point_view = the_relevant_value.code_points().unicode_substring_view(start);
  369. builder.append(after_insertion_point_view.as_string());
  370. the_relevant_value = MUST(builder.to_string());
  371. TRY(set_relevant_value(the_relevant_value));
  372. // 11. Let new length be the length of the value of the first argument.
  373. i64 new_length = replacement.code_points().length();
  374. // 12. Let new end be the sum of start and new length.
  375. auto new_end = start + new_length;
  376. // 13. Run the appropriate set of substeps from the following list:
  377. switch (selection_mode) {
  378. // If the fourth argument's value is "select"
  379. case Bindings::SelectionMode::Select:
  380. // Let selection start be start.
  381. selection_start = start;
  382. // Let selection end be new end.
  383. selection_end = new_end;
  384. break;
  385. // If the fourth argument's value is "start"
  386. case Bindings::SelectionMode::Start:
  387. // Let selection start and selection end be start.
  388. selection_start = start;
  389. selection_end = start;
  390. break;
  391. // If the fourth argument's value is "end"
  392. case Bindings::SelectionMode::End:
  393. selection_start = new_end;
  394. selection_end = new_end;
  395. break;
  396. // If the fourth argument's value is "preserve"
  397. case Bindings::SelectionMode::Preserve:
  398. // 1. Let old length be end minus start.
  399. auto old_length = end - start;
  400. // 2. Let delta be new length minus old length.
  401. auto delta = new_length - old_length;
  402. // 3. If selection start is greater than end, then increment it by delta.
  403. // (If delta is negative, i.e. the new text is shorter than the old text, then this will decrease the value of selection start.)
  404. // Otherwise: if selection start is greater than start, then set it to start.
  405. // (This snaps the start of the selection to the start of the new text if it was in the middle of the text that it replaced.)
  406. if (selection_start > end)
  407. selection_start += delta;
  408. else if (selection_start > start)
  409. selection_start = start;
  410. // 4. If selection end is greater than end, then increment it by delta in the same way.
  411. // Otherwise: if selection end is greater than start, then set it to new end.
  412. // (This snaps the end of the selection to the end of the new text if it was in the middle of the text that it replaced.)
  413. if (selection_end > end)
  414. selection_end += delta;
  415. else if (selection_end > start)
  416. selection_end = new_end;
  417. break;
  418. }
  419. // 14. Set the selection range with selection start and selection end.
  420. set_the_selection_range(selection_start, selection_end);
  421. return {};
  422. }
  423. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange
  424. WebIDL::ExceptionOr<void> FormAssociatedTextControlElement::set_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, Optional<String> direction)
  425. {
  426. // 1. If this element is an input element, and setSelectionRange() does not apply to this
  427. // element, throw an "InvalidStateError" DOMException.
  428. auto& html_element = form_associated_element_to_html_element();
  429. if (is<HTMLInputElement>(html_element) && !static_cast<HTMLInputElement&>(html_element).selection_or_range_applies())
  430. return WebIDL::InvalidStateError::create(html_element.realm(), "setSelectionRange does not apply to this input type"_fly_string);
  431. // 2. Set the selection range with start, end, and direction.
  432. set_the_selection_range(start, end, string_to_selection_direction(direction));
  433. return {};
  434. }
  435. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#set-the-selection-range
  436. void FormAssociatedTextControlElement::set_the_selection_range(Optional<WebIDL::UnsignedLong> start, Optional<WebIDL::UnsignedLong> end, SelectionDirection direction)
  437. {
  438. // 1. If start is null, let start be zero.
  439. start = start.value_or(0);
  440. // 2. If end is null, let end be zero.
  441. end = end.value_or(0);
  442. // 3. Set the selection of the text control to the sequence of code units within the relevant
  443. // value starting with the code unit at the startth position (in logical order) and ending
  444. // with the code unit at the (end-1)th position. Arguments greater than the length of the
  445. // relevant value of the text control (including the special value infinity) must be treated
  446. // as pointing at the end of the text control.
  447. auto the_relevant_value = relevant_value();
  448. auto relevant_value_length = the_relevant_value.code_points().length();
  449. auto new_selection_start = AK::min(start.value(), relevant_value_length);
  450. auto new_selection_end = AK::min(end.value(), relevant_value_length);
  451. // If end is less than or equal to start then the start of the selection and the end of the
  452. // selection must both be placed immediately before the character with offset end. In UAs
  453. // where there is no concept of an empty selection, this must set the cursor to be just
  454. // before the character with offset end.
  455. new_selection_start = AK::min(new_selection_start, new_selection_end);
  456. bool was_modified = m_selection_start != new_selection_start || m_selection_end != new_selection_end;
  457. m_selection_start = new_selection_start;
  458. m_selection_end = new_selection_end;
  459. // 4. If direction is not identical to either "backward" or "forward", or if the direction
  460. // argument was not given, set direction to "none".
  461. // NOTE: This is handled by the argument's default value and ::string_to_selection_direction().
  462. // 5. Set the selection direction of the text control to direction.
  463. was_modified |= m_selection_direction != direction;
  464. m_selection_direction = direction;
  465. // 6. If the previous steps caused the selection of the text control to be modified (in either
  466. // extent or direction), then queue an element task on the user interaction task source
  467. // given the element to fire an event named select at the element, with the bubbles attribute
  468. // initialized to true.
  469. if (was_modified) {
  470. auto& html_element = form_associated_element_to_html_element();
  471. // AD-HOC: If there is no selection, we do not fire the event. This seems to correspond to how
  472. // other browsers behave.
  473. if (m_selection_start != m_selection_end) {
  474. html_element.queue_an_element_task(Task::Source::UserInteraction, [&html_element] {
  475. auto select_event = DOM::Event::create(html_element.realm(), EventNames::select, { .bubbles = true });
  476. static_cast<DOM::EventTarget*>(&html_element)->dispatch_event(select_event);
  477. });
  478. }
  479. // AD-HOC: Notify the element that the selection was changed, so it can perform
  480. // element-specific updates.
  481. selection_was_changed(m_selection_start, m_selection_end);
  482. }
  483. }
  484. }