🚧 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:
Andrey Antukh 2020-04-10 14:30:06 +02:00 committed by Alonso Torres
parent 274a85186e
commit 7db2db96e1
10 changed files with 272 additions and 70 deletions

View file

@ -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;
}

View file

@ -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 {

View file

@ -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]

View file

@ -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]))

View file

@ -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

View file

@ -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
:selected selected
:index index
:objects objects
:key (:id item)}]))]))
[:& layer-item
{:item (get objects id)
:selected selected
:index index
:objects objects
:key id}])]))
;; --- Layers Toolbox

View file

@ -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]

View file

@ -21,18 +21,35 @@
on-drop (constantly nil)}
:as options}]
(let [ref (mf/use-ref nil)
[_, drop] (rdnd/useDrop
on-hover
(fn [item monitor]
(when (mf/ref-val ref)
(on-hover (unchecked-get item "data") monitor)))
on-drop
(fn [item monitor]
(when (mf/ref-val ref)
(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
:hover (fn [item monitor]
(when (mf/ref-val ref)
(on-hover (unchecked-get item "data") monitor)))
:drop (fn [item monitor]
(when (mf/ref-val ref)
(on-drop (unchecked-get item "data") monitor)))})
[props, drag] (rdnd/useDrag
: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))]))

View file

@ -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)