Coverage Report

Created: 2025-12-18 07:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}