From cae5b5e778ebbc8061b6fb08f6185bbcec54e8de Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 19 Mar 2020 17:40:29 +0100 Subject: [PATCH] :tada: Add right-click context menu on workspace. --- frontend/src/uxbox/main/data/workspace.cljs | 45 ++++++++++++ frontend/src/uxbox/main/ui/shapes/circle.cljs | 6 +- frontend/src/uxbox/main/ui/shapes/common.cljs | 67 ++++++++++------- frontend/src/uxbox/main/ui/shapes/frame.cljs | 2 + frontend/src/uxbox/main/ui/shapes/rect.cljs | 6 +- frontend/src/uxbox/main/ui/workspace.cljs | 4 +- .../uxbox/main/ui/workspace/context_menu.cljs | 72 +++++++++++++++++++ .../main/ui/workspace/sidebar/layers.cljs | 18 +++++ .../src/uxbox/main/ui/workspace/viewport.cljs | 31 +++----- 9 files changed, 197 insertions(+), 54 deletions(-) create mode 100644 frontend/src/uxbox/main/ui/workspace/context_menu.cljs diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 12533631a..a735df079 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1922,6 +1922,51 @@ query-params {:page-id (first page-ids)}] (rx/of (rt/nav :workspace path-params query-params)))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Context Menu +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::point gpt/point?) + +(defn show-context-menu + [{:keys [position] :as params}] + (us/verify ::point position) + (ptk/reify ::show-context-menu + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :context-menu] {:position position})))) + +(defn show-shape-context-menu + [{:keys [position shape] :as params}] + (us/verify ::point position) + (us/verify ::cp/minimal-shape shape) + (ptk/reify ::show-context-menu + ptk/UpdateEvent + (update [_ state] + (let [selected (get-in state [:workspace-local :selected]) + selected (cond + (empty? selected) + (conj selected (:id shape)) + + (contains? selected (:id shape)) + selected + + :else + #{(:id shape)}) + mdata {:position position + :selected selected + :shape shape}] + (-> state + (assoc-in [:workspace-local :context-menu] mdata) + (assoc-in [:workspace-local :selected] selected)))))) + +(def hide-context-menu + (ptk/reify ::hide-context-menu + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :context-menu] nil)))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Changes Reactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/uxbox/main/ui/shapes/circle.cljs b/frontend/src/uxbox/main/ui/shapes/circle.cljs index 4ff3a31c3..c8dedb3c4 100644 --- a/frontend/src/uxbox/main/ui/shapes/circle.cljs +++ b/frontend/src/uxbox/main/ui/shapes/circle.cljs @@ -23,9 +23,11 @@ [{:keys [shape] :as props}] (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) - on-mouse-down #(common/on-mouse-down % shape selected)] + on-mouse-down #(common/on-mouse-down % shape) + on-context-menu #(common/on-context-menu % shape)] [:g.shape {:class (when selected? "selected") - :on-mouse-down on-mouse-down} + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} [:& circle-shape {:shape shape}]])) ;; --- Circle Shape diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index 9bfc3416f..d3f21b17d 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -68,33 +68,46 @@ ([event {:keys [id type] :as shape} kk-tmp] (let [selected @refs/selected-shapes selected? (contains? selected id) - drawing? @refs/selected-drawing-tool] - (when-not (:blocked shape) - (cond - drawing? - nil + drawing? @refs/selected-drawing-tool + button (.-which (.-nativeEvent event))] + (when-not (:blocked shape) + (cond + (not= 1 button) + nil - (= type :frame) - (when selected? - (dom/stop-propagation event) - (st/emit! start-move-frame)) + drawing? + nil - (and (not selected?) (empty? selected)) - (do - (dom/stop-propagation event) - (st/emit! dw/deselect-all - (dw/select-shape id) - start-move-selected)) + (= type :frame) + (when selected? + (dom/stop-propagation event) + (st/emit! start-move-frame)) - (and (not selected?) (not (empty? selected))) - (do - (dom/stop-propagation event) - (if (kbd/shift? event) - (st/emit! (dw/select-shape id)) - (st/emit! dw/deselect-all - (dw/select-shape id) - start-move-selected))) - :else - (do - (dom/stop-propagation event) - (st/emit! start-move-selected))))))) + (and (not selected?) (empty? selected)) + (do + (dom/stop-propagation event) + (st/emit! dw/deselect-all + (dw/select-shape id) + start-move-selected)) + + (and (not selected?) (not (empty? selected))) + (do + (dom/stop-propagation event) + (if (kbd/shift? event) + (st/emit! (dw/select-shape id)) + (st/emit! dw/deselect-all + (dw/select-shape id) + start-move-selected))) + :else + (do + (dom/stop-propagation event) + (st/emit! start-move-selected))))))) + + +(defn on-context-menu + [event shape] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [position (dom/get-client-position event)] + (st/emit!(dw/show-shape-context-menu {:position position + :shape shape})))) diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index 93265dca1..a1dfad528 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -95,6 +95,7 @@ :deps (mf/deps (:id shape))}) selected? (mf/deref selected-iref) on-mouse-down #(common/on-mouse-down % shape) + on-context-menu #(common/on-context-menu % shape) shape (merge frame-default-props shape) childs (mapv #(get objects %) (:shapes shape)) @@ -105,6 +106,7 @@ (st/emit! dw/deselect-all (dw/select-shape (:id shape))))] [:g {:class (when selected? "selected") + :on-context-menu on-context-menu :on-double-click on-double-click :on-mouse-down on-mouse-down} [:& frame-shape {:shape shape :childs childs}]]))) diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index f30376b33..8c007f387 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -23,8 +23,10 @@ (mf/defrc rect-wrapper [props] (let [shape (unchecked-get props "shape") - on-mouse-down #(common/on-mouse-down % shape)] - [:g.shape {:on-mouse-down on-mouse-down} + on-mouse-down #(common/on-mouse-down % shape) + on-context-menu #(common/on-context-menu % shape)] + [:g.shape {:on-mouse-down on-mouse-down + :on-context-menu on-context-menu} [:& rect-shape {:shape shape}]])) ;; --- Rect Shape diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index d4c77437e..2278d05c3 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -22,9 +22,8 @@ [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.workspace.viewport :refer [viewport]] [uxbox.main.ui.workspace.colorpalette :refer [colorpalette]] - ;; [uxbox.main.ui.workspace.download] + [uxbox.main.ui.workspace.context-menu :refer [context-menu]] [uxbox.main.ui.workspace.header :refer [header]] - ;; [uxbox.main.ui.workspace.images] [uxbox.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [uxbox.main.ui.workspace.scroll :as scroll] [uxbox.main.ui.workspace.shortcuts :as shortcuts] @@ -74,6 +73,7 @@ [:& colorpalette]) [:main.main-content + [:& context-menu {}] [:section.workspace-content {:class classes :on-scroll on-scroll diff --git a/frontend/src/uxbox/main/ui/workspace/context_menu.cljs b/frontend/src/uxbox/main/ui/workspace/context_menu.cljs new file mode 100644 index 000000000..e6aa07fcc --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/context_menu.cljs @@ -0,0 +1,72 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.context-menu + "A workspace specific context menu (mouse right click)." + (:require + [beicon.core :as rx] + [lentes.core :as l] + [potok.core :as ptk] + [rumext.alpha :as mf] + [uxbox.main.store :as st] + [uxbox.main.refs :as refs] + [uxbox.main.streams :as ms] + [uxbox.builtins.icons :as i] + [uxbox.util.dom :as dom] + [uxbox.main.data.workspace :as dw] + [uxbox.main.ui.react-hooks :refer [use-rxsub]] + [uxbox.main.ui.components.dropdown :refer [dropdown]])) + +(def menu-ref + (-> (l/key :context-menu) + (l/derive refs/workspace-local))) + +(defn- prevent-default + [event] + (dom/prevent-default event) + (dom/stop-propagation event)) + +(mf/defc shape-context-menu + [{:keys [mdata] :as props}] + (let [shape (:shape mdata) + selected (:selected mdata) + + on-duplicate + (fn [event] + (st/emit! dw/duplicate-selected)) + + on-delete + (fn [event] + (st/emit! dw/delete-selected)) ] + [:* + [:li {:on-click on-duplicate} i/copy [:span "duplicate"]] + [:li {:on-click on-delete} i/trash [:span "delete"]]])) + +(mf/defc viewport-context-menu + [{:keys [mdata] :as props}] + [:* + [:li i/copy [:span "paste (TODO)"]] + [:li i/copy [:span "copy as svg (TODO)"]]]) + +(mf/defc context-menu + [props] + (let [mdata (mf/deref menu-ref)] + [:& dropdown {:show (boolean mdata) + :on-close #(st/emit! dw/hide-context-menu)} + [:ul.workspace-context-menu + {:style {:top (- (get-in mdata [:position :y]) 20) + :left (get-in mdata [:position :x])} + :on-context-menu prevent-default} + + (if (:shape mdata) + [:& shape-context-menu {:mdata mdata}] + [:& viewport-context-menu {:mdata mdata}])]])) + + + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index ef57c000d..6e8547214 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -118,6 +118,14 @@ (st/emit! dw/deselect-all (dw/select-shape id))))) + on-context-menu + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [pos (dom/get-client-position event)] + (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))) @@ -134,6 +142,7 @@ :on-hover on-hover :on-drop on-drop})] [:li {:ref dnd-ref + :on-context-menu on-context-menu :class (dom/classnames :selected selected? :dragging-TODO (:dragging? dprops))} @@ -195,6 +204,14 @@ (st/emit! dw/deselect-all (dw/select-shape id))))) + on-context-menu + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [pos (dom/get-client-position event)] + (st/emit! (dw/show-shape-context-menu {:position pos + :shape item})))) + on-drop (fn [item monitor] (st/emit! (dw/commit-shape-order-change (:obj-id item)))) @@ -211,6 +228,7 @@ :on-hover on-hover :on-drop on-drop})] [:li.group {:ref dnd-ref + :on-context-menu on-context-menu :class (dom/classnames :selected selected? :dragging-TODO (:dragging? dprops))} diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 2e86bd980..6ef975bbb 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -165,11 +165,8 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :context-menu ctrl? shift?)))) + (let [position (dom/get-client-position event)] + (st/emit! (dw/show-context-menu {:position position})))) on-mouse-up (fn [event] @@ -225,25 +222,14 @@ (st/emit! ::finish-positioning #_(dw/stop-viewport-positioning))) (st/emit! (ms/->KeyboardEvent :up key ctrl? shift?)))) - ;; translate-point-to-viewport - ;; (fn [pt] - ;; (let [viewport (mf/ref-node viewport-ref) - ;; brect (.getBoundingClientRect viewport) - ;; brect (gpt/point (parse-int (.-left brect)) - ;; (parse-int (.-top brect)))] - ;; (gpt/subtract pt brect))) - on-mouse-move (fn [event] ;; NOTE: offsetX and offsetY are marked as "experimental" on ;; MDN site but seems like they are supported on all ;; browsers so we can avoid translation opetation just using ;; this attributes. - (let [;; pt (gpt/point (.-clientX event) - ;; (.-clientY event)) - ;; pt (translate-point-to-viewport pt) - pt (gpt/point (.-offsetX (.-nativeEvent event)) - (.-offsetY (.-nativeEvent event)))] + (let [pt (dom/get-offset-position event)] + (reset! last-position pt) (st/emit! (ms/->PointerEvent :viewport pt (kbd/ctrl? event) (kbd/shift? event))))) @@ -251,10 +237,14 @@ on-mount (fn [] (let [key1 (events/listen js/document EventType.KEYDOWN on-key-down) - key2 (events/listen js/document EventType.KEYUP on-key-up)] + key2 (events/listen js/document EventType.KEYUP on-key-up) + dnode (mf/ref-val viewport-ref) + key3 (events/listen dnode EventType.MOUSEMOVE on-mouse-move)] (fn [] (events/unlistenByKey key1) - (events/unlistenByKey key2))))] + (events/unlistenByKey key2) + (events/unlistenByKey key3) + )))] (mf/use-effect on-mount) [:* @@ -266,7 +256,6 @@ :on-context-menu on-context-menu :on-click on-click :on-double-click on-double-click - :on-mouse-move on-mouse-move :on-mouse-down on-mouse-down :on-mouse-up on-mouse-up} [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}