From 25518a4ac011aef4e8f09da6078e2fb5dfcb7ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 21 May 2020 10:56:42 +0200 Subject: [PATCH 1/3] :bug: Implement undo in layer move operation --- common/uxbox/common/pages.cljc | 11 +++++++++-- frontend/src/uxbox/main/data/workspace.cljs | 16 ++++++++++------ .../uxbox/main/ui/workspace/sidebar/layers.cljs | 4 ++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index 673c8d6be..f4c04d406 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -72,6 +72,12 @@ (into result (calculate-invalid-targets child-id objects)))] (reduce reduce-fn result children))) +(defn- valid-frame-target + [shape-id parent-id objects] + (let [shape (get objects shape-id)] + (or (not= (:type shape) :frame) + (= parent-id uuid/zero)))) + (defn- insert-at-index [shapes index ids] (let [[before after] (split-at index shapes) @@ -389,8 +395,9 @@ ;; Check if the move from shape-id -> parent-id is valid is-valid-move (fn [shape-id] - (let [invalid (calculate-invalid-targets shape-id (:objects data))] - (not (invalid parent-id)))) + (let [invalid-targets (calculate-invalid-targets shape-id (:objects data))] + (and (not (invalid-targets parent-id)) + (valid-frame-target shape-id parent-id (:objects data))))) valid? (every? is-valid-move shapes) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 32cf0b413..a72937ebe 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -916,13 +916,11 @@ ;; --- Change Shape Order (D&D Ordering) -;; TODO: pending UNDO - (defn relocate-shape - [id parent-id index] + [id parent-id to-index] (us/verify ::us/uuid id) (us/verify ::us/uuid parent-id) - (us/verify number? index) + (us/verify number? to-index) (ptk/reify ::relocate-shape dwc/IUpdateGroup @@ -931,12 +929,18 @@ ptk/WatchEvent (watch [_ state stream] (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data page-id :objects]) + parent (get objects (cp/get-parent id objects)) + current-index (d/index-of (:shapes parent) id) selected (get-in state [:workspace-local :selected])] (rx/of (dwc/commit-changes [{:type :mov-objects :parent-id parent-id - :index index + :index to-index + :shapes (vec selected)}] + [{:type :mov-objects + :parent-id (:id parent) + :index current-index :shapes (vec selected)}] - [] {:commit-local? true})))))) ;; --- Change Page Order (D&D Ordering) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index af72e287a..406c7f1c8 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -153,9 +153,9 @@ (fn [side {:keys [id] :as data}] (if (= side :center) (st/emit! (dw/relocate-shape id (:id item) 0)) - (let [index (if (= side :top) (inc index) index) + (let [to-index (if (= side :top) (inc index) index) parent-id (cp/get-parent (:id item) objects)] - (st/emit! (dw/relocate-shape id parent-id index))))) + (st/emit! (dw/relocate-shape id parent-id to-index))))) [dprops dref] (hooks/use-sortable :type (str (:frame-id item)) From da77aa558e4ea7d0172df0e57280be430a0b4012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 25 May 2020 10:01:57 +0200 Subject: [PATCH 2/3] :sparkles: Auto open layer when hovering over it for a moment --- frontend/src/uxbox/main/ui/hooks.cljs | 133 ++++++++++-------- .../main/ui/workspace/sidebar/layers.cljs | 6 + 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/frontend/src/uxbox/main/ui/hooks.cljs b/frontend/src/uxbox/main/ui/hooks.cljs index 682964183..03841defe 100644 --- a/frontend/src/uxbox/main/ui/hooks.cljs +++ b/frontend/src/uxbox/main/ui/hooks.cljs @@ -18,6 +18,7 @@ [uxbox.util.transit :as t] [uxbox.util.dom :as dom] [uxbox.util.webapi :as wapi] + [uxbox.util.timers :as ts] ["mousetrap" :as mousetrap]) (:import goog.events.EventType)) @@ -31,7 +32,6 @@ #js [ob]) state)) - (s/def ::shortcuts (s/map-of ::us/string fn?)) @@ -66,18 +66,6 @@ [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.) @@ -97,73 +85,98 @@ :else :center) (if (> ypos thold) :bot :top)))) +(defn- set-timer + [state ms func] + (assoc state :timer (ts/schedule ms func))) + +(defn- cancel-timer + [state] + (let [timer (:timer state)] + (if timer + (do + (rx/dispose! timer) + (dissoc state :timer)) + state))) + +;; The dnd interface is broken in several ways. This is the official documentation +;; https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API +;; +;; And there is some discussion of the problems and many uncomplete solutions +;; https://github.com/lolmaus/jquery.dragbetter/#what-this-is-all-about +;; https://www.w3schools.com/jsref/event_relatedtarget.asp +;; https://stackoverflow.com/questions/14194324/firefox-firing-dragleave-when-dragging-over-text?noredirect=1&lq=1 +;; https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element + +;; This function is useful to debug the erratic dnd interface behaviour when something weird occurs +;; (defn- trace +;; [event data label] +;; (js/console.log +;; label +;; "[" (:name data) "]" +;; (if (.-currentTarget event) (.-textContent (.-currentTarget event)) "null") +;; (if (.-relatedTarget event) (.-textContent (.-relatedTarget event)) "null"))) + (defn use-sortable - [& {:keys [type data on-drop on-drag detect-center?] :as opts}] + [& {:keys [type data on-drop on-drag on-hold detect-center?] :as opts}] (let [ref (mf/use-ref) - state (mf/use-state {}) + state (mf/use-state {:over nil + :timer nil}) on-drag-start (fn [event] - ;; (dom/prevent-default event) (dom/stop-propagation event) + ;; (trace event data "drag-start") (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 data)))) + + on-drag-enter + (fn [event] + (dom/prevent-default event) ;; prevent default to allow drag enter + (let [target (.-currentTarget event) + related (.-relatedTarget event)] + (when-not (.contains target related) ;; ignore events triggered by elements that are + (dom/stop-propagation event) ;; children of the drop target + ;; (trace event data "drag-enter") + (when (fn? on-hold) + (swap! state (fn [state] + (-> state + (cancel-timer) + (set-timer 1000 on-hold)))))))) on-drag-over (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - + (dom/prevent-default event) ;; prevent default to allow drag over (let [target (dom/get-target event) + related (.-relatedTarget event) dtrans (unchecked-get event "dataTransfer") ypos (unchecked-get event "offsetY") height (unchecked-get target "clientHeight") side (drop-side height ypos detect-center?)] - - (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))))))) + (when-not (.contains target related) + (dom/stop-propagation event) + ;; (trace event data "drag-over") + (swap! state assoc :over side)))) 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)) + (dom/stop-propagation event) + ;; (trace event data "drag-leave") (swap! state (fn [state] - (if (:over state) - (dissoc state :over) - state)))))) + (-> state + (cancel-timer) + (dissoc :over))))))) on-drop' (fn [event] (dom/stop-propagation event) + ;; (trace event data "drop") (let [target (dom/get-target event) dtrans (unchecked-get event "dataTransfer") dtdata (.getData dtrans "application/json") @@ -172,21 +185,21 @@ height (unchecked-get target "clientHeight") side (drop-side height ypos detect-center?)] - ;; TODO: seems unnecessary (swap! state (fn [state] - (cond-> state - (:dragging? state) (dissoc :dragging?) - (:over state) (dissoc :over)))) + (-> state + (cancel-timer) + (dissoc :over)))) (when (fn? on-drop) (on-drop side (t/decode dtdata))))) on-drag-end (fn [event] + ;; (trace event data "drag-end") (swap! state (fn [state] - (cond-> state - (:dragging? state) (dissoc :dragging?) - (:over state) (dissoc :over))))) + (-> state + (cancel-timer) + (dissoc :over))))) on-mount (fn [] @@ -195,14 +208,14 @@ (.setAttribute dom "data-type" type) (.addEventListener dom "dragstart" on-drag-start false) - ;; (.addEventListener dom "dragenter" on-drag-enter 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 "dragenter" on-drag-enter) (.removeEventListener dom "dragover" on-drag-over) (.removeEventListener dom "dragleave" on-drag-leave) (.removeEventListener dom "drop" on-drop') @@ -211,4 +224,6 @@ (mf/use-effect (mf/deps type data on-drop) on-mount) + [(deref state) ref])) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index 406c7f1c8..1505e3c81 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -157,10 +157,16 @@ parent-id (cp/get-parent (:id item) objects)] (st/emit! (dw/relocate-shape id parent-id to-index))))) + on-hold + (fn [] + (when-not expanded? + (st/emit! (dw/toggle-collapse (:id item))))) + [dprops dref] (hooks/use-sortable :type (str (:frame-id item)) :on-drop on-drop :on-drag on-drag + :on-hold on-hold :detect-center? container? :data {:id (:id item) :index index From 47901870bd550c22d851165bc93458742d53dabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 25 May 2020 12:15:36 +0200 Subject: [PATCH 3/3] :sparkles: Allow dropping only objects of the same type --- frontend/src/uxbox/main/ui/hooks.cljs | 28 +++++++++---------- .../main/ui/workspace/sidebar/layers.cljs | 2 +- .../main/ui/workspace/sidebar/sitemap.cljs | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/uxbox/main/ui/hooks.cljs b/frontend/src/uxbox/main/ui/hooks.cljs index 03841defe..fdbcefa4b 100644 --- a/frontend/src/uxbox/main/ui/hooks.cljs +++ b/frontend/src/uxbox/main/ui/hooks.cljs @@ -117,7 +117,7 @@ ;; (if (.-relatedTarget event) (.-textContent (.-relatedTarget event)) "null"))) (defn use-sortable - [& {:keys [type data on-drop on-drag on-hold detect-center?] :as opts}] + [& {:keys [data-type data on-drop on-drag on-hold detect-center?] :as opts}] (let [ref (mf/use-ref) state (mf/use-state {:over nil :timer nil}) @@ -129,7 +129,7 @@ (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 data-type (t/encode data)) (when (fn? on-drag) (on-drag data)))) @@ -149,17 +149,18 @@ on-drag-over (fn [event] - (dom/prevent-default event) ;; prevent default to allow drag over - (let [target (dom/get-target event) + (let [dtrans (unchecked-get event "dataTransfer") + target (.-currentTarget event) related (.-relatedTarget event) - dtrans (unchecked-get event "dataTransfer") ypos (unchecked-get event "offsetY") height (unchecked-get target "clientHeight") side (drop-side height ypos detect-center?)] - (when-not (.contains target related) - (dom/stop-propagation event) - ;; (trace event data "drag-over") - (swap! state assoc :over side)))) + (when (.includes (.-types dtrans) data-type) + (dom/prevent-default event) ;; prevent default to allow drag over + (when-not (.contains target related) + (dom/stop-propagation event) + ;; (trace event data "drag-over") + (swap! state assoc :over side))))) on-drag-leave (fn [event] @@ -177,10 +178,10 @@ (fn [event] (dom/stop-propagation event) ;; (trace event data "drop") - (let [target (dom/get-target event) - dtrans (unchecked-get event "dataTransfer") - dtdata (.getData dtrans "application/json") + (let [dtrans (unchecked-get event "dataTransfer") + dtdata (.getData dtrans data-type) + target (.-currentTarget event) ypos (unchecked-get event "offsetY") height (unchecked-get target "clientHeight") side (drop-side height ypos detect-center?)] @@ -205,7 +206,6 @@ (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) @@ -222,7 +222,7 @@ (.removeEventListener dom "dragend" on-drag-end))))] (mf/use-effect - (mf/deps type data on-drop) + (mf/deps data on-drop) on-mount) [(deref state) ref])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index 1505e3c81..6b30180c8 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -163,7 +163,7 @@ (st/emit! (dw/toggle-collapse (:id item))))) [dprops dref] (hooks/use-sortable - :type (str (:frame-id item)) + :data-type "uxbox/layer" :on-drop on-drop :on-drag on-drag :on-hold on-hold diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index 3a15b36e0..d2bdaa40f 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -64,7 +64,7 @@ (st/emit! (dw/relocate-page id index)))) [dprops dref] (hooks/use-sortable - :type "page" + :data-type "uxbox/page" :on-drop on-drop :data {:id (:id page) :index index