Coverage Report

Created: 2025-11-16 07:46

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