Ladybird+LibWeb: Add basic select element support

This commit is contained in:
Bastiaan van der Plaat 2023-12-07 15:53:49 +01:00 committed by Andreas Kling
parent b439431488
commit 466153e680
Notes: sideshowbarker 2024-07-16 22:14:49 +09:00
28 changed files with 641 additions and 4 deletions

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select showcase</title>
</head>
<body>
<p>Basic select:</p>
<p>
<select onchange="document.getElementById('a-value').textContent = this.value">
<option value="one">One</option>
<option value="two">Two</option>
<option value="three">Three</option>
<option value="four">Four</option>
<option value="five">Five</option>
</select>
Value: <span id="a-value">?</span>
</p>
<p>Basic select with separators:</p>
<p>
<select onchange="document.getElementById('b-value').textContent = this.value" style="width: 50%;">
<option value="one">One</option>
<option value="two">Two</option>
<hr>
<option value="three">Three</option>
<option value="four">Four</option>
<hr>
<option value="five">Five</option>
<option value="six">Six</option>
</select>
Value: <span id="b-value">?</span>
</p>
<p>Basic select with option groups and separators:</p>
<p>
<select onchange="document.getElementById('c-value').textContent = this.value">
<optgroup label="8.01 Physics I: Classical Mechanics">
<option value="8.01.1">Lecture 01: Powers of Ten
<option value="8.01.2">Lecture 02: 1D Kinematics
<option value="8.01.3">Lecture 03: Vectors
<hr>
<option value="8.01.4">Lecture 04: Random
<optgroup label="8.02 Electricity and Magnetism">
<option value="8.02.1">Lecture 01: What holds our world together?
<option value="8.02.2">Lecture 02: Electric Field
<option value="8.02.3">Lecture 03: Electric Flux
<hr>
<option value="8.02.4">Lecture 04: Random
<optgroup label="8.03 Physics III: Vibrations and Waves">
<option value="8.03.1">Lecture 01: Periodic Phenomenon
<option value="8.03.2">Lecture 02: Beats
<option value="8.03.3">Lecture 03: Forced Oscillations with Damping
<hr>
<option value="8.03.4">Lecture 04: Random
</select>
Value: <span id="c-value">?</span>
</p>
</body>
</html>

View file

@ -36,7 +36,7 @@
@end
@interface LadybirdWebView : NSClipView
@interface LadybirdWebView : NSClipView <NSMenuDelegate>
- (instancetype)init:(id<LadybirdWebViewObserver>)observer;

View file

@ -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<Web::HTML::SelectItem> 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]));

View file

@ -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<Web::HTML::SelectItem> 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<QAction*>(sender());
auto value = action->data().value<QString>();
view().select_dropdown_closed(ak_string_from_qstring(value));
}
void Tab::update_reset_zoom_button()
{
auto zoom_level = view().zoom_level();

View file

@ -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 };

View file

@ -140,6 +140,7 @@ source_set("HTML") {
"PotentialCORSRequest.cpp",
"PromiseRejectionEvent.cpp",
"RemoteBrowsingContext.cpp",
"SelectItem.cpp",
"SessionHistoryEntry.cpp",
"SharedImageRequest.cpp",
"SourceSet.cpp",

View file

@ -0,0 +1,5 @@
1. "one"
2. 0
3. "two"
4. 3
5. "three"

View file

@ -0,0 +1,67 @@
<script src="include.js"></script>
<script>
test(() => {
let testCounter = 1;
function testPart(part) {
println(`${testCounter++}. ${JSON.stringify(part())}`);
}
// 1. Get select value of unedited
testPart(() => {
const select = document.createElement('select');
// FIXME: Remove selected attribute (currently this is needed because the DIN children are not loaded to select first item in test run)
select.innerHTML = `
<option value="one" selected>One</option>
<option value="two">Two</option>
<option value="three">Three</option>
`;
return select.value;
});
// 2. Get select selectedIndex
testPart(() => {
const select = document.createElement('select');
// FIXME: Remove selected attribute (currently this is needed because the DIN children are not loaded to select first item in test run)
select.innerHTML = `
<option value="one" selected>One</option>
<option value="two">Two</option>
<option value="three">Three</option>
`;
return select.selectedIndex;
});
// 3. Get select value of selectedIndex
testPart(() => {
const select = document.createElement('select');
select.innerHTML = `
<option value="one">One</option>
<option value="two">Two</option>
<option value="three">Three</option>
`;
select.selectedIndex = 1;
return select.value;
});
// 4. Get select length
testPart(() => {
const select = document.createElement('select');
select.innerHTML = `
<option value="one">One</option>
<option value="two">Two</option>
<option value="three">Three</option>
`;
return select.length;
});
// 5. Get select value of init selected
testPart(() => {
const select = document.createElement('select');
select.innerHTML = `
<option value="one">One</option>
<option value="two">Two</option>
<option value="three" selected>Three</option>
`;
return select.value;
});
});
</script>

View file

@ -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<Web::HTML::SelectItem> 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();

View file

@ -115,6 +115,8 @@ private:
void update_status(Optional<String> 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<WebView::OutOfProcessWebView> m_web_content_view;
@ -155,6 +157,9 @@ private:
RefPtr<GUI::Menu> m_go_back_context_menu;
RefPtr<GUI::Menu> m_go_forward_context_menu;
RefPtr<GUI::Menu> m_select_dropdown;
bool m_select_dropdown_closed_by_action { false };
DeprecatedString m_title;
RefPtr<Gfx::Bitmap const> m_icon;

View file

@ -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

View file

@ -1,15 +1,27 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.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/HTML/EventNames.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLHRElement.h>
#include <LibWeb/HTML/HTMLOptGroupElement.h>
#include <LibWeb/HTML/HTMLOptionElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
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<Layout::Node> HTMLSelectElement::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);
}
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-options
@ -177,6 +200,39 @@ Optional<ARIA::Role> 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<void> 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<String> strip_newlines(Optional<String> 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<SelectItem> items;
for (auto const& child : children_as_vector()) {
if (is<HTMLOptGroupElement>(*child)) {
auto& opt_group_element = verify_cast<HTMLOptGroupElement>(*child);
Vector<SelectItem> opt_group_items;
for (auto const& child : opt_group_element.children_as_vector()) {
if (is<HTMLOptionElement>(*child)) {
auto& option_element = verify_cast<HTMLOptionElement>(*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<HTMLHRElement>(*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<HTMLOptionElement>(*child)) {
auto& option_element = verify_cast<HTMLOptionElement>(*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<HTMLHRElement>(*child)) {
items.append(SelectItem { SelectItem::Type::Separator });
}
}
// Request select dropdown
auto weak_element = make_weak_ptr<HTMLSelectElement>();
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<String> 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<DOM::ShadowRoot>(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("<svg style=\"width: 16px; height: 16px;\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z\" /></svg>"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;
}
}
}
}

View file

@ -2,6 +2,7 @@
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -24,6 +25,8 @@ class HTMLSelectElement final
public:
virtual ~HTMLSelectElement() override;
virtual JS::GCPtr<Layout::Node> create_layout_node(NonnullRefPtr<CSS::StyleProperties>) override;
JS::GCPtr<HTMLOptionsCollection> 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<void> set_value(String const&);
bool is_open() const { return m_is_open; }
void set_is_open(bool);
@ -66,6 +72,14 @@ public:
virtual Optional<ARIA::Role> 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<String> 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<HTMLOptionsCollection> m_options;
bool m_is_open { false };
JS::GCPtr<DOM::Element> m_inner_text_element;
};
}

View file

@ -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;

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SelectItem.h"
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
template<>
ErrorOr<void> 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<Web::HTML::SelectItem> IPC::decode(Decoder& decoder)
{
auto type = TRY(decoder.decode<Web::HTML::SelectItem::Type>());
auto label = TRY(decoder.decode<Optional<String>>());
auto value = TRY(decoder.decode<Optional<String>>());
auto items = TRY(decoder.decode<Optional<Vector<Web::HTML::SelectItem>>>());
auto selected = TRY(decoder.decode<bool>());
return Web::HTML::SelectItem { type, move(label), move(value), move(items), selected };
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibIPC/Forward.h>
namespace Web::HTML {
struct SelectItem {
enum class Type {
OptionGroup,
Option,
Separator,
};
Type type;
Optional<String> label = {};
Optional<String> value = {};
Optional<Vector<SelectItem>> items = {};
bool selected = false;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, Web::HTML::SelectItem const&);
template<>
ErrorOr<Web::HTML::SelectItem> decode(Decoder&);
}

View file

@ -15,6 +15,7 @@
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/TraversableNavigable.h>
@ -323,7 +324,30 @@ void Page::color_picker_closed(Optional<Color> 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<HTML::HTMLInputElement>(*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<HTML::HTMLSelectElement> target, Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> 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<String> 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<HTML::HTMLSelectElement>(*m_pending_non_blocking_dialog_target);
select_element.did_select_value(move(value));
m_pending_non_blocking_dialog_target.clear();
}
}

View file

@ -30,6 +30,7 @@
#include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/Loader/FileRequest.h>
#include <LibWeb/PixelUnits.h>
@ -128,9 +129,13 @@ public:
void did_request_color_picker(WeakPtr<HTML::HTMLInputElement> target, Color current_color);
void color_picker_closed(Optional<Color> picked_color);
void did_request_select_dropdown(WeakPtr<HTML::HTMLSelectElement> target, Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items);
void select_dropdown_closed(Optional<String> value);
enum class PendingNonBlockingDialog {
None,
ColorPicker,
Select,
};
struct MediaContextMenu {
@ -185,7 +190,7 @@ private:
Optional<Optional<String>> m_pending_prompt_response;
PendingNonBlockingDialog m_pending_non_blocking_dialog { PendingNonBlockingDialog::None };
WeakPtr<HTML::HTMLInputElement> m_pending_non_blocking_dialog_target;
WeakPtr<HTML::HTMLElement> m_pending_non_blocking_dialog_target;
Optional<int> 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<DOM::EventTarget>, [[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<Web::HTML::SelectItem> items) {};
virtual void page_did_finish_text_test() {};

View file

@ -266,6 +266,11 @@ void ViewImplementation::color_picker_closed(Optional<Color> picked_color)
client().async_color_picker_closed(picked_color);
}
void ViewImplementation::select_dropdown_closed(Optional<String> value)
{
client().async_select_dropdown_closed(value);
}
void ViewImplementation::toggle_media_play_state()
{
client().async_toggle_media_play_state();

View file

@ -14,6 +14,7 @@
#include <LibGfx/StandardCursor.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWebView/Forward.h>
#include <LibWebView/WebContentClient.h>
@ -83,6 +84,7 @@ public:
void confirm_closed(bool accepted);
void prompt_closed(Optional<String> response);
void color_picker_closed(Optional<Color> picked_color);
void select_dropdown_closed(Optional<String> value);
void toggle_media_play_state();
void toggle_media_mute_state();
@ -154,6 +156,7 @@ public:
Function<Gfx::IntRect()> on_minimize_window;
Function<Gfx::IntRect()> on_fullscreen_window;
Function<void(Color current_color)> on_request_color_picker;
Function<void(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items)> on_request_select_dropdown;
Function<void(bool)> on_finish_handling_input_event;
Function<void()> on_text_test_finish;
Function<void(Gfx::Color)> on_theme_color_change;

View file

@ -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<Web::HTML::SelectItem> 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)

View file

@ -9,6 +9,7 @@
#include <AK/HashMap.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/SelectItem.h>
#include <WebContent/WebContentClientEndpoint.h>
#include <WebContent/WebContentServerEndpoint.h>
@ -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<Web::HTML::SelectItem> 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;

View file

@ -1052,6 +1052,11 @@ void ConnectionFromClient::color_picker_closed(Optional<Color> const& picked_col
page().page().color_picker_closed(picked_color);
}
void ConnectionFromClient::select_dropdown_closed(Optional<String> 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();

View file

@ -110,6 +110,7 @@ private:
virtual void confirm_closed(bool accepted) override;
virtual void prompt_closed(Optional<String> const& response) override;
virtual void color_picker_closed(Optional<Color> const& picked_color) override;
virtual void select_dropdown_closed(Optional<String> const& value) override;
virtual void toggle_media_play_state() override;
virtual void toggle_media_mute_state() override;

View file

@ -404,6 +404,11 @@ void PageClient::color_picker_closed(Optional<Color> picked_color)
page().color_picker_closed(picked_color);
}
void PageClient::select_dropdown_closed(Optional<String> value)
{
page().select_dropdown_closed(value);
}
Web::WebIDL::ExceptionOr<void> 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<Web::HTML::SelectItem> 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);

View file

@ -53,6 +53,7 @@ public:
void confirm_closed(bool accepted);
void prompt_closed(Optional<String> response);
void color_picker_closed(Optional<Color> picked_color);
void select_dropdown_closed(Optional<String> 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<Web::HTML::SelectItem> 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;

View file

@ -6,6 +6,7 @@
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/Page/Page.h>
#include <LibWebView/Attribute.h>
@ -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<Web::HTML::SelectItem> 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) =|

View file

@ -91,6 +91,7 @@ endpoint WebContentServer
confirm_closed(bool accepted) =|
prompt_closed(Optional<String> response) =|
color_picker_closed(Optional<Color> picked_color) =|
select_dropdown_closed(Optional<String> value) =|
toggle_media_play_state() =|
toggle_media_mute_state() =|