mirror of
https://github.com/penpot/penpot.git
synced 2025-05-06 16:35:54 +02:00
117 lines
4.4 KiB
Clojure
117 lines
4.4 KiB
Clojure
;; 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 app.main.ui.components.context-menu
|
|
(:require
|
|
[rumext.alpha :as mf]
|
|
[goog.object :as gobj]
|
|
[app.main.ui.components.dropdown :refer [dropdown']]
|
|
[app.main.ui.icons :as i]
|
|
[app.common.uuid :as uuid]
|
|
[app.util.data :refer [classnames]]
|
|
[app.util.dom :as dom]
|
|
[app.util.object :as obj]))
|
|
|
|
(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)
|
|
|
|
local (mf/use-state {:offset 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 @local))
|
|
(fn [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)
|
|
(- node-height)
|
|
0)]
|
|
|
|
(if (not= target-offset (:offset @local))
|
|
(swap! local assoc :offset target-offset))))))
|
|
|
|
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 (classnames :is-open open?
|
|
:fixed fixed?
|
|
:is-selectable is-selectable)
|
|
:style {:top (+ top (:offset @local))
|
|
:left left}}
|
|
(let [level (-> @local :levels peek)]
|
|
[:ul.context-menu-items {: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 [[option-name option-handler sub-options] (:options level)]
|
|
(when option-name
|
|
(if (= option-name :separator)
|
|
[:li.separator]
|
|
[:li.context-menu-item
|
|
{:class (classnames :is-selected (and selected
|
|
(= option-name selected)))
|
|
:key option-name}
|
|
(if-not sub-options
|
|
[:a.context-menu-action {:on-click option-handler}
|
|
option-name]
|
|
[:a.context-menu-action.submenu
|
|
{:data-no-close true
|
|
:on-click (enter-submenu option-name sub-options)}
|
|
option-name
|
|
[:span i/arrow-slide]])])))])]])))
|