Skip to content

Commit

Permalink
Backspace/delete word into mentions (#630)
Browse files Browse the repository at this point in the history
* add new tests
* add helper functions
* split existing delete function out into do_delete function
  • Loading branch information
artcodespace authored Mar 21, 2023
1 parent 2143b98 commit fa389c2
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 2 deletions.
25 changes: 25 additions & 0 deletions crates/wysiwyg/src/composer_model/delete_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ where
self.do_replace_text_in(S::default(), start, end)
}

/// To handle mentions we need to be able to check if a text node has a non-editable ancestor
fn cursor_is_inside_non_editable_text_node(&mut self) -> bool {
let (s, e) = self.safe_selection();
let range = self.state.dom.find_range(s, e);

let first_leaf = range.locations.iter().find(|loc| {
loc.is_leaf() || (loc.kind.is_block_kind() && loc.is_empty())
});

if let Some(leaf) = first_leaf {
self.state.dom.has_immutable_ancestor(&leaf.node_handle)
} else {
false
}
}

/// If we have cursor at the edge of or inside a non-editable text node, expand the selection to cover
/// the whole of that node before continuing with the backspace/deletion flow
fn handle_non_editable_selection(&mut self) {
Expand Down Expand Up @@ -124,7 +140,10 @@ where
/// Deletes the character after the current cursor position.
pub fn delete(&mut self) -> ComposerUpdate<S> {
self.push_state_to_history();
self.do_delete()
}

pub fn do_delete(&mut self) -> ComposerUpdate<S> {
self.handle_non_editable_selection();

if self.state.start == self.state.end {
Expand Down Expand Up @@ -186,6 +205,12 @@ where
direction: Direction,
location: DomLocation,
) -> ComposerUpdate<S> {
// we could have entered a non-editable node during this run, if this is the
// case, we handle it by calling the delete method once which will adjust the
// selection to cover that node and then remove it, ending the recursive calls
if self.cursor_is_inside_non_editable_text_node() {
return self.do_delete();
}
match self.state.dom.lookup_node_mut(&location.node_handle) {
// we should never be passed a container
DomNode::Container(_) => ComposerUpdate::keep(),
Expand Down
29 changes: 29 additions & 0 deletions crates/wysiwyg/src/dom/dom_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,17 @@ where
.cloned()
}

/// Determine if a node handle has any container ancestors with the attribute contenteditable=false
pub fn has_immutable_ancestor(&self, child_handle: &DomHandle) -> bool {
child_handle.with_ancestors().iter().rev().any(|handle| {
if let DomNode::Container(n) = self.lookup_node(handle) {
n.is_immutable()
} else {
false
}
})
}

/// Find the node based on its handle.
/// Panics if the handle is unset or invalid
pub fn lookup_node(&self, node_handle: &DomHandle) -> &DomNode<S> {
Expand Down Expand Up @@ -674,6 +685,8 @@ impl Display for ItemNode {

#[cfg(test)]
mod test {
use std::ffi::FromVecWithNulError;

use widestring::Utf16String;

use crate::dom::nodes::dom_node::DomNode;
Expand Down Expand Up @@ -968,6 +981,22 @@ mod test {
assert_eq!(range_by_node, actual_range);
}

#[test]
fn text_node_with_immutable_ancestor() {
let d = cm("<a contenteditable=\"false\" href=\"https://matrix.org\">|first</a>").state.dom;
let handle = DomHandle::from_raw(vec![0, 0]);
let output = d.has_immutable_ancestor(&handle);
assert!(output);
}

#[test]
fn text_node_without_immutable_ancestor() {
let d = cm("<a href=\"https://matrix.org\">|first</a>").state.dom;
let handle = DomHandle::from_raw(vec![0, 0]);
let output = d.has_immutable_ancestor(&handle);
assert!(!output);
}

#[test]
fn transaction_succeeds() {
let mut d = cm("|").state.dom;
Expand Down
4 changes: 2 additions & 2 deletions crates/wysiwyg/src/tests/test_deleting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ fn backspace_mention_multiple() {
#[test]
fn backspace_word_from_edge_of_link() {
let mut model = cm(
"<a contenteditable=\"false\" href=\"https://matrix.org\">test|</a>",
"<a contenteditable=\"false\" href=\"https://matrix.org\">two words|</a>",
);
model.backspace_word();
assert_eq!(restore_whitespace(&tx(&model)), "|");
Expand Down Expand Up @@ -937,7 +937,7 @@ fn delete_mention_multiple() {
#[test]
fn delete_word_from_edge_of_link() {
let mut model = cm(
"<a contenteditable=\"false\" href=\"https://matrix.org\">|test</a>",
"<a contenteditable=\"false\" href=\"https://matrix.org\">|two words</a>",
);
model.delete_word();
assert_eq!(restore_whitespace(&tx(&model)), "|");
Expand Down

0 comments on commit fa389c2

Please # to comment.