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/hooks.cljs b/frontend/src/uxbox/main/ui/hooks.cljs index 682964183..fdbcefa4b 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,118 +85,145 @@ :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 [data-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) "") + (.setData dtrans data-type (t/encode data)) (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) - - (let [target (dom/get-target event) - dtrans (unchecked-get event "dataTransfer") + (let [dtrans (unchecked-get event "dataTransfer") + target (.-currentTarget event) + related (.-relatedTarget event) 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 (.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] (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) - (let [target (dom/get-target event) - dtrans (unchecked-get event "dataTransfer") - dtdata (.getData dtrans "application/json") + ;; (trace event data "drop") + (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?)] - ;; 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 [] (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 "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') (.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 af72e287a..6b30180c8 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -153,14 +153,20 @@ (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))))) + + on-hold + (fn [] + (when-not expanded? + (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 :detect-center? container? :data {:id (:id item) :index index 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