;; 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.components.context-menu (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.main.refs :as refs] [app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [goog.object :as gobj] [rumext.v2 :as mf])) (mf/defc context-menu {::mf/wrap-props false} [props] (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") (assert (boolean? (gobj/get props "show")) "missing `show` prop") (assert (vector? (gobj/get props "options")) "missing `options` prop") (let [open? (gobj/get props "show") on-close (gobj/get props "on-close") options (gobj/get props "options") is-selectable (gobj/get props "selectable") selected (gobj/get props "selected") top (gobj/get props "top" 0) left (gobj/get props "left" 0) fixed? (gobj/get props "fixed?" false) min-width? (gobj/get props "min-width?" false) route (mf/deref refs/route) in-dashboard? (= :dashboard-projects (:name (:data route))) local (mf/use-state {:offset-y 0 :offset-x 0 :levels nil}) on-local-close (mf/use-callback (fn [] (swap! local assoc :levels [{:parent-option nil :options options}]) (on-close))) check-menu-offscreen (mf/use-callback (mf/deps top (:offset-y @local) left (:offset-x @local)) (fn [node] (when (some? node) (let [bounding_rect (dom/get-bounding-rect node) window_size (dom/get-window-size) {node-height :height node-width :width} bounding_rect {window-height :height window-width :width} window_size target-offset-y (if (> (+ top node-height) window-height) (- node-height) 0) target-offset-x (if (> (+ left node-width) window-width) (- node-width) 0)] (when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local))) (swap! local assoc :offset-y target-offset-y :offset-x target-offset-x)))))) enter-submenu (mf/use-callback (mf/deps options) (fn [option-name sub-options] (fn [event] (dom/stop-propagation event) (swap! local update :levels conj {:parent-option option-name :options sub-options})))) exit-submenu (mf/use-callback (fn [event] (dom/stop-propagation event) (swap! local update :levels pop))) props (obj/merge props #js {:on-close on-local-close})] (mf/use-effect (mf/deps options) #(swap! local assoc :levels [{:parent-option nil :options options}])) (when (and open? (some? (:levels @local))) [:> dropdown' props [:div.context-menu {:class (dom/classnames :is-open open? :fixed fixed? :is-selectable is-selectable) :style {:top (+ top (:offset-y @local)) :left (+ left (:offset-x @local))}} (let [level (-> @local :levels peek)] [:ul.context-menu-items {:class (dom/classnames :min-width min-width?) :ref check-menu-offscreen} (when-let [parent-option (:parent-option level)] [:* [:li.context-menu-item [:a.context-menu-action.submenu-back {:data-no-close true :on-click exit-submenu} [:span i/arrow-slide] parent-option]] [:li.separator]]) (for [[index [option-name option-handler sub-options data-test]] (d/enumerate (:options level))] (when option-name (if (= option-name :separator) [:li.separator {:key (dm/str "context-item-" index)}] [:li.context-menu-item {:class (dom/classnames :is-selected (and selected (= option-name selected))) :key (dm/str "context-item-" index)} (if-not sub-options [:a.context-menu-action {:on-click #(do (dom/stop-propagation %) (on-close) (option-handler %)) :data-test data-test} (if (and in-dashboard? (= option-name "Default")) (tr "dashboard.default-team-name") option-name)] [:a.context-menu-action.submenu {:data-no-close true :on-click (enter-submenu option-name sub-options) :data-test data-test} option-name [:span i/arrow-slide]])])))])]])))