mirror of
https://github.com/penpot/penpot.git
synced 2025-05-25 19:16:10 +02:00
🚧 Add ad-hoc d&d implementation.
React-Dnd is a very nice library but adds a lot of overhead. Causes a lot of latency when a number of elements grows.
This commit is contained in:
parent
274a85186e
commit
7db2db96e1
10 changed files with 272 additions and 70 deletions
|
@ -1018,3 +1018,21 @@ input[type=range]:focus::-ms-fill-upper {
|
|||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
[draggable] {
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* Required to make elements draggable in old WebKit */
|
||||
-khtml-user-drag: element;
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
.dnd-over-top {
|
||||
border-top: 1px solid white !important;
|
||||
}
|
||||
|
||||
.dnd-over-bot {
|
||||
border-bottom: 1px solid white !important;
|
||||
}
|
||||
|
|
|
@ -136,10 +136,9 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&.dragging-TODO {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
&.open {
|
||||
|
||||
|
|
|
@ -1407,20 +1407,27 @@
|
|||
|
||||
;; --- Change Shape Order (D&D Ordering)
|
||||
|
||||
(defn shape-order-change
|
||||
[id index]
|
||||
;; TODO: pending UNDO
|
||||
|
||||
(defn relocate-shape
|
||||
[id ref-id index]
|
||||
(us/verify ::us/uuid id)
|
||||
(us/verify ::us/uuid ref-id)
|
||||
(us/verify number? index)
|
||||
(ptk/reify ::change-shape-order
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
||||
(ptk/reify ::reloacate-shape
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (::page-id state)
|
||||
obj (get-in state [:workspace-data page-id :objects id])
|
||||
frm (get-in state [:workspace-data page-id :objects (:frame-id obj)])
|
||||
shp (remove #(= % id) (:shapes frm))
|
||||
[b a] (split-at index shp)
|
||||
shp (d/concat [] b [id] a)]
|
||||
(assoc-in state [:workspace-data page-id :objects (:id frm) :shapes] shp)))))
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
parent-id (helpers/get-parent ref-id objects)]
|
||||
(rx/of (commit-changes [{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:index index
|
||||
:shapes (vec selected)}]
|
||||
[]
|
||||
{:commit-local? true}))))))
|
||||
|
||||
(defn commit-shape-order-change
|
||||
[id]
|
||||
|
@ -2359,7 +2366,8 @@
|
|||
(fn [state] (assoc-in state [:workspace-local :selected] #{id})))))
|
||||
rx/empty))))))
|
||||
|
||||
(defn remove-group []
|
||||
(defn remove-group
|
||||
[]
|
||||
(ptk/reify ::remove-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.util.transit :as t]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.webapi :as wapi]
|
||||
["mousetrap" :as mousetrap])
|
||||
|
@ -65,3 +66,139 @@
|
|||
|
||||
[toggle @state]))
|
||||
|
||||
;; (defn- extract-type
|
||||
;; [dt]
|
||||
;; (let [types (unchecked-get dt "types")
|
||||
;; total (alength types)]
|
||||
;; (loop [i 0]
|
||||
;; (if (= i total)
|
||||
;; nil
|
||||
;; (if-let [match (re-find #"dnd/(.+)" (aget types i))]
|
||||
;; (second match)
|
||||
;; (recur (inc i)))))))
|
||||
|
||||
|
||||
(defn invisible-image
|
||||
[]
|
||||
(let [img (js/Image.)
|
||||
imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="]
|
||||
(set! (.-src img) imd)
|
||||
img))
|
||||
|
||||
(defn use-sortable
|
||||
[& {:keys [type data on-drop on-drag] :as opts}]
|
||||
(let [ref (mf/use-ref)
|
||||
state (mf/use-state {})
|
||||
|
||||
on-drag-start
|
||||
(fn [event]
|
||||
;; (dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [dtrans (unchecked-get event "dataTransfer")]
|
||||
(.setDragImage dtrans (invisible-image) 0 0)
|
||||
(set! (.-effectAllowed dtrans) "move")
|
||||
(.setData dtrans "application/json" (t/encode data))
|
||||
;; (.setData dtrans (str "dnd/" type) "")
|
||||
(when (fn? on-drag)
|
||||
(on-drag data))
|
||||
(swap! state (fn [state]
|
||||
(if (:dragging? state)
|
||||
state
|
||||
(assoc state :dragging? true))))))
|
||||
|
||||
on-drag-over
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
|
||||
(let [target (dom/get-target event)
|
||||
dtrans (unchecked-get event "dataTransfer")
|
||||
ypos (unchecked-get event "offsetY")
|
||||
height (unchecked-get target "clientHeight")
|
||||
thold (/ height 2)
|
||||
side (if (> ypos thold) :bot :top)]
|
||||
|
||||
(set! (.-dropEffect dtrans) "move")
|
||||
(set! (.-effectAllowed dtrans) "move")
|
||||
|
||||
(swap! state update :over (fn [state]
|
||||
(if (not= state side)
|
||||
side
|
||||
state)))))
|
||||
|
||||
;; on-drag-enter
|
||||
;; (fn [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (dom/stop-propagation event)
|
||||
;; (let [dtrans (unchecked-get event "dataTransfer")
|
||||
;; ty (extract-type dt)]
|
||||
;; (when (= ty type)
|
||||
;; #_(js/console.log "on-drag-enter" (:name data) ty type)
|
||||
;; #_(swap! state (fn [state]
|
||||
;; (if (:over? state)
|
||||
;; state
|
||||
;; (assoc state :over? true)))))))
|
||||
|
||||
on-drag-leave
|
||||
(fn [event]
|
||||
(let [target (.-currentTarget event)
|
||||
related (.-relatedTarget event)]
|
||||
(when-not (.contains target related)
|
||||
;; (js/console.log "on-drag-leave" (:name data))
|
||||
(swap! state (fn [state]
|
||||
(if (:over state)
|
||||
(dissoc state :over)
|
||||
state))))))
|
||||
|
||||
on-drop'
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [target (dom/get-target event)
|
||||
dtrans (unchecked-get event "dataTransfer")
|
||||
dtdata (.getData dtrans "application/json")
|
||||
|
||||
ypos (unchecked-get event "offsetY")
|
||||
height (unchecked-get target "clientHeight")
|
||||
thold (/ height 2)
|
||||
side (if (> ypos thold) :bot :top)]
|
||||
|
||||
;; TODO: seems unnecessary
|
||||
(swap! state (fn [state]
|
||||
(cond-> state
|
||||
(:dragging? state) (dissoc :dragging?)
|
||||
(:over state) (dissoc :over))))
|
||||
|
||||
(when (fn? on-drop)
|
||||
(on-drop side (t/decode dtdata)))))
|
||||
|
||||
on-drag-end
|
||||
(fn [event]
|
||||
(swap! state (fn [state]
|
||||
(cond-> state
|
||||
(:dragging? state) (dissoc :dragging?)
|
||||
(:over state) (dissoc :over)))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
(let [dom (mf/ref-val ref)]
|
||||
(.setAttribute dom "draggable" true)
|
||||
(.setAttribute dom "data-type" type)
|
||||
|
||||
(.addEventListener dom "dragstart" on-drag-start false)
|
||||
;; (.addEventListener dom "dragenter" on-drag-enter false)
|
||||
(.addEventListener dom "dragover" on-drag-over false)
|
||||
(.addEventListener dom "dragleave" on-drag-leave true)
|
||||
(.addEventListener dom "drop" on-drop' false)
|
||||
(.addEventListener dom "dragend" on-drag-end false)
|
||||
#(do
|
||||
(.removeEventListener dom "dragstart" on-drag-start)
|
||||
;; (.removeEventListener dom "dragenter" on-drag-enter)
|
||||
(.removeEventListener dom "dragover" on-drag-over)
|
||||
(.removeEventListener dom "dragleave" on-drag-leave)
|
||||
(.removeEventListener dom "drop" on-drop')
|
||||
(.removeEventListener dom "dragend" on-drag-end))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps type data on-drop)
|
||||
on-mount)
|
||||
[(deref state) ref]))
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
page (mf/deref refs/workspace-page)
|
||||
project (mf/deref refs/workspace-project)
|
||||
layout (mf/deref refs/workspace-layout)]
|
||||
[:> rdnd/provider {:backend rdnd/html5}
|
||||
[:*
|
||||
[:& header {:page page
|
||||
:file file
|
||||
:project project
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.hooks :as hooks]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
[uxbox.main.ui.shapes.icon :as icon]
|
||||
[uxbox.main.ui.workspace.sortable :refer [use-sortable]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.perf :as perf]
|
||||
[uxbox.util.uuid :as uuid]
|
||||
[uxbox.util.i18n :as i18n :refer [t]]))
|
||||
|
||||
|
@ -78,21 +80,33 @@
|
|||
{:on-double-click on-click}
|
||||
(:name shape "")])))
|
||||
|
||||
(defn- layer-item-memo-equals?
|
||||
[nprops oprops]
|
||||
(let [n-item (unchecked-get nprops "item")
|
||||
o-item (unchecked-get oprops "item")
|
||||
n-selc (unchecked-get nprops "selected")
|
||||
o-selc (unchecked-get oprops "selected")
|
||||
n-indx (unchecked-get nprops "index")
|
||||
o-indx (unchecked-get oprops "index")]
|
||||
;; (js/console.log "FOR" (:name n-item)
|
||||
;; "NEW SEL" n-selc
|
||||
;; "OLD SEL" o-selc)6
|
||||
(and (identical? n-item o-item)
|
||||
(identical? n-indx o-indx)
|
||||
(identical? n-selc o-selc))))
|
||||
|
||||
(def strip-attrs
|
||||
#(select-keys % [:id :frame :name :type :hidden :blocked]))
|
||||
(declare layer-item)
|
||||
|
||||
(mf/defc layer-item
|
||||
{:wrap [mf/memo]}
|
||||
{::mf/wrap [#(mf/memo' % layer-item-memo-equals?)]}
|
||||
[{:keys [index item selected objects] :as props}]
|
||||
(let [selected? (contains? selected (:id item))
|
||||
local (mf/use-state {:collapsed false})
|
||||
collapsed? (:collapsed @local)
|
||||
collapsed? (mf/use-state false)
|
||||
|
||||
toggle-collapse
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! local update :collapsed not))
|
||||
(swap! collapsed? not))
|
||||
|
||||
toggle-blocking
|
||||
(fn [event]
|
||||
|
@ -135,26 +149,35 @@
|
|||
(st/emit! (dw/show-shape-context-menu {:position pos
|
||||
:shape item}))))
|
||||
|
||||
on-hover
|
||||
(fn [item monitor]
|
||||
(st/emit! (dw/shape-order-change (:obj-id item) index)))
|
||||
on-drag
|
||||
(fn [{:keys [id]}]
|
||||
(when (not (contains? selected id))
|
||||
(st/emit! dw/deselect-all
|
||||
(dw/select-shape id))))
|
||||
|
||||
on-drop
|
||||
(fn [item monitor]
|
||||
(st/emit! (dw/commit-shape-order-change (:obj-id item))))
|
||||
(fn [side {:keys [id name] :as data}]
|
||||
(let [index (if (= :top side) (inc index) index)]
|
||||
;; (println "droping" name "on" side "of" (:name item) "/" index)
|
||||
(st/emit! (dw/relocate-shape id (:id item) index))))
|
||||
|
||||
[dprops dnd-ref] (use-sortable
|
||||
{:type (str "layer-item" (:frame-id item))
|
||||
:data {:obj-id (:id item)
|
||||
:page-id (:page item)
|
||||
:index index}
|
||||
:on-hover on-hover
|
||||
:on-drop on-drop})]
|
||||
[:li {:ref dnd-ref
|
||||
:on-context-menu on-context-menu
|
||||
[dprops dref] (hooks/use-sortable
|
||||
:type (str (:frame-id item))
|
||||
:on-drop on-drop
|
||||
:on-drag on-drag
|
||||
:data {:id (:id item)
|
||||
:index index
|
||||
:name (:name item)})
|
||||
]
|
||||
;; (prn "layer-item" (:name item) index)
|
||||
[:li {:on-context-menu on-context-menu
|
||||
:ref dref
|
||||
:data-index index
|
||||
:class (dom/classnames
|
||||
:dnd-over-top (= (:over dprops) :top)
|
||||
:dnd-over-bot (= (:over dprops) :bot)
|
||||
:selected selected?
|
||||
:dragging-TODO (:dragging? dprops))}
|
||||
)}
|
||||
[:div.element-list-body {:class (dom/classnames :selected selected?
|
||||
:icon-layer (= (:type item) :icon))
|
||||
:on-click select-shape
|
||||
|
@ -173,13 +196,13 @@
|
|||
(when (:shapes item)
|
||||
[:span.toggle-content
|
||||
{:on-click toggle-collapse
|
||||
:class (when-not collapsed? "inverse")}
|
||||
:class (when-not @collapsed? "inverse")}
|
||||
i/arrow-slide])]
|
||||
(when (and (:shapes item) (not collapsed?))
|
||||
(when (and (:shapes item) (not @collapsed?))
|
||||
[:ul.element-children
|
||||
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
[:& uxbox.main.ui.workspace.sidebar.layers/layer-item
|
||||
{:item item
|
||||
:selected selected
|
||||
:index index
|
||||
|
@ -193,15 +216,16 @@
|
|||
data (mf/deref refs/workspace-data)
|
||||
objects (:objects data)
|
||||
root (get objects uuid/zero)]
|
||||
|
||||
;; [:& perf/profiler {:label "layers-tree" :enabled false}
|
||||
[:ul.element-list
|
||||
(for [[index id] (reverse (d/enumerate (:shapes root)))]
|
||||
(let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
{:item (get objects id)
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (:id item)}]))]))
|
||||
:key id}])]))
|
||||
|
||||
;; --- Layers Toolbox
|
||||
|
||||
|
|
|
@ -60,15 +60,17 @@
|
|||
:index index}))
|
||||
|
||||
navigate-fn #(st/emit! (dw/go-to-page (:id page)))
|
||||
[dprops ref] (use-sortable {:type "page-item"
|
||||
:data {:id (:id page)
|
||||
:index index}
|
||||
:on-hover on-hover
|
||||
:on-drop on-drop})]
|
||||
[:li {:ref ref :class (classnames :selected selected?)}
|
||||
;; [dprops ref] (use-sortable {:type "page-item"
|
||||
;; :data {:id (:id page)
|
||||
;; :index index}
|
||||
;; :on-hover on-hover
|
||||
;; :on-drop on-drop})
|
||||
]
|
||||
[:li {:class (classnames :selected selected?)}
|
||||
[:div.element-list-body {:class (classnames
|
||||
:selected selected?
|
||||
:dragging (:dragging? dprops))
|
||||
;; :dragging (:dragging? dprops)
|
||||
)
|
||||
:on-click navigate-fn
|
||||
:on-double-click on-double-click}
|
||||
[:div.page-icon i/file-html]
|
||||
|
|
|
@ -21,18 +21,35 @@
|
|||
on-drop (constantly nil)}
|
||||
:as options}]
|
||||
(let [ref (mf/use-ref nil)
|
||||
[_, drop] (rdnd/useDrop
|
||||
#js {:accept type
|
||||
:hover (fn [item monitor]
|
||||
|
||||
on-hover
|
||||
(fn [item monitor]
|
||||
(when (mf/ref-val ref)
|
||||
(on-hover (unchecked-get item "data") monitor)))
|
||||
:drop (fn [item monitor]
|
||||
|
||||
on-drop
|
||||
(fn [item monitor]
|
||||
(when (mf/ref-val ref)
|
||||
(on-drop (unchecked-get item "data") monitor)))})
|
||||
[props, drag] (rdnd/useDrag
|
||||
(on-drop (unchecked-get item "data") monitor)))
|
||||
|
||||
on-drop-collect
|
||||
(fn [monitor]
|
||||
#js {:is-over (.isOver ^js monitor)
|
||||
:can-drop (.canDrop ^js monitor)})
|
||||
|
||||
on-drag-collect
|
||||
(fn [monitor]
|
||||
#js {:dragging? (.isDragging monitor)})
|
||||
|
||||
[props1, drop] (rdnd/useDrop
|
||||
#js {:accept type
|
||||
:collect on-drop-collect
|
||||
:hover on-hover
|
||||
:drop on-drop})
|
||||
[props2, drag] (rdnd/useDrag
|
||||
#js {:item #js {:type type :data data}
|
||||
:collect (fn [^js/ReactDnd.Monitor monitor]
|
||||
#js {:dragging? (.isDragging monitor)})})]
|
||||
:collect on-drag-collect})
|
||||
props (js/Object.assign props1 props2)]
|
||||
[(mfu/obj->map props)
|
||||
(drag (drop ref))]))
|
||||
|
||||
|
|
|
@ -295,10 +295,7 @@
|
|||
:on-drag-over on-drag-over
|
||||
:on-drop on-drop}
|
||||
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
|
||||
;; [:> js/React.Profiler
|
||||
;; {:id "foobar"
|
||||
;; :on-render (perf/react-on-profile)}
|
||||
;; [:& frame-and-shapes]]
|
||||
;; [:& perf/profiler {:label "viewport-frames"}
|
||||
[:& frames-wrapper {:page page}]
|
||||
|
||||
(when (seq selected)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue