mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-30 04:39:06 +00:00
LibWeb: Add command state & value overrides to DOM::Document
This commit is contained in:
parent
67800091c5
commit
e21ee10b3c
Notes:
github-actions[bot]
2025-01-10 22:38:29 +00:00
Author: https://github.com/gmta
Commit: e21ee10b3c
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3216
6 changed files with 173 additions and 7 deletions
|
@ -4,6 +4,7 @@
|
|||
* Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org>
|
||||
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
|
||||
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -662,6 +663,15 @@ WebIDL::ExceptionOr<Document*> Document::open(Optional<String> const&, Optional<
|
|||
// 11. Replace all with null within document, without firing any mutation events.
|
||||
replace_all(nullptr);
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#state-override
|
||||
// When document.open() is called and a document's singleton objects are all replaced by new instances of those
|
||||
// objects, editing state associated with that document (including the CSS styling flag, default single-line
|
||||
// container name, and any state overrides or value overrides) must be reset.
|
||||
set_css_styling_flag(false);
|
||||
set_default_single_line_container_name(HTML::TagNames::div);
|
||||
reset_command_state_overrides();
|
||||
reset_command_value_overrides();
|
||||
|
||||
// 12. If document is fully active, then:
|
||||
if (is_fully_active()) {
|
||||
// 1. Let newURL be a copy of entryDocument's URL.
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -765,6 +766,18 @@ public:
|
|||
bool css_styling_flag() const { return m_css_styling_flag; }
|
||||
void set_css_styling_flag(bool flag) { m_css_styling_flag = flag; }
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#state-override
|
||||
Optional<bool> command_state_override(FlyString const& command) const { return m_command_state_override.get(command); }
|
||||
void set_command_state_override(FlyString const& command, bool state) { m_command_state_override.set(command, state); }
|
||||
void clear_command_state_override(FlyString const& command) { m_command_state_override.remove(command); }
|
||||
void reset_command_state_overrides() { m_command_state_override.clear(); }
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#value-override
|
||||
Optional<String const&> command_value_override(FlyString const& command) const { return m_command_value_override.get(command); }
|
||||
void set_command_value_override(FlyString const& command, String const& value);
|
||||
void clear_command_value_override(FlyString const& command);
|
||||
void reset_command_value_overrides() { m_command_value_override.clear(); }
|
||||
|
||||
GC::Ptr<DOM::Document> container_document() const;
|
||||
|
||||
GC::Ptr<HTML::Storage> session_storage_holder() { return m_session_storage_holder; }
|
||||
|
@ -1089,6 +1102,12 @@ private:
|
|||
// https://w3c.github.io/editing/docs/execCommand/#css-styling-flag
|
||||
bool m_css_styling_flag { false };
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#state-override
|
||||
HashMap<FlyString, bool> m_command_state_override;
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#value-override
|
||||
HashMap<FlyString, String> m_command_value_override;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#session-storage-holder
|
||||
// A Document object has an associated session storage holder, which is null or a Storage object. It is initially null.
|
||||
GC::Ptr<HTML::Storage> m_session_storage_holder;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
* Copyright (c) 2024-2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -176,8 +176,65 @@ bool Document::query_command_indeterm(FlyString const& command)
|
|||
if (!optional_command.has_value())
|
||||
return false;
|
||||
auto const& command_definition = optional_command.value();
|
||||
if (!command_definition.indeterminate)
|
||||
if (!command_definition.indeterminate) {
|
||||
// https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values
|
||||
// If a command is a standard inline value command, it is indeterminate if among formattable nodes that are
|
||||
// effectively contained in the active range, there are two that have distinct effective command values.
|
||||
if (command.is_one_of(Editing::CommandNames::backColor, Editing::CommandNames::fontName,
|
||||
Editing::CommandNames::foreColor, Editing::CommandNames::hiliteColor)) {
|
||||
Optional<String> first_node_value;
|
||||
auto range = Editing::active_range(*this);
|
||||
bool has_distinct_values = false;
|
||||
Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref<Node> descendant) {
|
||||
if (!Editing::is_formattable_node(descendant))
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
auto node_value = Editing::effective_command_value(descendant, command);
|
||||
if (!node_value.has_value())
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
if (!first_node_value.has_value()) {
|
||||
first_node_value = node_value.value();
|
||||
} else if (first_node_value.value() != node_value.value()) {
|
||||
has_distinct_values = true;
|
||||
return TraversalDecision::Break;
|
||||
}
|
||||
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
return has_distinct_values;
|
||||
}
|
||||
|
||||
// If a command has inline command activated values defined but nothing else defines when it is indeterminate,
|
||||
// it is indeterminate if among formattable nodes effectively contained in the active range, there is at least
|
||||
// one whose effective command value is one of the given values and at least one whose effective command value
|
||||
// is not one of the given values.
|
||||
if (!command_definition.inline_activated_values.is_empty()) {
|
||||
auto range = Editing::active_range(*this);
|
||||
bool has_at_least_one_match = false;
|
||||
bool has_at_least_one_mismatch = false;
|
||||
Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref<Node> descendant) {
|
||||
if (!Editing::is_formattable_node(descendant))
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
auto node_value = Editing::effective_command_value(descendant, command);
|
||||
if (!node_value.has_value())
|
||||
return TraversalDecision::Continue;
|
||||
|
||||
if (command_definition.inline_activated_values.contains_slow(node_value.value()))
|
||||
has_at_least_one_match = true;
|
||||
else
|
||||
has_at_least_one_mismatch = true;
|
||||
|
||||
if (has_at_least_one_match && has_at_least_one_mismatch)
|
||||
return TraversalDecision::Break;
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
return has_at_least_one_match && has_at_least_one_mismatch;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Return true if command is indeterminate, otherwise false.
|
||||
return command_definition.indeterminate(*this);
|
||||
|
@ -191,10 +248,35 @@ bool Document::query_command_state(FlyString const& command)
|
|||
if (!optional_command.has_value())
|
||||
return false;
|
||||
auto const& command_definition = optional_command.release_value();
|
||||
if (!command_definition.state)
|
||||
return false;
|
||||
auto state_override = command_state_override(command);
|
||||
if (!command_definition.state && !state_override.has_value()) {
|
||||
// https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values
|
||||
// If a command has inline command activated values defined, its state is true if either no formattable node is
|
||||
// effectively contained in the active range, and the active range's start node's effective command value is one
|
||||
// of the given values;
|
||||
auto const& inline_values = command_definition.inline_activated_values;
|
||||
if (inline_values.is_empty())
|
||||
return false;
|
||||
auto range = Editing::active_range(*this);
|
||||
Vector<GC::Ref<Node>> formattable_nodes;
|
||||
Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref<Node> descendant) {
|
||||
if (Editing::is_formattable_node(descendant))
|
||||
formattable_nodes.append(descendant);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
if (formattable_nodes.is_empty())
|
||||
return inline_values.contains_slow(Editing::effective_command_value(range->start_container(), command).value_or({}));
|
||||
|
||||
// FIXME: 2. If the state override for command is set, return it.
|
||||
// or if there is at least one formattable node effectively contained in the active range, and all of them have
|
||||
// an effective command value equal to one of the given values.
|
||||
return all_of(formattable_nodes, [&](GC::Ref<Node> node) {
|
||||
return inline_values.contains_slow(Editing::effective_command_value(node, command).value_or({}));
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If the state override for command is set, return it.
|
||||
if (state_override.has_value())
|
||||
return state_override.release_value();
|
||||
|
||||
// 3. Return true if command's state is true, otherwise false.
|
||||
return command_definition.state(*this);
|
||||
|
@ -217,16 +299,45 @@ String Document::query_command_value(FlyString const& command)
|
|||
if (!optional_command.has_value())
|
||||
return {};
|
||||
auto const& command_definition = optional_command.release_value();
|
||||
if (!command_definition.value)
|
||||
auto value_override = command_value_override(command);
|
||||
if (!command_definition.value && !value_override.has_value())
|
||||
return {};
|
||||
|
||||
// FIXME: 2. If command is "fontSize" and its value override is set, convert the value override to an
|
||||
// integer number of pixels and return the legacy font size for the result.
|
||||
|
||||
// FIXME: 3. If the value override for command is set, return it.
|
||||
// 3. If the value override for command is set, return it.
|
||||
if (value_override.has_value())
|
||||
return value_override.release_value();
|
||||
|
||||
// 4. Return command's value.
|
||||
return command_definition.value(*this);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#value-override
|
||||
void Document::set_command_value_override(FlyString const& command, String const& value)
|
||||
{
|
||||
m_command_value_override.set(command, value);
|
||||
|
||||
// The value override for the backColor command must be the same as the value override for the hiliteColor command,
|
||||
// such that setting one sets the other to the same thing and unsetting one unsets the other.
|
||||
if (command == Editing::CommandNames::backColor)
|
||||
m_command_value_override.set(Editing::CommandNames::hiliteColor, value);
|
||||
else if (command == Editing::CommandNames::hiliteColor)
|
||||
m_command_value_override.set(Editing::CommandNames::backColor, value);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#value-override
|
||||
void Document::clear_command_value_override(FlyString const& command)
|
||||
{
|
||||
m_command_value_override.remove(command);
|
||||
|
||||
// The value override for the backColor command must be the same as the value override for the hiliteColor command,
|
||||
// such that setting one sets the other to the same thing and unsetting one unsets the other.
|
||||
if (command == Editing::CommandNames::backColor)
|
||||
m_command_value_override.remove(Editing::CommandNames::hiliteColor);
|
||||
else if (command == Editing::CommandNames::hiliteColor)
|
||||
m_command_value_override.remove(Editing::CommandNames::backColor);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2745,6 +2745,23 @@ GC::Ptr<DOM::Node> wrap(
|
|||
return new_parent;
|
||||
}
|
||||
|
||||
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range> range, Function<TraversalDecision(GC::Ref<DOM::Node>)> callback)
|
||||
{
|
||||
if (!range)
|
||||
return;
|
||||
|
||||
// A node can still be "effectively contained" in range even if it's not actually contained within the range; so we
|
||||
// need to do an inclusive subtree traversal since the common ancestor could be matched as well.
|
||||
range->common_ancestor_container()->for_each_in_inclusive_subtree([&](GC::Ref<DOM::Node> descendant) {
|
||||
if (!is_effectively_contained_in_range(descendant, *range)) {
|
||||
// NOTE: We cannot skip children here since if a descendant is not effectively contained within a range, its
|
||||
// children might still be.
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
return callback(descendant);
|
||||
});
|
||||
}
|
||||
|
||||
bool has_visible_children(GC::Ref<DOM::Node> node)
|
||||
{
|
||||
bool has_visible_child = false;
|
||||
|
|
|
@ -86,6 +86,7 @@ GC::Ptr<DOM::Node> wrap(Vector<GC::Ref<DOM::Node>>, Function<bool(GC::Ref<DOM::N
|
|||
|
||||
// Utility methods:
|
||||
|
||||
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range>, Function<TraversalDecision(GC::Ref<DOM::Node>)>);
|
||||
bool has_visible_children(GC::Ref<DOM::Node>);
|
||||
bool is_heading(FlyString const&);
|
||||
Optional<CSS::Display> resolved_display(GC::Ref<DOM::Node>);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2021-2022, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -470,6 +471,13 @@ void Selection::set_range(GC::Ptr<DOM::Range> range)
|
|||
|
||||
if (m_range)
|
||||
m_range->set_associated_selection({}, this);
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#state-override
|
||||
// Whenever the number of ranges in the selection changes to something different, and whenever a boundary point of
|
||||
// the range at a given index in the selection changes to something different, the state override and value override
|
||||
// must be unset for every command.
|
||||
m_document->reset_command_state_overrides();
|
||||
m_document->reset_command_value_overrides();
|
||||
}
|
||||
|
||||
GC::Ptr<DOM::Position> Selection::cursor_position() const
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue