diff --git a/src/uxbox/data/shapes.cljs b/src/uxbox/data/shapes.cljs index a18b6f760..8a72420c6 100644 --- a/src/uxbox/data/shapes.cljs +++ b/src/uxbox/data/shapes.cljs @@ -372,43 +372,23 @@ (defn group-selected [] - (letfn [(update-shapes-on-page [state pid selected group] - (as-> (get-in state [:pages-by-id pid :shapes]) $ - (remove selected $) - (into [group] $) - (assoc-in state [:pages-by-id pid :shapes] $))) + (reify + udp/IPageUpdate + rs/UpdateEvent + (-apply-update [_ state] + (let [pid (get-in state [:workspace :page]) + selected (get-in state [:workspace :selected])] + (stsh/group-shapes state selected pid))))) - (update-shapes-on-index [state shapes group] - (reduce (fn [state {:keys [id] :as shape}] - (as-> shape $ - (assoc $ :group group) - (assoc-in state [:shapes-by-id id] $))) - state - shapes)) - (valid-selection? [shapes] - (let [groups (into #{} (map :group shapes))] - (= 1 (count groups))))] - (reify - udp/IPageUpdate - rs/UpdateEvent - (-apply-update [_ state] - (let [shapes-by-id (get state :shapes-by-id) - sid (uuid/random) - pid (get-in state [:workspace :page]) - selected (get-in state [:workspace :selected]) - selected' (map #(get shapes-by-id %) selected) - group {:type :group - :name (str "Group " (rand-int 1000)) - :items (into [] selected) - :id sid - :page pid}] - (if (valid-selection? selected') - (as-> state $ - (update-shapes-on-index $ selected' sid) - (update-shapes-on-page $ pid selected sid) - (update $ :shapes-by-id assoc sid group) - (update $ :workspace assoc :selected #{})) - state)))))) +(defn degroup-selected + [] + (reify + udp/IPageUpdate + rs/UpdateEvent + (-apply-update [_ state] + (let [pid (get-in state [:workspace :page]) + selected (get-in state [:workspace :selected])] + (stsh/degroup-shapes state selected pid))))) ;; TODO: maybe split in two separate events (defn duplicate-selected diff --git a/src/uxbox/state/shapes.cljs b/src/uxbox/state/shapes.cljs index 73d7fe7d2..d0b0bfe96 100644 --- a/src/uxbox/state/shapes.cljs +++ b/src/uxbox/state/shapes.cljs @@ -264,3 +264,111 @@ match (partial try-match-shape xf selrect) shapes (get-in state [:pages-by-id page :shapes])] (reduce match #{} (sequence xf shapes)))) + +(defn group-shapes + [state shapes page] + (letfn [(replace-first-item [pred coll replacement] + (into [] + (concat + (take-while #(not (pred %)) coll) + [replacement] + (drop 1 (drop-while #(not (pred %)) coll))))) + + (move-shapes-to-new-group [state page shapes new-group] + (reduce (fn [state {:keys [id group] :as shape}] + (-> state + (update-in [:shapes-by-id group :items] #(remove (set [id]) %)) + (update-in [:pages-by-id page :shapes] #(remove (set [id]) %)) + (clear-empty-groups shape) + (assoc-in [:shapes-by-id id :group] new-group) + )) + state + shapes)) + + (update-shapes-on-page [state page shapes group] + (as-> (get-in state [:pages-by-id page :shapes]) $ + (replace-first-item (set shapes) $ group) + (remove (set shapes) $) + (into [] $) + (assoc-in state [:pages-by-id page :shapes] $))) + + (update-shapes-on-group [state parent-group shapes group] + (as-> (get-in state [:shapes-by-id parent-group :items]) $ + (replace-first-item (set shapes) $ group) + (remove (set shapes) $) + (into [] $) + (assoc-in state [:shapes-by-id parent-group :items] $))) + + (update-shapes-on-index [state shapes group] + (reduce (fn [state {:keys [id] :as shape}] + (as-> shape $ + (assoc $ :group group) + (assoc-in state [:shapes-by-id id] $))) + state + shapes))] + (let [sid (uuid/random) + shapes' (map #(get-in state [:shapes-by-id %]) shapes) + distinct-groups (distinct (map :group shapes')) + parent-group (cond + (not= 1 (count distinct-groups)) :multi + (nil? (first distinct-groups)) :page + :else (first distinct-groups)) + group {:type :group + :name (str "Group " (rand-int 1000)) + :items (into [] shapes) + :id sid + :page page}] + (as-> state $ + (update-shapes-on-index $ shapes' sid) + (cond + (= :multi parent-group) + (-> $ + (move-shapes-to-new-group page shapes' sid) + (update-in [:pages-by-id page :shapes] #(into [] (cons sid %)))) + (= :page parent-group) + (update-shapes-on-page $ page shapes sid) + :else + (update-shapes-on-group $ parent-group shapes sid)) + (update $ :shapes-by-id assoc sid group) + (cond + (= :multi parent-group) $ + (= :page parent-group) $ + :else (assoc-in $ [:shapes-by-id sid :group] parent-group)) + (update $ :workspace assoc :selected #{sid}))))) + +(defn degroup-shapes + [state shapes page] + (letfn [(empty-group [state page-id group-id] + (let [group (get-in state [:shapes-by-id group-id]) + parent-id (:group group) + position (if (nil? parent-id) + (index-of (get-in state [:pages-by-id page-id :shapes]) group-id) + (index-of (get-in state [:shapes-by-id parent-id :items]) group-id)) + reduce-func (fn [state item] + (if (nil? parent-id) + (-> state + (update-in [:pages-by-id page-id :shapes] #(drop-at-index position % item)) + (update-in [:shapes-by-id item] dissoc :group)) + (-> state + (update-in [:shapes-by-id parent-id :items] #(drop-at-index position % item)) + (assoc-in [:shapes-by-id item :group] parent-id))))] + + (as-> state $ + (reduce reduce-func $ (reverse (:items group))) + (if (nil? parent-id) + (update-in $ [:pages-by-id page-id :shapes] #(into [] (remove #{group-id} %))) + (update-in $ [:shapes-by-id parent-id :items] #(into [] (remove #{group-id} %)))) + (update-in $ [:shapes-by-id] dissoc group-id)))) + + (empty-groups [state page-id groups-ids] + (reduce (fn [state group-id] + (empty-group state page-id group-id)) + state + groups-ids))] + (let [shapes' (map #(get-in state [:shapes-by-id %]) shapes) + groups (filter #(= (:type %) :group) shapes') + groups-ids (map :id groups) + groups-items (remove (set groups-ids) (mapcat :items groups))] + (as-> state $ + (empty-groups $ page groups-ids) + (update $ :workspace assoc :selected (set groups-items)))))) diff --git a/src/uxbox/ui/workspace/shortcuts.cljs b/src/uxbox/ui/workspace/shortcuts.cljs index 9b15c51a3..591e51955 100644 --- a/src/uxbox/ui/workspace/shortcuts.cljs +++ b/src/uxbox/ui/workspace/shortcuts.cljs @@ -24,7 +24,9 @@ ;; --- Shortcuts (defonce ^:const +shortcuts+ - {:ctrl+g #(rs/emit! (dw/toggle-flag :grid)) + {:shift+g #(rs/emit! (dw/toggle-flag :grid)) + :ctrl+g #(rs/emit! (uds/group-selected)) + :ctrl+shift+g #(rs/emit! (uds/degroup-selected)) :ctrl+shift+m #(rs/emit! (dw/toggle-flag :sitemap)) :ctrl+shift+f #(rs/emit! (dw/toggle-flag :drawtools)) :ctrl+shift+i #(rs/emit! (dw/toggle-flag :icons)) diff --git a/src/uxbox/ui/workspace/sidebar/layers.cljs b/src/uxbox/ui/workspace/sidebar/layers.cljs index dad2fb593..df2a67805 100644 --- a/src/uxbox/ui/workspace/sidebar/layers.cljs +++ b/src/uxbox/ui/workspace/sidebar/layers.cljs @@ -290,11 +290,8 @@ page (rum/react (focus-page (:page workspace))) close #(rs/emit! (udw/toggle-flag :layers)) duplicate #(rs/emit! (uds/duplicate-selected)) - move-up #(rs/emit! (uds/move-selected-layer :up)) - move-down #(rs/emit! (uds/move-selected-layer :down)) - move-top #(rs/emit! (uds/move-selected-layer :top)) - move-bottom #(rs/emit! (uds/move-selected-layer :bottom)) group #(rs/emit! (uds/group-selected)) + degroup #(rs/emit! (uds/degroup-selected)) delete #(rs/emit! (uds/delete-selected)) dragel (volatile! nil)] (html @@ -314,12 +311,9 @@ (rum/with-key key))))]] [:div.layers-tools [:ul.layers-tools-content - [:li.layer-up {:on-click move-up} i/arrow-slide] - [:li.layer-top {:on-click move-top} i/arrow-end] - [:li.layer-down {:on-click move-down} i/arrow-slide] - [:li.layer-end {:on-click move-bottom} i/arrow-end] [:li.clone-layer {:on-click duplicate} i/copy] [:li.group-layer {:on-click group} i/folder] + [:li.degroup-layer {:on-click degroup} i/ungroup] [:li.delete-layer {:on-click delete} i/trash]]]]))) (def ^:static layers-toolbox diff --git a/test/uxbox/state/shapes_tests.cljs b/test/uxbox/state/shapes_tests.cljs index 50e722cf9..765fda4a8 100644 --- a/test/uxbox/state/shapes_tests.cljs +++ b/test/uxbox/state/shapes_tests.cljs @@ -305,3 +305,206 @@ ;; (pprint expected) ;; (pprint result) (t/is (= result expected)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Group Shapes +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; group a shape + +(t/deftest group-shapes-1 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1 2 3]}} + :shapes-by-id {1 {:id 1 :page 1} + 2 {:id 2 :page 1} + 3 {:id 3 :page 1}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{4}) + (assoc-in [:pages-by-id 1 :shapes] [1 4 3]) + (assoc-in [:shapes-by-id 2 :group] 4) + (assoc-in [:shapes-by-id 4] {:type :group :name "Group 10" + :items [2] :id 4 :page 1}))] + (with-redefs [uuid.core/random (constantly 4) + cljs.core/rand-int (constantly 10)] + (let [result (ssh/group-shapes initial [2] 1)] + (t/is (= result expected)))))) + + +;; group two shapes + +(t/deftest group-shapes-2 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1 2 3]}} + :shapes-by-id {1 {:id 1 :page 1} + 2 {:id 2 :page 1} + 3 {:id 3 :page 1}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{4}) + (assoc-in [:pages-by-id 1 :shapes] [1 4]) + (assoc-in [:shapes-by-id 2 :group] 4) + (assoc-in [:shapes-by-id 3 :group] 4) + (assoc-in [:shapes-by-id 4] {:type :group :name "Group 10" + :items [2 3] :id 4 :page 1}))] + (with-redefs [uuid.core/random (constantly 4) + cljs.core/rand-int (constantly 10)] + (let [result (ssh/group-shapes initial [2 3] 1)] + (t/is (= result expected)))))) + + +;; group group + +(t/deftest group-shapes-3 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1 2 3]}} + :shapes-by-id {1 {:id 1 :page 1} + 2 {:id 2 :page 1} + 3 {:id 3 :page 1 :type :group}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{4}) + (assoc-in [:pages-by-id 1 :shapes] [1 4]) + (assoc-in [:shapes-by-id 2 :group] 4) + (assoc-in [:shapes-by-id 3 :group] 4) + (assoc-in [:shapes-by-id 4] {:type :group :name "Group 10" + :items [2 3] :id 4 :page 1}))] + (with-redefs [uuid.core/random (constantly 4) + cljs.core/rand-int (constantly 10)] + (let [result (ssh/group-shapes initial [2 3] 1)] + (t/is (= result expected)))))) + + +;; group shapes inside a group + +(t/deftest group-shapes-3 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1 3]}} + :shapes-by-id {1 {:id 1 :page 1} + 2 {:id 2 :page 1 :group 3} + 3 {:id 3 :page 1 :type :group}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{4}) + (assoc-in [:pages-by-id 1 :shapes] [1 3]) + (assoc-in [:shapes-by-id 2 :group] 4) + (assoc-in [:shapes-by-id 3 :items] [4]) + (assoc-in [:shapes-by-id 4] {:type :group :name "Group 10" + :items [2] :id 4 :page 1 :group 3}))] + (with-redefs [uuid.core/random (constantly 4) + cljs.core/rand-int (constantly 10)] + (let [result (ssh/group-shapes initial [2] 1)] + (t/is (= result expected)))))) + +;; group shapes in multiple groups + +(t/deftest group-shapes-4 + (let [initial {:pages-by-id {1 {:id 1 :shapes [3 4]}} + :shapes-by-id {1 {:id 1 :page 1 :group 4} + 2 {:id 2 :page 1 :group 3} + 3 {:id 3 :page 1 :type :group :items [2]} + 4 {:id 4 :page 1 :type :group :imtes [3]}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{5}) + (assoc-in [:pages-by-id 1 :shapes] [5]) + (assoc-in [:shapes-by-id 1 :group] 5) + (assoc-in [:shapes-by-id 2 :group] 5) + (assoc-in [:shapes-by-id 5] {:type :group :name "Group 10" + :items [1 2] :id 5 :page 1}) + (update-in [:shapes-by-id] dissoc 3) + (update-in [:shapes-by-id] dissoc 4))] + (with-redefs [uuid.core/random (constantly 5) + cljs.core/rand-int (constantly 10)] + (let [result (ssh/group-shapes initial [1 2] 1)] + (t/is (= result expected)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Degroups +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; degroup a single group + +;; degroup group + +(t/deftest degroup-shapes-1 + (let [initial {:pages-by-id {1 {:id 1 :shapes [3]}} + :shapes-by-id {1 {:id 1 :page 1 :group 3} + 2 {:id 2 :page 1 :group 3} + 3 {:id 3 :page 1 :type :group :items [1 2]}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{1 2}) + (assoc-in [:pages-by-id 1 :shapes] [1 2]) + (update-in [:shapes-by-id 1] dissoc :group) + (update-in [:shapes-by-id 2] dissoc :group) + (update-in [:shapes-by-id] dissoc 3))] + (let [result (ssh/degroup-shapes initial [3] 1)] + (t/is (= result expected))))) + + +;; degroup group inside a group + +(t/deftest degroup-shapes-2 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1]}} + :shapes-by-id {1 {:id 1 :page 1 :type :group :items [2]} + 2 {:id 2 :page 1 :type :group :items [3] :group 1} + 3 {:id 3 :page 1 :group 2}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{3}) + (assoc-in [:pages-by-id 1 :shapes] [1]) + (update-in [:shapes-by-id] dissoc 2) + (assoc-in [:shapes-by-id 1 :items] [3]) + (assoc-in [:shapes-by-id 3 :group] 1))] + (let [result (ssh/degroup-shapes initial [2] 1)] + (t/is (= result expected))))) + +;; degroup multiple groups not nested + +(t/deftest degroup-shapes-3 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1 2]}} + :shapes-by-id {1 {:id 1 :page 1 :type :group :items [3]} + 2 {:id 2 :page 1 :type :group :items [4]} + 3 {:id 3 :page 1 :group 1} + 4 {:id 4 :page 1 :group 2}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{3 4}) + (assoc-in [:pages-by-id 1 :shapes] [3 4]) + (update :shapes-by-id dissoc 1) + (update :shapes-by-id dissoc 2) + (update-in [:shapes-by-id 3] dissoc :group) + (update-in [:shapes-by-id 4] dissoc :group))] + (let [result (ssh/degroup-shapes initial [1 2] 1)] + (t/is (= result expected))))) + +;; degroup multiple groups nested (child first) + +(t/deftest degroup-shapes-4 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1]}} + :shapes-by-id {1 {:id 1 :page 1 :type :group :items [2]} + 2 {:id 2 :page 1 :type :group :items [3] :group 1} + 3 {:id 3 :page 1 :group 2}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{3}) + (assoc-in [:pages-by-id 1 :shapes] [3]) + (update :shapes-by-id dissoc 1) + (update :shapes-by-id dissoc 2) + (update-in [:shapes-by-id 3] dissoc :group))] + (let [result (ssh/degroup-shapes initial [2 1] 1)] + (t/is (= result expected))))) + +;; degroup multiple groups nested (parent first) + +(t/deftest degroup-shapes-5 + (let [initial {:pages-by-id {1 {:id 1 :shapes [1]}} + :shapes-by-id {1 {:id 1 :page 1 :type :group :items [2]} + 2 {:id 2 :page 1 :type :group :items [3] :group 1} + 3 {:id 3 :page 1 :group 2}}} + + expected (-> initial + (assoc-in [:workspace :selected] #{3}) + (assoc-in [:pages-by-id 1 :shapes] [3]) + (update :shapes-by-id dissoc 1) + (update :shapes-by-id dissoc 2) + (update-in [:shapes-by-id 3] dissoc :group))] + (let [result (ssh/degroup-shapes initial [1 2] 1)] + (t/is (= result expected)))))