LibWeb: Implement the "removeFormat" editing command

This commit is contained in:
Jelle Raaijmakers 2025-01-08 16:47:11 +01:00 committed by Andreas Kling
parent ae12f7036b
commit aee8a75c40
Notes: github-actions[bot] 2025-01-10 22:36:28 +00:00
6 changed files with 143 additions and 0 deletions

View file

@ -1206,6 +1206,81 @@ bool command_italic_action(DOM::Document& document, String const&)
return true;
}
// https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command
bool command_remove_format_action(DOM::Document& document, String const&)
{
// 1. Let elements to remove be a list of every removeFormat candidate effectively contained in the active range.
Vector<GC::Ref<DOM::Element>> elements_to_remove;
for_each_node_effectively_contained_in_range(active_range(document), [&](GC::Ref<DOM::Node> descendant) {
if (is_remove_format_candidate(descendant))
elements_to_remove.append(static_cast<DOM::Element&>(*descendant));
return TraversalDecision::Continue;
});
// 2. For each element in elements to remove:
for (auto element : elements_to_remove) {
// 1. While element has children, insert the first child of element into the parent of element immediately
// before element, preserving ranges.
auto element_index = element->index();
while (element->has_children())
move_node_preserving_ranges(*element->first_child(), *element->parent(), element_index++);
// 2. Remove element from its parent.
element->remove();
}
// 3. If the active range's start node is an editable Text node, and its start offset is neither zero nor its start
// node's length, call splitText() on the active range's start node, with argument equal to the active range's
// start offset. Then set the active range's start node to the result, and its start offset to zero.
auto range = active_range(document);
auto start = range->start();
if (start.node->is_editable() && is<DOM::Text>(*start.node) && start.offset != 0 && start.offset != start.node->length()) {
auto new_node = MUST(static_cast<DOM::Text&>(*start.node).split_text(start.offset));
MUST(range->set_start(new_node, 0));
}
// 4. If the active range's end node is an editable Text node, and its end offset is neither zero nor its end node's
// length, call splitText() on the active range's end node, with argument equal to the active range's end offset.
auto end = range->end();
if (end.node->is_editable() && is<DOM::Text>(*end.node) && end.offset != 0 && end.offset != end.node->length())
MUST(static_cast<DOM::Text&>(*end.node).split_text(end.offset));
// 5. Let node list consist of all editable nodes effectively contained in the active range.
Vector<GC::Ref<DOM::Node>> node_list;
for_each_node_effectively_contained_in_range(active_range(document), [&](GC::Ref<DOM::Node> descendant) {
if (descendant->is_editable())
node_list.append(descendant);
return TraversalDecision::Continue;
});
// 6. For each node in node list, while node's parent is a removeFormat candidate in the same editing host as node,
// split the parent of the one-node list consisting of node.
for (auto node : node_list) {
while (node->parent() && is_remove_format_candidate(*node->parent()) && is_in_same_editing_host(*node->parent(), node))
split_the_parent_of_nodes({ node });
}
// 7. For each of the entries in the following list, in the given order, set the selection's value to null, with
// command as given.
// 1. subscript
// 2. bold
// 3. fontName
// 4. fontSize
// 5. foreColor
// 6. hiliteColor
// 7. italic
// 8. strikethrough
// 9. underline
for (auto command_name : { CommandNames::subscript, CommandNames::bold, CommandNames::fontName,
CommandNames::fontSize, CommandNames::foreColor, CommandNames::hiliteColor, CommandNames::italic,
CommandNames::strikethrough, CommandNames::underline }) {
set_the_selections_value(document, command_name, {});
}
// 8. Return true.
return true;
}
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
bool command_style_with_css_action(DOM::Document& document, String const& value)
{
@ -1305,6 +1380,11 @@ static Array const commands {
.relevant_css_property = CSS::PropertyID::FontStyle,
.inline_activated_values = { "italic"sv, "oblique"sv },
},
// https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command
CommandDefinition {
.command = CommandNames::removeFormat,
.action = command_remove_format_action,
},
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
CommandDefinition {
.command = CommandNames::styleWithCSS,

View file

@ -43,6 +43,7 @@ bool command_forward_delete_action(DOM::Document&, String const&);
bool command_insert_linebreak_action(DOM::Document&, String const&);
bool command_insert_paragraph_action(DOM::Document&, String const&);
bool command_italic_action(DOM::Document&, String const&);
bool command_remove_format_action(DOM::Document&, String const&);
bool command_style_with_css_action(DOM::Document&, String const&);
bool command_style_with_css_state(DOM::Document const&);

View file

@ -2246,6 +2246,48 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name)
HTML::TagNames::xmp);
}
// https://w3c.github.io/editing/docs/execCommand/#removeformat-candidate
bool is_remove_format_candidate(GC::Ref<DOM::Node> node)
{
// A removeFormat candidate is an editable HTML element with local name "abbr", "acronym", "b", "bdi", "bdo", "big",
// "blink", "cite", "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
// "span", "strike", "strong", "sub", "sup", "tt", "u", or "var".
if (!node->is_editable())
return false;
if (!is<HTML::HTMLElement>(*node))
return false;
return static_cast<HTML::HTMLElement&>(*node).local_name().is_one_of(
HTML::TagNames::abbr,
HTML::TagNames::acronym,
HTML::TagNames::b,
HTML::TagNames::bdi,
HTML::TagNames::bdo,
HTML::TagNames::big,
HTML::TagNames::blink,
HTML::TagNames::cite,
HTML::TagNames::code,
HTML::TagNames::dfn,
HTML::TagNames::em,
HTML::TagNames::font,
HTML::TagNames::i,
HTML::TagNames::ins,
HTML::TagNames::kbd,
HTML::TagNames::mark,
HTML::TagNames::nobr,
HTML::TagNames::q,
HTML::TagNames::s,
HTML::TagNames::samp,
HTML::TagNames::small,
HTML::TagNames::span,
HTML::TagNames::strike,
HTML::TagNames::strong,
HTML::TagNames::sub,
HTML::TagNames::sup,
HTML::TagNames::tt,
HTML::TagNames::u,
HTML::TagNames::var);
}
// https://w3c.github.io/editing/docs/execCommand/#simple-indentation-element
bool is_simple_indentation_element(GC::Ref<DOM::Node> node)
{

View file

@ -67,6 +67,7 @@ bool is_name_of_an_element_with_inline_contents(FlyString const&);
bool is_non_list_single_line_container(GC::Ref<DOM::Node>);
bool is_prohibited_paragraph_child(GC::Ref<DOM::Node>);
bool is_prohibited_paragraph_child_name(FlyString const&);
bool is_remove_format_candidate(GC::Ref<DOM::Node>);
bool is_simple_indentation_element(GC::Ref<DOM::Node>);
bool is_simple_modifiable_element(GC::Ref<DOM::Node>);
bool is_single_line_container(GC::Ref<DOM::Node>);

View file

@ -0,0 +1,2 @@
Div contents: "f<b>o</b>ob<i>a</i>r"
Div contents: "foobar"

View file

@ -0,0 +1,17 @@
<script src="../include.js"></script>
<div contenteditable="true">f<b>o</b>ob<i>a</i>r</div>
<script>
test(() => {
const range = document.createRange();
getSelection().addRange(range);
const divElm = document.querySelector('div');
println(`Div contents: "${divElm.innerHTML}"`);
// Remove all formatting from 'foobar'
range.setStart(divElm, 0);
range.setEnd(divElm, 5);
document.execCommand('removeFormat');
println(`Div contents: "${divElm.innerHTML}"`);
});
</script>