mirror of
https://github.com/penpot/penpot.git
synced 2025-05-10 16:46:37 +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
|
@ -6,7 +6,7 @@
|
|||
|
||||
(ns uxbox.builtins.colors
|
||||
(:require [uxbox.util.uuid :as uuid]
|
||||
[uxbox.util.data :refer (index-by)]))
|
||||
[uxbox.util.data :refer [index-by-id]]))
|
||||
|
||||
(def collections-list
|
||||
[{:name "UXBOX"
|
||||
|
@ -672,4 +672,4 @@
|
|||
"#3D464D"}}])
|
||||
|
||||
(def collections
|
||||
(index-by collections-list :id))
|
||||
(index-by-id collections-list))
|
||||
|
|
|
@ -67,12 +67,12 @@
|
|||
(add-watch html-history/path ::main #(on-navigate router %4))
|
||||
|
||||
(when (:auth storage)
|
||||
(st/emit! (udu/fetch-profile)))
|
||||
(st/emit! udu/fetch-profile))
|
||||
|
||||
(mf/mount (ui/app) (dom/get-element "app"))
|
||||
(mf/mount (mf/element ui/app) (dom/get-element "app"))
|
||||
(mf/mount (lightbox) (dom/get-element "lightbox"))
|
||||
(mf/mount (mf/element modal) (dom/get-element "modal"))
|
||||
(mf/mount (loader) (dom/get-element "loader"))
|
||||
(mf/mount (mf/element loader) (dom/get-element "loader"))
|
||||
|
||||
(on-navigate router cpath)))
|
||||
|
||||
|
|
|
@ -5,154 +5,147 @@
|
|||
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.auth
|
||||
(:require [cljs.spec.alpha :as s]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.store :refer [initial-state]]
|
||||
[uxbox.main.data.projects :as udp]
|
||||
[uxbox.main.data.users :as udu]
|
||||
[uxbox.util.messages :as uum]
|
||||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.util.storage :refer [storage]]))
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.store :refer [initial-state]]
|
||||
[uxbox.main.data.users :as du]
|
||||
[uxbox.util.messages :as um]
|
||||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.util.storage :refer [storage]]))
|
||||
|
||||
(s/def ::username string?)
|
||||
(s/def ::password string?)
|
||||
(s/def ::fullname string?)
|
||||
(s/def ::email us/email?)
|
||||
(s/def ::token string?)
|
||||
(s/def ::email ::us/email)
|
||||
|
||||
;; --- Logged In
|
||||
|
||||
(defrecord LoggedIn [data]
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(assoc state :auth data))
|
||||
(defn logged-in
|
||||
[data]
|
||||
(reify
|
||||
ptk/EventType
|
||||
(type [_] ::logged-in)
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(swap! storage assoc :auth data)
|
||||
(rx/of (udu/fetch-profile)
|
||||
(rt/navigate :dashboard/projects))))
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(assoc state :auth data))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(swap! storage assoc :auth data)
|
||||
(rx/of du/fetch-profile
|
||||
(rt/navigate :dashboard/projects)))))
|
||||
|
||||
(defn logged-in?
|
||||
[v]
|
||||
(instance? LoggedIn v))
|
||||
|
||||
(defn logged-in
|
||||
[data]
|
||||
(LoggedIn. data))
|
||||
(= (ptk/type v) ::logged-in))
|
||||
|
||||
;; --- Login
|
||||
|
||||
(defrecord Login [username password]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(merge state (dissoc initial-state :route :router)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [params {:username username
|
||||
:password password
|
||||
:scope "webapp"}
|
||||
on-error #(rx/of (uum/error (tr "errors.auth.unauthorized")))]
|
||||
(->> (rp/req :auth/login params)
|
||||
(rx/map :payload)
|
||||
(rx/map logged-in)
|
||||
(rx/catch rp/client-error? on-error)))))
|
||||
|
||||
(s/def ::login-event
|
||||
(s/def ::login-params
|
||||
(s/keys :req-un [::username ::password]))
|
||||
|
||||
(defn login
|
||||
[params]
|
||||
{:pre [(us/valid? ::login-event params)]}
|
||||
(map->Login params))
|
||||
[{:keys [username password] :as data}]
|
||||
(s/assert ::login-params data)
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(merge state (dissoc initial-state :route :router)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [params {:username username
|
||||
:password password
|
||||
:scope "webapp"}
|
||||
on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))]
|
||||
(->> (rp/req :auth/login params)
|
||||
(rx/map :payload)
|
||||
(rx/map logged-in)
|
||||
(rx/catch rp/client-error? on-error))))))
|
||||
|
||||
;; --- Logout
|
||||
|
||||
(defrecord ClearUserData []
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(merge state (dissoc initial-state :route :router)))
|
||||
(def clear-user-data
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(merge state (dissoc initial-state :route :router)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :auth/logout)
|
||||
(rx/ignore)))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :auth/logout)
|
||||
(rx/ignore)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state s]
|
||||
(reset! storage {})
|
||||
(i18n/set-default-locale!)))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state s]
|
||||
(reset! storage {})
|
||||
(i18n/set-default-locale!))))
|
||||
|
||||
(defrecord Logout []
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (rt/nav :auth/login)
|
||||
(->ClearUserData))))
|
||||
|
||||
(defn logout
|
||||
[]
|
||||
(->Logout))
|
||||
(def logout
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (rt/nav :auth/login)
|
||||
clear-user-data))))
|
||||
|
||||
;; --- Register
|
||||
|
||||
;; TODO: clean form on success
|
||||
|
||||
(defrecord Register [data on-error]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(handle-error [{payload :payload}]
|
||||
(on-error payload)
|
||||
(rx/empty))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/register data)
|
||||
(rx/map :payload)
|
||||
(rx/map (constantly ::registered))
|
||||
(rx/catch rp/client-error? handle-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::registered))
|
||||
(rx/take 1)
|
||||
(rx/map #(login data)))))))
|
||||
|
||||
(s/def ::register-event
|
||||
(s/keys :req-un [::fullname ::username ::email ::password]))
|
||||
(s/def ::register-params
|
||||
(s/keys :req-un [::fullname
|
||||
::username
|
||||
::password
|
||||
::email]))
|
||||
|
||||
(defn register
|
||||
"Create a register event instance."
|
||||
[data on-error]
|
||||
{:pre [(us/valid? ::register-event data)
|
||||
(fn? on-error)]}
|
||||
(Register. data on-error))
|
||||
(s/assert ::register-params data)
|
||||
(s/assert ::us/fn on-error)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(handle-error [{payload :payload}]
|
||||
(on-error payload)
|
||||
(rx/empty))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/register data)
|
||||
(rx/map :payload)
|
||||
(rx/map (constantly ::registered))
|
||||
(rx/catch rp/client-error? handle-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::registered))
|
||||
(rx/take 1)
|
||||
(rx/map #(login data))))))))
|
||||
|
||||
;; --- Recovery Request
|
||||
|
||||
(defrecord RecoveryRequest [data]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(println "on-error" payload)
|
||||
(rx/empty))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/recovery-request data)
|
||||
(rx/map (constantly ::recovery-requested))
|
||||
(rx/catch rp/client-error? on-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::recovery-requested))
|
||||
(rx/take 1)
|
||||
(rx/map #(uum/info (tr "auth.message.recovery-token-sent"))))))))
|
||||
|
||||
(s/def ::recovery-request-event
|
||||
(s/def ::recovery-request-params
|
||||
(s/keys :req-un [::username]))
|
||||
|
||||
(defn recovery-request
|
||||
[data]
|
||||
{:pre [(us/valid? ::recovery-request-event data)]}
|
||||
(RecoveryRequest. data))
|
||||
(s/assert ::recovery-request-params data)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(println "on-error" payload)
|
||||
(rx/empty))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/recovery-request data)
|
||||
(rx/map (constantly ::recovery-requested))
|
||||
(rx/catch rp/client-error? on-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::recovery-requested))
|
||||
(rx/take 1)
|
||||
;; TODO: this should be moved to the UI part
|
||||
(rx/map #(um/info (tr "auth.message.recovery-token-sent")))))))))
|
||||
|
||||
;; --- Check Recovery Token
|
||||
|
||||
|
@ -162,7 +155,7 @@
|
|||
(letfn [(on-error [{payload :payload}]
|
||||
(rx/of
|
||||
(rt/navigate :auth/login)
|
||||
(uum/error (tr "errors.auth.invalid-recovery-token"))))]
|
||||
(um/error (tr "errors.auth.invalid-recovery-token"))))]
|
||||
(->> (rp/req :auth/validate-recovery-token token)
|
||||
(rx/ignore)
|
||||
(rx/catch rp/client-error? on-error)))))
|
||||
|
@ -174,23 +167,22 @@
|
|||
|
||||
;; --- Recovery (Password)
|
||||
|
||||
(defrecord Recovery [token password]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(rx/of (uum/error (tr "errors.auth.invalid-recovery-token"))))
|
||||
(on-success [{payload :payload}]
|
||||
(rx/of
|
||||
(rt/navigate :auth/login)
|
||||
(uum/info (tr "auth.message.password-recovered"))))]
|
||||
(->> (rp/req :auth/recovery {:token token :password password})
|
||||
(rx/mapcat on-success)
|
||||
(rx/catch rp/client-error? on-error)))))
|
||||
|
||||
(s/def ::recovery-event
|
||||
(s/def ::token string?)
|
||||
(s/def ::recovery-params
|
||||
(s/keys :req-un [::username ::token]))
|
||||
|
||||
(defn recovery
|
||||
[{:keys [token password] :as data}]
|
||||
{:pre [(us/valid? ::recovery-event data)]}
|
||||
(Recovery. token password))
|
||||
(s/assert ::recovery-params data)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(rx/of (um/error (tr "errors.auth.invalid-recovery-token"))))
|
||||
(on-success [{payload :payload}]
|
||||
(rx/of
|
||||
(rt/navigate :auth/login)
|
||||
(um/info (tr "auth.message.password-recovered"))))]
|
||||
(->> (rp/req :auth/recovery {:token token :password password})
|
||||
(rx/mapcat on-success)
|
||||
(rx/catch rp/client-error? on-error))))))
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
[potok.core :as ptk]
|
||||
[uxbox.util.router :as r]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.forms :as sc]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.data.projects :as dp]
|
||||
[uxbox.main.data.colors :as dc]
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
[uxbox.util.data :refer [replace-by-id
|
||||
index-by]]))
|
||||
|
||||
;; TODO: this need refactor (completely broken)
|
||||
|
||||
;; --- Initialize History State
|
||||
|
||||
(declare fetch-history)
|
||||
|
@ -52,7 +54,7 @@
|
|||
(deftype PinnedPageHistoryFetched [items]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [items-map (index-by items :version)
|
||||
(let [items-map (index-by :version items)
|
||||
items-set (into #{} items)]
|
||||
(update-in state [:workspace :history]
|
||||
(fn [history]
|
||||
|
@ -164,7 +166,7 @@
|
|||
(assoc :history true
|
||||
:data (:data item)))]
|
||||
(-> state
|
||||
(udp/assoc-page page)
|
||||
(udp/unpack-page page)
|
||||
(assoc-in [:workspace :history :selected] version)))))
|
||||
|
||||
(defn select-page-history
|
||||
|
@ -203,7 +205,7 @@
|
|||
(set! noop true)
|
||||
state)
|
||||
(let [packed (get-in state [:packed-pages page-id])]
|
||||
(-> (udp/assoc-page state packed)
|
||||
(-> (udp/unpack-page state packed)
|
||||
(assoc-in [:workspace :history :deselecting] true)
|
||||
(assoc-in [:workspace :history :selected] nil))))))
|
||||
|
||||
|
|
|
@ -5,65 +5,78 @@
|
|||
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.pages
|
||||
(:require [cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[lentes.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.router :as r]
|
||||
[uxbox.util.timers :as ts]
|
||||
[uxbox.util.time :as dt]))
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.util.data :refer [index-by-id]]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.timers :as ts]
|
||||
[uxbox.util.uuid :as uuid]))
|
||||
|
||||
;; --- Specs
|
||||
;; --- Struct
|
||||
|
||||
(s/def ::grid-x-axis number?)
|
||||
(s/def ::grid-y-axis number?)
|
||||
(s/def ::grid-color string?)
|
||||
(s/def ::background string?)
|
||||
(s/def ::background-opacity number?)
|
||||
(s/def ::grid-alignment boolean?)
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
(s/def ::layout string?)
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::inst ::us/inst)
|
||||
(s/def ::type ::us/keyword)
|
||||
(s/def ::project ::us/uuid)
|
||||
(s/def ::created-at ::us/inst)
|
||||
(s/def ::modified-at ::us/inst)
|
||||
(s/def ::version ::us/number)
|
||||
(s/def ::width (s/and ::us/number ::us/positive))
|
||||
(s/def ::height (s/and ::us/number ::us/positive))
|
||||
(s/def ::grid-x-axis ::us/number)
|
||||
(s/def ::grid-y-axis ::us/number)
|
||||
(s/def ::grid-color ::us/string)
|
||||
(s/def ::order ::us/number)
|
||||
(s/def ::background ::us/string)
|
||||
(s/def ::background-opacity ::us/number)
|
||||
(s/def ::user ::us/uuid)
|
||||
|
||||
(s/def ::metadata
|
||||
(s/keys :req-un [::width ::height]
|
||||
:opt-un [::grid-y-axis
|
||||
::grid-x-axis
|
||||
::grid-color
|
||||
::grid-alignment
|
||||
::order
|
||||
::background
|
||||
::background-opacity
|
||||
::layout]))
|
||||
::background-opacity]))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::name string?)
|
||||
(s/def ::version integer?)
|
||||
(s/def ::project uuid?)
|
||||
(s/def ::user uuid?)
|
||||
(s/def ::created-at inst?)
|
||||
(s/def ::modified-at inst?)
|
||||
(s/def ::shapes
|
||||
(-> (s/coll-of uuid? :kind vector?)
|
||||
(s/nilable)))
|
||||
(s/coll-of ::us/uuid :kind vector? :into []))
|
||||
|
||||
(s/def ::page-entity
|
||||
(s/keys :req-un [::id
|
||||
::name
|
||||
::project
|
||||
::version
|
||||
::created-at
|
||||
::modified-at
|
||||
::user
|
||||
::metadata
|
||||
::shapes]))
|
||||
|
||||
;; TODO: add interactions to spec
|
||||
(s/def ::minimal-shape
|
||||
(s/keys :req-un [::type ::name]
|
||||
:opt-un [::id]))
|
||||
|
||||
(s/def :uxbox.backend/shapes
|
||||
(s/coll-of ::minimal-shape :kind vector?))
|
||||
|
||||
(s/def :uxbox.backend/data
|
||||
(s/keys :req-un [:uxbox.backend/shapes]))
|
||||
|
||||
(s/def ::server-page
|
||||
(s/keys :req-un [::id ::name
|
||||
::project
|
||||
::version
|
||||
::created-at
|
||||
::modified-at
|
||||
::user
|
||||
::metadata
|
||||
:uxbox.backend/data]))
|
||||
|
||||
;; --- Protocols
|
||||
|
||||
|
@ -77,45 +90,26 @@
|
|||
|
||||
;; --- Helpers
|
||||
|
||||
;; TODO: make sure remove all :tmp-* related attrs from shape
|
||||
|
||||
(defn pack-page-shapes
|
||||
"Create a hash-map of shapes indexed by their id that belongs
|
||||
to the provided page."
|
||||
[state page]
|
||||
(let [lookup-shape-xf (map #(get-in state [:shapes %]))]
|
||||
(reduce (fn reducer [acc {:keys [id type items] :as shape}]
|
||||
(let [shape (assoc shape :page (:id page))]
|
||||
(cond
|
||||
(= type :group)
|
||||
(reduce reducer
|
||||
(assoc acc id shape)
|
||||
(sequence lookup-shape-xf items))
|
||||
|
||||
(uuid? id)
|
||||
(assoc acc id shape)
|
||||
|
||||
:else acc)))
|
||||
{}
|
||||
(sequence lookup-shape-xf (:shapes page)))))
|
||||
|
||||
(defn pack-page
|
||||
"Return a packed version of page object ready
|
||||
for send to remore storage service."
|
||||
[state id]
|
||||
(let [page (get-in state [:pages id])
|
||||
shapes (pack-page-shapes state page)]
|
||||
(-> page
|
||||
(assoc-in [:data :shapes] (vec (:shapes page)))
|
||||
(assoc-in [:data :shapes-map] shapes)
|
||||
(dissoc :shapes))))
|
||||
(letfn [(pack-shapes [ids]
|
||||
(mapv #(get-in state [:shapes %]) ids))]
|
||||
(let [page (get-in state [:pages id])
|
||||
data {:shapes (pack-shapes (:shapes page))}]
|
||||
(-> page
|
||||
(assoc :data data)
|
||||
(dissoc :shapes)))))
|
||||
|
||||
(defn assoc-page
|
||||
(defn unpack-page
|
||||
"Unpacks packed page object and assocs it to the
|
||||
provided state."
|
||||
[state {:keys [id data] :as page}]
|
||||
(let [shapes (:shapes data)
|
||||
shapes-map (:shapes-map data)
|
||||
(let [shapes-data (:shapes data [])
|
||||
shapes (mapv :id shapes-data)
|
||||
shapes-map (index-by-id shapes-data)
|
||||
|
||||
page (-> page
|
||||
(dissoc :data)
|
||||
(assoc :shapes shapes))]
|
||||
|
@ -126,16 +120,16 @@
|
|||
(defn purge-page
|
||||
"Remove page and all related stuff from the state."
|
||||
[state id]
|
||||
(let [shapes (get state :shapes)]
|
||||
(let [pid (get-in state [:pages id :project])]
|
||||
(-> state
|
||||
(update-in [:projects pid :pages] #(filterv (partial not= id) %))
|
||||
(update :pages dissoc id)
|
||||
(update :packed-pages dissoc id)
|
||||
(assoc :shapes (reduce-kv (fn [acc k v]
|
||||
(if (= (:page v) id)
|
||||
(dissoc acc k)
|
||||
acc))
|
||||
shapes
|
||||
shapes)))))
|
||||
(update :shapes (fn [shapes] (->> shapes
|
||||
(map second)
|
||||
(filter #(= (:page %) id))
|
||||
(map :id)
|
||||
(apply dissoc shapes)))))))
|
||||
|
||||
(defn assoc-packed-page
|
||||
[state {:keys [id] :as page}]
|
||||
|
@ -147,168 +141,183 @@
|
|||
|
||||
;; --- Pages Fetched
|
||||
|
||||
(deftype PagesFetched [id pages]
|
||||
IDeref
|
||||
(-deref [_] (list id pages))
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [get-order #(get-in % [:metadata :order])
|
||||
pages (sort-by get-order pages)
|
||||
page-ids (into [] (map :id) pages)]
|
||||
(as-> state $
|
||||
(assoc-in $ [:projects id :pages] page-ids)
|
||||
;; TODO: this is a workaround
|
||||
(assoc-in $ [:projects id :page-id] (first page-ids))
|
||||
(reduce assoc-page $ pages)
|
||||
(reduce assoc-packed-page $ pages)))))
|
||||
|
||||
(defn pages-fetched
|
||||
[id pages]
|
||||
{:pre [(uuid? id) (coll? pages)]}
|
||||
(PagesFetched. id pages))
|
||||
(s/assert ::us/uuid id)
|
||||
(s/assert ::us/coll pages)
|
||||
(reify
|
||||
IDeref
|
||||
(-deref [_] (list id pages))
|
||||
|
||||
ptk/EventType
|
||||
(type [_] ::page-fetched)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [get-order #(get-in % [:metadata :order])
|
||||
pages (sort-by get-order pages)
|
||||
page-ids (into [] (map :id) pages)]
|
||||
(as-> state $
|
||||
(assoc-in $ [:projects id :pages] page-ids)
|
||||
(reduce unpack-page $ pages)
|
||||
(reduce assoc-packed-page $ pages))))))
|
||||
|
||||
(defn pages-fetched?
|
||||
[v]
|
||||
(instance? PagesFetched v))
|
||||
(= ::page-fetched (ptk/type v)))
|
||||
|
||||
;; --- Fetch Pages (by project id)
|
||||
|
||||
(deftype FetchPages [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :fetch/pages-by-project {:project id})
|
||||
(rx/map :payload)
|
||||
(rx/map #(pages-fetched id %)))))
|
||||
|
||||
(defn fetch-pages
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(FetchPages. id))
|
||||
(s/assert ::us/uuid id)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :fetch/pages-by-project {:project id})
|
||||
(rx/map :payload)
|
||||
(rx/map #(pages-fetched id %))))))
|
||||
|
||||
;; --- Page Created
|
||||
|
||||
(declare reorder-pages)
|
||||
(declare rehash-pages)
|
||||
|
||||
(deftype PageCreated [data]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-page data)
|
||||
(assoc-packed-page data)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (reorder-pages (:project data)))))
|
||||
|
||||
(s/def ::page-created
|
||||
(s/keys :req-un [::id
|
||||
::name
|
||||
::project
|
||||
::metadata]))
|
||||
(s/def ::page-created-params
|
||||
(s/keys :req-un [::id ::name ::project ::metadata]))
|
||||
|
||||
(defn page-created
|
||||
[data]
|
||||
{:pre [(us/valid? ::page-created data)]}
|
||||
(PageCreated. data))
|
||||
(s/assert ::page-created-params data)
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (:project data)]
|
||||
(-> state
|
||||
(update-in [:projects pid :pages] (fnil conj []) (:id data))
|
||||
(unpack-page data)
|
||||
(assoc-packed-page data))))
|
||||
|
||||
;; --- Create Page
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (rehash-pages (:project data))))))
|
||||
|
||||
(deftype CreatePage [name project width height layout]
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [params {:name name
|
||||
:project project
|
||||
:data {}
|
||||
:metadata {:width width
|
||||
:height height
|
||||
:layout layout
|
||||
:order -100}}]
|
||||
(->> (rp/req :create/page params)
|
||||
(rx/map :payload)
|
||||
(rx/map page-created)))))
|
||||
;; --- Create Page Form
|
||||
|
||||
(s/def ::create-page
|
||||
(s/keys :req-un [::name
|
||||
::project
|
||||
::width
|
||||
::height
|
||||
::layout]))
|
||||
(s/def ::form-created-page-params
|
||||
(s/keys :req-un [::name ::project ::width ::height]))
|
||||
|
||||
(defn create-page
|
||||
(defn form->create-page
|
||||
[{:keys [name project width height layout] :as data}]
|
||||
{:pre [(us/valid? ::create-page data)]}
|
||||
(CreatePage. name project width height layout))
|
||||
(s/assert ::form-created-page-params data)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [canvas {:id (uuid/random)
|
||||
:name "Canvas 1"
|
||||
:type :canvas
|
||||
:x1 200
|
||||
:y1 200
|
||||
:x2 (+ 200 width)
|
||||
:y2 (+ 200 height)}
|
||||
metadata {:width width
|
||||
:height height
|
||||
:order -100}
|
||||
params {:name name
|
||||
:project project
|
||||
:data {:shapes [canvas]}
|
||||
:metadata metadata}]
|
||||
(->> (rp/req :create/page params)
|
||||
(rx/map :payload)
|
||||
(rx/map page-created))))))
|
||||
|
||||
;; --- Update Page Form
|
||||
|
||||
(s/def ::form-update-page-params
|
||||
(s/keys :req-un [::id ::name ::width ::height]))
|
||||
|
||||
(defn form->update-page
|
||||
[{:keys [id name width height] :as data}]
|
||||
(s/assert ::form-update-page-params data)
|
||||
(reify
|
||||
IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:pages id]
|
||||
(fn [page]
|
||||
(-> (assoc page :name name)
|
||||
(assoc-in [:name] name)
|
||||
(assoc-in [:metadata :width] width)
|
||||
(assoc-in [:metadata :height] height)))))))
|
||||
|
||||
;; --- Page Persisted
|
||||
|
||||
(deftype PagePersisted [data]
|
||||
IDeref
|
||||
(-deref [_] data)
|
||||
(defn page-persisted
|
||||
[data]
|
||||
(s/assert ::server-page data)
|
||||
(reify
|
||||
cljs.core/IDeref
|
||||
(-deref [_] data)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [{:keys [id version]} data]
|
||||
(-> state
|
||||
(assoc-in [:pages id :version] version)
|
||||
(assoc-packed-page data)))))
|
||||
ptk/EventType
|
||||
(type [_] ::page-persisted)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [{:keys [id version]} data]
|
||||
(-> state
|
||||
(assoc-in [:pages id :version] version)
|
||||
(assoc-packed-page data))))))
|
||||
|
||||
(defn- page-persisted?
|
||||
[event]
|
||||
(instance? PagePersisted event))
|
||||
|
||||
;; TODO: add page spec
|
||||
|
||||
(defn page-persisted
|
||||
[data]
|
||||
{:pre [(map? data)]}
|
||||
(PagePersisted. data))
|
||||
(= (ptk/type event) ::page-persisted))
|
||||
|
||||
;; --- Persist Page
|
||||
|
||||
(deftype PersistPage [id on-success]
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [page (get-in state [:pages id])]
|
||||
(if (:history page)
|
||||
(rx/empty)
|
||||
(let [page (pack-page state id)]
|
||||
(->> (rp/req :update/page page)
|
||||
(rx/map :payload)
|
||||
(rx/do #(when (fn? on-success)
|
||||
(ts/schedule-on-idle on-success)))
|
||||
(rx/map page-persisted)))))))
|
||||
(defn persist-page
|
||||
([id] (persist-page id identity))
|
||||
([id on-success]
|
||||
(assert (uuid? id))
|
||||
(reify
|
||||
ptk/EventType
|
||||
(type [_] ::persist-page)
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [page (get-in state [:pages id])]
|
||||
(if (:history page)
|
||||
(rx/empty)
|
||||
(let [page (pack-page state id)]
|
||||
(->> (rp/req :update/page page)
|
||||
(rx/map :payload)
|
||||
(rx/do #(when (fn? on-success)
|
||||
(ts/schedule-on-idle on-success)))
|
||||
(rx/map page-persisted)))))))))
|
||||
|
||||
(defn persist-page?
|
||||
[v]
|
||||
(instance? PersistPage v))
|
||||
|
||||
(defn persist-page
|
||||
([id]
|
||||
{:pre [(uuid? id)]}
|
||||
(PersistPage. id (constantly nil)))
|
||||
([id on-success]
|
||||
{:pre [(uuid? id)]}
|
||||
(PersistPage. id on-success)))
|
||||
(= ::persist-page (ptk/type v)))
|
||||
|
||||
;; --- Page Metadata Persisted
|
||||
|
||||
(deftype MetadataPersisted [id data]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:pages id :version] (:version data))))
|
||||
|
||||
(s/def ::metadata-persisted-event
|
||||
(s/def ::metadata-persisted-params
|
||||
(s/keys :req-un [::id ::version]))
|
||||
|
||||
(defn metadata-persisted?
|
||||
[v]
|
||||
(instance? MetadataPersisted v))
|
||||
|
||||
(defn metadata-persisted
|
||||
[{:keys [id] :as data}]
|
||||
{:pre [(us/valid? ::metadata-persisted-event data)]}
|
||||
(MetadataPersisted. id data))
|
||||
(s/assert ::metadata-persisted-params data)
|
||||
(reify
|
||||
ptk/EventType
|
||||
(type [_] ::metadata-persisted)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:pages id :version] (:version data)))))
|
||||
|
||||
(defn metadata-persisted?
|
||||
[v]
|
||||
(= ::metadata-persisted (ptk/type v)))
|
||||
|
||||
|
||||
;; --- Persist Page Metadata
|
||||
|
||||
|
@ -329,52 +338,49 @@
|
|||
|
||||
;; --- Update Page
|
||||
|
||||
(deftype UpdatePage [id data]
|
||||
IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:pages id] merge (dissoc data :id :version))))
|
||||
|
||||
(defn update-page
|
||||
[id data]
|
||||
{:pre [(uuid? id) (us/valid? ::page-entity data)]}
|
||||
(UpdatePage. id data))
|
||||
(defn update-page-attrs
|
||||
[{:keys [id] :as data}]
|
||||
(s/assert ::page-entity data)
|
||||
(reify
|
||||
IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:pages id] merge (dissoc data :id :version)))))
|
||||
|
||||
;; --- Update Page Metadata
|
||||
|
||||
(deftype UpdateMetadata [id metadata]
|
||||
IMetadataUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(assoc-in state [:pages id :metadata] metadata)))
|
||||
|
||||
(defn update-metadata
|
||||
[id metadata]
|
||||
{:pre [(uuid? id) (us/valid? ::metadata metadata)]}
|
||||
(UpdateMetadata. id metadata))
|
||||
(s/assert ::id id)
|
||||
(s/assert ::metadata metadata)
|
||||
(reify
|
||||
IMetadataUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(assoc-in state [:pages id :metadata] metadata))))
|
||||
|
||||
;; --- Reorder Pages
|
||||
|
||||
;; --- Rehash Pages
|
||||
;;
|
||||
;; A post processing event that normalizes the
|
||||
;; page order numbers after a user sorting
|
||||
;; operation for a concrete project.
|
||||
|
||||
(defn reorder-pages
|
||||
[project-id]
|
||||
{:pre [(uuid? project-id)]}
|
||||
(defn rehash-pages
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(let [page-ids (get-in state [:projects project-id :pages])]
|
||||
(let [page-ids (get-in state [:projects id :pages])]
|
||||
(reduce (fn [state [index id]]
|
||||
(assoc-in state [:pages id :metadata :order] index))
|
||||
;; TODO: this is workaround
|
||||
(assoc-in state [:projects project-id :page-id] (first page-ids))
|
||||
state
|
||||
(map-indexed vector page-ids))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-ids (get-in state [:projects project-id :pages])]
|
||||
(let [page-ids (get-in state [:projects id :pages])]
|
||||
(->> (rx/from-coll page-ids)
|
||||
(rx/map persist-metadata))))))
|
||||
|
||||
|
@ -391,72 +397,45 @@
|
|||
pages (vec (concat before [page-id] after))]
|
||||
(assoc-in state [:projects project-id :pages] pages)))))
|
||||
|
||||
;; --- Persist Page Form
|
||||
;;
|
||||
;; A specialized event for persist data
|
||||
;; from the update page form.
|
||||
|
||||
(deftype PersistPageUpdateForm [id name width height layout]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page (-> (get-in state [:pages id])
|
||||
(assoc-in [:name] name)
|
||||
(assoc-in [:metadata :width] width)
|
||||
(assoc-in [:metadata :height] height)
|
||||
(assoc-in [:metadata :layout] layout))]
|
||||
(rx/of (update-page id page)))))
|
||||
|
||||
(s/def ::persist-page-update-form
|
||||
(s/keys :req-un [::name ::width ::height ::layout]))
|
||||
|
||||
(defn persist-page-update-form
|
||||
[id {:keys [name width height layout] :as data}]
|
||||
{:pre [(uuid? id) (us/valid? ::persist-page-update-form data)]}
|
||||
(PersistPageUpdateForm. id name width height layout))
|
||||
|
||||
;; --- Delete Page (by id)
|
||||
|
||||
(deftype DeletePage [id callback]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(letfn [(on-success [_]
|
||||
#(purge-page % id))]
|
||||
(->> (rp/req :delete/page id)
|
||||
(rx/map on-success)
|
||||
(rx/tap callback)
|
||||
(rx/filter identity)))))
|
||||
|
||||
(defn delete-page
|
||||
([id] (DeletePage. id (constantly nil)))
|
||||
([id callback] (DeletePage. id callback)))
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(purge-page state id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :delete/page id)
|
||||
(rx/map (constantly ::delete-completed))))))
|
||||
|
||||
;; --- Watch Page Changes
|
||||
|
||||
(deftype WatchPageChanges [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(= % ::stop-page-watcher))
|
||||
(rx/take 1))]
|
||||
(rx/merge
|
||||
(->> stream
|
||||
(rx/filter #(or (satisfies? IPageUpdate %)
|
||||
(= ::page-update %)))
|
||||
(rx/take-until stopper)
|
||||
(rx/debounce 1000)
|
||||
(rx/mapcat #(rx/merge (rx/of (persist-page id))
|
||||
(->> (rx/filter page-persisted? stream)
|
||||
(rx/take 1)
|
||||
(rx/ignore)))))
|
||||
(->> stream
|
||||
(rx/filter #(satisfies? IMetadataUpdate %))
|
||||
(rx/take-until stopper)
|
||||
(rx/debounce 1000)
|
||||
(rx/mapcat #(rx/merge (rx/of (persist-metadata id))
|
||||
(->> (rx/filter metadata-persisted? stream)
|
||||
(rx/take 1)
|
||||
(rx/ignore)))))))))
|
||||
|
||||
(defn watch-page-changes
|
||||
[id]
|
||||
(WatchPageChanges. id))
|
||||
(s/assert ::us/uuid id)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (rx/filter #(= % ::stop-page-watcher) stream)]
|
||||
(->> (rx/merge
|
||||
(->> stream
|
||||
(rx/filter #(or (satisfies? IPageUpdate %)
|
||||
(= ::page-update %)))
|
||||
(rx/debounce 1000)
|
||||
(rx/mapcat #(rx/merge (rx/of (persist-page id))
|
||||
(->> (rx/filter page-persisted? stream)
|
||||
(rx/take 1)
|
||||
(rx/ignore)))))
|
||||
(->> stream
|
||||
(rx/filter #(satisfies? IMetadataUpdate %))
|
||||
(rx/debounce 1000)
|
||||
(rx/mapcat #(rx/merge (rx/of (persist-metadata id))
|
||||
(->> (rx/filter metadata-persisted? stream)
|
||||
(rx/take 1)
|
||||
(rx/ignore))))))
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
|
|
|
@ -5,16 +5,17 @@
|
|||
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.projects
|
||||
(:require [cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.time :as dt]
|
||||
[uxbox.util.router :as rt]))
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.util.uuid :as uuid]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.time :as dt]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
;; --- Specs
|
||||
|
||||
|
@ -38,7 +39,7 @@
|
|||
(defn assoc-project
|
||||
"A reduce function for assoc the project to the state map."
|
||||
[state {:keys [id] :as project}]
|
||||
{:pre [(us/valid? ::project-entity project)]}
|
||||
(s/assert ::project-entity project)
|
||||
(update-in state [:projects id] merge project))
|
||||
|
||||
(defn dissoc-project
|
||||
|
@ -159,74 +160,34 @@
|
|||
|
||||
;; --- Create Project
|
||||
|
||||
(defrecord CreateProject [name width height layout]
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [project-data {:name name}
|
||||
page-data {:name "Page 0"
|
||||
:data {}
|
||||
:metadata {:width width
|
||||
:height height
|
||||
:layout layout
|
||||
:order 0}}]
|
||||
(s/def ::create-project-params
|
||||
(s/keys :req-un [::name ::width ::height]))
|
||||
|
||||
(defn create-project
|
||||
[{:keys [name] :as params}]
|
||||
(s/assert ::create-project-params params)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [this state stream]
|
||||
(->> (rp/req :create/project {:name name})
|
||||
(rx/map :payload)
|
||||
(rx/mapcat (fn [{:keys [id] :as project}]
|
||||
(rp/req :create/page (assoc page-data :project id))))
|
||||
(rx/map #(fetch-projects))))))
|
||||
(rx/of #(assoc-project % project)
|
||||
(udp/form->create-page (assoc params :project id)))))))))
|
||||
|
||||
(s/def ::create-project-event
|
||||
(s/keys :req-un [::name
|
||||
::udp/width
|
||||
::udp/height
|
||||
::udp/layout]))
|
||||
|
||||
(defn create-project
|
||||
[data]
|
||||
{:pre [(us/valid? ::create-project-event data)]}
|
||||
(map->CreateProject data))
|
||||
|
||||
;; --- Go To & Go To Page
|
||||
|
||||
(deftype GoToFirstPage [pages]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [[page & rest] (sort-by #(get-in % [:metadata :order]) pages)
|
||||
params {:project (:project page)
|
||||
:page (:id page)}]
|
||||
(rx/of (rt/navigate :workspace/page params)))))
|
||||
|
||||
(defn go-to-first-page
|
||||
[pages]
|
||||
(GoToFirstPage. pages))
|
||||
|
||||
(defrecord GoTo [project-id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:projects project-id :page-id])]
|
||||
(if-not page-id
|
||||
(rx/empty)
|
||||
(let [params {:project project-id
|
||||
:page page-id}]
|
||||
(rx/of (rt/navigate :workspace/page params)))))))
|
||||
|
||||
(defrecord GoToPage [project-id page-id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [params {:project project-id
|
||||
:page page-id}]
|
||||
(rx/of (rt/navigate :workspace/page params)))))
|
||||
;; --- Go To Project
|
||||
|
||||
(defn go-to
|
||||
"A shortcut event that redirects the user to the
|
||||
first page of the project."
|
||||
([project-id]
|
||||
{:pre [(uuid? project-id)]}
|
||||
(GoTo. project-id))
|
||||
([project-id page-id]
|
||||
{:pre [(uuid? project-id)
|
||||
(uuid? page-id)]}
|
||||
(GoToPage. project-id page-id)))
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [[page & rest-pages] (get-in state [:projects id :pages])]
|
||||
(when page
|
||||
(let [params {:project id :page page}]
|
||||
(rx/of (rt/nav :workspace/page params))))))))
|
||||
|
||||
|
||||
;; --- Update Opts (Filtering & Ordering)
|
||||
|
||||
|
|
|
@ -2,527 +2,541 @@
|
|||
;; 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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.shapes
|
||||
(:require [cljs.spec.alpha :as s]
|
||||
[lentes.core :as l]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.workers :as uwrk]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.data.shapes-impl :as impl]
|
||||
[uxbox.main.user-events :as uev]
|
||||
[uxbox.util.data :refer [dissoc-in]]
|
||||
[uxbox.util.forms :as sc]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.router :as r]
|
||||
[uxbox.util.uuid :as uuid]))
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.util.data :refer [index-of]]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.uuid :as uuid]))
|
||||
|
||||
;; --- Specs
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::blocked boolean?)
|
||||
(s/def ::collapsed boolean?)
|
||||
(s/def ::content string?)
|
||||
(s/def ::fill-color string?)
|
||||
(s/def ::fill-opacity number?)
|
||||
(s/def ::line-height number?)
|
||||
(s/def ::letter-spacing number?)
|
||||
(s/def ::text-align #{"left" "right" "center" "justify"})
|
||||
(s/def ::font-family string?)
|
||||
(s/def ::font-size number?)
|
||||
(s/def ::font-style string?)
|
||||
(s/def ::font-weight string?)
|
||||
(s/def ::font-size number?)
|
||||
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
|
||||
(s/def ::stroke-width number?)
|
||||
(s/def ::stroke-color string?)
|
||||
(s/def ::stroke-opacity number?)
|
||||
(s/def ::rx number?)
|
||||
(s/def ::ry number?)
|
||||
(s/def ::height number?)
|
||||
(s/def ::hidden boolean?)
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::letter-spacing number?)
|
||||
(s/def ::line-height number?)
|
||||
(s/def ::locked boolean?)
|
||||
(s/def ::name string?)
|
||||
(s/def ::page uuid?)
|
||||
(s/def ::proportion number?)
|
||||
(s/def ::proportion-lock boolean?)
|
||||
(s/def ::collapsed boolean?)
|
||||
(s/def ::hidden boolean?)
|
||||
(s/def ::blocked boolean?)
|
||||
(s/def ::locked boolean?)
|
||||
(s/def ::rx number?)
|
||||
(s/def ::ry number?)
|
||||
(s/def ::stroke-color string?)
|
||||
(s/def ::stroke-opacity number?)
|
||||
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
|
||||
(s/def ::stroke-width number?)
|
||||
(s/def ::text-align #{"left" "right" "center" "justify"})
|
||||
(s/def ::type #{:rect :path :circle :image :text})
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
(s/def ::x1 number?)
|
||||
(s/def ::y1 number?)
|
||||
(s/def ::x2 number?)
|
||||
(s/def ::y1 number?)
|
||||
(s/def ::y2 number?)
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::page uuid?)
|
||||
(s/def ::type #{:rect
|
||||
:group
|
||||
:path
|
||||
:circle
|
||||
:image
|
||||
:text})
|
||||
|
||||
(s/def ::attributes
|
||||
(s/keys :opt-un [::fill-color
|
||||
(s/keys :opt-un [::blocked
|
||||
::collapsed
|
||||
::content
|
||||
::fill-color
|
||||
::fill-opacity
|
||||
::line-height
|
||||
::letter-spacing
|
||||
::text-align
|
||||
::font-family
|
||||
::font-size
|
||||
::font-style
|
||||
::font-weight
|
||||
::font-size
|
||||
::stroke-style
|
||||
::stroke-width
|
||||
::hidden
|
||||
::letter-spacing
|
||||
::line-height
|
||||
::locked
|
||||
::proportion
|
||||
::proportion-lock
|
||||
::rx ::ry
|
||||
::stroke-color
|
||||
::stroke-opacity
|
||||
::rx ::ry
|
||||
::stroke-style
|
||||
::stroke-width
|
||||
::text-align
|
||||
::x1 ::x2
|
||||
::y1 ::y2
|
||||
::proportion-lock
|
||||
::proportion
|
||||
::collapsed
|
||||
::hidden
|
||||
::blocked
|
||||
::locked]))
|
||||
::y1 ::y2]))
|
||||
|
||||
(s/def ::minimal-shape
|
||||
(s/keys :req-un [::id ::page ::type ::name]))
|
||||
|
||||
(s/def ::shape
|
||||
(s/merge (s/keys ::req-un [::id ::page ::type]) ::attributes))
|
||||
(s/and ::minimal-shape ::attributes))
|
||||
|
||||
(s/def ::rect-like-shape
|
||||
(s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type]))
|
||||
|
||||
;; --- Shapes CRUD
|
||||
;; --- Shape Creation
|
||||
|
||||
(deftype AddShape [data]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [shape (geom/setup-proportions data)
|
||||
page-id (get-in state [:workspace :current])]
|
||||
(impl/assoc-shape-to-page state shape page-id))))
|
||||
(defn retrieve-used-names
|
||||
"Returns a set of already used names by shapes
|
||||
in the current page."
|
||||
[{:keys [shapes] :as state}]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
xf (comp (filter #(= pid (:page %)))
|
||||
(map :name))]
|
||||
(into #{} xf (vals shapes))))
|
||||
|
||||
(defn add-shape
|
||||
[data]
|
||||
{:pre [(us/valid? ::shape data)]}
|
||||
(AddShape. data))
|
||||
(defn generate-unique-name
|
||||
"A unique name generator based on the previous
|
||||
state of the used names."
|
||||
[state basename]
|
||||
(let [used (retrieve-used-names state)]
|
||||
(loop [counter 1]
|
||||
(let [candidate (str basename "-" counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate)))))
|
||||
|
||||
;; --- Delete Shape
|
||||
(defn assoc-shape-to-page
|
||||
[state shape page]
|
||||
(let [shape-id (uuid/random)
|
||||
shape-name (generate-unique-name state (:name shape))
|
||||
shape (assoc shape
|
||||
:page page
|
||||
:id shape-id
|
||||
:name shape-name)]
|
||||
(-> state
|
||||
(update-in [:pages page :shapes] #(into [] (cons shape-id %)))
|
||||
(assoc-in [:shapes shape-id] shape))))
|
||||
|
||||
(deftype DeleteShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(impl/dissoc-shape state shape))))
|
||||
(defn duplicate-shapes'
|
||||
([state shapes page]
|
||||
(duplicate-shapes' state shapes page nil))
|
||||
([state shapes page group]
|
||||
(letfn [(duplicate-shape [state shape page group]
|
||||
(if (= (:type shape) :group)
|
||||
(let [id (uuid/random)
|
||||
items (:items shape)
|
||||
name (generate-unique-name state (str (:name shape) "-copy"))
|
||||
shape (assoc shape
|
||||
:id id
|
||||
:page page
|
||||
:items []
|
||||
:name name)
|
||||
state (if (nil? group)
|
||||
(-> state
|
||||
(update-in [:pages page :shapes]
|
||||
#(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))
|
||||
(-> state
|
||||
(update-in [:shapes group :items]
|
||||
#(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape)))]
|
||||
(->> (map #(get-in state [:shapes %]) items)
|
||||
(reverse)
|
||||
(reduce #(duplicate-shape %1 %2 page id) state)))
|
||||
(let [id (uuid/random)
|
||||
name (generate-unique-name state (str (:name shape) "-copy"))
|
||||
shape (-> (dissoc shape :group)
|
||||
(assoc :id id :page page :name name)
|
||||
(merge (when group {:group group})))]
|
||||
(if (nil? group)
|
||||
(-> state
|
||||
(update-in [:pages page :shapes] #(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))
|
||||
(-> state
|
||||
(update-in [:shapes group :items] #(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))))))]
|
||||
(reduce #(duplicate-shape %1 %2 page group) state shapes))))
|
||||
|
||||
(defn delete-shape
|
||||
"Remove the shape using its id."
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(DeleteShape. id))
|
||||
(defn duplicate-shapes
|
||||
([state shapes]
|
||||
(duplicate-shapes state shapes nil))
|
||||
([state shapes page]
|
||||
(letfn [(all-toplevel? [coll]
|
||||
(every? #(nil? (:group %)) coll))
|
||||
(all-same-group? [coll]
|
||||
(let [group (:group (first coll))]
|
||||
(every? #(= group (:group %)) coll)))]
|
||||
(let [shapes (reverse (mapv #(get-in state [:shapes %]) shapes))]
|
||||
(cond
|
||||
(all-toplevel? shapes)
|
||||
(let [page (or page (:page (first shapes)))]
|
||||
(duplicate-shapes' state shapes page))
|
||||
|
||||
;; --- Rename Shape
|
||||
(all-same-group? shapes)
|
||||
(let [page (or page (:page (first shapes)))
|
||||
group (:group (first shapes))]
|
||||
(duplicate-shapes' state shapes page group))
|
||||
|
||||
(deftype RenameShape [id name]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :name] name)))
|
||||
:else
|
||||
(let [page (or page (:page (first shapes)))]
|
||||
(duplicate-shapes' state shapes page)))))))
|
||||
|
||||
(defn rename-shape
|
||||
[id name]
|
||||
{:pre [(uuid? id) (string? name)]}
|
||||
(RenameShape. id name))
|
||||
;; --- Delete Shapes
|
||||
|
||||
;; --- Update Rotation
|
||||
(defn dissoc-from-index
|
||||
"A function that dissoc shape from the indexed
|
||||
data structure of shapes from the state."
|
||||
[state {:keys [id type] :as shape}]
|
||||
(if (= :group type)
|
||||
(let [items (map #(get-in state [:shapes %]) (:items shape))]
|
||||
(as-> state $
|
||||
(update-in $ [:shapes] dissoc id)
|
||||
(reduce dissoc-from-index $ items)))
|
||||
(update-in state [:shapes] dissoc id)))
|
||||
|
||||
(deftype UpdateShapeRotation [id rotation]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :rotation] rotation)))
|
||||
(defn dissoc-from-page
|
||||
"Given a shape, try to remove its reference from the
|
||||
corresponding page."
|
||||
[state {:keys [id page] :as shape}]
|
||||
(as-> (get-in state [:pages page :shapes]) $
|
||||
(into [] (remove #(= % id) $))
|
||||
(assoc-in state [:pages page :shapes] $)))
|
||||
|
||||
(defn update-rotation
|
||||
[id rotation]
|
||||
{:pre [(uuid? id)
|
||||
(number? rotation)
|
||||
(>= rotation 0)
|
||||
(>= 360 rotation)]}
|
||||
(UpdateShapeRotation. id rotation))
|
||||
(defn dissoc-from-group
|
||||
"Given a shape, try to remove its reference from the
|
||||
corresponding group (only if it belongs to one group)."
|
||||
[state {:keys [id group] :as shape}]
|
||||
(if-let [group' (get-in state [:shapes group])]
|
||||
(as-> (:items group') $
|
||||
(into [] (remove #(= % id) $))
|
||||
(assoc-in state [:shapes group :items] $))
|
||||
state))
|
||||
|
||||
;; --- Update Dimensions
|
||||
(declare dissoc-shape)
|
||||
|
||||
(deftype UpdateDimensions [id dimensions]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] geom/resize-dim dimensions)))
|
||||
(defn clear-empty-groups
|
||||
"Given the shape, try to clean all empty groups
|
||||
that this shape belongs to.
|
||||
|
||||
(s/def ::update-dimensions-opts
|
||||
(s/keys :opt-un [::width ::height]))
|
||||
The main purpose of this function is remove the
|
||||
all empty parent groups of recently removed
|
||||
shape."
|
||||
[state {:keys [group] :as shape}]
|
||||
(if-let [group' (get-in state [:shapes group])]
|
||||
(if (empty? (:items group'))
|
||||
(-> (dissoc-shape state group')
|
||||
(update-in [:workspace :selected] disj (:id group')))
|
||||
state)
|
||||
state))
|
||||
|
||||
(defn update-dimensions
|
||||
"A helper event just for update the position
|
||||
of the shape using the width and height attrs
|
||||
instread final point of coordinates."
|
||||
[id opts]
|
||||
{:pre [(uuid? id) (us/valid? ::update-dimensions-opts opts)]}
|
||||
(UpdateDimensions. id opts))
|
||||
(defn dissoc-shape
|
||||
"Given a shape, removes it from the state."
|
||||
[state shape]
|
||||
(as-> state $
|
||||
(dissoc-from-page $ shape)
|
||||
(dissoc-from-group $ shape)
|
||||
(dissoc-from-index $ shape)
|
||||
(clear-empty-groups $ shape)))
|
||||
|
||||
;; --- Update Shape Position
|
||||
;; --- Shape Movements
|
||||
|
||||
(deftype UpdateShapePosition [id point]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] geom/absolute-move point)))
|
||||
(defn- drop-at-index
|
||||
[index coll v]
|
||||
(let [[fst snd] (split-at index coll)]
|
||||
(into [] (concat fst [v] snd))))
|
||||
|
||||
(defn update-position
|
||||
"Update the start position coordenate of the shape."
|
||||
[id point]
|
||||
{:pre [(uuid? id) (gpt/point? point)]}
|
||||
(UpdateShapePosition. id point))
|
||||
(defn drop-relative
|
||||
[state loc sid]
|
||||
{:pre [(not (nil? sid))]}
|
||||
(let [shape (get-in state [:shapes (first sid)])
|
||||
{:keys [page group]} shape
|
||||
sid (:id shape)
|
||||
|
||||
;; --- Update Shape Text
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
(deftype UpdateShapeTextContent [id text]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :content] text)))
|
||||
index (case loc
|
||||
:first 0
|
||||
:after (min (- (count shapes) 1) (inc (index-of shapes sid)))
|
||||
:before (max 0 (- (index-of shapes sid) 1))
|
||||
:last (- (count shapes) 1))
|
||||
|
||||
(defn update-text
|
||||
[id text]
|
||||
{:pre [(uuid? id) (string? text)]}
|
||||
(UpdateShapeTextContent. id text))
|
||||
state (-> state
|
||||
(dissoc-from-page shape)
|
||||
(dissoc-from-group shape))
|
||||
|
||||
;; --- Update Shape Attrs
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
(declare UpdateAttrs)
|
||||
;; TODO: moved
|
||||
(deftype UpdateAttrs [id attrs]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [{:keys [type] :as shape} (get-in state [:shapes id])]
|
||||
(if (= type :group)
|
||||
(rx/from-coll (map #(UpdateAttrs. % attrs) (:items shape)))
|
||||
(rx/of #(update-in % [:shapes id] merge attrs))))))
|
||||
shapes (drop-at-index index shapes sid)]
|
||||
|
||||
(defn update-attrs
|
||||
[id attrs]
|
||||
{:pre [(uuid? id) (us/valid? ::attributes attrs)]}
|
||||
(let [atts (us/extract attrs ::attributes)]
|
||||
(UpdateAttrs. id attrs)))
|
||||
(if group
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes group :items] shapes)
|
||||
(update-in $ [:shapes sid] assoc :group group)
|
||||
(clear-empty-groups $ shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:pages page :shapes] shapes)
|
||||
(update-in $ [:shapes sid] dissoc :group)
|
||||
(clear-empty-groups $ shape)))))
|
||||
|
||||
;; --- Shape Proportions
|
||||
(defn drop-aside
|
||||
[state loc tid sid]
|
||||
{:pre [(not= tid sid)
|
||||
(not (nil? tid))
|
||||
(not (nil? sid))]}
|
||||
(let [{:keys [page group]} (get-in state [:shapes tid])
|
||||
source (get-in state [:shapes sid])
|
||||
|
||||
(deftype LockShapeProportions [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [[width height] (-> (get-in state [:shapes id])
|
||||
(geom/size)
|
||||
(keep [:width :height]))
|
||||
proportion (/ width height)]
|
||||
(update-in state [:shapes id] assoc
|
||||
:proportion proportion
|
||||
:proportion-lock true))))
|
||||
state (-> state
|
||||
(dissoc-from-page source)
|
||||
(dissoc-from-group source))
|
||||
|
||||
(defn lock-proportions
|
||||
"Mark proportions of the shape locked and save the current
|
||||
proportion as additional precalculated property."
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(LockShapeProportions. id))
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
(deftype UnlockShapeProportions [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :proportion-lock] false)))
|
||||
index (case loc
|
||||
:after (inc (index-of shapes tid))
|
||||
:before (index-of shapes tid))
|
||||
|
||||
(defn unlock-proportions
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnlockShapeProportions. id))
|
||||
shapes (drop-at-index index shapes sid)]
|
||||
(if group
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes group :items] shapes)
|
||||
(update-in $ [:shapes sid] assoc :group group)
|
||||
(clear-empty-groups $ source))
|
||||
(as-> state $
|
||||
(assoc-in $ [:pages page :shapes] shapes)
|
||||
(update-in $ [:shapes sid] dissoc :group)
|
||||
(clear-empty-groups $ source)))))
|
||||
|
||||
;; --- Group Collapsing
|
||||
(def drop-after #(drop-aside %1 :after %2 %3))
|
||||
(def drop-before #(drop-aside %1 :before %2 %3))
|
||||
|
||||
(deftype CollapseGroupShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] assoc :collapsed true)))
|
||||
|
||||
(defn collapse-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(CollapseGroupShape. id))
|
||||
|
||||
(deftype UncollapseGroupShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] assoc :collapsed false)))
|
||||
|
||||
(defn uncollapse-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UncollapseGroupShape. id))
|
||||
|
||||
;; --- Shape Visibility
|
||||
|
||||
(deftype HideShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-hidden [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :hidden] true)
|
||||
(reduce mark-hidden $ (:items shape)))
|
||||
(assoc-in state [:shapes id :hidden] true))))]
|
||||
(mark-hidden state id))))
|
||||
|
||||
(defn hide-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(HideShape. id))
|
||||
|
||||
(deftype ShowShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-visible [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :hidden] false)
|
||||
(reduce mark-visible $ (:items shape)))
|
||||
(assoc-in state [:shapes id :hidden] false))))]
|
||||
(mark-visible state id))))
|
||||
|
||||
(defn show-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(ShowShape. id))
|
||||
|
||||
;; --- Shape Blocking
|
||||
|
||||
(deftype BlockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-blocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :blocked] true)
|
||||
(reduce mark-blocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :blocked] true))))]
|
||||
(mark-blocked state id))))
|
||||
|
||||
(defn block-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(BlockShape. id))
|
||||
|
||||
(deftype UnblockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-unblocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :blocked] false)
|
||||
(reduce mark-unblocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :blocked] false))))]
|
||||
(mark-unblocked state id))))
|
||||
|
||||
(defn unblock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnblockShape. id))
|
||||
|
||||
;; --- Shape Locking
|
||||
|
||||
(deftype LockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-locked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :locked] true)
|
||||
(reduce mark-locked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :locked] true))))]
|
||||
(mark-locked state id))))
|
||||
|
||||
(defn lock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(LockShape. id))
|
||||
|
||||
(deftype UnlockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-unlocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :locked] false)
|
||||
(reduce mark-unlocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :locked] false))))]
|
||||
(mark-unlocked state id))))
|
||||
|
||||
(defn unlock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnlockShape. id))
|
||||
|
||||
;; --- Drop Shape
|
||||
|
||||
(deftype DropShape [sid tid loc]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(impl/drop-shape state sid tid loc)))
|
||||
(defn drop-inside
|
||||
[state tid sid]
|
||||
{:pre [(not= tid sid)]}
|
||||
(let [source (get-in state [:shapes sid])
|
||||
state (-> state
|
||||
(dissoc-from-page source)
|
||||
(dissoc-from-group source))
|
||||
shapes (get-in state [:shapes tid :items])]
|
||||
(if (seq shapes)
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes tid :items] (conj shapes sid))
|
||||
(update-in $ [:shapes sid] assoc :group tid))
|
||||
state)))
|
||||
|
||||
(defn drop-shape
|
||||
"Event used in drag and drop for transfer shape
|
||||
from one position to an other."
|
||||
[sid tid loc]
|
||||
{:pre [(uuid? sid)
|
||||
(uuid? tid)
|
||||
(keyword? loc)]}
|
||||
(DropShape. sid tid loc))
|
||||
[state sid tid loc]
|
||||
(if (= tid sid)
|
||||
state
|
||||
(case loc
|
||||
:inside (drop-inside state tid sid)
|
||||
:before (drop-before state tid sid)
|
||||
:after (drop-after state tid sid)
|
||||
(throw (ex-info "Invalid data" {})))))
|
||||
|
||||
;; --- Update Interaction
|
||||
(defn move-layer
|
||||
[state shape loc]
|
||||
(case loc
|
||||
:up (drop-relative state :before shape)
|
||||
:down (drop-relative state :after shape)
|
||||
:top (drop-relative state :first shape)
|
||||
:bottom (drop-relative state :last shape)
|
||||
(throw (ex-info "Invalid data" {}))))
|
||||
|
||||
(deftype UpdateInteraction [shape interaction]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (or (:id interaction)
|
||||
(uuid/random))
|
||||
data (assoc interaction :id id)]
|
||||
(assoc-in state [:shapes shape :interactions id] data))))
|
||||
;; --- Shape Selection
|
||||
|
||||
(defn update-interaction
|
||||
[shape interaction]
|
||||
(UpdateInteraction. shape interaction))
|
||||
(defn- try-match-shape
|
||||
[xf selrect acc {:keys [type id items] :as shape}]
|
||||
(cond
|
||||
(geom/contained-in? shape selrect)
|
||||
(conj acc id)
|
||||
|
||||
;; --- Delete Interaction
|
||||
(geom/overlaps? shape selrect)
|
||||
(conj acc id)
|
||||
|
||||
(deftype DeleteInteracton [shape id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes shape :interactions] dissoc id)))
|
||||
(:locked shape)
|
||||
acc
|
||||
|
||||
(defn delete-interaction
|
||||
[shape id]
|
||||
{:pre [(uuid? id) (uuid? shape)]}
|
||||
(DeleteInteracton. shape id))
|
||||
(= type :group)
|
||||
(reduce (partial try-match-shape xf selrect)
|
||||
acc (sequence xf items))
|
||||
|
||||
;; --- Path Modifications
|
||||
:else
|
||||
acc))
|
||||
|
||||
(deftype UpdatePath [id index delta]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id :segments index] gpt/add delta)))
|
||||
(defn match-by-selrect
|
||||
[state page-id selrect]
|
||||
(let [xf (comp (map #(get-in state [:shapes %]))
|
||||
(remove :hidden)
|
||||
(remove :blocked)
|
||||
(remove #(= :canvas (:type %)))
|
||||
(map geom/selection-rect))
|
||||
match (partial try-match-shape xf selrect)
|
||||
shapes (get-in state [:pages page-id :shapes])]
|
||||
(reduce match #{} (sequence xf shapes))))
|
||||
|
||||
(defn update-path
|
||||
"Update a concrete point in the path shape."
|
||||
[id index delta]
|
||||
{:pre [(uuid? id) (number? index) (gpt/point? delta)]}
|
||||
(UpdatePath. id index delta))
|
||||
(defn group-shapes
|
||||
[state shapes page]
|
||||
(letfn [(replace-first-item [pred coll replacement]
|
||||
(into []
|
||||
(concat
|
||||
(take-while #(not (pred %)) coll)
|
||||
[replacement]
|
||||
(drop 1 (drop-while #(not (pred %)) coll)))))
|
||||
|
||||
(deftype InitialPathPointAlign [id index]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [shape (get-in state [:shapes id])
|
||||
point (get-in shape [:segments index])]
|
||||
(->> (uwrk/align-point point)
|
||||
(rx/map #(update-path id index %))))))
|
||||
(move-shapes-to-new-group [state page shapes new-group]
|
||||
(reduce (fn [state {:keys [id group] :as shape}]
|
||||
(-> state
|
||||
(update-in [:shapes group :items] #(remove (set [id]) %))
|
||||
(update-in [:pages page :shapes] #(remove (set [id]) %))
|
||||
(clear-empty-groups shape)
|
||||
(assoc-in [:shapes id :group] new-group)
|
||||
))
|
||||
state
|
||||
shapes))
|
||||
|
||||
(defn initial-path-point-align
|
||||
"Event responsible of align a specified point of the
|
||||
shape by `index` with the grid."
|
||||
[id index]
|
||||
{:pre [(uuid? id)
|
||||
(number? index)
|
||||
(not (neg? index))]}
|
||||
(InitialPathPointAlign. id index))
|
||||
(update-shapes-on-page [state page shapes group]
|
||||
(as-> (get-in state [:pages page :shapes]) $
|
||||
(replace-first-item (set shapes) $ group)
|
||||
(remove (set shapes) $)
|
||||
(into [] $)
|
||||
(assoc-in state [:pages page :shapes] $)))
|
||||
|
||||
;; --- Events (implicit) (for selected)
|
||||
(update-shapes-on-group [state parent-group shapes group]
|
||||
(as-> (get-in state [:shapes parent-group :items]) $
|
||||
(replace-first-item (set shapes) $ group)
|
||||
(remove (set shapes) $)
|
||||
(into [] $)
|
||||
(assoc-in state [:shapes parent-group :items] $)))
|
||||
|
||||
;; NOTE: moved to workspace
|
||||
(deftype DeselectAll []
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace :selected] #{}))
|
||||
(update-shapes-on-index [state shapes group]
|
||||
(reduce (fn [state {:keys [id] :as shape}]
|
||||
(as-> shape $
|
||||
(assoc $ :group group)
|
||||
(assoc-in state [:shapes id] $)))
|
||||
state
|
||||
shapes))]
|
||||
(let [sid (uuid/random)
|
||||
shapes' (map #(get-in state [:shapes %]) shapes)
|
||||
distinct-groups (distinct (map :group shapes'))
|
||||
parent-group (cond
|
||||
(not= 1 (count distinct-groups)) :multi
|
||||
(nil? (first distinct-groups)) :page
|
||||
:else (first distinct-groups))
|
||||
name (generate-unique-name state "Group")
|
||||
group {:type :group
|
||||
:name name
|
||||
:items (into [] shapes)
|
||||
:id sid
|
||||
:page page}]
|
||||
(as-> state $
|
||||
(update-shapes-on-index $ shapes' sid)
|
||||
(cond
|
||||
(= :multi parent-group)
|
||||
(-> $
|
||||
(move-shapes-to-new-group page shapes' sid)
|
||||
(update-in [:pages page :shapes] #(into [] (cons sid %))))
|
||||
(= :page parent-group)
|
||||
(update-shapes-on-page $ page shapes sid)
|
||||
:else
|
||||
(update-shapes-on-group $ parent-group shapes sid))
|
||||
(update $ :shapes assoc sid group)
|
||||
(cond
|
||||
(= :multi parent-group) $
|
||||
(= :page parent-group) $
|
||||
:else (assoc-in $ [:shapes sid :group] parent-group))
|
||||
(update $ :workspace assoc :selected #{sid})))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/just ::uev/interrupt)))
|
||||
(defn degroup-shapes
|
||||
[state shapes page-id]
|
||||
(letfn [(get-relocation-position [state {id :id parent-id :group}]
|
||||
(if (nil? parent-id)
|
||||
(index-of (get-in state [:pages page-id :shapes]) id)
|
||||
(index-of (get-in state [:shapes parent-id :items]) id)))
|
||||
|
||||
(defn deselect-all
|
||||
"Clear all possible state of drawing, edition
|
||||
or any similar action taken by the user."
|
||||
[]
|
||||
(DeselectAll.))
|
||||
(relocate-shape [state shape-id parent-id position]
|
||||
(if (nil? parent-id)
|
||||
(-> state
|
||||
(update-in [:pages page-id :shapes] #(drop-at-index position % shape-id))
|
||||
(update-in [:shapes shape-id] dissoc :group))
|
||||
(-> state
|
||||
(update-in [:shapes parent-id :items] #(drop-at-index position % shape-id))
|
||||
(assoc-in [:shapes shape-id :group] parent-id))))
|
||||
|
||||
;; --- Group Selected Shapes
|
||||
(remove-group [state {id :id parent-id :group}]
|
||||
(let [xform (remove #{id})]
|
||||
(as-> state $
|
||||
(update $ :shapes dissoc id)
|
||||
(if (nil? parent-id)
|
||||
(update-in $ [:pages page-id :shapes] #(into [] xform %))
|
||||
(update-in $ [:shapes parent-id :items] #(into [] xform %))))))
|
||||
|
||||
(deftype GroupSelectedShapes []
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace :page])
|
||||
selected (get-in state [:workspace :selected])]
|
||||
(assert (not (empty? selected)) "selected set is empty")
|
||||
(assert (uuid? id) "selected page is not an uuid")
|
||||
(impl/group-shapes state selected id))))
|
||||
(relocate-group-items [state {id :id parent-id :group items :items :as group}]
|
||||
(let [position (get-relocation-position state group)]
|
||||
(as-> state $
|
||||
(reduce #(relocate-shape %1 %2 parent-id position) $ (reverse items))
|
||||
(remove-group $ group))))
|
||||
|
||||
(defn group-selected
|
||||
[]
|
||||
(GroupSelectedShapes.))
|
||||
(select-degrouped [state groups]
|
||||
(let [items (into #{} (mapcat :items groups))]
|
||||
(assoc-in state [:workspace :selected] items)))
|
||||
|
||||
;; --- Ungroup Selected Shapes
|
||||
(remove-from-parent [state id parent-id]
|
||||
(assert (not (nil? parent-id)) "parent-id should never be nil here")
|
||||
(update-in state [:shapes parent-id :items] #(into [] (remove #{id}) %)))
|
||||
|
||||
(deftype UngroupSelectedShapes []
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace :page])
|
||||
selected (get-in state [:workspace :selected])]
|
||||
(assert (not (empty? selected)) "selected set is empty")
|
||||
(assert (uuid? id) "selected page is not an uuid")
|
||||
(impl/degroup-shapes state selected id))))
|
||||
(strip-empty-groups [state parent-id]
|
||||
(if (nil? parent-id)
|
||||
state
|
||||
(let [group (get-in state [:shapes parent-id])]
|
||||
(if (empty? (:items group))
|
||||
(-> state
|
||||
(remove-group group)
|
||||
(strip-empty-groups (:group group)))
|
||||
state))))
|
||||
|
||||
(defn ungroup-selected
|
||||
[]
|
||||
(UngroupSelectedShapes.))
|
||||
(selective-degroup [state [shape & rest :as shapes]]
|
||||
(let [group (get-in state [:shapes (:group shape)])
|
||||
position (get-relocation-position state group)
|
||||
parent-id (:group group)]
|
||||
(as-> state $
|
||||
(assoc-in $ [:workspace :selected] (into #{} (map :id shapes)))
|
||||
(reduce (fn [state {shape-id :id}]
|
||||
(-> state
|
||||
(relocate-shape shape-id parent-id position)
|
||||
(remove-from-parent shape-id (:id group))))
|
||||
$ (reverse shapes))
|
||||
(strip-empty-groups $ (:id group)))))]
|
||||
|
||||
;; --- Duplicate Selected
|
||||
(let [shapes (into #{} (map #(get-in state [:shapes %])) shapes)
|
||||
groups (into #{} (filter #(= (:type %) :group)) shapes)
|
||||
parents (into #{} (map :group) shapes)]
|
||||
(cond
|
||||
(and (= (count shapes) (count groups))
|
||||
(= 1 (count parents))
|
||||
(not (empty? groups)))
|
||||
(as-> state $
|
||||
(reduce relocate-group-items $ groups)
|
||||
(reduce remove-group $ groups)
|
||||
(select-degrouped $ groups))
|
||||
|
||||
(deftype DuplicateSelected []
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [selected (get-in state [:workspace :selected])]
|
||||
(impl/duplicate-shapes state selected))))
|
||||
|
||||
(defn duplicate-selected
|
||||
[]
|
||||
(DuplicateSelected.))
|
||||
(and (empty? groups)
|
||||
(= 1 (count parents))
|
||||
(not (nil? (first parents))))
|
||||
(selective-degroup state shapes)
|
||||
|
||||
:else
|
||||
(throw (ex-info "invalid condition for degrouping" {}))))))
|
||||
|
||||
(defn materialize-xfmt
|
||||
[state id xfmt]
|
||||
(let [{:keys [type items] :as shape} (get-in state [:shapes id])]
|
||||
(if (= type :group)
|
||||
(reduce #(materialize-xfmt %1 %2 xfmt) state items)
|
||||
(update-in state [:shapes id] geom/transform xfmt))))
|
||||
|
|
|
@ -1,470 +0,0 @@
|
|||
;; 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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.shapes-impl
|
||||
(:require [lentes.core :as l]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.uuid :as uuid]
|
||||
[uxbox.util.data :refer (index-of)]))
|
||||
|
||||
;; --- Shape Creation
|
||||
|
||||
(defn retrieve-used-names
|
||||
"Returns a set of already used names by shapes
|
||||
in the current page."
|
||||
[{:keys [shapes] :as state}]
|
||||
(let [page (l/focus ul/selected-page state)
|
||||
xform (comp (map second)
|
||||
(filter #(= page (:page %)))
|
||||
(map :name))]
|
||||
(into #{} xform shapes)))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator based on the previous
|
||||
state of the used names."
|
||||
[state basename]
|
||||
(let [used (retrieve-used-names state)]
|
||||
(loop [counter 1]
|
||||
(let [candidate (str basename "-" counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate)))))
|
||||
|
||||
(defn assoc-shape-to-page
|
||||
[state shape page]
|
||||
(let [shape-id (uuid/random)
|
||||
shape-name (generate-unique-name state (:name shape))
|
||||
shape (assoc shape
|
||||
:page page
|
||||
:id shape-id
|
||||
:name shape-name)]
|
||||
(-> state
|
||||
(update-in [:pages page :shapes] #(into [] (cons shape-id %)))
|
||||
(assoc-in [:shapes shape-id] shape))))
|
||||
|
||||
(defn duplicate-shapes'
|
||||
([state shapes page]
|
||||
(duplicate-shapes' state shapes page nil))
|
||||
([state shapes page group]
|
||||
(letfn [(duplicate-shape [state shape page group]
|
||||
(if (= (:type shape) :group)
|
||||
(let [id (uuid/random)
|
||||
items (:items shape)
|
||||
name (generate-unique-name state (str (:name shape) "-copy"))
|
||||
shape (assoc shape
|
||||
:id id
|
||||
:page page
|
||||
:items []
|
||||
:name name)
|
||||
state (if (nil? group)
|
||||
(-> state
|
||||
(update-in [:pages page :shapes]
|
||||
#(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))
|
||||
(-> state
|
||||
(update-in [:shapes group :items]
|
||||
#(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape)))]
|
||||
(->> (map #(get-in state [:shapes %]) items)
|
||||
(reverse)
|
||||
(reduce #(duplicate-shape %1 %2 page id) state)))
|
||||
(let [id (uuid/random)
|
||||
name (generate-unique-name state (str (:name shape) "-copy"))
|
||||
shape (-> (dissoc shape :group)
|
||||
(assoc :id id :page page :name name)
|
||||
(merge (when group {:group group})))]
|
||||
(if (nil? group)
|
||||
(-> state
|
||||
(update-in [:pages page :shapes] #(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))
|
||||
(-> state
|
||||
(update-in [:shapes group :items] #(into [] (cons id %)))
|
||||
(assoc-in [:shapes id] shape))))))]
|
||||
(reduce #(duplicate-shape %1 %2 page group) state shapes))))
|
||||
|
||||
(defn duplicate-shapes
|
||||
([state shapes]
|
||||
(duplicate-shapes state shapes nil))
|
||||
([state shapes page]
|
||||
(letfn [(all-toplevel? [coll]
|
||||
(every? #(nil? (:group %)) coll))
|
||||
(all-same-group? [coll]
|
||||
(let [group (:group (first coll))]
|
||||
(every? #(= group (:group %)) coll)))]
|
||||
(let [shapes (reverse (mapv #(get-in state [:shapes %]) shapes))]
|
||||
(cond
|
||||
(all-toplevel? shapes)
|
||||
(let [page (or page (:page (first shapes)))]
|
||||
(duplicate-shapes' state shapes page))
|
||||
|
||||
(all-same-group? shapes)
|
||||
(let [page (or page (:page (first shapes)))
|
||||
group (:group (first shapes))]
|
||||
(duplicate-shapes' state shapes page group))
|
||||
|
||||
:else
|
||||
(let [page (or page (:page (first shapes)))]
|
||||
(duplicate-shapes' state shapes page)))))))
|
||||
|
||||
;; --- Delete Shapes
|
||||
|
||||
(defn dissoc-from-index
|
||||
"A function that dissoc shape from the indexed
|
||||
data structure of shapes from the state."
|
||||
[state {:keys [id type] :as shape}]
|
||||
(if (= :group type)
|
||||
(let [items (map #(get-in state [:shapes %]) (:items shape))]
|
||||
(as-> state $
|
||||
(update-in $ [:shapes] dissoc id)
|
||||
(reduce dissoc-from-index $ items)))
|
||||
(update-in state [:shapes] dissoc id)))
|
||||
|
||||
(defn dissoc-from-page
|
||||
"Given a shape, try to remove its reference from the
|
||||
corresponding page."
|
||||
[state {:keys [id page] :as shape}]
|
||||
(as-> (get-in state [:pages page :shapes]) $
|
||||
(into [] (remove #(= % id) $))
|
||||
(assoc-in state [:pages page :shapes] $)))
|
||||
|
||||
(defn dissoc-from-group
|
||||
"Given a shape, try to remove its reference from the
|
||||
corresponding group (only if it belongs to one group)."
|
||||
[state {:keys [id group] :as shape}]
|
||||
(if-let [group' (get-in state [:shapes group])]
|
||||
(as-> (:items group') $
|
||||
(into [] (remove #(= % id) $))
|
||||
(assoc-in state [:shapes group :items] $))
|
||||
state))
|
||||
|
||||
(declare dissoc-shape)
|
||||
|
||||
(defn clear-empty-groups
|
||||
"Given the shape, try to clean all empty groups
|
||||
that this shape belongs to.
|
||||
|
||||
The main purpose of this function is remove the
|
||||
all empty parent groups of recently removed
|
||||
shape."
|
||||
[state {:keys [group] :as shape}]
|
||||
(if-let [group' (get-in state [:shapes group])]
|
||||
(if (empty? (:items group'))
|
||||
(-> (dissoc-shape state group')
|
||||
(update-in [:workspace :selected] disj (:id group')))
|
||||
state)
|
||||
state))
|
||||
|
||||
(defn dissoc-shape
|
||||
"Given a shape, removes it from the state."
|
||||
[state shape]
|
||||
(as-> state $
|
||||
(dissoc-from-page $ shape)
|
||||
(dissoc-from-group $ shape)
|
||||
(dissoc-from-index $ shape)
|
||||
(clear-empty-groups $ shape)))
|
||||
|
||||
;; --- Shape Movements
|
||||
|
||||
(defn- drop-at-index
|
||||
[index coll v]
|
||||
(let [[fst snd] (split-at index coll)]
|
||||
(into [] (concat fst [v] snd))))
|
||||
|
||||
(defn drop-relative
|
||||
[state loc sid]
|
||||
{:pre [(not (nil? sid))]}
|
||||
(let [shape (get-in state [:shapes (first sid)])
|
||||
{:keys [page group]} shape
|
||||
sid (:id shape)
|
||||
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
index (case loc
|
||||
:first 0
|
||||
:after (min (- (count shapes) 1) (inc (index-of shapes sid)))
|
||||
:before (max 0 (- (index-of shapes sid) 1))
|
||||
:last (- (count shapes) 1))
|
||||
|
||||
state (-> state
|
||||
(dissoc-from-page shape)
|
||||
(dissoc-from-group shape))
|
||||
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
shapes (drop-at-index index shapes sid)]
|
||||
|
||||
(if group
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes group :items] shapes)
|
||||
(update-in $ [:shapes sid] assoc :group group)
|
||||
(clear-empty-groups $ shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:pages page :shapes] shapes)
|
||||
(update-in $ [:shapes sid] dissoc :group)
|
||||
(clear-empty-groups $ shape)))))
|
||||
|
||||
(defn drop-aside
|
||||
[state loc tid sid]
|
||||
{:pre [(not= tid sid)
|
||||
(not (nil? tid))
|
||||
(not (nil? sid))]}
|
||||
(let [{:keys [page group]} (get-in state [:shapes tid])
|
||||
source (get-in state [:shapes sid])
|
||||
|
||||
state (-> state
|
||||
(dissoc-from-page source)
|
||||
(dissoc-from-group source))
|
||||
|
||||
shapes (if group
|
||||
(get-in state [:shapes group :items])
|
||||
(get-in state [:pages page :shapes]))
|
||||
|
||||
index (case loc
|
||||
:after (inc (index-of shapes tid))
|
||||
:before (index-of shapes tid))
|
||||
|
||||
shapes (drop-at-index index shapes sid)]
|
||||
(if group
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes group :items] shapes)
|
||||
(update-in $ [:shapes sid] assoc :group group)
|
||||
(clear-empty-groups $ source))
|
||||
(as-> state $
|
||||
(assoc-in $ [:pages page :shapes] shapes)
|
||||
(update-in $ [:shapes sid] dissoc :group)
|
||||
(clear-empty-groups $ source)))))
|
||||
|
||||
(def drop-after #(drop-aside %1 :after %2 %3))
|
||||
(def drop-before #(drop-aside %1 :before %2 %3))
|
||||
|
||||
(defn drop-inside
|
||||
[state tid sid]
|
||||
{:pre [(not= tid sid)]}
|
||||
(let [source (get-in state [:shapes sid])
|
||||
state (-> state
|
||||
(dissoc-from-page source)
|
||||
(dissoc-from-group source))
|
||||
shapes (get-in state [:shapes tid :items])]
|
||||
(if (seq shapes)
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes tid :items] (conj shapes sid))
|
||||
(update-in $ [:shapes sid] assoc :group tid))
|
||||
state)))
|
||||
|
||||
(defn drop-shape
|
||||
[state sid tid loc]
|
||||
(if (= tid sid)
|
||||
state
|
||||
(case loc
|
||||
:inside (drop-inside state tid sid)
|
||||
:before (drop-before state tid sid)
|
||||
:after (drop-after state tid sid)
|
||||
(throw (ex-info "Invalid data" {})))))
|
||||
|
||||
(defn move-layer
|
||||
[state shape loc]
|
||||
(case loc
|
||||
:up (drop-relative state :before shape)
|
||||
:down (drop-relative state :after shape)
|
||||
:top (drop-relative state :first shape)
|
||||
:bottom (drop-relative state :last shape)
|
||||
(throw (ex-info "Invalid data" {}))))
|
||||
|
||||
;; --- Shape Selection
|
||||
|
||||
(defn- try-match-shape
|
||||
[xf selrect acc {:keys [type id items] :as shape}]
|
||||
(cond
|
||||
(geom/contained-in? shape selrect)
|
||||
(conj acc id)
|
||||
|
||||
(geom/overlaps? shape selrect)
|
||||
(conj acc id)
|
||||
|
||||
(:locked shape)
|
||||
acc
|
||||
|
||||
(= type :group)
|
||||
(reduce (partial try-match-shape xf selrect)
|
||||
acc (sequence xf items))
|
||||
|
||||
:else
|
||||
acc))
|
||||
|
||||
(defn match-by-selrect
|
||||
[state page-id selrect]
|
||||
(let [xf (comp (map #(get-in state [:shapes %]))
|
||||
(remove :hidden)
|
||||
(remove :blocked)
|
||||
(map geom/selection-rect))
|
||||
match (partial try-match-shape xf selrect)
|
||||
shapes (get-in state [:pages page-id :shapes])]
|
||||
(reduce match #{} (sequence xf shapes))))
|
||||
|
||||
(defn group-shapes
|
||||
[state shapes page]
|
||||
(letfn [(replace-first-item [pred coll replacement]
|
||||
(into []
|
||||
(concat
|
||||
(take-while #(not (pred %)) coll)
|
||||
[replacement]
|
||||
(drop 1 (drop-while #(not (pred %)) coll)))))
|
||||
|
||||
(move-shapes-to-new-group [state page shapes new-group]
|
||||
(reduce (fn [state {:keys [id group] :as shape}]
|
||||
(-> state
|
||||
(update-in [:shapes group :items] #(remove (set [id]) %))
|
||||
(update-in [:pages page :shapes] #(remove (set [id]) %))
|
||||
(clear-empty-groups shape)
|
||||
(assoc-in [:shapes id :group] new-group)
|
||||
))
|
||||
state
|
||||
shapes))
|
||||
|
||||
(update-shapes-on-page [state page shapes group]
|
||||
(as-> (get-in state [:pages page :shapes]) $
|
||||
(replace-first-item (set shapes) $ group)
|
||||
(remove (set shapes) $)
|
||||
(into [] $)
|
||||
(assoc-in state [:pages page :shapes] $)))
|
||||
|
||||
(update-shapes-on-group [state parent-group shapes group]
|
||||
(as-> (get-in state [:shapes parent-group :items]) $
|
||||
(replace-first-item (set shapes) $ group)
|
||||
(remove (set shapes) $)
|
||||
(into [] $)
|
||||
(assoc-in state [:shapes parent-group :items] $)))
|
||||
|
||||
(update-shapes-on-index [state shapes group]
|
||||
(reduce (fn [state {:keys [id] :as shape}]
|
||||
(as-> shape $
|
||||
(assoc $ :group group)
|
||||
(assoc-in state [:shapes id] $)))
|
||||
state
|
||||
shapes))]
|
||||
(let [sid (uuid/random)
|
||||
shapes' (map #(get-in state [:shapes %]) shapes)
|
||||
distinct-groups (distinct (map :group shapes'))
|
||||
parent-group (cond
|
||||
(not= 1 (count distinct-groups)) :multi
|
||||
(nil? (first distinct-groups)) :page
|
||||
:else (first distinct-groups))
|
||||
name (generate-unique-name state "Group")
|
||||
group {:type :group
|
||||
:name name
|
||||
:items (into [] shapes)
|
||||
:id sid
|
||||
:page page}]
|
||||
(as-> state $
|
||||
(update-shapes-on-index $ shapes' sid)
|
||||
(cond
|
||||
(= :multi parent-group)
|
||||
(-> $
|
||||
(move-shapes-to-new-group page shapes' sid)
|
||||
(update-in [:pages page :shapes] #(into [] (cons sid %))))
|
||||
(= :page parent-group)
|
||||
(update-shapes-on-page $ page shapes sid)
|
||||
:else
|
||||
(update-shapes-on-group $ parent-group shapes sid))
|
||||
(update $ :shapes assoc sid group)
|
||||
(cond
|
||||
(= :multi parent-group) $
|
||||
(= :page parent-group) $
|
||||
:else (assoc-in $ [:shapes sid :group] parent-group))
|
||||
(update $ :workspace assoc :selected #{sid})))))
|
||||
|
||||
(defn degroup-shapes
|
||||
[state shapes page-id]
|
||||
(letfn [(get-relocation-position [state {id :id parent-id :group}]
|
||||
(if (nil? parent-id)
|
||||
(index-of (get-in state [:pages page-id :shapes]) id)
|
||||
(index-of (get-in state [:shapes parent-id :items]) id)))
|
||||
|
||||
(relocate-shape [state shape-id parent-id position]
|
||||
(if (nil? parent-id)
|
||||
(-> state
|
||||
(update-in [:pages page-id :shapes] #(drop-at-index position % shape-id))
|
||||
(update-in [:shapes shape-id] dissoc :group))
|
||||
(-> state
|
||||
(update-in [:shapes parent-id :items] #(drop-at-index position % shape-id))
|
||||
(assoc-in [:shapes shape-id :group] parent-id))))
|
||||
|
||||
(remove-group [state {id :id parent-id :group}]
|
||||
(let [xform (remove #{id})]
|
||||
(as-> state $
|
||||
(update $ :shapes dissoc id)
|
||||
(if (nil? parent-id)
|
||||
(update-in $ [:pages page-id :shapes] #(into [] xform %))
|
||||
(update-in $ [:shapes parent-id :items] #(into [] xform %))))))
|
||||
|
||||
(relocate-group-items [state {id :id parent-id :group items :items :as group}]
|
||||
(let [position (get-relocation-position state group)]
|
||||
(as-> state $
|
||||
(reduce #(relocate-shape %1 %2 parent-id position) $ (reverse items))
|
||||
(remove-group $ group))))
|
||||
|
||||
(select-degrouped [state groups]
|
||||
(let [items (into #{} (mapcat :items groups))]
|
||||
(assoc-in state [:workspace :selected] items)))
|
||||
|
||||
(remove-from-parent [state id parent-id]
|
||||
(assert (not (nil? parent-id)) "parent-id should never be nil here")
|
||||
(update-in state [:shapes parent-id :items] #(into [] (remove #{id}) %)))
|
||||
|
||||
(strip-empty-groups [state parent-id]
|
||||
(if (nil? parent-id)
|
||||
state
|
||||
(let [group (get-in state [:shapes parent-id])]
|
||||
(if (empty? (:items group))
|
||||
(-> state
|
||||
(remove-group group)
|
||||
(strip-empty-groups (:group group)))
|
||||
state))))
|
||||
|
||||
(selective-degroup [state [shape & rest :as shapes]]
|
||||
(let [group (get-in state [:shapes (:group shape)])
|
||||
position (get-relocation-position state group)
|
||||
parent-id (:group group)]
|
||||
(as-> state $
|
||||
(assoc-in $ [:workspace :selected] (into #{} (map :id shapes)))
|
||||
(reduce (fn [state {shape-id :id}]
|
||||
(-> state
|
||||
(relocate-shape shape-id parent-id position)
|
||||
(remove-from-parent shape-id (:id group))))
|
||||
$ (reverse shapes))
|
||||
(strip-empty-groups $ (:id group)))))]
|
||||
(let [shapes (into #{} (map #(get-in state [:shapes %])) shapes)
|
||||
groups (into #{} (filter #(= (:type %) :group)) shapes)
|
||||
parents (into #{} (map :group) shapes)]
|
||||
(cond
|
||||
(and (= (count shapes) (count groups))
|
||||
(= 1 (count parents))
|
||||
(not (empty? groups)))
|
||||
(as-> state $
|
||||
(reduce relocate-group-items $ groups)
|
||||
(reduce remove-group $ groups)
|
||||
(select-degrouped $ groups))
|
||||
|
||||
(and (empty? groups)
|
||||
(= 1 (count parents))
|
||||
(not (nil? (first parents))))
|
||||
(selective-degroup state shapes)
|
||||
|
||||
:else
|
||||
(throw (ex-info "invalid condition for degrouping" {}))))))
|
||||
|
||||
(defn materialize-xfmt
|
||||
[state id xfmt]
|
||||
(let [{:keys [type items] :as shape} (get-in state [:shapes id])]
|
||||
(if (= type :group)
|
||||
(reduce #(materialize-xfmt %1 %2 xfmt) state items)
|
||||
(update-in state [:shapes id] geom/transform xfmt))))
|
|
@ -21,7 +21,8 @@
|
|||
(deftype WatchPageChanges [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream
|
||||
nil
|
||||
#_(let [stopper (->> stream
|
||||
(rx/filter #(= % ::udp/stop-page-watcher))
|
||||
(rx/take 1))]
|
||||
(->> stream
|
||||
|
@ -41,7 +42,8 @@
|
|||
(defrecord SaveUndoEntry [id]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page (udp/pack-page state id)
|
||||
state
|
||||
#_(let [page (udp/pack-page state id)
|
||||
undo {:data (:data page)
|
||||
:metadata (:metadata page)}]
|
||||
(-> state
|
||||
|
@ -88,7 +90,7 @@
|
|||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (get-in state [:workspace :page])
|
||||
#_(let [page-id (get-in state [:workspace :page])
|
||||
undo-state (get-in state [:undo page-id])
|
||||
stack (:stack undo-state)
|
||||
selected (:selected undo-state 0)]
|
||||
|
@ -123,7 +125,7 @@
|
|||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (get-in state [:workspace :page])
|
||||
#_(let [page-id (get-in state [:workspace :page])
|
||||
undo-state (get-in state [:undo page-id])
|
||||
stack (:stack undo-state)
|
||||
selected (:selected undo-state)]
|
||||
|
|
|
@ -2,129 +2,121 @@
|
|||
;; 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) 2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.users
|
||||
(:require
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.util.i18n :as i18n :refer (tr)]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.util.messages :as uum]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.storage :refer [storage]]))
|
||||
|
||||
;; --- Profile Fetched
|
||||
;; --- Common Specs
|
||||
|
||||
(deftype ProfileFetched [data]
|
||||
ptk/UpdateEvent
|
||||
(update [this state]
|
||||
(assoc state :profile data))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [this state stream]
|
||||
(swap! storage assoc :profile data)
|
||||
;; (prn "profile-fetched" data)
|
||||
(when-let [lang (get-in data [:metadata :language])]
|
||||
(i18n/set-current-locale! lang))))
|
||||
|
||||
(defn profile-fetched
|
||||
[data]
|
||||
(ProfileFetched. data))
|
||||
|
||||
;; --- Fetch Profile
|
||||
|
||||
(deftype FetchProfile []
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :fetch/profile)
|
||||
(rx/map :payload)
|
||||
(rx/map profile-fetched))))
|
||||
|
||||
(defn fetch-profile
|
||||
[]
|
||||
(FetchProfile.))
|
||||
|
||||
;; --- Profile Updated
|
||||
|
||||
(deftype ProfileUpdated [data]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (profile-fetched data)
|
||||
(uum/info (tr "settings.profile.profile-saved")))))
|
||||
|
||||
(defn profile-updated
|
||||
[data]
|
||||
(ProfileUpdated. data))
|
||||
|
||||
;; --- Update Profile
|
||||
|
||||
(deftype UpdateProfile [data on-success on-error]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(letfn [(handle-error [{payload :payload}]
|
||||
(on-error payload)
|
||||
(rx/empty))]
|
||||
(let [data (-> (:profile state)
|
||||
(assoc :fullname (:fullname data))
|
||||
(assoc :email (:email data))
|
||||
(assoc :username (:username data))
|
||||
(assoc-in [:metadata :language] (:language data)))]
|
||||
(prn "update-profile" data)
|
||||
(->> (rp/req :update/profile data)
|
||||
(rx/map :payload)
|
||||
(rx/do on-success)
|
||||
(rx/map profile-updated)
|
||||
(rx/catch rp/client-error? handle-error))))))
|
||||
|
||||
(s/def ::fullname string?)
|
||||
(s/def ::email us/email?)
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::username string?)
|
||||
(s/def ::fullname string?)
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::password string?)
|
||||
(s/def ::language string?)
|
||||
|
||||
(s/def ::update-profile
|
||||
(s/keys :req-un [::fullname
|
||||
::email
|
||||
::language
|
||||
::username]))
|
||||
|
||||
(defn update-profile
|
||||
[data on-success on-error]
|
||||
{:pre [(us/valid? ::update-profile data)
|
||||
(fn? on-error)
|
||||
(fn? on-success)]}
|
||||
(UpdateProfile. data on-success on-error))
|
||||
|
||||
;; --- Update Password (Form)
|
||||
|
||||
(deftype UpdatePassword [data on-success on-error]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [params {:old-password (:password-old data)
|
||||
:password (:password-1 data)}]
|
||||
(->> (rp/req :update/profile-password params)
|
||||
(rx/catch rp/client-error? (fn [e]
|
||||
(on-error (:payload e))
|
||||
(rx/empty)))
|
||||
(rx/do on-success)
|
||||
(rx/ignore)))))
|
||||
|
||||
(s/def ::photo string?)
|
||||
(s/def ::created-at inst?)
|
||||
(s/def ::password-1 string?)
|
||||
(s/def ::password-2 string?)
|
||||
(s/def ::password-old string?)
|
||||
|
||||
(s/def ::update-password
|
||||
;; --- Profile Fetched
|
||||
|
||||
(s/def ::profile-fetched-params
|
||||
(s/keys :req-un [::id
|
||||
::username
|
||||
::fullname
|
||||
::email
|
||||
::created-at
|
||||
::photo]))
|
||||
|
||||
(defn profile-fetched
|
||||
[data]
|
||||
(s/assert ::profile-fetched-params data)
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :profile data))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state stream]
|
||||
(swap! storage assoc :profile data)
|
||||
(when-let [lang (get-in data [:metadata :language])]
|
||||
(i18n/set-current-locale! lang)))))
|
||||
|
||||
;; --- Fetch Profile
|
||||
|
||||
(def fetch-profile
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :fetch/profile)
|
||||
(rx/map :payload)
|
||||
(rx/map profile-fetched)))))
|
||||
|
||||
;; --- Update Profile
|
||||
|
||||
(s/def ::update-profile-params
|
||||
(s/keys :req-un [::fullname
|
||||
::email
|
||||
::username
|
||||
::language]))
|
||||
|
||||
(defn form->update-profile
|
||||
[data on-success on-error]
|
||||
(s/assert ::update-profile-params data)
|
||||
(s/assert ::us/fn on-error)
|
||||
(s/assert ::us/fn on-success)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(letfn [(handle-error [{payload :payload}]
|
||||
(on-error payload)
|
||||
(rx/empty))]
|
||||
(let [data (-> (:profile state)
|
||||
(assoc :fullname (:fullname data))
|
||||
(assoc :email (:email data))
|
||||
(assoc :username (:username data))
|
||||
(assoc-in [:metadata :language] (:language data)))]
|
||||
(->> (rp/req :update/profile data)
|
||||
(rx/map :payload)
|
||||
(rx/do on-success)
|
||||
(rx/map profile-fetched)
|
||||
(rx/catch rp/client-error? handle-error)))))))
|
||||
|
||||
;; --- Update Password (Form)
|
||||
|
||||
(s/def ::update-password-params
|
||||
(s/keys :req-un [::password-1
|
||||
::password-2
|
||||
::password-old]))
|
||||
|
||||
(defn update-password
|
||||
[data & {:keys [on-success on-error]}]
|
||||
{:pre [(us/valid? ::update-password data)
|
||||
(fn? on-success)
|
||||
(fn? on-error)]}
|
||||
(UpdatePassword. data on-success on-error))
|
||||
[data {:keys [on-success on-error]}]
|
||||
(s/assert ::update-password-params data)
|
||||
(s/assert ::us/fn on-success)
|
||||
(s/assert ::us/fn on-error)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [params {:old-password (:password-old data)
|
||||
:password (:password-1 data)}]
|
||||
(->> (rp/req :update/profile-password params)
|
||||
(rx/catch rp/client-error? (fn [e]
|
||||
(on-error (:payload e))
|
||||
(rx/empty)))
|
||||
(rx/do on-success)
|
||||
(rx/ignore))))))
|
||||
|
||||
|
||||
;; --- Update Photo
|
||||
|
||||
|
@ -133,7 +125,7 @@
|
|||
(watch [_ state stream]
|
||||
(->> (rp/req :update/profile-photo {:file file})
|
||||
(rx/do done)
|
||||
(rx/map fetch-profile))))
|
||||
(rx/map (constantly fetch-profile)))))
|
||||
|
||||
(defn update-photo
|
||||
([file] (update-photo file (constantly nil)))
|
||||
|
|
|
@ -13,19 +13,14 @@
|
|||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.history :as udh]
|
||||
[uxbox.main.data.icons :as udi]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.data.projects :as dp]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.shapes-impl :as simpl]
|
||||
[uxbox.main.data.workspace.ruler :as wruler]
|
||||
[uxbox.main.data.shapes :as ds]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.workers :as uwrk]
|
||||
[uxbox.util.data :refer [dissoc-in index-of]]
|
||||
[uxbox.util.forms :as sc]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.math :as mth]
|
||||
|
@ -35,8 +30,10 @@
|
|||
|
||||
;; --- Expose inner functions
|
||||
|
||||
(def start-ruler wruler/start-ruler)
|
||||
(def clear-ruler wruler/clear-ruler)
|
||||
(def start-ruler nil)
|
||||
(def clear-ruler nil)
|
||||
|
||||
(defn interrupt? [e] (= e :interrupt))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; General workspace events
|
||||
|
@ -258,7 +255,7 @@
|
|||
(->> (:clipboard state)
|
||||
(filter #(= id (:id %)))
|
||||
(first)))]
|
||||
(simpl/duplicate-shapes state (:items selected) page-id))))
|
||||
(ds/duplicate-shapes state (:items selected) page-id))))
|
||||
|
||||
(defn paste-from-clipboard
|
||||
"Copy selected shapes to clipboard."
|
||||
|
@ -327,18 +324,50 @@
|
|||
{:pre [(uuid? id)]}
|
||||
(InitializeAlignment. id))
|
||||
|
||||
;; --- Duplicate Selected
|
||||
|
||||
(def duplicate-selected
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [selected (get-in state [:workspace :selected])]
|
||||
(ds/duplicate-shapes state selected)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shapes on Workspace events
|
||||
;; Shapes events
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn add-shape
|
||||
[data]
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; TODO: revisit the `setup-proportions` seems unnecesary
|
||||
(let [shape (assoc (geom/setup-proportions data)
|
||||
:id (uuid/random))
|
||||
pid (get-in state [:workspace :current])]
|
||||
(ds/assoc-shape-to-page state shape pid)))))
|
||||
|
||||
(defn delete-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(ds/dissoc-shape state shape)))))
|
||||
|
||||
(defrecord SelectShape [id]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace page-id :selected])]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace pid :selected])]
|
||||
(if (contains? selected id)
|
||||
(update-in state [:workspace page-id :selected] disj id)
|
||||
(update-in state [:workspace page-id :selected] conj id))))
|
||||
(update-in state [:workspace pid :selected] disj id)
|
||||
(update-in state [:workspace pid :selected] conj id))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
|
@ -353,9 +382,10 @@
|
|||
(defrecord DeselectAll []
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (get-in state [:workspace :current])]
|
||||
(assoc-in state [:workspace page-id :selected] #{})))
|
||||
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(update-in state [:workspace pid] #(-> %
|
||||
(assoc :selected #{})
|
||||
(dissoc :selected-canvas)))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/just :interrupt)))
|
||||
|
@ -388,37 +418,32 @@
|
|||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
selrect (get-in state [:workspace pid :selrect])
|
||||
shapes (simpl/match-by-selrect state pid selrect)]
|
||||
shapes (ds/match-by-selrect state pid selrect)]
|
||||
(assoc-in state [:workspace pid :selected] shapes)))))
|
||||
|
||||
;; --- Update Shape Attrs
|
||||
|
||||
(deftype UpdateShapeAttrs [id attrs]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] merge attrs)))
|
||||
|
||||
(defn update-shape-attrs
|
||||
[id attrs]
|
||||
{:pre [(uuid? id) (us/valid? ::uds/attributes attrs)]}
|
||||
(let [atts (us/extract attrs ::uds/attributes)]
|
||||
(UpdateShapeAttrs. id attrs)))
|
||||
(s/assert ::us/uuid id)
|
||||
(s/assert ::ds/attributes attrs)
|
||||
(let [atts (s/conform ::ds/attributes attrs)]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] merge attrs)))))
|
||||
|
||||
;; --- Update Selected Shapes attrs
|
||||
|
||||
|
||||
(deftype UpdateSelectedShapesAttrs [attrs]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace pid :selected])]
|
||||
(rx/from-coll (map #(update-shape-attrs % attrs) selected)))))
|
||||
|
||||
(defn update-selected-shapes-attrs
|
||||
[attrs]
|
||||
{:pre [(us/valid? ::uds/attributes attrs)]}
|
||||
(UpdateSelectedShapesAttrs. attrs))
|
||||
|
||||
(s/assert ::ds/attributes attrs)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace pid :selected])]
|
||||
(rx/from-coll (map #(update-shape-attrs % attrs) selected))))))
|
||||
|
||||
;; --- Move Selected
|
||||
|
||||
|
@ -446,67 +471,87 @@
|
|||
:fast (gpt/point (if align? (* 3 gx) 10)
|
||||
(if align? (* 3 gy) 10))}))
|
||||
|
||||
(declare apply-temporal-displacement)
|
||||
(declare initial-shape-align)
|
||||
(declare apply-displacement)
|
||||
|
||||
(defrecord MoveSelected [direction speed]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace :current])
|
||||
workspace (get-in state [:workspace page-id])
|
||||
selected (:selected workspace)
|
||||
flags (:flags workspace)
|
||||
align? (refs/alignment-activated? flags)
|
||||
metadata (merge c/page-metadata (get-in state [:pages page-id :metadata]))
|
||||
distance (get-displacement-distance metadata align?)
|
||||
displacement (get-displacement direction speed distance)]
|
||||
(rx/concat
|
||||
(when align?
|
||||
(rx/concat
|
||||
(rx/from-coll (map initial-shape-align selected))
|
||||
(rx/from-coll (map apply-displacement selected))))
|
||||
(rx/from-coll (map #(apply-temporal-displacement % displacement) selected))
|
||||
(rx/from-coll (map apply-displacement selected))))))
|
||||
(declare assoc-temporal-modifier)
|
||||
(declare materialize-current-modifier)
|
||||
(declare apply-temporal-displacement)
|
||||
|
||||
(s/def ::direction #{:up :down :right :left})
|
||||
(s/def ::speed #{:std :fast})
|
||||
|
||||
(defn move-selected
|
||||
[direction speed]
|
||||
{:pre [(us/valid? ::direction direction)
|
||||
(us/valid? ::speed speed)]}
|
||||
(MoveSelected. direction speed))
|
||||
(s/assert ::direction direction)
|
||||
(s/assert ::speed speed)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace :current])
|
||||
workspace (get-in state [:workspace page-id])
|
||||
selected (:selected workspace)
|
||||
flags (:flags workspace)
|
||||
align? (refs/alignment-activated? flags)
|
||||
metadata (merge c/page-metadata (get-in state [:pages page-id :metadata]))
|
||||
distance (get-displacement-distance metadata align?)
|
||||
displacement (get-displacement direction speed distance)]
|
||||
(rx/concat
|
||||
(when align?
|
||||
(rx/concat
|
||||
(rx/from-coll (map initial-shape-align selected))
|
||||
(rx/from-coll (map apply-displacement selected))))
|
||||
(rx/from-coll (map #(apply-temporal-displacement % displacement) selected))
|
||||
(rx/from-coll (map materialize-current-modifier selected)))))))
|
||||
|
||||
;; --- Move Selected Layer
|
||||
|
||||
(defrecord MoveSelectedLayer [loc]
|
||||
(defn move-selected-layer
|
||||
[loc]
|
||||
(assert (s/valid? ::direction loc))
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace id :selected])]
|
||||
(ds/move-layer state selected loc)))))
|
||||
|
||||
;; --- Update Shape Position
|
||||
|
||||
(deftype UpdateShapePosition [id point]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace id :selected])]
|
||||
(simpl/move-layer state selected loc))))
|
||||
(update-in state [:shapes id] geom/absolute-move point)))
|
||||
|
||||
(defn move-selected-layer
|
||||
[loc]
|
||||
{:pre [(us/valid? ::direction loc)]}
|
||||
(MoveSelectedLayer. loc))
|
||||
(defn update-position
|
||||
"Update the start position coordenate of the shape."
|
||||
[id point]
|
||||
{:pre [(uuid? id) (gpt/point? point)]}
|
||||
(UpdateShapePosition. id point))
|
||||
|
||||
;; --- Delete Selected
|
||||
|
||||
(defrecord DeleteSelected []
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [id (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace id :selected])]
|
||||
(rx/from-coll
|
||||
(into [(deselect-all)] (map #(uds/delete-shape %) selected))))))
|
||||
|
||||
(defn delete-selected
|
||||
(def delete-selected
|
||||
"Deselect all and remove all selected shapes."
|
||||
[]
|
||||
(DeleteSelected.))
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [id (get-in state [:workspace :current])
|
||||
selected (get-in state [:workspace id :selected])]
|
||||
(rx/from-coll
|
||||
(into [(deselect-all)] (map #(delete-shape %) selected)))))))
|
||||
|
||||
;; --- Rename Shape
|
||||
|
||||
(defn rename-shape
|
||||
[id name]
|
||||
{:pre [(uuid? id) (string? name)]}
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :name] name))))
|
||||
|
||||
;; --- Change Shape Order (Ordering)
|
||||
|
||||
|
@ -525,86 +570,54 @@
|
|||
|
||||
;; --- Shape Transformations
|
||||
|
||||
(defrecord InitialShapeAlign [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id])
|
||||
(geom/shape->rect-shape state))
|
||||
point (gpt/point x1 y1)]
|
||||
(->> (uwrk/align-point point)
|
||||
(rx/map (fn [{:keys [x y] :as pt}]
|
||||
(apply-temporal-displacement id (gpt/subtract pt point))))))))
|
||||
|
||||
(defn initial-shape-align
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(InitialShapeAlign. id))
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id])
|
||||
(geom/shape->rect-shape state))
|
||||
point (gpt/point x1 y1)]
|
||||
(->> (uwrk/align-point point)
|
||||
(rx/map (fn [{:keys [x y] :as pt}]
|
||||
(apply-temporal-displacement id (gpt/subtract pt point)))))))))
|
||||
|
||||
|
||||
;; --- Apply Temporal Displacement
|
||||
|
||||
(defrecord ApplyTemporalDisplacement [id delta]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
prev (get-in state [:workspace pid :modifiers id :displacement] (gmt/matrix))
|
||||
curr (gmt/translate prev delta)]
|
||||
(assoc-in state [:workspace pid :modifiers id :displacement] curr))))
|
||||
|
||||
(defn apply-temporal-displacement
|
||||
[id pt]
|
||||
{:pre [(uuid? id) (gpt/point? pt)]}
|
||||
(ApplyTemporalDisplacement. id pt))
|
||||
[id delta]
|
||||
{:pre [(uuid? id) (gpt/point? delta)]}
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [prev (get-in state [:shapes id :modifier-mtx] (gmt/matrix))
|
||||
curr (gmt/translate prev delta)]
|
||||
(rx/of (assoc-temporal-modifier id curr))))))
|
||||
|
||||
;; --- Apply Displacement
|
||||
;; --- Modifiers
|
||||
|
||||
(defrecord ApplyDisplacement [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
displacement (get-in state [:workspace pid :modifiers id :displacement])]
|
||||
(if (gmt/matrix? displacement)
|
||||
(rx/of #(simpl/materialize-xfmt % id displacement)
|
||||
#(update-in % [:workspace pid :modifiers id] dissoc :displacement)
|
||||
::udp/page-update)
|
||||
(rx/empty)))))
|
||||
|
||||
(defn apply-displacement
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(ApplyDisplacement. id))
|
||||
|
||||
;; --- Apply Temporal Resize Matrix
|
||||
|
||||
(deftype ApplyTemporalResize [sid xfmt]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(assoc-in state [:workspace pid :modifiers sid :resize] xfmt))))
|
||||
|
||||
(defn apply-temporal-resize
|
||||
"Attach temporal resize transformation to the shape."
|
||||
(defn assoc-temporal-modifier
|
||||
[id xfmt]
|
||||
{:pre [(gmt/matrix? xfmt) (uuid? id)]}
|
||||
(ApplyTemporalResize. id xfmt))
|
||||
{:pre [(uuid? id)
|
||||
(gmt/matrix? xfmt)]}
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :modifier-mtx] xfmt))))
|
||||
|
||||
;; --- Apply Resize Matrix
|
||||
|
||||
(deftype ApplyResize [id]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
resize (get-in state [:workspace pid :modifiers id :resize])]
|
||||
(if (gmt/matrix? resize)
|
||||
(rx/of #(simpl/materialize-xfmt % id resize)
|
||||
#(update-in % [:workspace pid :modifiers id] dissoc :resize)
|
||||
::udp/page-update)
|
||||
(rx/empty)))))
|
||||
|
||||
(defn apply-resize
|
||||
"Apply definitivelly the resize matrix transformation to the shape."
|
||||
(defn materialize-current-modifier
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(ApplyResize. id))
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [xfmt (get-in state [:shapes id :modifier-mtx])]
|
||||
(when (gmt/matrix? xfmt)
|
||||
(rx/of #(update-in % [:shapes id] geom/transform xfmt)
|
||||
#(update-in % [:shapes id] dissoc :modifier-mtx)
|
||||
::udp/page-update))))))
|
||||
|
||||
;; --- Start shape "edition mode"
|
||||
|
||||
|
@ -627,19 +640,273 @@
|
|||
|
||||
;; --- Select for Drawing
|
||||
|
||||
(defn select-for-drawing
|
||||
[shape]
|
||||
(def clear-drawing
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
current (get-in state [:workspace pid :drawing-tool])]
|
||||
(if (or (nil? shape)
|
||||
(= shape current))
|
||||
(update-in state [:workspace pid] dissoc :drawing :drawing-tool)
|
||||
(update-in state [:workspace pid] assoc
|
||||
:drawing shape
|
||||
:drawing-tool shape))))))
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(update-in state [:workspace pid] dissoc :drawing-tool :drawing)))))
|
||||
|
||||
(defn select-for-drawing?
|
||||
[e]
|
||||
(= (::type (meta e)) ::select-for-drawing))
|
||||
|
||||
(defn select-for-drawing
|
||||
([tool] (select-for-drawing tool nil))
|
||||
([tool data]
|
||||
(reify
|
||||
IMeta
|
||||
(-meta [_] {::type ::select-for-drawing})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(update-in state [:workspace pid] assoc :drawing-tool tool :drawing data))))))
|
||||
|
||||
;; --- Shape Proportions
|
||||
|
||||
(deftype LockShapeProportions [id]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [[width height] (-> (get-in state [:shapes id])
|
||||
(geom/size)
|
||||
(keep [:width :height]))
|
||||
proportion (/ width height)]
|
||||
(update-in state [:shapes id] assoc
|
||||
:proportion proportion
|
||||
:proportion-lock true))))
|
||||
|
||||
(defn lock-proportions
|
||||
"Mark proportions of the shape locked and save the current
|
||||
proportion as additional precalculated property."
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(LockShapeProportions. id))
|
||||
|
||||
(deftype UnlockShapeProportions [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:shapes id :proportion-lock] false)))
|
||||
|
||||
(defn unlock-proportions
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnlockShapeProportions. id))
|
||||
|
||||
;; --- Update Dimensions
|
||||
|
||||
(s/def ::width (s/and ::us/number ::us/positive))
|
||||
(s/def ::height (s/and ::us/number ::us/positive))
|
||||
|
||||
(s/def ::update-dimensions
|
||||
(s/keys :opt-un [::width ::height]))
|
||||
|
||||
(defn update-dimensions
|
||||
"A helper event just for update the position
|
||||
of the shape using the width and height attrs
|
||||
instread final point of coordinates."
|
||||
[id dimensions]
|
||||
(s/assert ::us/uuid id)
|
||||
(s/assert ::update-dimensions dimensions)
|
||||
(reify
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id] geom/resize-dim dimensions))))
|
||||
|
||||
;; --- Update Interaction
|
||||
|
||||
(deftype UpdateInteraction [shape interaction]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (or (:id interaction)
|
||||
(uuid/random))
|
||||
data (assoc interaction :id id)]
|
||||
(assoc-in state [:shapes shape :interactions id] data))))
|
||||
|
||||
(defn update-interaction
|
||||
[shape interaction]
|
||||
(UpdateInteraction. shape interaction))
|
||||
|
||||
;; --- Delete Interaction
|
||||
|
||||
(deftype DeleteInteracton [shape id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes shape :interactions] dissoc id)))
|
||||
|
||||
(defn delete-interaction
|
||||
[shape id]
|
||||
{:pre [(uuid? id) (uuid? shape)]}
|
||||
(DeleteInteracton. shape id))
|
||||
|
||||
;; --- Path Modifications
|
||||
|
||||
(deftype UpdatePath [id index delta]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:shapes id :segments index] gpt/add delta)))
|
||||
|
||||
(defn update-path
|
||||
"Update a concrete point in the path shape."
|
||||
[id index delta]
|
||||
{:pre [(uuid? id) (number? index) (gpt/point? delta)]}
|
||||
(UpdatePath. id index delta))
|
||||
|
||||
;; --- Initial Path Point Alignment
|
||||
|
||||
(deftype InitialPathPointAlign [id index]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [shape (get-in state [:shapes id])
|
||||
point (get-in shape [:segments index])]
|
||||
(->> (uwrk/align-point point)
|
||||
(rx/map #(update-path id index %))))))
|
||||
|
||||
(defn initial-path-point-align
|
||||
"Event responsible of align a specified point of the
|
||||
shape by `index` with the grid."
|
||||
[id index]
|
||||
{:pre [(uuid? id)
|
||||
(number? index)
|
||||
(not (neg? index))]}
|
||||
(InitialPathPointAlign. id index))
|
||||
|
||||
;; --- Shape Visibility
|
||||
|
||||
(deftype HideShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-hidden [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :hidden] true)
|
||||
(reduce mark-hidden $ (:items shape)))
|
||||
(assoc-in state [:shapes id :hidden] true))))]
|
||||
(mark-hidden state id))))
|
||||
|
||||
(defn hide-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(HideShape. id))
|
||||
|
||||
(deftype ShowShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-visible [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :hidden] false)
|
||||
(reduce mark-visible $ (:items shape)))
|
||||
(assoc-in state [:shapes id :hidden] false))))]
|
||||
(mark-visible state id))))
|
||||
|
||||
(defn show-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(ShowShape. id))
|
||||
|
||||
;; --- Shape Blocking
|
||||
|
||||
(deftype BlockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-blocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :blocked] true)
|
||||
(reduce mark-blocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :blocked] true))))]
|
||||
(mark-blocked state id))))
|
||||
|
||||
(defn block-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(BlockShape. id))
|
||||
|
||||
(deftype UnblockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-unblocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :blocked] false)
|
||||
(reduce mark-unblocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :blocked] false))))]
|
||||
(mark-unblocked state id))))
|
||||
|
||||
(defn unblock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnblockShape. id))
|
||||
|
||||
;; --- Shape Locking
|
||||
|
||||
(deftype LockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-locked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :locked] true)
|
||||
(reduce mark-locked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :locked] true))))]
|
||||
(mark-locked state id))))
|
||||
|
||||
(defn lock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(LockShape. id))
|
||||
|
||||
(deftype UnlockShape [id]
|
||||
udp/IPageUpdate
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(letfn [(mark-unlocked [state id]
|
||||
(let [shape (get-in state [:shapes id])]
|
||||
(if (= :group (:type shape))
|
||||
(as-> state $
|
||||
(assoc-in $ [:shapes id :locked] false)
|
||||
(reduce mark-unlocked $ (:items shape)))
|
||||
(assoc-in state [:shapes id :locked] false))))]
|
||||
(mark-unlocked state id))))
|
||||
|
||||
(defn unlock-shape
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(UnlockShape. id))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Pages
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn delete-page
|
||||
[id]
|
||||
{:pre [(uuid? id)]}
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:pages id :project])]
|
||||
(rx/merge
|
||||
(rx/of (udp/delete-page id))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::udp/delete-completed))
|
||||
(rx/map #(dp/go-to pid))
|
||||
(rx/take 1)))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Selection Rect IMPL
|
||||
|
@ -662,6 +929,42 @@
|
|||
:y2 end-y
|
||||
:type :rect)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Canvas Interactions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; ;; --- Group Collapsing
|
||||
|
||||
;; (deftype CollapseGroupShape [id]
|
||||
;; udp/IPageUpdate
|
||||
;; ptk/UpdateEvent
|
||||
;; (update [_ state]
|
||||
;; (update-in state [:shapes id] assoc :collapsed true)))
|
||||
|
||||
;; (defn collapse-shape
|
||||
;; [id]
|
||||
;; {:pre [(uuid? id)]}
|
||||
;; (CollapseGroupShape. id))
|
||||
|
||||
;; (deftype UncollapseGroupShape [id]
|
||||
;; udp/IPageUpdate
|
||||
;; ptk/UpdateEvent
|
||||
;; (update [_ state]
|
||||
;; (update-in state [:shapes id] assoc :collapsed false)))
|
||||
|
||||
;; (defn uncollapse-shape
|
||||
;; [id]
|
||||
;; {:pre [(uuid? id)]}
|
||||
;; (UncollapseGroupShape. id))
|
||||
|
||||
(defn select-canvas
|
||||
[id]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(update-in state [:workspace pid] assoc :selected-canvas id)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Server Interactions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -670,16 +973,15 @@
|
|||
|
||||
;; Is a workspace aware wrapper over uxbox.data.pages/UpdateMetadata event.
|
||||
|
||||
(defrecord UpdateMetadata [id metadata]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (udp/update-metadata id metadata)
|
||||
(initialize-alignment id))))
|
||||
|
||||
(defn update-metadata
|
||||
[id metadata]
|
||||
{:pre [(uuid? id) (us/valid? ::udp/metadata metadata)]}
|
||||
(UpdateMetadata. id metadata))
|
||||
(s/assert ::us/uuid id)
|
||||
(s/assert ::udp/metadata metadata)
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (udp/update-metadata id metadata)
|
||||
(initialize-alignment id)))))
|
||||
|
||||
(defrecord OpenView [page-id]
|
||||
ptk/WatchEvent
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns uxbox.main.data.workspace.ruler
|
||||
"Workspace ruler related events. Mostly or all events
|
||||
are related to UI logic."
|
||||
(:require [beicon.core :as rx]
|
||||
#_(:require [beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.streams :as streams]
|
||||
|
@ -17,79 +17,79 @@
|
|||
|
||||
;; --- Constants
|
||||
|
||||
(declare stop-ruler?)
|
||||
(declare clear-ruler)
|
||||
(declare update-ruler)
|
||||
;; (declare stop-ruler?)
|
||||
;; (declare clear-ruler)
|
||||
;; (declare update-ruler)
|
||||
|
||||
(def ^:private immanted-zones
|
||||
(let [transform #(vector (- % 7) (+ % 7) %)
|
||||
right (map transform (range 0 181 15))
|
||||
left (map (comp transform -) (range 0 181 15))]
|
||||
(vec (concat right left))))
|
||||
;; (def ^:private immanted-zones
|
||||
;; (let [transform #(vector (- % 7) (+ % 7) %)
|
||||
;; right (map transform (range 0 181 15))
|
||||
;; left (map (comp transform -) (range 0 181 15))]
|
||||
;; (vec (concat right left))))
|
||||
|
||||
(defn- align-position
|
||||
[pos]
|
||||
(let [angle (gpt/angle pos)]
|
||||
(reduce (fn [pos [a1 a2 v]]
|
||||
(if (< a1 angle a2)
|
||||
(reduced (gpt/update-angle pos v))
|
||||
pos))
|
||||
pos
|
||||
immanted-zones)))
|
||||
;; (defn- align-position
|
||||
;; [pos]
|
||||
;; (let [angle (gpt/angle pos)]
|
||||
;; (reduce (fn [pos [a1 a2 v]]
|
||||
;; (if (< a1 angle a2)
|
||||
;; (reduced (gpt/update-angle pos v))
|
||||
;; pos))
|
||||
;; pos
|
||||
;; immanted-zones)))
|
||||
|
||||
;; --- Start Ruler
|
||||
;; ;; --- Start Ruler
|
||||
|
||||
(deftype StartRuler []
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
pos (get-in state [:workspace :pointer :viewport])]
|
||||
(assoc-in state [:workspace pid :ruler] {:start pos :end pos})))
|
||||
;; (deftype StartRuler []
|
||||
;; ptk/UpdateEvent
|
||||
;; (update [_ state]
|
||||
;; (let [pid (get-in state [:workspace :current])
|
||||
;; pos (get-in state [:workspace :pointer :viewport])]
|
||||
;; (assoc-in state [:workspace pid :ruler] {:start pos :end pos})))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (->> (rx/filter #(= ::uev/interrupt %) stream)
|
||||
(rx/take 1))]
|
||||
(->> streams/mouse-position
|
||||
(rx/take-until stoper)
|
||||
(rx/map (juxt :viewport :ctrl))
|
||||
(rx/map (fn [[pt ctrl?]]
|
||||
(update-ruler pt ctrl?)))))))
|
||||
;; ptk/WatchEvent
|
||||
;; (watch [_ state stream]
|
||||
;; (let [stoper (->> (rx/filter #(= ::uev/interrupt %) stream)
|
||||
;; (rx/take 1))]
|
||||
;; (->> streams/mouse-position
|
||||
;; (rx/take-until stoper)
|
||||
;; (rx/map (juxt :viewport :ctrl))
|
||||
;; (rx/map (fn [[pt ctrl?]]
|
||||
;; (update-ruler pt ctrl?)))))))
|
||||
|
||||
(defn start-ruler
|
||||
[]
|
||||
(StartRuler.))
|
||||
;; (defn start-ruler
|
||||
;; []
|
||||
;; (StartRuler.))
|
||||
|
||||
;; --- Update Ruler
|
||||
;; ;; --- Update Ruler
|
||||
|
||||
(deftype UpdateRuler [point ctrl?]
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
ruler (get-in state [:workspace pid :ruler])]
|
||||
(if-not ctrl?
|
||||
(assoc-in state [:workspace pid :ruler :end] point)
|
||||
(let [start (get-in state [:workspace pid :ruler :start])
|
||||
end (-> (gpt/subtract point start)
|
||||
(align-position)
|
||||
(gpt/add start))]
|
||||
(assoc-in state [:workspace pid :ruler :end] end))))))
|
||||
;; (deftype UpdateRuler [point ctrl?]
|
||||
;; ptk/UpdateEvent
|
||||
;; (update [_ state]
|
||||
;; (let [pid (get-in state [:workspace :current])
|
||||
;; ruler (get-in state [:workspace pid :ruler])]
|
||||
;; (if-not ctrl?
|
||||
;; (assoc-in state [:workspace pid :ruler :end] point)
|
||||
;; (let [start (get-in state [:workspace pid :ruler :start])
|
||||
;; end (-> (gpt/subtract point start)
|
||||
;; (align-position)
|
||||
;; (gpt/add start))]
|
||||
;; (assoc-in state [:workspace pid :ruler :end] end))))))
|
||||
|
||||
(defn update-ruler
|
||||
[point ctrl?]
|
||||
{:pre [(gpt/point? point)
|
||||
(boolean? ctrl?)]}
|
||||
(UpdateRuler. point ctrl?))
|
||||
;; (defn update-ruler
|
||||
;; [point ctrl?]
|
||||
;; {:pre [(gpt/point? point)
|
||||
;; (boolean? ctrl?)]}
|
||||
;; (UpdateRuler. point ctrl?))
|
||||
|
||||
;; --- Clear Ruler
|
||||
;; ;; --- Clear Ruler
|
||||
|
||||
(deftype ClearRuler []
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
(update-in state [:workspace pid] dissoc :ruler))))
|
||||
;; (deftype ClearRuler []
|
||||
;; ptk/UpdateEvent
|
||||
;; (update [_ state]
|
||||
;; (let [pid (get-in state [:workspace :current])]
|
||||
;; (update-in state [:workspace pid] dissoc :ruler))))
|
||||
|
||||
(defn clear-ruler
|
||||
[]
|
||||
(ClearRuler.))
|
||||
;; (defn clear-ruler
|
||||
;; []
|
||||
;; (ClearRuler.))
|
||||
|
||||
|
|
|
@ -6,23 +6,7 @@
|
|||
|
||||
;; TODO: DEPRECTATED, maintained just for temporal documentation, delete on near future
|
||||
|
||||
(ns uxbox.main.data.workspace-drawing
|
||||
"Workspace drawing data events and impl."
|
||||
(:require [beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[lentes.core :as l]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.streams :as streams]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.workers :as uwrk]
|
||||
[uxbox.main.user-events :as uev]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.util.geom.path :as pth]
|
||||
[uxbox.util.geom.point :as gpt]))
|
||||
(ns uxbox.main.data.workspace-drawing)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Events
|
||||
|
@ -234,7 +218,7 @@
|
|||
;; (rx/filter uev/mouse-up?)
|
||||
;; (rx/take 1)))
|
||||
;; start? (volatile! true)
|
||||
;; mouse (->> streams/viewport-mouse-position
|
||||
;; mouse (->> streams/mouse-position
|
||||
;; (rx/take-until stoper)
|
||||
;; (rx/mapcat conditional-align)
|
||||
;; (rx/map translate-to-canvas)
|
||||
|
@ -325,7 +309,7 @@
|
|||
;; (defn- on-init-draw-free-path
|
||||
;; [shape stoper]
|
||||
;; (let [stoper (get-path-stoper-stream stoper true)
|
||||
;; mouse (->> streams/viewport-mouse-position
|
||||
;; mouse (->> streams/mouse-position
|
||||
;; (rx/mapcat conditional-align)
|
||||
;; (rx/map translate-to-canvas))
|
||||
|
||||
|
@ -341,7 +325,7 @@
|
|||
;; [shape stoper]
|
||||
;; (let [last-point (volatile! @refs/canvas-mouse-position)
|
||||
;; stoper (get-path-stoper-stream stoper)
|
||||
;; mouse (->> (rx/sample 10 streams/viewport-mouse-position)
|
||||
;; mouse (->> (rx/sample 10 streams/mouse-position)
|
||||
;; (rx/mapcat conditional-align)
|
||||
;; (rx/map translate-to-canvas))
|
||||
;; points (->> (get-path-point-stream)
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
:image (move-rect shape dpoint)
|
||||
:rect (move-rect shape dpoint)
|
||||
:text (move-rect shape dpoint)
|
||||
:curve (move-path shape dpoint)
|
||||
:path (move-path shape dpoint)
|
||||
:circle (move-circle shape dpoint)
|
||||
:group (move-group shape dpoint)))
|
||||
|
@ -125,13 +126,10 @@
|
|||
"Calculate the size of the shape."
|
||||
[shape]
|
||||
(case (:type shape)
|
||||
:group (assoc shape :width 100 :height 100)
|
||||
:circle (size-circle shape)
|
||||
:text (size-rect shape)
|
||||
:rect (size-rect shape)
|
||||
:icon (size-rect shape)
|
||||
:image (size-rect shape)
|
||||
:path (size-path shape)))
|
||||
:curve (size-path shape)
|
||||
:path (size-path shape)
|
||||
(size-rect shape)))
|
||||
|
||||
(defn- size-path
|
||||
[{:keys [segments x1 y1 x2 y2] :as shape}]
|
||||
|
@ -179,11 +177,13 @@
|
|||
(defn setup-proportions
|
||||
[shape]
|
||||
(case (:type shape)
|
||||
:canvas (setup-proportions-rect shape)
|
||||
:rect (setup-proportions-rect shape)
|
||||
:circle (setup-proportions-rect shape)
|
||||
:icon (setup-proportions-image shape)
|
||||
:image (setup-proportions-image shape)
|
||||
:text shape
|
||||
:curve (setup-proportions-rect shape)
|
||||
:path (setup-proportions-rect shape)))
|
||||
|
||||
(defn setup-proportions-image
|
||||
|
@ -461,6 +461,7 @@
|
|||
(case type
|
||||
:circle (circle->rect-shape state shape)
|
||||
:path (path->rect-shape state shape)
|
||||
:curve (path->rect-shape state shape)
|
||||
shape)))
|
||||
|
||||
(defn shapes->rect-shape
|
||||
|
@ -512,11 +513,13 @@
|
|||
"Apply the matrix transformation to shape."
|
||||
[{:keys [type] :as shape} xfmt]
|
||||
(case type
|
||||
:canvas (transform-rect shape xfmt)
|
||||
:rect (transform-rect shape xfmt)
|
||||
:icon (transform-rect shape xfmt)
|
||||
:text (transform-rect shape xfmt)
|
||||
:image (transform-rect shape xfmt)
|
||||
:path (transform-path shape xfmt)
|
||||
:curve (transform-path shape xfmt)
|
||||
:circle (transform-circle shape xfmt)))
|
||||
|
||||
(defn- transform-rect
|
||||
|
@ -564,7 +567,6 @@
|
|||
;; --- Outer Rect
|
||||
|
||||
(declare selection-rect-generic)
|
||||
(declare selection-rect-group)
|
||||
|
||||
(defn rotation-matrix
|
||||
"Generate a rotation matrix from shape."
|
||||
|
@ -589,25 +591,15 @@
|
|||
([shape]
|
||||
(selection-rect @st/state shape))
|
||||
([state shape]
|
||||
(let [{:keys [displacement resize]} (:modifiers shape)]
|
||||
(let [modifier (:modifier-mtx shape)]
|
||||
(-> (shape->rect-shape shape)
|
||||
(assoc :type :rect :id (:id shape))
|
||||
(transform (or resize (gmt/matrix)))
|
||||
(transform (or displacement (gmt/matrix)))
|
||||
(transform (or modifier (gmt/matrix)))
|
||||
(rotate-shape)
|
||||
(size)))))
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn resolve-parent
|
||||
"Recursively resolve the real shape parent."
|
||||
([shape]
|
||||
(resolve-parent @st/state shape))
|
||||
([state {:keys [group] :as shape}]
|
||||
(if group
|
||||
(resolve-parent state (get-in state [:shapes group]))
|
||||
shape)))
|
||||
|
||||
(defn contained-in?
|
||||
"Check if a shape is contained in the
|
||||
provided selection rect."
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
(ns uxbox.main.lenses
|
||||
(:require [lentes.core :as l]))
|
||||
|
||||
;; --- Workspace
|
||||
;; --- FIXME: remove this ns
|
||||
|
||||
(def workspace (l/key :workspace))
|
||||
(def workspace-flags (comp workspace (l/key :flags)))
|
||||
|
||||
(def selected-drawing (comp workspace (l/key :drawing)))
|
||||
(def selected-shapes (comp workspace (l/key :selected)))
|
||||
(def selected-page (comp workspace (l/key :page)))
|
||||
(def selected-project (comp workspace (l/key :project)))
|
|
@ -52,6 +52,10 @@
|
|||
(-> (l/key :selected)
|
||||
(l/derive workspace)))
|
||||
|
||||
(def selected-canvas
|
||||
(-> (l/key :selected-canvas)
|
||||
(l/derive workspace)))
|
||||
|
||||
(def toolboxes
|
||||
(-> (l/key :toolboxes)
|
||||
(l/derive workspace)))
|
||||
|
@ -100,28 +104,6 @@
|
|||
(l/lens alignment-activated?))
|
||||
(l/derive workspace)))
|
||||
|
||||
;; ...
|
||||
|
||||
(def mouse-position
|
||||
(-> (l/in [:workspace :pointer])
|
||||
(l/derive st/state)))
|
||||
|
||||
(def canvas-mouse-position
|
||||
(-> (l/key :canvas)
|
||||
(l/derive mouse-position)))
|
||||
|
||||
(def viewport-mouse-position
|
||||
(-> (l/key :viewport)
|
||||
(l/derive mouse-position)))
|
||||
|
||||
(def window-mouse-position
|
||||
(-> (l/key :window)
|
||||
(l/derive mouse-position)))
|
||||
|
||||
(def workspace-scroll
|
||||
(-> (l/in [:workspace :scroll])
|
||||
(l/derive st/state)))
|
||||
|
||||
(def shapes-by-id
|
||||
(-> (l/key :shapes)
|
||||
(l/derive st/state)))
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
;; 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) 2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.repo.pages
|
||||
"A main interface for access to remote resources."
|
||||
(:require [beicon.core :as rx]
|
||||
[uxbox.config :refer (url)]
|
||||
[uxbox.main.repo.impl :refer (request send!)]
|
||||
[uxbox.util.transit :as t]))
|
||||
(:require
|
||||
[uxbox.config :refer [url]]
|
||||
[uxbox.main.repo.impl :refer [request send!]]))
|
||||
|
||||
(defmethod request :fetch/pages
|
||||
[type data]
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
;; 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) 2017 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.streams
|
||||
"A collection of derived streams."
|
||||
(:require [beicon.core :as rx]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.user-events :as uev]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.workers :as uwrk]
|
||||
[uxbox.util.geom.point :as gpt]))
|
||||
|
||||
;; --- Events
|
||||
|
||||
(defn- user-interaction-event?
|
||||
[event]
|
||||
(or (uev/keyboard-event? event)
|
||||
(uev/mouse-event? event)))
|
||||
|
||||
(defonce events
|
||||
(rx/filter user-interaction-event? st/stream))
|
||||
|
||||
;; --- Mouse Position Stream
|
||||
|
||||
(defonce mouse-position
|
||||
(rx/filter uev/pointer-event? st/stream))
|
||||
|
||||
(defonce canvas-mouse-position
|
||||
(->> mouse-position
|
||||
(rx/map :canvas)
|
||||
(rx/share)))
|
||||
|
||||
(defonce viewport-mouse-position
|
||||
(->> mouse-position
|
||||
(rx/map :viewport)
|
||||
(rx/share)))
|
||||
|
||||
(defonce window-mouse-position
|
||||
(->> mouse-position
|
||||
(rx/map :window)
|
||||
(rx/share)))
|
||||
|
||||
(defonce mouse-position-ctrl
|
||||
(->> mouse-position
|
||||
(rx/map :ctrl)
|
||||
(rx/share)))
|
||||
|
||||
(defn- coords-delta
|
||||
[[old new]]
|
||||
(gpt/subtract new old))
|
||||
|
||||
(defonce mouse-position-deltas
|
||||
(->> viewport-mouse-position
|
||||
(rx/sample 10)
|
||||
(rx/map #(gpt/divide % @refs/selected-zoom))
|
||||
(rx/mapcat (fn [point]
|
||||
(if @refs/selected-alignment
|
||||
(uwrk/align-point point)
|
||||
(rx/of point))))
|
||||
(rx/buffer 2 1)
|
||||
(rx/map coords-delta)
|
||||
(rx/share)))
|
|
@ -11,7 +11,6 @@
|
|||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.core :as mx]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.auth :refer [logout]]
|
||||
|
@ -55,14 +54,17 @@
|
|||
(defn- on-error
|
||||
"A default error handler."
|
||||
[{:keys [status] :as error}]
|
||||
(js/console.error "on-error:" (pr-str error))
|
||||
(js/console.error (.-stack error))
|
||||
(js/console.error "Unhandled Error:"
|
||||
"\n - message:" (ex-message error)
|
||||
"\n - data:" (pr-str (ex-data error))
|
||||
"\n - stack:" (.-stack error))
|
||||
(reset! st/loader false)
|
||||
(cond
|
||||
;; Unauthorized or Auth timeout
|
||||
(and (:status error)
|
||||
(or (= (:status error) 403)
|
||||
(= (:status error) 419)))
|
||||
|
||||
(ts/schedule 0 #(st/emit! (rt/nav :auth/login)))
|
||||
|
||||
;; Conflict
|
||||
|
@ -81,43 +83,39 @@
|
|||
|
||||
;; --- Main App (Component)
|
||||
|
||||
(mf/def app
|
||||
:mixins [mx/reactive]
|
||||
(def route-iref
|
||||
(-> (l/key :route)
|
||||
(l/derive st/state)))
|
||||
|
||||
:init
|
||||
(fn [own props]
|
||||
(assoc own ::route-ref (l/derive (l/key :route) st/state)))
|
||||
(mf/defc app
|
||||
[props]
|
||||
(let [route (mf/deref route-iref)]
|
||||
(case (get-in route [:data :name])
|
||||
:auth/login (mf/element auth/login-page)
|
||||
:auth/register (mf/element auth/register-page)
|
||||
;; :auth/recovery-request (auth/recovery-request-page)
|
||||
|
||||
:render
|
||||
(fn [own props]
|
||||
(let [route (mx/react (::route-ref own))
|
||||
route-id (get-in route [:data :name])]
|
||||
(case route-id
|
||||
:auth/login (mf/element auth/login-page)
|
||||
:auth/register (auth/register-page)
|
||||
:auth/recovery-request (auth/recovery-request-page)
|
||||
;; :auth/recovery
|
||||
;; (let [token (get-in route [:params :path :token])]
|
||||
;; (auth/recovery-page token))
|
||||
|
||||
:auth/recovery
|
||||
(let [token (get-in route [:params :path :token])]
|
||||
(auth/recovery-page token))
|
||||
(:settings/profile
|
||||
:settings/password
|
||||
:settings/notifications)
|
||||
(mf/element settings/settings #js {:route route})
|
||||
|
||||
(:settings/profile
|
||||
:settings/password
|
||||
:settings/notifications)
|
||||
(mf/element settings/settings #js {:route route})
|
||||
(:dashboard/projects
|
||||
:dashboard/icons
|
||||
:dashboard/images
|
||||
:dashboard/colors)
|
||||
(mf/element dashboard/dashboard #js {:route route})
|
||||
|
||||
(:dashboard/projects
|
||||
:dashboard/icons
|
||||
:dashboard/images
|
||||
:dashboard/colors)
|
||||
(mf/element dashboard/dashboard #js {:route route})
|
||||
:workspace/page
|
||||
(let [project-id (uuid (get-in route [:params :path :project]))
|
||||
page-id (uuid (get-in route [:params :path :page]))]
|
||||
[:& workspace-page {:project-id project-id
|
||||
:page-id page-id
|
||||
:key page-id}])
|
||||
|
||||
:workspace/page
|
||||
(let [project-id (uuid (get-in route [:params :path :project]))
|
||||
page-id (uuid (get-in route [:params :path :page]))]
|
||||
[:& workspace-page {:project-id project-id
|
||||
:page-id page-id
|
||||
:key page-id}])
|
||||
nil)))
|
||||
|
||||
nil
|
||||
))))
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
(ns uxbox.main.ui.auth
|
||||
(:require [uxbox.main.ui.auth.login :as login]
|
||||
[uxbox.main.ui.auth.register :as register]
|
||||
[uxbox.main.ui.auth.recovery-request :as recovery-request]
|
||||
[uxbox.main.ui.auth.recovery :as recovery]))
|
||||
#_[uxbox.main.ui.auth.recovery-request :as recovery-request]
|
||||
#_[uxbox.main.ui.auth.recovery :as recovery]))
|
||||
|
||||
(def login-page login/login-page)
|
||||
(def register-page register/register-page)
|
||||
(def recovery-page recovery/recovery-page)
|
||||
(def recovery-request-page recovery-request/recovery-request-page)
|
||||
;; (def recovery-page recovery/recovery-page)
|
||||
;; (def recovery-request-page recovery-request/recovery-request-page)
|
||||
|
|
|
@ -8,44 +8,30 @@
|
|||
(ns uxbox.main.ui.auth.login
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.config :as cfg]
|
||||
[uxbox.main.data.auth :as da]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.router :as rt]))
|
||||
[uxbox.util.i18n :refer [tr]]
|
||||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.spec :as us]))
|
||||
|
||||
(def form-data (fm/focus-data :login st/state))
|
||||
(def form-errors (fm/focus-errors :login st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :login))
|
||||
(def assoc-errors (partial fm/assoc-errors :login))
|
||||
(def clear-form (partial fm/clear-form :login))
|
||||
|
||||
(s/def ::username ::fm/non-empty-string)
|
||||
(s/def ::password ::fm/non-empty-string)
|
||||
(s/def ::username ::us/not-empty-string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
|
||||
(s/def ::login-form
|
||||
(s/keys :req-un [::username ::password]))
|
||||
|
||||
(defn- on-change
|
||||
[event field]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
|
||||
(defn- on-submit
|
||||
[event data]
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (da/login {:username (:username data)
|
||||
:password (:password data)})))
|
||||
(let [{:keys [username password]} (:clean-data form)]
|
||||
(st/emit! (da/login {:username username
|
||||
:password password}))))
|
||||
|
||||
(mf/defc demo-warning
|
||||
[_]
|
||||
|
@ -56,36 +42,41 @@
|
|||
[:strong "DO NOT USE"] " for real work, " [:br]
|
||||
" the projects will be periodicaly wiped."]])
|
||||
|
||||
|
||||
(mf/defc login-form
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[]
|
||||
(let [data (mf/react form-data)
|
||||
valid? (fm/valid? ::login-form data)]
|
||||
[:form {:on-submit #(on-submit % data)}
|
||||
(let [{:keys [data] :as form} (fm/use-form ::login-form {})]
|
||||
[:form {:on-submit #(on-submit % form)}
|
||||
[:div.login-content
|
||||
(when cfg/isdemo
|
||||
[:& demo-warning])
|
||||
|
||||
[:input.input-text
|
||||
{:name "email"
|
||||
{:name "username"
|
||||
:tab-index "2"
|
||||
:value (:username data "")
|
||||
:on-change #(on-change % :username)
|
||||
:class (fm/error-class form :username)
|
||||
:on-blur (fm/on-input-blur form :username)
|
||||
:on-change (fm/on-input-change form :username)
|
||||
:placeholder (tr "auth.email-or-username")
|
||||
:type "text"}]
|
||||
[:input.input-text
|
||||
{:name "password"
|
||||
:tab-index "3"
|
||||
:value (:password data "")
|
||||
:on-change #(on-change % :password)
|
||||
:class (fm/error-class form :password)
|
||||
:on-blur (fm/on-input-blur form :password)
|
||||
:on-change (fm/on-input-change form :password)
|
||||
:placeholder (tr "auth.password")
|
||||
:type "password"}]
|
||||
[:input.btn-primary
|
||||
{:name "login"
|
||||
:tab-index "4"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "auth.signin")
|
||||
:type "submit"}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth/recovery-request))
|
||||
:tab-index "5"}
|
||||
|
@ -94,14 +85,10 @@
|
|||
:tab-index "6"}
|
||||
(tr "auth.no-account")]]]]))
|
||||
|
||||
|
||||
;; {:mixins [mx/static (fm/clear-mixin st/store :login)]}
|
||||
|
||||
(mf/defc login-page
|
||||
[]
|
||||
(mf/use-effect (constantly #(st/emit! (fm/clear-form :login))))
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
(messages-widget)
|
||||
[:& messages-widget]
|
||||
[:a i/logo]
|
||||
[:& login-form]]])
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
(ns uxbox.main.ui.auth.recovery
|
||||
(:require
|
||||
[cljs.spec.alpha :as s :include-macros true]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
|
@ -21,62 +21,62 @@
|
|||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
(def form-data (fm/focus-data :recovery st/state))
|
||||
(def form-errors (fm/focus-errors :recovery st/state))
|
||||
;; (def form-data (fm/focus-data :recovery st/state))
|
||||
;; (def form-errors (fm/focus-errors :recovery st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :recovery))
|
||||
(def assoc-errors (partial fm/assoc-errors :recovery))
|
||||
(def clear-form (partial fm/clear-form :recovery))
|
||||
;; (def assoc-value (partial fm/assoc-value :recovery))
|
||||
;; (def assoc-errors (partial fm/assoc-errors :recovery))
|
||||
;; (def clear-form (partial fm/clear-form :recovery))
|
||||
|
||||
;; --- Recovery Form
|
||||
;; ;; --- Recovery Form
|
||||
|
||||
(s/def ::password ::fm/non-empty-string)
|
||||
(s/def ::recovery-form
|
||||
(s/keys :req-un [::password]))
|
||||
;; (s/def ::password ::fm/non-empty-string)
|
||||
;; (s/def ::recovery-form
|
||||
;; (s/keys :req-un [::password]))
|
||||
|
||||
(mx/defc recovery-form
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
[token]
|
||||
(let [data (merge (mx/react form-data) {:token token})
|
||||
valid? (fm/valid? ::recovery-form data)]
|
||||
(letfn [(on-change [field event]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
(on-submit [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (uda/recovery data)
|
||||
(clear-form)))]
|
||||
[:form {:on-submit on-submit}
|
||||
[:div.login-content
|
||||
[:input.input-text
|
||||
{:name "password"
|
||||
:value (:password data "")
|
||||
:on-change (partial on-change :password)
|
||||
:placeholder (tr "recover.password.placeholder")
|
||||
:type "password"}]
|
||||
[:input.btn-primary
|
||||
{:name "login"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:value (tr "recover.recover-password")
|
||||
:type "submit"}]
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]])))
|
||||
;; (mx/defc recovery-form
|
||||
;; {:mixins [mx/static mx/reactive]}
|
||||
;; [token]
|
||||
;; (let [data (merge (mx/react form-data) {:token token})
|
||||
;; valid? (fm/valid? ::recovery-form data)]
|
||||
;; (letfn [(on-change [field event]
|
||||
;; (let [value (dom/event->value event)]
|
||||
;; (st/emit! (assoc-value field value))))
|
||||
;; (on-submit [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (st/emit! (uda/recovery data)
|
||||
;; (clear-form)))]
|
||||
;; [:form {:on-submit on-submit}
|
||||
;; [:div.login-content
|
||||
;; [:input.input-text
|
||||
;; {:name "password"
|
||||
;; :value (:password data "")
|
||||
;; :on-change (partial on-change :password)
|
||||
;; :placeholder (tr "recover.password.placeholder")
|
||||
;; :type "password"}]
|
||||
;; [:input.btn-primary
|
||||
;; {:name "login"
|
||||
;; :class (when-not valid? "btn-disabled")
|
||||
;; :disabled (not valid?)
|
||||
;; :value (tr "recover.recover-password")
|
||||
;; :type "submit"}]
|
||||
;; [:div.login-links
|
||||
;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]])))
|
||||
|
||||
;; --- Recovery Page
|
||||
;; ;; --- Recovery Page
|
||||
|
||||
(defn- recovery-page-init
|
||||
[own]
|
||||
(let [[token] (::mx/args own)]
|
||||
(st/emit! (uda/validate-recovery-token token))
|
||||
own))
|
||||
;; (defn- recovery-page-init
|
||||
;; [own]
|
||||
;; (let [[token] (::mx/args own)]
|
||||
;; (st/emit! (uda/validate-recovery-token token))
|
||||
;; own))
|
||||
|
||||
(mx/defc recovery-page
|
||||
{:mixins [mx/static (fm/clear-mixin st/store :recovery)]
|
||||
:init recovery-page-init}
|
||||
[token]
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
(messages-widget)
|
||||
[:a i/logo]
|
||||
(recovery-form token)]])
|
||||
;; (mx/defc recovery-page
|
||||
;; {:mixins [mx/static (fm/clear-mixin st/store :recovery)]
|
||||
;; :init recovery-page-init}
|
||||
;; [token]
|
||||
;; [:div.login
|
||||
;; [:div.login-body
|
||||
;; (messages-widget)
|
||||
;; [:a i/logo]
|
||||
;; (recovery-form token)]])
|
||||
|
|
|
@ -20,52 +20,52 @@
|
|||
[rumext.core :as mx :include-macros true]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
(def form-data (fm/focus-data :recovery-request st/state))
|
||||
(def form-errors (fm/focus-errors :recovery-request st/state))
|
||||
;; (def form-data (fm/focus-data :recovery-request st/state))
|
||||
;; (def form-errors (fm/focus-errors :recovery-request st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :profile-password))
|
||||
(def assoc-errors (partial fm/assoc-errors :profile-password))
|
||||
(def clear-form (partial fm/clear-form :profile-password))
|
||||
;; (def assoc-value (partial fm/assoc-value :profile-password))
|
||||
;; (def assoc-errors (partial fm/assoc-errors :profile-password))
|
||||
;; (def clear-form (partial fm/clear-form :profile-password))
|
||||
|
||||
(s/def ::username ::fm/non-empty-string)
|
||||
(s/def ::recovery-request-form (s/keys :req-un [::username]))
|
||||
;; (s/def ::username ::fm/non-empty-string)
|
||||
;; (s/def ::recovery-request-form (s/keys :req-un [::username]))
|
||||
|
||||
(mx/defc recovery-request-form
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
[]
|
||||
(let [data (mx/react form-data)
|
||||
valid? (fm/valid? ::recovery-request-form data)]
|
||||
(letfn [(on-change [event]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value :username value))))
|
||||
(on-submit [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (uda/recovery-request data)
|
||||
(clear-form)))]
|
||||
[:form {:on-submit on-submit}
|
||||
[:div.login-content
|
||||
[:input.input-text
|
||||
{:name "username"
|
||||
:value (:username data "")
|
||||
:on-change on-change
|
||||
:placeholder (tr "recovery-request.username-or-email.placeholder")
|
||||
:type "text"}]
|
||||
[:input.btn-primary
|
||||
{:name "login"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:value (tr "recovery-request.recover-password")
|
||||
:type "submit"}]
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]])))
|
||||
;; (mx/defc recovery-request-form
|
||||
;; {:mixins [mx/static mx/reactive]}
|
||||
;; []
|
||||
;; (let [data (mx/react form-data)
|
||||
;; valid? (fm/valid? ::recovery-request-form data)]
|
||||
;; (letfn [(on-change [event]
|
||||
;; (let [value (dom/event->value event)]
|
||||
;; (st/emit! (assoc-value :username value))))
|
||||
;; (on-submit [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (st/emit! (uda/recovery-request data)
|
||||
;; (clear-form)))]
|
||||
;; [:form {:on-submit on-submit}
|
||||
;; [:div.login-content
|
||||
;; [:input.input-text
|
||||
;; {:name "username"
|
||||
;; :value (:username data "")
|
||||
;; :on-change on-change
|
||||
;; :placeholder (tr "recovery-request.username-or-email.placeholder")
|
||||
;; :type "text"}]
|
||||
;; [:input.btn-primary
|
||||
;; {:name "login"
|
||||
;; :class (when-not valid? "btn-disabled")
|
||||
;; :disabled (not valid?)
|
||||
;; :value (tr "recovery-request.recover-password")
|
||||
;; :type "submit"}]
|
||||
;; [:div.login-links
|
||||
;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]])))
|
||||
|
||||
;; --- Recovery Request Page
|
||||
;; ;; --- Recovery Request Page
|
||||
|
||||
(mx/defc recovery-request-page
|
||||
{:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]}
|
||||
[]
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
(messages-widget)
|
||||
[:a i/logo]
|
||||
(recovery-request-form)]])
|
||||
;; (mx/defc recovery-request-page
|
||||
;; {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]}
|
||||
;; []
|
||||
;; [:div.login
|
||||
;; [:div.login-body
|
||||
;; (messages-widget)
|
||||
;; [:a i/logo]
|
||||
;; (recovery-request-form)]])
|
||||
|
|
|
@ -6,118 +6,134 @@
|
|||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.auth.register
|
||||
(:require [cljs.spec.alpha :as s :include-macros true]
|
||||
[lentes.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
[uxbox.util.router :as rt]))
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :refer [tr]]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
(def form-data (fm/focus-data :register st/state))
|
||||
(def form-errors (fm/focus-errors :register st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :register))
|
||||
(def assoc-error (partial fm/assoc-error :register))
|
||||
(def clear-form (partial fm/clear-form :register))
|
||||
|
||||
;; TODO: add better password validation
|
||||
|
||||
(s/def ::username ::fm/non-empty-string)
|
||||
(s/def ::fullname ::fm/non-empty-string)
|
||||
(s/def ::password ::fm/non-empty-string)
|
||||
(s/def ::username ::fm/not-empty-string)
|
||||
(s/def ::fullname ::fm/not-empty-string)
|
||||
(s/def ::password ::fm/not-empty-string)
|
||||
(s/def ::email ::fm/email)
|
||||
|
||||
(s/def ::register-form
|
||||
(s/keys :req-un [::username
|
||||
::password
|
||||
::fullname
|
||||
::email
|
||||
::password]))
|
||||
::email]))
|
||||
|
||||
(mx/defc register-form
|
||||
{:mixins [mx/static mx/reactive
|
||||
(fm/clear-mixin st/store :register)]}
|
||||
[]
|
||||
(let [data (mx/react form-data)
|
||||
errors (mx/react form-errors)
|
||||
valid? (fm/valid? ::register-form data)]
|
||||
(letfn [(on-change [field event]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
(on-error [{:keys [type code] :as payload}]
|
||||
(case code
|
||||
:uxbox.services.users/registration-disabled
|
||||
(st/emit! (tr "errors.api.form.registration-disabled"))
|
||||
:uxbox.services.users/email-already-exists
|
||||
(st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists")))
|
||||
:uxbox.services.users/username-already-exists
|
||||
(st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists")))))
|
||||
(on-submit [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (uda/register data on-error)))]
|
||||
[:form {:on-submit on-submit}
|
||||
[:div.login-content
|
||||
[:input.input-text
|
||||
{:name "fullname"
|
||||
:tab-index "2"
|
||||
:value (:fullname data "")
|
||||
:on-change (partial on-change :fullname)
|
||||
:placeholder (tr "register.fullname.placeholder")
|
||||
:type "text"}]
|
||||
(fm/input-error errors :fullname)
|
||||
(defn- on-error
|
||||
[error form]
|
||||
(case (:code error)
|
||||
:uxbox.services.users/registration-disabled
|
||||
(st/emit! (tr "errors.api.form.registration-disabled"))
|
||||
|
||||
[:input.input-text
|
||||
{:name "username"
|
||||
:tab-index "3"
|
||||
:value (:username data "")
|
||||
:on-change (partial on-change :username)
|
||||
:placeholder (tr "register.username.placeholder")
|
||||
:type "text"}]
|
||||
(fm/input-error errors :username)
|
||||
:uxbox.services.users/email-already-exists
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:type ::api
|
||||
:message "errors.api.form.email-already-exists"})
|
||||
|
||||
[:input.input-text
|
||||
{:name "email"
|
||||
:tab-index "4"
|
||||
:ref "email"
|
||||
:value (:email data "")
|
||||
:on-change (partial on-change :email)
|
||||
:placeholder (tr "register.email.placeholder")
|
||||
:type "text"}]
|
||||
(fm/input-error errors :email)
|
||||
:uxbox.services.users/username-already-exists
|
||||
(swap! form assoc-in [:errors :username]
|
||||
{:type ::api
|
||||
:message "errors.api.form.username-already-exists"})))
|
||||
|
||||
[:input.input-text
|
||||
{:name "password"
|
||||
:tab-index "5"
|
||||
:ref "password"
|
||||
:value (:password data "")
|
||||
:on-change (partial on-change :password)
|
||||
:placeholder (tr "register.password.placeholder")
|
||||
:type "password"}]
|
||||
(fm/input-error errors :password)
|
||||
(defn- on-submit
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(let [data (:clean-data form)
|
||||
on-error #(on-error % form)]
|
||||
(st/emit! (uda/register data on-error))))
|
||||
|
||||
[:input.btn-primary
|
||||
{:name "login"
|
||||
:tab-index "6"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:value (tr "register.get-started")
|
||||
:type "submit"}]
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "register.already-have-account")]]]])))
|
||||
(mf/defc register-form
|
||||
[props]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::register-form {})]
|
||||
[:form {:on-submit #(on-submit % form)}
|
||||
[:div.login-content
|
||||
[:input.input-text
|
||||
{:name "fullname"
|
||||
:tab-index "1"
|
||||
:value (:fullname data "")
|
||||
:class (fm/error-class form :fullname)
|
||||
:on-blur (fm/on-input-blur form :fullname)
|
||||
:on-change (fm/on-input-change form :fullname)
|
||||
:placeholder (tr "register.fullname.placeholder")
|
||||
:type "text"}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :fullname}]
|
||||
|
||||
[:input.input-text
|
||||
{:type "text"
|
||||
:name "username"
|
||||
:tab-index "2"
|
||||
:class (fm/error-class form :username)
|
||||
:on-blur (fm/on-input-blur form :username)
|
||||
:on-change (fm/on-input-change form :username)
|
||||
:value (:username data "")
|
||||
:placeholder (tr "settings.profile.your-username")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :username}]
|
||||
|
||||
[:input.input-text
|
||||
{:type "email"
|
||||
:name "email"
|
||||
:tab-index "3"
|
||||
:class (fm/error-class form :email)
|
||||
:on-blur (fm/on-input-blur form :email)
|
||||
:on-change (fm/on-input-change form :email)
|
||||
:value (:email data "")
|
||||
:placeholder (tr "settings.profile.your-email")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :email}]
|
||||
|
||||
|
||||
[:input.input-text
|
||||
{:name "password"
|
||||
:tab-index "4"
|
||||
:value (:password data "")
|
||||
:class (fm/error-class form :password)
|
||||
:on-blur (fm/on-input-blur form :password)
|
||||
:on-change (fm/on-input-change form :password)
|
||||
:placeholder (tr "register.password.placeholder")
|
||||
:type "password"}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :email}]
|
||||
|
||||
[:input.btn-primary
|
||||
{:type "submit"
|
||||
:tab-index "5"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "register.get-started")}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth/login))}
|
||||
(tr "register.already-have-account")]]]]))
|
||||
|
||||
;; --- Register Page
|
||||
|
||||
(mx/defc register-page
|
||||
{:mixins [mx/static]}
|
||||
[own]
|
||||
(mf/defc register-page
|
||||
[props]
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
(messages-widget)
|
||||
[:a i/logo]
|
||||
(register-form)]])
|
||||
[:& register-form]]])
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
[{:keys [route] :as props}]
|
||||
(let [[section type id] (parse-route route)]
|
||||
[:main.dashboard-main
|
||||
(messages-widget)
|
||||
[:& messages-widget]
|
||||
[:& header {:section section}]
|
||||
(case section
|
||||
:dashboard/icons
|
||||
|
|
|
@ -9,20 +9,18 @@
|
|||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.data.projects :as udp]
|
||||
[uxbox.main.exports :as exports]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.dashboard.projects-createform]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
[uxbox.util.blob :as blob]
|
||||
[uxbox.main.ui.confirm :refer [confirm-dialog]]
|
||||
[uxbox.main.ui.dashboard.projects-forms :refer [create-project-dialog]]
|
||||
[uxbox.util.data :refer [read-string]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.i18n :as t :refer (tr)]
|
||||
[uxbox.util.i18n :as t :refer [tr]]
|
||||
[uxbox.util.router :as r]
|
||||
[uxbox.util.time :as dt]))
|
||||
|
||||
|
@ -34,15 +32,14 @@
|
|||
|
||||
;; --- Refs
|
||||
|
||||
(def opts-ref
|
||||
(def opts-iref
|
||||
(-> (l/in [:dashboard :projects])
|
||||
(l/derive st/state)))
|
||||
|
||||
(def projects-ref
|
||||
(def projects-iref
|
||||
(-> (l/key :projects)
|
||||
(l/derive st/state)))
|
||||
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn sort-projects-by
|
||||
|
@ -117,26 +114,9 @@
|
|||
|
||||
(mf/defc grid-item-thumbnail
|
||||
[{:keys [project] :as props}]
|
||||
(let [url (mf/use-state nil)]
|
||||
#_(mf/use-effect
|
||||
{:deps #js [(:page-id project)]
|
||||
:init (fn []
|
||||
(when-let [page-id (:page-id project)]
|
||||
(let [svg (exports/render-page page-id)
|
||||
uri (some-> svg
|
||||
(blob/create "image/svg+xml")
|
||||
(blob/create-uri))]
|
||||
(reset! url uri)
|
||||
uri)))
|
||||
:end #(when % (blob/revoke-uri %))})
|
||||
(if @url
|
||||
[:div.grid-item-th
|
||||
{:style {:background-image (str "url('" @url "')")}}]
|
||||
[:div.grid-item-th
|
||||
[:img.img-th {:src "/images/project-placeholder.svg"
|
||||
:alt (tr "ds.project-thumbnail.alt")}]])))
|
||||
|
||||
|
||||
[:div.grid-item-th
|
||||
[:img.img-th {:src "/images/project-placeholder.svg"
|
||||
:alt (tr "ds.project-thumbnail.alt")}]])
|
||||
|
||||
;; --- Grid Item
|
||||
|
||||
|
@ -148,7 +128,7 @@
|
|||
delete #(st/emit! (udp/delete-project project))
|
||||
on-delete #(do
|
||||
(dom/stop-propagation %)
|
||||
(udl/open! :confirm {:on-accept delete}))
|
||||
(modal/show! confirm-dialog {:on-accept delete}))
|
||||
on-blur #(let [target (dom/event->target %)
|
||||
name (dom/get-value target)
|
||||
id (:id project)]
|
||||
|
@ -193,13 +173,14 @@
|
|||
[{:keys [opts] :as props}]
|
||||
(let [order (:order opts :created)
|
||||
filter (:filter opts "")
|
||||
projects (mf/deref projects-ref)
|
||||
projects (mf/deref projects-iref)
|
||||
projects (->> (vals projects)
|
||||
(filter-projects-by filter)
|
||||
(sort-projects-by order))
|
||||
on-click #(do
|
||||
(dom/prevent-default %)
|
||||
(udl/open! :create-project))]
|
||||
(modal/show! create-project-dialog {})
|
||||
#_(udl/open! :create-project))]
|
||||
[:section.dashboard-grid
|
||||
[:h2 (tr "ds.project-title")]
|
||||
[:div.dashboard-grid-content
|
||||
|
@ -214,7 +195,7 @@
|
|||
(mf/defc projects-page
|
||||
[_]
|
||||
(mf/use-effect #(st/emit! (udp/initialize)))
|
||||
(let [opts (mf/deref opts-ref)]
|
||||
(let [opts (mf/deref opts-iref)]
|
||||
[:section.dashboard-content
|
||||
[:& menu {:opts opts}]
|
||||
[:& grid {:opts opts}]]))
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
;; 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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.dashboard.projects-createform
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.data.projects :as udp]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.lightbox :as lbx]
|
||||
[uxbox.util.data :refer [read-string parse-int]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :as t :refer [tr]]
|
||||
[uxbox.util.router :as r]
|
||||
[uxbox.util.time :as dt]))
|
||||
|
||||
(def form-data (fm/focus-data :create-project st/state))
|
||||
(def form-errors (fm/focus-errors :create-project st/state))
|
||||
(def assoc-value (partial fm/assoc-value :create-project))
|
||||
(def clear-form (partial fm/clear-form :create-project))
|
||||
|
||||
(s/def ::name ::fm/non-empty-string)
|
||||
(s/def ::layout ::fm/non-empty-string)
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
|
||||
(s/def ::project-form
|
||||
(s/keys :req-un [::name
|
||||
::width
|
||||
::height
|
||||
::layout]))
|
||||
|
||||
;; --- Create Project Form
|
||||
|
||||
(mx/defc layout-input
|
||||
[{:keys [::layout-id] :as data}]
|
||||
(let [layout (get c/page-layouts layout-id)]
|
||||
[:div
|
||||
[:input {:type "radio"
|
||||
:key layout-id
|
||||
:id layout-id
|
||||
:name "project-layout"
|
||||
:value (:name layout)
|
||||
:checked (= layout-id (:layout data))
|
||||
:on-change #(st/emit! (assoc-value :layout layout-id)
|
||||
(assoc-value :width (:width layout))
|
||||
(assoc-value :height (:height layout)))}]
|
||||
[:label {:value (:name layout)
|
||||
:for layout-id}
|
||||
(:name layout)]]))
|
||||
|
||||
(mx/defc layout-selector
|
||||
[props]
|
||||
[:div.input-radio.radio-primary
|
||||
(layout-input (assoc props ::layout-id "mobile"))
|
||||
(layout-input (assoc props ::layout-id "tablet"))
|
||||
(layout-input (assoc props ::layout-id "notebook"))
|
||||
(layout-input (assoc props ::layout-id "desktop"))])
|
||||
|
||||
(mf/def create-project-form
|
||||
:mixins #{mf/reactive}
|
||||
:render
|
||||
(fn [own props]
|
||||
(let [data (merge c/project-defaults (mf/react form-data))
|
||||
valid? (fm/valid? ::project-form data)]
|
||||
(letfn [(on-submit [event]
|
||||
(dom/prevent-default event)
|
||||
(when valid?
|
||||
(st/emit! (udp/create-project data)
|
||||
(udl/close-lightbox))))
|
||||
|
||||
(update-size [field e]
|
||||
(let [value (dom/event->value e)
|
||||
value (parse-int value)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
|
||||
(update-name [e]
|
||||
(let [value (dom/event->value e)]
|
||||
(st/emit! (assoc-value :name value))))
|
||||
(swap-size []
|
||||
(st/emit! (assoc-value :width (:height data))
|
||||
(assoc-value :height (:width data))))]
|
||||
[:form {:on-submit on-submit}
|
||||
[:input#project-name.input-text
|
||||
{:placeholder (tr "ds.project.placeholder")
|
||||
:type "text"
|
||||
:value (:name data)
|
||||
:auto-focus true
|
||||
:on-change update-name}]
|
||||
[:div.project-size
|
||||
[:div.input-element.pixels
|
||||
[:span (tr "ds.width")]
|
||||
[:input#project-witdh.input-text
|
||||
{:placeholder (tr "ds.width")
|
||||
:type "number"
|
||||
:min 0 ;;TODO check this value
|
||||
:max 666666 ;;TODO check this value
|
||||
:value (str (:width data))
|
||||
:on-change (partial update-size :width)}]]
|
||||
[:a.toggle-layout {:on-click swap-size} i/toggle]
|
||||
[:div.input-element.pixels
|
||||
[:span (tr "ds.height")]
|
||||
[:input#project-height.input-text
|
||||
{:placeholder (tr "ds.height")
|
||||
:type "number"
|
||||
:min 0 ;;TODO check this value
|
||||
:max 666666 ;;TODO check this value
|
||||
:value (str (:height data))
|
||||
:on-change (partial update-size :height)}]]]
|
||||
|
||||
;; Layout selector
|
||||
(layout-selector data)
|
||||
|
||||
;; Submit
|
||||
[:input#project-btn.btn-primary
|
||||
{:value (tr "ds.go")
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:type "submit"}]]))))
|
||||
|
||||
;; --- Create Project Lightbox
|
||||
|
||||
(mf/def create-project-lightbox
|
||||
:will-unmount
|
||||
(fn [own]
|
||||
(st/emit! (fm/clear-form :create-project))
|
||||
own)
|
||||
|
||||
:render
|
||||
(fn [own]
|
||||
[:div.lightbox-body
|
||||
[:h3 (tr "ds.project.new")]
|
||||
(create-project-form)
|
||||
[:a.close {:on-click #(st/emit! (udl/close-lightbox))}
|
||||
i/close]]))
|
||||
|
||||
(defmethod lbx/render-lightbox :create-project
|
||||
[_]
|
||||
(create-project-lightbox))
|
||||
|
102
frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs
Normal file
102
frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs
Normal file
|
@ -0,0 +1,102 @@
|
|||
;; 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) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.dashboard.projects-forms
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.projects :as udp]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :as t :refer [tr]]))
|
||||
|
||||
(s/def ::name ::fm/not-empty-string)
|
||||
(s/def ::width ::fm/number-str)
|
||||
(s/def ::height ::fm/number-str)
|
||||
|
||||
(s/def ::project-form
|
||||
(s/keys :req-un [::name ::width ::height]))
|
||||
|
||||
(def defaults
|
||||
{:name ""
|
||||
:width "1366"
|
||||
:height "768"})
|
||||
|
||||
;; --- Create Project Form
|
||||
|
||||
(defn- on-submit
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(let [data (:clean-data form)]
|
||||
(st/emit! (udp/create-project data))
|
||||
(modal/hide!)))
|
||||
|
||||
(defn- swap-size
|
||||
[event {:keys [data] :as form}]
|
||||
(swap! data assoc
|
||||
:width (:height data)
|
||||
:height (:width data)))
|
||||
|
||||
(mf/defc create-project-form
|
||||
[props]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::project-form defaults)]
|
||||
[:form {:on-submit #(on-submit % form)}
|
||||
[:input.input-text
|
||||
{:placeholder "New project name"
|
||||
:type "text"
|
||||
:name "name"
|
||||
:value (:name data)
|
||||
:class (fm/error-class form :name)
|
||||
:on-blur (fm/on-input-blur form :name)
|
||||
:on-change (fm/on-input-change form :name)
|
||||
:auto-focus true}]
|
||||
[:div.project-size
|
||||
[:div.input-element.pixels
|
||||
[:span "Width"]
|
||||
[:input#project-witdh.input-text
|
||||
{:placeholder "Width"
|
||||
:name "width"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 5000
|
||||
:class (fm/error-class form :width)
|
||||
:on-blur (fm/on-input-blur form :width)
|
||||
:on-change (fm/on-input-change form :width)
|
||||
:value (:width data)}]]
|
||||
[:a.toggle-layout {:on-click #(swap-size % form)} i/toggle]
|
||||
[:div.input-element.pixels
|
||||
[:span "Height"]
|
||||
[:input#project-height.input-text
|
||||
{:placeholder "Height"
|
||||
:type "number"
|
||||
:name "height"
|
||||
:min 0
|
||||
:max 5000
|
||||
:class (fm/error-class form :height)
|
||||
:on-blur (fm/on-input-blur form :height)
|
||||
:on-change (fm/on-input-change form :height)
|
||||
:value (:height data)}]]]
|
||||
|
||||
;; Submit
|
||||
[:input#project-btn.btn-primary
|
||||
{:value "Go go go!"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:type "submit"}]]))
|
||||
|
||||
;; --- Create Project Lightbox
|
||||
|
||||
(mf/defc create-project-dialog
|
||||
[props]
|
||||
[:div.lightbox-body
|
||||
[:h3 "New project"]
|
||||
[:& create-project-form]
|
||||
[:a.close {:on-click modal/hide!} i/close]])
|
||||
|
|
@ -2,17 +2,17 @@
|
|||
;; 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) 2016-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.ui.loader
|
||||
(:require [uxbox.main.store :as st]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[rumext.core :as mx :include-macros true]))
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.store :as st]))
|
||||
|
||||
;; --- Component
|
||||
|
||||
(mx/defc loader
|
||||
{:mixins [mx/reactive mx/static]}
|
||||
(mf/defc loader
|
||||
[]
|
||||
(when (mx/react st/loader)
|
||||
(when (mf/deref st/loader)
|
||||
[:div.loader-content i/loader]))
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
(ns uxbox.main.ui.messages
|
||||
(:require [lentes.core :as l]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.messages :as uum]
|
||||
[rumext.core :as mx :include-macros true]))
|
||||
(:require
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.messages :as um]))
|
||||
|
||||
(def ^:private message-ref
|
||||
(def ^:private message-iref
|
||||
(-> (l/key :message)
|
||||
(l/derive st/state)))
|
||||
|
||||
(mx/defc messages-widget
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
(mf/defc messages-widget
|
||||
[]
|
||||
(let [message (mx/react message-ref)
|
||||
on-close #(st/emit! (uum/hide))]
|
||||
(uum/messages-widget (assoc message :on-close on-close))))
|
||||
(let [message (mf/deref message-iref)
|
||||
on-close #(st/emit! (um/hide))]
|
||||
[:& um/messages-widget {:message message
|
||||
:on-close on-close}]))
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
[{:keys [route] :as props}]
|
||||
(let [section (get-in route [:data :name])]
|
||||
[:main.dashboard-main
|
||||
(messages-widget)
|
||||
[:& messages-widget]
|
||||
[:& header {:section section}]
|
||||
(case section
|
||||
:settings/profile (mf/element profile/profile-page)
|
||||
|
|
|
@ -2,99 +2,93 @@
|
|||
;; 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) 2016-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2016-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2016-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.settings.password
|
||||
(:require [cljs.spec.alpha :as s :include-macros true]
|
||||
[lentes.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.data.users :as udu]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.settings.header :refer [header]]
|
||||
[uxbox.util.i18n :refer [tr]]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.messages :as um]
|
||||
[rumext.core :as mx :include-macros true]))
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cljs.spec.alpha :as s]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.users :as udu]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :refer [tr]]
|
||||
[uxbox.util.messages :as um]))
|
||||
|
||||
(def form-data (fm/focus-data :profile-password st/state))
|
||||
(def form-errors (fm/focus-errors :profile-password st/state))
|
||||
(defn- on-error
|
||||
[form error]
|
||||
(case (:code error)
|
||||
:uxbox.services.users/old-password-not-match
|
||||
(swap! form assoc-in [:errors :password-old]
|
||||
{:type ::api :message "settings.password.wrong-old-password"})
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :profile-password))
|
||||
(def assoc-error (partial fm/assoc-error :profile-password))
|
||||
(def clear-form (partial fm/clear-form :profile-password))
|
||||
:else (throw (ex-info "unexpected" {:error error}))))
|
||||
|
||||
;; TODO: add better password validation
|
||||
(defn- on-submit
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(let [data (:clean-data form)
|
||||
opts {:on-success #(st/emit! (um/info (tr "settings.password.password-saved")))
|
||||
:on-error #(on-error form %)}]
|
||||
(st/emit! (udu/update-password data opts))))
|
||||
|
||||
(s/def ::password-1 ::fm/non-empty-string)
|
||||
(s/def ::password-2 ::fm/non-empty-string)
|
||||
(s/def ::password-old ::fm/non-empty-string)
|
||||
(s/def ::password-1 ::fm/not-empty-string)
|
||||
(s/def ::password-2 ::fm/not-empty-string)
|
||||
(s/def ::password-old ::fm/not-empty-string)
|
||||
|
||||
(s/def ::password-form
|
||||
(s/keys :req-un [::password-1
|
||||
::password-2
|
||||
::password-old]))
|
||||
|
||||
(mx/defc password-form
|
||||
{:mixins [mx/reactive mx/static]}
|
||||
[]
|
||||
(let [data (mx/react form-data)
|
||||
errors (mx/react form-errors)
|
||||
valid? (fm/valid? ::password-form data)]
|
||||
(letfn [(on-change [field event]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
(on-success []
|
||||
(st/emit! (um/info (tr "settings.password.password-saved"))))
|
||||
(on-error [{:keys [code] :as payload}]
|
||||
(case code
|
||||
:uxbox.services.users/old-password-not-match
|
||||
(st/emit! (assoc-error :password-old (tr "settings.password.wrong-old-password")))
|
||||
(mf/defc password-form
|
||||
[props]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::password-form {})]
|
||||
[:form.password-form {:on-submit #(on-submit % form)}
|
||||
[:span.user-settings-label (tr "settings.password.change-password")]
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:name "password-old"
|
||||
:value (:password-old data "")
|
||||
:class (fm/error-class form :password-old)
|
||||
:on-blur (fm/on-input-blur form :password-old)
|
||||
:on-change (fm/on-input-change form :password-old)
|
||||
:placeholder (tr "settings.password.old-password")}]
|
||||
|
||||
:else
|
||||
(throw (ex-info "unexpected" {:error payload}))))
|
||||
(on-submit [event]
|
||||
(st/emit! (udu/update-password data
|
||||
:on-success on-success
|
||||
:on-error on-error)))]
|
||||
[:form.password-form
|
||||
[:span.user-settings-label (tr "settings.password.change-password")]
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:class (fm/error-class errors :password-old)
|
||||
:value (:password-old data "")
|
||||
:on-change (partial on-change :password-old)
|
||||
:placeholder (tr "settings.password.old-password")}]
|
||||
(fm/input-error errors :password-old)
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:class (fm/error-class errors :password-1)
|
||||
:value (:password-1 data "")
|
||||
:on-change (partial on-change :password-1)
|
||||
:placeholder (tr "settings.password.new-password")}]
|
||||
(fm/input-error errors :password-1)
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:class (fm/error-class errors :password-2)
|
||||
:value (:password-2 data "")
|
||||
:on-change (partial on-change :password-2)
|
||||
:placeholder (tr "settings.password.confirm-password")}]
|
||||
(fm/input-error errors :password-2)
|
||||
[:input.btn-primary
|
||||
{:type "button"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:on-click on-submit
|
||||
:value (tr "settings.update-settings")}]])))
|
||||
[:& fm/field-error {:form form :field :password-old :type ::api}]
|
||||
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:name "password-1"
|
||||
:value (:password-1 data "")
|
||||
:class (fm/error-class form :password-1)
|
||||
:on-blur (fm/on-input-blur form :password-1)
|
||||
:on-change (fm/on-input-change form :password-1)
|
||||
:placeholder (tr "settings.password.new-password")}]
|
||||
;; [:& fm/field-error {:form form :field :password-1}]
|
||||
|
||||
[:input.input-text
|
||||
{:type "password"
|
||||
:name "password-2"
|
||||
:value (:password-2 data "")
|
||||
:class (fm/error-class form :password-2)
|
||||
:on-blur (fm/on-input-blur form :password-2)
|
||||
:on-change (fm/on-input-change form :password-2)
|
||||
:placeholder (tr "settings.password.confirm-password")}]
|
||||
;; [:& fm/field-error {:form form :field :password-2}]
|
||||
|
||||
[:input.btn-primary
|
||||
{:type "submit"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "settings.update-settings")}]]))
|
||||
|
||||
;; --- Password Page
|
||||
|
||||
(mx/defc password-page
|
||||
[]
|
||||
(mf/defc password-page
|
||||
[props]
|
||||
[:section.dashboard-content.user-settings
|
||||
[:section.user-settings-content
|
||||
(password-form)]])
|
||||
[:& password-form]]])
|
||||
|
|
|
@ -14,35 +14,28 @@
|
|||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.users :as udu]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.data :refer [read-string]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.util.interop :refer [iterable->seq]]))
|
||||
[uxbox.util.interop :refer [iterable->seq]]
|
||||
[uxbox.util.messages :as um]))
|
||||
|
||||
|
||||
(def form-data (fm/focus-data :profile st/state))
|
||||
(def form-errors (fm/focus-errors :profile st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :profile))
|
||||
(def assoc-error (partial fm/assoc-error :profile))
|
||||
(def clear-form (partial fm/clear-form :profile))
|
||||
|
||||
(defn profile->form
|
||||
(defn- profile->form
|
||||
[profile]
|
||||
(let [language (get-in profile [:metadata :language])]
|
||||
(-> (select-keys profile [:fullname :username :email])
|
||||
(cond-> language (assoc :language language)))))
|
||||
|
||||
(def profile-ref
|
||||
(-> (comp (l/key :profile)
|
||||
(l/lens profile->form))
|
||||
(def ^:private profile-ref
|
||||
(-> (l/key :profile)
|
||||
(l/derive st/state)))
|
||||
|
||||
(s/def ::fullname ::fm/non-empty-string)
|
||||
(s/def ::username ::fm/non-empty-string)
|
||||
(s/def ::fullname ::fm/not-empty-string)
|
||||
(s/def ::username ::fm/not-empty-string)
|
||||
(s/def ::language ::fm/not-empty-string)
|
||||
(s/def ::email ::fm/email)
|
||||
(s/def ::language #{"en" "fr"})
|
||||
|
||||
(s/def ::profile-form
|
||||
(s/keys :req-un [::fullname
|
||||
|
@ -51,69 +44,94 @@
|
|||
::email]))
|
||||
|
||||
(defn- on-error
|
||||
[{:keys [code] :as payload}]
|
||||
(case code
|
||||
:uxbox.services.users/registration-disabled
|
||||
(st/emit! (tr "errors.api.form.registration-disabled"))
|
||||
[error form]
|
||||
(case (:code error)
|
||||
:uxbox.services.users/email-already-exists
|
||||
(st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists")))
|
||||
:uxbox.services.users/username-already-exists
|
||||
(st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists")))))
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:type ::api
|
||||
:message "errors.api.form.email-already-exists"})
|
||||
|
||||
(defn- on-field-change
|
||||
[event field]
|
||||
(let [value (dom/event->value event)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
:uxbox.services.users/username-already-exists
|
||||
(swap! form assoc-in [:errors :username]
|
||||
{:type ::api
|
||||
:message "errors.api.form.username-already-exists"})))
|
||||
|
||||
(defn- initial-data
|
||||
[]
|
||||
(merge {:language @i18n/locale}
|
||||
(profile->form (deref profile-ref))))
|
||||
|
||||
(defn- on-submit
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(let [data (:clean-data form)
|
||||
on-success #(st/emit! (um/info (tr "settings.profile.profile-saved")))
|
||||
on-error #(on-error % form)]
|
||||
(st/emit! (udu/form->update-profile data on-success on-error))))
|
||||
|
||||
;; --- Profile Form
|
||||
(mf/def profile-form
|
||||
:mixins [mf/memo mf/reactive mf/sync-render (fm/clear-mixin st/store :profile)]
|
||||
:render
|
||||
(fn [own props]
|
||||
(let [data (merge {:language @i18n/locale}
|
||||
(mf/react profile-ref)
|
||||
(mf/react form-data))
|
||||
errors (mf/react form-errors)
|
||||
valid? (fm/valid? ::profile-form data)
|
||||
on-success #(st/emit! (clear-form))
|
||||
on-submit #(st/emit! (udu/update-profile data on-success on-error))]
|
||||
[:form.profile-form
|
||||
[:span.user-settings-label (tr "settings.profile.section-basic-data")]
|
||||
[:input.input-text
|
||||
{:type "text"
|
||||
:on-change #(on-field-change % :fullname)
|
||||
:value (:fullname data "")
|
||||
:placeholder (tr "settings.profile.your-name")}]
|
||||
[:input.input-text
|
||||
{:type "text"
|
||||
:on-change #(on-field-change % :username)
|
||||
:value (:username data "")
|
||||
:placeholder (tr "settings.profile.your-username")}]
|
||||
(fm/input-error errors :username)
|
||||
[:input.input-text
|
||||
{:type "email"
|
||||
:on-change #(on-field-change % :email)
|
||||
:value (:email data "")
|
||||
:placeholder (tr "settings.profile.your-email")}]
|
||||
(fm/input-error errors :email)
|
||||
|
||||
[:span.user-settings-label (tr "settings.profile.section-i18n-data")]
|
||||
[:select.input-select {:value (:language data)
|
||||
:on-change #(on-field-change % :language)}
|
||||
[:option {:value "en"} "English"]
|
||||
[:option {:value "fr"} "Français"]]
|
||||
(mf/defc profile-form
|
||||
[props]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::profile-form initial-data)]
|
||||
[:form.profile-form {:on-submit #(on-submit % form)}
|
||||
[:span.user-settings-label (tr "settings.profile.section-basic-data")]
|
||||
[:input.input-text
|
||||
{:type "text"
|
||||
:name "fullname"
|
||||
:class (fm/error-class form :fullname)
|
||||
:on-blur (fm/on-input-blur form :fullname)
|
||||
:on-change (fm/on-input-change form :fullname)
|
||||
:value (:fullname data "")
|
||||
:placeholder (tr "settings.profile.your-name")}]
|
||||
|
||||
[:input.btn-primary
|
||||
{:type "button"
|
||||
:class (when-not valid? "btn-disabled")
|
||||
:disabled (not valid?)
|
||||
:on-click on-submit
|
||||
:value (tr "settings.update-settings")}]])))
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :fullname}]
|
||||
[:input.input-text
|
||||
{:type "text"
|
||||
:name "username"
|
||||
:class (fm/error-class form :username)
|
||||
:on-blur (fm/on-input-blur form :username)
|
||||
:on-change (fm/on-input-change form :username)
|
||||
:value (:username data "")
|
||||
:placeholder (tr "settings.profile.your-username")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :username}]
|
||||
|
||||
[:input.input-text
|
||||
{:type "email"
|
||||
:name "email"
|
||||
:class (fm/error-class form :email)
|
||||
:on-blur (fm/on-input-blur form :email)
|
||||
:on-change (fm/on-input-change form :email)
|
||||
:value (:email data "")
|
||||
:placeholder (tr "settings.profile.your-email")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
:field :email}]
|
||||
|
||||
[:span.user-settings-label (tr "settings.profile.section-i18n-data")]
|
||||
[:select.input-select {:value (:language data)
|
||||
:name "language"
|
||||
:class (fm/error-class form :language)
|
||||
:on-blur (fm/on-input-blur form :language)
|
||||
:on-change (fm/on-input-change form :language)}
|
||||
[:option {:value "en"} "English"]
|
||||
[:option {:value "fr"} "Français"]]
|
||||
|
||||
[:input.btn-primary
|
||||
{:type "submit"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "settings.update-settings")}]]))
|
||||
|
||||
;; --- Profile Photo Form
|
||||
|
||||
(mf/defc profile-photo-form
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[]
|
||||
(letfn [(on-change [event]
|
||||
(let [target (dom/get-target event)
|
||||
|
@ -122,7 +140,7 @@
|
|||
(first))]
|
||||
(st/emit! (udu/update-photo file))
|
||||
(dom/clean-value! target)))]
|
||||
(let [{:keys [photo]} (mf/react profile-ref)
|
||||
(let [{:keys [photo] :as profile} (mf/deref profile-ref)
|
||||
photo (if (or (str/empty? photo) (nil? photo))
|
||||
"images/avatar.jpg"
|
||||
photo)]
|
||||
|
@ -140,4 +158,4 @@
|
|||
[:section.user-settings-content
|
||||
[:span.user-settings-label (tr "settings.profile.your-avatar")]
|
||||
[:& profile-photo-form]
|
||||
(profile-form)]])
|
||||
[:& profile-form]]])
|
||||
|
|
|
@ -14,12 +14,15 @@
|
|||
[uxbox.main.ui.shapes.image :as image]
|
||||
[uxbox.main.ui.shapes.path :as path]
|
||||
[uxbox.main.ui.shapes.rect :as rect]
|
||||
[uxbox.main.ui.shapes.canvas :as canvas]
|
||||
[uxbox.main.ui.shapes.text :as text]))
|
||||
|
||||
(defn render-shape
|
||||
[shape]
|
||||
(mf/html
|
||||
(case (:type shape)
|
||||
:canvas [:& canvas/canvas-component {:shape shape}]
|
||||
:curve [:& path/path-component {:shape shape}]
|
||||
:text [:& text/text-component {:shape shape}]
|
||||
:icon [:& icon/icon-component {:shape shape}]
|
||||
:rect [:& rect/rect-component {:shape shape}]
|
||||
|
|
42
frontend/src/uxbox/main/ui/shapes/canvas.cljs
Normal file
42
frontend/src/uxbox/main/ui/shapes/canvas.cljs
Normal file
|
@ -0,0 +1,42 @@
|
|||
;; 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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.shapes.canvas
|
||||
(:require
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.ui.shapes.common :as common]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.shapes.rect :refer [rect-shape]]
|
||||
;; [uxbox.main.ui.workspace.streams :as uws]
|
||||
[uxbox.util.data :refer [parse-int]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.geom.point :as gpt]))
|
||||
|
||||
(def canvas-default-props
|
||||
{:fill-color "#ffffff"})
|
||||
|
||||
(mf/defc canvas-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected (:id shape))
|
||||
on-mouse-down #(common/on-mouse-down % shape selected)
|
||||
shape (merge canvas-default-props shape)]
|
||||
(letfn [(on-double-click [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape (:id shape))))]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& rect-shape {:shape shape}]])))
|
||||
|
||||
|
|
@ -21,37 +21,37 @@
|
|||
|
||||
(mf/defc circle-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [modifiers (mf/deref (refs/selected-modifiers (:id shape)))
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected (:id shape))
|
||||
on-mouse-down #(common/on-mouse-down % shape selected)]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& circle-shape {:shape shape :modifiers modifiers}]]))
|
||||
[:& circle-shape {:shape shape}]]))
|
||||
|
||||
;; --- Circle Shape
|
||||
|
||||
(mf/defc circle-shape
|
||||
[{:keys [shape modifiers] :as props}]
|
||||
(let [{:keys [id rotation cx cy]} shape
|
||||
{:keys [resize displacement]} modifiers
|
||||
[{:keys [shape] :as props}]
|
||||
(let [{:keys [id rotation cx cy modifier-mtx]} shape
|
||||
|
||||
shape (cond-> shape
|
||||
displacement (geom/transform displacement)
|
||||
resize (geom/transform resize))
|
||||
shape (cond
|
||||
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
|
||||
:else shape)
|
||||
|
||||
center (gpt/point (:cx shape)
|
||||
(:cy shape))
|
||||
|
||||
rotation (or rotation 0)
|
||||
|
||||
moving? (boolean displacement)
|
||||
moving? (boolean modifier-mtx)
|
||||
|
||||
xfmt (-> (gmt/matrix)
|
||||
(gmt/rotate* rotation center))
|
||||
transform (when (pos? rotation)
|
||||
(str (-> (gmt/matrix)
|
||||
(gmt/rotate* rotation center))))
|
||||
|
||||
props {:id (str "shape-" id)
|
||||
:class (classnames :move-cursor moving?)
|
||||
:transform (str xfmt)}
|
||||
:transform transform}
|
||||
|
||||
attrs (merge props
|
||||
(attrs/extract-style-attrs shape)
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
[uxbox.main.ui.workspace.streams :as ws]
|
||||
[uxbox.main.ui.workspace.streams :as uws]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.dom :as dom]))
|
||||
|
||||
;; --- Shape Movement (by mouse)
|
||||
|
@ -24,20 +25,17 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
wst (get-in state [:workspace pid])
|
||||
stoper (->> ws/interaction-events
|
||||
(rx/filter ws/mouse-up?)
|
||||
(rx/take 1))
|
||||
stream (->> ws/mouse-position-deltas
|
||||
(rx/take-until stoper))]
|
||||
(rx/concat
|
||||
(when (refs/alignment-activated? (:flags wst))
|
||||
(rx/of (dw/initial-shape-align id)))
|
||||
(rx/map #(dw/apply-temporal-displacement id %) stream)
|
||||
(rx/of (dw/apply-displacement id)))))))
|
||||
flags (get-in state [:workspace pid :flags])
|
||||
stoper (rx/filter uws/mouse-up? stream)]
|
||||
(rx/concat
|
||||
(when (refs/alignment-activated? flags)
|
||||
(rx/of (dw/initial-shape-align id)))
|
||||
(->> uws/mouse-position-deltas
|
||||
(rx/map #(dw/apply-temporal-displacement id %))
|
||||
(rx/take-until stoper))
|
||||
(rx/of (dw/materialize-current-modifier id)))))))
|
||||
|
||||
(defn start-move-selected
|
||||
[]
|
||||
(def start-move-selected
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
|
@ -45,9 +43,8 @@
|
|||
selected (get-in state [:workspace pid :selected])]
|
||||
(rx/from-coll (map start-move selected))))))
|
||||
|
||||
|
||||
(defn on-mouse-down
|
||||
[event {:keys [id] :as shape} selected]
|
||||
[event {:keys [id type] :as shape} selected]
|
||||
(let [selected? (contains? selected id)
|
||||
drawing? @refs/selected-drawing-tool]
|
||||
(when-not (:blocked shape)
|
||||
|
@ -55,11 +52,17 @@
|
|||
drawing?
|
||||
nil
|
||||
|
||||
(= type :canvas)
|
||||
(when selected?
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! start-move-selected))
|
||||
|
||||
(and (not selected?) (empty? selected))
|
||||
(do
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dw/select-shape id)
|
||||
(start-move-selected)))
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id)
|
||||
start-move-selected))
|
||||
|
||||
(and (not selected?) (not (empty? selected)))
|
||||
(do
|
||||
|
@ -68,8 +71,8 @@
|
|||
(st/emit! (dw/select-shape id))
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id)
|
||||
(start-move-selected))))
|
||||
start-move-selected)))
|
||||
:else
|
||||
(do
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (start-move-selected)))))))
|
||||
(st/emit! start-move-selected))))))
|
||||
|
|
|
@ -23,14 +23,12 @@
|
|||
|
||||
(mf/defc icon-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [id (:id shape)
|
||||
modifiers (mf/deref (refs/selected-modifiers id))
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected id)
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected (:id shape))
|
||||
on-mouse-down #(common/on-mouse-down % shape selected)]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& icon-shape {:shape shape :modifiers modifiers}]]))
|
||||
[:& icon-shape {:shape shape}]]))
|
||||
|
||||
;; --- Icon Shape
|
||||
|
||||
|
@ -43,22 +41,20 @@
|
|||
(gmt/rotate* mt rotation center)))
|
||||
|
||||
(mf/defc icon-shape
|
||||
[{:keys [shape modifiers] :as props}]
|
||||
(let [{:keys [id content metadata rotation x1 y1]} shape
|
||||
{:keys [resize displacement]} modifiers
|
||||
[{:keys [shape] :as props}]
|
||||
(let [{:keys [id content metadata rotation modifier-mtx]} shape
|
||||
|
||||
xfmt (cond-> (gmt/matrix)
|
||||
displacement (gmt/multiply displacement)
|
||||
resize (gmt/multiply resize))
|
||||
shape (cond
|
||||
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
|
||||
:else shape)
|
||||
|
||||
{:keys [x1 y1 width height] :as shape} (-> (geom/transform shape xfmt)
|
||||
(geom/size))
|
||||
{:keys [x1 y1 width height] :as shape} (geom/size shape)
|
||||
|
||||
transform (when (pos? rotation)
|
||||
(str (rotate (gmt/matrix) shape)))
|
||||
|
||||
view-box (apply str (interpose " " (:view-box metadata)))
|
||||
xfmt (cond-> (gmt/matrix)
|
||||
(pos? rotation) (rotate shape))
|
||||
|
||||
moving? (boolean displacement)
|
||||
moving? (boolean modifier-mtx)
|
||||
props {:id (str id)
|
||||
:x x1
|
||||
:y y1
|
||||
|
@ -70,7 +66,7 @@
|
|||
:dangerouslySetInnerHTML #js {:__html content}}
|
||||
|
||||
attrs (merge props (attrs/extract-style-attrs shape))]
|
||||
[:g {:transform (str xfmt)}
|
||||
[:g {:transform transform}
|
||||
[:> :svg (normalize-props attrs) ]]))
|
||||
|
||||
;; --- Icon SVG
|
||||
|
|
|
@ -30,8 +30,7 @@
|
|||
|
||||
(mf/defc image-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [modifiers (mf/deref (refs/selected-modifiers (:id shape)))
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
image (mf/deref (image-ref (:image shape)))
|
||||
selected? (contains? selected (:id shape))
|
||||
on-mouse-down #(common/on-mouse-down % shape selected)]
|
||||
|
@ -42,27 +41,23 @@
|
|||
[:g.shape {:class (when selected? "selected")
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& image-shape {:shape shape
|
||||
:image image
|
||||
:modifiers modifiers}]])))
|
||||
:image image}]])))
|
||||
|
||||
;; --- Image Shape
|
||||
|
||||
(mf/defc image-shape
|
||||
[{:keys [shape image modifiers] :as props}]
|
||||
(let [{:keys [id x1 y1 width height]} (geom/size shape)
|
||||
{:keys [resize displacement]} modifiers
|
||||
[{:keys [shape image] :as props}]
|
||||
(let [{:keys [id x1 y1 width height modifier-mtx]} (geom/size shape)
|
||||
moving? (boolean modifier-mtx)
|
||||
transform (when (gmt/matrix? modifier-mtx)
|
||||
(str modifier-mtx))
|
||||
|
||||
xfmt (cond-> (gmt/matrix)
|
||||
resize (gmt/multiply resize)
|
||||
displacement (gmt/multiply displacement))
|
||||
|
||||
moving? (boolean displacement)
|
||||
props {:x x1 :y y1
|
||||
:id (str "shape-" id)
|
||||
:preserve-aspect-ratio "none"
|
||||
:class (classnames :move-cursor moving?)
|
||||
:xlink-href (:url image)
|
||||
:transform (str xfmt)
|
||||
:transform transform
|
||||
:width width
|
||||
:height height}
|
||||
attrs (merge props (attrs/extract-style-attrs shape))]
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
(:require
|
||||
[cuerdas.core :as str :include-macros true]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.shapes.attrs :as attrs]
|
||||
[uxbox.main.ui.shapes.common :as common]
|
||||
[uxbox.util.data :refer [classnames normalize-props]]))
|
||||
[uxbox.util.data :refer [classnames normalize-props]]
|
||||
[uxbox.util.geom.matrix :as gmt]))
|
||||
|
||||
;; --- Path Component
|
||||
|
||||
|
@ -22,20 +23,17 @@
|
|||
|
||||
(mf/defc path-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [modifiers (mf/deref (refs/selected-modifiers (:id shape)))
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected (:id shape))]
|
||||
(letfn [(on-mouse-down [event]
|
||||
(common/on-mouse-down event shape selected))
|
||||
(on-double-click [event]
|
||||
(when selected?
|
||||
(prn "on-double-click")
|
||||
(st/emit! (udw/start-edition-mode (:id shape)))))]
|
||||
(st/emit! (dw/start-edition-mode (:id shape)))))]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& path-shape {:shape shape
|
||||
:modifiers modifiers
|
||||
:background? true}]])))
|
||||
|
||||
;; --- Path Shape
|
||||
|
@ -62,12 +60,13 @@
|
|||
(recur buffer (inc index)))))))
|
||||
|
||||
(mf/defc path-shape
|
||||
[{:keys [shape modifiers background?] :as props}]
|
||||
(let [{:keys [resize displacement]} modifiers
|
||||
shape (cond-> shape
|
||||
displacement (geom/transform displacement)
|
||||
resize (geom/transform resize))
|
||||
moving? (boolean displacement)
|
||||
[{:keys [shape background?] :as props}]
|
||||
(let [modifier-mtx (:modifier-mtx shape)
|
||||
shape (cond
|
||||
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
|
||||
:else shape)
|
||||
|
||||
moving? (boolean modifier-mtx)
|
||||
|
||||
pdata (render-path shape)
|
||||
props {:id (str (:id shape))
|
||||
|
|
|
@ -22,16 +22,12 @@
|
|||
|
||||
(mf/defc rect-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [id (:id shape)
|
||||
modifiers (mf/deref (refs/selected-modifiers id))
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected id)
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected? (contains? selected (:id shape))
|
||||
on-mouse-down #(common/on-mouse-down % shape selected)]
|
||||
;; shape (assoc shape :modifiers modifiers)]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:& rect-shape {:shape shape
|
||||
:modifiers modifiers}]]))
|
||||
[:& rect-shape {:shape shape}]]))
|
||||
|
||||
;; --- Rect Shape
|
||||
|
||||
|
@ -43,27 +39,25 @@
|
|||
(gmt/rotate* mt rotation center)))
|
||||
|
||||
(mf/defc rect-shape
|
||||
[{:keys [shape modifiers] :as props}]
|
||||
(let [{:keys [id rotation]} shape
|
||||
{:keys [displacement resize]} modifiers
|
||||
[{:keys [shape] :as props}]
|
||||
(let [{:keys [id rotation modifier-mtx]} shape
|
||||
|
||||
xfmt (cond-> (gmt/matrix)
|
||||
displacement (gmt/multiply displacement)
|
||||
resize (gmt/multiply resize))
|
||||
shape (cond
|
||||
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
|
||||
:else shape)
|
||||
|
||||
{:keys [x1 y1 width height] :as shape} (-> (geom/transform shape xfmt)
|
||||
(geom/size))
|
||||
{:keys [x1 y1 width height] :as shape} (geom/size shape)
|
||||
|
||||
xfmt (cond-> (gmt/matrix)
|
||||
(pos? rotation) (rotate shape))
|
||||
transform (when (pos? rotation)
|
||||
(str (rotate (gmt/matrix) shape)))
|
||||
|
||||
moving? (boolean displacement)
|
||||
moving? (boolean modifier-mtx)
|
||||
|
||||
props {:x x1 :y y1
|
||||
:id (str "shape-" id)
|
||||
:class-name (classnames :move-cursor moving?)
|
||||
:width width
|
||||
:height height
|
||||
:transform (str xfmt)}
|
||||
:transform transform}
|
||||
attrs (merge (attrs/extract-style-attrs shape) props)]
|
||||
[:> :rect (normalize-props attrs)]))
|
||||
|
|
|
@ -39,30 +39,25 @@
|
|||
(declare text-shape-wrapper)
|
||||
(declare text-shape-edit)
|
||||
|
||||
(mf/def text-component
|
||||
:mixins [mf/memo mf/reactive]
|
||||
:render
|
||||
(fn [own {:keys [shape] :as props}]
|
||||
(let [{:keys [id x1 y1 content group]} shape
|
||||
modifiers (mf/react (refs/selected-modifiers id))
|
||||
selected (mf/react refs/selected-shapes)
|
||||
edition? (= (mf/react refs/selected-edition) id)
|
||||
selected? (and (contains? selected id)
|
||||
(= (count selected) 1))
|
||||
shape (assoc shape :modifiers modifiers)]
|
||||
(letfn [(on-mouse-down [event]
|
||||
(handle-mouse-down event shape selected))
|
||||
(on-double-click [event]
|
||||
;; TODO: handle grouping event propagation
|
||||
;; TODO: handle actions locking properly
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (udw/start-edition-mode id)))]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-down on-mouse-down}
|
||||
(if edition?
|
||||
[:& text-shape-edit {:shape shape}]
|
||||
[:& text-shape-wrapper {:shape shape}])]))))
|
||||
(mf/defc text-component
|
||||
[{:keys [shape] :as props}]
|
||||
(let [{:keys [id x1 y1 content group]} shape
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
edition? (= edition id)
|
||||
selected? (and (contains? selected id)
|
||||
(= (count selected) 1))]
|
||||
(letfn [(on-mouse-down [event]
|
||||
(handle-mouse-down event shape selected))
|
||||
(on-double-click [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (udw/start-edition-mode id)))]
|
||||
[:g.shape {:class (when selected? "selected")
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-down on-mouse-down}
|
||||
(if edition?
|
||||
[:& text-shape-edit {:shape shape}]
|
||||
[:& text-shape-wrapper {:shape shape}])])))
|
||||
|
||||
;; --- Text Styles Helpers
|
||||
|
||||
|
@ -126,7 +121,7 @@
|
|||
style (make-style shape)
|
||||
on-input (fn [ev]
|
||||
(let [content (dom/event->inner-text ev)]
|
||||
(st/emit! (uds/update-text id content))))]
|
||||
(st/emit! (udw/update-shape-attrs id {:content content}))))]
|
||||
[:foreignObject {:x x1 :y y1 :width width :height height}
|
||||
[:div {:style (normalize-props style)
|
||||
:ref (::container own)
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
[:li {:on-click #(on-click % :settings/notifications)}
|
||||
i/mail
|
||||
[:span (tr "ds.user.notifications")]]
|
||||
[:li {:on-click #(on-click % (da/logout))}
|
||||
[:li {:on-click #(on-click % da/logout)}
|
||||
i/exit
|
||||
[:span (tr "ds.user.exit")]]]))
|
||||
|
||||
|
@ -49,10 +49,9 @@
|
|||
(l/derive st/state)))
|
||||
|
||||
(mf/defc user
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[_]
|
||||
[props]
|
||||
(let [open (mf/use-state false)
|
||||
profile (mf/react profile-ref)
|
||||
profile (mf/deref profile-ref)
|
||||
photo (if (str/empty? (:photo profile ""))
|
||||
"/images/avatar.jpg"
|
||||
(:photo profile))]
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
(:require
|
||||
[beicon.core :as rx]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.history :as udh]
|
||||
|
@ -52,7 +51,7 @@
|
|||
(let [prev-zoom @refs/selected-zoom
|
||||
dom (mf/ref-node canvas)
|
||||
scroll-position (scroll/get-current-position-absolute dom)
|
||||
mouse-point @uws/viewport-mouse-position]
|
||||
mouse-point @uws/mouse-position]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(if (pos? (.-deltaY event))
|
||||
|
@ -62,7 +61,7 @@
|
|||
|
||||
(defn- subscribe
|
||||
[canvas page]
|
||||
(scroll/scroll-to-page-center (mf/ref-node canvas) page)
|
||||
;; (scroll/scroll-to-page-center (mf/ref-node canvas) page)
|
||||
(st/emit! (udp/watch-page-changes (:id page))
|
||||
(udu/watch-page-changes (:id page)))
|
||||
(let [sub (shortcuts/init)]
|
||||
|
@ -83,10 +82,10 @@
|
|||
:no-tool-bar-left (not left-sidebar?)
|
||||
:scrolling (:viewport-positionig workspace))]
|
||||
|
||||
(mf/use-effect {:deps #js [canvas page]
|
||||
:fn #(subscribe canvas page)})
|
||||
(mf/use-effect #(subscribe canvas page)
|
||||
#js [(:id page)])
|
||||
[:*
|
||||
(messages-widget)
|
||||
[:& messages-widget]
|
||||
[:& header {:page page
|
||||
:flags flags
|
||||
:key (:id page)}]
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
;; 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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.workspace.canvas
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.shapes :as uus]
|
||||
[uxbox.main.ui.workspace.drawarea :refer [draw-area]]
|
||||
[uxbox.main.ui.workspace.selection :refer [selection-handlers]]
|
||||
[uxbox.util.geom.point :as gpt]))
|
||||
|
||||
;; --- Background
|
||||
|
||||
(mf/def background
|
||||
:mixins [mf/memo]
|
||||
:render
|
||||
(fn [own {:keys [background] :as metadata}]
|
||||
[:rect
|
||||
{:x 0 :y 0
|
||||
:width "100%"
|
||||
:height "100%"
|
||||
:fill (or background "#ffffff")}]))
|
||||
|
||||
;; --- Canvas
|
||||
|
||||
(mf/defc canvas
|
||||
[{:keys [page wst] :as props}]
|
||||
(let [{:keys [metadata id]} page
|
||||
zoom (:zoom wst 1) ;; NOTE: maybe forward wst to draw-area
|
||||
width (:width metadata)
|
||||
height (:height metadata)]
|
||||
[:svg.page-canvas {:x c/canvas-start-x
|
||||
:y c/canvas-start-y
|
||||
:width width
|
||||
:height height}
|
||||
[:& background metadata]
|
||||
#_[:svg.page-layout
|
||||
[:g.main
|
||||
(for [id (reverse (:shapes page))]
|
||||
[:& uus/shape-component {:id id :key id}])
|
||||
(when (seq (:selected wst))
|
||||
[:& selection-handlers {:wst wst}])
|
||||
(when-let [dshape (:drawing wst)]
|
||||
[:& draw-area {:shape dshape
|
||||
:zoom (:zoom wst)
|
||||
:modifiers (:modifiers wst)}])]]]))
|
|
@ -21,19 +21,24 @@
|
|||
[uxbox.main.workers :as uwrk]
|
||||
[uxbox.util.math :as mth]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.data :refer [seek]]
|
||||
[uxbox.util.geom.matrix :as gmt]
|
||||
[uxbox.util.geom.path :as path]
|
||||
[uxbox.util.geom.point :as gpt]))
|
||||
[uxbox.util.geom.point :as gpt]
|
||||
[uxbox.util.uuid :as uuid]))
|
||||
|
||||
;; --- Events
|
||||
|
||||
(declare handle-drawing)
|
||||
(declare handle-drawing-generic)
|
||||
(declare handle-drawing-path)
|
||||
(declare handle-drawing-free-path)
|
||||
(declare handle-drawing-curve)
|
||||
(declare handle-finish-drawing)
|
||||
(declare conditional-align)
|
||||
|
||||
(defn start-drawing
|
||||
[object]
|
||||
[type]
|
||||
{:pre [(keyword? type)]}
|
||||
(let [id (gensym "drawing")]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
|
@ -42,35 +47,69 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [lock (get-in state [:workspace :drawing-lock])]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
lock (get-in state [:workspace :drawing-lock])]
|
||||
(if (= lock id)
|
||||
(rx/merge (->> stream
|
||||
(rx/filter #(= % handle-finish-drawing))
|
||||
(rx/take 1)
|
||||
(rx/map (fn [_] #(update % :workspace dissoc :drawing-lock))))
|
||||
(rx/of (handle-drawing object)))
|
||||
(rx/merge
|
||||
(->> (rx/filter #(= % handle-finish-drawing) stream)
|
||||
(rx/take 1)
|
||||
(rx/map (fn [_] #(update % :workspace dissoc :drawing-lock))))
|
||||
(rx/of (handle-drawing type)))
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn- conditional-align [point align?]
|
||||
(if align?
|
||||
(uwrk/align-point point)
|
||||
(rx/of point)))
|
||||
(def ^:private minimal-shapes
|
||||
[{:type :rect
|
||||
:name "Rect"
|
||||
:stroke-color "#000000"}
|
||||
{:type :image}
|
||||
{:type :circle
|
||||
:name "Circle"}
|
||||
{:type :path
|
||||
:name "Path"
|
||||
:stroke-style :solid
|
||||
:stroke-color "#000000"
|
||||
:stroke-width 2
|
||||
:fill-color "#000000"
|
||||
:fill-opacity 0
|
||||
:segments []}
|
||||
{:type :canvas
|
||||
:name "Canvas"
|
||||
:stroke-color "#000000"}
|
||||
{:type :curve
|
||||
:name "Path"
|
||||
:stroke-style :solid
|
||||
:stroke-color "#000000"
|
||||
:stroke-width 2
|
||||
:fill-color "#000000"
|
||||
:fill-opacity 0
|
||||
:segments []}
|
||||
{:type :text
|
||||
:name "Text"
|
||||
:content "Type your text here"}])
|
||||
|
||||
(defn- make-minimal-shape
|
||||
[type]
|
||||
(let [tool (seek #(= type (:type %)) minimal-shapes)]
|
||||
(assert tool "unexpected drawing tool")
|
||||
(assoc tool :id (uuid/random))))
|
||||
|
||||
;; TODO: maybe this should be a simple function
|
||||
(defn handle-drawing
|
||||
[shape]
|
||||
[type]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
data (make-minimal-shape type)]
|
||||
(update-in state [:workspace pid :drawing] merge data)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of
|
||||
(if (= :path (:type shape))
|
||||
(if (:free shape)
|
||||
(handle-drawing-free-path shape)
|
||||
(handle-drawing-path shape))
|
||||
(handle-drawing-generic shape))))))
|
||||
(case type
|
||||
:path (rx/of handle-drawing-path)
|
||||
:curve (rx/of handle-drawing-curve)
|
||||
(rx/of handle-drawing-generic)))))
|
||||
|
||||
(defn- handle-drawing-generic
|
||||
[shape]
|
||||
(def handle-drawing-generic
|
||||
(letfn [(initialize-drawing [state point]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
shape (get-in state [:workspace pid :drawing])
|
||||
|
@ -80,24 +119,13 @@
|
|||
:y2 (+ (:y point) 2)})]
|
||||
(assoc-in state [:workspace pid :drawing] (assoc shape ::initialized? true))))
|
||||
|
||||
;; TODO: this is a new approach for resizing, when all the
|
||||
;; subsystem are migrated to the new resize approach, this
|
||||
;; function should be moved into uxbox.main.geom ns.
|
||||
(resize-shape [shape point lock?]
|
||||
(if (= (:type shape) :circle)
|
||||
(let [rx (mth/abs (- (:x point) (:cx shape)))
|
||||
ry (mth/abs (- (:y point) (:cy shape)))]
|
||||
(if lock?
|
||||
(assoc shape :rx rx :ry ry)
|
||||
(assoc shape :rx rx :ry rx)))
|
||||
(let [width (- (:x point) (:x1 shape))
|
||||
height (- (:y point) (:y1 shape))
|
||||
proportion (:proportion shape 1)]
|
||||
(assoc shape
|
||||
:x2 (+ (:x1 shape) width)
|
||||
:y2 (if lock?
|
||||
(+ (:y1 shape) (/ width proportion))
|
||||
(+ (:y1 shape) height))))))
|
||||
(let [shape (-> (geom/shape->rect-shape shape)
|
||||
(geom/size))
|
||||
result (geom/resize-shape :bottom-right shape point lock?)
|
||||
scale (geom/calculate-scale-ratio shape result)
|
||||
mtx (geom/generate-resize-matrix :bottom-right shape scale)]
|
||||
(assoc shape :modifier-mtx mtx)))
|
||||
|
||||
(update-drawing [state point lock?]
|
||||
(let [pid (get-in state [:workspace :current])]
|
||||
|
@ -107,28 +135,26 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
zoom (get-in state [:workspace pid :zoom])
|
||||
flags (get-in state [:workspace pid :flags])
|
||||
{:keys [zoom flags]} (get-in state [:workspace pid])
|
||||
align? (refs/alignment-activated? flags)
|
||||
|
||||
stoper (->> (rx/filter #(or (uws/mouse-up? %) (= % :interrupt)) stream)
|
||||
(rx/take 1))
|
||||
stoper? #(or (uws/mouse-up? %) (= % :interrupt))
|
||||
stoper (rx/filter stoper? stream)
|
||||
|
||||
mouse (->> uws/viewport-mouse-position
|
||||
mouse (->> uws/mouse-position
|
||||
(rx/mapcat #(conditional-align % align?))
|
||||
(rx/with-latest vector uws/mouse-position-ctrl))]
|
||||
(rx/map #(gpt/divide % zoom)))]
|
||||
(rx/concat
|
||||
(->> uws/viewport-mouse-position
|
||||
(->> mouse
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(conditional-align % align?))
|
||||
(rx/map (fn [pt] #(initialize-drawing % pt))))
|
||||
(->> mouse
|
||||
(rx/with-latest vector uws/mouse-position-ctrl)
|
||||
(rx/map (fn [[pt ctrl?]] #(update-drawing % pt ctrl?)))
|
||||
(rx/take-until stoper))
|
||||
(rx/of handle-finish-drawing)))))))
|
||||
|
||||
(defn handle-drawing-path
|
||||
[shape]
|
||||
(def handle-drawing-path
|
||||
(letfn [(stoper-event? [{:keys [type shift] :as event}]
|
||||
(or (= event :interrupt)
|
||||
(and (uws/mouse-event? event)
|
||||
|
@ -162,17 +188,17 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
zoom (get-in state [:workspace pid :zoom])
|
||||
flags (get-in state [:workspace pid :flags])
|
||||
align? (refs/alignment-activated? flags)
|
||||
{:keys [zoom flags]} (get-in state [:workspace pid])
|
||||
|
||||
last-point (volatile! @uws/viewport-mouse-position)
|
||||
align? (refs/alignment-activated? flags)
|
||||
last-point (volatile! (gpt/divide @uws/mouse-position zoom))
|
||||
|
||||
stoper (->> (rx/filter stoper-event? stream)
|
||||
(rx/take 1))
|
||||
(rx/share))
|
||||
|
||||
mouse (->> (rx/sample 10 uws/viewport-mouse-position)
|
||||
(rx/mapcat #(conditional-align % align?)))
|
||||
mouse (->> (rx/sample 10 uws/mouse-position)
|
||||
(rx/mapcat #(conditional-align % align?))
|
||||
(rx/map #(gpt/divide % zoom)))
|
||||
|
||||
points (->> stream
|
||||
(rx/filter uws/mouse-click?)
|
||||
|
@ -186,7 +212,6 @@
|
|||
(rx/with-latest vector counter)
|
||||
(rx/map flatten))
|
||||
|
||||
|
||||
imm-transform #(vector (- % 7) (+ % 7) %)
|
||||
immanted-zones (vec (concat
|
||||
(map imm-transform (range 0 181 15))
|
||||
|
@ -205,8 +230,8 @@
|
|||
|
||||
(->> points
|
||||
(rx/take-until stoper)
|
||||
(rx/map (fn [pt]
|
||||
#(insert-point-segment % pt))))
|
||||
(rx/map (fn [pt]#(insert-point-segment % pt))))
|
||||
|
||||
(rx/concat
|
||||
(->> stream'
|
||||
(rx/map (fn [[point ctrl? index :as xxx]]
|
||||
|
@ -221,8 +246,7 @@
|
|||
(rx/of remove-dangling-segmnet
|
||||
handle-finish-drawing))))))))
|
||||
|
||||
(defn- handle-drawing-free-path
|
||||
[shape]
|
||||
(def handle-drawing-curve
|
||||
(letfn [(stoper-event? [{:keys [type shift] :as event}]
|
||||
(or (= event :interrupt)
|
||||
(and (uws/mouse-event? event) (= type :up))))
|
||||
|
@ -242,15 +266,14 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
zoom (get-in state [:workspace pid :zoom])
|
||||
flags (get-in state [:workspace pid :flags])
|
||||
{:keys [zoom flags]} (get-in state [:workspace pid])
|
||||
|
||||
align? (refs/alignment-activated? flags)
|
||||
stoper (rx/filter stoper-event? stream)
|
||||
mouse (->> (rx/sample 10 uws/mouse-position)
|
||||
(rx/mapcat #(conditional-align % align?))
|
||||
(rx/map #(gpt/divide % zoom)))]
|
||||
|
||||
stoper (->> (rx/filter stoper-event? stream)
|
||||
(rx/take 1))
|
||||
|
||||
mouse (->> (rx/sample 10 uws/viewport-mouse-position)
|
||||
(rx/mapcat #(conditional-align % align?)))]
|
||||
(rx/concat
|
||||
(rx/of initialize-drawing)
|
||||
(->> mouse
|
||||
|
@ -265,22 +288,17 @@
|
|||
(watch [_ state stream]
|
||||
(let [pid (get-in state [:workspace :current])
|
||||
shape (get-in state [:workspace pid :drawing])]
|
||||
(if (::initialized? shape)
|
||||
(let [resize-mtx (get-in state [:workspace pid :modifiers (:id shape) :resize])
|
||||
shape (cond-> shape
|
||||
resize-mtx (geom/transform resize-mtx))]
|
||||
(rx/of
|
||||
;; Remove the stalled modifiers
|
||||
;; TODO: maybe a specific event for "clear modifiers"
|
||||
#(update-in % [:workspace pid :modifiers] dissoc (:id shape))
|
||||
|
||||
;; Unselect the drawing tool
|
||||
#(update-in % [:workspace pid] dissoc :drawing :drawing-tool)
|
||||
|
||||
(rx/concat
|
||||
(rx/of dw/clear-drawing)
|
||||
(when (::initialized? shape)
|
||||
(let [modifier-mtx (:modifier-mtx shape)
|
||||
shape (if (gmt/matrix? modifier-mtx)
|
||||
(geom/transform shape modifier-mtx)
|
||||
shape)
|
||||
shape (dissoc shape ::initialized? :modifier-mtx)]
|
||||
;; Add & select the cred shape to the workspace
|
||||
(ds/add-shape shape)
|
||||
(dw/select-first-shape)))
|
||||
(rx/of #(update-in % [:workspace pid] dissoc :drawing :drawing-tool)))))))
|
||||
(rx/of (dw/add-shape shape)
|
||||
(dw/select-first-shape)))))))))
|
||||
|
||||
(def close-drawing-path
|
||||
(reify
|
||||
|
@ -295,11 +313,12 @@
|
|||
(declare path-draw-area)
|
||||
|
||||
(mf/defc draw-area
|
||||
[{:keys [zoom shape modifiers] :as props}]
|
||||
(if (= (:type shape) :path)
|
||||
[:& path-draw-area {:shape shape}]
|
||||
[:& generic-draw-area {:shape (assoc shape :modifiers modifiers)
|
||||
:zoom zoom}]))
|
||||
[{:keys [zoom shape] :as props}]
|
||||
(when (:id shape)
|
||||
(case (:type shape)
|
||||
(:path :curve) [:& path-draw-area {:shape shape}]
|
||||
[:& generic-draw-area {:shape shape
|
||||
:zoom zoom}])))
|
||||
|
||||
(mf/defc generic-draw-area
|
||||
[{:keys [shape zoom]}]
|
||||
|
@ -328,7 +347,7 @@
|
|||
(when-let [{:keys [x y] :as segment} (first (:segments shape))]
|
||||
[:g
|
||||
(shapes/render-shape shape)
|
||||
(when-not (:free shape)
|
||||
(when (not= :curve (:type shape))
|
||||
[:circle.close-bezier
|
||||
{:cx x
|
||||
:cy y
|
||||
|
@ -336,3 +355,8 @@
|
|||
:on-click on-click
|
||||
:on-mouse-enter on-mouse-enter
|
||||
:on-mouse-leave on-mouse-leave}])])))
|
||||
|
||||
(defn- conditional-align [point align?]
|
||||
(if align?
|
||||
(uwrk/align-point point)
|
||||
(rx/of point)))
|
||||
|
|
|
@ -30,9 +30,8 @@
|
|||
;; --- Zoom Widget
|
||||
|
||||
(mf/defc zoom-widget
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[props]
|
||||
(let [zoom (mf/react refs/selected-zoom)
|
||||
(let [zoom (mf/deref refs/selected-zoom)
|
||||
increase #(st/emit! (dw/increase-zoom))
|
||||
decrease #(st/emit! (dw/decrease-zoom))]
|
||||
[:ul.options-view
|
||||
|
|
|
@ -48,13 +48,11 @@
|
|||
|
||||
(on-uploaded [[image]]
|
||||
(let [{:keys [id name width height]} image
|
||||
shape {:type :image
|
||||
:name name
|
||||
:id (uuid/random)
|
||||
shape {:name name
|
||||
:metadata {:width width
|
||||
:height height}
|
||||
:image id}]
|
||||
(st/emit! (dw/select-for-drawing shape))
|
||||
(st/emit! (dw/select-for-drawing :image shape))
|
||||
(modal/hide!)))
|
||||
|
||||
(on-files-selected [event]
|
||||
|
@ -93,13 +91,11 @@
|
|||
(mf/defc image-item
|
||||
[{:keys [image] :as props}]
|
||||
(letfn [(on-click [event]
|
||||
(let [shape {:type :image
|
||||
:name (:name image)
|
||||
:id (uuid/random)
|
||||
(let [shape {:name (:name image)
|
||||
:metadata {:width (:width image)
|
||||
:height (:height image)}
|
||||
:image (:id image)}]
|
||||
(st/emit! (dw/select-for-drawing shape))
|
||||
(st/emit! (dw/select-for-drawing :image shape))
|
||||
(modal/hide!)))]
|
||||
[:div.library-item {:on-click on-click}
|
||||
[:div.library-item-th
|
||||
|
|
|
@ -10,10 +10,9 @@
|
|||
(:require
|
||||
[beicon.core :as rx]
|
||||
[lentes.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
|
@ -34,19 +33,13 @@
|
|||
|
||||
;; --- Resize Implementation
|
||||
|
||||
;; TODO: this function need to be refactored
|
||||
|
||||
(defn- start-resize
|
||||
[vid ids shape]
|
||||
(letfn [(on-resize [shape [point lock?]]
|
||||
(letfn [(resize [shape [point lock?]]
|
||||
(let [result (geom/resize-shape vid shape point lock?)
|
||||
scale (geom/calculate-scale-ratio shape result)
|
||||
mtx (geom/generate-resize-matrix vid shape scale)
|
||||
xfm (map #(udw/apply-temporal-resize % mtx))]
|
||||
(apply st/emit! (sequence xfm ids))))
|
||||
|
||||
(on-end []
|
||||
(apply st/emit! (map udw/apply-resize ids)))
|
||||
mtx (geom/generate-resize-matrix vid shape scale)]
|
||||
(apply rx/of (map #(dw/assoc-temporal-modifier % mtx) ids))))
|
||||
|
||||
;; Unifies the instantaneous proportion lock modifier
|
||||
;; activated by Ctrl key and the shapes own proportion
|
||||
|
@ -65,19 +58,23 @@
|
|||
;; Apply the current zoom factor to the point.
|
||||
(apply-zoom [point]
|
||||
(gpt/divide point @refs/selected-zoom))]
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [shape (->> (geom/shape->rect-shape shape)
|
||||
(geom/size))
|
||||
stoper (rx/filter ws/mouse-up? stream)]
|
||||
(rx/concat
|
||||
(->> ws/mouse-position
|
||||
(rx/map apply-zoom)
|
||||
(rx/mapcat apply-grid-alignment)
|
||||
(rx/with-latest vector ws/mouse-position-ctrl)
|
||||
(rx/map normalize-proportion-lock)
|
||||
(rx/mapcat (partial resize shape))
|
||||
(rx/take-until stoper))
|
||||
(rx/from-coll (map dw/materialize-current-modifier ids))))))))
|
||||
|
||||
(let [shape (->> (geom/shape->rect-shape shape)
|
||||
(geom/size))
|
||||
stoper (->> ws/interaction-events
|
||||
(rx/filter ws/mouse-up?)
|
||||
(rx/take 1))
|
||||
stream (->> ws/viewport-mouse-position
|
||||
(rx/take-until stoper)
|
||||
(rx/map apply-zoom)
|
||||
(rx/mapcat apply-grid-alignment)
|
||||
(rx/with-latest vector ws/mouse-position-ctrl)
|
||||
(rx/map normalize-proportion-lock))]
|
||||
(rx/subscribe stream (partial on-resize shape) nil on-end))))
|
||||
;; (rx/subscribe stream (partial on-resize shape) nil on-end))))
|
||||
|
||||
;; --- Controls (Component)
|
||||
|
||||
|
@ -160,22 +157,23 @@
|
|||
(letfn [(on-mouse-down [event index]
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(let [stoper (get-edition-stream-stoper ws/interaction-events)
|
||||
;; TODO: this need code ux refactor
|
||||
(let [stoper (get-edition-stream-stoper)
|
||||
stream (rx/take-until stoper ws/mouse-position-deltas)]
|
||||
(when @refs/selected-alignment
|
||||
(st/emit! (uds/initial-path-point-align (:id shape) index)))
|
||||
(st/emit! (dw/initial-path-point-align (:id shape) index)))
|
||||
(rx/subscribe stream #(on-handler-move % index))))
|
||||
|
||||
(get-edition-stream-stoper [stream]
|
||||
(get-edition-stream-stoper []
|
||||
(let [stoper? #(and (ws/mouse-event? %) (= (:type %) :up))]
|
||||
(rx/merge
|
||||
(rx/filter stoper? stream)
|
||||
(->> stream
|
||||
(rx/filter stoper? st/stream)
|
||||
(->> st/stream
|
||||
(rx/filter #(= % :interrupt))
|
||||
(rx/take 1)))))
|
||||
|
||||
(on-handler-move [delta index]
|
||||
(st/emit! (uds/update-path (:id shape) index delta)))]
|
||||
(st/emit! (dw/update-path (:id shape) index delta)))]
|
||||
|
||||
(let [displacement (:displacement modifiers)
|
||||
segments (cond->> (:segments shape)
|
||||
|
@ -191,30 +189,20 @@
|
|||
:style {:cursor "pointer"}}])])))
|
||||
|
||||
(mf/defc multiple-selection-handlers
|
||||
[{:keys [shapes modifiers zoom] :as props}]
|
||||
[{:keys [shapes zoom] :as props}]
|
||||
(let [shape (->> shapes
|
||||
(map #(assoc % :modifiers (get modifiers (:id %))))
|
||||
(map #(geom/selection-rect %))
|
||||
(geom/shapes->rect-shape)
|
||||
(geom/selection-rect))
|
||||
on-click #(do (dom/stop-propagation %2)
|
||||
(start-resize %1 (map :id shapes) shape))]
|
||||
(st/emit! (start-resize %1 (mapv :id shapes) shape)))]
|
||||
[:& controls {:shape shape
|
||||
:zoom zoom
|
||||
:on-click on-click}]))
|
||||
|
||||
(mf/defc single-selection-handlers
|
||||
[{:keys [shape zoom modifiers] :as props}]
|
||||
(let [on-click #(do (dom/stop-propagation %2)
|
||||
(start-resize %1 #{(:id shape)} shape))
|
||||
shape (-> (assoc shape :modifiers modifiers)
|
||||
(geom/selection-rect))]
|
||||
[:& controls {:shape shape :zoom zoom :on-click on-click}]))
|
||||
|
||||
(mf/defc text-edition-selection-handlers
|
||||
[{:keys [shape modifiers zoom] :as props}]
|
||||
(let [{:keys [x1 y1 width height] :as shape} (-> (assoc shape :modifiers modifiers)
|
||||
(geom/selection-rect))]
|
||||
[{:keys [shape zoom] :as props}]
|
||||
(let [{:keys [x1 y1 width height] :as shape} (geom/selection-rect shape)]
|
||||
[:g.controls
|
||||
[:rect.main {:x x1 :y y1
|
||||
:width width
|
||||
|
@ -225,6 +213,13 @@
|
|||
:stroke-opacity "0.5"
|
||||
:fill "transparent"}}]]))
|
||||
|
||||
(mf/defc single-selection-handlers
|
||||
[{:keys [shape zoom] :as props}]
|
||||
(let [on-click #(do (dom/stop-propagation %2)
|
||||
(st/emit! (start-resize %1 #{(:id shape)} shape)))
|
||||
shape (geom/selection-rect shape)]
|
||||
[:& controls {:shape shape :zoom zoom :on-click on-click}]))
|
||||
|
||||
(def ^:private shapes-map-iref
|
||||
(-> (l/key :shapes)
|
||||
(l/derive st/state)))
|
||||
|
@ -233,35 +228,29 @@
|
|||
[{:keys [wst] :as props}]
|
||||
(let [shapes-map (mf/deref shapes-map-iref)
|
||||
shapes (map #(get shapes-map %) (:selected wst))
|
||||
edition? (:edition wst)
|
||||
modifiers (:modifiers wst)
|
||||
edition (:edition wst)
|
||||
zoom (:zoom wst 1)
|
||||
num (count shapes)
|
||||
{:keys [id type] :as shape} (first shapes)]
|
||||
|
||||
|
||||
(cond
|
||||
(zero? num)
|
||||
nil
|
||||
|
||||
(> num 1)
|
||||
[:& multiple-selection-handlers {:shapes shapes
|
||||
:modifiers modifiers
|
||||
:zoom zoom}]
|
||||
|
||||
(and (= type :text)
|
||||
(= edition? (:id shape)))
|
||||
(= edition (:id shape)))
|
||||
[:& text-edition-selection-handlers {:shape shape
|
||||
:modifiers (get modifiers id)
|
||||
:zoom zoom}]
|
||||
(and (= type :path)
|
||||
(= edition? (:id shape)))
|
||||
(and (or (= type :path)
|
||||
(= type :curve))
|
||||
(= edition (:id shape)))
|
||||
[:& path-edition-selection-handlers {:shape shape
|
||||
:zoom zoom
|
||||
:modifiers (get modifiers id)}]
|
||||
|
||||
:zoom zoom}]
|
||||
|
||||
:else
|
||||
[:& single-selection-handlers {:shape shape
|
||||
:modifiers (get modifiers id)
|
||||
:zoom zoom}])))
|
||||
|
|
|
@ -12,10 +12,7 @@
|
|||
[uxbox.main.store :as st]
|
||||
[uxbox.main.data.lightbox :as dl]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.undo :as udu]
|
||||
[uxbox.main.data.history :as udh]
|
||||
[uxbox.main.ui.workspace.sidebar.drawtools :as wsd])
|
||||
[uxbox.main.data.undo :as du])
|
||||
(:import goog.events.EventType
|
||||
goog.events.KeyCodes
|
||||
goog.ui.KeyboardShortcutHandler
|
||||
|
@ -27,26 +24,24 @@
|
|||
|
||||
(defonce +shortcuts+
|
||||
{:shift+g #(st/emit! (dw/toggle-flag :grid))
|
||||
:ctrl+g #(st/emit! (uds/group-selected))
|
||||
:ctrl+shift+g #(st/emit! (uds/ungroup-selected))
|
||||
:ctrl+shift+m #(st/emit! (dw/toggle-flag :sitemap))
|
||||
:ctrl+shift+f #(st/emit! (dw/toggle-flag :drawtools))
|
||||
:ctrl+shift+i #(st/emit! (dw/toggle-flag :icons))
|
||||
:ctrl+shift+l #(st/emit! (dw/toggle-flag :layers))
|
||||
:ctrl+0 #(st/emit! (dw/reset-zoom))
|
||||
:ctrl+r #(st/emit! (dw/toggle-flag :ruler))
|
||||
:ctrl+d #(st/emit! (uds/duplicate-selected))
|
||||
:ctrl+d #(st/emit! dw/duplicate-selected)
|
||||
:ctrl+c #(st/emit! (dw/copy-to-clipboard))
|
||||
:ctrl+v #(st/emit! (dw/paste-from-clipboard))
|
||||
:ctrl+shift+v #(dl/open! :clipboard)
|
||||
:ctrl+z #(st/emit! (udu/undo))
|
||||
:ctrl+shift+z #(st/emit! (udu/redo))
|
||||
:ctrl+y #(st/emit! (udu/redo))
|
||||
:ctrl+b #(st/emit! (dw/select-for-drawing wsd/+draw-tool-rect+))
|
||||
:ctrl+e #(st/emit! (dw/select-for-drawing wsd/+draw-tool-circle+))
|
||||
:ctrl+t #(st/emit! (dw/select-for-drawing wsd/+draw-tool-text+))
|
||||
:ctrl+z #(st/emit! (du/undo))
|
||||
:ctrl+shift+z #(st/emit! (du/redo))
|
||||
:ctrl+y #(st/emit! (du/redo))
|
||||
:ctrl+b #(st/emit! (dw/select-for-drawing :rect))
|
||||
:ctrl+e #(st/emit! (dw/select-for-drawing :circle))
|
||||
:ctrl+t #(st/emit! (dw/select-for-drawing :text))
|
||||
:esc #(st/emit! (dw/deselect-all))
|
||||
:delete #(st/emit! (dw/delete-selected))
|
||||
:delete #(st/emit! dw/delete-selected)
|
||||
:ctrl+up #(st/emit! (dw/move-selected-layer :up))
|
||||
:ctrl+down #(st/emit! (dw/move-selected-layer :down))
|
||||
:ctrl+shift+up #(st/emit! (dw/move-selected-layer :top))
|
||||
|
|
|
@ -21,17 +21,16 @@
|
|||
(mf/defc left-sidebar
|
||||
{:wrap [mf/wrap-memo]}
|
||||
[{:keys [flags page] :as props}]
|
||||
[:aside#settings-bar.settings-bar.settings-bar-left
|
||||
[:> rdnd/provider {:backend rdnd/html5}
|
||||
[:div.settings-bar-inside
|
||||
(when (contains? flags :sitemap)
|
||||
[:& sitemap-toolbox {:project-id (:project page)
|
||||
:current-page-id (:id page)
|
||||
:page page}])
|
||||
#_(when (contains? flags :document-history)
|
||||
(history-toolbox page-id))
|
||||
(when (contains? flags :layers)
|
||||
[:& layers-toolbox {:page page}])]]])
|
||||
[:aside.settings-bar.settings-bar-left
|
||||
[:div.settings-bar-inside
|
||||
(when (contains? flags :sitemap)
|
||||
[:& sitemap-toolbox {:project-id (:project page)
|
||||
:current-page-id (:id page)
|
||||
:page page}])
|
||||
#_(when (contains? flags :document-history)
|
||||
(history-toolbox page-id))
|
||||
(when (contains? flags :layers)
|
||||
[:& layers-toolbox {:page page}])]])
|
||||
|
||||
;; --- Right Sidebar (Component)
|
||||
|
||||
|
@ -43,6 +42,5 @@
|
|||
[:& draw-toolbox {:flags flags}])
|
||||
(when (contains? flags :element-options)
|
||||
[:& options-toolbox {:page page}])
|
||||
(when (contains? flags :icons)
|
||||
#_(icons-toolbox))]])
|
||||
|
||||
#_(when (contains? flags :icons)
|
||||
(icons-toolbox))]])
|
||||
|
|
|
@ -17,96 +17,69 @@
|
|||
|
||||
;; --- Constants
|
||||
|
||||
(def +draw-tool-rect+
|
||||
{:type :rect
|
||||
:id (uuid/random)
|
||||
:name "Rect"
|
||||
:stroke-color "#000000"})
|
||||
|
||||
(def +draw-tool-circle+
|
||||
{:type :circle
|
||||
:id (uuid/random)
|
||||
:name "Circle"})
|
||||
|
||||
(def +draw-tool-path+
|
||||
{:type :path
|
||||
:id (uuid/random)
|
||||
:name "Path"
|
||||
:stroke-style :solid
|
||||
:stroke-color "#000000"
|
||||
:stroke-width 2
|
||||
:fill-color "#000000"
|
||||
:fill-opacity 0
|
||||
;; :close? true
|
||||
:points []})
|
||||
|
||||
(def +draw-tool-curve+
|
||||
(assoc +draw-tool-path+
|
||||
:id (uuid/random)
|
||||
:free true))
|
||||
|
||||
(def +draw-tool-text+
|
||||
{:type :text
|
||||
:id (uuid/random)
|
||||
:name "Text"
|
||||
:content "Hello world"})
|
||||
|
||||
(def +draw-tools+
|
||||
[{:icon i/box
|
||||
:help "ds.help.rect"
|
||||
:shape +draw-tool-rect+
|
||||
:type :rect
|
||||
:priority 1}
|
||||
{:icon i/circle
|
||||
:help "ds.help.circle"
|
||||
:shape +draw-tool-circle+
|
||||
:type :circle
|
||||
:priority 2}
|
||||
{:icon i/text
|
||||
:help "ds.help.text"
|
||||
:shape +draw-tool-text+
|
||||
:type :text
|
||||
:priority 4}
|
||||
{:icon i/curve
|
||||
:help "ds.help.path"
|
||||
:shape +draw-tool-path+
|
||||
:type :path
|
||||
:priority 5}
|
||||
{:icon i/pencil
|
||||
:help "ds.help.curve"
|
||||
:shape +draw-tool-curve+
|
||||
:priority 6}])
|
||||
:type :curve
|
||||
:priority 6}
|
||||
;; TODO: we need an icon for canvas creation
|
||||
{:icon i/box
|
||||
:help "ds.help.canvas"
|
||||
:type :canvas
|
||||
:priority 7}])
|
||||
|
||||
;; --- Draw Toolbox (Component)
|
||||
|
||||
(mf/defc draw-toolbox
|
||||
{:wrap [mf/wrap-memo]}
|
||||
[{:keys [flags] :as props}]
|
||||
(let [close #(st/emit! (dw/toggle-flag :drawtools))
|
||||
dtool (mf/deref refs/selected-drawing-tool)
|
||||
tools (->> (into [] +draw-tools+)
|
||||
(sort-by (comp :priority second)))
|
||||
(letfn [(close [event]
|
||||
(st/emit! (dw/deactivate-flag :drawtools)))
|
||||
(select [event tool]
|
||||
(st/emit! :interrupt
|
||||
(dw/deactivate-ruler)
|
||||
(dw/select-for-drawing tool)))
|
||||
(toggle-ruler [event]
|
||||
(st/emit! (dw/select-for-drawing nil)
|
||||
(dw/deselect-all)
|
||||
(dw/toggle-ruler)))]
|
||||
|
||||
select-drawtool #(st/emit! :interrupt
|
||||
(dw/deactivate-ruler)
|
||||
(dw/select-for-drawing %))
|
||||
toggle-ruler #(st/emit! (dw/select-for-drawing nil)
|
||||
(dw/deselect-all)
|
||||
(dw/toggle-ruler))]
|
||||
(let [selected (mf/deref refs/selected-drawing-tool)
|
||||
tools (sort-by (comp :priority second) +draw-tools+)]
|
||||
[:div.tool-window.drawing-tools
|
||||
[:div.tool-window-bar
|
||||
[:div.tool-window-icon i/window]
|
||||
[:span (tr "ds.settings.draw-tools")]
|
||||
[:div.tool-window-close {:on-click close} i/close]]
|
||||
[:div.tool-window-content
|
||||
(for [item tools]
|
||||
(let [selected? (= (:type item) selected)]
|
||||
[:div.tool-btn.tooltip.tooltip-hover
|
||||
{:alt (tr (:help item))
|
||||
:class (when selected? "selected")
|
||||
:key (:type item)
|
||||
:on-click #(select % (:type item))}
|
||||
(:icon item)]))
|
||||
|
||||
[:div#form-tools.tool-window.drawing-tools
|
||||
[:div.tool-window-bar
|
||||
[:div.tool-window-icon i/window]
|
||||
[:span (tr "ds.settings.draw-tools")]
|
||||
[:div.tool-window-close {:on-click close} i/close]]
|
||||
[:div.tool-window-content
|
||||
(for [[i props] (map-indexed vector tools)]
|
||||
(let [selected? (= dtool (:shape props))]
|
||||
[:div.tool-btn.tooltip.tooltip-hover
|
||||
{:alt (tr (:help props))
|
||||
:class (when selected? "selected")
|
||||
:key i
|
||||
:on-click (partial select-drawtool (:shape props))}
|
||||
(:icon props)]))
|
||||
[:div.tool-btn.tooltip.tooltip-hover
|
||||
{:alt (tr "ds.help.ruler")
|
||||
:on-click toggle-ruler
|
||||
:class (when (contains? flags :ruler) "selected")}
|
||||
i/ruler-tool]]]))
|
||||
#_[:div.tool-btn.tooltip.tooltip-hover
|
||||
{:alt (tr "ds.help.ruler")
|
||||
:on-click toggle-ruler
|
||||
:class (when (contains? flags :ruler) "selected")}
|
||||
i/ruler-tool]]])))
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.icons :as udi]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.lenses :as ul]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.dashboard.icons :as icons]
|
||||
[uxbox.main.ui.shapes.icon :as icon]
|
||||
|
@ -23,12 +22,12 @@
|
|||
|
||||
;; --- Refs
|
||||
|
||||
(def ^:private drawing-shape-ref
|
||||
"A focused vision of the drawing property
|
||||
of the workspace status. This avoids
|
||||
rerender the whole toolbox on each workspace
|
||||
change."
|
||||
(l/derive ul/selected-drawing st/state))
|
||||
;; (def ^:private drawing-shape-ref
|
||||
;; "A focused vision of the drawing property
|
||||
;; of the workspace status. This avoids
|
||||
;; rerender the whole toolbox on each workspace
|
||||
;; change."
|
||||
;; (l/derive ul/selected-drawing st/state))
|
||||
|
||||
(def ^:private icons-toolbox-ref
|
||||
(-> (l/in [:workspace :icons-toolbox])
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
|
@ -45,9 +44,10 @@
|
|||
on-blur (fn [event]
|
||||
(let [target (dom/event->target event)
|
||||
parent (.-parentNode target)
|
||||
parent (.-parentNode parent)
|
||||
name (dom/get-value target)]
|
||||
(set! (.-draggable parent) true)
|
||||
(st/emit! (uds/rename-shape (:id shape) name))
|
||||
(st/emit! (dw/rename-shape (:id shape) name))
|
||||
(swap! local assoc :edition false)))
|
||||
on-key-down (fn [event]
|
||||
(js/console.log event)
|
||||
|
@ -55,7 +55,8 @@
|
|||
(on-blur event)))
|
||||
on-click (fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [parent (.-parentNode (.-target event))]
|
||||
(let [parent (.-parentNode (.-target event))
|
||||
parent (.-parentNode parent)]
|
||||
(set! (.-draggable parent) false))
|
||||
(swap! local assoc :edition true))]
|
||||
(if (:edition @local)
|
||||
|
@ -78,18 +79,18 @@
|
|||
(let [id (:id shape)
|
||||
blocked? (:blocked shape)]
|
||||
(if blocked?
|
||||
(st/emit! (uds/unblock-shape id))
|
||||
(st/emit! (uds/block-shape id)))))
|
||||
(st/emit! (dw/unblock-shape id))
|
||||
(st/emit! (dw/block-shape id)))))
|
||||
|
||||
(toggle-visibility [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [id (:id shape)
|
||||
hidden? (:hidden shape)]
|
||||
(if hidden?
|
||||
(st/emit! (uds/show-shape id))
|
||||
(st/emit! (uds/hide-shape id)))
|
||||
(st/emit! (dw/show-shape id))
|
||||
(st/emit! (dw/hide-shape id)))
|
||||
(when (contains? selected id)
|
||||
(st/emit! (udw/select-shape id)))))
|
||||
(st/emit! (dw/select-shape id)))))
|
||||
|
||||
(select-shape [event]
|
||||
(dom/prevent-default event)
|
||||
|
@ -100,24 +101,20 @@
|
|||
nil
|
||||
|
||||
(.-ctrlKey event)
|
||||
(st/emit! (udw/select-shape id))
|
||||
(st/emit! (dw/select-shape id))
|
||||
|
||||
(> (count selected) 1)
|
||||
(st/emit! (udw/deselect-all)
|
||||
(udw/select-shape id))
|
||||
|
||||
(contains? selected id)
|
||||
(st/emit! (udw/select-shape id))
|
||||
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id))
|
||||
:else
|
||||
(st/emit! (udw/deselect-all)
|
||||
(udw/select-shape id)))))
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id)))))
|
||||
|
||||
(on-drop [item monitor]
|
||||
(st/emit! (udp/persist-page (:page shape))))
|
||||
|
||||
(on-hover [item monitor]
|
||||
(st/emit! (udw/change-shape-order {:id (:shape-id item)
|
||||
(st/emit! (dw/change-shape-order {:id (:shape-id item)
|
||||
:index index})))]
|
||||
(let [selected? (contains? selected (:id shape))
|
||||
[dprops dnd-ref] (use-sortable
|
||||
|
@ -133,8 +130,7 @@
|
|||
:dragging-TODO (:dragging? dprops))}
|
||||
[:div.element-list-body {:class (classnames :selected selected?)
|
||||
:on-click select-shape
|
||||
:on-double-click #(dom/stop-propagation %)
|
||||
:draggable true}
|
||||
:on-double-click #(dom/stop-propagation %)}
|
||||
[:div.element-actions
|
||||
[:div.toggle-element {:class (when-not (:hidden shape) "selected")
|
||||
:on-click toggle-visibility}
|
||||
|
@ -145,20 +141,52 @@
|
|||
[:div.element-icon (element-icon shape)]
|
||||
[:& layer-name {:shape shape}]]])))
|
||||
|
||||
|
||||
;; --- Layer Canvas
|
||||
|
||||
;; (mf/defc layer-canvas
|
||||
;; [{:keys [canvas selected index] :as props}]
|
||||
;; (letfn [(select-shape [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (st/emit! (dw/select-canvas (:id canvas))))
|
||||
;; (let [selected? (contains? selected (:id shape))]
|
||||
;; [:li {:class (classnames
|
||||
;; :selected selected?)}
|
||||
;; [:div.element-list-body {:class (classnames :selected selected?)
|
||||
;; :on-click select-shape
|
||||
;; :on-double-click #(dom/stop-propagation %)
|
||||
;; :draggable true}
|
||||
;; [:div.element-actions
|
||||
;; [:div.toggle-element {:class (when-not (:hidden shape) "selected")
|
||||
;; :on-click toggle-visibility}
|
||||
;; i/eye]
|
||||
;; [:div.block-element {:class (when (:blocked shape) "selected")
|
||||
;; :on-click toggle-blocking}
|
||||
;; i/lock]]
|
||||
;; [:div.element-icon (element-icon shape)]
|
||||
;; [:& layer-name {:shape shape}]]])))
|
||||
|
||||
;; --- Layers List
|
||||
|
||||
(def ^:private shapes-iref
|
||||
(-> (l/key :shapes)
|
||||
(l/derive st/state)))
|
||||
|
||||
(def ^:private canvas-iref
|
||||
(-> (l/key :canvas)
|
||||
(l/derive st/state)))
|
||||
|
||||
(mf/defc layers-list
|
||||
[{:keys [shapes selected] :as props}]
|
||||
(let [shapes-map (mf/deref shapes-iref)]
|
||||
(let [shapes-map (mf/deref shapes-iref)
|
||||
canvas-map (mf/deref canvas-iref)
|
||||
selected-shapes (mf/deref refs/selected-shapes)
|
||||
selected-canvas (mf/deref refs/selected-canvas)]
|
||||
[:div.tool-window-content
|
||||
[:ul.element-list
|
||||
(for [[index id] (map-indexed vector shapes)]
|
||||
[:& layer-item {:shape (get shapes-map id)
|
||||
:selected selected
|
||||
:selected selected-shapes
|
||||
:index index
|
||||
:key id}])]]))
|
||||
|
||||
|
@ -166,7 +194,7 @@
|
|||
|
||||
(mf/defc layers-toolbox
|
||||
[{:keys [page selected] :as props}]
|
||||
(let [on-click #(st/emit! (udw/toggle-flag :layers))
|
||||
(let [on-click #(st/emit! (dw/toggle-flag :layers))
|
||||
selected (mf/deref refs/selected-shapes)]
|
||||
[:div#layers.tool-window
|
||||
[:div.tool-window-bar
|
||||
|
|
|
@ -93,14 +93,14 @@
|
|||
value (parse-int value 0)
|
||||
sid (:id shape)
|
||||
props {attr value}]
|
||||
(st/emit! (uds/update-dimensions sid props))))
|
||||
(st/emit! (udw/update-dimensions sid props))))
|
||||
|
||||
(defn- on-rotation-change
|
||||
[event shape]
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-int value 0)
|
||||
sid (:id shape)]
|
||||
(st/emit! (uds/update-rotation sid value))))
|
||||
(st/emit! (udw/update-shape-attrs sid {:rotation value}))))
|
||||
|
||||
(defn- on-position-change
|
||||
[event shape attr]
|
||||
|
@ -108,11 +108,11 @@
|
|||
value (parse-int value nil)
|
||||
sid (:id shape)
|
||||
point (gpt/point {attr value})]
|
||||
(st/emit! (uds/update-position sid point))))
|
||||
(st/emit! (udw/update-position sid point))))
|
||||
|
||||
(defn- on-proportion-lock-change
|
||||
[event shape]
|
||||
(if (:proportion-lock shape)
|
||||
(st/emit! (uds/unlock-proportions (:id shape)))
|
||||
(st/emit! (uds/lock-proportions (:id shape)))))
|
||||
(st/emit! (udw/unlock-proportions (:id shape)))
|
||||
(st/emit! (udw/lock-proportions (:id shape)))))
|
||||
|
||||
|
|
|
@ -90,14 +90,13 @@
|
|||
value (parse-int value 0)
|
||||
sid (:id shape)
|
||||
props {attr value}]
|
||||
(st/emit! (uds/update-dimensions sid props))))
|
||||
(st/emit! (udw/update-dimensions sid props))))
|
||||
|
||||
(defn- on-rotation-change
|
||||
[event shape]
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-int value 0)
|
||||
sid (:id shape)]
|
||||
(st/emit! (uds/update-rotation sid value))))
|
||||
value (parse-int value 0)]
|
||||
(st/emit! (udw/update-shape-attrs (:id shape) {:rotation value}))))
|
||||
|
||||
(defn- on-position-change
|
||||
[event shape attr]
|
||||
|
@ -105,11 +104,11 @@
|
|||
value (parse-int value nil)
|
||||
sid (:id shape)
|
||||
point (gpt/point {attr value})]
|
||||
(st/emit! (uds/update-position sid point))))
|
||||
(st/emit! (udw/update-position sid point))))
|
||||
|
||||
(defn- on-proportion-lock-change
|
||||
[event shape]
|
||||
(if (:proportion-lock shape)
|
||||
(st/emit! (uds/unlock-proportions (:id shape)))
|
||||
(st/emit! (uds/lock-proportions (:id shape)))))
|
||||
(st/emit! (udw/unlock-proportions (:id shape)))
|
||||
(st/emit! (udw/lock-proportions (:id shape)))))
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.workspace :as udw]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.geom :as geom]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.util.data :refer (parse-int parse-float read-string)]
|
||||
|
@ -106,30 +105,30 @@
|
|||
(let [value (dom/event->value event)
|
||||
value (parse-int value 0)
|
||||
props {attr value}]
|
||||
(st/emit! (uds/update-dimensions (:id shape) props))))
|
||||
(st/emit! (dw/update-dimensions (:id shape) props))))
|
||||
|
||||
(defn- on-rotation-change
|
||||
[event shape]
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-int value 0)]
|
||||
(st/emit! (uds/update-rotation (:id shape) value))))
|
||||
(st/emit! (dw/update-shape-attrs (:id shape) {:rotation value}))))
|
||||
|
||||
(defn- on-opacity-change
|
||||
[event shape]
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-float value 1)
|
||||
value (/ value 10000)]
|
||||
(st/emit! (uds/update-attrs (:id shape) {:opacity value}))))
|
||||
(st/emit! (dw/update-shape-attrs (:id shape) {:opacity value}))))
|
||||
|
||||
(defn- on-position-change
|
||||
[event shape attr]
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-int value nil)
|
||||
point (gpt/point {attr value})]
|
||||
(st/emit! (uds/update-position (:id shape) point))))
|
||||
(st/emit! (dw/update-position (:id shape) point))))
|
||||
|
||||
(defn- on-proportion-lock-change
|
||||
[event shape]
|
||||
(if (:proportion-lock shape)
|
||||
(st/emit! (uds/unlock-proportions (:id shape)))
|
||||
(st/emit! (uds/lock-proportions (:id shape)))))
|
||||
(st/emit! (dw/unlock-proportions (:id shape)))
|
||||
(st/emit! (dw/lock-proportions (:id shape)))))
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.data.shapes :as uds]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.colorpicker :as cp]
|
||||
|
@ -59,7 +59,7 @@
|
|||
(delete [item]
|
||||
(let [sid (:id shape)
|
||||
id (:id item)]
|
||||
(st/emit! (uds/delete-interaction sid id))))
|
||||
(st/emit! (dw/delete-interaction sid id))))
|
||||
(on-delete [item event]
|
||||
(dom/prevent-default event)
|
||||
(let [delete (partial delete item)]
|
||||
|
@ -455,7 +455,7 @@
|
|||
(dom/prevent-default event)
|
||||
(let [sid (:id shape)
|
||||
data (deref form)]
|
||||
(st/emit! (uds/update-interaction sid data))
|
||||
(st/emit! (dw/update-interaction sid data))
|
||||
(reset! form nil)))
|
||||
(on-cancel [event]
|
||||
(dom/prevent-default event)
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
(on-name-change [event]
|
||||
(let [value (-> (dom/event->value event)
|
||||
(str/trim))]
|
||||
(st/emit! (->> (assoc page :name value)
|
||||
(udp/update-page (:id page))))))
|
||||
(st/emit! (-> (assoc page :name value)
|
||||
(udp/update-page-attrs)))))
|
||||
|
||||
(show-color-picker [event]
|
||||
(let [x (.-clientX event)
|
||||
|
|
|
@ -87,23 +87,23 @@
|
|||
[event shape attr]
|
||||
(let [value (-> (dom/event->value event)
|
||||
(parse-int 0))]
|
||||
(st/emit! (uds/update-dimensions (:id shape) {attr value}))))
|
||||
(st/emit! (udw/update-dimensions (:id shape) {attr value}))))
|
||||
|
||||
(defn- on-rotation-change
|
||||
[event shape]
|
||||
(let [value (-> (dom/event->value event)
|
||||
(parse-int 0))]
|
||||
(st/emit! (uds/update-rotation (:id shape) value))))
|
||||
(let [value (dom/event->value event)
|
||||
value (parse-int value 0)]
|
||||
(st/emit! (udw/update-shape-attrs (:id shape) {:rotation value}))))
|
||||
|
||||
(defn- on-position-change
|
||||
[event shape attr]
|
||||
(let [value (-> (dom/event->value event)
|
||||
(parse-int nil))
|
||||
point (gpt/point {attr value})]
|
||||
(st/emit! (uds/update-position (:id shape) point))))
|
||||
(st/emit! (udw/update-position (:id shape) point))))
|
||||
|
||||
(defn- on-proportion-lock-change
|
||||
[event shape]
|
||||
(if (:proportion-lock shape)
|
||||
(st/emit! (uds/unlock-proportions (:id shape)))
|
||||
(st/emit! (uds/lock-proportions (:id shape)))))
|
||||
(st/emit! (udw/unlock-proportions (:id shape)))
|
||||
(st/emit! (udw/lock-proportions (:id shape)))))
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
{:mixins [mx/static]}
|
||||
[menu {:keys [id] :as shape}]
|
||||
(letfn [(update-attrs [attrs]
|
||||
(st/emit! (uds/update-attrs id attrs)))
|
||||
(st/emit! (udw/update-shape-attrs id attrs)))
|
||||
(on-font-family-change [event]
|
||||
(let [value (dom/event->value event)
|
||||
attrs {:font-family (read-string value)
|
||||
|
|
|
@ -10,21 +10,18 @@
|
|||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[rumext.util :as mfu]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.data.projects :as dp]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.lightbox :as lbx]
|
||||
[uxbox.main.ui.workspace.sidebar.sitemap-pageform]
|
||||
[uxbox.main.ui.confirm :refer [confirm-dialog]]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.main.ui.workspace.sidebar.sitemap-forms :refer [page-form-dialog]]
|
||||
[uxbox.main.ui.workspace.sortable :refer [use-sortable]]
|
||||
[uxbox.util.data :refer [classnames]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.dom.dnd :as dnd]
|
||||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.i18n :refer [tr]]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
;; --- Page Item
|
||||
|
@ -32,17 +29,15 @@
|
|||
(mf/defc page-item
|
||||
[{:keys [page index deletable? selected?] :as props}]
|
||||
(letfn [(on-edit [event]
|
||||
(udl/open! :page-form {:page page}))
|
||||
(modal/show! page-form-dialog {:page page}))
|
||||
(delete []
|
||||
(let [next #(st/emit! (dp/go-to (:project page)))]
|
||||
(st/emit! (udp/delete-page (:id page) next))))
|
||||
|
||||
(st/emit! (dw/delete-page (:id page))))
|
||||
(on-delete [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(udl/open! :confirm {:on-accept delete}))
|
||||
(modal/show! confirm-dialog {:on-accept delete}))
|
||||
(on-drop [item monitor]
|
||||
(st/emit! (udp/reorder-pages (:project page))))
|
||||
(st/emit! (udp/rehash-pages (:project page))))
|
||||
(on-hover [item monitor]
|
||||
(st/emit! (udp/move-page {:project-id (:project-id item)
|
||||
:page-id (:page-id item)
|
||||
|
@ -101,7 +96,7 @@
|
|||
:fn #(-> (l/in [:projects project-id])
|
||||
(l/derive st/state))})
|
||||
project (mf/deref project-iref)
|
||||
create #(udl/open! :page-form {:page {:project project-id}})
|
||||
create #(modal/show! page-form-dialog {:page {:project project-id}})
|
||||
close #(st/emit! (dw/toggle-flag :sitemap))]
|
||||
[:div.sitemap.tool-window
|
||||
[:div.tool-window-bar
|
||||
|
|
114
frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs
Normal file
114
frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs
Normal file
|
@ -0,0 +1,114 @@
|
|||
;; 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) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.workspace.sidebar.sitemap-forms
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cljs.spec.alpha :as s]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.spec :as us]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :refer [tr]]))
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::project ::us/uuid)
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::width ::us/number-str)
|
||||
(s/def ::height ::us/number-str)
|
||||
|
||||
(s/def ::page-form
|
||||
(s/keys :req-un [::id
|
||||
::project
|
||||
::name
|
||||
::width
|
||||
::height]))
|
||||
|
||||
(def defaults
|
||||
{:name ""
|
||||
:width "1366"
|
||||
:height "768"})
|
||||
|
||||
(defn- on-submit
|
||||
[event form]
|
||||
(dom/prevent-default event)
|
||||
(modal/hide!)
|
||||
(let [data (:clean-data form)]
|
||||
(if (nil? (:id data))
|
||||
(st/emit! (udp/form->create-page data))
|
||||
(st/emit! (udp/form->update-page data)))))
|
||||
|
||||
(defn- swap-size
|
||||
[event {:keys [data] :as form}]
|
||||
(swap! data assoc
|
||||
:width (:height data)
|
||||
:height (:width data)))
|
||||
|
||||
(defn- initial-data
|
||||
[page]
|
||||
(merge {:name "" :width "1366" :height "768"}
|
||||
(select-keys page [:name :id :project])
|
||||
(select-keys (:metadata page) [:width :height])))
|
||||
|
||||
(mf/defc page-form
|
||||
[{:keys [page] :as props}]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::page-form #(initial-data page))]
|
||||
[:form {:on-submit #(on-submit % form)}
|
||||
[:input.input-text
|
||||
{:placeholder "Page name"
|
||||
:type "text"
|
||||
:name "name"
|
||||
:class (fm/error-class form :name)
|
||||
:on-blur (fm/on-input-blur form :name)
|
||||
:on-change (fm/on-input-change form :name)
|
||||
:value (:name data)
|
||||
:auto-focus true}]
|
||||
[:div.project-size
|
||||
[:div.input-element.pixels
|
||||
[:span "Width"]
|
||||
[:input#project-witdh.input-text
|
||||
{:placeholder "Width"
|
||||
:name "width"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 5000
|
||||
:class (fm/error-class form :width)
|
||||
:on-blur (fm/on-input-blur form :width)
|
||||
:on-change (fm/on-input-change form :width)
|
||||
:value (:width data)}]]
|
||||
[:a.toggle-layout {:on-click #(swap-size % form)} i/toggle]
|
||||
[:div.input-element.pixels
|
||||
[:span "Height"]
|
||||
[:input#project-height.input-text
|
||||
{:placeholder "Height"
|
||||
:name "height"
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 5000
|
||||
:class (fm/error-class form :height)
|
||||
:on-blur (fm/on-input-blur form :height)
|
||||
:on-change (fm/on-input-change form :height)
|
||||
:value (:height data)}]]]
|
||||
[:input.btn-primary
|
||||
{:value "Go go go!"
|
||||
:type "submit"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))}]]))
|
||||
|
||||
(mf/defc page-form-dialog
|
||||
[{:keys [page] :as props}]
|
||||
[:div.lightbox-body
|
||||
(if (nil? (:id page))
|
||||
[:h3 "New page"]
|
||||
[:h3 "Edit page"])
|
||||
[:& page-form {:page page}]
|
||||
[:a.close {:on-click modal/hide!} i/close]])
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
;; 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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.workspace.sidebar.sitemap-pageform
|
||||
(:require [cljs.spec.alpha :as s :include-macros true]
|
||||
[lentes.core :as l]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.constants :as c]
|
||||
[uxbox.main.data.pages :as udp]
|
||||
[uxbox.main.data.lightbox :as udl]
|
||||
[uxbox.main.ui.lightbox :as lbx]
|
||||
[uxbox.util.data :refer [parse-int]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.router :as r]
|
||||
[rumext.core :as mx :include-macros true]))
|
||||
|
||||
|
||||
(def form-data (fm/focus-data :workspace-page-form st/state))
|
||||
(def form-errors (fm/focus-errors :workspace-page-form st/state))
|
||||
|
||||
(def assoc-value (partial fm/assoc-value :workspace-page-form))
|
||||
(def assoc-error (partial fm/assoc-error :workspace-page-form))
|
||||
(def clear-form (partial fm/clear-form :workspace-page-form))
|
||||
|
||||
;; --- Lightbox
|
||||
|
||||
(s/def ::name ::fm/non-empty-string)
|
||||
(s/def ::layout ::fm/non-empty-string)
|
||||
(s/def ::width number?)
|
||||
(s/def ::height number?)
|
||||
|
||||
(s/def ::page-form
|
||||
(s/keys :req-un [::name
|
||||
::width
|
||||
::height
|
||||
::layout]))
|
||||
|
||||
(mx/defc layout-input
|
||||
[data id]
|
||||
(let [{:keys [id name width height]} (get c/page-layouts id)]
|
||||
(letfn [(on-change [event]
|
||||
(st/emit! (assoc-value :layout id)
|
||||
(assoc-value :width width)
|
||||
(assoc-value :height height)))]
|
||||
[:div
|
||||
[:input {:type "radio"
|
||||
:id id
|
||||
:name "project-layout"
|
||||
:value id
|
||||
:checked (when (= id (:layout data)) "checked")
|
||||
:on-change on-change}]
|
||||
[:label {:value id :for id} name]])))
|
||||
|
||||
(mx/defc page-form
|
||||
{:mixins [mx/static mx/reactive]}
|
||||
[{:keys [metadata id] :as page}]
|
||||
(let [data (merge c/page-defaults
|
||||
(select-keys page [:name :id :project])
|
||||
(select-keys metadata [:width :height :layout])
|
||||
(mx/react form-data))
|
||||
valid? (fm/valid? ::page-form data)]
|
||||
(letfn [(update-size [field e]
|
||||
(let [value (dom/event->value e)
|
||||
value (parse-int value)]
|
||||
(st/emit! (assoc-value field value))))
|
||||
(update-name [e]
|
||||
(let [value (dom/event->value e)]
|
||||
(st/emit! (assoc-value :name value))))
|
||||
(toggle-sizes []
|
||||
(let [{:keys [width height]} data]
|
||||
(st/emit! (assoc-value :width width)
|
||||
(assoc-value :height height))))
|
||||
(on-cancel [e]
|
||||
(dom/prevent-default e)
|
||||
(udl/close!))
|
||||
(on-save [e]
|
||||
(dom/prevent-default e)
|
||||
(udl/close!)
|
||||
(if (nil? id)
|
||||
(st/emit! (udp/create-page data))
|
||||
(st/emit! (udp/persist-page-update-form id data))))]
|
||||
[:form
|
||||
[:input#project-name.input-text
|
||||
{:placeholder (tr "ds.page.placeholder")
|
||||
:type "text"
|
||||
:value (:name data "")
|
||||
:auto-focus true
|
||||
:on-change update-name}]
|
||||
[:div.project-size
|
||||
[:div.input-element.pixels
|
||||
[:span (tr "ds.width")]
|
||||
[:input#project-witdh.input-text
|
||||
{:placeholder (tr "ds.width")
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 4000
|
||||
:value (:width data)
|
||||
:on-change #(update-size :width %)}]]
|
||||
[:a.toggle-layout {:on-click toggle-sizes} i/toggle]
|
||||
[:div.input-element.pixels
|
||||
[:span (tr "ds.height")]
|
||||
[:input#project-height.input-text
|
||||
{:placeholder (tr "ds.height")
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 4000
|
||||
:value (:height data)
|
||||
:on-change #(update-size :height %)}]]]
|
||||
|
||||
[:div.input-radio.radio-primary
|
||||
(layout-input data "mobile")
|
||||
(layout-input data "tablet")
|
||||
(layout-input data "notebook")
|
||||
(layout-input data "desktop")]
|
||||
|
||||
[:input#project-btn.btn-primary
|
||||
{:value (tr "ds.go")
|
||||
:disabled (not valid?)
|
||||
:on-click on-save
|
||||
:type "button"}]])))
|
||||
|
||||
(mx/defc page-form-lightbox
|
||||
{:mixins [mx/static (fm/clear-mixin st/store :workspace-page-form)]}
|
||||
[{:keys [id] :as page}]
|
||||
(letfn [(on-cancel [event]
|
||||
(dom/prevent-default event)
|
||||
(udl/close!))]
|
||||
(let [creation? (nil? id)]
|
||||
[:div.lightbox-body
|
||||
(if creation?
|
||||
[:h3 (tr "ds.page.new")]
|
||||
[:h3 (tr "ds.page.edit")])
|
||||
(page-form page)
|
||||
[:a.close {:on-click on-cancel} i/close]])))
|
||||
|
||||
(defmethod lbx/render-lightbox :page-form
|
||||
[{:keys [page]}]
|
||||
(page-form-lightbox page))
|
|
@ -52,21 +52,7 @@
|
|||
(and (mouse-event? v)
|
||||
(= :click (:type v))))
|
||||
|
||||
(defrecord PointerEvent [window
|
||||
viewport
|
||||
ctrl
|
||||
shift])
|
||||
|
||||
(defn pointer-event
|
||||
[window viewport ctrl shift]
|
||||
{:pre [(gpt/point? window)
|
||||
(gpt/point? viewport)
|
||||
(boolean? ctrl)
|
||||
(boolean? shift)]}
|
||||
(PointerEvent. window
|
||||
viewport
|
||||
ctrl
|
||||
shift))
|
||||
(defrecord PointerEvent [source pt ctrl shift])
|
||||
|
||||
(defn pointer-event?
|
||||
[v]
|
||||
|
@ -90,33 +76,26 @@
|
|||
|
||||
;; --- Derived streams
|
||||
|
||||
;; TODO: this shoul be DEPRECATED
|
||||
(defonce interaction-events
|
||||
(rx/filter interaction-event? st/stream))
|
||||
|
||||
(defonce mouse-position
|
||||
(rx/filter pointer-event? st/stream))
|
||||
|
||||
(defonce viewport-mouse-position
|
||||
(let [sub (rx/behavior-subject nil)]
|
||||
(-> (rx/map :viewport mouse-position)
|
||||
(rx/subscribe-with sub))
|
||||
sub))
|
||||
|
||||
(defonce window-mouse-position
|
||||
(let [sub (rx/behavior-subject nil)]
|
||||
(-> (rx/map :window mouse-position)
|
||||
(rx/subscribe-with sub))
|
||||
(let [sub (rx/behavior-subject nil)
|
||||
ob (->> st/stream
|
||||
(rx/filter pointer-event?)
|
||||
(rx/filter #(= :viewport (:source %)))
|
||||
(rx/map :pt))]
|
||||
(rx/subscribe-with ob sub)
|
||||
sub))
|
||||
|
||||
(defonce mouse-position-ctrl
|
||||
(let [sub (rx/behavior-subject nil)]
|
||||
(-> (rx/map :ctrl mouse-position)
|
||||
(rx/subscribe-with sub))
|
||||
(let [sub (rx/behavior-subject nil)
|
||||
ob (->> st/stream
|
||||
(rx/filter pointer-event?)
|
||||
(rx/map :ctrl)
|
||||
(rx/dedupe))]
|
||||
(rx/subscribe-with ob sub)
|
||||
sub))
|
||||
|
||||
(defonce mouse-position-deltas
|
||||
(->> viewport-mouse-position
|
||||
(->> mouse-position
|
||||
(rx/sample 10)
|
||||
(rx/map #(gpt/divide % @refs/selected-zoom))
|
||||
(rx/mapcat (fn [point]
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
[uxbox.main.ui.workspace.canvas :refer [canvas]]
|
||||
[uxbox.main.ui.workspace.grid :refer [grid]]
|
||||
[uxbox.main.ui.workspace.ruler :refer [ruler]]
|
||||
[uxbox.main.ui.workspace.streams :as uws]
|
||||
|
@ -37,7 +36,7 @@
|
|||
|
||||
(mf/defc coordinates
|
||||
[{:keys [zoom] :as props}]
|
||||
(let [coords (some-> (use-rxsub uws/viewport-mouse-position)
|
||||
(let [coords (some-> (use-rxsub uws/mouse-position)
|
||||
(gpt/divide zoom)
|
||||
(gpt/round 0))]
|
||||
[:ul.coordinates
|
||||
|
@ -60,16 +59,16 @@
|
|||
:circle "Drag to draw a Circle"
|
||||
nil))
|
||||
|
||||
(mf/defc cursor-tooltip
|
||||
{:wrap [mf/wrap-memo]}
|
||||
[{:keys [tooltip]}]
|
||||
(let [coords (mf/deref refs/window-mouse-position)]
|
||||
[:span.cursor-tooltip
|
||||
{:style
|
||||
{:position "fixed"
|
||||
:left (str (+ (:x coords) 5) "px")
|
||||
:top (str (- (:y coords) 25) "px")}}
|
||||
tooltip]))
|
||||
;; (mf/defc cursor-tooltip
|
||||
;; {:wrap [mf/wrap-memo]}
|
||||
;; [{:keys [tooltip]}]
|
||||
;; (let [coords (mf/deref refs/window-mouse-position)]
|
||||
;; [:span.cursor-tooltip
|
||||
;; {:style
|
||||
;; {:position "fixed"
|
||||
;; :left (str (+ (:x coords) 5) "px")
|
||||
;; :top (str (- (:y coords) 25) "px")}}
|
||||
;; tooltip]))
|
||||
|
||||
;; --- Selection Rect
|
||||
|
||||
|
@ -89,15 +88,13 @@
|
|||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (->> (rx/merge (rx/filter #(= % :interrupt) stream)
|
||||
(rx/filter uws/mouse-up? stream))
|
||||
(rx/take 1))]
|
||||
(let [stoper (rx/filter #(or (dw/interrupt? %) (uws/mouse-up? %)) stream)]
|
||||
(rx/concat
|
||||
(->> uws/viewport-mouse-position
|
||||
(rx/of (dw/deselect-all))
|
||||
(->> uws/mouse-position
|
||||
(rx/map (fn [pos] #(update-state % pos)))
|
||||
(rx/take-until stoper))
|
||||
(rx/of (dw/deselect-all)
|
||||
dw/select-shapes-by-current-selrect
|
||||
(rx/of dw/select-shapes-by-current-selrect
|
||||
clear-state)))))))
|
||||
|
||||
(mf/defc selrect
|
||||
|
@ -115,34 +112,76 @@
|
|||
;; --- Viewport Positioning
|
||||
|
||||
(def handle-viewport-positioning
|
||||
(reify
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (->> (rx/filter #(= ::finish-positioning %) stream)
|
||||
(rx/take 1))
|
||||
reference @uws/viewport-mouse-position
|
||||
dom (dom/get-element "workspace-viewport")]
|
||||
(->> uws/viewport-mouse-position
|
||||
(rx/map (fn [point]
|
||||
(let [{:keys [x y]} (gpt/subtract point reference)
|
||||
cx (.-scrollLeft dom)
|
||||
cy (.-scrollTop dom)]
|
||||
(set! (.-scrollLeft dom) (- cx x))
|
||||
(set! (.-scrollTop dom) (- cy y)))))
|
||||
(rx/take-until stoper)
|
||||
(rx/ignore))))))
|
||||
(letfn [(on-point [dom reference point]
|
||||
(let [{:keys [x y]} (gpt/subtract point reference)
|
||||
cx (.-scrollLeft dom)
|
||||
cy (.-scrollTop dom)]
|
||||
(set! (.-scrollLeft dom) (- cx x))
|
||||
(set! (.-scrollTop dom) (- cy y))))]
|
||||
(reify
|
||||
ptk/EffectEvent
|
||||
(effect [_ state stream]
|
||||
(let [stoper (rx/filter #(= ::finish-positioning %) stream)
|
||||
reference @uws/mouse-position
|
||||
dom (dom/get-element "workspace-viewport")]
|
||||
(-> (rx/take-until stoper uws/mouse-position)
|
||||
(rx/subscribe #(on-point dom reference %))))))))
|
||||
|
||||
;; --- Viewport
|
||||
|
||||
(mf/def viewport
|
||||
:init
|
||||
(fn [own props]
|
||||
(assoc own ::viewport (mf/create-ref)))
|
||||
(mf/defc viewport
|
||||
[{:keys [page] :as props}]
|
||||
(let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/deref refs/workspace)
|
||||
viewport-ref (mf/use-ref nil)
|
||||
tooltip (or tooltip (get-shape-tooltip drawing-tool))
|
||||
zoom (or zoom 1)]
|
||||
(letfn [(on-mouse-down [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :down ctrl? shift?)))
|
||||
(when (not edition)
|
||||
(if drawing-tool
|
||||
(st/emit! (start-drawing drawing-tool))
|
||||
(st/emit! :interrupt handle-selrect))))
|
||||
|
||||
:did-mount
|
||||
(fn [own]
|
||||
(letfn [(translate-point-to-viewport [pt]
|
||||
(let [viewport (mf/ref-node (::viewport own))
|
||||
(on-context-menu [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :context-menu ctrl? shift?))))
|
||||
|
||||
(on-mouse-up [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :up ctrl? shift?))))
|
||||
|
||||
(on-click [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :click ctrl? shift?))))
|
||||
|
||||
(on-double-click [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :double-click ctrl? shift?))))
|
||||
|
||||
(translate-point-to-viewport [pt]
|
||||
(let [viewport (mf/ref-node viewport-ref)
|
||||
brect (.getBoundingClientRect viewport)
|
||||
brect (gpt/point (parse-int (.-left brect))
|
||||
(parse-int (.-top brect)))]
|
||||
|
@ -173,115 +212,56 @@
|
|||
(st/emit! ::finish-positioning #_(dw/stop-viewport-positioning)))
|
||||
(st/emit! (uws/keyboard-event :up key ctrl? shift?))))
|
||||
|
||||
(on-mousemove [event]
|
||||
(let [wpt (gpt/point (.-clientX event)
|
||||
(.-clientY event))
|
||||
vpt (translate-point-to-viewport wpt)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
event {:ctrl ctrl?
|
||||
:shift shift?
|
||||
:window-coords wpt
|
||||
:viewport-coords vpt}]
|
||||
(st/emit! (uws/pointer-event wpt vpt ctrl? shift?))))]
|
||||
(on-mouse-move [event]
|
||||
(let [pt (gpt/point (.-clientX event)
|
||||
(.-clientY event))
|
||||
pt (translate-point-to-viewport pt)]
|
||||
(st/emit! (uws/->PointerEvent :viewport pt
|
||||
(kbd/ctrl? event)
|
||||
(kbd/shift? event)))))
|
||||
|
||||
(let [key1 (events/listen js/document EventType.MOUSEMOVE on-mousemove)
|
||||
key2 (events/listen js/document EventType.KEYDOWN on-key-down)
|
||||
key3 (events/listen js/document EventType.KEYUP on-key-up)]
|
||||
(assoc own
|
||||
::key1 key1
|
||||
::key2 key2
|
||||
::key3 key3))))
|
||||
(on-mount []
|
||||
(let [key1 (events/listen js/document EventType.KEYDOWN on-key-down)
|
||||
key2 (events/listen js/document EventType.KEYUP on-key-up)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2))))]
|
||||
|
||||
:will-unmount
|
||||
(fn [own]
|
||||
(events/unlistenByKey (::key1 own))
|
||||
(events/unlistenByKey (::key2 own))
|
||||
(events/unlistenByKey (::key3 own))
|
||||
(dissoc own ::key1 ::key2 ::key3))
|
||||
(mf/use-effect on-mount)
|
||||
[:*
|
||||
[:& coordinates {:zoom zoom}]
|
||||
#_[:div.tooltip-container
|
||||
(when tooltip
|
||||
[:& cursor-tooltip {:tooltip tooltip}])]
|
||||
[:svg.viewport {:width (* c/viewport-width zoom)
|
||||
:height (* c/viewport-height zoom)
|
||||
:ref viewport-ref
|
||||
:class (when drawing-tool "drawing")
|
||||
:on-context-menu on-context-menu
|
||||
:on-click on-click
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-move on-mouse-move
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-mouse-up on-mouse-up}
|
||||
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
|
||||
(when page
|
||||
[:*
|
||||
(for [id (reverse (:shapes page))]
|
||||
[:& uus/shape-component {:id id :key id}])
|
||||
|
||||
:mixins [mf/reactive]
|
||||
(when (seq (:selected wst))
|
||||
[:& selection-handlers {:wst wst}])
|
||||
|
||||
:render
|
||||
(fn [own {:keys [page] :as props}]
|
||||
(let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/react refs/workspace)
|
||||
tooltip (or tooltip (get-shape-tooltip drawing-tool))
|
||||
zoom (or zoom 1)]
|
||||
(letfn [(on-mouse-down [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :down ctrl? shift?)))
|
||||
(when (not edition)
|
||||
(if drawing-tool
|
||||
(st/emit! (start-drawing drawing-tool))
|
||||
(st/emit! :interrupt handle-selrect))))
|
||||
(on-context-menu [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :context-menu ctrl? shift?))))
|
||||
(on-mouse-up [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :up ctrl? shift?))))
|
||||
(on-click [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :click ctrl? shift?))))
|
||||
(on-double-click [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (uws/mouse-event :double-click ctrl? shift?))))]
|
||||
[:*
|
||||
[:& coordinates {:zoom zoom}]
|
||||
[:div.tooltip-container
|
||||
(when tooltip
|
||||
[:& cursor-tooltip {:tooltip tooltip}])]
|
||||
[:svg.viewport {:width (* c/viewport-width zoom)
|
||||
:height (* c/viewport-height zoom)
|
||||
:ref (::viewport own)
|
||||
:class (when drawing-tool "drawing")
|
||||
:on-context-menu on-context-menu
|
||||
:on-click on-click
|
||||
:on-double-click on-double-click
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-mouse-up on-mouse-up}
|
||||
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
|
||||
(when page
|
||||
[:& canvas {:page page :wst wst}])
|
||||
|
||||
(when page
|
||||
[:*
|
||||
(for [id (reverse (:shapes page))]
|
||||
[:& uus/shape-component {:id id :key id}])
|
||||
|
||||
(when (seq (:selected wst))
|
||||
[:& selection-handlers {:wst wst}])
|
||||
|
||||
(when-let [dshape (:drawing wst)]
|
||||
[:& draw-area {:shape dshape
|
||||
:zoom (:zoom wst)
|
||||
:modifiers (:modifiers wst)}])])
|
||||
(when-let [dshape (:drawing wst)]
|
||||
[:& draw-area {:shape dshape
|
||||
:zoom (:zoom wst)
|
||||
:modifiers (:modifiers wst)}])])
|
||||
|
||||
|
||||
|
||||
(if (contains? flags :grid)
|
||||
[:& grid {:page page}])]
|
||||
(when (contains? flags :ruler)
|
||||
[:& ruler {:zoom zoom :ruler (:ruler wst)}])
|
||||
[:& selrect {:data (:selrect wst)}]]]))))
|
||||
(if (contains? flags :grid)
|
||||
[:& grid {:page page}])]
|
||||
(when (contains? flags :ruler)
|
||||
[:& ruler {:zoom zoom :ruler (:ruler wst)}])
|
||||
[:& selrect {:data (:selrect wst)}]]])))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
(:require [beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.forms :as sc]
|
||||
[uxbox.util.data :refer (parse-int)]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.data.pages :as udpg]
|
||||
|
|
|
@ -56,9 +56,8 @@
|
|||
(l/derive st/state)))
|
||||
|
||||
(mf/defc app
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[]
|
||||
(let [route (mf/react route-ref)]
|
||||
(let [route (mf/deref route-ref)]
|
||||
(case (get-in route [:data :name])
|
||||
:view/notfound (notfound-page)
|
||||
:view/viewer (let [{:keys [token id]} (get-in route [:params :path])]
|
||||
|
|
|
@ -34,9 +34,8 @@
|
|||
;; --- Component
|
||||
|
||||
(mf/defc viewer-page
|
||||
{:wrap [mf/wrap-reactive]}
|
||||
[{:keys [token id]}]
|
||||
(let [{:keys [project pages flags]} (mf/react state-ref)]
|
||||
(let [{:keys [project pages flags]} (mf/deref state-ref)]
|
||||
(mf/use-effect
|
||||
{:fn #(st/emit! (dv/initialize token))})
|
||||
(when (seq pages)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue