2020-10-12 23:17:57 +00:00
|
|
|
//
|
|
|
|
// ComposeContentWarningTextField.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 10/12/20.
|
|
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
2021-05-04 03:12:59 +00:00
|
|
|
struct ComposeEmojiTextField: UIViewRepresentable {
|
2020-10-12 23:17:57 +00:00
|
|
|
typealias UIViewType = UITextField
|
|
|
|
|
|
|
|
@EnvironmentObject private var uiState: ComposeUIState
|
|
|
|
|
2021-05-04 03:12:59 +00:00
|
|
|
@Binding private var text: String
|
|
|
|
private let placeholder: String
|
|
|
|
private var didChange: ((String) -> Void)?
|
|
|
|
private var didEndEditing: (() -> Void)?
|
|
|
|
private var backgroundColor: UIColor? = nil
|
|
|
|
|
|
|
|
init(text: Binding<String>, placeholder: String) {
|
|
|
|
self._text = text
|
|
|
|
self.placeholder = placeholder
|
|
|
|
self.didChange = nil
|
|
|
|
self.didEndEditing = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mutating func didChange(_ didChange: @escaping (String) -> Void) -> Self {
|
|
|
|
self.didChange = didChange
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
mutating func didEndEditing(_ didEndEditing: @escaping () -> Void) -> Self {
|
|
|
|
self.didEndEditing = didEndEditing
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
mutating func backgroundColor(_ color: UIColor) -> Self {
|
|
|
|
self.backgroundColor = color
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
func makeUIView(context: Context) -> UITextField {
|
|
|
|
let view = UITextField()
|
|
|
|
|
2021-05-04 03:12:59 +00:00
|
|
|
view.placeholder = placeholder
|
2020-10-12 23:17:57 +00:00
|
|
|
view.borderStyle = .roundedRect
|
|
|
|
|
|
|
|
view.delegate = context.coordinator
|
|
|
|
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
|
|
|
|
2021-05-04 03:12:59 +00:00
|
|
|
view.backgroundColor = backgroundColor
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
context.coordinator.textField = view
|
|
|
|
context.coordinator.uiState = uiState
|
|
|
|
context.coordinator.text = $text
|
|
|
|
|
|
|
|
return view
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateUIView(_ uiView: UITextField, context: Context) {
|
2021-05-01 23:18:00 +00:00
|
|
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
|
|
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
|
|
|
} else {
|
|
|
|
uiView.text = text
|
|
|
|
}
|
2021-05-04 03:12:59 +00:00
|
|
|
context.coordinator.didChange = didChange
|
|
|
|
context.coordinator.didEndEditing = didEndEditing
|
2020-10-12 23:17:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
|
return Coordinator()
|
|
|
|
}
|
|
|
|
|
|
|
|
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
|
|
|
|
weak var textField: UITextField?
|
|
|
|
var text: Binding<String>!
|
|
|
|
var uiState: ComposeUIState!
|
2021-05-04 03:12:59 +00:00
|
|
|
var didChange: ((String) -> Void)?
|
|
|
|
var didEndEditing: (() -> Void)?
|
2020-10-12 23:17:57 +00:00
|
|
|
|
2021-05-01 23:18:00 +00:00
|
|
|
var skipSettingTextOnNextUpdate = false
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
@objc func didChange(_ textField: UITextField) {
|
|
|
|
text.wrappedValue = textField.text ?? ""
|
2021-05-04 03:12:59 +00:00
|
|
|
didChange?(text.wrappedValue)
|
2020-10-12 23:17:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
|
|
uiState.autocompleteHandler = self
|
|
|
|
updateAutocompleteState(textField: textField)
|
|
|
|
}
|
|
|
|
|
|
|
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
|
|
updateAutocompleteState(textField: textField)
|
2021-05-04 03:12:59 +00:00
|
|
|
didEndEditing?()
|
2020-10-12 23:17:57 +00:00
|
|
|
}
|
|
|
|
|
2021-05-01 23:18:00 +00:00
|
|
|
func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
|
|
// see MainComposeTextView.Coordinator.textViewDidChangeSelection(_:)
|
|
|
|
skipSettingTextOnNextUpdate = true
|
|
|
|
self.updateAutocompleteState(textField: textField)
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
func autocomplete(with string: String) {
|
|
|
|
guard let textField = textField,
|
|
|
|
let text = textField.text,
|
|
|
|
let selectedRange = textField.selectedTextRange,
|
|
|
|
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:39:50 +00:00
|
|
|
let distanceToEnd = textField.offset(from: selectedRange.start, to: textField.endOfDocument)
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
|
|
|
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
|
|
|
|
2020-10-13 02:03:50 +00:00
|
|
|
let insertSpace: Bool
|
2020-10-18 15:11:47 +00:00
|
|
|
if distanceToEnd > 0 {
|
|
|
|
let charAfterCursor = text[characterBeforeCursorIndex]
|
2020-10-13 02:03:50 +00:00
|
|
|
insertSpace = charAfterCursor != " " && charAfterCursor != "\n"
|
|
|
|
} else {
|
|
|
|
insertSpace = true
|
|
|
|
}
|
|
|
|
let string = insertSpace ? string + " " : string
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
textField.text!.replaceSubrange(lastWordStartIndex..<characterBeforeCursorIndex, with: string)
|
2020-10-18 15:11:47 +00:00
|
|
|
self.didChange(textField)
|
|
|
|
self.updateAutocompleteState(textField: textField)
|
2020-10-12 23:39:50 +00:00
|
|
|
|
|
|
|
// keep the cursor at the same position in the text, immediately after what was inserted
|
2020-10-18 15:11:47 +00:00
|
|
|
// if we inserted a space, move the cursor 1 farther so it's immediately after the pre-existing space
|
|
|
|
let insertSpaceOffset = insertSpace ? 0 : 1
|
|
|
|
let newCursorPosition = textField.position(from: textField.endOfDocument, offset: -distanceToEnd + insertSpaceOffset)!
|
2020-10-12 23:39:50 +00:00
|
|
|
textField.selectedTextRange = textField.textRange(from: newCursorPosition, to: newCursorPosition)
|
2020-10-12 23:17:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func updateAutocompleteState(textField: UITextField) {
|
|
|
|
guard let selectedRange = textField.selectedTextRange,
|
|
|
|
let text = textField.text,
|
|
|
|
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
|
|
|
|
uiState.autocompleteState = nil
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if lastWordStartIndex > text.startIndex {
|
|
|
|
let c = text[text.index(before: lastWordStartIndex)]
|
|
|
|
if isPermittedForAutocomplete(c) || c == ":" {
|
|
|
|
uiState.autocompleteState = nil
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
|
|
|
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
|
|
|
|
|
|
|
if lastWordStartIndex >= text.startIndex {
|
|
|
|
let lastWord = text[lastWordStartIndex..<cursorIndex]
|
|
|
|
let exceptFirst = lastWord[lastWord.index(after: lastWord.startIndex)...]
|
|
|
|
|
|
|
|
if lastWord.first == ":" {
|
|
|
|
uiState.autocompleteState = .emoji(String(exceptFirst))
|
|
|
|
} else {
|
|
|
|
uiState.autocompleteState = nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
uiState.autocompleteState = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func isPermittedForAutocomplete(_ c: Character) -> Bool {
|
|
|
|
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"
|
|
|
|
}
|
|
|
|
|
|
|
|
private func findAutocompleteLastWord(textField: UITextField) -> String.Index? {
|
|
|
|
guard textField.isFirstResponder,
|
|
|
|
let selectedRange = textField.selectedTextRange,
|
|
|
|
selectedRange.isEmpty,
|
|
|
|
let text = textField.text,
|
|
|
|
!text.isEmpty else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
|
|
|
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
|
|
|
|
2020-10-24 15:26:29 +00:00
|
|
|
guard cursorIndex != text.startIndex else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-12 23:17:57 +00:00
|
|
|
var lastWordStartIndex = text.index(before: cursorIndex)
|
|
|
|
while true {
|
|
|
|
let c = text[lastWordStartIndex]
|
|
|
|
|
|
|
|
if !isPermittedForAutocomplete(c) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if lastWordStartIndex > text.startIndex {
|
|
|
|
lastWordStartIndex = text.index(before: lastWordStartIndex)
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return lastWordStartIndex
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|