Andrew-Chen-Wang / RichEditorView

Rich Text Editor in Swift. Newly Featured Code and Swift 5 compatible of cjwirth/RichEditorView.
BSD 3-Clause "New" or "Revised" License
134 stars 59 forks source link

How to set caret at specific location #28

Open mixtly87 opened 1 year ago

mixtly87 commented 1 year ago

I want to support email-compose kind of view so that when you forward or reply on existing email, it shows previous content bellow, setting the cursor at the top of the RichEditorView.

However, I'm not sure how to achieve that. Is it possible to set a caret cursor at specified location in RichEditorView?

Andrew-Chen-Wang commented 1 year ago

Yes I believe it is possible. In Resources/editor/rich_editor.js, you can create a new RE.fn function that your Swift code in RichEditorView can call via runJs() (runJs can also be called elsewhere, so long as you have access to the class). The internals of the Javascript function can be modeled after this stackoverflow answer: https://stackoverflow.com/a/30938898

Assuming we already know the position of the text, you can 1) focus in on the window (this might be run via another Swift command that calls runJs("focus") that 2) next selects the range as minimally as possible that the user can type on.

In other words, if you can figure out how to do it via JavaScript like on any normal HTML website, you can do it here as well. Hope that helps

mixtly87 commented 1 year ago

Thanks @Andrew-Chen-Wang for pointing me to the right direction. I was able to focus the web view and set the cursor at the beginning of the document by adding the code in viewDidAppear of relevant view controller:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    textView.webView.becomeFirstResponder()
    textView.setCursorAtBegining()
}

and adding to RichEditorView.swift:

public func setCursorAtBegining() {
    runJS("RE.setCursorAtBegining()")
}

and also adding to rich_editor.js:

RE.setCursorAtBegining = function() {
    var el = document.getElementById("editor");
    var range = document.createRange();
    var sel = window.getSelection();
    range.setStart(el.childNodes[0], 0);
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
    el.focus();
}

however the problem I'm facing is that whole tableView is scrolled to bottom. So the RichEditorView is embedded into a UITableViewCell (it's Mail Compose view controller), and the cell has a proper height to fit entire RichTextView with its entire content.

But this problem with scrolling... do you have a suggestion on how to prevent the UITableView from scrolling to bottom when RichTextView is focused?

Andrew-Chen-Wang commented 1 year ago

I'm not sure which thing is scrolling to the bottom, but regardless, there is a JS event listener for focus that allows you to set clientX back to the top. If the table view is scrolling to the bottom, you can add a callback to Swift. I believe there is a callback that you can create using RE.callback that'll let you send back any data type. Then in Swift, you can adjust the scroll.

mixtly87 commented 1 year ago

Btw focusing for me worked on simulator but not on device (iPhone Mini 12, iOS 16.3.1). What fixed it was this thread.

In viewDidAppear:

textView.webView.setKeyboardRequiresUserInteraction(false)
textView.focus(at: .zero)

and WKWebView extension:

extension WKWebView {

    func setKeyboardRequiresUserInteraction( _ value: Bool) {

        guard let WKContentViewClass = NSClassFromString("WKContentView") else { return }

        typealias OlderClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void
        typealias NewerClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void

        let olderSelector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
        let iOS12_2Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
        let iOS13Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
        if let method = class_getInstanceMethod(WKContentViewClass, iOS13Selector) {

            let originalImp: IMP = method_getImplementation(method)
            let original: NewerClosureType = unsafeBitCast(originalImp, to: NewerClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, iOS13Selector, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        } else if let method = class_getInstanceMethod(WKContentViewClass, iOS12_2Selector) {

            let originalImp: IMP = method_getImplementation(method)
            let original: NewerClosureType = unsafeBitCast(originalImp, to: NewerClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, iOS12_2Selector, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        } else  if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) {
            let originalImp: IMP = method_getImplementation(method)
            let original: OlderClosureType = unsafeBitCast(originalImp, to: OlderClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
                original(me, olderSelector, arg0, !value, arg2, arg3)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }        
    }    
}