HTMLTextAreaElement.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. /*
  2. * Copyright (c) 2020, the SerenityOS developers.
  3. * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
  4. * Copyright (c) 2024, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
  5. * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
  6. *
  7. * SPDX-License-Identifier: BSD-2-Clause
  8. */
  9. #include <AK/Utf16View.h>
  10. #include <LibWeb/Bindings/HTMLTextAreaElementPrototype.h>
  11. #include <LibWeb/Bindings/Intrinsics.h>
  12. #include <LibWeb/CSS/StyleProperties.h>
  13. #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
  14. #include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
  15. #include <LibWeb/DOM/Document.h>
  16. #include <LibWeb/DOM/ElementFactory.h>
  17. #include <LibWeb/DOM/Event.h>
  18. #include <LibWeb/DOM/ShadowRoot.h>
  19. #include <LibWeb/DOM/Text.h>
  20. #include <LibWeb/HTML/HTMLTextAreaElement.h>
  21. #include <LibWeb/HTML/Numbers.h>
  22. #include <LibWeb/Infra/Strings.h>
  23. #include <LibWeb/Namespace.h>
  24. #include <LibWeb/Painting/Paintable.h>
  25. #include <LibWeb/Selection/Selection.h>
  26. namespace Web::HTML {
  27. GC_DEFINE_ALLOCATOR(HTMLTextAreaElement);
  28. HTMLTextAreaElement::HTMLTextAreaElement(DOM::Document& document, DOM::QualifiedName qualified_name)
  29. : HTMLElement(document, move(qualified_name))
  30. , m_input_event_timer(Core::Timer::create_single_shot(0, [weak_this = make_weak_ptr()]() {
  31. if (!weak_this)
  32. return;
  33. static_cast<HTMLTextAreaElement*>(weak_this.ptr())->queue_firing_input_event();
  34. }))
  35. {
  36. }
  37. HTMLTextAreaElement::~HTMLTextAreaElement() = default;
  38. void HTMLTextAreaElement::adjust_computed_style(CSS::StyleProperties& style)
  39. {
  40. // https://drafts.csswg.org/css-display-3/#unbox
  41. if (style.display().is_contents())
  42. style.set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::None)));
  43. // AD-HOC: We rewrite `display: inline` to `display: inline-block`.
  44. // This is required for the internal shadow tree to work correctly in layout.
  45. if (style.display().is_inline_outside() && style.display().is_flow_inside())
  46. style.set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::InlineBlock)));
  47. if (style.property(CSS::PropertyID::Width).has_auto())
  48. style.set_property(CSS::PropertyID::Width, CSS::LengthStyleValue::create(CSS::Length(cols(), CSS::Length::Type::Ch)));
  49. if (style.property(CSS::PropertyID::Height).has_auto())
  50. style.set_property(CSS::PropertyID::Height, CSS::LengthStyleValue::create(CSS::Length(rows(), CSS::Length::Type::Lh)));
  51. }
  52. void HTMLTextAreaElement::initialize(JS::Realm& realm)
  53. {
  54. Base::initialize(realm);
  55. WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLTextAreaElement);
  56. }
  57. void HTMLTextAreaElement::visit_edges(Cell::Visitor& visitor)
  58. {
  59. Base::visit_edges(visitor);
  60. visitor.visit(m_placeholder_element);
  61. visitor.visit(m_placeholder_text_node);
  62. visitor.visit(m_inner_text_element);
  63. visitor.visit(m_text_node);
  64. }
  65. void HTMLTextAreaElement::did_receive_focus()
  66. {
  67. if (!m_text_node)
  68. return;
  69. m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
  70. if (m_placeholder_text_node)
  71. m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidReceiveFocus);
  72. }
  73. void HTMLTextAreaElement::did_lose_focus()
  74. {
  75. if (m_text_node)
  76. m_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
  77. if (m_placeholder_text_node)
  78. m_placeholder_text_node->invalidate_style(DOM::StyleInvalidationReason::DidLoseFocus);
  79. // The change event fires when the value is committed, if that makes sense for the control,
  80. // or else when the control loses focus
  81. queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
  82. auto change_event = DOM::Event::create(realm(), HTML::EventNames::change);
  83. change_event->set_bubbles(true);
  84. dispatch_event(change_event);
  85. });
  86. }
  87. // https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
  88. i32 HTMLTextAreaElement::default_tab_index_value() const
  89. {
  90. // See the base function for the spec comments.
  91. return 0;
  92. }
  93. // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-form-reset-control
  94. void HTMLTextAreaElement::reset_algorithm()
  95. {
  96. // The reset algorithm for textarea elements is to set the dirty value flag back to false,
  97. m_dirty_value = false;
  98. // and set the raw value of element to its child text content.
  99. set_raw_value(child_text_content());
  100. if (m_text_node) {
  101. m_text_node->set_text_content(m_raw_value);
  102. update_placeholder_visibility();
  103. }
  104. }
  105. // https://w3c.github.io/webdriver/#dfn-clear-algorithm
  106. void HTMLTextAreaElement::clear_algorithm()
  107. {
  108. // The clear algorithm for textarea elements is to set the dirty value flag back to false,
  109. m_dirty_value = false;
  110. // and set the raw value of element to an empty string.
  111. set_raw_value(child_text_content());
  112. // Unlike their associated reset algorithms, changes made to form controls as part of these algorithms do count as
  113. // changes caused by the user (and thus, e.g. do cause input events to fire).
  114. queue_firing_input_event();
  115. }
  116. // https://html.spec.whatwg.org/multipage/forms.html#the-textarea-element:concept-node-clone-ext
  117. WebIDL::ExceptionOr<void> HTMLTextAreaElement::cloned(DOM::Node& copy, bool)
  118. {
  119. // The cloning steps for textarea elements must propagate the raw value and dirty value flag from the node being cloned to the copy.
  120. auto& textarea_copy = verify_cast<HTMLTextAreaElement>(copy);
  121. textarea_copy.m_raw_value = m_raw_value;
  122. textarea_copy.m_dirty_value = m_dirty_value;
  123. return {};
  124. }
  125. void HTMLTextAreaElement::form_associated_element_was_inserted()
  126. {
  127. create_shadow_tree_if_needed();
  128. }
  129. void HTMLTextAreaElement::form_associated_element_was_removed(DOM::Node*)
  130. {
  131. set_shadow_root(nullptr);
  132. }
  133. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-defaultvalue
  134. String HTMLTextAreaElement::default_value() const
  135. {
  136. // The defaultValue attribute's getter must return the element's child text content.
  137. return child_text_content();
  138. }
  139. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-defaultvalue
  140. void HTMLTextAreaElement::set_default_value(String const& default_value)
  141. {
  142. // The defaultValue attribute's setter must string replace all with the given value within this element.
  143. string_replace_all(default_value);
  144. }
  145. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-value
  146. String HTMLTextAreaElement::value() const
  147. {
  148. // The value IDL attribute must, on getting, return the element's API value.
  149. return api_value();
  150. }
  151. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-value
  152. void HTMLTextAreaElement::set_value(String const& value)
  153. {
  154. // 1. Let oldAPIValue be this element's API value.
  155. auto old_api_value = api_value();
  156. // 2. Set this element's raw value to the new value.
  157. set_raw_value(value);
  158. // 3. Set this element's dirty value flag to true.
  159. m_dirty_value = true;
  160. // 4. If the new API value is different from oldAPIValue, then move the text entry cursor position to the end of
  161. // the text control, unselecting any selected text and resetting the selection direction to "none".
  162. if (api_value() != old_api_value) {
  163. if (m_text_node) {
  164. m_text_node->set_data(m_raw_value);
  165. update_placeholder_visibility();
  166. set_the_selection_range(m_text_node->length(), m_text_node->length());
  167. }
  168. }
  169. }
  170. void HTMLTextAreaElement::set_raw_value(String value)
  171. {
  172. auto old_raw_value = move(m_raw_value);
  173. m_raw_value = move(value);
  174. m_api_value.clear();
  175. if (m_raw_value != old_raw_value)
  176. relevant_value_was_changed();
  177. }
  178. // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-fe-api-value-3
  179. String HTMLTextAreaElement::api_value() const
  180. {
  181. // The algorithm for obtaining the element's API value is to return the element's raw value, with newlines normalized.
  182. if (!m_api_value.has_value())
  183. m_api_value = Infra::normalize_newlines(m_raw_value);
  184. return *m_api_value;
  185. }
  186. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value
  187. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_relevant_value(String const& value)
  188. {
  189. set_value(value);
  190. return {};
  191. }
  192. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-textlength
  193. u32 HTMLTextAreaElement::text_length() const
  194. {
  195. // The textLength IDL attribute must return the length of the element's API value.
  196. return AK::utf16_code_unit_length_from_utf8(api_value());
  197. }
  198. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-checkvalidity
  199. bool HTMLTextAreaElement::check_validity()
  200. {
  201. dbgln("(STUBBED) HTMLTextAreaElement::check_validity(). Called on: {}", debug_description());
  202. return true;
  203. }
  204. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity
  205. bool HTMLTextAreaElement::report_validity()
  206. {
  207. dbgln("(STUBBED) HTMLTextAreaElement::report_validity(). Called on: {}", debug_description());
  208. return true;
  209. }
  210. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-setcustomvalidity
  211. void HTMLTextAreaElement::set_custom_validity(String const& error)
  212. {
  213. dbgln("(STUBBED) HTMLTextAreaElement::set_custom_validity(\"{}\"). Called on: {}", error, debug_description());
  214. }
  215. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-maxlength
  216. WebIDL::Long HTMLTextAreaElement::max_length() const
  217. {
  218. // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers.
  219. if (auto maxlength_string = get_attribute(HTML::AttributeNames::maxlength); maxlength_string.has_value()) {
  220. if (auto maxlength = parse_non_negative_integer(*maxlength_string); maxlength.has_value())
  221. return *maxlength;
  222. }
  223. return -1;
  224. }
  225. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_max_length(WebIDL::Long value)
  226. {
  227. // The maxLength IDL attribute must reflect the maxlength content attribute, limited to only non-negative numbers.
  228. return set_attribute(HTML::AttributeNames::maxlength, TRY(convert_non_negative_integer_to_string(realm(), value)));
  229. }
  230. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-minlength
  231. WebIDL::Long HTMLTextAreaElement::min_length() const
  232. {
  233. // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers.
  234. if (auto minlength_string = get_attribute(HTML::AttributeNames::minlength); minlength_string.has_value()) {
  235. if (auto minlength = parse_non_negative_integer(*minlength_string); minlength.has_value())
  236. return *minlength;
  237. }
  238. return -1;
  239. }
  240. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_min_length(WebIDL::Long value)
  241. {
  242. // The minLength IDL attribute must reflect the minlength content attribute, limited to only non-negative numbers.
  243. return set_attribute(HTML::AttributeNames::minlength, TRY(convert_non_negative_integer_to_string(realm(), value)));
  244. }
  245. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-cols
  246. unsigned HTMLTextAreaElement::cols() const
  247. {
  248. // The cols and rows attributes are limited to only positive numbers with fallback. The cols IDL attribute's default value is 20.
  249. if (auto cols_string = get_attribute(HTML::AttributeNames::cols); cols_string.has_value()) {
  250. if (auto cols = parse_non_negative_integer(*cols_string); cols.has_value() && *cols > 0 && *cols <= 2147483647)
  251. return *cols;
  252. }
  253. return 20;
  254. }
  255. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_cols(WebIDL::UnsignedLong cols)
  256. {
  257. if (cols == 0 || cols > 2147483647)
  258. cols = 20;
  259. return set_attribute(HTML::AttributeNames::cols, String::number(cols));
  260. }
  261. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-rows
  262. WebIDL::UnsignedLong HTMLTextAreaElement::rows() const
  263. {
  264. // The cols and rows attributes are limited to only positive numbers with fallback. The rows IDL attribute's default value is 2.
  265. if (auto rows_string = get_attribute(HTML::AttributeNames::rows); rows_string.has_value()) {
  266. if (auto rows = parse_non_negative_integer(*rows_string); rows.has_value() && *rows > 0 && *rows <= 2147483647)
  267. return *rows;
  268. }
  269. return 2;
  270. }
  271. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_rows(WebIDL::UnsignedLong rows)
  272. {
  273. if (rows == 0 || rows > 2147483647)
  274. rows = 2;
  275. return set_attribute(HTML::AttributeNames::rows, String::number(rows));
  276. }
  277. WebIDL::UnsignedLong HTMLTextAreaElement::selection_start_binding() const
  278. {
  279. return selection_start().value();
  280. }
  281. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_selection_start_binding(WebIDL::UnsignedLong const& value)
  282. {
  283. return set_selection_start(value);
  284. }
  285. WebIDL::UnsignedLong HTMLTextAreaElement::selection_end_binding() const
  286. {
  287. return selection_end().value();
  288. }
  289. WebIDL::ExceptionOr<void> HTMLTextAreaElement::set_selection_end_binding(WebIDL::UnsignedLong const& value)
  290. {
  291. return set_selection_end(value);
  292. }
  293. String HTMLTextAreaElement::selection_direction_binding() const
  294. {
  295. return selection_direction().value();
  296. }
  297. void HTMLTextAreaElement::set_selection_direction_binding(String const& direction)
  298. {
  299. // NOTE: The selectionDirection setter never returns an error for textarea elements.
  300. MUST(static_cast<FormAssociatedTextControlElement&>(*this).set_selection_direction_binding(direction));
  301. }
  302. void HTMLTextAreaElement::create_shadow_tree_if_needed()
  303. {
  304. if (shadow_root())
  305. return;
  306. auto shadow_root = realm().create<DOM::ShadowRoot>(document(), *this, Bindings::ShadowRootMode::Closed);
  307. set_shadow_root(shadow_root);
  308. auto element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
  309. MUST(shadow_root->append_child(element));
  310. m_placeholder_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
  311. m_placeholder_element->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::Placeholder);
  312. MUST(element->append_child(*m_placeholder_element));
  313. m_placeholder_text_node = realm().create<DOM::Text>(document(), String {});
  314. m_placeholder_text_node->set_data(get_attribute_value(HTML::AttributeNames::placeholder));
  315. MUST(m_placeholder_element->append_child(*m_placeholder_text_node));
  316. m_inner_text_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
  317. MUST(element->append_child(*m_inner_text_element));
  318. m_text_node = realm().create<DOM::Text>(document(), String {});
  319. handle_readonly_attribute(attribute(HTML::AttributeNames::readonly));
  320. // NOTE: If `children_changed()` was called before now, `m_raw_value` will hold the text content.
  321. // Otherwise, it will get filled in whenever that does get called.
  322. m_text_node->set_text_content(m_raw_value);
  323. handle_maxlength_attribute();
  324. MUST(m_inner_text_element->append_child(*m_text_node));
  325. update_placeholder_visibility();
  326. }
  327. // https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly
  328. void HTMLTextAreaElement::handle_readonly_attribute(Optional<String> const& maybe_value)
  329. {
  330. // The readonly attribute is a boolean attribute that controls whether or not the user can edit the form control. When specified, the element is not mutable.
  331. m_is_mutable = !maybe_value.has_value();
  332. if (m_text_node)
  333. m_text_node->set_always_editable(m_is_mutable);
  334. }
  335. // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-maxlength
  336. void HTMLTextAreaElement::handle_maxlength_attribute()
  337. {
  338. if (m_text_node) {
  339. auto max_length = this->max_length();
  340. if (max_length >= 0) {
  341. m_text_node->set_max_length(max_length);
  342. } else {
  343. m_text_node->set_max_length({});
  344. }
  345. }
  346. }
  347. void HTMLTextAreaElement::update_placeholder_visibility()
  348. {
  349. if (!m_placeholder_element)
  350. return;
  351. if (!m_text_node)
  352. return;
  353. auto placeholder_text = get_attribute(AttributeNames::placeholder);
  354. if (placeholder_text.has_value() && m_text_node->data().is_empty()) {
  355. MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "block"sv));
  356. MUST(m_inner_text_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "none"sv));
  357. } else {
  358. MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "none"sv));
  359. MUST(m_inner_text_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "block"sv));
  360. }
  361. }
  362. // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:children-changed-steps
  363. void HTMLTextAreaElement::children_changed()
  364. {
  365. // The children changed steps for textarea elements must, if the element's dirty value flag is false,
  366. // set the element's raw value to its child text content.
  367. if (!m_dirty_value) {
  368. set_raw_value(child_text_content());
  369. if (m_text_node)
  370. m_text_node->set_text_content(m_raw_value);
  371. update_placeholder_visibility();
  372. }
  373. }
  374. void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value)
  375. {
  376. if (name == HTML::AttributeNames::placeholder) {
  377. if (m_placeholder_text_node)
  378. m_placeholder_text_node->set_data(value.value_or(String {}));
  379. } else if (name == HTML::AttributeNames::readonly) {
  380. handle_readonly_attribute(value);
  381. } else if (name == HTML::AttributeNames::maxlength) {
  382. handle_maxlength_attribute();
  383. }
  384. }
  385. void HTMLTextAreaElement::did_edit_text_node()
  386. {
  387. VERIFY(m_text_node);
  388. set_raw_value(m_text_node->data());
  389. // Any time the user causes the element's raw value to change, the user agent must queue an element task on the user
  390. // interaction task source given the textarea element to fire an event named input at the textarea element, with the
  391. // bubbles and composed attributes initialized to true. User agents may wait for a suitable break in the user's
  392. // interaction before queuing the task; for example, a user agent could wait for the user to have not hit a key for
  393. // 100ms, so as to only fire the event when the user pauses, instead of continuously for each keystroke.
  394. m_input_event_timer->restart(100);
  395. // A textarea element's dirty value flag must be set to true whenever the user interacts with the control in a way that changes the raw value.
  396. m_dirty_value = true;
  397. update_placeholder_visibility();
  398. }
  399. void HTMLTextAreaElement::queue_firing_input_event()
  400. {
  401. queue_an_element_task(HTML::Task::Source::UserInteraction, [this]() {
  402. auto change_event = DOM::Event::create(realm(), HTML::EventNames::input, { .bubbles = true, .composed = true });
  403. dispatch_event(change_event);
  404. });
  405. }
  406. bool HTMLTextAreaElement::is_focusable() const
  407. {
  408. return enabled();
  409. }
  410. }