{-# 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 -- | Returns the full URI-decoded URL of the current window location. getLocationUrl :: (MonadJSM m) => m Text getLocationUrl = withLocation (Location.getHref >=> decodeURIText) -- | Returns the host of the current window location getLocationHost :: (MonadJSM m) => m Text getLocationHost = withLocation Location.getHost -- | Returns the protocol/scheme (e.g. @http:@ or @https:@) of the current window location getLocationProtocol :: (MonadJSM m) => m Text getLocationProtocol = withLocation Location.getProtocol -- | Returns the URI-decoded location after the host and port; i.e. returns the path, query, and fragment of the location. 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) -- | Returns the URI-decoded path of the current window location. getLocationPath :: (MonadJSM m) => m Text getLocationPath = withLocation (Location.getPathname >=> decodeURIText) -- | Returns the URI-decoded fragment/hash of the current window location. getLocationFragment :: (MonadJSM m) => m Text getLocationFragment = withLocation (Location.getHash >=> decodeURIText) -- | Decodes a URI with JavaScript's @decodeURI@ function. -- -- FIXME: @decodeURI@ will throw when URI is malformed 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 -- | Builds a Dynamic carrying the current window location. browserHistoryWith :: (MonadJSM m, TriggerEvent t m, MonadHold t m) => (forall jsm. MonadJSM jsm => Location -> jsm a) -- ^ A function to encode the window location in a more useful form (e.g. @getLocationAfterHost@). -> 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 --TODO: Pending https://github.com/haskell/network-uri/issues/39, ensure that --we're handling escaping of URIs correctly data HistoryItem = HistoryItem { _historyItem_state :: SerializedScriptValue , _historyItem_uri :: URI -- ^ NOTE: All URIs in this module are assumed to be already percent-escaped } data HistoryStateUpdate = HistoryStateUpdate { _historyStateUpdate_state :: SerializedScriptValue , _historyStateUpdate_title :: Text , _historyStateUpdate_uri :: Maybe URI -- ^ If Just, update the URI; otherwise leave it unchanged -- NOTE: All URIs in this module are assumed to be already percent-escaped } 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 "" -- Username and password don't seem to be available in most browsers <$> 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] --TODO: Handle title setting better manageHistory' :: (MonadFix m, MonadJSM m, TriggerEvent t m, MonadHold t m, PerformEvent t m, MonadJSM (Performable m)) => Event t () -- ^ Don't do anything until this event has fired -> 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) -- Accumulate the events before switchover (_, 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] --TODO: Handle title setting better