HTMLTextAreaElement.cpp 19 KB

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