;; 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/. ;; ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.context-menu "A workspace specific context menu (mouse right click)." (:require-macros [app.main.style :refer [css]]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.common.types.page :as ctp] [app.main.data.events :as ev] [app.main.data.modal :as modal] [app.main.data.shortcuts :as scd] [app.main.data.workspace :as dw] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.shape-icon-refactor :as sic] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] [okulary.core :as l] [rumext.v2 :as mf])) (def menu-ref (l/derived :context-menu refs/workspace-local)) (defn- prevent-default [event] (dom/prevent-default event) (dom/stop-propagation event)) (mf/defc menu-entry [{:keys [title shortcut on-click on-pointer-enter on-pointer-leave on-unmount children selected? icon] :as props}] (let [submenu-ref (mf/use-ref nil) hovering? (mf/use-ref false) new-css-system (mf/use-ctx ctx/new-css-system) on-pointer-enter (mf/use-callback (fn [] (mf/set-ref-val! hovering? true) (let [submenu-node (mf/ref-val submenu-ref)] (when (some? submenu-node) (dom/set-css-property! submenu-node "display" "block"))) (when on-pointer-enter (on-pointer-enter)))) on-pointer-leave (mf/use-callback (fn [] (mf/set-ref-val! hovering? false) (let [submenu-node (mf/ref-val submenu-ref)] (when (some? submenu-node) (timers/schedule 200 #(when-not (mf/ref-val hovering?) (dom/set-css-property! submenu-node "display" "none"))))) (when on-pointer-leave (on-pointer-leave)))) set-dom-node (mf/use-callback (fn [dom] (let [submenu-node (mf/ref-val submenu-ref)] (when (and (some? dom) (some? submenu-node)) (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] (mf/use-effect (mf/deps on-unmount) (constantly on-unmount)) (if icon [:li {:class (if new-css-system (dom/classnames (css :icon-menu-item) true) (dom/classnames :icon-menu-item true)) :ref set-dom-node :on-click on-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} [:span {:class (if new-css-system (dom/classnames (css :icon-wrapper) true) (dom/classnames :icon-wrapper true))} (if selected? [:span {:class (if new-css-system (dom/classnames (css :selected-icon) true) (dom/classnames :selected-icon true))} (if new-css-system i/tick-refactor i/tick)] [:span {:class (if new-css-system (dom/classnames (css :selected-icon) true) (dom/classnames :selected-icon true))}]) [:span {:class (if new-css-system (dom/classnames (css :shape-icon) true) (dom/classnames :shape-icon true))} icon]] [:span {:class (if new-css-system (dom/classnames (css :title) true) (dom/classnames :title true))} title]] [:li {:class (dom/classnames (css :context-menu-item) new-css-system) :ref set-dom-node :on-click on-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} [:span {:class (if new-css-system (dom/classnames (css :title) true) (dom/classnames :title true))} title] (when shortcut [:span {:class (if new-css-system (dom/classnames (css :shortcut) true) (dom/classnames :shortcut true))} (if new-css-system (for [sc (scd/split-sc shortcut)] [:span {:class (dom/classnames (css :shortcut-key) true)} sc]) (or shortcut ""))]) (when (> (count children) 1) (if new-css-system [:span {:class (dom/classnames (css :submenu-icon) true)} i/arrow-refactor] [:span.submenu-icon i/arrow-slide])) (when (> (count children) 1) [:ul {:class (if new-css-system (dom/classnames (css :workspace-context-submenu) true) (dom/classnames :workspace-context-menu true)) :ref submenu-ref :style {:display "none" :left 250} :on-context-menu prevent-default} children])]))) (mf/defc menu-separator [] (let [new-css-system (mf/use-ctx ctx/new-css-system)] [:li {:class (if new-css-system (dom/classnames (css :separator) true) (dom/classnames :separator true))}])) (mf/defc context-menu-edit [_] (let [do-copy #(st/emit! (dw/copy-selected)) do-cut #(st/emit! (dw/copy-selected) (dw/delete-selected)) do-paste #(st/emit! dw/paste) do-duplicate #(st/emit! (dw/duplicate-selected true))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) :on-click do-copy}] [:& menu-entry {:title (tr "workspace.shape.menu.cut") :shortcut (sc/get-tooltip :cut) :on-click do-cut}] [:& menu-entry {:title (tr "workspace.shape.menu.paste") :shortcut (sc/get-tooltip :paste) :on-click do-paste}] [:& menu-entry {:title (tr "workspace.shape.menu.duplicate") :shortcut (sc/get-tooltip :duplicate) :on-click do-duplicate}] [:& menu-separator]])) (mf/defc context-menu-layer-position [{:keys [shapes]}] (let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up))) do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top))) do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down))) do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom))) select-shapes (fn [id] #(st/emit! (dws/select-shape id))) on-pointer-enter (fn [id] #(st/emit! (dw/highlight-shape id))) on-pointer-leave (fn [id] #(st/emit! (dw/dehighlight-shape id))) on-unmount (fn [id] #(st/emit! (dw/dehighlight-shape id))) ;; NOTE: we use deref instead of mf/deref on objects because ;; we really don't want rerender on object changes hover-ids (deref refs/current-hover-ids) objects (deref refs/workspace-page-objects) hover-objs (into [] (keep (d/getf objects)) hover-ids)] [:* (when (> (count hover-objs) 1) [:& menu-entry {:title (tr "workspace.shape.menu.select-layer")} (for [object hover-objs] [:& menu-entry {:title (:name object) :key (dm/str (:id object)) :selected? (some #(= object %) shapes) :on-click (select-shapes (:id object)) :on-pointer-enter (on-pointer-enter (:id object)) :on-pointer-leave (on-pointer-leave (:id object)) :on-unmount (on-unmount (:id object)) :icon (sic/element-icon-refactor {:shape object})}])]) [:& menu-entry {:title (tr "workspace.shape.menu.forward") :shortcut (sc/get-tooltip :bring-forward) :on-click do-bring-forward}] [:& menu-entry {:title (tr "workspace.shape.menu.front") :shortcut (sc/get-tooltip :bring-front) :on-click do-bring-to-front}] [:& menu-entry {:title (tr "workspace.shape.menu.backward") :shortcut (sc/get-tooltip :bring-backward) :on-click do-send-backward}] [:& menu-entry {:title (tr "workspace.shape.menu.back") :shortcut (sc/get-tooltip :bring-back) :on-click do-send-to-back}] [:& menu-separator]])) (mf/defc context-menu-flip [] (let [do-flip-vertical #(st/emit! (dw/flip-vertical-selected)) do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.flip-vertical") :shortcut (sc/get-tooltip :flip-vertical) :on-click do-flip-vertical}] [:& menu-entry {:title (tr "workspace.shape.menu.flip-horizontal") :shortcut (sc/get-tooltip :flip-horizontal) :on-click do-flip-horizontal}] [:& menu-separator]])) (mf/defc context-menu-thumbnail [{:keys [shapes]}] (let [single? (= (count shapes) 1) has-frame? (some cph/frame-shape? shapes) do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))] (when (and single? has-frame?) [:* (if (every? :use-for-thumbnail shapes) [:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-remove") :on-click do-toggle-thumbnail}] [:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-set") :shortcut (sc/get-tooltip :thumbnail-set) :on-click do-toggle-thumbnail}]) [:& menu-separator]]))) (mf/defc context-menu-group [{:keys [shapes]}] (let [multiple? (> (count shapes) 1) single? (= (count shapes) 1) do-create-artboard-from-selection #(st/emit! (dwsh/create-artboard-from-selection)) has-frame? (->> shapes (d/seek cph/frame-shape?)) has-group? (->> shapes (d/seek cph/group-shape?)) has-bool? (->> shapes (d/seek cph/bool-shape?)) has-mask? (->> shapes (d/seek :masked-group)) is-group? (and single? has-group?) is-bool? (and single? has-bool?) do-create-group #(st/emit! dw/group-selected) do-mask-group #(st/emit! dw/mask-group) do-remove-group #(st/emit! dw/ungroup-selected) do-unmask-group #(st/emit! dw/unmask-group)] [:* (when (or has-bool? has-group? has-mask? has-frame?) [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") :shortcut (sc/get-tooltip :ungroup) :on-click do-remove-group}]) [:& menu-entry {:title (tr "workspace.shape.menu.group") :shortcut (sc/get-tooltip :group) :on-click do-create-group}] (when (or multiple? (and is-group? (not has-mask?)) is-bool?) [:& menu-entry {:title (tr "workspace.shape.menu.mask") :shortcut (sc/get-tooltip :mask) :on-click do-mask-group}]) (when has-mask? [:& menu-entry {:title (tr "workspace.shape.menu.unmask") :shortcut (sc/get-tooltip :unmask) :on-click do-unmask-group}]) [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection") :shortcut (sc/get-tooltip :artboard-selection) :on-click do-create-artboard-from-selection}] [:& menu-separator]])) (mf/defc context-focus-mode-menu [{:keys []}] (let [focus (mf/deref refs/workspace-focus-selected) do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] [:& menu-entry {:title (if (empty? focus) (tr "workspace.focus.focus-on") (tr "workspace.focus.focus-off")) :shortcut (sc/get-tooltip :toggle-focus-mode) :on-click do-toggle-focus-mode}])) (mf/defc context-menu-path [{:keys [shapes disable-flatten? disable-booleans?]}] (let [multiple? (> (count shapes) 1) single? (= (count shapes) 1) has-group? (->> shapes (d/seek cph/group-shape?)) has-bool? (->> shapes (d/seek cph/bool-shape?)) has-frame? (->> shapes (d/seek cph/frame-shape?)) has-path? (->> shapes (d/seek cph/path-shape?)) is-group? (and single? has-group?) is-bool? (and single? has-bool?) is-frame? (and single? has-frame?) do-start-editing (fn [] (timers/schedule #(st/emit! (dw/start-editing-selected)))) do-transform-to-path #(st/emit! (dw/convert-selected-to-path)) make-do-bool (fn [bool-type] #(cond multiple? (st/emit! (dw/create-bool bool-type)) is-group? (st/emit! (dw/group-to-bool (-> shapes first :id) bool-type)) is-bool? (st/emit! (dw/change-bool-type (-> shapes first :id) bool-type))))] [:* (when (and single? (not is-frame?)) [:& menu-entry {:title (tr "workspace.shape.menu.edit") :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) (when-not (or disable-flatten? has-frame? has-path?) [:& menu-entry {:title (tr "workspace.shape.menu.transform-to-path") :on-click do-transform-to-path}]) (when (and (not disable-booleans?) (or multiple? (and single? (or is-group? is-bool?)))) [:& menu-entry {:title (tr "workspace.shape.menu.path")} [:& menu-entry {:title (tr "workspace.shape.menu.union") :shortcut (sc/get-tooltip :bool-union) :on-click (make-do-bool :union)}] [:& menu-entry {:title (tr "workspace.shape.menu.difference") :shortcut (sc/get-tooltip :bool-difference) :on-click (make-do-bool :difference)}] [:& menu-entry {:title (tr "workspace.shape.menu.intersection") :shortcut (sc/get-tooltip :bool-intersection) :on-click (make-do-bool :intersection)}] [:& menu-entry {:title (tr "workspace.shape.menu.exclude") :shortcut (sc/get-tooltip :bool-exclude) :on-click (make-do-bool :exclude)}] (when (and single? is-bool? (not disable-flatten?)) [:* [:& menu-separator] [:& menu-entry {:title (tr "workspace.shape.menu.flatten") :on-click do-transform-to-path}]])])])) (mf/defc context-menu-layer-options [{:keys [shapes]}] (let [ids (mapv :id shapes) do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false})) do-hide-shape #(st/emit! (dw/update-shape-flags ids {:hidden true})) do-lock-shape #(st/emit! (dw/update-shape-flags ids {:blocked true})) do-unlock-shape #(st/emit! (dw/update-shape-flags ids {:blocked false}))] [:* (if (every? :hidden shapes) [:& menu-entry {:title (tr "workspace.shape.menu.show") :shortcut (sc/get-tooltip :toggle-visibility) :on-click do-show-shape}] [:& menu-entry {:title (tr "workspace.shape.menu.hide") :shortcut (sc/get-tooltip :toggle-visibility) :on-click do-hide-shape}]) (if (every? :blocked shapes) [:& menu-entry {:title (tr "workspace.shape.menu.unlock") :shortcut (sc/get-tooltip :toggle-lock) :on-click do-unlock-shape}] [:& menu-entry {:title (tr "workspace.shape.menu.lock") :shortcut (sc/get-tooltip :toggle-lock) :on-click do-lock-shape}])])) (mf/defc context-menu-prototype [{:keys [shapes]}] (let [options (mf/deref refs/workspace-page-options) options-mode (mf/deref refs/options-mode-global) do-add-flow #(st/emit! (dwi/add-flow-selected-frame)) do-remove-flow #(st/emit! (dwi/remove-flow (:id %))) flows (:flows options) prototype? (= options-mode :prototype) single? (= (count shapes) 1) has-frame? (->> shapes (d/seek cph/frame-shape?)) is-frame? (and single? has-frame?)] (when (and prototype? is-frame?) (let [flow (ctp/get-frame-flow flows (-> shapes first :id))] (if (some? flow) [:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start") :on-click (do-remove-flow flow)}] [:& menu-entry {:title (tr "workspace.shape.menu.flow-start") :on-click do-add-flow}]))))) (mf/defc context-menu-flex [{:keys [shapes]}] (let [single? (= (count shapes) 1) has-frame? (->> shapes (d/seek cph/frame-shape?)) is-frame? (and single? has-frame?) is-flex-container? (and is-frame? (= :flex (:layout (first shapes)))) ids (->> shapes (map :id)) add-layout (fn [type] (st/emit! (if is-frame? (dwsl/create-layout-from-id ids type true) (dwsl/create-layout-from-selection type)))) remove-flex #(st/emit! (dwsl/remove-layout ids))] [:* (when (not is-flex-container?) [:div [:& menu-separator] [:& menu-entry {:title (tr "workspace.shape.menu.add-flex") :shortcut (sc/get-tooltip :toggle-layout-flex) :on-click #(add-layout :flex)}] [:& menu-entry {:title (tr "workspace.shape.menu.add-grid") :shortcut (sc/get-tooltip :toggle-layout-grid) :on-click #(add-layout :grid)}]]) (when is-flex-container? [:div [:& menu-separator] [:& menu-entry {:title (tr "workspace.shape.menu.remove-flex") :shortcut (sc/get-tooltip :toggle-layout-flex) :on-click remove-flex}]])])) (mf/defc context-menu-component [{:keys [shapes]}] (let [single? (= (count shapes) 1) components-v2 (features/use-feature :components-v2) has-component? (some true? (map #(ctk/instance-head? %) shapes)) is-component? (and single? (-> shapes first :component-id some?)) in-copy-not-root? (some true? (map #(ctk/in-component-copy-not-root? %) shapes)) objects (deref refs/workspace-page-objects) touched? (and single? (cph/component-touched? objects (:id (first shapes)))) can-update-main? (or (not components-v2) touched?) first-shape (first shapes) {:keys [id component-id component-file]} first-shape main-instance? (ctk/main-instance? first-shape) component-shapes (filter #(ctk/instance-head? %) shapes) touched-components (filter #(cph/component-touched? objects (:id %)) component-shapes) can-update-main-of-any? (or (not components-v2) (not-empty touched-components)) current-file-id (mf/use-ctx ctx/current-file-id) local-component? (= component-file current-file-id) remote-components (filter #(not= (:component-file %) current-file-id) component-shapes) workspace-data (deref refs/workspace-data) workspace-libraries (deref refs/workspace-libraries) component (if local-component? (ctkl/get-component workspace-data component-id) (ctf/get-component workspace-libraries component-file component-id)) is-dangling? (nil? component) lacks-annotation? (nil? (:annotation component)) lib-exists? (and (not local-component?) (some? (get workspace-libraries component-file))) do-add-component #(st/emit! (dwl/add-component)) do-add-multiple-components #(st/emit! (dwl/add-multiple-components)) do-detach-component #(st/emit! (dwl/detach-component id)) do-detach-component-in-bulk #(st/emit! dwl/detach-selected-components) do-reset-component #(st/emit! (dwl/reset-component id)) do-reset-component-in-bulk (fn [] (let [undo-id (js/Symbol)] (st/emit! (dwu/start-undo-transaction undo-id)) (apply st/emit! (map #(dwl/reset-component (:id %)) touched-components)) (st/emit! (dwu/commit-undo-transaction undo-id)))) do-show-component #(st/emit! (dw/go-to-component component-id)) do-show-in-assets #(st/emit! (if components-v2 (dw/show-component-in-assets component-id) (dw/go-to-component component-id))) create-annotation #(when components-v2 (st/emit! (dw/set-annotations-id-for-create (:id first-shape)))) do-navigate-component-file #(st/emit! (dwl/nav-to-component-file component-file)) do-update-component #(st/emit! (dwl/update-component-sync id component-file)) do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk touched-components component-file)) do-restore-component #(st/emit! (dwl/restore-component component-file component-id) (dw/go-to-main-instance nil component-id)) do-update-remote-component #(st/emit! (modal/show {:type :confirm :message "" :title (tr "modals.update-remote-component.message") :hint (tr "modals.update-remote-component.hint") :cancel-label (tr "modals.update-remote-component.cancel") :accept-label (tr "modals.update-remote-component.accept") :accept-style :primary :on-accept do-update-component})) do-update-in-bulk (fn [] (if (empty? remote-components) (do-update-component-in-bulk) #(st/emit! (modal/show {:type :confirm :message "" :title (tr "modals.update-remote-component-in-bulk.message") :hint (tr "modals.update-remote-component-in-bulk.hint") :items remote-components :cancel-label (tr "modals.update-remote-component.cancel") :accept-label (tr "modals.update-remote-component.accept") :accept-style :primary :on-accept do-update-component-in-bulk}))))] [:* [:* (when (or (not in-copy-not-root?) (and has-component? (not single?))) [:& menu-separator]) (when-not in-copy-not-root? [:& menu-entry {:title (tr "workspace.shape.menu.create-component") :shortcut (sc/get-tooltip :create-component) :on-click do-add-component}]) (when-not (or single? in-copy-not-root?) [:& menu-entry {:title (tr "workspace.shape.menu.create-multiple-components") :on-click do-add-multiple-components}]) (when (and has-component? (not single?)) [:& menu-entry {:title (tr "workspace.shape.menu.detach-instances-in-bulk") :shortcut (sc/get-tooltip :detach-component) :on-click do-detach-component-in-bulk}]) (when (and has-component? can-update-main-of-any? (not single?)) [:* [:& menu-entry {:title (tr "workspace.shape.menu.update-components-in-bulk") :on-click do-update-in-bulk}] [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component-in-bulk}]] )] (when is-component? ;; WARNING: this menu is the same as the context menu at the sidebar. ;; If you change it, you must change equally the file ;; app/main/ui/workspace/sidebar/options/menus/component.cljs [:* [:& menu-separator] (if main-instance? [:* [:& menu-entry {:title (tr "workspace.shape.menu.show-in-assets") :on-click do-show-in-assets}] (when (and components-v2 local-component? lacks-annotation?) [:& menu-entry {:title (tr "workspace.shape.menu.create-annotation") :on-click create-annotation}])] (if local-component? (if is-dangling? [:* [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :shortcut (sc/get-tooltip :detach-component) :on-click do-detach-component}] (when can-update-main? [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}]) (when components-v2 [:& menu-entry {:title (tr "workspace.shape.menu.restore-main") :on-click do-restore-component}])] [:* [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :shortcut (sc/get-tooltip :detach-component) :on-click do-detach-component}] (when can-update-main? [:* [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}] [:& menu-entry {:title (tr "workspace.shape.menu.update-main") :on-click do-update-component}]]) [:& menu-entry {:title (tr "workspace.shape.menu.show-main") :on-click do-show-component}]]) (if is-dangling? [:* [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :shortcut (sc/get-tooltip :detach-component) :on-click do-detach-component}] (when can-update-main? [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}]) (when (and components-v2 lib-exists?) [:& menu-entry {:title (tr "workspace.shape.menu.restore-main") :on-click do-restore-component}])] [:* [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :shortcut (sc/get-tooltip :detach-component) :on-click do-detach-component}] (when can-update-main? [:* [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}] [:& menu-entry {:title (tr "workspace.shape.menu.update-main") :on-click do-update-remote-component}]]) [:& menu-entry {:title (tr "workspace.shape.menu.go-main") :on-click do-navigate-component-file}]])))]) [:& menu-separator]])) (mf/defc context-menu-delete [] (let [do-delete #(st/emit! (dw/delete-selected))] [:& menu-entry {:title (tr "workspace.shape.menu.delete") :shortcut (sc/get-tooltip :delete) :on-click do-delete}])) (mf/defc shape-context-menu {::mf/wrap [mf/memo]} [{:keys [mdata] :as props}] (let [{:keys [disable-booleans? disable-flatten?]} mdata shapes (mf/deref refs/selected-objects) props #js {:shapes shapes :disable-booleans? disable-booleans? :disable-flatten? disable-flatten?}] (when-not (empty? shapes) [:* [:> context-menu-edit props] [:> context-menu-layer-position props] [:> context-menu-flip props] [:> context-menu-thumbnail props] [:> context-menu-group props] [:> context-focus-mode-menu props] [:> context-menu-path props] [:> context-menu-layer-options props] [:> context-menu-prototype props] [:> context-menu-flex props] [:> context-menu-component props] [:> context-menu-delete props]]))) (mf/defc page-item-context-menu [{:keys [mdata] :as props}] (let [page (:page mdata) deletable? (:deletable? mdata) id (:id page) delete-fn #(st/emit! (dw/delete-page id)) do-delete #(st/emit! (modal/show {:type :confirm :title (tr "modals.delete-page.title") :message (tr "modals.delete-page.body") :on-accept delete-fn})) do-duplicate #(st/emit! (dw/duplicate-page id)) do-rename #(st/emit! (dw/start-rename-page-item id))] [:* (when deletable? [:& menu-entry {:title (tr "workspace.assets.delete") :on-click do-delete}]) [:& menu-entry {:title (tr "workspace.assets.rename") :on-click do-rename}] [:& menu-entry {:title (tr "workspace.assets.duplicate") :on-click do-duplicate}]])) (mf/defc viewport-context-menu [] (let [focus (mf/deref refs/workspace-focus-selected) do-paste #(st/emit! dw/paste) do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui) (vary-meta assoc ::ev/origin "workspace-context-menu"))) do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.paste") :shortcut (sc/get-tooltip :paste) :on-click do-paste}] [:& menu-entry {:title (tr "workspace.shape.menu.hide-ui") :shortcut (sc/get-tooltip :hide-ui) :on-click do-hide-ui}] (when (d/not-empty? focus) [:& menu-entry {:title (tr "workspace.focus.focus-off") :shortcut (sc/get-tooltip :toggle-focus-mode) :on-click do-toggle-focus-mode}])])) (mf/defc context-menu [] (let [mdata (mf/deref menu-ref) top (- (get-in mdata [:position :y]) 20) left (get-in mdata [:position :x]) dropdown-ref (mf/use-ref) new-css-system (mf/use-ctx ctx/new-css-system)] (mf/use-effect (mf/deps mdata) #(let [dropdown (mf/ref-val dropdown-ref)] (when dropdown (let [bounding-rect (dom/get-bounding-rect dropdown) window-size (dom/get-window-size) delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0) delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0) new-style (str "top: " (- top delta-y) "px; " "left: " (- left delta-x) "px;")] (when (or (> delta-x 0) (> delta-y 0)) (.setAttribute ^js dropdown "style" new-style)))))) [:& dropdown {:show (boolean mdata) :on-close #(st/emit! dw/hide-context-menu)} [:ul {:class (if new-css-system (dom/classnames (css :workspace-context-menu) true) (dom/classnames :workspace-context-menu true)) :ref dropdown-ref :style {:top top :left left} :on-context-menu prevent-default} (case (:kind mdata) :shape [:& shape-context-menu {:mdata mdata}] :page [:& page-item-context-menu {:mdata mdata}] [:& viewport-context-menu {:mdata mdata}])]]))