2{-# LANGUAGE ConstraintKinds #-}
3{-# LANGUAGE FlexibleContexts #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE RecursiveDo #-}
10{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE TemplateHaskell #-}
12{-# LANGUAGE TypeFamilies #-}
13{-# LANGUAGE UndecidableInstances #-}
14module Obelisk.Frontend
18 , runFrontendWithConfigsAndCurrentRoute
22 , module Obelisk.Frontend.Cookie
25import Prelude hiding ((.))
27import Control.Category
29import Control.Monad.Fix
30import Control.Monad.IO.Class
31import Control.Monad.Primitive
32import Control.Monad.Reader
33import Control.Monad.Ref
34import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
35import Data.ByteString (ByteString)
36import Data.Foldable (for_)
37import Data.Functor.Sum
39import Data.Maybe (catMaybes)
40import Data.Monoid ((<>))
41import Data.Text (Text)
42import qualified GHCJS.DOM as DOM
43import qualified GHCJS.DOM.Types as DOM
44import qualified GHCJS.DOM.History as DOM
45import qualified GHCJS.DOM.Window as DOM
46import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
47import GHCJS.DOM (currentDocument)
48import GHCJS.DOM.Document (getHead)
49import GHCJS.DOM.Node (Node, removeChild_)
50import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
51import GHCJS.DOM.ParentNode (querySelectorAll)
52import Obelisk.Frontend.Cookie
53import Obelisk.Route.Frontend
55import Reflex.Host.Class
57import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
58import qualified Obelisk.ExecutableConfig.Lookup as Lookup
65type ObeliskWidget js t route m =
69 , MonadSample t (Performable m)
70 , MonadReflexCreateTrigger t m
77 , MonadRef (Performable m)
78 , Ref (Performable m) ~ Ref IO
79 , MonadFix (Performable m)
82 , PrebuildAgnostic t route m
83 , PrebuildAgnostic t route (Client m)
88type PrebuildAgnostic t route m =
93 , HasConfigs (Performable m)
96data Frontend route = Frontend
97 { _frontend_head :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
98 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
101baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
102baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
104removeHTMLConfigs :: JSM ()
105removeHTMLConfigs = void $ runMaybeT $ do
106 doc <- MaybeT currentDocument
107 hd <- MaybeT $ getHead doc
108 es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109 for_ es $ removeChild_ hd
111-- | Collect all nodes in the node list.
113-- TODO: this and the version in exe-config/ghcjs/lookup should be
114-- upstreamed to jsaddle.
115nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
118 -- Warning! len is unsigned. If the NodeList is empty, we must avoid
119 -- accidentally traversing over [0..maxBound::Word]
120 nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
121 pure $ catMaybes nodes
123setInitialRoute :: Bool -> JSM ()
124setInitialRoute useHash = do
125 traceM "setInitialRoute"
126 window <- DOM.currentWindowUnchecked
127 initialLocation <- DOM.getLocation window
128 initialUri <- getLocationUri initialLocation
129 history <- DOM.getHistory window
130 DOM.replaceState history jsNull ("" :: Text) $ Just $
131 show $ setAdaptedUriPath useHash "/" initialUri
133data FrontendMode = FrontendMode
134 { _frontendMode_hydrate :: Bool
135 -- ^ There is already a rendering of the DOM in place; hydrate it rather
136 -- than building new DOM
137 , _frontendMode_adjustRoute :: Bool
138 -- ^ The page can't use regular routes, so encode routes into the hash
142-- | Run the frontend, setting the initial route to "/" on platforms where no
143-- route exists ambiently in the context (e.g. anything but web).
144-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
146 :: forall backendRoute route
147 . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
148 -> Frontend (R route)
150runFrontend validFullEncoder frontend = do
151 let mode = FrontendMode
152 { _frontendMode_hydrate =
158 , _frontendMode_adjustRoute =
165 configs <- liftIO Lookup.getConfigs
166 when (_frontendMode_hydrate mode) removeHTMLConfigs
167 -- There's no fundamental reason that adjustRoute needs to control setting the
168 -- initial route and *also* the useHash parameter; that's why these are
169 -- separate here. However, currently, they are always the same.
170 when (_frontendMode_adjustRoute mode) $ do
171 setInitialRoute $ _frontendMode_adjustRoute mode
172 runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
174runFrontendWithConfigsAndCurrentRoute
175 :: forall backendRoute route
177 -> Map Text ByteString
178 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
179 -> Frontend (R route)
181runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
182 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
184 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
185 Right x -> Identity x
186 w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
187 , Ref (Performable m) ~ Ref IO
189 , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
190 , MonadHold DomTimeline m
192 , MonadRef (Performable m)
193 , MonadReflexCreateTrigger DomTimeline m
194 , PerformEvent DomTimeline m
195 , PostBuild DomTimeline m
197 , MonadSample DomTimeline (Performable m)
199 , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
200 , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
201 , MonadFix (Performable m)
203 , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
204 , MonadIO (Performable m)
206 => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
207 -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
208 -> FloatingWidget () ()
209 w appendHead appendBody = do
210 rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
211 (switchover'', fire) <- newTriggerEvent
212 mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
213 -- The order here is important - baseTag has to be before headWidget!
215 _frontend_head frontend
216 mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
217 _frontend_body frontend
218 switchover' <- case _frontendMode_hydrate mode of
219 True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
220 False -> getPostBuild
221 performEvent_ $ liftIO (fire ()) <$ switchover'
224 if _frontendMode_hydrate mode
225 then runHydrationWidgetWithHeadAndBody (pure ()) w
226 else runImmediateWidgetWithHeadAndBody w
231 , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
233 => Map Text ByteString
241renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
242 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
243 html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
247 injectExecutableConfigs configs
248 _frontend_head frontend
251 _frontend_body frontend
253 return $ "<!DOCTYPE html>" <> html
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list