{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module Reflex.Dom.Location
( browserHistoryWith
, getLocationAfterHost
, getLocationFragment
, getLocationHost
, getLocationPath
, getLocationProtocol
, getLocationUrl
, manageHistory
, manageHistory'
, HistoryCommand (..)
, HistoryStateUpdate (..)
, HistoryItem (..)
, getLocationUri
) where
import Reflex
import Reflex.Dom.Builder.Immediate (wrapDomEvent)
import Control.Lens ((^.))
import Control.Monad ((>=>))
import Control.Monad.Fix (MonadFix)
import Data.Align (align)
import Data.Monoid
import Data.Text (Text)
import Data.These (These(..))
import qualified GHCJS.DOM as DOM
import qualified GHCJS.DOM.EventM as DOM
import qualified GHCJS.DOM.Location as Location
import qualified GHCJS.DOM.History as History
import qualified GHCJS.DOM.PopStateEvent as PopStateEvent
import GHCJS.DOM.Types (Location, History, SerializedScriptValue (..), liftJSM)
import qualified GHCJS.DOM.Types as DOM
import qualified GHCJS.DOM.Window as Window
import qualified GHCJS.DOM.WindowEventHandlers as DOM
import Language.Javascript.JSaddle (FromJSString, MonadJSM, ToJSString, fromJSValUnchecked, js1, ToJSVal (..), FromJSVal (..))
import Network.URI
withLocation :: (MonadJSM m) => (Location -> m a) -> m a
withLocation f = DOM.currentWindowUnchecked >>= Window.getLocation >>= f
getLocationUrl :: (MonadJSM m) => m Text
getLocationUrl = withLocation (Location.getHref >=> decodeURIText)
getLocationHost :: (MonadJSM m) => m Text
getLocationHost = withLocation Location.getHost
getLocationProtocol :: (MonadJSM m) => m Text
getLocationProtocol = withLocation Location.getProtocol
getLocationAfterHost :: (MonadJSM m) => m Text
getLocationAfterHost = withLocation $ \loc -> do
pathname <- Location.getPathname loc
search <- Location.getSearch loc
hash <- Location.getHash loc
decodeURI (mconcat [pathname, search, hash] :: Text)
getLocationPath :: (MonadJSM m) => m Text
getLocationPath = withLocation (Location.getPathname >=> decodeURIText)
getLocationFragment :: (MonadJSM m) => m Text
getLocationFragment = withLocation (Location.getHash >=> decodeURIText)
decodeURI :: (MonadJSM m, ToJSString a, FromJSString b) => a -> m b
decodeURI input = do
window <- DOM.currentWindowUnchecked
window' <- DOM.liftJSM $ toJSVal window
DOM.liftJSM $ window' ^. js1 ("decodeURI"::Text) input >>= fromJSValUnchecked
decodeURIText :: (MonadJSM m) => Text -> m Text
decodeURIText = decodeURI
browserHistoryWith :: (MonadJSM m, TriggerEvent t m, MonadHold t m)
=> (forall jsm. MonadJSM jsm => Location -> jsm a)
-> m (Dynamic t a)
browserHistoryWith f = do
window <- DOM.currentWindowUnchecked
location <- Window.getLocation window
loc0 <- f location
locEv <- wrapDomEvent window (`DOM.on` DOM.popState) $ f location
holdDyn loc0 locEv
data HistoryItem = HistoryItem
{ _historyItem_state :: SerializedScriptValue
, _historyItem_uri :: URI
}
data HistoryStateUpdate = HistoryStateUpdate
{ _historyStateUpdate_state :: SerializedScriptValue
, _historyStateUpdate_title :: Text
, _historyStateUpdate_uri :: Maybe URI
}
data HistoryCommand
= HistoryCommand_PushState HistoryStateUpdate
| HistoryCommand_ReplaceState HistoryStateUpdate
runHistoryCommand :: MonadJSM m => History -> HistoryCommand -> m ()
runHistoryCommand history = \case
HistoryCommand_PushState su -> History.pushState history
(_historyStateUpdate_state su)
(_historyStateUpdate_title su)
(show <$> _historyStateUpdate_uri su)
HistoryCommand_ReplaceState su -> History.replaceState history
(_historyStateUpdate_state su)
(_historyStateUpdate_title su)
(show <$> _historyStateUpdate_uri su)
getLocationUriAuth :: MonadJSM m => Location -> m URIAuth
getLocationUriAuth location = URIAuth ""
<$> Location.getHostname location
<*> (appendColonIfNotEmpty <$> Location.getPort location)
where appendColonIfNotEmpty = \case
"" -> ""
x -> ":" <> x
getLocationUri :: MonadJSM m => Location -> m URI
getLocationUri location = URI
<$> Location.getProtocol location
<*> (Just <$> getLocationUriAuth location)
<*> Location.getPathname location
<*> Location.getSearch location
<*> Location.getHash location
manageHistory :: (MonadJSM m, TriggerEvent t m, MonadHold t m, PerformEvent t m, MonadJSM (Performable m)) => Event t HistoryCommand -> m (Dynamic t HistoryItem)
manageHistory runCmd = do
window <- DOM.currentWindowUnchecked
location <- Window.getLocation window
history <- Window.getHistory window
let getCurrentHistoryItem = HistoryItem
<$> History.getState history
<*> getLocationUri location
item0 <- liftJSM getCurrentHistoryItem
itemSetInternal <- performEvent $ ffor runCmd $ \cmd -> liftJSM $ do
runHistoryCommand history cmd
getCurrentHistoryItem
itemSetExternal <- wrapDomEvent window (`DOM.on` DOM.popState) $ do
e <- DOM.event
HistoryItem
<$> (SerializedScriptValue <$> PopStateEvent.getState e)
<*> getLocationUri location
holdDyn item0 $ leftmost [itemSetInternal, itemSetExternal]
manageHistory'
:: (MonadFix m, MonadJSM m, TriggerEvent t m, MonadHold t m, PerformEvent t m, MonadJSM (Performable m))
=> Event t ()
-> Event t HistoryCommand
-> m (Dynamic t HistoryItem)
manageHistory' switchover runCmd = do
window <- DOM.currentWindowUnchecked
location <- Window.getLocation window
history <- Window.getHistory window
let getCurrentHistoryItem = HistoryItem
<$> History.getState history
<*> getLocationUri location
item0 <- liftJSM getCurrentHistoryItem
itemSetExternal' <- wrapDomEvent window (`DOM.on` DOM.popState) $ do
e <- DOM.event
HistoryItem
<$> (SerializedScriptValue <$> PopStateEvent.getState e)
<*> getLocationUri location
let f :: (Bool, Maybe a) -> These a () -> (Maybe (Bool, Maybe a), Maybe a)
f (switched, acc) = \case
This change
| switched -> (Nothing, Just change)
| otherwise -> (Just (switched, Just change), Nothing)
That () -> (Just (True, Nothing), acc)
These change () -> (Just (True, Nothing), Just change)
(_, cmd') <- mapAccumMaybeB f (False, Nothing) $ align (leftmost [Left <$> runCmd, Right <$> itemSetExternal']) switchover
let (itemSetInternal', itemSetExternal) = fanEither cmd'
itemSetInternal <- performEvent $ ffor itemSetInternal' $ \cmd -> liftJSM $ do
runHistoryCommand history cmd
getCurrentHistoryItem
holdDyn item0 $ leftmost [itemSetInternal, itemSetExternal]