mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-08 09:09:43 +00:00
LibWeb: Implement the "removeFormat" editing command
This commit is contained in:
parent
ae12f7036b
commit
aee8a75c40
Notes:
github-actions[bot]
2025-01-10 22:36:28 +00:00
Author: https://github.com/gmta
Commit: aee8a75c40
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3216
6 changed files with 143 additions and 0 deletions
|
@ -1206,6 +1206,81 @@ bool command_italic_action(DOM::Document& document, String const&)
|
||||||
return true;
|
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
|
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
|
||||||
bool command_style_with_css_action(DOM::Document& document, String const& value)
|
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,
|
.relevant_css_property = CSS::PropertyID::FontStyle,
|
||||||
.inline_activated_values = { "italic"sv, "oblique"sv },
|
.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
|
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
.command = CommandNames::styleWithCSS,
|
.command = CommandNames::styleWithCSS,
|
||||||
|
|
|
@ -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_linebreak_action(DOM::Document&, String const&);
|
||||||
bool command_insert_paragraph_action(DOM::Document&, String const&);
|
bool command_insert_paragraph_action(DOM::Document&, String const&);
|
||||||
bool command_italic_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_action(DOM::Document&, String const&);
|
||||||
bool command_style_with_css_state(DOM::Document const&);
|
bool command_style_with_css_state(DOM::Document const&);
|
||||||
|
|
||||||
|
|
|
@ -2246,6 +2246,48 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name)
|
||||||
HTML::TagNames::xmp);
|
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
|
// https://w3c.github.io/editing/docs/execCommand/#simple-indentation-element
|
||||||
bool is_simple_indentation_element(GC::Ref<DOM::Node> node)
|
bool is_simple_indentation_element(GC::Ref<DOM::Node> node)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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_non_list_single_line_container(GC::Ref<DOM::Node>);
|
||||||
bool is_prohibited_paragraph_child(GC::Ref<DOM::Node>);
|
bool is_prohibited_paragraph_child(GC::Ref<DOM::Node>);
|
||||||
bool is_prohibited_paragraph_child_name(FlyString const&);
|
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_indentation_element(GC::Ref<DOM::Node>);
|
||||||
bool is_simple_modifiable_element(GC::Ref<DOM::Node>);
|
bool is_simple_modifiable_element(GC::Ref<DOM::Node>);
|
||||||
bool is_single_line_container(GC::Ref<DOM::Node>);
|
bool is_single_line_container(GC::Ref<DOM::Node>);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Div contents: "f<b>o</b>ob<i>a</i>r"
|
||||||
|
Div contents: "foobar"
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue