LibWeb: Fix crashing after input into empty contenteditable

Change `EventHandler::handle_keydown()` to no longer assume the cursor
position's node is always a `DOM::Text`. While this assumption holds
for `HTMLInputElement` that has a shadow DOM with a text node, an empty
`contenteditable` might not have any children. With this change,
`handle_keydown()` creates a new text node if the cursor position's
node is not a text node.
This commit is contained in:
Aliaksandr Kalenik 2024-02-24 02:53:35 +01:00 committed by Andreas Kling
parent 5d76fa72e9
commit 906ac71eca
5 changed files with 47 additions and 9 deletions

View file

@ -0,0 +1,15 @@
<script src="../include.js"></script>
<style>
#input {
width: 100px;
height: 100px;
border: 1px solid black;
}
</style>
<div id="input" contenteditable="true"></div>
<script>
test(() => {
const input = document.getElementById("input");
internals.sendText(input, "hello");
});
</script>

View file

@ -28,6 +28,7 @@ public:
JS::GCPtr<Node> node() { return m_node; }
JS::GCPtr<Node const> node() const { return m_node; }
void set_node(JS::NonnullGCPtr<Node> node) { m_node = node; }
unsigned offset() const { return m_offset; }
bool offset_is_at_end_of_node() const;

View file

@ -114,6 +114,15 @@ void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Position> position, u
node.set_data(MUST(builder.to_string()));
node.invalidate_style();
} else {
auto& node = *position->node();
auto& realm = node.realm();
StringBuilder builder;
builder.append_code_point(code_point);
auto text = realm.heap().allocate<DOM::Text>(realm, node.document(), MUST(builder.to_string()));
MUST(node.append_child(*text));
position->set_node(text);
position->set_offset(1);
}
// FIXME: When nodes are removed from the DOM, the associated layout nodes become stale and still

View file

@ -787,8 +787,6 @@ bool EventHandler::handle_keydown(KeyCode key, u32 modifiers, u32 code_point)
return false;
if (m_browsing_context->cursor_position() && m_browsing_context->cursor_position()->node()->is_editable()) {
auto& node = verify_cast<DOM::Text>(*m_browsing_context->cursor_position()->node());
if (key == KeyCode::Key_Backspace) {
if (!m_browsing_context->decrement_cursor_position_offset()) {
// FIXME: Move to the previous node and delete the last character there.
@ -819,23 +817,37 @@ bool EventHandler::handle_keydown(KeyCode key, u32 modifiers, u32 code_point)
return true;
}
if (key == KeyCode::Key_Home) {
m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, 0));
auto& cursor_position_node = *m_browsing_context->cursor_position()->node();
if (cursor_position_node.is_text())
m_browsing_context->set_cursor_position(DOM::Position::create(realm, cursor_position_node, 0));
return true;
}
if (key == KeyCode::Key_End) {
m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, (unsigned)node.data().bytes().size()));
auto& cursor_position_node = *m_browsing_context->cursor_position()->node();
if (cursor_position_node.is_text()) {
auto& text_node = static_cast<DOM::Text&>(cursor_position_node);
m_browsing_context->set_cursor_position(DOM::Position::create(realm, text_node, (unsigned)text_node.data().bytes().size()));
}
return true;
}
if (key == KeyCode::Key_Return) {
if (is<HTML::HTMLInputElement>(node.editable_text_node_owner())) {
auto& input_element = static_cast<HTML::HTMLInputElement&>(*node.editable_text_node_owner());
if (auto* form = input_element.form()) {
HTML::HTMLInputElement* input_element = nullptr;
if (auto node = m_browsing_context->cursor_position()->node()) {
if (node->is_text()) {
auto& text_node = static_cast<DOM::Text&>(*node);
if (is<HTML::HTMLInputElement>(text_node.editable_text_node_owner()))
input_element = static_cast<HTML::HTMLInputElement*>(text_node.editable_text_node_owner());
} else if (node->is_html_input_element()) {
input_element = static_cast<HTML::HTMLInputElement*>(node.ptr());
}
}
if (input_element) {
if (auto* form = input_element->form()) {
form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors();
return true;
}
input_element.commit_pending_changes();
input_element->commit_pending_changes();
return true;
}
}