mirror of
https://github.com/penpot/penpot.git
synced 2025-06-06 06:31:38 +02:00
Merge branch 'wip/multicanvas' of github.com:uxbox/uxbox into i18n/multicanvas
This commit is contained in:
commit
d8afb97c7a
84 changed files with 3327 additions and 3774 deletions
|
@ -17,11 +17,11 @@
|
|||
"Return a indexed map of the collection
|
||||
keyed by the result of executing the getter
|
||||
over each element of the collection."
|
||||
[coll getter]
|
||||
[getter coll]
|
||||
(persistent!
|
||||
(reduce #(assoc! %1 (getter %2) %2) (transient {}) coll)))
|
||||
|
||||
(def index-by-id #(index-by % :id))
|
||||
(def index-by-id #(index-by :id %))
|
||||
|
||||
(defn remove-nil-vals
|
||||
"Given a map, return a map removing key-value
|
||||
|
|
|
@ -5,208 +5,118 @@
|
|||
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.util.forms
|
||||
(:refer-clojure :exclude [uuid])
|
||||
(:require
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s :include-macros true]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.i18n :refer [tr]]))
|
||||
|
||||
;; --- Form Validation Api
|
||||
;; --- Handlers Helpers
|
||||
|
||||
(defn- impl-mutator
|
||||
[v update-fn]
|
||||
(specify v
|
||||
IReset
|
||||
(-reset! [_ new-value]
|
||||
(update-fn new-value))
|
||||
|
||||
ISwap
|
||||
(-swap!
|
||||
([self f] (update-fn f))
|
||||
([self f x] (update-fn #(f % x)))
|
||||
([self f x y] (update-fn #(f % x y)))
|
||||
([self f x y more] (update-fn #(apply f % x y more))))))
|
||||
|
||||
(defn- translate-error-type
|
||||
[name]
|
||||
"errors.undefined-error")
|
||||
|
||||
(defn- interpret-problem
|
||||
[acc {:keys [path pred val via in] :as problem}]
|
||||
;; (prn "interpret-problem" problem)
|
||||
(cond
|
||||
(and (empty? path)
|
||||
(= (first pred) 'contains?))
|
||||
(let [path (conj path (last pred))]
|
||||
(update-in acc path assoc :missing))
|
||||
(list? pred)
|
||||
(= (first (last pred)) 'cljs.core/contains?))
|
||||
(let [path (conj path (last (last pred)))]
|
||||
(assoc-in acc path {:name ::missing :type :builtin}))
|
||||
|
||||
(and (seq path)
|
||||
(= 1 (count path)))
|
||||
(update-in acc path assoc :invalid)
|
||||
(and (not (empty? path))
|
||||
(not (empty? via)))
|
||||
(assoc-in acc path {:name (last via) :type :builtin})
|
||||
|
||||
:else acc))
|
||||
|
||||
(defn validate
|
||||
[spec data]
|
||||
(when-not (s/valid? spec data)
|
||||
(let [report (s/explain-data spec data)]
|
||||
(reduce interpret-problem {} (::s/problems report)))))
|
||||
(defn use-form
|
||||
[spec initial]
|
||||
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
|
||||
:errors {}
|
||||
:touched {}})
|
||||
clean-data (s/conform spec (:data state))
|
||||
problems (when (= ::s/invalid clean-data)
|
||||
(::s/problems (s/explain-data spec (:data state))))
|
||||
|
||||
(defn valid?
|
||||
[spec data]
|
||||
(s/valid? spec data))
|
||||
|
||||
errors (merge (reduce interpret-problem {} problems)
|
||||
(:errors state))]
|
||||
(-> (assoc state
|
||||
:errors errors
|
||||
:clean-data (when (not= clean-data ::s/invalid) clean-data)
|
||||
:valid (and (empty? errors)
|
||||
(not= clean-data ::s/invalid)))
|
||||
(impl-mutator update-state))))
|
||||
|
||||
(defn on-input-change
|
||||
[{:keys [data] :as form} field]
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data field] value)
|
||||
(update :errors dissoc field)))))))
|
||||
|
||||
(defn on-input-blur
|
||||
[{:keys [touched] :as form} field]
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)]
|
||||
(when-not (get touched field)
|
||||
(swap! form assoc-in [:touched field] true)))))
|
||||
|
||||
;; --- Helper Components
|
||||
|
||||
(mf/defc field-error
|
||||
[{:keys [form field type]
|
||||
:or {only (constantly true)}
|
||||
:as props}]
|
||||
(let [touched? (get-in form [:touched field])
|
||||
{:keys [message code] :as error} (get-in form [:errors field])]
|
||||
(when (and touched? error
|
||||
(cond
|
||||
(nil? type) true
|
||||
(keyword? type) (= (:type error) type)
|
||||
(ifn? type) (type (:type error))
|
||||
:else false))
|
||||
(prn "field-error" error)
|
||||
[:ul.form-errors
|
||||
[:li {:key code} (tr message)]])))
|
||||
|
||||
(defn error-class
|
||||
[form field]
|
||||
(when (and (get-in form [:errors field])
|
||||
(get-in form [:touched field]))
|
||||
"invalid"))
|
||||
|
||||
;; --- Form Specs and Conformers
|
||||
|
||||
(def ^:private email-re
|
||||
#"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
||||
|
||||
(def ^:private number-re
|
||||
#"^[-+]?[0-9]*\.?[0-9]+$")
|
||||
|
||||
(def ^:private color-re
|
||||
#"^#[0-9A-Fa-f]{6}$")
|
||||
|
||||
(s/def ::email
|
||||
(s/and string? #(boolean (re-matches email-re %))))
|
||||
|
||||
(s/def ::non-empty-string
|
||||
(s/and string? #(not (str/empty? %))))
|
||||
|
||||
(defn- parse-number
|
||||
[v]
|
||||
(cond
|
||||
(re-matches number-re v) (js/parseFloat v)
|
||||
(number? v) v
|
||||
:else ::s/invalid))
|
||||
|
||||
(s/def ::string-number
|
||||
(s/conformer parse-number str))
|
||||
|
||||
(s/def ::color
|
||||
(s/and string? #(boolean (re-matches color-re %))))
|
||||
|
||||
;; --- Form State Events
|
||||
|
||||
;; --- Assoc Error
|
||||
|
||||
(defrecord AssocError [type field error]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:errors type field] error)))
|
||||
|
||||
(defn assoc-error
|
||||
([type field]
|
||||
(assoc-error type field nil))
|
||||
([type field error]
|
||||
{:pre [(keyword? type)
|
||||
(keyword? field)
|
||||
(any? error)]}
|
||||
(AssocError. type field error)))
|
||||
|
||||
;; --- Assoc Errors
|
||||
|
||||
(defrecord AssocErrors [type errors]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:errors type] errors)))
|
||||
|
||||
(defn assoc-errors
|
||||
([type]
|
||||
(assoc-errors type nil))
|
||||
([type errors]
|
||||
{:pre [(keyword? type)
|
||||
(or (map? errors)
|
||||
(nil? errors))]}
|
||||
(AssocErrors. type errors)))
|
||||
|
||||
;; --- Assoc Value
|
||||
|
||||
(declare clear-error)
|
||||
|
||||
(defrecord AssocValue [type field value]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [form-path (into [:forms type] (if (coll? field) field [field]))]
|
||||
(assoc-in state form-path value)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (clear-error type field))))
|
||||
|
||||
(defn assoc-value
|
||||
[type field value]
|
||||
{:pre [(keyword? type)
|
||||
(keyword? field)
|
||||
(any? value)]}
|
||||
(AssocValue. type field value))
|
||||
|
||||
;; --- Clear Values
|
||||
|
||||
(defrecord ClearValues [type]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:forms type] nil)))
|
||||
|
||||
(defn clear-values
|
||||
[type]
|
||||
{:pre [(keyword? type)]}
|
||||
(ClearValues. type))
|
||||
|
||||
;; --- Clear Error
|
||||
|
||||
(deftype ClearError [type field]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [errors (get-in state [:errors type])]
|
||||
(if (map? errors)
|
||||
(assoc-in state [:errors type] (dissoc errors field))
|
||||
(update state :errors dissoc type)))))
|
||||
|
||||
(defn clear-error
|
||||
[type field]
|
||||
{:pre [(keyword? type)
|
||||
(keyword? field)]}
|
||||
(ClearError. type field))
|
||||
|
||||
;; --- Clear Errors
|
||||
|
||||
(defrecord ClearErrors [type]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:errors type] nil)))
|
||||
|
||||
(defn clear-errors
|
||||
[type]
|
||||
{:pre [(keyword? type)]}
|
||||
(ClearErrors. type))
|
||||
|
||||
;; --- Clear Form
|
||||
|
||||
(deftype ClearForm [type]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (clear-values type)
|
||||
(clear-errors type))))
|
||||
|
||||
(defn clear-form
|
||||
[type]
|
||||
{:pre [(keyword? type)]}
|
||||
(ClearForm. type))
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn focus-data
|
||||
[type state]
|
||||
(-> (l/in [:forms type])
|
||||
(l/derive state)))
|
||||
|
||||
(defn focus-errors
|
||||
[type state]
|
||||
(-> (l/in [:errors type])
|
||||
(l/derive state)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Form UI
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mx/defc input-error
|
||||
[errors field]
|
||||
(when-let [error (get errors field)]
|
||||
[:ul.form-errors
|
||||
[:li {:key error} (tr error)]]))
|
||||
|
||||
(defn error-class
|
||||
[errors field]
|
||||
(when (get errors field)
|
||||
"invalid"))
|
||||
|
||||
(defn clear-mixin
|
||||
[store type]
|
||||
{:will-unmount (fn [own]
|
||||
(ptk/emit! store (clear-form type))
|
||||
own)})
|
||||
;; TODO: migrate to uxbox.util.spec
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::not-empty-string ::us/not-empty-string)
|
||||
(s/def ::color ::us/color)
|
||||
(s/def ::number-str ::us/number-str)
|
||||
|
|
|
@ -6,16 +6,17 @@
|
|||
|
||||
(ns uxbox.util.messages
|
||||
"Messages notifications."
|
||||
(:require [lentes.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.util.timers :as ts]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
[uxbox.util.data :refer [classnames]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.i18n :refer [tr]]))
|
||||
(:require
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.util.data :refer [classnames]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.timers :as ts]
|
||||
[uxbox.util.i18n :refer [tr]]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Events
|
||||
|
@ -25,33 +26,12 @@
|
|||
|
||||
(def +animation-timeout+ 600)
|
||||
|
||||
;; --- Message Event
|
||||
;; --- Main API
|
||||
|
||||
(declare hide)
|
||||
(declare show)
|
||||
(declare show?)
|
||||
|
||||
(deftype Show [data]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [message (assoc data :state :visible)]
|
||||
(assoc state :message message)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [stoper (->> (rx/filter show? s)
|
||||
(rx/take 1))]
|
||||
(->> (rx/of (hide))
|
||||
(rx/delay (:timeout data))
|
||||
(rx/take-until stoper)))))
|
||||
|
||||
(defn show
|
||||
[message]
|
||||
(Show. message))
|
||||
|
||||
(defn show?
|
||||
[v]
|
||||
(instance? Show v))
|
||||
|
||||
(defn error
|
||||
[message & {:keys [timeout] :or {timeout 3000}}]
|
||||
(show {:content message
|
||||
|
@ -72,27 +52,44 @@
|
|||
:timeout js/Number.MAX_SAFE_INTEGER
|
||||
:type :dialog}))
|
||||
|
||||
;; --- Hide Message
|
||||
;; --- Show Event
|
||||
|
||||
(deftype Hide [^:mutable canceled?]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :message
|
||||
(fn [v]
|
||||
(if (nil? v)
|
||||
(do (set! canceled? true) nil)
|
||||
(assoc v :state :hide)))))
|
||||
(defn show
|
||||
[data]
|
||||
(reify
|
||||
ptk/EventType
|
||||
(type [_] ::show)
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(if canceled?
|
||||
(rx/empty)
|
||||
(->> (rx/of #(dissoc state :message))
|
||||
(rx/delay +animation-timeout+)))))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [message (assoc data :state :visible)]
|
||||
(assoc state :message message)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [stoper (->> (rx/filter show? s)
|
||||
(rx/take 1))]
|
||||
(->> (rx/of (hide))
|
||||
(rx/delay (:timeout data))
|
||||
(rx/take-until stoper))))))
|
||||
|
||||
(defn show?
|
||||
[v]
|
||||
(= ::show (ptk/type v)))
|
||||
|
||||
;; --- Hide Event
|
||||
|
||||
(defn hide
|
||||
[]
|
||||
(Hide. false))
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :message assoc :state :hide))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rx/of #(dissoc % :message))
|
||||
(rx/delay +animation-timeout+)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UI Components
|
||||
|
@ -100,10 +97,10 @@
|
|||
|
||||
;; --- Notification Component
|
||||
|
||||
(mx/defc notification-box
|
||||
{:mixins [mx/static]}
|
||||
[{:keys [type on-close] :as message}]
|
||||
(let [classes (classnames :error (= type :error)
|
||||
(mf/defc notification-box
|
||||
[{:keys [message on-close] :as message}]
|
||||
(let [type (:type message)
|
||||
classes (classnames :error (= type :error)
|
||||
:info (= type :info)
|
||||
:hide-message (= (:state message) :hide)
|
||||
:quick true)]
|
||||
|
@ -114,9 +111,8 @@
|
|||
|
||||
;; --- Dialog Component
|
||||
|
||||
(mx/defc dialog-box
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
[{:keys [on-accept on-cancel on-close] :as message}]
|
||||
(mf/defc dialog-box
|
||||
[{:keys [on-accept on-cancel on-close message] :as props}]
|
||||
(let [classes (classnames :info true
|
||||
:hide-message (= (:state message) :hide))]
|
||||
(letfn [(accept [event]
|
||||
|
@ -143,11 +139,10 @@
|
|||
|
||||
;; --- Main Component (entry point)
|
||||
|
||||
(mx/defc messages-widget
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
[message]
|
||||
(mf/defc messages-widget
|
||||
[{:keys [message] :as props}]
|
||||
(case (:type message)
|
||||
:error (notification-box message)
|
||||
:info (notification-box message)
|
||||
:dialog (dialog-box message)
|
||||
:error (mf/element notification-box props)
|
||||
:info (mf/element notification-box props)
|
||||
:dialog (mf/element dialog-box props)
|
||||
nil))
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.util.spec
|
||||
(:require [cljs.spec.alpha :as s]))
|
||||
(:require [cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
;; --- Constants
|
||||
|
||||
|
@ -15,11 +17,17 @@
|
|||
(def uuid-rx
|
||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
|
||||
|
||||
(def number-rx
|
||||
#"^[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$")
|
||||
|
||||
(def ^:private color-re
|
||||
#"^#[0-9A-Fa-f]{6}$")
|
||||
|
||||
;; --- Predicates
|
||||
|
||||
(defn email?
|
||||
[v]
|
||||
(and string?
|
||||
(and (string? v)
|
||||
(re-matches email-rx v)))
|
||||
|
||||
(defn color?
|
||||
|
@ -31,8 +39,6 @@
|
|||
[v]
|
||||
(instance? js/File v))
|
||||
|
||||
;; TODO: properly implement
|
||||
|
||||
(defn url-str?
|
||||
[v]
|
||||
(string? v))
|
||||
|
@ -42,6 +48,29 @@
|
|||
(s/def ::uuid uuid?)
|
||||
(s/def ::email email?)
|
||||
(s/def ::color color?)
|
||||
(s/def ::string string?)
|
||||
(s/def ::number number?)
|
||||
(s/def ::positive pos?)
|
||||
(s/def ::inst inst?)
|
||||
(s/def ::keyword keyword?)
|
||||
(s/def ::fn fn?)
|
||||
(s/def ::coll coll?)
|
||||
|
||||
(s/def ::not-empty-string
|
||||
(s/and string? #(not (str/empty? %))))
|
||||
|
||||
|
||||
(defn- conform-number-str
|
||||
[v]
|
||||
(cond
|
||||
(re-matches number-rx v) (js/parseFloat v)
|
||||
(number? v) v
|
||||
:else ::s/invalid))
|
||||
|
||||
(s/def ::number-str
|
||||
(s/conformer conform-number-str str))
|
||||
|
||||
(s/def ::color color?)
|
||||
|
||||
;; --- Public Api
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue