mirror of
https://github.com/penpot/penpot.git
synced 2025-05-04 19:15:55 +02:00
230 lines
8 KiB
Clojure
230 lines
8 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.globals :as globals]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.object :as obj]
|
|
[app.util.simple-math :as sm]
|
|
[app.util.strings :as ust]
|
|
[goog.events :as events]
|
|
[rumext.alpha :as mf])
|
|
(:import goog.events.EventType))
|
|
|
|
(defn num? [val]
|
|
(and (number? val)
|
|
(not (math/nan? val))
|
|
(math/finite? val)))
|
|
|
|
(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")
|
|
step-val-str (obj/get props "step")
|
|
wrap-value? (obj/get props "data-wrap")
|
|
on-change (obj/get props "onChange")
|
|
on-blur (obj/get props "onBlur")
|
|
title (obj/get props "title")
|
|
default-val (obj/get props "default" 0)
|
|
precision (obj/get props "precision")
|
|
|
|
;; 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)
|
|
|
|
;; We need to store the handle-blur ref so we can call it on unmount
|
|
handle-blur-ref (mf/use-ref nil)
|
|
dirty-ref (mf/use-ref false)
|
|
|
|
;; This `value` represents the previous value and is used as
|
|
;; initil value for the simple math expression evaluation.
|
|
value (d/parse-integer value-str default-val)
|
|
|
|
min-val (cond
|
|
(number? min-val-str)
|
|
min-val-str
|
|
|
|
(string? min-val-str)
|
|
(d/parse-integer min-val-str))
|
|
|
|
max-val (cond
|
|
(number? max-val-str)
|
|
max-val-str
|
|
|
|
(string? max-val-str)
|
|
(d/parse-integer max-val-str))
|
|
|
|
step-val (cond
|
|
(number? step-val-str)
|
|
step-val-str
|
|
|
|
(string? step-val-str)
|
|
(d/parse-integer step-val-str)
|
|
|
|
:else 1)
|
|
|
|
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
|
|
(cond-> (number? precision)
|
|
(math/precision precision))
|
|
(cond-> (nil? precision)
|
|
(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 (if (some? precision)
|
|
(ust/format-precision new-value precision)
|
|
(str new-value))))))
|
|
|
|
apply-value
|
|
(mf/use-callback
|
|
(mf/deps on-change update-input value)
|
|
(fn [new-value]
|
|
(mf/set-ref-val! dirty-ref false)
|
|
(when (and (not= new-value value) (some? on-change))
|
|
(on-change 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? (* step-val 10) (* step-val -10))
|
|
(if up? step-val (- step-val)))
|
|
|
|
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) (- step-val))
|
|
|
|
(and wrap-value? (num? min-val) (num? max-val)
|
|
(< new-value min-val) down?)
|
|
(-> new-value (- min-val) (+ max-val) (+ step-val))
|
|
|
|
(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]
|
|
(mf/set-ref-val! dirty-ref true)
|
|
(let [up? (kbd/up-arrow? event)
|
|
down? (kbd/down-arrow? event)
|
|
enter? (kbd/enter? event)
|
|
esc? (kbd/esc? event)
|
|
input-node (mf/ref-val ref)]
|
|
(when (or up? down?)
|
|
(set-delta event up? down?))
|
|
(when enter?
|
|
(let [new-value (parse-value)]
|
|
(apply-value new-value)
|
|
(dom/blur! input-node)))
|
|
(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 on-blur)
|
|
(fn [_]
|
|
(let [new-value (or (parse-value) default-val)]
|
|
(if new-value
|
|
(apply-value new-value)
|
|
(update-input new-value)))
|
|
(when on-blur (on-blur))))
|
|
|
|
on-click
|
|
(mf/use-callback
|
|
(fn [event]
|
|
(let [target (dom/get-target event)]
|
|
(when (some? ref)
|
|
(let [current (mf/ref-val ref)]
|
|
(when (and (some? current) (not (.contains current target)))
|
|
(dom/blur! current)))))))
|
|
|
|
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! "title" title)
|
|
(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)]
|
|
(dom/set-value! input-node value-str))))
|
|
|
|
(mf/use-effect
|
|
(mf/deps handle-blur)
|
|
(fn []
|
|
(mf/set-ref-val! handle-blur-ref {:fn handle-blur})))
|
|
|
|
(mf/use-layout-effect
|
|
(fn []
|
|
#(when (mf/ref-val dirty-ref)
|
|
(let [handle-blur (:fn (mf/ref-val handle-blur-ref))]
|
|
(handle-blur)))))
|
|
|
|
(mf/use-layout-effect
|
|
(fn []
|
|
(let [keys [(events/listen globals/window EventType.POINTERDOWN on-click)
|
|
(events/listen globals/window EventType.MOUSEDOWN on-click)
|
|
(events/listen globals/window EventType.CLICK on-click)]]
|
|
#(doseq [key keys]
|
|
(events/unlistenByKey key)))))
|
|
|
|
[:> :input props]))
|
|
|