ContextMenu.purs 4.32 KB
Newer Older
1
-- | The ContextMenu component renders a generic context menu
James Laver's avatar
James Laver committed
2
module Gargantext.Components.ContextMenu.ContextMenu where
3
  -- (MenuProps, Action(..), separator) where
4 5

import Prelude hiding (div)
6 7 8 9 10 11 12 13 14 15 16
import Data.Maybe ( Maybe(..) )
import Data.Nullable ( Nullable, null, toMaybe )
import Data.Tuple.Nested ( (/\) )
import Data.Traversable ( traverse_ )
import DOM.Simple as DOM
import DOM.Simple.Event as DE
import DOM.Simple.EventListener ( Callback, callback )
import DOM.Simple.Element as Element
import DOM.Simple.Window ( window )
import DOM.Simple.Document ( document )
import DOM.Simple.Types ( DOMRect )
17
import Effect (Effect)
18
import FFI.Simple ((..))
James Laver's avatar
James Laver committed
19
import Reactix as R
20
import Reactix.DOM.HTML as HTML
James Laver's avatar
James Laver committed
21

22
import Gargantext.Utils.Reactix as R2
James Laver's avatar
James Laver committed
23

24 25
thisModule = "Gargantext.Components.ContextMenu.ContextMenu"

26 27 28
type Props t = (
    x :: Number
  , y :: Number
29
  , onClose :: Effect Unit
30
  )
James Laver's avatar
James Laver committed
31

32
contextMenu :: forall t. R2.Component (Props t)
33 34 35
contextMenu = R.createElement contextMenuCpt

contextMenuCpt :: forall t. R.Component (Props t)
36
contextMenuCpt = R.hooksComponentWithModule thisModule "contextMenu" cpt
James Laver's avatar
James Laver committed
37
  where
38
    cpt menu@{ x, y, onClose } children = do
James Laver's avatar
James Laver committed
39
      host <- R2.getPortalHost
40
      root <- R.useRef null
James Laver's avatar
James Laver committed
41 42
      rect /\ setRect <- R.useState $ \_ -> Nothing
      R.useLayoutEffect1 (R.readRef root) $ do
43
        traverse_
James Laver's avatar
James Laver committed
44
          (\r -> setRect (\_ -> Just (Element.boundingRect r)))
45
          (toMaybe $ R.readRef root)
James Laver's avatar
James Laver committed
46
        pure $ pure unit
47
      R.useLayoutEffect2 root rect (contextMenuEffect onClose root)
48 49 50 51 52 53 54 55
      let cs = [
            HTML.div { className: "popover-content" }
            [ HTML.div { className: "panel panel-default" }
              [ HTML.ul { className: "list-group" }
                children
              ]
            ]
      ]
56
      pure $ R.createPortal [ elems root menu rect $ cs ] host
57 58
    elems ref menu (Just rect) = HTML.div
        { ref
59
        , key: "context-menu"
60 61 62 63 64 65
        , className: "context-menu"
        , style: position menu rect
        , data: {toggle: "popover", placement: "right"}
        }
    elems ref _ _ = HTML.div
        { ref
66
        , key: "context-menu"
67 68 69
        , className: "context-menu"
        , data: {toggle: "popover", placement: "right"}
        }
70 71

contextMenuEffect
72
  :: forall t.
73
     Effect Unit
74
  -> R.Ref (Nullable DOM.Element)
James Laver's avatar
James Laver committed
75
  -> Effect (Effect Unit)
76
contextMenuEffect onClose rootRef =
77 78
  case R.readNullableRef rootRef of
    Just root -> do
79 80
      let onClick = documentClickHandler onClose root
      let onScroll = documentScrollHandler onClose
81 82
      DOM.addEventListener document "click" onClick
      DOM.addEventListener document "scroll" onScroll
James Laver's avatar
James Laver committed
83
      pure $ do
84 85
        DOM.removeEventListener document "click" onClick
        DOM.removeEventListener document "scroll" onScroll
James Laver's avatar
James Laver committed
86
    Nothing -> pure R.nothing
87

88 89
documentClickHandler :: Effect Unit -> DOM.Element -> Callback DE.MouseEvent
documentClickHandler onClose menu =
90
  R2.named "hideMenuOnClickOutside" $ callback $ \e ->
91
    when (Element.contains menu (DE.target e)) onClose
92

93 94 95
documentScrollHandler :: Effect Unit -> Callback DE.MouseEvent
documentScrollHandler onClose =
  R2.named "hideMenuOnScroll" $ callback $ \e -> onClose
96 97 98 99 100 101 102 103 104

position :: forall t. Record (Props t) -> DOMRect -> { left :: Number, top :: Number }
position mouse {width: menuWidth, height: menuHeight} = {left, top}
  where left = if isRight then mouse.x else mouse.x - menuWidth
        top = if isAbove then mouse.y else mouse.y - menuHeight
        isRight = screenWidth - mouse.x > menuWidth -- is there enough space to show above
        isAbove = screenHeight - mouse.y > menuHeight -- is there enough space to show to the right?
        screenWidth = window .. "innerWidth"
        screenHeight = window .. "innerHeight"
James Laver's avatar
James Laver committed
105 106

contextMenuItem :: Array R.Element -> R.Element
107
contextMenuItem = R.createElement contextMenuItemCpt {}
James Laver's avatar
James Laver committed
108

109
contextMenuItemCpt :: R.Component ()
110
contextMenuItemCpt = R.hooksComponentWithModule thisModule "contextMenuItem" cpt
James Laver's avatar
James Laver committed
111
  where
112
    cpt _props children = pure $ HTML.li { className: "context-menu-item" } children
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129

-- -- CSS Classes

-- menuClass :: String
-- menuClass = "context-menu"

-- menuShownClass :: String
-- menuShownClass = "context-menu-shown"

-- menuHiddenClass :: String
-- menuHiddenClass = "context-menu-hidden"

-- itemClass :: String
-- itemClass = "context-menu-item"

-- separatorClass :: String
-- separatorClass = "context-menu-item"
130