Skip to content

Commit

Permalink
LibWeb: Implement document.execCommand('insertLinebreak')
Browse files Browse the repository at this point in the history
  • Loading branch information
gmta committed Dec 10, 2024
1 parent ae53059 commit 495006d
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 0 deletions.
77 changes: 77 additions & 0 deletions Libraries/LibWeb/Editing/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/HTMLLIElement.h>
#include <LibWeb/HTML/HTMLTableElement.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Namespace.h>

namespace Web::Editing {
Expand Down Expand Up @@ -374,6 +375,81 @@ bool command_delete_action(DOM::Document& document, String const&)
return true;
}

// https://w3c.github.io/editing/docs/execCommand/#the-insertlinebreak-command
bool command_insert_linebreak_action(DOM::Document& document, String const&)
{
// 1. Delete the selection, with strip wrappers false.
auto& selection = *document.get_selection();
delete_the_selection(selection, true, false);

// 2. If the active range's start node is neither editable nor an editing host, return true.
auto& active_range = *selection.range();
auto start_node = active_range.start_container();
if (!start_node->is_editable_or_editing_host())
return true;

// 3. If the active range's start node is an Element, and "br" is not an allowed child of it, return true.
if (is<DOM::Element>(*start_node) && !is_allowed_child_of_node(HTML::TagNames::br, start_node))
return true;

// 4. If the active range's start node is not an Element, and "br" is not an allowed child of the active range's
// start node's parent, return true.
if (!is<DOM::Element>(*start_node) && start_node->parent() && !is_allowed_child_of_node(HTML::TagNames::br, GC::Ref { *start_node->parent() }))
return true;

// 5. If the active range's start node is a Text node and its start offset is zero, call collapse() on the context
// object's selection, with first argument equal to the active range's start node's parent and second argument
// equal to the active range's start node's index.
if (is<DOM::Text>(*start_node) && active_range.start_offset() == 0)
MUST(selection.collapse(start_node->parent(), start_node->index()));

// 6. If the active range's start node is a Text node and its start offset is the length of its start node, call
// collapse() on the context object's selection, with first argument equal to the active range's start node's
// parent and second argument equal to one plus the active range's start node's index.
if (is<DOM::Text>(*start_node) && active_range.start_offset() == start_node->length())
MUST(selection.collapse(start_node->parent(), start_node->index() + 1));

// AD-HOC: If the active range's start node is a Text node and its resolved value for "white-space" is one of "pre",
// "pre-line" or "pre-wrap":
// * Insert a newline (\n) character at the active range's start offset;
// * Collapse the selection with active range's start node as the first argument and one plus active range's
// start offset as the second argument
// * Insert another newline (\n) character if the active range's start offset is equal to the length of the
// active range's start node.
// * Return true.
if (is<DOM::Text>(*start_node) && start_node->layout_node()) {
auto& text_node = static_cast<DOM::Text&>(*start_node);
auto white_space = text_node.layout_node()->computed_values().white_space();
if (first_is_one_of(white_space, CSS::WhiteSpace::Pre, CSS::WhiteSpace::PreLine, CSS::WhiteSpace::PreWrap)) {
MUST(text_node.insert_data(active_range.start_offset(), "\n"_string));
MUST(selection.collapse(start_node, active_range.start_offset() + 1));
if (selection.range()->start_offset() == start_node->length())
MUST(text_node.insert_data(active_range.start_offset(), "\n"_string));
return true;
}
}

// 7. Let br be the result of calling createElement("br") on the context object.
auto br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));

// 8. Call insertNode(br) on the active range.
MUST(active_range.insert_node(br));

// 9. Call collapse() on the context object's selection, with br's parent as the first argument and one plus br's
// index as the second argument.
MUST(selection.collapse(br->parent(), br->index() + 1));

// 10. If br is a collapsed line break, call createElement("br") on the context object and let extra br be the
// result, then call insertNode(extra br) on the active range.
if (is_collapsed_line_break(br)) {
auto extra_br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
MUST(active_range.insert_node(extra_br));
}

// 11. Return true.
return true;
}

// https://w3c.github.io/editing/docs/execCommand/#the-insertparagraph-command
bool command_insert_paragraph_action(DOM::Document& document, String const&)
{
Expand Down Expand Up @@ -705,6 +781,7 @@ bool command_style_with_css_state(DOM::Document const& document)
static Array const commands {
CommandDefinition { CommandNames::delete_, command_delete_action, {}, {}, {} },
CommandDefinition { CommandNames::defaultParagraphSeparator, command_default_paragraph_separator_action, {}, {}, command_default_paragraph_separator_value },
CommandDefinition { CommandNames::insertLineBreak, command_insert_linebreak_action, {}, {}, {} },
CommandDefinition { CommandNames::insertParagraph, command_insert_paragraph_action, {}, {}, {} },
CommandDefinition { CommandNames::styleWithCSS, command_style_with_css_action, {}, command_style_with_css_state, {} },
};
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/Editing/Commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Optional<CommandDefinition const&> find_command_definition(FlyString const&);
bool command_default_paragraph_separator_action(DOM::Document&, String const&);
String command_default_paragraph_separator_value(DOM::Document const&);
bool command_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_style_with_css_action(DOM::Document&, String const&);
bool command_style_with_css_state(DOM::Document const&);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Before: "foobar"
After: "foo<br>bar"
Before: "<p style="white-space: pre">foobar</p>"
After: "<p style="white-space: pre">foo
bar</p>"
Before: "<p style="white-space: pre">foobar</p>"
After: "<p style="white-space: pre">foobar

</p>"
40 changes: 40 additions & 0 deletions Tests/LibWeb/Text/input/Editing/execCommand-insertLinebreak.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script src="../include.js"></script>
<div id="a" contenteditable>foobar</div>
<div id="b" contenteditable><p style="white-space: pre">foobar</p></div>
<div id="c" contenteditable><p style="white-space: pre">foobar</p></div>
<script>
test(() => {
let divA = document.querySelector('div#a');
let divB = document.querySelector('div#b');
let divC = document.querySelector('div#c');

document.body.offsetWidth // Force a layout

// A: Insert linebreak after 'foo'
println(`Before: "${divA.innerHTML}"`);
var range = document.createRange();
range.setStart(divA.firstChild, 3);
getSelection().addRange(range);
document.execCommand('insertLinebreak');
println(`After: "${divA.innerHTML}"`);
getSelection().empty();

// B: Insert linebreak after 'foo'
println(`Before: "${divB.innerHTML}"`);
var range = document.createRange();
range.setStart(divB.firstChild.firstChild, 3);
getSelection().addRange(range);
document.execCommand('insertLinebreak');
println(`After: "${divB.innerHTML}"`);
getSelection().empty();

// C: Insert linebreak after 'bar'
println(`Before: "${divC.innerHTML}"`);
var range = document.createRange();
range.setStart(divC.firstChild.firstChild, 6);
getSelection().addRange(range);
document.execCommand('insertLinebreak');
println(`After: "${divC.innerHTML}"`);
getSelection().empty();
});
</script>

0 comments on commit 495006d

Please sign in to comment.