mirror of
https://github.com/penpot/penpot.git
synced 2026-05-22 16:33:55 +00:00
340 lines
11 KiB
Clojure
340 lines
11 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
;; defined by the Mozilla Public License, v. 2.0.
|
|
;;
|
|
;; Copyright (c) 2020 UXBOX Labs SL
|
|
|
|
(ns app.main.ui.workspace.sidebar.layers
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.pages :as cp]
|
|
[app.common.pages-helpers :as cph]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.workspace :as dw]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.hooks :as hooks]
|
|
[app.main.ui.icons :as i]
|
|
[app.main.ui.keyboard :as kbd]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :as i18n :refer [t]]
|
|
[app.util.object :as obj]
|
|
[app.util.perf :as perf]
|
|
[app.util.timers :as ts]
|
|
[beicon.core :as rx]
|
|
[okulary.core :as l]
|
|
[rumext.alpha :as mf]))
|
|
|
|
;; --- Helpers
|
|
|
|
(mf/defc element-icon
|
|
[{:keys [shape] :as props}]
|
|
(case (:type shape)
|
|
:frame i/artboard
|
|
:image i/image
|
|
:line i/line
|
|
:circle i/circle
|
|
:path i/curve
|
|
:rect i/box
|
|
:curve i/curve
|
|
:text i/text
|
|
:group (if (some? (:component-id shape))
|
|
i/component
|
|
(if (:masked-group? shape)
|
|
i/mask
|
|
i/folder))
|
|
nil))
|
|
|
|
;; --- Layer Name
|
|
|
|
(mf/defc layer-name
|
|
[{:keys [shape] :as props}]
|
|
(let [local (mf/use-state {})
|
|
edit-input-ref (mf/use-ref)
|
|
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! (dw/update-shape (:id shape) {:name name}))
|
|
(swap! local assoc :edition false)))
|
|
on-key-down (fn [event]
|
|
(when (kbd/enter? event)
|
|
(on-blur event)))
|
|
on-click (fn [event]
|
|
(dom/prevent-default event)
|
|
(let [parent (.-parentNode (.-target event))
|
|
parent (.-parentNode parent)]
|
|
(set! (.-draggable parent) false))
|
|
(swap! local assoc :edition true))]
|
|
|
|
(mf/use-effect
|
|
(mf/deps (:edition @local))
|
|
#(when (:edition @local)
|
|
(let [edit-input (mf/ref-val edit-input-ref)]
|
|
(dom/select-text! edit-input))
|
|
nil))
|
|
|
|
(if (:edition @local)
|
|
[:input.element-name
|
|
{:type "text"
|
|
:ref edit-input-ref
|
|
:on-blur on-blur
|
|
:on-key-down on-key-down
|
|
:auto-focus true
|
|
:default-value (:name shape "")}]
|
|
[:span.element-name
|
|
{:on-double-click on-click}
|
|
(:name shape "")
|
|
(when (seq (:touched shape)) " *")])))
|
|
|
|
(defn- make-collapsed-iref
|
|
[id]
|
|
#(-> (l/in [:expanded id])
|
|
(l/derived refs/workspace-local)))
|
|
|
|
(mf/defc layer-item
|
|
[{:keys [index item selected objects] :as props}]
|
|
(let [id (:id item)
|
|
selected? (contains? selected id)
|
|
container? (or (= (:type item) :frame) (= (:type item) :group))
|
|
|
|
expanded-iref (mf/use-memo
|
|
(mf/deps id)
|
|
(make-collapsed-iref id))
|
|
|
|
expanded? (mf/deref expanded-iref)
|
|
|
|
toggle-collapse
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(if (and expanded? (kbd/shift? event))
|
|
(st/emit! dw/collapse-all)
|
|
(st/emit! (dw/toggle-collapse id))))
|
|
|
|
toggle-blocking
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(if (:blocked item)
|
|
(st/emit! (dw/update-shape-flags id {:blocked false}))
|
|
(st/emit! (dw/update-shape-flags id {:blocked true}))))
|
|
|
|
toggle-visibility
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(if (:hidden item)
|
|
(st/emit! (dw/update-shape-flags id {:hidden false}))
|
|
(st/emit! (dw/update-shape-flags id {:hidden true}))))
|
|
|
|
select-shape
|
|
(fn [event]
|
|
(dom/prevent-default event)
|
|
(let [id (:id item)]
|
|
(cond
|
|
(or (:blocked item)
|
|
(:hidden item))
|
|
nil
|
|
|
|
(.-shiftKey event)
|
|
(st/emit! (dw/select-shape id true))
|
|
|
|
(> (count selected) 1)
|
|
(st/emit! (dw/deselect-all)
|
|
(dw/select-shape id))
|
|
:else
|
|
(st/emit! (dw/deselect-all)
|
|
(dw/select-shape id)))))
|
|
|
|
on-context-menu
|
|
(fn [event]
|
|
(dom/prevent-default event)
|
|
(dom/stop-propagation event)
|
|
(let [pos (dom/get-client-position event)]
|
|
(st/emit! (dw/show-shape-context-menu {:position pos
|
|
:shape item}))))
|
|
|
|
on-drag
|
|
(fn [{:keys [id]}]
|
|
(when (not (contains? selected id))
|
|
(st/emit! (dw/deselect-all)
|
|
(dw/select-shape id))))
|
|
|
|
on-drop
|
|
(fn [side {:keys [id] :as data}]
|
|
(if (= side :center)
|
|
(st/emit! (dw/relocate-selected-shapes (:id item) 0))
|
|
(let [to-index (if (= side :top) (inc index) index)
|
|
parent-id (cph/get-parent (:id item) objects)]
|
|
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))
|
|
|
|
on-hold
|
|
(fn []
|
|
(when-not expanded?
|
|
(st/emit! (dw/toggle-collapse (:id item)))))
|
|
|
|
[dprops dref] (hooks/use-sortable
|
|
:data-type "app/layer"
|
|
:on-drop on-drop
|
|
:on-drag on-drag
|
|
:on-hold on-hold
|
|
:detect-center? container?
|
|
:data {:id (:id item)
|
|
:index index
|
|
:name (:name item)})]
|
|
|
|
(mf/use-effect
|
|
(mf/deps selected)
|
|
(fn []
|
|
(when (and (= (count selected) 1) selected?)
|
|
(.scrollIntoView (mf/ref-val dref) #js {:block "nearest", :behavior "smooth"}))))
|
|
|
|
[:li {:on-context-menu on-context-menu
|
|
:ref dref
|
|
:class (dom/classnames
|
|
:component (not (nil? (:component-id item)))
|
|
:masked (:masked-group? item)
|
|
:dnd-over (= (:over dprops) :center)
|
|
:dnd-over-top (= (:over dprops) :top)
|
|
:dnd-over-bot (= (:over dprops) :bot)
|
|
:selected selected?)}
|
|
|
|
[:div.element-list-body {:class (dom/classnames :selected selected?
|
|
:icon-layer (= (:type item) :icon))
|
|
:on-click select-shape
|
|
:on-double-click #(dom/stop-propagation %)}
|
|
[:& element-icon {:shape item}]
|
|
[:& layer-name {:shape item}]
|
|
|
|
[:div.element-actions
|
|
[:div.toggle-element {:class (when (:hidden item) "selected")
|
|
:on-click toggle-visibility}
|
|
(if (:hidden item) i/eye-closed i/eye)]
|
|
[:div.block-element {:class (when (:blocked item) "selected")
|
|
:on-click toggle-blocking}
|
|
(if (:blocked item) i/lock i/lock-open)]]
|
|
|
|
(when (:shapes item)
|
|
[:span.toggle-content
|
|
{:on-click toggle-collapse
|
|
:class (when expanded? "inverse")}
|
|
i/arrow-slide])]
|
|
(when (and (:shapes item) expanded?)
|
|
[:ul.element-children
|
|
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
|
(when-let [item (get objects id)]
|
|
[:& layer-item
|
|
{:item item
|
|
:selected selected
|
|
:index index
|
|
:objects objects
|
|
:key (:id item)}]))])]))
|
|
|
|
(defn frame-wrapper-memo-equals?
|
|
[oprops nprops]
|
|
(let [new-sel (unchecked-get nprops "selected")
|
|
old-sel (unchecked-get oprops "selected")
|
|
new-itm (unchecked-get nprops "item")
|
|
old-itm (unchecked-get oprops "item")
|
|
new-idx (unchecked-get nprops "index")
|
|
old-idx (unchecked-get oprops "index")
|
|
new-obs (unchecked-get nprops "objects")
|
|
old-obs (unchecked-get oprops "objects")]
|
|
(and (= new-itm old-itm)
|
|
(identical? new-idx old-idx)
|
|
(let [childs (cph/get-children (:id new-itm) new-obs)
|
|
childs' (conj childs (:id new-itm))]
|
|
(and (or (= new-sel old-sel)
|
|
(not (or (boolean (some new-sel childs'))
|
|
(boolean (some old-sel childs')))))
|
|
(loop [ids (rest childs)
|
|
id (first childs)]
|
|
(if (nil? id)
|
|
true
|
|
(if (= (get new-obs id)
|
|
(get old-obs id))
|
|
(recur (rest ids)
|
|
(first ids))
|
|
false))))))))
|
|
|
|
;; This components is a piece for sharding equality check between top
|
|
;; level frames and try to avoid rerender frames that are does not
|
|
;; affected by the selected set.
|
|
|
|
(mf/defc frame-wrapper
|
|
{::mf/wrap-props false
|
|
::mf/wrap [#(mf/memo' % frame-wrapper-memo-equals?)
|
|
#(mf/deferred % ts/idle-then-raf)]}
|
|
[props]
|
|
[:> layer-item props])
|
|
|
|
(mf/defc layers-tree
|
|
{::mf/wrap [#(mf/memo % =)]}
|
|
[{:keys [objects] :as props}]
|
|
(let [selected (mf/deref refs/selected-shapes)
|
|
root (get objects uuid/zero)]
|
|
[:ul.element-list
|
|
[:& hooks/sortable-container {}
|
|
(for [[index id] (reverse (d/enumerate (:shapes root)))]
|
|
(let [obj (get objects id)]
|
|
(if (= (:type obj) :frame)
|
|
[:& frame-wrapper
|
|
{:item obj
|
|
:selected selected
|
|
:index index
|
|
:objects objects
|
|
:key id}]
|
|
[:& layer-item
|
|
{:item obj
|
|
:selected selected
|
|
:index index
|
|
:objects objects
|
|
:key id}])))]]))
|
|
|
|
(defn- strip-objects
|
|
[objects]
|
|
(let [strip-data #(select-keys % [:id
|
|
:name
|
|
:blocked
|
|
:hidden
|
|
:shapes
|
|
:type
|
|
:content
|
|
:parent-id
|
|
:component-id
|
|
:component-file
|
|
:shape-ref
|
|
:touched
|
|
:metadata
|
|
:masked-group?])]
|
|
(persistent!
|
|
(reduce-kv (fn [res id obj]
|
|
(assoc! res id (strip-data obj)))
|
|
(transient {})
|
|
objects))))
|
|
|
|
(mf/defc layers-tree-wrapper
|
|
{::mf/wrap-props false
|
|
::mf/wrap [mf/memo #(mf/throttle % 200)]}
|
|
[props]
|
|
(let [objects (obj/get props "objects")
|
|
objects (strip-objects objects)]
|
|
[:& layers-tree {:objects objects}]))
|
|
|
|
;; --- Layers Toolbox
|
|
|
|
(mf/defc layers-toolbox
|
|
{:wrap [mf/memo]}
|
|
[]
|
|
(let [page (mf/deref refs/workspace-page)]
|
|
[:div#layers.tool-window
|
|
[:div.tool-window-bar
|
|
[:div.tool-window-icon i/layers]
|
|
[:span (:name page)]]
|
|
[:div.tool-window-content
|
|
[:& layers-tree-wrapper {:key (:id page)
|
|
:objects (:objects page)}]]]))
|