diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index e443bc3ac..762012e46 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -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; +} diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 90d0949b1..af647e801 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -51,7 +51,7 @@ grid-template-rows: 12rem 25% 1fr; } - + flex-direction: column; overflow: hidden; padding-top: 40px; @@ -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 { diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 9e579d65b..a82185ff1 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -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] diff --git a/frontend/src/uxbox/main/ui/hooks.cljs b/frontend/src/uxbox/main/ui/hooks.cljs index 128ee8a06..ec0927f57 100644 --- a/frontend/src/uxbox/main/ui/hooks.cljs +++ b/frontend/src/uxbox/main/ui/hooks.cljs @@ -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])) diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index 948ab15ab..e4e8b2dec 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -102,7 +102,7 @@ [:& frame-shape {:shape (geom/transform-shape shape) :childs childs}]]))))) -(defn frame-shape +(defn frame-shape [shape-wrapper] (mf/fnc frame-shape [{:keys [shape childs] :as props}] diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 5cedc8073..00fc3fa37 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -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 diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index bb46a548f..365d30d7f 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -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 diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index 208356d67..e079158fe 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -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] diff --git a/frontend/src/uxbox/main/ui/workspace/sortable.cljs b/frontend/src/uxbox/main/ui/workspace/sortable.cljs index d6d2b3ab8..66e1fe25e 100644 --- a/frontend/src/uxbox/main/ui/workspace/sortable.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sortable.cljs @@ -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))])) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 0a4d01be0..e7a58bf89 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -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)