module Graphics.UI.Threepenny.Core (
-- * Guide
-- $guide
-- * Server
-- $server
Config(..), startGUI,
loadFile, loadDirectory,
-- * Browser Window
Window, title, cookies, getRequestLocation,
-- * DOM elements
-- | Create and manipulate DOM elements.
Element, mkElement, getWindow, delete, (#+), string,
getHead, getBody,
children, text, html, attr, style, value,
getValuesList,
getElementsByTagName, getElementByTagName, getElementsById, getElementById,
-- * Layout
-- | Combinators for quickly creating layouts.
-- They can be adjusted with CSS later on.
grid, row, column,
-- * Events
-- | For a list of predefined events, see "Graphics.UI.Threepenny.Events".
EventData(..), domEvent, on,
module Control.Event,
-- * Attributes
-- | For a list of predefined attributes, see "Graphics.UI.Threepenny.Attributes".
(#), (#.), element,
Attr, WriteAttr, ReadAttr, ReadWriteAttr(..),
set, get, mkReadWriteAttr, mkWriteAttr, mkReadAttr,
-- * JavaScript FFI
-- | Direct interface to JavaScript in the browser window.
debug, clear,
ToJS, FFI, ffi, JSFunction, runFunction, callFunction,
callDeferredFunction, atomic,
-- * Internal and oddball functions
updateElement, manifestElement, audioPlay, fromProp,
) where
import Data.IORef
import Data.Maybe (listToMaybe)
import Data.Functor
import Data.String (fromString)
import Control.Concurrent.MVar
import Control.Event
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader as Reader
import Network.URI
import Text.JSON
import qualified Graphics.UI.Threepenny.Internal.Core as Core
import Graphics.UI.Threepenny.Internal.Core
(getRequestLocation,
ToJS, FFI, ffi, JSFunction,
debug, clear, callFunction, runFunction, callDeferredFunction, atomic, )
import qualified Graphics.UI.Threepenny.Internal.Types as Core
import Graphics.UI.Threepenny.Internal.Types (Window, Config, EventData)
{-----------------------------------------------------------------------------
Guide
------------------------------------------------------------------------------}
{- $guide
Threepenny runs a small web server that displays the user interface
as a web page to any browser that connects to it.
To start the web server, use the 'startGUI' function.
Creating of DOM elements is easy,
the '(#+)' combinator allows a style similar to HTML combinator libraries.
Existing DOM elements can be accessed much in the same way they are
accessed from JavaScript; they can be searched, updated, moved and
inspected. Events can be bound to DOM elements and handled.
Applications written in Threepenny are multithreaded. Each client (user)
has a separate thread which runs with no awareness of the asynchronous
protocol below. Each session should only be accessed from one
thread. There is not yet any clever architecture for accessing the
(single threaded) web browser from multi-threaded Haskell. That's
my recommendation. You can choose to ignore it, but don't blame me
when you run an element search and you get a click event as a
result.
This project was originally called Ji.
-}
{-----------------------------------------------------------------------------
Server
------------------------------------------------------------------------------}
{- $server
To display the user interface, you have to start a server using 'startGUI'.
Then, visit the URL in your browser
(assuming that you have set the port number to @tpPort=10000@
in the server configuration).
-}
-- | Start server for GUI sessions.
startGUI
:: Config -- ^ Server configuration.
-> (Window -> IO ()) -- ^ Action to run whenever a client browser connects.
-> IO ()
startGUI config handler =
Core.serve config $ \w -> handler w >> Core.handleEvents w
-- | Make a local file available as a relative URI.
loadFile
:: Window -- ^ Browser window
-> String -- ^ MIME type
-> FilePath -- ^ Local path to the file
-> IO String -- ^ Generated URI
loadFile w mime path = Core.loadFile w (fromString mime) path
-- | Make a local directory available as a relative URI.
loadDirectory :: Window -> FilePath -> IO String
loadDirectory = Core.loadDirectory
{-----------------------------------------------------------------------------
Browser window
------------------------------------------------------------------------------}
-- | Title of the client window.
title :: WriteAttr Window String
title = mkWriteAttr Core.setTitle
-- | Cookies on the client.
cookies :: ReadAttr Window [(String,String)]
cookies = mkReadAttr Core.getRequestCookies
{-----------------------------------------------------------------------------
Elements
------------------------------------------------------------------------------}
type Value = String
-- | Reference to an element in the DOM of the client window.
newtype Element = Element (MVar Elem)
data Elem
= Alive Core.Element -- element exists in a window
| Limbo Value (Window -> IO Core.Element) -- still needs to be created
-- Turn a live reference into an 'Element'.
-- Note that multiple MVars may now point to the same live reference,
-- but this is ok since live references never change.
fromAlive :: Core.Element -> IO Element
fromAlive e = Element <$> newMVar (Alive e)
-- Update an element that may be in Limbo.
updateElement :: (Core.Element -> IO ()) -> Element -> IO ()
updateElement f (Element me) = do
e <- takeMVar me
case e of
Alive e -> do -- update immediately
f e
putMVar me $ Alive e
Limbo value create -> -- update on creation
putMVar me $ Limbo value $ \w -> create w >>= \e -> f e >> return e
-- Given a browser window, make sure that the element exists there.
-- TODO: 1. Throw exception if the element exists in another window.
-- 2. Don't throw exception, but move the element across windows.
manifestElement :: Window -> Element -> IO Core.Element
manifestElement w (Element me) = do
e1 <- takeMVar me
e2 <- case e1 of
Alive e -> return e
Limbo v create -> do { e2 <- create w; Core.setAttr "value" v e2; return e2 }
putMVar me $ Alive e2
return e2
-- Append a child element to a parent element. Non-blocking.
appendTo
:: Element -- ^ Parent.
-> Element -- ^ Child.
-> IO ()
appendTo parent child = do
flip updateElement parent $ \x -> do
y <- manifestElement (Core.getWindow x) child
Core.appendElementTo x y
-- | Make a new DOM element.
mkElement
:: String -- ^ Tag name
-> IO Element
mkElement tag = Element <$> newMVar (Limbo "" $ \w -> Core.newElement w tag)
-- | Retreive the browser 'Window' in which the element resides.
--
-- Note that elements do not reside in any browser window when they are first created.
-- To move the element to a particular browser window,
-- you have to append it to a parent, for instance with the `(#+)` operator.
--
-- WARNING: The ability to move elements from one browser window to another
-- is currently not implemented yet.
getWindow :: Element -> IO (Maybe Window)
getWindow (Element ref) = do
e1 <- readMVar ref
return $ case e1 of
Alive e -> Just $ Core.getWindow e
Limbo _ _ -> Nothing
-- | Delete the given element.
delete :: Element -> IO ()
delete = updateElement (Core.delete)
-- | Append DOM elements as children to a given element.
(#+) :: IO Element -> [IO Element] -> IO Element
(#+) mx mys = do
x <- mx
ys <- sequence mys
mapM_ (appendTo x) ys
return x
-- | Child elements of a given element.
children :: WriteAttr Element [Element]
children = mkWriteAttr set
where
set xs x = do
updateElement Core.emptyEl x
mapM_ (appendTo x) xs
-- | Child elements of a given element as a HTML string.
html :: WriteAttr Element String
html = mkWriteAttr (updateElement . Core.setHtml)
-- | HTML attributes of an element.
attr :: String -> WriteAttr Element String
attr name = mkWriteAttr (updateElement . Core.setAttr name)
-- | Set CSS style of an Element
style :: WriteAttr Element [(String,String)]
style = mkWriteAttr (updateElement . Core.setStyle)
-- | Value attribute of an element.
-- Particularly relevant for control widgets like 'input'.
value :: Attr Element String
value = mkReadWriteAttr get set
where
get (Element ref) = getValue =<< readMVar ref
set v (Element ref) = updateMVar (setValue v) ref
getValue (Limbo v _) = return v
getValue (Alive e ) = Core.getValue e
setValue v (Limbo _ f) = return $ Limbo v f
setValue v (Alive e ) = Core.setAttr "value" v e >> return (Alive e)
updateMVar f ref = do
x <- takeMVar ref
y <- f x
putMVar ref y
-- | Get values from inputs. Blocks. This is faster than many 'getValue' invocations.
getValuesList
:: [Element] -- ^ A list of elements to get the values of.
-> IO [String] -- ^ The list of plain text values.
getValuesList = mapM (get value)
-- TODO: improve this to use Core.getValuesList
-- | Text content of an element.
text :: WriteAttr Element String
text = mkWriteAttr (updateElement . Core.setText)
-- | Make a @span@ element with a given text content.
string :: String -> IO Element
string s = mkElement "span" # set text s
-- | Get the head of the page.
getHead :: Window -> IO Element
getHead = fromAlive <=< Core.getHead
-- | Get the body of the page.
getBody :: Window -> IO Element
getBody = fromAlive <=< Core.getBody
-- | Get an element by its tag name. Blocks.
getElementByTagName
:: Window -- ^ Browser window
-> String -- ^ The tag name.
-> IO (Maybe Element) -- ^ An element (if any) with that tag name.
getElementByTagName window = liftM listToMaybe . getElementsByTagName window
-- | Get all elements of the given tag name. Blocks.
getElementsByTagName
:: Window -- ^ Browser window
-> String -- ^ The tag name.
-> IO [Element] -- ^ All elements with that tag name.
getElementsByTagName window name =
mapM fromAlive =<< Core.getElementsByTagName window name
-- | Get an element by a particular ID. Blocks.
getElementById
:: Window -- ^ Browser window
-> String -- ^ The ID string.
-> IO (Maybe Element) -- ^ Element (if any) with given ID.
getElementById window id = listToMaybe `fmap` getElementsById window [id]
-- | Get a list of elements by particular IDs. Blocks.
getElementsById
:: Window -- ^ Browser window
-> [String] -- ^ The ID string.
-> IO [Element] -- ^ Elements with given ID.
getElementsById window name =
mapM fromAlive =<< Core.getElementsById window name
{-----------------------------------------------------------------------------
Oddball
------------------------------------------------------------------------------}
audioPlay = updateElement Core.audioPlay
-- Turn a jQuery property @.prop()@ into an attribute.
fromProp :: String -> (JSValue -> a) -> (a -> JSValue) -> Attr Element a
fromProp name from to = mkReadWriteAttr get set
where
set x = updateElement (Core.setProp name $ to x)
get (Element ref) = do
me <- readMVar ref
case me of
Limbo _ _ -> error "'checked' attribute: element must be in a browser window"
Alive e -> from <$> Core.getProp name e
{-----------------------------------------------------------------------------
Layout
------------------------------------------------------------------------------}
-- | Align given elements in a row. Special case of 'grid'.
row :: [IO Element] -> IO Element
row xs = grid [xs]
-- | Align given elements in a column. Special case of 'grid'.
column :: [IO Element] -> IO Element
column = grid . map (:[])
-- | Align given elements in a rectangular grid.
--
-- Layout is achieved by using the CSS @display:table@ property.
-- The following element tree will be generated
--
-- >
-- >
-- >
-- > ...
-- >
-- > ...
-- >
--
-- You can customatize the actual layout by assigning an @id@ to the element
-- and changing the @.table@, @.table-row@ and @table-column@
-- classes in a custom CSS file.
grid :: [[IO Element]] -> IO Element
grid mrows = do
rows0 <- mapM (sequence) mrows
rows <- forM rows0 $ \row0 -> do
row <- forM row0 $ \entry ->
wrap "table-cell" [entry]
wrap "table-row" row
wrap "table" rows
where
wrap c xs = mkElement "div" # set (attr "class") c #+ map element xs
{-----------------------------------------------------------------------------
Events
------------------------------------------------------------------------------}
-- | Obtain DOM event for a given element.
domEvent
:: String
-- ^ Event name. A full list can be found at
-- .
-- Note that the @on@-prefix is not included,
-- the name is @click@ and so on.
-> Element -- ^ Element where the event is to occur.
-> Event EventData
domEvent name element = Control.Event.Event $ \handler -> do
ref <- newIORef $ return ()
let
-- register handler and remember unregister function
register' = flip updateElement element $ \e -> do
unregister <- register (Core.bind name e) handler
writeIORef ref unregister
-- update element to unregister the event handler
unregister' = flip updateElement element $ \_ -> do
join $ readIORef ref
register'
return unregister'
-- | Convenience function to register 'Event's for 'Element's.
--
-- Example usage.
--
-- > on click element $ \_ -> ...
on :: (element -> Event a) -> element -> (a -> IO void) -> IO ()
on f x h = register (f x) (void . h) >> return ()
{-----------------------------------------------------------------------------
Attributes
------------------------------------------------------------------------------}
infixl 8 #
infixl 8 #+
infixl 8 #.
-- | Reverse function application.
-- Allows convenient notation for setting properties.
--
-- Example usage.
--
-- > mkElement "div"
-- > # set style [("color","#CCAABB")]
-- > # set draggable True
-- > # set children otherElements
(#) :: a -> (a -> b) -> b
(#) = flip ($)
-- | Convenient combinator for setting the CSS class on element creation.
(#.) :: IO Element -> String -> IO Element
(#.) mx s = mx # set (attr "class") s
-- | Convience synonym for 'return' to make elements work well with 'set'.
--
-- Example usage.
--
-- > e <- mkElement "button"
-- > element e # set text "Ok"
element :: Element -> IO Element
element = return
-- | Attributes can be 'set' and 'get'.
type Attr x a = ReadWriteAttr x a a
-- | Attribute that only supports the 'get' operation.
type ReadAttr x o = ReadWriteAttr x () o
-- | Attribute that only supports the 'set' operation.
type WriteAttr x i = ReadWriteAttr x i ()
-- | Generalized attribute with different types for getting and setting.
data ReadWriteAttr x i o = ReadWriteAttr
{ get' :: x -> IO o
, set' :: i -> x -> IO ()
}
-- | Set value of an attribute in the 'IO' monad.
-- Best used in conjunction with '#'.
set :: MonadIO m => ReadWriteAttr x i o -> i -> m x -> m x
set attr i mx = do { x <- mx; liftIO (set' attr i x); return x; }
-- | Get attribute value.
get :: ReadWriteAttr x i o -> x -> IO o
get = get'
-- | Build an attribute from a getter and a setter.
mkReadWriteAttr
:: (x -> IO o) -- ^ Getter.
-> (i -> x -> IO ()) -- ^ Setter.
-> ReadWriteAttr x i o
mkReadWriteAttr get set = ReadWriteAttr { get' = get, set' = set }
-- | Build attribute from a getter.
mkReadAttr :: (x -> IO o) -> ReadAttr x o
mkReadAttr get = mkReadWriteAttr get (\_ _ -> return ())
-- | Build attribute from a setter.
mkWriteAttr :: (i -> x -> IO ()) -> WriteAttr x i
mkWriteAttr set = mkReadWriteAttr (\_ -> return ()) set