/src/serenity/Userland/Libraries/LibWeb/HTML/HTMLElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/StringBuilder.h> |
8 | | #include <LibWeb/ARIA/Roles.h> |
9 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
10 | | #include <LibWeb/Bindings/HTMLElementPrototype.h> |
11 | | #include <LibWeb/DOM/Document.h> |
12 | | #include <LibWeb/DOM/ElementFactory.h> |
13 | | #include <LibWeb/DOM/IDLEventListener.h> |
14 | | #include <LibWeb/DOM/LiveNodeList.h> |
15 | | #include <LibWeb/DOM/ShadowRoot.h> |
16 | | #include <LibWeb/HTML/BrowsingContext.h> |
17 | | #include <LibWeb/HTML/CustomElements/CustomElementDefinition.h> |
18 | | #include <LibWeb/HTML/DOMStringMap.h> |
19 | | #include <LibWeb/HTML/ElementInternals.h> |
20 | | #include <LibWeb/HTML/EventHandler.h> |
21 | | #include <LibWeb/HTML/Focus.h> |
22 | | #include <LibWeb/HTML/HTMLAnchorElement.h> |
23 | | #include <LibWeb/HTML/HTMLAreaElement.h> |
24 | | #include <LibWeb/HTML/HTMLBaseElement.h> |
25 | | #include <LibWeb/HTML/HTMLBodyElement.h> |
26 | | #include <LibWeb/HTML/HTMLElement.h> |
27 | | #include <LibWeb/HTML/HTMLLabelElement.h> |
28 | | #include <LibWeb/HTML/NavigableContainer.h> |
29 | | #include <LibWeb/HTML/VisibilityState.h> |
30 | | #include <LibWeb/HTML/Window.h> |
31 | | #include <LibWeb/Infra/CharacterTypes.h> |
32 | | #include <LibWeb/Infra/Strings.h> |
33 | | #include <LibWeb/Layout/Box.h> |
34 | | #include <LibWeb/Layout/BreakNode.h> |
35 | | #include <LibWeb/Layout/TextNode.h> |
36 | | #include <LibWeb/Namespace.h> |
37 | | #include <LibWeb/Painting/PaintableBox.h> |
38 | | #include <LibWeb/UIEvents/EventNames.h> |
39 | | #include <LibWeb/UIEvents/FocusEvent.h> |
40 | | #include <LibWeb/UIEvents/MouseEvent.h> |
41 | | #include <LibWeb/UIEvents/PointerEvent.h> |
42 | | #include <LibWeb/WebIDL/DOMException.h> |
43 | | #include <LibWeb/WebIDL/ExceptionOr.h> |
44 | | |
45 | | namespace Web::HTML { |
46 | | |
47 | | JS_DEFINE_ALLOCATOR(HTMLElement); |
48 | | |
49 | | HTMLElement::HTMLElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
50 | 0 | : Element(document, move(qualified_name)) |
51 | 0 | { |
52 | 0 | } |
53 | | |
54 | 0 | HTMLElement::~HTMLElement() = default; |
55 | | |
56 | | void HTMLElement::initialize(JS::Realm& realm) |
57 | 0 | { |
58 | 0 | Base::initialize(realm); |
59 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLElement); |
60 | 0 | } |
61 | | |
62 | | void HTMLElement::visit_edges(Cell::Visitor& visitor) |
63 | 0 | { |
64 | 0 | Base::visit_edges(visitor); |
65 | 0 | visitor.visit(m_dataset); |
66 | 0 | visitor.visit(m_labels); |
67 | 0 | visitor.visit(m_attached_internals); |
68 | 0 | } |
69 | | |
70 | | JS::NonnullGCPtr<DOMStringMap> HTMLElement::dataset() |
71 | 0 | { |
72 | 0 | if (!m_dataset) |
73 | 0 | m_dataset = DOMStringMap::create(*this); |
74 | 0 | return *m_dataset; |
75 | 0 | } |
76 | | |
77 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-dir |
78 | | StringView HTMLElement::dir() const |
79 | 0 | { |
80 | | // FIXME: This should probably be `Reflect` in the IDL. |
81 | | // The dir IDL attribute on an element must reflect the dir content attribute of that element, limited to only known values. |
82 | 0 | auto dir = get_attribute_value(HTML::AttributeNames::dir); |
83 | 0 | #define __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE(keyword) \ |
84 | 0 | if (dir.equals_ignoring_ascii_case(#keyword##sv)) \ |
85 | 0 | return #keyword##sv; |
86 | 0 | ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTES |
87 | 0 | #undef __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE |
88 | | |
89 | 0 | return {}; |
90 | 0 | } |
91 | | |
92 | | void HTMLElement::set_dir(String const& dir) |
93 | 0 | { |
94 | 0 | MUST(set_attribute(HTML::AttributeNames::dir, dir)); |
95 | 0 | } |
96 | | |
97 | | bool HTMLElement::is_editable() const |
98 | 0 | { |
99 | 0 | switch (m_content_editable_state) { |
100 | 0 | case ContentEditableState::True: |
101 | 0 | return true; |
102 | 0 | case ContentEditableState::False: |
103 | 0 | return false; |
104 | 0 | case ContentEditableState::Inherit: |
105 | 0 | return parent() && parent()->is_editable(); |
106 | 0 | default: |
107 | 0 | VERIFY_NOT_REACHED(); |
108 | 0 | } |
109 | 0 | } |
110 | | |
111 | | bool HTMLElement::is_focusable() const |
112 | 0 | { |
113 | 0 | return m_content_editable_state == ContentEditableState::True; |
114 | 0 | } |
115 | | |
116 | | // https://html.spec.whatwg.org/multipage/interaction.html#dom-iscontenteditable |
117 | | bool HTMLElement::is_content_editable() const |
118 | 0 | { |
119 | | // The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or |
120 | | // editable, and false otherwise. |
121 | 0 | return is_editable(); |
122 | 0 | } |
123 | | |
124 | | StringView HTMLElement::content_editable() const |
125 | 0 | { |
126 | 0 | switch (m_content_editable_state) { |
127 | 0 | case ContentEditableState::True: |
128 | 0 | return "true"sv; |
129 | 0 | case ContentEditableState::False: |
130 | 0 | return "false"sv; |
131 | 0 | case ContentEditableState::Inherit: |
132 | 0 | return "inherit"sv; |
133 | 0 | } |
134 | 0 | VERIFY_NOT_REACHED(); |
135 | 0 | } |
136 | | |
137 | | // https://html.spec.whatwg.org/multipage/interaction.html#contenteditable |
138 | | WebIDL::ExceptionOr<void> HTMLElement::set_content_editable(StringView content_editable) |
139 | 0 | { |
140 | 0 | if (content_editable.equals_ignoring_ascii_case("inherit"sv)) { |
141 | 0 | remove_attribute(HTML::AttributeNames::contenteditable); |
142 | 0 | return {}; |
143 | 0 | } |
144 | 0 | if (content_editable.equals_ignoring_ascii_case("true"sv)) { |
145 | 0 | MUST(set_attribute(HTML::AttributeNames::contenteditable, "true"_string)); |
146 | 0 | return {}; |
147 | 0 | } |
148 | 0 | if (content_editable.equals_ignoring_ascii_case("false"sv)) { |
149 | 0 | MUST(set_attribute(HTML::AttributeNames::contenteditable, "false"_string)); |
150 | 0 | return {}; |
151 | 0 | } |
152 | 0 | return WebIDL::SyntaxError::create(realm(), "Invalid contentEditable value, must be 'true', 'false', or 'inherit'"_string); |
153 | 0 | } |
154 | | |
155 | | // https://html.spec.whatwg.org/multipage/dom.html#set-the-inner-text-steps |
156 | | void HTMLElement::set_inner_text(StringView text) |
157 | 0 | { |
158 | | // 1. Let fragment be the rendered text fragment for value given element's node document. |
159 | | // 2. Replace all with fragment within element. |
160 | 0 | remove_all_children(); |
161 | 0 | append_rendered_text_fragment(text); |
162 | |
|
163 | 0 | set_needs_style_update(true); |
164 | 0 | } |
165 | | |
166 | | // https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute:dom-outertext-2 |
167 | | WebIDL::ExceptionOr<void> HTMLElement::set_outer_text(String) |
168 | 0 | { |
169 | 0 | dbgln("FIXME: Implement HTMLElement::set_outer_text()"); |
170 | 0 | return {}; |
171 | 0 | } |
172 | | |
173 | | // https://html.spec.whatwg.org/multipage/dom.html#rendered-text-fragment |
174 | | void HTMLElement::append_rendered_text_fragment(StringView input) |
175 | 0 | { |
176 | | // FIXME: 1. Let fragment be a new DocumentFragment whose node document is document. |
177 | | // Instead of creating a DocumentFragment the nodes are appended directly. |
178 | | |
179 | | // 2. Let position be a position variable for input, initially pointing at the start of input. |
180 | | // 3. Let text be the empty string. |
181 | | // 4. While position is not past the end of input: |
182 | 0 | while (!input.is_empty()) { |
183 | | // 1. Collect a sequence of code points that are not U+000A LF or U+000D CR from input given position, and set text to the result. |
184 | 0 | auto newline_index = input.find_any_of("\n\r"sv); |
185 | 0 | size_t const sequence_end_index = newline_index.value_or(input.length()); |
186 | 0 | StringView const text = input.substring_view(0, sequence_end_index); |
187 | 0 | input = input.substring_view_starting_after_substring(text); |
188 | | |
189 | | // 2. If text is not the empty string, then append a new Text node whose data is text and node document is document to fragment. |
190 | 0 | if (!text.is_empty()) { |
191 | 0 | MUST(append_child(document().create_text_node(MUST(String::from_utf8(text))))); |
192 | 0 | } |
193 | | |
194 | | // 3. While position is not past the end of input, and the code point at position is either U+000A LF or U+000D CR: |
195 | 0 | while (input.starts_with('\n') || input.starts_with('\r')) { |
196 | | // 1. If the code point at position is U+000D CR and the next code point is U+000A LF, then advance position to the next code point in input. |
197 | 0 | if (input.starts_with("\r\n"sv)) { |
198 | | // 2. Advance position to the next code point in input. |
199 | 0 | input = input.substring_view(2); |
200 | 0 | } else { |
201 | | // 2. Advance position to the next code point in input. |
202 | 0 | input = input.substring_view(1); |
203 | 0 | } |
204 | | |
205 | | // 3. Append the result of creating an element given document, br, and the HTML namespace to fragment. |
206 | 0 | auto br_element = DOM::create_element(document(), HTML::TagNames::br, Namespace::HTML).release_value(); |
207 | 0 | MUST(append_child(br_element)); |
208 | 0 | } |
209 | 0 | } |
210 | 0 | } |
211 | | |
212 | | // https://html.spec.whatwg.org/multipage/dom.html#get-the-text-steps |
213 | | String HTMLElement::get_the_text_steps() |
214 | 0 | { |
215 | | // FIXME: Implement this according to spec. |
216 | |
|
217 | 0 | StringBuilder builder; |
218 | | |
219 | | // innerText for element being rendered takes visibility into account, so force a layout and then walk the layout tree. |
220 | 0 | document().update_layout(); |
221 | 0 | if (!layout_node()) |
222 | 0 | return text_content().value_or(String {}); |
223 | | |
224 | 0 | Function<void(Layout::Node const&)> recurse = [&](auto& node) { |
225 | 0 | for (auto* child = node.first_child(); child; child = child->next_sibling()) { |
226 | 0 | if (is<Layout::TextNode>(child)) |
227 | 0 | builder.append(verify_cast<Layout::TextNode>(*child).text_for_rendering()); |
228 | 0 | if (is<Layout::BreakNode>(child)) |
229 | 0 | builder.append('\n'); |
230 | 0 | recurse(*child); |
231 | 0 | } |
232 | 0 | }; |
233 | 0 | recurse(*layout_node()); |
234 | |
|
235 | 0 | return MUST(builder.to_string()); |
236 | 0 | } |
237 | | |
238 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-innertext |
239 | | String HTMLElement::inner_text() |
240 | 0 | { |
241 | | // The innerText and outerText getter steps are to return the result of running get the text steps with this. |
242 | 0 | return get_the_text_steps(); |
243 | 0 | } |
244 | | |
245 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-outertext |
246 | | String HTMLElement::outer_text() |
247 | 0 | { |
248 | | // The innerText and outerText getter steps are to return the result of running get the text steps with this. |
249 | 0 | return get_the_text_steps(); |
250 | 0 | } |
251 | | |
252 | | // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetparent |
253 | | JS::GCPtr<DOM::Element> HTMLElement::offset_parent() const |
254 | 0 | { |
255 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
256 | | |
257 | | // 1. If any of the following holds true return null and terminate this algorithm: |
258 | | // - The element does not have an associated CSS layout box. |
259 | | // - The element is the root element. |
260 | | // - The element is the HTML body element. |
261 | | // - The element’s computed value of the position property is fixed. |
262 | 0 | if (!layout_node()) |
263 | 0 | return nullptr; |
264 | 0 | if (is_document_element()) |
265 | 0 | return nullptr; |
266 | 0 | if (is<HTML::HTMLBodyElement>(*this)) |
267 | 0 | return nullptr; |
268 | 0 | if (layout_node()->is_fixed_position()) |
269 | 0 | return nullptr; |
270 | | |
271 | | // 2. Return the nearest ancestor element of the element for which at least one of the following is true |
272 | | // and terminate this algorithm if such an ancestor is found: |
273 | | // - The computed value of the position property is not static. |
274 | | // - It is the HTML body element. |
275 | | // - The computed value of the position property of the element is static |
276 | | // and the ancestor is one of the following HTML elements: td, th, or table. |
277 | | |
278 | 0 | for (auto* ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) { |
279 | 0 | if (!ancestor->layout_node()) |
280 | 0 | continue; |
281 | 0 | if (ancestor->layout_node()->is_positioned()) |
282 | 0 | return const_cast<Element*>(ancestor); |
283 | 0 | if (is<HTML::HTMLBodyElement>(*ancestor)) |
284 | 0 | return const_cast<Element*>(ancestor); |
285 | 0 | if (!ancestor->layout_node()->is_positioned() && ancestor->local_name().is_one_of(HTML::TagNames::td, HTML::TagNames::th, HTML::TagNames::table)) |
286 | 0 | return const_cast<Element*>(ancestor); |
287 | 0 | } |
288 | | |
289 | | // 3. Return null. |
290 | 0 | return nullptr; |
291 | 0 | } |
292 | | |
293 | | // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsettop |
294 | | int HTMLElement::offset_top() const |
295 | 0 | { |
296 | | // 1. If the element is the HTML body element or does not have any associated CSS layout box |
297 | | // return zero and terminate this algorithm. |
298 | 0 | if (is<HTML::HTMLBodyElement>(*this)) |
299 | 0 | return 0; |
300 | | |
301 | | // NOTE: Ensure that layout is up-to-date before looking at metrics. |
302 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
303 | |
|
304 | 0 | if (!layout_node()) |
305 | 0 | return 0; |
306 | | |
307 | 0 | CSSPixels top_border_edge_of_element; |
308 | 0 | if (paintable()->is_paintable_box()) { |
309 | 0 | top_border_edge_of_element = paintable_box()->absolute_border_box_rect().y(); |
310 | 0 | } else { |
311 | 0 | top_border_edge_of_element = paintable()->box_type_agnostic_position().y(); |
312 | 0 | } |
313 | | |
314 | | // 2. If the offsetParent of the element is null |
315 | | // return the y-coordinate of the top border edge of the first CSS layout box associated with the element, |
316 | | // relative to the initial containing block origin, |
317 | | // ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm. |
318 | 0 | auto offset_parent = this->offset_parent(); |
319 | 0 | if (!offset_parent || !offset_parent->layout_node()) { |
320 | 0 | return top_border_edge_of_element.to_int(); |
321 | 0 | } |
322 | | |
323 | | // 3. Return the result of subtracting the y-coordinate of the top padding edge |
324 | | // of the first box associated with the offsetParent of the element |
325 | | // from the y-coordinate of the top border edge of the first box associated with the element, |
326 | | // relative to the initial containing block origin, |
327 | | // ignoring any transforms that apply to the element and its ancestors. |
328 | | |
329 | | // NOTE: We give special treatment to the body element to match other browsers. |
330 | | // Spec bug: https://github.com/w3c/csswg-drafts/issues/10549 |
331 | | |
332 | 0 | CSSPixels top_padding_edge_of_offset_parent; |
333 | 0 | if (offset_parent->is_html_body_element() && !offset_parent->paintable()->is_positioned()) { |
334 | 0 | top_padding_edge_of_offset_parent = 0; |
335 | 0 | } else if (offset_parent->paintable()->is_paintable_box()) { |
336 | 0 | top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().y(); |
337 | 0 | } else { |
338 | 0 | top_padding_edge_of_offset_parent = offset_parent->paintable()->box_type_agnostic_position().y(); |
339 | 0 | } |
340 | 0 | return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int(); |
341 | 0 | } |
342 | | |
343 | | // https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetleft |
344 | | int HTMLElement::offset_left() const |
345 | 0 | { |
346 | | // 1. If the element is the HTML body element or does not have any associated CSS layout box return zero and terminate this algorithm. |
347 | 0 | if (is<HTML::HTMLBodyElement>(*this)) |
348 | 0 | return 0; |
349 | | |
350 | | // NOTE: Ensure that layout is up-to-date before looking at metrics. |
351 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
352 | |
|
353 | 0 | if (!layout_node()) |
354 | 0 | return 0; |
355 | | |
356 | 0 | CSSPixels left_border_edge_of_element; |
357 | 0 | if (paintable()->is_paintable_box()) { |
358 | 0 | left_border_edge_of_element = paintable_box()->absolute_border_box_rect().x(); |
359 | 0 | } else { |
360 | 0 | left_border_edge_of_element = paintable()->box_type_agnostic_position().x(); |
361 | 0 | } |
362 | | |
363 | | // 2. If the offsetParent of the element is null |
364 | | // return the x-coordinate of the left border edge of the first CSS layout box associated with the element, |
365 | | // relative to the initial containing block origin, |
366 | | // ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm. |
367 | 0 | auto offset_parent = this->offset_parent(); |
368 | 0 | if (!offset_parent || !offset_parent->layout_node()) { |
369 | 0 | return left_border_edge_of_element.to_int(); |
370 | 0 | } |
371 | | |
372 | | // 3. Return the result of subtracting the x-coordinate of the left padding edge |
373 | | // of the first CSS layout box associated with the offsetParent of the element |
374 | | // from the x-coordinate of the left border edge of the first CSS layout box associated with the element, |
375 | | // relative to the initial containing block origin, |
376 | | // ignoring any transforms that apply to the element and its ancestors. |
377 | | |
378 | | // NOTE: We give special treatment to the body element to match other browsers. |
379 | | // Spec bug: https://github.com/w3c/csswg-drafts/issues/10549 |
380 | | |
381 | 0 | CSSPixels left_padding_edge_of_offset_parent; |
382 | 0 | if (offset_parent->is_html_body_element() && !offset_parent->paintable()->is_positioned()) { |
383 | 0 | left_padding_edge_of_offset_parent = 0; |
384 | 0 | } else if (offset_parent->paintable()->is_paintable_box()) { |
385 | 0 | left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().x(); |
386 | 0 | } else { |
387 | 0 | left_padding_edge_of_offset_parent = offset_parent->paintable()->box_type_agnostic_position().x(); |
388 | 0 | } |
389 | 0 | return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int(); |
390 | 0 | } |
391 | | |
392 | | // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth |
393 | | int HTMLElement::offset_width() const |
394 | 0 | { |
395 | | // NOTE: Ensure that layout is up-to-date before looking at metrics. |
396 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
397 | | |
398 | | // 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. |
399 | 0 | if (!paintable_box()) |
400 | 0 | return 0; |
401 | | |
402 | | // 2. Return the width of the axis-aligned bounding box of the border boxes of all fragments generated by the element’s principal box, |
403 | | // ignoring any transforms that apply to the element and its ancestors. |
404 | | // FIXME: Account for inline boxes. |
405 | 0 | return paintable_box()->border_box_width().to_int(); |
406 | 0 | } |
407 | | |
408 | | // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight |
409 | | int HTMLElement::offset_height() const |
410 | 0 | { |
411 | | // NOTE: Ensure that layout is up-to-date before looking at metrics. |
412 | 0 | const_cast<DOM::Document&>(document()).update_layout(); |
413 | | |
414 | | // 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. |
415 | 0 | if (!paintable_box()) |
416 | 0 | return 0; |
417 | | |
418 | | // 2. Return the height of the axis-aligned bounding box of the border boxes of all fragments generated by the element’s principal box, |
419 | | // ignoring any transforms that apply to the element and its ancestors. |
420 | | // FIXME: Account for inline boxes. |
421 | 0 | return paintable_box()->border_box_height().to_int(); |
422 | 0 | } |
423 | | |
424 | | // https://html.spec.whatwg.org/multipage/links.html#cannot-navigate |
425 | | bool HTMLElement::cannot_navigate() const |
426 | 0 | { |
427 | | // An element element cannot navigate if one of the following is true: |
428 | | |
429 | | // - element's node document is not fully active |
430 | 0 | if (!document().is_fully_active()) |
431 | 0 | return true; |
432 | | |
433 | | // - element is not an a element and is not connected. |
434 | 0 | return !is<HTML::HTMLAnchorElement>(this) && !is_connected(); |
435 | 0 | } |
436 | | |
437 | | void HTMLElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
438 | 0 | { |
439 | 0 | Element::attribute_changed(name, old_value, value); |
440 | |
|
441 | 0 | if (name == HTML::AttributeNames::contenteditable) { |
442 | 0 | if (!value.has_value()) { |
443 | 0 | m_content_editable_state = ContentEditableState::Inherit; |
444 | 0 | } else { |
445 | 0 | if (value->is_empty() || value->equals_ignoring_ascii_case("true"sv)) { |
446 | | // "true", an empty string or a missing value map to the "true" state. |
447 | 0 | m_content_editable_state = ContentEditableState::True; |
448 | 0 | } else if (value->equals_ignoring_ascii_case("false"sv)) { |
449 | | // "false" maps to the "false" state. |
450 | 0 | m_content_editable_state = ContentEditableState::False; |
451 | 0 | } else { |
452 | | // Having no such attribute or an invalid value maps to the "inherit" state. |
453 | 0 | m_content_editable_state = ContentEditableState::Inherit; |
454 | 0 | } |
455 | 0 | } |
456 | 0 | } |
457 | | |
458 | | // 1. If namespace is not null, or localName is not the name of an event handler content attribute on element, then return. |
459 | | // FIXME: Add the namespace part once we support attribute namespaces. |
460 | 0 | #undef __ENUMERATE |
461 | 0 | #define __ENUMERATE(attribute_name, event_name) \ |
462 | 0 | if (name == HTML::AttributeNames::attribute_name) { \ |
463 | 0 | element_event_handler_attribute_changed(event_name, value); \ |
464 | 0 | } |
465 | 0 | ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE) |
466 | 0 | #undef __ENUMERATE |
467 | 0 | } |
468 | | |
469 | | // https://html.spec.whatwg.org/multipage/interaction.html#dom-focus |
470 | | void HTMLElement::focus() |
471 | 0 | { |
472 | | // 1. If the element is marked as locked for focus, then return. |
473 | 0 | if (m_locked_for_focus) |
474 | 0 | return; |
475 | | |
476 | | // 2. Mark the element as locked for focus. |
477 | 0 | m_locked_for_focus = true; |
478 | | |
479 | | // 3. Run the focusing steps for the element. |
480 | 0 | run_focusing_steps(this); |
481 | | |
482 | | // FIXME: 4. If the value of the preventScroll dictionary member of options is false, |
483 | | // then scroll the element into view with scroll behavior "auto", |
484 | | // block flow direction position set to an implementation-defined value, |
485 | | // and inline base direction position set to an implementation-defined value. |
486 | | |
487 | | // 5. Unmark the element as locked for focus. |
488 | 0 | m_locked_for_focus = false; |
489 | 0 | } |
490 | | |
491 | | // https://html.spec.whatwg.org/multipage/webappapis.html#fire-a-synthetic-pointer-event |
492 | | bool HTMLElement::fire_a_synthetic_pointer_event(FlyString const& type, DOM::Element& target, bool not_trusted) |
493 | 0 | { |
494 | | // 1. Let event be the result of creating an event using PointerEvent. |
495 | | // 2. Initialize event's type attribute to e. |
496 | 0 | auto event = UIEvents::PointerEvent::create(realm(), type); |
497 | | |
498 | | // 3. Initialize event's bubbles and cancelable attributes to true. |
499 | 0 | event->set_bubbles(true); |
500 | 0 | event->set_cancelable(true); |
501 | | |
502 | | // 4. Set event's composed flag. |
503 | 0 | event->set_composed(true); |
504 | | |
505 | | // 5. If the not trusted flag is set, initialize event's isTrusted attribute to false. |
506 | 0 | if (not_trusted) { |
507 | 0 | event->set_is_trusted(false); |
508 | 0 | } |
509 | | |
510 | | // FIXME: 6. Initialize event's ctrlKey, shiftKey, altKey, and metaKey attributes according to the current state |
511 | | // of the key input device, if any (false for any keys that are not available). |
512 | | |
513 | | // FIXME: 7. Initialize event's view attribute to target's node document's Window object, if any, and null otherwise. |
514 | | |
515 | | // FIXME: 8. event's getModifierState() method is to return values appropriately describing the current state of the key input device. |
516 | | |
517 | | // 9. Return the result of dispatching event at target. |
518 | 0 | return target.dispatch_event(event); |
519 | 0 | } |
520 | | |
521 | | // https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels-dev |
522 | | JS::GCPtr<DOM::NodeList> HTMLElement::labels() |
523 | 0 | { |
524 | | // Labelable elements and all input elements have a live NodeList object associated with them that represents the list of label elements, in tree order, |
525 | | // whose labeled control is the element in question. The labels IDL attribute of labelable elements that are not form-associated custom elements, |
526 | | // and the labels IDL attribute of input elements, on getting, must return that NodeList object, and that same value must always be returned, |
527 | | // unless this element is an input element whose type attribute is in the Hidden state, in which case it must instead return null. |
528 | 0 | if (!is_labelable()) |
529 | 0 | return {}; |
530 | | |
531 | 0 | if (!m_labels) { |
532 | 0 | m_labels = DOM::LiveNodeList::create(realm(), root(), DOM::LiveNodeList::Scope::Descendants, [&](auto& node) { |
533 | 0 | return is<HTMLLabelElement>(node) && verify_cast<HTMLLabelElement>(node).control() == this; |
534 | 0 | }); |
535 | 0 | } |
536 | |
|
537 | 0 | return m_labels; |
538 | 0 | } |
539 | | |
540 | | // https://html.spec.whatwg.org/multipage/interaction.html#dom-click |
541 | | void HTMLElement::click() |
542 | 0 | { |
543 | | // 1. If this element is a form control that is disabled, then return. |
544 | 0 | if (auto* form_control = dynamic_cast<FormAssociatedElement*>(this)) { |
545 | 0 | if (!form_control->enabled()) |
546 | 0 | return; |
547 | 0 | } |
548 | | |
549 | | // 2. If this element's click in progress flag is set, then return. |
550 | 0 | if (m_click_in_progress) |
551 | 0 | return; |
552 | | |
553 | | // 3. Set this element's click in progress flag. |
554 | 0 | m_click_in_progress = true; |
555 | | |
556 | | // 4. Fire a synthetic pointer event named click at this element, with the not trusted flag set. |
557 | 0 | fire_a_synthetic_pointer_event(HTML::EventNames::click, *this, true); |
558 | | |
559 | | // 5. Unset this element's click in progress flag. |
560 | 0 | m_click_in_progress = false; |
561 | 0 | } |
562 | | |
563 | | // https://html.spec.whatwg.org/multipage/interaction.html#dom-blur |
564 | | void HTMLElement::blur() |
565 | 0 | { |
566 | | // The blur() method, when invoked, should run the unfocusing steps for the element on which the method was called. |
567 | 0 | run_unfocusing_steps(this); |
568 | | |
569 | | // User agents may selectively or uniformly ignore calls to this method for usability reasons. |
570 | 0 | } |
571 | | |
572 | | Optional<ARIA::Role> HTMLElement::default_role() const |
573 | 0 | { |
574 | | // https://www.w3.org/TR/html-aria/#el-address |
575 | 0 | if (local_name() == TagNames::address) |
576 | 0 | return ARIA::Role::group; |
577 | | // https://www.w3.org/TR/html-aria/#el-article |
578 | 0 | if (local_name() == TagNames::article) |
579 | 0 | return ARIA::Role::article; |
580 | | // https://www.w3.org/TR/html-aria/#el-aside |
581 | 0 | if (local_name() == TagNames::aside) |
582 | 0 | return ARIA::Role::complementary; |
583 | | // https://www.w3.org/TR/html-aria/#el-b |
584 | 0 | if (local_name() == TagNames::b) |
585 | 0 | return ARIA::Role::generic; |
586 | | // https://www.w3.org/TR/html-aria/#el-bdi |
587 | 0 | if (local_name() == TagNames::bdi) |
588 | 0 | return ARIA::Role::generic; |
589 | | // https://www.w3.org/TR/html-aria/#el-bdo |
590 | 0 | if (local_name() == TagNames::bdo) |
591 | 0 | return ARIA::Role::generic; |
592 | | // https://www.w3.org/TR/html-aria/#el-code |
593 | 0 | if (local_name() == TagNames::code) |
594 | 0 | return ARIA::Role::code; |
595 | | // https://www.w3.org/TR/html-aria/#el-dfn |
596 | 0 | if (local_name() == TagNames::dfn) |
597 | 0 | return ARIA::Role::term; |
598 | | // https://www.w3.org/TR/html-aria/#el-em |
599 | 0 | if (local_name() == TagNames::em) |
600 | 0 | return ARIA::Role::emphasis; |
601 | | // https://www.w3.org/TR/html-aria/#el-figure |
602 | 0 | if (local_name() == TagNames::figure) |
603 | 0 | return ARIA::Role::figure; |
604 | | // https://www.w3.org/TR/html-aria/#el-footer |
605 | 0 | if (local_name() == TagNames::footer) { |
606 | | // TODO: If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=contentinfo |
607 | | // Otherwise, role=generic |
608 | 0 | return ARIA::Role::generic; |
609 | 0 | } |
610 | | // https://www.w3.org/TR/html-aria/#el-header |
611 | 0 | if (local_name() == TagNames::header) { |
612 | | // TODO: If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=banner |
613 | | // Otherwise, role=generic |
614 | 0 | return ARIA::Role::generic; |
615 | 0 | } |
616 | | // https://www.w3.org/TR/html-aria/#el-hgroup |
617 | 0 | if (local_name() == TagNames::hgroup) |
618 | 0 | return ARIA::Role::group; |
619 | | // https://www.w3.org/TR/html-aria/#el-i |
620 | 0 | if (local_name() == TagNames::i) |
621 | 0 | return ARIA::Role::generic; |
622 | | // https://www.w3.org/TR/html-aria/#el-main |
623 | 0 | if (local_name() == TagNames::main) |
624 | 0 | return ARIA::Role::main; |
625 | | // https://www.w3.org/TR/html-aria/#el-nav |
626 | 0 | if (local_name() == TagNames::nav) |
627 | 0 | return ARIA::Role::navigation; |
628 | | // https://www.w3.org/TR/html-aria/#el-s |
629 | 0 | if (local_name() == TagNames::s) |
630 | 0 | return ARIA::Role::deletion; |
631 | | // https://www.w3.org/TR/html-aria/#el-samp |
632 | 0 | if (local_name() == TagNames::samp) |
633 | 0 | return ARIA::Role::generic; |
634 | | // https://www.w3.org/TR/html-aria/#el-section |
635 | 0 | if (local_name() == TagNames::section) { |
636 | | // TODO: role=region if the section element has an accessible name |
637 | | // Otherwise, no corresponding role |
638 | 0 | return ARIA::Role::region; |
639 | 0 | } |
640 | | // https://www.w3.org/TR/html-aria/#el-small |
641 | 0 | if (local_name() == TagNames::small) |
642 | 0 | return ARIA::Role::generic; |
643 | | // https://www.w3.org/TR/html-aria/#el-strong |
644 | 0 | if (local_name() == TagNames::strong) |
645 | 0 | return ARIA::Role::strong; |
646 | | // https://www.w3.org/TR/html-aria/#el-sub |
647 | 0 | if (local_name() == TagNames::sub) |
648 | 0 | return ARIA::Role::subscript; |
649 | | // https://www.w3.org/TR/html-aria/#el-summary |
650 | 0 | if (local_name() == TagNames::summary) |
651 | 0 | return ARIA::Role::button; |
652 | | // https://www.w3.org/TR/html-aria/#el-sup |
653 | 0 | if (local_name() == TagNames::sup) |
654 | 0 | return ARIA::Role::superscript; |
655 | | // https://www.w3.org/TR/html-aria/#el-u |
656 | 0 | if (local_name() == TagNames::u) |
657 | 0 | return ARIA::Role::generic; |
658 | | |
659 | 0 | return {}; |
660 | 0 | } |
661 | | |
662 | | // https://html.spec.whatwg.org/multipage/semantics.html#get-an-element's-target |
663 | | String HTMLElement::get_an_elements_target() const |
664 | 0 | { |
665 | | // To get an element's target, given an a, area, or form element element, run these steps: |
666 | | |
667 | | // 1. If element has a target attribute, then return that attribute's value. |
668 | 0 | auto maybe_target = attribute(AttributeNames::target); |
669 | 0 | if (maybe_target.has_value()) |
670 | 0 | return maybe_target.release_value(); |
671 | | |
672 | | // FIXME: 2. If element's node document contains a base element with a |
673 | | // target attribute, then return the value of the target attribute of the |
674 | | // first such base element. |
675 | | |
676 | | // 3. Return the empty string. |
677 | 0 | return String {}; |
678 | 0 | } |
679 | | |
680 | | // https://html.spec.whatwg.org/multipage/links.html#get-an-element's-noopener |
681 | | TokenizedFeature::NoOpener HTMLElement::get_an_elements_noopener(StringView target) const |
682 | 0 | { |
683 | | // To get an element's noopener, given an a, area, or form element element and a string target: |
684 | 0 | auto rel = MUST(get_attribute_value(HTML::AttributeNames::rel).to_lowercase()); |
685 | 0 | auto link_types = rel.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace); |
686 | | |
687 | | // 1. If element's link types include the noopener or noreferrer keyword, then return true. |
688 | 0 | if (link_types.contains_slow("noopener"sv) || link_types.contains_slow("noreferrer"sv)) |
689 | 0 | return TokenizedFeature::NoOpener::Yes; |
690 | | |
691 | | // 2. If element's link types do not include the opener keyword and |
692 | | // target is an ASCII case-insensitive match for "_blank", then return true. |
693 | 0 | if (!link_types.contains_slow("opener"sv) && Infra::is_ascii_case_insensitive_match(target, "_blank"sv)) |
694 | 0 | return TokenizedFeature::NoOpener::Yes; |
695 | | |
696 | | // 3. Return false. |
697 | 0 | return TokenizedFeature::NoOpener::No; |
698 | 0 | } |
699 | | |
700 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<ElementInternals>> HTMLElement::attach_internals() |
701 | 0 | { |
702 | | // 1. If this's is value is not null, then throw a "NotSupportedError" DOMException. |
703 | 0 | if (is_value().has_value()) |
704 | 0 | return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to a customized build-in element"_string); |
705 | | |
706 | | // 2. Let definition be the result of looking up a custom element definition given this's node document, its namespace, its local name, and null as the is value. |
707 | 0 | auto definition = document().lookup_custom_element_definition(namespace_uri(), local_name(), is_value()); |
708 | | |
709 | | // 3. If definition is null, then throw an "NotSupportedError" DOMException. |
710 | 0 | if (!definition) |
711 | 0 | return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to an element that is not a custom element"_string); |
712 | | |
713 | | // 4. If definition's disable internals is true, then throw a "NotSupportedError" DOMException. |
714 | 0 | if (definition->disable_internals()) |
715 | 0 | return WebIDL::NotSupportedError::create(realm(), "ElementInternals are disabled for this custom element"_string); |
716 | | |
717 | | // 5. If this's attached internals is non-null, then throw an "NotSupportedError" DOMException. |
718 | 0 | if (m_attached_internals) |
719 | 0 | return WebIDL::NotSupportedError::create(realm(), "ElementInternals already attached"_string); |
720 | | |
721 | | // 6. If this's custom element state is not "precustomized" or "custom", then throw a "NotSupportedError" DOMException. |
722 | 0 | if (!first_is_one_of(custom_element_state(), DOM::CustomElementState::Precustomized, DOM::CustomElementState::Custom)) |
723 | 0 | return WebIDL::NotSupportedError::create(realm(), "Custom element is in an invalid state to attach ElementInternals"_string); |
724 | | |
725 | | // 7. Set this's attached internals to a new ElementInternals instance whose target element is this. |
726 | 0 | auto internals = ElementInternals::create(realm(), *this); |
727 | |
|
728 | 0 | m_attached_internals = internals; |
729 | | |
730 | | // 8. Return this's attached internals. |
731 | 0 | return { internals }; |
732 | 0 | } |
733 | | |
734 | | // https://html.spec.whatwg.org/multipage/popover.html#dom-popover |
735 | | Optional<String> HTMLElement::popover() const |
736 | 0 | { |
737 | | // FIXME: This should probably be `Reflect` in the IDL. |
738 | | // The popover IDL attribute must reflect the popover attribute, limited to only known values. |
739 | 0 | auto value = get_attribute(HTML::AttributeNames::popover); |
740 | |
|
741 | 0 | if (!value.has_value()) |
742 | 0 | return {}; |
743 | | |
744 | 0 | if (value.value().is_empty() || value.value().equals_ignoring_ascii_case("auto"sv)) |
745 | 0 | return "auto"_string; |
746 | | |
747 | 0 | return "manual"_string; |
748 | 0 | } |
749 | | |
750 | | // https://html.spec.whatwg.org/multipage/popover.html#dom-popover |
751 | | WebIDL::ExceptionOr<void> HTMLElement::set_popover(Optional<String> value) |
752 | 0 | { |
753 | | // FIXME: This should probably be `Reflect` in the IDL. |
754 | | // The popover IDL attribute must reflect the popover attribute, limited to only known values. |
755 | 0 | if (value.has_value()) |
756 | 0 | return set_attribute(HTML::AttributeNames::popover, value.release_value()); |
757 | | |
758 | 0 | remove_attribute(HTML::AttributeNames::popover); |
759 | 0 | return {}; |
760 | 0 | } |
761 | | |
762 | | void HTMLElement::did_receive_focus() |
763 | 0 | { |
764 | 0 | if (m_content_editable_state != ContentEditableState::True) |
765 | 0 | return; |
766 | | |
767 | 0 | DOM::Text* text = nullptr; |
768 | 0 | for_each_in_inclusive_subtree_of_type<DOM::Text>([&](auto& node) { |
769 | 0 | text = &node; |
770 | 0 | return TraversalDecision::Continue; |
771 | 0 | }); |
772 | |
|
773 | 0 | if (!text) { |
774 | 0 | document().set_cursor_position(DOM::Position::create(realm(), *this, 0)); |
775 | 0 | return; |
776 | 0 | } |
777 | 0 | document().set_cursor_position(DOM::Position::create(realm(), *text, text->length())); |
778 | 0 | } |
779 | | |
780 | | // https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel |
781 | | String HTMLElement::access_key_label() const |
782 | 0 | { |
783 | 0 | dbgln("FIXME: Implement HTMLElement::access_key_label()"); |
784 | 0 | return String {}; |
785 | 0 | } |
786 | | |
787 | | } |