Fix text editing issues on characters added right after mention link

Changelog-Fixed: Fix text editing issues on characters added right after mention link
Closes: https://github.com/damus-io/damus/issues/1375
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Tested-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino
2023-08-19 19:04:18 +00:00
committed by William Casarin
parent c464a26151
commit dcb94635ea
4 changed files with 214 additions and 3 deletions

View File

@@ -204,6 +204,8 @@ struct PostView: View {
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
focusWordAttributes = (word, range)
self.newCursorIndex = nil
}, updateCursorPosition: { newCursorIndex in
self.newCursorIndex = newCursorIndex
})
.environmentObject(tagModel)
.focused($focus)

View File

@@ -14,6 +14,7 @@ struct TextViewWrapper: UIViewRepresentable {
let cursorIndex: Int?
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
let updateCursorPosition: ((Int) -> Void)
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
@@ -34,11 +35,11 @@ struct TextViewWrapper: UIViewRepresentable {
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.isScrollEnabled = postTextViewCanScroll
let range = uiView.selectedRange
uiView.attributedText = attributedText
TextViewWrapper.setTextProperties(uiView)
setCursorPosition(textView: uiView)
let range = uiView.selectedRange
uiView.selectedRange = NSRange(location: range.location + tagModel.diff, length: range.length)
tagModel.diff = 0
@@ -52,16 +53,18 @@ struct TextViewWrapper: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention)
Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition)
}
class Coordinator: NSObject, UITextViewDelegate {
@Binding var attributedText: NSMutableAttributedString
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
let updateCursorPosition: ((Int) -> Void)
init(attributedText: Binding<NSMutableAttributedString>, getFocusWordForMention: ((String?, NSRange?) -> Void)?) {
init(attributedText: Binding<NSMutableAttributedString>, getFocusWordForMention: ((String?, NSRange?) -> Void)?, updateCursorPosition: @escaping ((Int) -> Void)) {
_attributedText = attributedText
self.getFocusWordForMention = getFocusWordForMention
self.updateCursorPosition = updateCursorPosition
}
func textViewDidChange(_ textView: UITextView) {
@@ -96,6 +99,65 @@ struct TextViewWrapper: UIViewRepresentable {
}
return NSRange(location: startOffset, length: length)
}
// This `UITextViewDelegate` method is automatically called by the editor when edits occur, to check whether a change should occur
// We will use this method to manually handle edits concerning mention ("@") links, to avoid manual text edits to attributed mention links
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let attributedString = textView.attributedText else {
return true // If we cannot get an attributed string, just fail gracefully and allow changes
}
var mutable = NSMutableAttributedString(attributedString: attributedString)
let entireRange = NSRange(location: 0, length: attributedString.length)
var shouldAllowChange = true
var performEditActionManually = false
attributedString.enumerateAttribute(.link, in: entireRange, options: []) { (value, linkRange, stop) in
guard value != nil else {
return // This range is not a link. Skip checking.
}
if range.contains(linkRange.upperBound) && range.contains(linkRange.lowerBound) {
// Edit range engulfs all of this link's range.
// This link will naturally disappear, so no work needs to be done in this range.
return
}
else if linkRange.intersection(range) != nil {
// If user tries to change an existing link directly, remove the link attribute
mutable.removeAttribute(.link, range: linkRange)
// Perform action manually to flush above changes to the view, and to prevent the character being added from having an attributed link property
performEditActionManually = true
return
}
else if range.location == linkRange.location + linkRange.length && range.length == 0 {
// If we are inserting a character at the right edge of a link, UITextInput tends to include the new character inside the link.
// Therefore, we need to manually append that character outside of the link
performEditActionManually = true
return
}
}
if performEditActionManually {
shouldAllowChange = false
addUnattributedText(text, to: &mutable, inRange: range)
attributedText = mutable
// Move caret to the end of the newly changed text.
updateCursorPosition(range.location + text.count)
}
return shouldAllowChange
}
func addUnattributedText(_ text: String, to attributedString: inout NSMutableAttributedString, inRange range: NSRange) {
if range.length == 0 {
attributedString.insert(NSAttributedString(string: text, attributes: nil), at: range.location)
}
else {
attributedString.replaceCharacters(in: range, with: text)
}
}
}
}