From e87dc6d34cc5b79e7a6f8845f6c0f6d869220448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Tue, 23 Feb 2021 14:02:21 +0100 Subject: [PATCH] :sparkles: Add context menu with right click in dashboard --- .../styles/main/partials/context-menu.scss | 4 + .../main/partials/dashboard-sidebar.scss | 2 +- .../app/main/ui/components/context_menu.cljs | 8 +- .../src/app/main/ui/dashboard/file_menu.cljs | 96 ++++++++++++++++ frontend/src/app/main/ui/dashboard/files.cljs | 32 ++---- frontend/src/app/main/ui/dashboard/grid.cljs | 105 +++++------------- .../app/main/ui/dashboard/project_menu.cljs | 53 +++++++++ .../src/app/main/ui/dashboard/projects.cljs | 2 +- .../src/app/main/ui/dashboard/sidebar.cljs | 47 +++++--- frontend/src/app/util/dom.cljs | 3 + 10 files changed, 235 insertions(+), 117 deletions(-) create mode 100644 frontend/src/app/main/ui/dashboard/file_menu.cljs create mode 100644 frontend/src/app/main/ui/dashboard/project_menu.cljs diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss index 7a585c7fd5..bd2f715b21 100644 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ b/frontend/resources/styles/main/partials/context-menu.scss @@ -21,6 +21,10 @@ visibility: visible; } +.context-menu.fixed { + position: fixed; +} + .context-menu-items { background: $color-white; border-radius: $br-small; diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 801e91d5b7..d2f0e6b1ac 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -162,7 +162,7 @@ overflow: unset; } - li { + & > li { align-items: center; cursor: pointer; display: flex; diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index 86b257333d..2c51f7f2ea 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -27,8 +27,9 @@ options (gobj/get props "options") is-selectable (gobj/get props "selectable") selected (gobj/get props "selected") - top (gobj/get props "top") - left (gobj/get props "left") + top (gobj/get props "top" 0) + left (gobj/get props "left" 0) + fixed? (gobj/get props "fixed?" false) offset (mf/use-state 0) @@ -36,7 +37,7 @@ (mf/use-callback (mf/deps top @offset) (fn [node] - (when node + (when (and node (not fixed?)) (let [{node-height :height} (dom/get-bounding-rect node) {window-height :height} (dom/get-window-size) target-offset (if (> (+ top node-height) window-height) @@ -49,6 +50,7 @@ (when open? [:> dropdown' props [:div.context-menu {:class (classnames :is-open open? + :fixed fixed? :is-selectable is-selectable) :style {:top (+ top @offset) :left left}} diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs new file mode 100644 index 0000000000..600c2bf61c --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -0,0 +1,96 @@ +;; 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) 2021 UXBOX Labs SL + +(ns app.main.ui.dashboard.file-menu + (:require + [app.main.data.dashboard :as dd] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.components.context-menu :refer [context-menu]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc file-menu + [{:keys [file show? on-edit on-menu-close top left] :as props}] + (assert (some? file) "missing `file` prop") + (assert (boolean? show?) "missing `show?` prop") + (assert (fn? on-edit) "missing `on-edit` prop") + (assert (fn? on-menu-close) "missing `on-menu-close` prop") + (let [top (or top 0) + left (or left 0) + + delete-fn + (mf/use-callback + (mf/deps file) + (st/emitf (dd/delete-file file))) + + on-delete + (mf/use-callback + (mf/deps file) + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-file-confirm.title") + :message (tr "modals.delete-file-confirm.message") + :accept-label (tr "modals.delete-file-confirm.accept") + :on-accept delete-fn})))) + + add-shared + (mf/use-callback + (mf/deps file) + (st/emitf (dd/set-file-shared (assoc file :is-shared true)))) + + del-shared + (mf/use-callback + (mf/deps file) + (st/emitf (dd/set-file-shared (assoc file :is-shared false)))) + + on-add-shared + (mf/use-callback + (mf/deps file) + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :message "" + :title (tr "modals.add-shared-confirm.message" (:name file)) + :hint (tr "modals.add-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.add-shared-confirm.accept") + :accept-style :primary + :on-accept add-shared})))) + + on-del-shared + (mf/use-callback + (mf/deps file) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :message "" + :title (tr "modals.remove-shared-confirm.message" (:name file)) + :hint (tr "modals.remove-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.remove-shared-confirm.accept") + :on-accept del-shared}))))] + + [:& context-menu {:on-close on-menu-close + :show show? + :fixed? (or (not= top 0) (not= left 0)) + :top top + :left left + :options [[(tr "labels.rename") on-edit] + [(tr "labels.delete") on-delete] + (if (:is-shared file) + [(tr "dashboard.remove-shared") on-del-shared] + [(tr "dashboard.add-shared") on-add-shared])]}])) + diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 8a4caf6f03..6bfa862c0c 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -15,6 +15,7 @@ [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.project-menu :refer [project-menu]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t]] @@ -45,23 +46,6 @@ (mf/deps project) (st/emitf (dd/toggle-project-pin project))) - delete-fn - (mf/use-callback - (mf/deps project) - (fn [event] - (st/emit! (dd/delete-project project) - (rt/nav :dashboard-projects {:team-id (:id team)})))) - - on-delete - (mf/use-callback - (mf/deps project) - (st/emitf (modal/show - {:type :confirm - :title (t locale "modals.delete-project-confirm.title") - :message (t locale "modals.delete-project-confirm.message") - :accept-label (t locale "modals.delete-project-confirm.accept") - :on-accept delete-fn}))) - on-create-clicked (mf/use-callback (mf/deps project) @@ -81,12 +65,14 @@ (st/emit! (dd/rename-project (assoc project :name name))) (swap! local assoc :edition false))}] [:div.dashboard-title - [:h1 (:name project)] - [:div.icon {:on-click on-menu-click} i/actions] - [:& context-menu {:on-close on-menu-close - :show (:menu-open @local) - :options [[(t locale "labels.rename") on-edit] - [(t locale "labels.delete") on-delete]]}] + [:h1 {:on-double-click on-edit} + (:name project)] + [:div.icon {:on-click on-menu-click} + i/actions] + [:& project-menu {:project project + :show? (:menu-open @local) + :on-edit on-edit + :on-menu-close on-menu-close}] [:div.icon.pin-icon {:class (when (:is-pinned project) "active") :on-click toggle-pin} diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 91e7c89d86..6479aad0f2 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -17,6 +17,7 @@ [app.main.fonts :as fonts] [app.main.store :as st] [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.dashboard.file-menu :refer [file-menu]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.icons :as i] [app.main.worker :as wrk] @@ -61,84 +62,36 @@ (mf/defc grid-item {:wrap [mf/memo]} [{:keys [id file] :as props}] - (let [local (mf/use-state {:menu-open false :edition false}) + (let [local (mf/use-state {:menu-open false + :menu-pos nil + :edition false}) locale (mf/deref i18n/locale) - on-close (mf/use-callback #(swap! local assoc :menu-open false)) + menu-ref (mf/use-ref) - delete-fn + on-menu-close (mf/use-callback - (mf/deps file) - (st/emitf (dd/delete-file file))) - - on-delete - (mf/use-callback - (mf/deps file) - (fn [event] - (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :title (t locale "modals.delete-file-confirm.title") - :message (t locale "modals.delete-file-confirm.message") - :accept-label (t locale "modals.delete-file-confirm.accept") - :on-accept delete-fn})))) + #(swap! local assoc :menu-open false)) on-navigate (mf/use-callback (mf/deps id) - (fn [] - (let [pparams {:project-id (:project-id file) - :file-id (:id file)} - qparams {:page-id (first (get-in file [:data :pages]))}] - (st/emit! (rt/nav :workspace pparams qparams))))) - - - add-shared - (mf/use-callback - (mf/deps file) - (st/emitf (dd/set-file-shared (assoc file :is-shared true)))) - - del-shared - (mf/use-callback - (mf/deps file) - (st/emitf (dd/set-file-shared (assoc file :is-shared false)))) - - on-add-shared - (mf/use-callback - (mf/deps file) (fn [event] - (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :message "" - :title (t locale "modals.add-shared-confirm.message" (:name file)) - :hint (t locale "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (t locale "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared})))) - - on-del-shared - (mf/use-callback - (mf/deps file) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :message "" - :title (t locale "modals.remove-shared-confirm.message" (:name file)) - :hint (t locale "modals.remove-shared-confirm.hint") - :cancel-label :omit - :accept-label (t locale "modals.remove-shared-confirm.accept") - :on-accept del-shared})))) + (let [menu-icon (mf/ref-val menu-ref) + target (dom/get-target event)] + (when-not (dom/child? target menu-icon) + (let [pparams {:project-id (:project-id file) + :file-id (:id file)} + qparams {:page-id (first (get-in file [:data :pages]))}] + (st/emit! (rt/nav :workspace pparams qparams))))))) on-menu-click (mf/use-callback (mf/deps file) (fn [event] (dom/prevent-default event) - (dom/stop-propagation event) - (swap! local assoc :menu-open true))) + (let [position (dom/get-client-position event)] + (swap! local assoc :menu-open true + :menu-pos position)))) edit (mf/use-callback @@ -154,10 +107,10 @@ (dom/stop-propagation event) (swap! local assoc :edition true - :menu-open false))) + :menu-open false)))] - ] - [:div.grid-item.project-th {:on-click on-navigate} + [:div.grid-item.project-th {:on-click on-navigate + :on-context-menu on-menu-click} [:div.overlay] [:& grid-item-thumbnail {:file file}] (when (:is-shared file) @@ -171,15 +124,15 @@ [:div.project-th-actions {:class (dom/classnames :force-display (:menu-open @local))} [:div.project-th-icon.menu - {:on-click on-menu-click} - i/actions] - [:& context-menu {:on-close on-close - :show (:menu-open @local) - :options [[(t locale "labels.rename") on-edit] - [(t locale "labels.delete") on-delete] - (if (:is-shared file) - [(t locale "dashboard.remove-shared") on-del-shared] - [(t locale "dashboard.add-shared") on-add-shared])]}]]])) + {:ref menu-ref + :on-click on-menu-click} + i/actions]] + [:& file-menu {:file file + :show? (:menu-open @local) + :left (:x (:menu-pos @local)) + :top (:y (:menu-pos @local)) + :on-edit on-edit + :on-menu-close on-menu-close}]])) (mf/defc empty-placeholder [] diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs new file mode 100644 index 0000000000..71aad431cc --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -0,0 +1,53 @@ +;; 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) 2021 UXBOX Labs SL + +(ns app.main.ui.dashboard.project-menu + (:require + [app.main.data.dashboard :as dd] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.components.context-menu :refer [context-menu]] + [app.util.i18n :as i18n :refer [tr]] + [app.util.router :as rt] + [rumext.alpha :as mf])) + +(mf/defc project-menu + [{:keys [project show? on-edit on-menu-close top left] :as props}] + (assert (some? project) "missing `project` prop") + (assert (boolean? show?) "missing `show?` prop") + (assert (fn? on-edit) "missing `on-edit` prop") + (assert (fn? on-menu-close) "missing `on-menu-close` prop") + (let [top (or top 0) + left (or left 0) + + delete-fn + (mf/use-callback + (mf/deps project) + (fn [event] + (st/emit! (dd/delete-project project) + (rt/nav :dashboard-projects {:team-id (:team-id project)})))) + + on-delete + (mf/use-callback + (mf/deps project) + (st/emitf (modal/show + {:type :confirm + :title (tr "modals.delete-project-confirm.title") + :message (tr "modals.delete-project-confirm.message") + :accept-label (tr "modals.delete-project-confirm.accept") + :on-accept delete-fn})))] + + [:& context-menu {:on-close on-menu-close + :show show? + :fixed? (or (not= top 0) (not= left 0)) + :top top + :left left + :options [[(tr "labels.rename") on-edit] + [(tr "labels.delete") on-delete]]}])) + diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 4aa913ea2a..6d1a9799c6 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -65,6 +65,7 @@ (mf/deps project) (st/emitf (rt/nav :dashboard-files {:team-id (:team-id project) :project-id (:id project)}))) + toggle-pin (mf/use-callback (mf/deps project) @@ -88,7 +89,6 @@ params {:project-id (:id project)}] (st/emit! (dd/create-file (with-meta params mdata))))))] - [:div.dashboard-project-row {:class (when first? "first")} [:div.project (when-not (:is-default project) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index f5d98b0311..84111389b7 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -24,6 +24,7 @@ [app.main.ui.components.forms :as fm] [app.main.ui.dashboard.comments :refer [comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.project-menu :refer [project-menu]] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] [app.util.avatars :as avatars] @@ -42,10 +43,12 @@ (mf/defc sidebar-project [{:keys [item selected?] :as props}] - (let [dstate (mf/deref refs/dashboard-local) - edit-id (:project-for-edit dstate) + (let [dstate (mf/deref refs/dashboard-local) + edit-id (:project-for-edit dstate) - edition? (mf/use-state (= (:id item) edit-id)) + local (mf/use-state {:menu-open false + :menu-pos nil + :edition? (= (:id item) edit-id)}) on-click (mf/use-callback @@ -54,23 +57,41 @@ (st/emit! (rt/nav :dashboard-files {:team-id (:team-id item) :project-id (:id item)})))) - on-dbl-click - (mf/use-callback #(reset! edition? true)) + on-menu-click + (mf/use-callback (fn [event] + (let [position (dom/get-client-position event)] + (dom/prevent-default event) + (swap! local assoc :menu-open true + :menu-pos position)))) + + on-menu-close + (mf/use-callback #(swap! local assoc :menu-open false)) + + on-edit-open + (mf/use-callback #(swap! local assoc :edition? true)) on-edit (mf/use-callback (mf/deps item) (fn [name] (st/emit! (dd/rename-project (assoc item :name name))) - (reset! edition? false)))] + (swap! local assoc :edition? false)))] - [:li {:on-click on-click - :on-double-click on-dbl-click - :class (when selected? "current")} - (if @edition? - [:& inline-edition {:content (:name item) - :on-end on-edit}] - [:span.element-title (:name item)])])) + [:* + [:li {:on-click on-click + :on-double-click on-edit-open + :on-context-menu on-menu-click + :class (when selected? "current")} + (if (:edition? @local) + [:& inline-edition {:content (:name item) + :on-end on-edit}] + [:span.element-title (:name item)])] + [:& project-menu {:project item + :show? (:menu-open @local) + :left (:x (:menu-pos @local)) + :top (:y (:menu-pos @local)) + :on-edit on-edit-open + :on-menu-close on-menu-close}]])) (mf/defc sidebar-search [{:keys [search-term team-id locale] :as props}] diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 502e88be82..214018a820 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -249,6 +249,9 @@ (let [class-list (.-classList ^js node)] (.contains ^js class-list class-name))) +(defn child? [node1 node2] + (.contains ^js node2 ^js node1)) + (defn get-user-agent [] (.-userAgent globals/navigator))