Browse Source

fix: enable auto-correct and auto-capitalization on iOS

related to https://github.com/logseq/db-test/issues/623
Tienson Qin 4 days ago
parent
commit
28c397e894

+ 60 - 0
ios/App/App/LiquidTabsPlugin.swift

@@ -1,6 +1,7 @@
 import Foundation
 import Capacitor
 import SwiftUI
+import WebKit
 
 @objc(LiquidTabsPlugin)
 public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
@@ -8,6 +9,8 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
     static weak var shared: LiquidTabsPlugin?
 
     private let store = LiquidTabsStore.shared
+    private var keyboardHackScriptInstalled = false
+    private let keyboardHackHandlerName = "keyboardHackKey"
 
     public let identifier = "LiquidTabsPlugin"
     public let jsName = "LiquidTabsPlugin"
@@ -19,6 +22,7 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
     public override func load() {
         super.load()
         LiquidTabsPlugin.shared = self
+        installKeyboardHackScript()
     }
 
     // MARK: - Methods from JS
@@ -79,4 +83,60 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
     func notifySearchChanged(query: String) {
         notifyListeners("searchChanged", data: ["query": query])
     }
+
+    func notifyKeyboardHackKey(key: String) {
+        notifyListeners("keyboardHackKey", data: ["key": key])
+    }
+
+    private func installKeyboardHackScript() {
+        guard !keyboardHackScriptInstalled,
+              let controller = bridge?.webView?.configuration.userContentController else {
+            return
+        }
+
+        keyboardHackScriptInstalled = true
+        controller.removeScriptMessageHandler(forName: keyboardHackHandlerName)
+        controller.add(self, name: keyboardHackHandlerName)
+
+        let source = """
+        (function() {
+          if (window.__logseqKeyboardHackInstalled) return;
+          window.__logseqKeyboardHackInstalled = true;
+          window.addEventListener('keydown', function(e) {
+            var k = null;
+            switch (e.key) {
+              case 'Backspace':
+                k = 'backspace';
+                break;
+              case 'Enter':
+              case 'Return':
+                k = 'enter';
+                break;
+              default:
+                if (e.keyCode === 8) k = 'backspace';
+                else if (e.keyCode === 13) k = 'enter';
+                break;
+            }
+            if (!k) return;
+            try {
+              window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.\(keyboardHackHandlerName).postMessage({ key: k });
+            } catch (_) {}
+          }, true);
+        })();
+        """
+
+        let script = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: false)
+        controller.addUserScript(script)
+    }
+}
+
+extension LiquidTabsPlugin: WKScriptMessageHandler {
+    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        guard message.name == keyboardHackHandlerName else { return }
+
+        if let body = message.body as? [String: Any],
+           let key = body["key"] as? String {
+            notifyKeyboardHackKey(key: key)
+        }
+    }
 }

+ 23 - 1
ios/App/App/LiquidTabsRootView.swift

@@ -10,8 +10,27 @@ import UIKit
 struct KeyboardHackField: UIViewRepresentable {
     @Binding var shouldShow: Bool
 
+    // Capture Backspace/Enter on the hidden field and forward to JS.
+    class KeyboardHackTextField: UITextField {
+        var onKeyPress: ((String) -> Void)?
+
+        override func deleteBackward() {
+            super.deleteBackward()
+            onKeyPress?("backspace")
+            text = ""
+        }
+
+        override func insertText(_ text: String) {
+            super.insertText(text)
+            if text == "\n" {
+                onKeyPress?("enter")
+            }
+            self.text = ""
+        }
+    }
+
     class Coordinator {
-        let textField = UITextField()
+        let textField = KeyboardHackTextField()
     }
 
     func makeCoordinator() -> Coordinator {
@@ -23,6 +42,9 @@ struct KeyboardHackField: UIViewRepresentable {
         let tf = context.coordinator.textField
         tf.isHidden = true
         tf.keyboardType = .default
+        tf.onKeyPress = { key in
+            LiquidTabsPlugin.shared?.notifyKeyboardHackKey(key: key)
+        }
         container.addSubview(tf)
         return container
     }

+ 11 - 7
src/main/frontend/handler/editor.cljs

@@ -2564,12 +2564,14 @@
   (some? (dom/closest el ".block-editor")))
 
 (defn keydown-new-block-handler [^js e]
-  (let [state (get-state)]
-    (when (or (nil? (.-target e)) (inside-of-editor-block (.-target e)))
+  (let [state (get-state)
+        target (when e (.-target e))]
+    (when (or (nil? target)
+              (inside-of-editor-block target))
       (if (or (state/doc-mode-enter-for-new-line?) (inside-of-single-block (rum/dom-node state)))
         (keydown-new-line)
         (do
-          (.preventDefault e)
+          (when e (.preventDefault e))
           (keydown-new-block state))))))
 
 (defn keydown-new-line-handler [e]
@@ -2848,12 +2850,12 @@
         (delete-and-update
          input current-pos (util/safe-inc-current-pos-from-start (.-value input) current-pos))))))
 
-(defn- delete-block-when-zero-pos
+(defn delete-block-when-zero-pos!
   [^js e]
   (let [^js input (state/get-input)
         current-pos (cursor/pos input)]
     (when (zero? current-pos)
-      (.preventDefault e)
+      (when e (.preventDefault e))
       (let [repo (state/get-current-repo)
             block* (state/get-edit-block)
             block (db/entity (:db/id block*))
@@ -2862,7 +2864,7 @@
             custom-query? (get-in editor-state [:config :custom-query?])
             top-block? (= (:db/id (or (ldb/get-left-sibling block) (:block/parent block)))
                           (:db/id (:block/page block)))
-            single-block? (inside-of-single-block (.-target e))
+            single-block? (if e (inside-of-single-block (.-target e)) false)
             root-block? (= (:block.temp/container block) (str (:block/uuid block)))]
         (when (and (not (and top-block? (not (string/blank? value))))
                    (not root-block?)
@@ -2897,7 +2899,9 @@
             (delete-and-update input selected-start selected-end))
 
           (zero? current-pos)
-          (delete-block-when-zero-pos e)
+          (when-not (mobile-util/native-ios?)
+            ;; native iOS handled by `mobile.bottom-tabs/add-keyboard-hack-listener!`
+            (delete-block-when-zero-pos! e))
 
           (and (> current-pos 0)
                (contains? #{commands/command-trigger commands/command-ask}

+ 24 - 2
src/main/mobile/bottom_tabs.cljs

@@ -1,8 +1,10 @@
 (ns mobile.bottom-tabs
   "iOS bottom tabs"
   (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.state :as state]
             [frontend.util :as util]
             [logseq.common.util :as common-util]
             [mobile.state :as mobile-state]))
@@ -55,9 +57,28 @@
    liquid-tabs
    "searchChanged"
    (fn [data]
-      ;; data is like { query: string }
+       ;; data is like { query: string }
      (f (.-query data)))))
 
+(defn add-keyboard-hack-listener!
+  "Listen for Backspace or Enter while the invisible keyboard field is focused."
+  []
+  (.addListener
+   liquid-tabs
+   "keyboardHackKey"
+   (fn [data]
+     ;; data is like { key: string }
+     (when-let [k (.-key data)]
+       (case k
+         "backspace"
+         (editor-handler/delete-block-when-zero-pos! nil)
+         "enter"
+         (when-let [input (state/get-input)]
+           (let [value (.-value input)]
+             (when (string/blank? value)
+               (editor-handler/keydown-new-block-handler nil))))
+         nil)))))
+
 (defonce add-tab-listeners!
   (do
     (add-tab-selected-listener!
@@ -85,7 +106,8 @@
       ;; wire up search handler
        (js/console.log "Native search query" q)
        (reset! mobile-state/*search-input q)
-       (reset! mobile-state/*search-last-input-at (common-util/time-ms))))))
+       (reset! mobile-state/*search-last-input-at (common-util/time-ms))))
+    (add-keyboard-hack-listener!)))
 
 (defn configure
   []

+ 2 - 2
src/main/mobile/components/ui.cljs

@@ -23,8 +23,8 @@
   ([t]
    [:input.absolute.top-4.left-0.w-1.h-1.opacity-0
     {:id (str "keep-keyboard-open-input" t)
-     :auto-capitalize "off"
-     :auto-correct "false"}]))
+     :auto-capitalize "sentences"
+     :auto-correct "true"}]))
 
 (rum/defc notification-clear-all
   []