/src/serenity/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020-2021, the SerenityOS developers. |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/StringBuilder.h> |
8 | | #include <AK/Utf8View.h> |
9 | | #include <LibLocale/Segmenter.h> |
10 | | #include <LibWeb/DOM/Document.h> |
11 | | #include <LibWeb/DOM/Position.h> |
12 | | #include <LibWeb/DOM/Range.h> |
13 | | #include <LibWeb/DOM/Text.h> |
14 | | #include <LibWeb/HTML/BrowsingContext.h> |
15 | | #include <LibWeb/Layout/Viewport.h> |
16 | | #include <LibWeb/Page/EditEventHandler.h> |
17 | | |
18 | | namespace Web { |
19 | | |
20 | | void EditEventHandler::handle_delete_character_after(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> cursor_position) |
21 | 0 | { |
22 | 0 | auto& node = verify_cast<DOM::Text>(*cursor_position->node()); |
23 | 0 | auto& text = node.data(); |
24 | |
|
25 | 0 | auto next_offset = node.grapheme_segmenter().next_boundary(cursor_position->offset()); |
26 | 0 | if (!next_offset.has_value()) { |
27 | | // FIXME: Move to the next node and delete the first character there. |
28 | 0 | return; |
29 | 0 | } |
30 | | |
31 | 0 | StringBuilder builder; |
32 | 0 | builder.append(text.bytes_as_string_view().substring_view(0, cursor_position->offset())); |
33 | 0 | builder.append(text.bytes_as_string_view().substring_view(*next_offset)); |
34 | 0 | node.set_data(MUST(builder.to_string())); |
35 | |
|
36 | 0 | document->user_did_edit_document_text({}); |
37 | 0 | } |
38 | | |
39 | | // This method is quite convoluted but this is necessary to make editing feel intuitive. |
40 | | void EditEventHandler::handle_delete(JS::NonnullGCPtr<DOM::Document> document, DOM::Range& range) |
41 | 0 | { |
42 | 0 | auto* start = verify_cast<DOM::Text>(range.start_container()); |
43 | 0 | auto* end = verify_cast<DOM::Text>(range.end_container()); |
44 | |
|
45 | 0 | if (start == end) { |
46 | 0 | StringBuilder builder; |
47 | 0 | builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset())); |
48 | 0 | builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset())); |
49 | |
|
50 | 0 | start->set_data(MUST(builder.to_string())); |
51 | 0 | } else { |
52 | | // Remove all the nodes that are fully enclosed in the range. |
53 | 0 | HashTable<DOM::Node*> queued_for_deletion; |
54 | 0 | for (auto* node = start->next_in_pre_order(); node; node = node->next_in_pre_order()) { |
55 | 0 | if (node == end) |
56 | 0 | break; |
57 | | |
58 | 0 | queued_for_deletion.set(node); |
59 | 0 | } |
60 | 0 | for (auto* parent = start->parent(); parent; parent = parent->parent()) |
61 | 0 | queued_for_deletion.remove(parent); |
62 | 0 | for (auto* parent = end->parent(); parent; parent = parent->parent()) |
63 | 0 | queued_for_deletion.remove(parent); |
64 | 0 | for (auto* node : queued_for_deletion) |
65 | 0 | node->remove(); |
66 | | |
67 | | // Join the parent nodes of start and end. |
68 | 0 | DOM::Node *insert_after = start, *remove_from = end, *parent_of_end = end->parent(); |
69 | 0 | while (remove_from) { |
70 | 0 | auto* next_sibling = remove_from->next_sibling(); |
71 | |
|
72 | 0 | remove_from->remove(); |
73 | 0 | insert_after->parent()->insert_before(*remove_from, *insert_after); |
74 | |
|
75 | 0 | insert_after = remove_from; |
76 | 0 | remove_from = next_sibling; |
77 | 0 | } |
78 | 0 | if (!parent_of_end->has_children()) { |
79 | 0 | if (parent_of_end->parent()) |
80 | 0 | parent_of_end->remove(); |
81 | 0 | } |
82 | | |
83 | | // Join the start and end nodes. |
84 | 0 | StringBuilder builder; |
85 | 0 | builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset())); |
86 | 0 | builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset())); |
87 | |
|
88 | 0 | start->set_data(MUST(builder.to_string())); |
89 | 0 | end->remove(); |
90 | 0 | } |
91 | |
|
92 | 0 | document->user_did_edit_document_text({}); |
93 | 0 | } |
94 | | |
95 | | void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> position, u32 code_point) |
96 | 0 | { |
97 | 0 | StringBuilder builder; |
98 | 0 | builder.append_code_point(code_point); |
99 | 0 | handle_insert(document, position, MUST(builder.to_string())); |
100 | 0 | } |
101 | | |
102 | | void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> position, String data) |
103 | 0 | { |
104 | 0 | if (is<DOM::Text>(*position->node())) { |
105 | 0 | auto& node = verify_cast<DOM::Text>(*position->node()); |
106 | |
|
107 | 0 | StringBuilder builder; |
108 | 0 | builder.append(node.data().bytes_as_string_view().substring_view(0, position->offset())); |
109 | 0 | builder.append(data); |
110 | 0 | builder.append(node.data().bytes_as_string_view().substring_view(position->offset())); |
111 | | |
112 | | // Cut string by max length |
113 | | // FIXME: Cut by UTF-16 code units instead of raw bytes |
114 | 0 | if (auto max_length = node.max_length(); max_length.has_value() && builder.string_view().length() > *max_length) { |
115 | 0 | node.set_data(MUST(String::from_utf8(builder.string_view().substring_view(0, *max_length)))); |
116 | 0 | } else { |
117 | 0 | node.set_data(MUST(builder.to_string())); |
118 | 0 | } |
119 | 0 | node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion); |
120 | 0 | } else { |
121 | 0 | auto& node = *position->node(); |
122 | 0 | auto& realm = node.realm(); |
123 | 0 | auto text = realm.heap().allocate<DOM::Text>(realm, node.document(), data); |
124 | 0 | MUST(node.append_child(*text)); |
125 | 0 | position->set_node(text); |
126 | 0 | position->set_offset(1); |
127 | 0 | } |
128 | |
|
129 | 0 | document->user_did_edit_document_text({}); |
130 | 0 | } |
131 | | |
132 | | } |