diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index 64e0e72c1..fa6063b8c 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -11,7 +11,7 @@ [cuerdas.core :as str] [potok.core :as ptk] [uxbox.main.repo :as rp] - [uxbox.util.data :refer [index-by-id]] + [uxbox.util.data :refer [index-by-id concatv]] [uxbox.util.spec :as us] [uxbox.util.timers :as ts] [uxbox.util.uuid :as uuid])) @@ -97,7 +97,8 @@ (letfn [(pack-shapes [ids] (mapv #(get-in state [:shapes %]) ids))] (let [page (get-in state [:pages id]) - data {:shapes (pack-shapes (:shapes page))}] + data {:shapes (pack-shapes (concatv (:canvas page) + (:shapes page)))}] (-> page (assoc :data data) (dissoc :shapes))))) @@ -106,13 +107,20 @@ "Unpacks packed page object and assocs it to the provided state." [state {:keys [id data] :as page}] - (let [shapes-data (:shapes data []) - shapes (mapv :id shapes-data) - shapes-map (index-by-id shapes-data) + (let [shapes-list (:shapes data []) + + shapes (->> shapes-list + (filter #(not= :canvas (:type %))) + (mapv :id)) + canvas (->> shapes-list + (filter #(= :canvas (:type %))) + (mapv :id)) + + shapes-map (index-by-id shapes-list) page (-> page (dissoc :data) - (assoc :shapes shapes))] + (assoc :shapes shapes :canvas canvas))] (-> state (update :shapes merge shapes-map) (update :pages assoc id page)))) diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs index 681ceef1e..9e3a89ed2 100644 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ b/frontend/src/uxbox/main/data/shapes.cljs @@ -113,9 +113,11 @@ :page page :id shape-id :name shape-name)] - (-> state - (update-in [:pages page :shapes] #(into [] (cons shape-id %))) - (assoc-in [:shapes shape-id] shape)))) + (as-> state $ + (if (= :canvas (:type shape)) + (update-in $ [:pages page :canvas] conj shape-id) + (update-in $ [:pages page :shapes] conj shape-id)) + (assoc-in $ [:shapes shape-id] shape)))) (defn duplicate-shapes' ([state shapes page] diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 306c454ca..93744025e 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -553,7 +553,7 @@ (update [_ state] (assoc-in state [:shapes id :name] name)))) -;; --- Change Shape Order (Ordering) +;; --- Change Shape Order (D&D Ordering) (defn change-shape-order [{:keys [id index] :as params}] @@ -568,6 +568,22 @@ shapes (vec (concat before [id] after))] (assoc-in state [:pages page-id :shapes] shapes))))) +;; --- Change Canvas Order (D&D Ordering) + +(defn change-canvas-order + [{:keys [id index] :as params}] + (s/assert ::us/uuid id) + (s/assert ::us/number index) + (reify + ptk/UpdateEvent + (update [_ state] + (let [page-id (get-in state [:shapes id :page]) + canvas (get-in state [:pages page-id :canvas]) + canvas (into [] (remove #(= % id)) canvas) + [before after] (split-at index canvas) + canvas (vec (concat before [id] after))] + (assoc-in state [:pages page-id :canvas] canvas))))) + ;; --- Shape Transformations (defn initial-shape-align @@ -619,6 +635,32 @@ #(update-in % [:shapes id] dissoc :modifier-mtx) ::udp/page-update)))))) +(defn rehash-shape-relationship + "Checks shape overlaping with existing canvas, if one or more + overlaps, assigns the shape to the first one." + [id] + (s/assert ::us/uuid id) + (letfn [(overlaps? [canvas shape] + (let [shape1 (geom/shape->rect-shape canvas) + shape2 (geom/shape->rect-shape shape)] + (geom/overlaps? shape1 shape2)))] + (reify + ptk/EventType + (type [_] ::rehash-shape-relationship) + + ptk/UpdateEvent + (update [_ state] + (let [shape (get-in state [:shapes id]) + xform (comp (map #(get-in state [:shapes %])) + (filter #(overlaps? % shape)) + (take 1)) + canvas (->> (get-in state [:pages (:page shape) :canvas]) + (sequence xform) + (first))] + (if canvas + (update-in state [:shapes id] assoc :canvas (:id canvas)) + (update-in state [:shapes id] assoc :canvas nil))))))) + ;; --- Start shape "edition mode" (defn start-edition-mode @@ -777,79 +819,49 @@ ;; --- Shape Visibility -(deftype HideShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-hidden [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :hidden] true) - (reduce mark-hidden $ (:items shape))) - (assoc-in state [:shapes id :hidden] true))))] - (mark-hidden state id)))) - -(defn hide-shape - [id] - {:pre [(uuid? id)]} - (HideShape. id)) - -(deftype ShowShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-visible [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :hidden] false) - (reduce mark-visible $ (:items shape))) - (assoc-in state [:shapes id :hidden] false))))] - (mark-visible state id)))) - -(defn show-shape - [id] - {:pre [(uuid? id)]} - (ShowShape. id)) +(defn set-hidden-attr + [id value] + (s/assert ::us/uuid id) + (s/assert ::us/boolean value) + (letfn [(impl-set-hidden [state id] + (let [{:keys [type] :as shape} (get-in state [:shapes id])] + (as-> state $ + (assoc-in $ [:shapes id :hidden] value) + (if (= :canvas type) + (let [shapes (get-in state [:pages (:page shape) :shapes]) + xform (comp (map #(get-in state [:shapes %])) + (filter #(= id (:canvas %))) + (map :id))] + (reduce impl-set-hidden $ (sequence xform shapes))) + $))))] + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (impl-set-hidden state id))))) ;; --- Shape Blocking -(deftype BlockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-blocked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :blocked] true) - (reduce mark-blocked $ (:items shape))) - (assoc-in state [:shapes id :blocked] true))))] - (mark-blocked state id)))) - -(defn block-shape - [id] - {:pre [(uuid? id)]} - (BlockShape. id)) - -(deftype UnblockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-unblocked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :blocked] false) - (reduce mark-unblocked $ (:items shape))) - (assoc-in state [:shapes id :blocked] false))))] - (mark-unblocked state id)))) - -(defn unblock-shape - [id] - {:pre [(uuid? id)]} - (UnblockShape. id)) +(defn set-blocked-attr + [id value] + (s/assert ::us/uuid id) + (s/assert ::us/boolean value) + (letfn [(impl-set-blocked [state id] + (let [{:keys [type] :as shape} (get-in state [:shapes id])] + (as-> state $ + (assoc-in $ [:shapes id :blocked] value) + (if (= :canvas type) + (let [shapes (get-in state [:pages (:page shape) :shapes]) + xform (comp (map #(get-in state [:shapes %])) + (filter #(= id (:canvas %))) + (map :id))] + (reduce impl-set-blocked $ (sequence xform shapes))) + $))))] + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (impl-set-blocked state id))))) ;; --- Shape Locking diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index a570faf25..8b202d0d9 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -56,8 +56,8 @@ [{:keys [status] :as error}] (js/console.error "Unhandled Error:" "\n - message:" (ex-message error) - "\n - data:" (pr-str (ex-data error)) - "\n - stack:" (.-stack error)) + "\n - data:" (pr-str (ex-data error))) + (js/console.error error) (reset! st/loader false) (cond ;; Unauthorized or Auth timeout diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index c30f5b095..aba6cfbd2 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -33,7 +33,8 @@ (->> uws/mouse-position-deltas (rx/map #(dw/apply-temporal-displacement id %)) (rx/take-until stoper)) - (rx/of (dw/materialize-current-modifier id))))))) + (rx/of (dw/materialize-current-modifier id) + (dw/rehash-shape-relationship id))))))) (def start-move-selected (reify diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index b630d1354..ba1762288 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -17,10 +17,14 @@ [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.shapes.icon :as icon] [uxbox.main.ui.workspace.sortable :refer [use-sortable]] - [uxbox.util.data :refer [classnames]] + [uxbox.util.data :refer [classnames enumerate]] [uxbox.util.dom :as dom] [uxbox.util.i18n :refer (tr)])) +(def ^:private shapes-iref + (-> (l/key :shapes) + (l/derive st/state))) + ;; --- Helpers (defn- element-icon @@ -76,21 +80,13 @@ [{:keys [shape selected index] :as props}] (letfn [(toggle-blocking [event] (dom/stop-propagation event) - (let [id (:id shape) - blocked? (:blocked shape)] - (if blocked? - (st/emit! (dw/unblock-shape id)) - (st/emit! (dw/block-shape id))))) + (let [{:keys [id blocked]} shape] + (st/emit! (dw/set-blocked-attr id (not blocked))))) (toggle-visibility [event] (dom/stop-propagation event) - (let [id (:id shape) - hidden? (:hidden shape)] - (if hidden? - (st/emit! (dw/show-shape id)) - (st/emit! (dw/hide-shape id))) - (when (contains? selected id) - (st/emit! (dw/select-shape id))))) + (let [{:keys [id hidden]} shape] + (st/emit! (dw/set-hidden-attr id (not hidden))))) (select-shape [event] (dom/prevent-default event) @@ -115,7 +111,7 @@ (on-hover [item monitor] (st/emit! (dw/change-shape-order {:id (:shape-id item) - :index index})))] + :index index})))] (let [selected? (contains? selected (:id shape)) [dprops dnd-ref] (use-sortable {:type "layer-item" @@ -141,65 +137,129 @@ [:div.element-icon (element-icon shape)] [:& layer-name {:shape shape}]]]))) +(mf/defc canvas-item + [{:keys [canvas shapes selected index] :as props}] + (letfn [(toggle-blocking [event] + (dom/stop-propagation event) + (let [{:keys [id blocked]} canvas] + (st/emit! (dw/set-blocked-attr id (not blocked))))) -;; --- Layer Canvas + (toggle-visibility [event] + (dom/stop-propagation event) + (let [{:keys [id hidden]} canvas] + (st/emit! (dw/set-hidden-attr id (not hidden))))) -;; (mf/defc layer-canvas -;; [{:keys [canvas selected index] :as props}] -;; (letfn [(select-shape [event] -;; (dom/prevent-default event) -;; (st/emit! (dw/select-canvas (:id canvas)))) -;; (let [selected? (contains? selected (:id shape))] -;; [:li {:class (classnames -;; :selected selected?)} -;; [:div.element-list-body {:class (classnames :selected selected?) -;; :on-click select-shape -;; :on-double-click #(dom/stop-propagation %) -;; :draggable true} -;; [:div.element-actions -;; [:div.toggle-element {:class (when-not (:hidden shape) "selected") -;; :on-click toggle-visibility} -;; i/eye] -;; [:div.block-element {:class (when (:blocked shape) "selected") -;; :on-click toggle-blocking} -;; i/lock]] -;; [:div.element-icon (element-icon shape)] -;; [:& layer-name {:shape shape}]]]))) + (select-shape [event] + (dom/prevent-default event) + (let [id (:id canvas)] + (cond + (or (:blocked canvas) + (:hidden canvas)) + nil + + (.-ctrlKey event) + (st/emit! (dw/select-shape id)) + + (> (count selected) 1) + (st/emit! (dw/deselect-all) + (dw/select-shape id)) + :else + (st/emit! (dw/deselect-all) + (dw/select-shape id))))) + + (on-drop [item monitor] + (st/emit! (udp/persist-page (:page canvas)))) + + (on-hover [item monitor] + (prn "canvas-item$hover" (:id canvas)) + (st/emit! (dw/change-canvas-order {:id (:canvas-id item) + :index index})))] + (let [selected? (contains? selected (:id canvas)) + collapsed? (:collapsed canvas false) + + shapes (filter #(= (:canvas (second %)) (:id canvas)) shapes) + [dprops dnd-ref] (use-sortable + {:type "canvas-item" + :data {:canvas-id (:id canvas) + :page-id (:page canvas) + :index index} + :on-hover on-hover + :on-drop on-drop})] + [:li.group {:ref dnd-ref + :class (classnames + :selected selected? + :dragging-TODO (:dragging? dprops))} + [:div.element-list-body {:class (classnames :selected selected?) + :on-click select-shape + :on-double-click #(dom/stop-propagation %)} + [:div.element-actions + [:div.toggle-element {:class (when-not (:hidden canvas) "selected") + :on-click toggle-visibility} + i/eye] + [:div.block-element {:class (when (:blocked canvas) "selected") + :on-click toggle-blocking} + i/lock]] + [:div.element-icon i/folder] + [:& layer-name {:shape canvas}] + [:span.toggle-content + { ;; :on-click toggle-collapse + :class (when-not collapsed? "inverse")} + i/arrow-slide]] + [:ul + (for [[index shape] shapes] + [:& layer-item {:shape shape + :selected selected + :index index + :key (:id shape)}])]]))) ;; --- Layers List -(def ^:private shapes-iref - (-> (l/key :shapes) - (l/derive st/state))) - -(def ^:private canvas-iref - (-> (l/key :canvas) - (l/derive st/state))) - (mf/defc layers-list [{:keys [shapes selected] :as props}] - (let [shapes-map (mf/deref shapes-iref) - canvas-map (mf/deref canvas-iref) - selected-shapes (mf/deref refs/selected-shapes) - selected-canvas (mf/deref refs/selected-canvas)] - [:div.tool-window-content - [:ul.element-list - (for [[index id] (map-indexed vector shapes)] - [:& layer-item {:shape (get shapes-map id) - :selected selected-shapes - :index index - :key id}])]])) + [:ul.element-list + (for [[index shape] shapes] + [:& layer-item {:shape shape + :selected selected + :index index + :key (:id shape)}])]) + +(mf/defc canvas-list + [{:keys [shapes canvas selected] :as props}] + [:ul.element-list + (for [[index item] canvas] + [:& canvas-item {:canvas item + :shapes shapes + :selected selected + :index index + :key (:id item)}])]) ;; --- Layers Toolbox (mf/defc layers-toolbox [{:keys [page selected] :as props}] (let [on-click #(st/emit! (dw/toggle-flag :layers)) - selected (mf/deref refs/selected-shapes)] + selected (mf/deref refs/selected-shapes) + shapes-by-id (mf/deref shapes-iref) + canvas (->> (:canvas page) + (map #(get shapes-by-id %)) + (enumerate)) + all-shapes (->> (:shapes page) + (map #(get shapes-by-id %))) + + shapes (->> all-shapes + (filter #(not (:canvas %))) + (enumerate)) + + all-shapes (enumerate all-shapes)] + [:div#layers.tool-window [:div.tool-window-bar [:div.tool-window-icon i/layers] [:span (tr "ds.settings.layers")] [:div.tool-window-close {:on-click on-click} i/close]] - [:& layers-list {:shapes (:shapes page) - :selected selected}]])) + [:div.tool-window-content + [:& canvas-list {:canvas canvas + :shapes all-shapes + :selected selected}] + [:& layers-list {:shapes shapes + :selected selected}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 3c7d2ee82..05e4ec8db 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -246,6 +246,9 @@ [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} (when page [:* + (for [id (reverse (:canvas page))] + [:& uus/shape-component {:id id :key id}]) + (for [id (reverse (:shapes page))] [:& uus/shape-component {:id id :key id}]) diff --git a/frontend/src/uxbox/util/data.cljs b/frontend/src/uxbox/util/data.cljs index 03e6f29a5..d35f657f6 100644 --- a/frontend/src/uxbox/util/data.cljs +++ b/frontend/src/uxbox/util/data.cljs @@ -79,6 +79,26 @@ (disj s v) (conj s v))) +(defn enumerate + ([items] (enumerate items 0)) + ([items start] + (loop [idx start + items items + res []] + (if (empty? items) + res + (recur (inc idx) + (rest items) + (conj res [idx (first items)])))))) + +(defn concatv + [& colls] + (loop [colls colls + result []] + (if (seq colls) + (recur (rest colls) (reduce conj result (first colls))) + result))) + (defn seek ([pred coll] (seek pred coll nil))