🎉 Create reusable components

This commit is contained in:
Andrés Moya 2020-09-01 15:09:57 +02:00
parent 8396357f36
commit 5e585201d3
17 changed files with 515 additions and 168 deletions

View file

@ -22,6 +22,7 @@
[app.config :as cfg]
[app.main.constants :as c]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws]
@ -1266,70 +1267,19 @@
;; GROUPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn group-shape
[id frame-id selected selection-rect]
{:id id
:type :group
:name (name (gensym "Group-"))
:shapes []
:frame-id frame-id
:x (:x selection-rect)
:y (:y selection-rect)
:width (:width selection-rect)
:height (:height selection-rect)})
(def group-selected
(ptk/reify ::group-selected
ptk/WatchEvent
(watch [_ state stream]
(let [id (uuid/next)
page-id (:current-page-id state)
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
items (->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
(map #(assoc % ::index (cph/position-on-parent (:id %) objects)))
(sort-by ::index))]
(when (not-empty items)
(let [selrect (geom/selection-rect items)
frame-id (-> items first :frame-id)
parent-id (-> items first :parent-id)
group (-> (group-shape id frame-id selected selrect)
(geom/setup selrect))
index (::index (first items))
rchanges [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:parent-id parent-id
:obj group
:index index}
{:type :mov-objects
:page-id page-id
:parent-id id
:shapes (->> items
(map :id)
(into #{})
(vec))}]
uchanges
(reduce (fn [res obj]
(conj res {:type :mov-objects
:page-id page-id
:parent-id (:parent-id obj)
:index (::index obj)
:shapes [(:id obj)]}))
[]
items)
uchanges (conj uchanges {:type :del-obj :id id :page-id page-id})]
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [[group rchanges uchanges]
(dws/prepare-create-group page-id shapes "Group-" false)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set id)))))))))
(dws/select-shapes (d/ordered-set (:id group))))))))))
(def ungroup-selected
(ptk/reify ::ungroup-selected
@ -1342,34 +1292,11 @@
group (get objects group-id)]
(when (and (= 1 (count selected))
(= (:type group) :group))
(let [shapes (:shapes group)
parent-id (cph/get-parent group-id objects)
parent (get objects parent-id)
index-in-parent (->> (:shapes parent)
(map-indexed vector)
(filter #(#{group-id} (second %)))
(ffirst))
rchanges [{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
uchanges [{:type :add-obj
:page-id page-id
:id group-id
:frame-id (:frame-id group)
:obj (assoc group :shapes [])}
{:type :mov-objects
:page-id page-id
:parent-id group-id
:shapes shapes}
{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes [group-id]
:index index-in-parent}]]
(let [[rchanges uchanges]
(dws/prepare-remove-group page-id group objects)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1512,6 +1439,7 @@
"+" #(st/emit! (increase-zoom nil))
"-" #(st/emit! (decrease-zoom nil))
"ctrl+g" #(st/emit! group-selected)
"ctrl+k" #(st/emit! dwl/add-component)
"shift+g" #(st/emit! ungroup-selected)
"shift+0" #(st/emit! reset-zoom)
"shift+1" #(st/emit! zoom-to-fit-all)

View file

@ -13,6 +13,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.common.pages :as cp]
[app.main.repo :as rp]
[app.main.store :as st]
@ -68,7 +69,7 @@
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn delete-color
[{:keys [id] :as color}]
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-color
ptk/WatchEvent
@ -94,7 +95,7 @@
(defn delete-media
[{:keys [id] :as media}]
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-media
ptk/WatchEvent
@ -106,3 +107,118 @@
:object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(declare clone-shape)
(def add-component
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
shapes (dws/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; we need to create a group before creating the component.
[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dws/prepare-create-group page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(clone-shape group nil objects)
rchanges (conj rchanges
{:type :add-component
:id (:id new-shape)
:name (:name new-shape)
:new-shapes new-shapes})
rchanges (into rchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val (:component-id updated-shape)}]})
updated-shapes))
uchanges (conj uchanges
{:type :del-component
:id (:id new-shape)})
uchanges (into uchanges
(map (fn [updated-shape]
{:type :mod-obj
:page-id page-id
:id (:id updated-shape)
:operations [{:type :set
:attr :component-id
:val nil}]})
updated-shapes))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(defn- clone-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links
to the new ones."
[shape parent-id objects]
(let [new-id (uuid/next)]
(if (nil? (:shapes shape))
; TODO: unify this case with the empty child-ids case.
(let [new-shape (assoc shape
:id new-id
:parent-id parent-id
:frame-id nil)]
[new-shape
[new-shape]
[(assoc shape :component-id (:id new-shape))]])
(loop [child-ids (seq (:shapes shape))
new-children []
updated-children []]
(if (empty? child-ids)
(let [new-shape (assoc shape
:id new-id
:parent-id parent-id
:frame-id nil
:shapes (map :id new-children))]
[new-shape
(conj new-children new-shape)
(conj updated-children
(assoc shape :component-id (:id new-shape)))])
(let [child-id (first child-ids)
child (get objects child-id)
[new-child new-child-shapes updated-child-shapes]
(clone-shape child new-id objects)]
(recur
(next child-ids)
(into new-children new-child-shapes)
(into updated-children updated-child-shapes))))))))
(defn delete-component
[{:keys [id] :as params}]
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [_ state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
:id id}]
uchanges [{:type :add-component
:id id
:name (:name component)
:new-shapes (:objects component)}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))

View file

@ -183,6 +183,86 @@
(rx/of deselect-all (select-shape (:id selected))))))))
;; --- Group shapes
(defn shapes-for-grouping
[objects selected]
(->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
(map #(assoc % ::index (cph/position-on-parent (:id %) objects)))
(sort-by ::index)))
(defn- make-group
[shapes prefix keep-name]
(let [selrect (geom/selection-rect shapes)
frame-id (-> shapes first :frame-id)
parent-id (-> shapes first :parent-id)
group-name (if (and keep-name (= (count shapes) 1))
(:name (first shapes))
(name (gensym prefix)))]
(-> (cp/make-minimal-group frame-id selrect group-name)
(geom/setup selrect)
(assoc :shapes (map :id shapes)))))
(defn prepare-create-group
[page-id shapes prefix keep-name]
(let [group (make-group shapes prefix keep-name)
rchanges [{:type :add-obj
:id (:id group)
:page-id page-id
:frame-id (:frame-id (first shapes))
:parent-id (:parent-id (first shapes))
:obj group
:index (::index (first shapes))}
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes (map :id shapes)}]
uchanges (conj
(map (fn [obj] {:type :mov-objects
:page-id page-id
:parent-id (:parent-id obj)
:index (::index obj)
:shapes [(:id obj)]})
shapes)
{:type :del-obj
:id (:id group)
:page-id page-id})]
[group rchanges uchanges]))
(defn prepare-remove-group
[page-id group objects]
(let [shapes (:shapes group)
parent-id (cph/get-parent (:id group) objects)
parent (get objects parent-id)
index-in-parent (->> (:shapes parent)
(map-indexed vector)
(filter #(#{(:id group)} (second %)))
(ffirst))
rchanges [{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
uchanges [{:type :add-obj
:page-id page-id
:id (:id group)
:frame-id (:frame-id group)
:obj (assoc group :shapes [])}
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes shapes}
{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes [(:id group)]
:index index-in-parent}]]
[rchanges uchanges]))
;; --- Duplicate Shapes
(declare prepare-duplicate-change)
(declare prepare-duplicate-frame-change)

View file

@ -156,3 +156,35 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame :view-box vbox}]]))
;; TODO: unify with frame-svg?
(mf/defc component-svg
{::mf/wrap [mf/memo]}
[{:keys [objects group zoom] :or {zoom 1} :as props}]
(let [modifier (-> (gpt/point (:x group) (:y group))
(gpt/negate)
(gmt/translate-matrix))
group-id (:id group)
modifier-ids (concat [group-id] (cph/get-children group-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids)
group (assoc-in group [:modifiers :displacement] modifier)
width (* (:width group) zoom)
height (* (:height group) zoom)
vbox (str "0 0 " (:width group 0)
" " (:height group 0))
wrapper (mf/use-memo
(mf/deps objects)
#(group-wrapper-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape group :view-box vbox}]]))

View file

@ -67,4 +67,4 @@
(defn ^:export dump-objects []
(let [page-id (get @state :current-page-id)]
(logjs "state" (get-in @state [:workspace-data page-id :objects]))))
(logjs "state" (get-in @state [:workspace-data :pages-index page-id :objects]))))

View file

@ -31,6 +31,7 @@
(def chat (icon-xref :chat))
(def circle (icon-xref :circle))
(def close (icon-xref :close))
(def component (icon-xref :component))
(def copy (icon-xref :copy))
(def curve (icon-xref :curve))
(def download (icon-xref :download))

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.ui.hooks :refer [use-rxsub]]
[app.main.ui.components.dropdown :refer [dropdown]]))
@ -59,7 +60,8 @@
do-lock-shape #(st/emit! (dw/update-shape-flags id {:blocked true}))
do-unlock-shape #(st/emit! (dw/update-shape-flags id {:blocked false}))
do-create-group #(st/emit! dw/group-selected)
do-remove-group #(st/emit! dw/ungroup-selected)]
do-remove-group #(st/emit! dw/ungroup-selected)
do-add-component #(st/emit! dwl/add-component)]
[:*
[:& menu-entry {:title "Copy"
:shortcut "Ctrl + c"
@ -101,13 +103,17 @@
[:& menu-entry {:title "Hide"
:on-click do-hide-shape}])
(if (:blocked shape)
[:& menu-entry {:title "Unlock"
:on-click do-unlock-shape}]
[:& menu-entry {:title "Lock"
:on-click do-lock-shape}])
[:& menu-separator]
[:& menu-entry {:title "Create component"
:shortcut "Ctrl + K"
:on-click do-add-component}]
[:& menu-separator]
[:& menu-entry {:title "Delete"
:shortcut "Supr"

View file

@ -14,6 +14,7 @@
[app.common.geom.shapes :as geom]
[app.common.media :as cm]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.workspace :as dw]
@ -21,6 +22,7 @@
[app.main.data.colors :as dc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.exports :as exports]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
@ -38,6 +40,62 @@
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc components-box
[{:keys [file-id local? components] :as props}]
(let [state (mf/use-state {:menu-open false
:top nil
:left nil
:component-id nil})
on-delete
(mf/use-callback
(mf/deps state)
(fn []
(let [params {:id (:component-id @state)}]
(st/emit! (dwl/delete-component params)))))
on-context-menu
(mf/use-callback
(fn [component-id]
(fn [event]
(when local?
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
(dom/prevent-default event)
(swap! state assoc :menu-open true
:top top
:left left
:component-id component-id))))))
on-drag-start
(mf/use-callback
(fn [path event]
(dnd/set-data! event "text/uri-list" (cfg/resolve-media-path path))
(dnd/set-allowed-effect! event "move")))]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.components")
[:span (str "\u00A0(") (count components) ")"]] ;; Unicode 00A0 is non-breaking space
[:div.group-grid.big
(for [component components]
[:div.grid-cell {:key (:id component)
:draggable true
:on-context-menu (on-context-menu (:id component))
:on-drag-start (partial on-drag-start (:path component))}
[:& exports/component-svg {:group (get-in component [:objects (:id component)])
:objects (:objects component)}]
[:div.cell-name (:name component)]])
(when local?
[:& context-menu
{:selectable false
:show (:menu-open @state)
:on-close #(swap! state assoc :menu-open false)
:top (:top @state)
:left (:left @state)
:options [[(tr "workspace.assets.delete") on-delete]]}])]]))
(mf/defc graphics-box
[{:keys [file-id local? objects open? on-open on-close] :as props}]
(let [input-ref (mf/use-ref nil)
@ -126,7 +184,6 @@
:left (:left @state)
:options [[(tr "workspace.assets.delete") on-delete]]}])])]))
(mf/defc color-item
[{:keys [color local? locale file-id] :as props}]
(let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
@ -287,32 +344,45 @@
(vals (get-in state [:workspace-libraries id :data :media])))))
st/state =))
(defn file-components-ref
[id]
(l/derived (fn [state]
(let [wfile (:workspace-file state)]
(if (= (:id wfile) id)
(vals (get-in wfile [:data :components]))
(vals (get-in state [:workspace-libraries id :data :components])))))
st/state =))
(defn apply-filters
[coll filters]
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters))))
coll))
(->> coll
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters)))))
(sort-by #(str/lower (:name %)))))
(mf/defc file-library
[{:keys [file local? open? filters locale] :as props}]
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
toggles (mf/use-state #{:graphics :colors})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)]
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)
components-ref (mf/use-memo (mf/deps (:id file)) #(file-components-ref (:id file)))
components (apply-filters (mf/deref components-ref) filters)]
[:div.tool-window
[:div.tool-window-bar
@ -332,15 +402,23 @@
[:a {:href (str "#" url) :target "_blank"} i/chain]]])]
(when @open?
(let [show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :graphics))
(or (> (count media) 0)
(str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all)
(= (:box filters) :colors))
(or (> (count colors) 0)
(str/empty? (:term filters))))]
(let [show-components? (and (or (= (:box filters) :all)
(= (:box filters) :components))
(or (> (count components) 0)
(str/empty? (:term filters))))
show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :graphics))
(or (> (count media) 0)
(str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all)
(= (:box filters) :colors))
(or (> (count colors) 0)
(str/empty? (:term filters))))]
[:div.tool-window-content
(when show-components?
[:& components-box {:file-id (:id file)
:local? local?
:components components}])
(when show-graphics?
[:& graphics-box {:file-id (:id file)
:local? local?
@ -357,10 +435,11 @@
:on-open #(swap! toggles conj :colors)
:on-close #(swap! toggles disj :colors)}])
(when (and (not show-graphics?) (not show-colors?))
(when (and (not show-components?) (not show-graphics?) (not show-colors?))
[:div.asset-group
[:div.group-title (t locale "workspace.assets.not-found")]])]))]))
(mf/defc assets-toolbox
[{:keys [team-id file] :as props}]
(let [libraries (mf/deref refs/workspace-libraries)

View file

@ -43,7 +43,9 @@
:rect i/box
:curve i/curve
:text i/text
:group i/folder
:group (if (nil? (:component-id shape))
i/folder
i/component)
nil))
;; --- Layer Name
@ -186,6 +188,7 @@
[:li {:on-context-menu on-context-menu
:ref dref
:class (dom/classnames
:component (not (nil? (:component-id item)))
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot)
@ -285,7 +288,16 @@
(defn- strip-objects
[objects]
(let [strip-data #(select-keys % [:id :name :blocked :hidden :shapes :type :content :parent-id :metadata])]
(let [strip-data #(select-keys % [:id
:name
:blocked
:hidden
:shapes
:type
:content
:parent-id
:component-id
:metadata])]
(persistent!
(reduce-kv (fn [res id obj]
(assoc! res id (strip-data obj)))