mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 10:38:11 +00:00
LibWeb: Flesh out HTMLTextAreaElement
Give it a shadow tree, similar to HTMLInputElement's, so that we can actually edit its contents at a basic level. Add some CSS to use the `rows` and `cols` attributes as the size if they are present.
This commit is contained in:
parent
4897643ffb
commit
9e227dfc16
3 changed files with 123 additions and 3 deletions
|
@ -38,7 +38,10 @@ input, textarea {
|
||||||
textarea {
|
textarea {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
width: attr(cols ch, 20ch);
|
||||||
|
height: attr(rows lh, 2lh);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=submit], input[type=button], input[type=reset], input[type=checkbox], input[type=radio] {
|
input[type=submit], input[type=button], input[type=reset], input[type=checkbox], input[type=radio] {
|
||||||
|
|
|
@ -1,27 +1,75 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2020, the SerenityOS developers.
|
* Copyright (c) 2020, the SerenityOS developers.
|
||||||
|
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <LibWeb/Bindings/Intrinsics.h>
|
#include <LibWeb/Bindings/Intrinsics.h>
|
||||||
|
#include <LibWeb/CSS/StyleProperties.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
||||||
|
#include <LibWeb/DOM/Document.h>
|
||||||
|
#include <LibWeb/DOM/ElementFactory.h>
|
||||||
|
#include <LibWeb/DOM/Event.h>
|
||||||
|
#include <LibWeb/DOM/ShadowRoot.h>
|
||||||
|
#include <LibWeb/DOM/Text.h>
|
||||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||||
|
#include <LibWeb/Namespace.h>
|
||||||
|
|
||||||
namespace Web::HTML {
|
namespace Web::HTML {
|
||||||
|
|
||||||
HTMLTextAreaElement::HTMLTextAreaElement(DOM::Document& document, DOM::QualifiedName qualified_name)
|
HTMLTextAreaElement::HTMLTextAreaElement(DOM::Document& document, DOM::QualifiedName qualified_name)
|
||||||
: HTMLElement(document, move(qualified_name))
|
: HTMLElement(document, move(qualified_name))
|
||||||
|
, m_raw_value(DeprecatedString::empty())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
HTMLTextAreaElement::~HTMLTextAreaElement() = default;
|
HTMLTextAreaElement::~HTMLTextAreaElement() = default;
|
||||||
|
|
||||||
|
JS::GCPtr<Layout::Node> HTMLTextAreaElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
|
||||||
|
{
|
||||||
|
// AD-HOC: We rewrite `display: inline` to `display: inline-block`.
|
||||||
|
// This is required for the internal shadow tree to work correctly in layout.
|
||||||
|
if (style->display().is_inline_outside() && style->display().is_flow_inside())
|
||||||
|
style->set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::InlineBlock)));
|
||||||
|
|
||||||
|
return Element::create_layout_node_for_display_type(document(), style->display(), style, this);
|
||||||
|
}
|
||||||
|
|
||||||
void HTMLTextAreaElement::initialize(JS::Realm& realm)
|
void HTMLTextAreaElement::initialize(JS::Realm& realm)
|
||||||
{
|
{
|
||||||
Base::initialize(realm);
|
Base::initialize(realm);
|
||||||
set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLTextAreaElementPrototype>(realm, "HTMLTextAreaElement"));
|
set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLTextAreaElementPrototype>(realm, "HTMLTextAreaElement"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::visit_edges(Cell::Visitor& visitor)
|
||||||
|
{
|
||||||
|
Base::visit_edges(visitor);
|
||||||
|
visitor.visit(m_inner_text_element);
|
||||||
|
visitor.visit(m_text_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::did_receive_focus()
|
||||||
|
{
|
||||||
|
auto* browsing_context = document().browsing_context();
|
||||||
|
if (!browsing_context)
|
||||||
|
return;
|
||||||
|
if (!m_text_node)
|
||||||
|
return;
|
||||||
|
browsing_context->set_cursor_position(DOM::Position { *m_text_node, 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::did_lose_focus()
|
||||||
|
{
|
||||||
|
// The change event fires when the value is committed, if that makes sense for the control,
|
||||||
|
// or else when the control loses focus
|
||||||
|
queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
|
||||||
|
auto change_event = DOM::Event::create(realm(), HTML::EventNames::change);
|
||||||
|
change_event->set_bubbles(true);
|
||||||
|
dispatch_event(change_event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
|
// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
|
||||||
i32 HTMLTextAreaElement::default_tab_index_value() const
|
i32 HTMLTextAreaElement::default_tab_index_value() const
|
||||||
{
|
{
|
||||||
|
@ -32,7 +80,54 @@ i32 HTMLTextAreaElement::default_tab_index_value() const
|
||||||
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-form-reset-control
|
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-form-reset-control
|
||||||
void HTMLTextAreaElement::reset_algorithm()
|
void HTMLTextAreaElement::reset_algorithm()
|
||||||
{
|
{
|
||||||
// FIXME: The reset algorithm for textarea elements is to set the dirty value flag back to false, and set the raw value of element to its child text content.
|
// The reset algorithm for textarea elements is to set the dirty value flag back to false,
|
||||||
|
m_dirty = false;
|
||||||
|
// and set the raw value of element to its child text content.
|
||||||
|
m_raw_value = child_text_content();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::form_associated_element_was_inserted()
|
||||||
|
{
|
||||||
|
create_shadow_tree_if_needed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::create_shadow_tree_if_needed()
|
||||||
|
{
|
||||||
|
if (shadow_root_internal())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed);
|
||||||
|
auto element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
|
||||||
|
|
||||||
|
m_inner_text_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
|
||||||
|
|
||||||
|
// NOTE: The text content of the <textarea> element is not available to us yet.
|
||||||
|
// It gets filled in by `children_changed()`.
|
||||||
|
m_text_node = heap().allocate<DOM::Text>(realm(), document(), String {});
|
||||||
|
m_text_node->set_always_editable(true);
|
||||||
|
m_text_node->set_editable_text_node_owner(Badge<HTMLTextAreaElement> {}, *this);
|
||||||
|
|
||||||
|
MUST(m_inner_text_element->append_child(*m_text_node));
|
||||||
|
MUST(element->append_child(*m_inner_text_element));
|
||||||
|
MUST(shadow_root->append_child(element));
|
||||||
|
set_shadow_root(shadow_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:children-changed-steps
|
||||||
|
void HTMLTextAreaElement::children_changed()
|
||||||
|
{
|
||||||
|
// The children changed steps for textarea elements must, if the element's dirty value flag is false,
|
||||||
|
// set the element's raw value to its child text content.
|
||||||
|
if (!m_dirty) {
|
||||||
|
m_raw_value = child_text_content();
|
||||||
|
m_text_node->set_text_content(m_raw_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLTextAreaElement::did_edit_text_node(Badge<Web::HTML::BrowsingContext>)
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
m_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <LibWeb/ARIA/Roles.h>
|
#include <LibWeb/ARIA/Roles.h>
|
||||||
|
#include <LibWeb/DOM/Text.h>
|
||||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||||
#include <LibWeb/HTML/HTMLElement.h>
|
#include <LibWeb/HTML/HTMLElement.h>
|
||||||
|
|
||||||
|
@ -15,22 +16,30 @@ namespace Web::HTML {
|
||||||
|
|
||||||
class HTMLTextAreaElement final
|
class HTMLTextAreaElement final
|
||||||
: public HTMLElement
|
: public HTMLElement
|
||||||
, public FormAssociatedElement {
|
, public FormAssociatedElement
|
||||||
|
, public DOM::EditableTextNodeOwner {
|
||||||
WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement);
|
WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement);
|
||||||
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement)
|
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual ~HTMLTextAreaElement() override;
|
virtual ~HTMLTextAreaElement() override;
|
||||||
|
|
||||||
|
virtual JS::GCPtr<Layout::Node> create_layout_node(NonnullRefPtr<CSS::StyleProperties>) override;
|
||||||
|
|
||||||
DeprecatedString const& type() const
|
DeprecatedString const& type() const
|
||||||
{
|
{
|
||||||
static DeprecatedString textarea = "textarea";
|
static DeprecatedString textarea = "textarea";
|
||||||
return textarea;
|
return textarea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ^DOM::EditableTextNodeOwner
|
||||||
|
virtual void did_edit_text_node(Badge<BrowsingContext>) override;
|
||||||
|
|
||||||
// ^EventTarget
|
// ^EventTarget
|
||||||
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-textarea-element
|
// https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute:the-textarea-element
|
||||||
virtual bool is_focusable() const override { return true; }
|
virtual bool is_focusable() const override { return true; }
|
||||||
|
virtual void did_lose_focus() override;
|
||||||
|
virtual void did_receive_focus() override;
|
||||||
|
|
||||||
// ^FormAssociatedElement
|
// ^FormAssociatedElement
|
||||||
// https://html.spec.whatwg.org/multipage/forms.html#category-listed
|
// https://html.spec.whatwg.org/multipage/forms.html#category-listed
|
||||||
|
@ -51,6 +60,10 @@ public:
|
||||||
|
|
||||||
virtual void reset_algorithm() override;
|
virtual void reset_algorithm() override;
|
||||||
|
|
||||||
|
virtual void form_associated_element_was_inserted() override;
|
||||||
|
|
||||||
|
virtual void children_changed() override;
|
||||||
|
|
||||||
// https://www.w3.org/TR/html-aria/#el-textarea
|
// https://www.w3.org/TR/html-aria/#el-textarea
|
||||||
virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::textbox; }
|
virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::textbox; }
|
||||||
|
|
||||||
|
@ -58,9 +71,18 @@ private:
|
||||||
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);
|
HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName);
|
||||||
|
|
||||||
virtual void initialize(JS::Realm&) override;
|
virtual void initialize(JS::Realm&) override;
|
||||||
|
virtual void visit_edges(Cell::Visitor&) override;
|
||||||
|
|
||||||
// ^DOM::Element
|
// ^DOM::Element
|
||||||
virtual i32 default_tab_index_value() const override;
|
virtual i32 default_tab_index_value() const override;
|
||||||
|
|
||||||
|
void create_shadow_tree_if_needed();
|
||||||
|
|
||||||
|
JS::GCPtr<DOM::Element> m_inner_text_element;
|
||||||
|
JS::GCPtr<DOM::Text> m_text_node;
|
||||||
|
|
||||||
|
bool m_dirty { false };
|
||||||
|
DeprecatedString m_raw_value;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue