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