penpot/frontend/src/app/main/ui/components/numeric_input.cljs

159 lines
5.5 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.components.numeric-input
(:require
[app.common.data :as d]
[app.common.math :as math]
[app.common.spec :as us]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.simple-math :as sm]
[rumext.alpha :as mf]))
(mf/defc numeric-input
{::mf/wrap-props false
::mf/forward-ref true}
[props external-ref]
(let [value-str (obj/get props "value")
min-val-str (obj/get props "min")
max-val-str (obj/get props "max")
wrap-value? (obj/get props "data-wrap")
on-change (obj/get props "onChange")
;; We need a ref pointing to the input dom element, but the user
;; of this component may provide one (that is forwarded here).
;; So we use the external ref if provided, and the local one if not.
local-ref (mf/use-ref)
ref (or external-ref local-ref)
value (d/parse-integer value-str)
min-val (when (string? min-val-str)
(d/parse-integer min-val-str))
max-val (when (string? max-val-str)
(d/parse-integer max-val-str))
num? (fn [val] (and (number? val)
(not (math/nan? val))
(math/finite? val)))
parse-value
(mf/use-callback
(mf/deps ref min-val max-val value)
(fn []
(let [input-node (mf/ref-val ref)
new-value (-> (dom/get-value input-node)
(sm/expr-eval value))]
(when (num? new-value)
(-> new-value
(math/round)
(cljs.core/max us/min-safe-int)
(cljs.core/min us/max-safe-int)
(cond->
(num? min-val)
(cljs.core/max min-val)
(num? max-val)
(cljs.core/min max-val)))))))
update-input
(mf/use-callback
(mf/deps ref)
(fn [new-value]
(let [input-node (mf/ref-val ref)]
(dom/set-value! input-node (str new-value)))))
apply-value
(mf/use-callback
(mf/deps on-change update-input value)
(fn [new-value]
(when (and (some? new-value) (not= new-value value) (some? on-change))
(on-change new-value))
(when (some? new-value)
(update-input new-value))))
set-delta
(mf/use-callback
(mf/deps wrap-value? min-val max-val parse-value apply-value)
(fn [event up? down?]
(let [current-value (parse-value)]
(when current-value
(let [increment (if (kbd/shift? event)
(if up? 10 -10)
(if up? 1 -1))
new-value (+ current-value increment)
new-value (cond
(and wrap-value? (num? max-val) (num? min-val)
(> new-value max-val) up?)
(-> new-value (- max-val) (+ min-val) (- 1))
(and wrap-value? (num? min-val) (num? max-val)
(< new-value min-val) down?)
(-> new-value (- min-val) (+ max-val) (+ 1))
(and (num? min-val) (< new-value min-val))
min-val
(and (num? max-val) (> new-value max-val))
max-val
:else new-value)]
(apply-value new-value))))))
handle-key-down
(mf/use-callback
(mf/deps set-delta apply-value update-input)
(fn [event]
(let [up? (kbd/up-arrow? event)
down? (kbd/down-arrow? event)
enter? (kbd/enter? event)
esc? (kbd/esc? event)]
(when (or up? down?)
(set-delta event up? down?))
(when enter?
(let [new-value (parse-value)]
(apply-value new-value)))
(when esc?
(update-input value-str)))))
handle-mouse-wheel
(mf/use-callback
(mf/deps set-delta)
(fn [event]
(set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0))))
handle-blur
(mf/use-callback
(mf/deps parse-value apply-value update-input)
(fn [_]
(let [new-value (parse-value)]
(if new-value
(apply-value new-value)
(update-input value-str)))))
props (-> props
(obj/without ["value" "onChange"])
(obj/set! "className" "input-text")
(obj/set! "type" "text")
(obj/set! "ref" ref)
(obj/set! "defaultValue" value-str)
(obj/set! "onWheel" handle-mouse-wheel)
(obj/set! "onKeyDown" handle-key-down)
(obj/set! "onBlur" handle-blur))]
(mf/use-effect
(mf/deps value-str)
(fn []
(when-let [input-node (mf/ref-val ref)]
(when-not (dom/active? input-node)
(dom/set-value! input-node value-str)))))
[:> :input props]))