diff --git a/Base/res/html/misc/select.html b/Base/res/html/misc/select.html
new file mode 100644
index 0000000000..2618a21f75
--- /dev/null
+++ b/Base/res/html/misc/select.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Select showcase
+
+
+ Basic select:
+
+
+ Value: ?
+
+
+ Basic select with separators:
+
+
+ Value: ?
+
+
+ Basic select with option groups and separators:
+
+
+ Value: ?
+
+
+
diff --git a/Ladybird/AppKit/UI/LadybirdWebView.h b/Ladybird/AppKit/UI/LadybirdWebView.h
index d1f35bce95..6ec20458bb 100644
--- a/Ladybird/AppKit/UI/LadybirdWebView.h
+++ b/Ladybird/AppKit/UI/LadybirdWebView.h
@@ -36,7 +36,7 @@
@end
-@interface LadybirdWebView : NSClipView
+@interface LadybirdWebView : NSClipView
- (instancetype)init:(id)observer;
diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm
index 3f524e10cf..06f33fbc36 100644
--- a/Ladybird/AppKit/UI/LadybirdWebView.mm
+++ b/Ladybird/AppKit/UI/LadybirdWebView.mm
@@ -62,6 +62,7 @@ struct HideCursor {
@property (nonatomic, strong) NSMenu* image_context_menu;
@property (nonatomic, strong) NSMenu* audio_context_menu;
@property (nonatomic, strong) NSMenu* video_context_menu;
+@property (nonatomic, strong) NSMenu* select_dropdown;
@property (nonatomic, strong) NSTextField* status_label;
@property (nonatomic, strong) NSAlert* dialog;
@@ -608,6 +609,21 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
[panel makeKeyAndOrderFront:nil];
};
+ self.select_dropdown = [[NSMenu alloc] initWithTitle:@"Select Dropdown"];
+ [self.select_dropdown setDelegate:self];
+
+ m_web_view_bridge->on_request_select_dropdown = [self](Gfx::IntPoint content_position, i32 minimum_width, Vector items) {
+ [self.select_dropdown removeAllItems];
+ self.select_dropdown.minimumWidth = minimum_width;
+ for (auto const& item : items) {
+ [self selectDropdownAdd:self.select_dropdown
+ item:item];
+ }
+
+ auto* event = Ladybird::create_context_menu_mouse_event(self, content_position);
+ [NSMenu popUpContextMenu:self.select_dropdown withEvent:event forView:self];
+ };
+
m_web_view_bridge->on_get_all_cookies = [](auto const& url) {
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
return [delegate cookieJar].get_all_cookies(url);
@@ -703,6 +719,48 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
};
}
+- (void)selectDropdownAdd:(NSMenu*)menu item:(Web::HTML::SelectItem const&)item
+{
+ if (item.type == Web::HTML::SelectItem::Type::OptionGroup) {
+ NSMenuItem* subtitle = [[NSMenuItem alloc]
+ initWithTitle:Ladybird::string_to_ns_string(item.label.value_or(""_string))
+ action:nil
+ keyEquivalent:@""];
+ subtitle.enabled = false;
+ [menu addItem:subtitle];
+
+ for (auto const& item : *item.items) {
+ [self selectDropdownAdd:menu
+ item:item];
+ }
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Option) {
+ NSMenuItem* menuItem = [[NSMenuItem alloc]
+ initWithTitle:Ladybird::string_to_ns_string(item.label.value_or(""_string))
+ action:@selector(selectDropdownAction:)
+ keyEquivalent:@""];
+ [menuItem setRepresentedObject:Ladybird::string_to_ns_string(item.value.value_or(""_string))];
+ [menuItem setEnabled:YES];
+ [menuItem setState:item.selected ? NSControlStateValueOn : NSControlStateValueOff];
+ [menu addItem:menuItem];
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Separator) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ }
+}
+
+- (void)selectDropdownAction:(NSMenuItem*)menuItem
+{
+ auto value = Ladybird::ns_string_to_string([menuItem representedObject]);
+ m_web_view_bridge->select_dropdown_closed(value);
+}
+
+- (void)menuDidClose:(NSMenu*)menu
+{
+ if (!menu.highlightedItem)
+ m_web_view_bridge->select_dropdown_closed({});
+}
+
- (void)colorPickerClosed:(NSNotification*)notification
{
m_web_view_bridge->color_picker_closed(Ladybird::ns_color_to_gfx_color([[NSColorPanel sharedColorPanel] color]));
diff --git a/Ladybird/Qt/Tab.cpp b/Ladybird/Qt/Tab.cpp
index 2b962b65c6..b36e85c86e 100644
--- a/Ladybird/Qt/Tab.cpp
+++ b/Ladybird/Qt/Tab.cpp
@@ -226,6 +226,22 @@ Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, St
m_dialog = nullptr;
};
+ m_select_dropdown = new QMenu("Select Dropdown", this);
+ QObject::connect(m_select_dropdown, &QMenu::aboutToHide, this, [this]() {
+ if (!m_select_dropdown->activeAction())
+ view().select_dropdown_closed({});
+ });
+
+ view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector items) {
+ m_select_dropdown->clear();
+ m_select_dropdown->setMinimumWidth(minimum_width);
+ for (auto const& item : items) {
+ select_dropdown_add_item(m_select_dropdown, item);
+ }
+
+ m_select_dropdown->exec(mapToGlobal(QPoint(content_position.x(), content_position.y())));
+ };
+
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
view().on_received_source = [this](auto const& url, auto const& source) {
@@ -569,6 +585,37 @@ Tab::~Tab()
close_sub_widgets();
}
+void Tab::select_dropdown_add_item(QMenu* menu, Web::HTML::SelectItem const& item)
+{
+ if (item.type == Web::HTML::SelectItem::Type::OptionGroup) {
+ QAction* subtitle = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this);
+ subtitle->setDisabled(true);
+ menu->addAction(subtitle);
+
+ for (auto const& item : *item.items) {
+ select_dropdown_add_item(menu, item);
+ }
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Option) {
+ QAction* action = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this);
+ action->setCheckable(true);
+ action->setChecked(item.selected);
+ action->setData(QVariant(qstring_from_ak_string(item.value.value_or(""_string))));
+ QObject::connect(action, &QAction::triggered, this, &Tab::select_dropdown_action);
+ menu->addAction(action);
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Separator) {
+ menu->addSeparator();
+ }
+}
+
+void Tab::select_dropdown_action()
+{
+ QAction* action = qobject_cast(sender());
+ auto value = action->data().value();
+ view().select_dropdown_closed(ak_string_from_qstring(value));
+}
+
void Tab::update_reset_zoom_button()
{
auto zoom_level = view().zoom_level();
diff --git a/Ladybird/Qt/Tab.h b/Ladybird/Qt/Tab.h
index d02b45b88e..50bbf11f27 100644
--- a/Ladybird/Qt/Tab.h
+++ b/Ladybird/Qt/Tab.h
@@ -54,12 +54,15 @@ public:
public slots:
void focus_location_editor();
void location_edit_return_pressed();
+ void select_dropdown_action();
signals:
void title_changed(int id, QString);
void favicon_changed(int id, QIcon);
private:
+ void select_dropdown_add_item(QMenu* menu, Web::HTML::SelectItem const& item);
+
virtual void resizeEvent(QResizeEvent*) override;
virtual bool event(QEvent*) override;
@@ -106,6 +109,8 @@ private:
QAction* m_media_context_menu_loop_action { nullptr };
URL m_media_context_menu_url;
+ QMenu* m_select_dropdown { nullptr };
+
int tab_index();
bool m_is_history_navigation { false };
diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
index 37fce57faa..08bda3326c 100644
--- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
+++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn
@@ -140,6 +140,7 @@ source_set("HTML") {
"PotentialCORSRequest.cpp",
"PromiseRejectionEvent.cpp",
"RemoteBrowsingContext.cpp",
+ "SelectItem.cpp",
"SessionHistoryEntry.cpp",
"SharedImageRequest.cpp",
"SourceSet.cpp",
diff --git a/Tests/LibWeb/Text/expected/select.txt b/Tests/LibWeb/Text/expected/select.txt
new file mode 100644
index 0000000000..97a482e775
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/select.txt
@@ -0,0 +1,5 @@
+1. "one"
+2. 0
+3. "two"
+4. 3
+5. "three"
diff --git a/Tests/LibWeb/Text/input/select.html b/Tests/LibWeb/Text/input/select.html
new file mode 100644
index 0000000000..8381c66bf0
--- /dev/null
+++ b/Tests/LibWeb/Text/input/select.html
@@ -0,0 +1,67 @@
+
+
diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp
index 87ae4994df..b10df27305 100644
--- a/Userland/Applications/Browser/Tab.cpp
+++ b/Userland/Applications/Browser/Tab.cpp
@@ -578,6 +578,23 @@ Tab::Tab(BrowserWindow& window)
m_dialog = nullptr;
};
+ m_select_dropdown = GUI::Menu::construct();
+ m_select_dropdown->on_visibility_change = [this](bool visible) {
+ if (!visible && !m_select_dropdown_closed_by_action)
+ view().select_dropdown_closed({});
+ };
+
+ view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32, Vector items) {
+ m_select_dropdown_closed_by_action = false;
+ m_select_dropdown->remove_all_actions();
+ // FIXME: Set menu minimum width
+ for (auto const& item : items) {
+ select_dropdown_add_item(*m_select_dropdown, item);
+ }
+
+ m_select_dropdown->popup(view().screen_relative_rect().location().translated(content_position));
+ };
+
view().on_received_source = [this](auto& url, auto& source) {
view_source(url, source);
};
@@ -699,6 +716,31 @@ Tab::Tab(BrowserWindow& window)
};
}
+void Tab::select_dropdown_add_item(GUI::Menu& menu, Web::HTML::SelectItem const& item)
+{
+ if (item.type == Web::HTML::SelectItem::Type::OptionGroup) {
+ auto subtitle = GUI::Action::create(MUST(DeprecatedString::from_utf8(item.label.value_or(""_string))), nullptr);
+ subtitle->set_enabled(false);
+ menu.add_action(subtitle);
+
+ for (auto const& item : *item.items) {
+ select_dropdown_add_item(menu, item);
+ }
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Option) {
+ auto action = GUI::Action::create(MUST(DeprecatedString::from_utf8(item.label.value_or(""_string))), [this, item](GUI::Action&) {
+ m_select_dropdown_closed_by_action = true;
+ view().select_dropdown_closed(item.value.value_or(""_string));
+ });
+ action->set_checkable(true);
+ action->set_checked(item.selected);
+ menu.add_action(action);
+ }
+ if (item.type == Web::HTML::SelectItem::Type::Separator) {
+ menu.add_separator();
+ }
+}
+
void Tab::update_reset_zoom_button()
{
auto zoom_level = view().zoom_level();
diff --git a/Userland/Applications/Browser/Tab.h b/Userland/Applications/Browser/Tab.h
index 7eccbd432e..01edfd5476 100644
--- a/Userland/Applications/Browser/Tab.h
+++ b/Userland/Applications/Browser/Tab.h
@@ -115,6 +115,8 @@ private:
void update_status(Optional text_override = {}, i32 count_waiting = 0);
void close_sub_widgets();
+ void select_dropdown_add_item(GUI::Menu& menu, Web::HTML::SelectItem const& item);
+
WebView::History m_history;
RefPtr m_web_content_view;
@@ -155,6 +157,9 @@ private:
RefPtr m_go_back_context_menu;
RefPtr m_go_forward_context_menu;
+ RefPtr m_select_dropdown;
+ bool m_select_dropdown_closed_by_action { false };
+
DeprecatedString m_title;
RefPtr m_icon;
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index 89ee2f2a1b..3722f2adb0 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -384,6 +384,7 @@ set(SOURCES
HTML/Scripting/TemporaryExecutionContext.cpp
HTML/Scripting/WindowEnvironmentSettingsObject.cpp
HTML/Scripting/WorkerEnvironmentSettingsObject.cpp
+ HTML/SelectItem.cpp
HTML/SessionHistoryEntry.cpp
HTML/SharedImageRequest.cpp
HTML/SourceSet.cpp
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp
index 82090fadf5..8a4cabaae1 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp
@@ -1,15 +1,27 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2021-2022, Andreas Kling
+ * Copyright (c) 2023, Bastiaan van der Plaat
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
+#include
#include
#include
#include
+#include
+#include
+#include
+#include
namespace Web::HTML {
@@ -32,6 +44,17 @@ void HTMLSelectElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_options);
+ visitor.visit(m_inner_text_element);
+}
+
+JS::GCPtr HTMLSelectElement::create_layout_node(NonnullRefPtr 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);
}
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-options
@@ -177,6 +200,39 @@ Optional HTMLSelectElement::default_role() const
return ARIA::Role::combobox;
}
+String HTMLSelectElement::value() const
+{
+ for (auto const& option_element : list_of_options())
+ if (option_element->selected())
+ return option_element->value();
+ return ""_string;
+}
+
+WebIDL::ExceptionOr HTMLSelectElement::set_value(String const& value)
+{
+ for (auto const& option_element : list_of_options())
+ option_element->set_selected(option_element->value() == value);
+ update_inner_text_element();
+ document().invalidate_layout();
+
+ // When the user agent is to send select update notifications, queue an element task on the user interaction task source given the select element to run these steps:
+ queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
+ // FIXME: 1. Set the select element's user interacted to true.
+
+ // 2. Fire an event named input at the select element, with the bubbles and composed attributes initialized to true.
+ auto input_event = DOM::Event::create(realm(), HTML::EventNames::input);
+ input_event->set_bubbles(true);
+ input_event->set_composed(true);
+ dispatch_event(input_event);
+
+ // 3. Fire an event named change at the select element, with the bubbles attribute initialized to true.
+ auto change_event = DOM::Event::create(realm(), HTML::EventNames::change);
+ change_event->set_bubbles(true);
+ dispatch_event(*change_event);
+ });
+ return {};
+}
+
void HTMLSelectElement::set_is_open(bool open)
{
if (open == m_is_open)
@@ -186,4 +242,139 @@ void HTMLSelectElement::set_is_open(bool open)
invalidate_style();
}
+bool HTMLSelectElement::has_activation_behavior() const
+{
+ return true;
+}
+
+static Optional strip_newlines(Optional string)
+{
+ // FIXME: Move this to a more general function
+ if (!string.has_value())
+ return {};
+
+ StringBuilder builder;
+ for (auto c : string.value().bytes_as_string_view()) {
+ if (c == '\r' || c == '\n') {
+ builder.append(' ');
+ } else {
+ builder.append(c);
+ }
+ }
+ return MUST(Infra::strip_and_collapse_whitespace(MUST(builder.to_string())));
+}
+
+void HTMLSelectElement::activation_behavior(DOM::Event const&)
+{
+ // Populate select items
+ Vector items;
+ for (auto const& child : children_as_vector()) {
+ if (is(*child)) {
+ auto& opt_group_element = verify_cast(*child);
+ Vector opt_group_items;
+ for (auto const& child : opt_group_element.children_as_vector()) {
+ if (is(*child)) {
+ auto& option_element = verify_cast(*child);
+ auto option_value = option_element.value();
+ opt_group_items.append(SelectItem { SelectItem::Type::Option, strip_newlines(option_element.text_content()), option_value, {}, option_element.selected() });
+ }
+ if (is(*child)) {
+ opt_group_items.append(SelectItem { SelectItem::Type::Separator });
+ }
+ }
+ items.append(SelectItem { SelectItem::Type::OptionGroup, opt_group_element.get_attribute(AttributeNames::label), {}, opt_group_items });
+ }
+
+ if (is(*child)) {
+ auto& option_element = verify_cast(*child);
+ auto option_value = option_element.value();
+ items.append(SelectItem { SelectItem::Type::Option, strip_newlines(option_element.text_content()), option_value, {}, option_element.selected() });
+ }
+ if (is(*child)) {
+ items.append(SelectItem { SelectItem::Type::Separator });
+ }
+ }
+
+ // Request select dropdown
+ auto weak_element = make_weak_ptr();
+ auto rect = get_bounding_client_rect();
+ document().browsing_context()->top_level_browsing_context()->page().did_request_select_dropdown(weak_element, Gfx::IntPoint { rect->x(), rect->y() }, rect->width(), items);
+ set_is_open(true);
+}
+
+void HTMLSelectElement::did_select_value(Optional value)
+{
+ set_is_open(false);
+ if (value.has_value()) {
+ MUST(set_value(*value));
+ }
+}
+
+void HTMLSelectElement::form_associated_element_was_inserted()
+{
+ create_shadow_tree_if_needed();
+
+ // Wait until children are ready
+ queue_an_element_task(HTML::Task::Source::Microtask, [this] {
+ // Select first option when no other option is selected
+ if (selected_index() == -1) {
+ auto options = list_of_options();
+ if (options.size() > 0) {
+ options.at(0)->set_selected(true);
+ update_inner_text_element();
+ document().invalidate_layout();
+ }
+ }
+ });
+}
+
+void HTMLSelectElement::form_associated_element_was_removed(DOM::Node*)
+{
+ set_shadow_root(nullptr);
+}
+
+void HTMLSelectElement::create_shadow_tree_if_needed()
+{
+ if (shadow_root_internal())
+ return;
+
+ auto shadow_root = heap().allocate(realm(), document(), *this, Bindings::ShadowRootMode::Closed);
+ set_shadow_root(shadow_root);
+
+ auto border = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
+ MUST(border->set_attribute(HTML::AttributeNames::style, R"~~~(
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ padding: 4px;
+ border: 1px solid ButtonBorder;
+ background-color: ButtonFace;
+)~~~"_string));
+ MUST(shadow_root->append_child(border));
+
+ m_inner_text_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
+ MUST(m_inner_text_element->set_attribute(HTML::AttributeNames::style, R"~~~(
+ flex: 1;
+)~~~"_string));
+ MUST(border->append_child(*m_inner_text_element));
+
+ // FIXME: Find better way to add chevron icon
+ auto chevron_icon_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
+ MUST(chevron_icon_element->set_inner_html(""sv));
+ MUST(border->append_child(*chevron_icon_element));
+
+ update_inner_text_element();
+}
+
+void HTMLSelectElement::update_inner_text_element()
+{
+ // Update inner text element to text content of selected option
+ for (auto const& option_element : list_of_options()) {
+ if (option_element->selected()) {
+ m_inner_text_element->set_text_content(strip_newlines(option_element->text_content()));
+ return;
+ }
+ }
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h
index a12d02d852..d57914783a 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h
@@ -2,6 +2,7 @@
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2021-2022, Andreas Kling
* Copyright (c) 2022, Luke Wilde
+ * Copyright (c) 2023, Bastiaan van der Plaat
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -24,6 +25,8 @@ class HTMLSelectElement final
public:
virtual ~HTMLSelectElement() override;
+ virtual JS::GCPtr create_layout_node(NonnullRefPtr) override;
+
JS::GCPtr const& options();
size_t length();
@@ -34,6 +37,9 @@ public:
int selected_index() const;
void set_selected_index(int);
+ virtual String value() const override;
+ WebIDL::ExceptionOr set_value(String const&);
+
bool is_open() const { return m_is_open; }
void set_is_open(bool);
@@ -66,6 +72,14 @@ public:
virtual Optional default_role() const override;
+ virtual bool has_activation_behavior() const override;
+ virtual void activation_behavior(DOM::Event const&) override;
+
+ virtual void form_associated_element_was_inserted() override;
+ virtual void form_associated_element_was_removed(DOM::Node*) override;
+
+ void did_select_value(Optional value);
+
private:
HTMLSelectElement(DOM::Document&, DOM::QualifiedName);
@@ -75,8 +89,12 @@ private:
// ^DOM::Element
virtual i32 default_tab_index_value() const override;
+ void create_shadow_tree_if_needed();
+ void update_inner_text_element();
+
JS::GCPtr m_options;
bool m_is_open { false };
+ JS::GCPtr m_inner_text_element;
};
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl
index 6ad36f3bc7..e7b9c7c00d 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl
+++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl
@@ -31,7 +31,7 @@ interface HTMLSelectElement : HTMLElement {
// FIXME: [SameObject] readonly attribute HTMLCollection selectedOptions;
attribute long selectedIndex;
- // FIXME: attribute DOMString value;
+ attribute DOMString value;
// FIXME: readonly attribute boolean willValidate;
// FIXME: readonly attribute ValidityState validity;
diff --git a/Userland/Libraries/LibWeb/HTML/SelectItem.cpp b/Userland/Libraries/LibWeb/HTML/SelectItem.cpp
new file mode 100644
index 0000000000..3638a66a62
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/SelectItem.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023, Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "SelectItem.h"
+#include
+#include
+
+template<>
+ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SelectItem const& select_item)
+{
+ TRY(encoder.encode(select_item.type));
+ TRY(encoder.encode(select_item.label));
+ TRY(encoder.encode(select_item.value));
+ TRY(encoder.encode(select_item.items));
+ TRY(encoder.encode(select_item.selected));
+ return {};
+}
+
+template<>
+ErrorOr IPC::decode(Decoder& decoder)
+{
+ auto type = TRY(decoder.decode());
+ auto label = TRY(decoder.decode>());
+ auto value = TRY(decoder.decode>());
+ auto items = TRY(decoder.decode>>());
+ auto selected = TRY(decoder.decode());
+ return Web::HTML::SelectItem { type, move(label), move(value), move(items), selected };
+}
diff --git a/Userland/Libraries/LibWeb/HTML/SelectItem.h b/Userland/Libraries/LibWeb/HTML/SelectItem.h
new file mode 100644
index 0000000000..c342f39677
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/SelectItem.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023, Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace Web::HTML {
+
+struct SelectItem {
+ enum class Type {
+ OptionGroup,
+ Option,
+ Separator,
+ };
+
+ Type type;
+ Optional label = {};
+ Optional value = {};
+ Optional> items = {};
+ bool selected = false;
+};
+
+}
+
+namespace IPC {
+
+template<>
+ErrorOr encode(Encoder&, Web::HTML::SelectItem const&);
+
+template<>
+ErrorOr decode(Decoder&);
+
+}
diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp
index 51a4824293..e72182e005 100644
--- a/Userland/Libraries/LibWeb/Page/Page.cpp
+++ b/Userland/Libraries/LibWeb/Page/Page.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -323,7 +324,30 @@ void Page::color_picker_closed(Optional picked_color)
m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
if (m_pending_non_blocking_dialog_target) {
- m_pending_non_blocking_dialog_target->did_pick_color(picked_color);
+ auto& input_element = verify_cast(*m_pending_non_blocking_dialog_target);
+ input_element.did_pick_color(move(picked_color));
+ m_pending_non_blocking_dialog_target.clear();
+ }
+ }
+}
+
+void Page::did_request_select_dropdown(WeakPtr target, Gfx::IntPoint content_position, i32 minimum_width, Vector items)
+{
+ if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) {
+ m_pending_non_blocking_dialog = PendingNonBlockingDialog::Select;
+ m_pending_non_blocking_dialog_target = move(target);
+ m_client->page_did_request_select_dropdown(content_position, minimum_width, move(items));
+ }
+}
+
+void Page::select_dropdown_closed(Optional value)
+{
+ if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::Select) {
+ m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
+
+ if (m_pending_non_blocking_dialog_target) {
+ auto& select_element = verify_cast(*m_pending_non_blocking_dialog_target);
+ select_element.did_select_value(move(value));
m_pending_non_blocking_dialog_target.clear();
}
}
diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h
index b9845cdbfb..767024107e 100644
--- a/Userland/Libraries/LibWeb/Page/Page.h
+++ b/Userland/Libraries/LibWeb/Page/Page.h
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include
#include
@@ -128,9 +129,13 @@ public:
void did_request_color_picker(WeakPtr target, Color current_color);
void color_picker_closed(Optional picked_color);
+ void did_request_select_dropdown(WeakPtr target, Gfx::IntPoint content_position, i32 minimum_width, Vector items);
+ void select_dropdown_closed(Optional value);
+
enum class PendingNonBlockingDialog {
None,
ColorPicker,
+ Select,
};
struct MediaContextMenu {
@@ -185,7 +190,7 @@ private:
Optional> m_pending_prompt_response;
PendingNonBlockingDialog m_pending_non_blocking_dialog { PendingNonBlockingDialog::None };
- WeakPtr m_pending_non_blocking_dialog_target;
+ WeakPtr m_pending_non_blocking_dialog_target;
Optional m_media_context_menu_element_id;
@@ -272,6 +277,7 @@ public:
// https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable
virtual void page_did_request_file_picker(WeakPtr, [[maybe_unused]] bool multiple) {};
virtual void page_did_request_color_picker([[maybe_unused]] Color current_color) {};
+ virtual void page_did_request_select_dropdown([[maybe_unused]] Gfx::IntPoint content_position, [[maybe_unused]] i32 minimum_width, [[maybe_unused]] Vector items) {};
virtual void page_did_finish_text_test() {};
diff --git a/Userland/Libraries/LibWebView/ViewImplementation.cpp b/Userland/Libraries/LibWebView/ViewImplementation.cpp
index 840114b854..b0b0469437 100644
--- a/Userland/Libraries/LibWebView/ViewImplementation.cpp
+++ b/Userland/Libraries/LibWebView/ViewImplementation.cpp
@@ -266,6 +266,11 @@ void ViewImplementation::color_picker_closed(Optional picked_color)
client().async_color_picker_closed(picked_color);
}
+void ViewImplementation::select_dropdown_closed(Optional value)
+{
+ client().async_select_dropdown_closed(value);
+}
+
void ViewImplementation::toggle_media_play_state()
{
client().async_toggle_media_play_state();
diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h
index 4f9d5770a6..82b995cb41 100644
--- a/Userland/Libraries/LibWebView/ViewImplementation.h
+++ b/Userland/Libraries/LibWebView/ViewImplementation.h
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
@@ -83,6 +84,7 @@ public:
void confirm_closed(bool accepted);
void prompt_closed(Optional response);
void color_picker_closed(Optional picked_color);
+ void select_dropdown_closed(Optional value);
void toggle_media_play_state();
void toggle_media_mute_state();
@@ -154,6 +156,7 @@ public:
Function on_minimize_window;
Function on_fullscreen_window;
Function on_request_color_picker;
+ Function items)> on_request_select_dropdown;
Function on_finish_handling_input_event;
Function on_text_test_finish;
Function on_theme_color_change;
diff --git a/Userland/Libraries/LibWebView/WebContentClient.cpp b/Userland/Libraries/LibWebView/WebContentClient.cpp
index 8cee533bec..277593d2cd 100644
--- a/Userland/Libraries/LibWebView/WebContentClient.cpp
+++ b/Userland/Libraries/LibWebView/WebContentClient.cpp
@@ -384,6 +384,12 @@ void WebContentClient::did_request_color_picker(Color const& current_color)
m_view.on_request_color_picker(current_color);
}
+void WebContentClient::did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector const& items)
+{
+ if (m_view.on_request_select_dropdown)
+ m_view.on_request_select_dropdown(content_position, minimum_width, items);
+}
+
void WebContentClient::did_finish_handling_input_event(bool event_was_accepted)
{
if (m_view.on_finish_handling_input_event)
diff --git a/Userland/Libraries/LibWebView/WebContentClient.h b/Userland/Libraries/LibWebView/WebContentClient.h
index ecfb9e503b..96fae8ced1 100644
--- a/Userland/Libraries/LibWebView/WebContentClient.h
+++ b/Userland/Libraries/LibWebView/WebContentClient.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
@@ -82,6 +83,7 @@ private:
virtual Messages::WebContentClient::DidRequestFullscreenWindowResponse did_request_fullscreen_window() override;
virtual void did_request_file(DeprecatedString const& path, i32) override;
virtual void did_request_color_picker(Color const& current_color) override;
+ virtual void did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) override;
virtual void did_finish_handling_input_event(bool event_was_accepted) override;
virtual void did_finish_text_test() override;
virtual void did_change_theme_color(Gfx::Color color) override;
diff --git a/Userland/Services/WebContent/ConnectionFromClient.cpp b/Userland/Services/WebContent/ConnectionFromClient.cpp
index 4fcafa7bad..e69c6e9844 100644
--- a/Userland/Services/WebContent/ConnectionFromClient.cpp
+++ b/Userland/Services/WebContent/ConnectionFromClient.cpp
@@ -1052,6 +1052,11 @@ void ConnectionFromClient::color_picker_closed(Optional const& picked_col
page().page().color_picker_closed(picked_color);
}
+void ConnectionFromClient::select_dropdown_closed(Optional const& value)
+{
+ page().page().select_dropdown_closed(value);
+}
+
void ConnectionFromClient::toggle_media_play_state()
{
page().page().toggle_media_play_state().release_value_but_fixme_should_propagate_errors();
diff --git a/Userland/Services/WebContent/ConnectionFromClient.h b/Userland/Services/WebContent/ConnectionFromClient.h
index cf2fc794ad..7e85e79114 100644
--- a/Userland/Services/WebContent/ConnectionFromClient.h
+++ b/Userland/Services/WebContent/ConnectionFromClient.h
@@ -110,6 +110,7 @@ private:
virtual void confirm_closed(bool accepted) override;
virtual void prompt_closed(Optional const& response) override;
virtual void color_picker_closed(Optional const& picked_color) override;
+ virtual void select_dropdown_closed(Optional const& value) override;
virtual void toggle_media_play_state() override;
virtual void toggle_media_mute_state() override;
diff --git a/Userland/Services/WebContent/PageClient.cpp b/Userland/Services/WebContent/PageClient.cpp
index b97253943e..7c7ba7c099 100644
--- a/Userland/Services/WebContent/PageClient.cpp
+++ b/Userland/Services/WebContent/PageClient.cpp
@@ -404,6 +404,11 @@ void PageClient::color_picker_closed(Optional picked_color)
page().color_picker_closed(picked_color);
}
+void PageClient::select_dropdown_closed(Optional value)
+{
+ page().select_dropdown_closed(value);
+}
+
Web::WebIDL::ExceptionOr PageClient::toggle_media_play_state()
{
return page().toggle_media_play_state();
@@ -504,6 +509,11 @@ void PageClient::page_did_request_color_picker(Color current_color)
client().async_did_request_color_picker(current_color);
}
+void PageClient::page_did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items)
+{
+ client().async_did_request_select_dropdown(content_position, minimum_width, items);
+}
+
void PageClient::page_did_change_theme_color(Gfx::Color color)
{
client().async_did_change_theme_color(color);
diff --git a/Userland/Services/WebContent/PageClient.h b/Userland/Services/WebContent/PageClient.h
index 576a9a5429..61f320986d 100644
--- a/Userland/Services/WebContent/PageClient.h
+++ b/Userland/Services/WebContent/PageClient.h
@@ -53,6 +53,7 @@ public:
void confirm_closed(bool accepted);
void prompt_closed(Optional response);
void color_picker_closed(Optional picked_color);
+ void select_dropdown_closed(Optional value);
[[nodiscard]] Gfx::Color background_color() const;
@@ -118,6 +119,7 @@ private:
virtual void page_did_close_browsing_context(Web::HTML::BrowsingContext const&) override;
virtual void request_file(Web::FileRequest) override;
virtual void page_did_request_color_picker(Color current_color) override;
+ virtual void page_did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items) override;
virtual void page_did_finish_text_test() override;
virtual void page_did_change_theme_color(Gfx::Color color) override;
virtual void page_did_insert_clipboard_entry(String data, String presentation_style, String mime_type) override;
diff --git a/Userland/Services/WebContent/WebContentClient.ipc b/Userland/Services/WebContent/WebContentClient.ipc
index b4fd9a79e7..7fb17ee9b9 100644
--- a/Userland/Services/WebContent/WebContentClient.ipc
+++ b/Userland/Services/WebContent/WebContentClient.ipc
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
#include
@@ -62,6 +63,7 @@ endpoint WebContentClient
did_request_fullscreen_window() => (Gfx::IntRect window_rect)
did_request_file(DeprecatedString path, i32 request_id) =|
did_request_color_picker(Color current_color) =|
+ did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items) =|
did_finish_handling_input_event(bool event_was_accepted) =|
did_change_theme_color(Gfx::Color color) =|
did_insert_clipboard_entry(String data, String presentation_style, String mime_type) =|
diff --git a/Userland/Services/WebContent/WebContentServer.ipc b/Userland/Services/WebContent/WebContentServer.ipc
index 5926171255..d46222fa11 100644
--- a/Userland/Services/WebContent/WebContentServer.ipc
+++ b/Userland/Services/WebContent/WebContentServer.ipc
@@ -91,6 +91,7 @@ endpoint WebContentServer
confirm_closed(bool accepted) =|
prompt_closed(Optional response) =|
color_picker_closed(Optional picked_color) =|
+ select_dropdown_closed(Optional value) =|
toggle_media_play_state() =|
toggle_media_mute_state() =|