From e86253ee451456f4ae0bf7ae2a1670156f6ae746 Mon Sep 17 00:00:00 2001 From: Ulises M Date: Mon, 22 Jan 2024 22:46:11 -0600 Subject: [PATCH] Add simple cursor-based input handling to `token-input` --- .../components/wallet/token_input/view.cljs | 36 +++--- .../wallet/send/input_amount/view.cljs | 103 +++++++++++++----- 2 files changed, 97 insertions(+), 42 deletions(-) diff --git a/src/quo/components/wallet/token_input/view.cljs b/src/quo/components/wallet/token_input/view.cljs index 7ee37a91682a..ab7f999d5039 100644 --- a/src/quo/components/wallet/token_input/view.cljs +++ b/src/quo/components/wallet/token_input/view.cljs @@ -1,6 +1,7 @@ (ns quo.components.wallet.token-input.view (:require [clojure.string :as string] + [oops.core :as oops] [quo.components.buttons.button.view :as button] [quo.components.dividers.divider-line.view :as divider-line] [quo.components.markdown.text :as text] @@ -72,18 +73,25 @@ [token-name-text theme text]]) (defn input-section - [{:keys [on-change-text value value-atom]}] - (let [input-ref (atom nil) - set-ref #(reset! input-ref %) - focus-input #(when-let [ref ^js @input-ref] - (.focus ref)) - controlled-input? (some? value) - handle-on-change-text (fn [v] - (when-not controlled-input? - (reset! value-atom v)) - (when on-change-text - (on-change-text v)))] - (fn [{:keys [theme token customization-color show-keyboard? crypto? currency value error?] + [{:keys [on-change-text value value-atom on-selection-change]}] + (let [input-ref (atom nil) + set-ref #(reset! input-ref %) + focus-input #(when-let [ref ^js @input-ref] + (.focus ref)) + controlled-input? (some? value) + handle-on-change-text (fn [v] + (when-not controlled-input? + (reset! value-atom v)) + (when on-change-text + (on-change-text v))) + handle-selection-change (fn [^js e] + (when on-selection-change + (-> e + (oops/oget "nativeEvent.selection") + (js->clj :keywordize-keys true) + (on-selection-change))))] + (fn [{:keys [theme token customization-color show-keyboard? crypto? currency value error? + selection] :or {show-keyboard? true}}] [rn/pressable {:on-press focus-input @@ -102,7 +110,9 @@ :max-length 12 :on-change-text handle-on-change-text :selection-color customization-color - :show-soft-input-on-focus show-keyboard?} + :show-soft-input-on-focus show-keyboard? + :on-selection-change handle-selection-change + :selection (clj->js selection)} controlled-input? (assoc :value value) (not controlled-input?) (assoc :default-value @value-atom))]] [token-label diff --git a/src/status_im/contexts/wallet/send/input_amount/view.cljs b/src/status_im/contexts/wallet/send/input_amount/view.cljs index 34a53af7d255..860463ad8ddd 100644 --- a/src/status_im/contexts/wallet/send/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -30,28 +30,54 @@ (defn valid-input? [current v] (let [max-length 12 - length-owerflow? (>= (count current) max-length) + length-overflow? (>= (count current) max-length) extra-dot? (and (= v dot) (string/includes? current dot)) extra-leading-zero? (and (= current "0") (= "0" (str v))) non-numeric? (re-find not-digits-or-dot-pattern (str v))] - (not (or non-numeric? extra-dot? extra-leading-zero? length-owerflow?)))) + (not (or non-numeric? extra-dot? extra-leading-zero? length-overflow?)))) + +(defn- add-char-to-string + [s c idx] + (let [size (count s)] + (if (= size idx) + (str s c) + (str (subs s 0 idx) + c + (subs s idx size))))) + +(defn- move-input-cursor + ([input-selection-atom new-idx] + (move-input-cursor input-selection-atom new-idx new-idx)) + ([input-selection-atom new-start-idx new-end-idx] + (let [start-idx (if (< new-start-idx 0) 0 new-start-idx) + end-idx (if (< new-end-idx 0) 0 new-start-idx)] + (swap! input-selection-atom assoc :start start-idx :end end-idx)))) (defn- normalize-input - [current v] - (cond - (and (string/blank? current) (= v dot)) - (str "0" v) + [current v input-selection-atom] + (let [{:keys [start end]} @input-selection-atom] + (if (= start end) + (cond + (and (string/blank? current) (= v dot)) + (do + (move-input-cursor input-selection-atom 2) + (str "0" v)) - (and (= current "0") (not= v dot)) - (str v) + (and (= current "0") (not= v dot)) + (do + (move-input-cursor input-selection-atom 1) + (str v)) - :else - (str current v))) + :else + (do + (move-input-cursor input-selection-atom (inc start)) + (add-char-to-string current v start))) + current))) (defn- make-new-input - [current v] + [current v input-selection-atom] (if (valid-input? current v) - (normalize-input current v) + (normalize-input current v input-selection-atom) current)) (defn- reset-input-error @@ -59,6 +85,11 @@ (reset! input-error (> new-value prev-value))) +(defn delete-from-string + [s idx] + (let [size (count s)] + (str (subs s 0 (dec idx)) (subs s idx size)))) + (defn- f-view-internal ;; crypto-decimals and limit-crypto args are needed for component tests only [{:keys [crypto-decimals limit-crypto]}] @@ -76,6 +107,7 @@ input-error (reagent/atom false) current-limit (reagent/atom {:amount limit-crypto :currency token-symbol}) + input-selection (reagent/atom {:start 0 :end 0}) handle-swap (fn [crypto?] (let [num-value (parse-double @input-value)] (reset! current-limit (if crypto? @@ -88,7 +120,9 @@ input-error))) handle-keyboard-press (fn [v] (let [current-value @input-value - new-value (make-new-input current-value v) + new-value (make-new-input current-value + v + input-selection) num-value (or (parse-double new-value) 0) current-limit-amount (:amount @current-limit)] (when (not loading-suggested-routes?) @@ -97,17 +131,27 @@ (reagent/flush)))) handle-delete (fn [_] (when-not loading-suggested-routes? - (let [current-limit-amount (:amount @current-limit)] - (swap! input-value #(subs % 0 (dec (count %)))) + (let [{:keys [start end]} @input-selection + current-limit-amount (:amount @current-limit)] (reset-input-error @input-value current-limit-amount input-error) - (reagent/flush)))) + (when (= start end) + (swap! input-value delete-from-string start) + (move-input-cursor input-selection (dec start)))) + (reagent/flush))) handle-on-change (fn [v] (when (valid-input? @input-value v) (let [num-value (or (parse-double v) 0) current-limit-amount (:amount @current-limit)] (reset! input-value v) (reset-input-error num-value current-limit-amount input-error) - (reagent/flush))))] + (reagent/flush)))) + selection-change (fn [selection] + ;; `reagent/flush` is needed to properly propagate the + ;; input cursor state. Since this is a controlled + ;; component the cursor will become static if + ;; `reagent/flush` is removed. + (reset! input-selection selection) + (reagent/flush))] (fn [{:keys [on-confirm] :or {on-confirm #(rf/dispatch [:wallet/send-select-amount {:amount @input-value @@ -146,19 +190,20 @@ :on-press #(rf/dispatch [:navigate-back-within-stack :wallet-send-input-amount]) :switcher-type :select-account}] [quo/token-input - {:container-style style/input-container - :token token-symbol - :currency currency - :crypto-decimals crypto-decimals + {:container-style style/input-container + :token token-symbol + :currency currency + :crypto-decimals crypto-decimals :error? @input-error - :networks (:networks token) - :title (i18n/label :t/send-limit {:limit limit-label}) - :conversion conversion-rate - :show-keyboard? false - :value @input-value - :on-swap handle-swap - :on-change-text (fn [text] - (handle-on-change text))}] + :networks (:networks token) + :title (i18n/label :t/send-limit {:limit limit-label}) + :conversion conversion-rate + :show-keyboard? false + :value @input-value + :selection @input-selection + :on-swap handle-swap + :on-change-text handle-on-change + :on-selection-change selection-change}] [routes/view {:amount amount :routes suggested-routes