packages feed

threepenny-gui-contextmenu-0.1.0.0: src/ContextMenu.hs

-- This module could potentially be rewritten and heavily simplified once the
-- custom context menu spec in HTML 5.1 is adopted by all major browsers. At
-- time of writing the spec is only a recommendation and only implemented by
-- Mozilla Firefox.

module ContextMenu where

import           ContextMenu.Style
import           ContextMenu.Util
import           Control.Monad               (when)
import qualified Graphics.UI.Threepenny      as UI
import           Graphics.UI.Threepenny.Core

-- |A menu item is some text to be displayed and either UI actions to execute or
-- a nested menu.
data MenuItem a = MenuItem { mIText :: String, mIValue :: MenuItemValue a }
data MenuItemValue a = MenuItemActions [Action] | NestedMenu [MenuItem a]

-- |Constructor for a menu item that contains UI actions to execute.
actionMenuItem :: String -> [Action] -> MenuItem a
actionMenuItem text actions =
    MenuItem { mIText = text, mIValue = MenuItemActions actions }

-- |Constructor for a menu item that contains a nested menu.
nestedMenuItem :: String -> [MenuItem a] -> MenuItem a
nestedMenuItem text nested =
    MenuItem { mIText = text ++ "  ›", mIValue = NestedMenu nested }

-- |Creates a custom context menu and attaches it to a given element. Prevents
-- the default context menu from occuring.
contextMenu :: [MenuItem a] -> Element -> UI ()
contextMenu items sourceEl = do
    contextMenuEls <- contextMenu' items sourceEl
    element sourceEl #+ contextMenuEls
    preventDefaultContextMenu sourceEl

-- |Attaches a custom context menu to an element.
contextMenu' :: [MenuItem a] -> Element -> UI [Action]
contextMenu' items sourceEl = do
    rmTargetEl <- UI.div # set style rmTargetStyle
    let closeRmTarget = dimensions "0" "0" rmTargetEl
    (menuEl, closeMenu, closeNestedMenus) <- newMenu [closeRmTarget] items
    -- Display menu on a contextmenu event.
    on UI.contextmenu sourceEl $ \(x, y) ->
        displayAt x y menuEl >> dimensions "100vw" "100vh" rmTargetEl
    -- Hide everything on rmTarget click.
    on UI.mousedown rmTargetEl $ const $
        closeRmTarget >> closeMenu >> sequence closeNestedMenus
    -- Hide nested menus on hover over rmTarget.
    on UI.hover rmTargetEl $ const $ sequence closeNestedMenus
    return [element rmTargetEl, element menuEl]

-- |Returns a menu element, an action to close the menu and actions to close any
-- nested menus.
newMenu :: [Action] -> [MenuItem a] -> UI (Element, Action, [Action])
newMenu closeParents menuItems = do
    menuEl <- UI.li # set style menuStyle
    let closeMenu = display "none" menuEl
    -- Menu items as elements and respective list of actions to close nested
    -- menus. :: UI [(Element, [Action])]
    menuItemEls <- mapM (menuItem $ closeParents ++ [closeMenu]) menuItems
    element menuEl #+ map (element . fst) menuItemEls
        -- On hover over a menu item we want close any nested menus from
        -- *other* menu items.
    let closeOtherMenusOnHover ((el1, _), i1) xs =
          on UI.hover el1 $ const $ do
              let closeIfNotSelf ((_, closeEl2), i2) =
                    when (i1 /= i2) (sequence_ closeEl2)
              mapM closeIfNotSelf xs
    mapPairsWithIndex menuItemEls closeOtherMenusOnHover
    return (menuEl, closeMenu, concat (map snd menuItemEls))

-- |Returns a menu item element and actions to open and close it.
menuItem :: [Action] -> MenuItem a -> UI (Element, [Action])
menuItem closeAbove (MenuItem text value) = do
    menuItemEl <- UI.li # set UI.text text # set style menuItemStyle
    highlightWhileHover menuItemEl
    case value of
        MenuItemActions actions -> do
            -- On click execute the actions and close the entire menu.
            on UI.click menuItemEl $ const $ do
                sequence_ $ closeAbove ++ actions
                liftIO $ putStrLn "event clicked"
            return (menuItemEl, [])
        NestedMenu nestedMenuItems -> do
            (nestedMenuEl, closeMenu, closeNestedMenu)
                 <- newMenu closeAbove nestedMenuItems
            -- Position a nested menu relative to this menu item.
            element menuItemEl # set UI.position "relative"
            element nestedMenuEl # set UI.position "absolute" # set UI.right "0px" # set UI.top "0px"
            element menuItemEl #+ [element nestedMenuEl]
            -- On hover display the nested menu.
            on UI.hover menuItemEl $ const $ display "block" nestedMenuEl
            return (menuItemEl, [closeMenu] ++ closeNestedMenu)

-- |Highlight a given element while it is hovered over.
highlightWhileHover :: Element -> UI ()
highlightWhileHover el = whileHover el
    (element el # set style [("background-color", "#DEF"   )])
    (element el # set style [("background-color", "inherit")])

-- |CSS class used to identify elements on which to prevent a default context
-- menu from opening.
preventDefaultClass = "__prevent-default-context-menu"

-- |Prevents a default context menu opening from the given element.
preventDefaultContextMenu :: Element -> UI ()
preventDefaultContextMenu el = do
    element el # set UI.class_ preventDefaultClass
    runFunction $ ffi "$(%1).bind('contextmenu', e => e.preventDefault())"
                      ("." ++ preventDefaultClass)