diff --git a/cabal.project b/cabal.project index 0c878afa20afaa7b2401df855520c9693f5d3b41..8af4ed50f4ab6cdda8775106e219bf13c534fa39 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,6 @@ packages: core , router , widgets - , tests , examples , isreal diff --git a/docs/default.nix b/docs/default.nix index e7645bbd389ab37b3244b27ceb3d3dde8b3fc540..8d116c52407640c7beab5071a932c4b7503b1062 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -3,7 +3,7 @@ let theme = fetchurl { url = https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable; - sha256 = "10r2mqd74ddc14qz01m2n80446m6f9ybr098g1sfhifk15y622mn"; + sha256 = "11nd3nn7bpphz9fzli15xp2aq3pbhnsc28ksv1g4w15awrmsw2h9"; }; diff --git a/examples/package.yaml b/examples/package.yaml index 5a503e1d2f406565213b590f7561863c88501702..b5aa4be74d33a090863348ac0e973ca193055837 100644 --- a/examples/package.yaml +++ b/examples/package.yaml @@ -275,6 +275,7 @@ executables: - wai - wai-app-static - warp + - generic-monoid - Shpadoinkle - Shpadoinkle-html diff --git a/html/Shpadoinkle/Html/Utils.hs b/html/Shpadoinkle/Html/Utils.hs index f23ec7ed380dad3b3c748dd92410bae4c437227b..e8dc85aea9cc48cafde4ec668b02ba34008ce0b5 100644 --- a/html/Shpadoinkle/Html/Utils.hs +++ b/html/Shpadoinkle/Html/Utils.hs @@ -8,17 +8,19 @@ module Shpadoinkle.Html.Utils where -import Control.Monad (forM_) -import Data.Text (Text) -import GHCJS.DOM (currentDocumentUnchecked) -import GHCJS.DOM.Document as Doc (createElement, createTextNode, - getBodyUnsafe, getHeadUnsafe, - setTitle) -import GHCJS.DOM.Element (setAttribute, setInnerHTML) -import GHCJS.DOM.Node (appendChild) -import GHCJS.DOM.Types (ToJSString, liftJSM, toJSVal) +import Control.Monad (forM_) +import Data.Text (Text) +import GHCJS.DOM (currentDocumentUnchecked) +import GHCJS.DOM.Document as Doc (createElement, + createTextNode, + getBodyUnsafe, + getHeadUnsafe, setTitle) +import GHCJS.DOM.Element (setAttribute, setInnerHTML) +import GHCJS.DOM.Node (appendChild) +import GHCJS.DOM.NonElementParentNode (getElementById) +import GHCJS.DOM.Types (ToJSString, liftJSM, toJSVal) -import Shpadoinkle (JSM, MonadJSM, RawNode (RawNode)) +import Shpadoinkle (MonadJSM, RawNode (RawNode)) default (Text) @@ -62,8 +64,8 @@ getBody = do liftJSM $ RawNode <$> toJSVal body -addMeta :: [(Text, Text)] -> JSM () -addMeta ps = do +addMeta :: MonadJSM m => [(Text, Text)] -> m () +addMeta ps = liftJSM $ do doc <- currentDocumentUnchecked tag <- createElement doc ("meta" :: Text) forM_ ps $ uncurry (setAttribute tag) @@ -71,8 +73,17 @@ addMeta ps = do () <$ appendChild headRaw tag -addScriptSrc :: Text -> JSM () -addScriptSrc src = do +createDivWithId :: MonadJSM m => Text -> m () +createDivWithId did = liftJSM $ do + doc <- currentDocumentUnchecked + tag <- createElement doc ("div" :: Text) + setAttribute tag "id" did + body <- Doc.getHeadUnsafe doc + () <$ appendChild body tag + + +addScriptSrc :: MonadJSM m => Text -> m () +addScriptSrc src = liftJSM $ do doc <- currentDocumentUnchecked tag <- createElement doc ("script" :: Text) setAttribute tag ("src" :: Text) src @@ -80,8 +91,8 @@ addScriptSrc src = do () <$ appendChild headRaw tag -addScriptText :: Text -> JSM () -addScriptText js = do +addScriptText :: MonadJSM m => Text -> m () +addScriptText js = liftJSM $ do doc <- currentDocumentUnchecked tag <- createElement doc ("script" :: Text) setAttribute tag ("type" :: Text) ("text/javascript" :: Text) @@ -91,6 +102,12 @@ addScriptText js = do () <$ appendChild headRaw tag +getById :: MonadJSM m => Text -> m RawNode +getById did = liftJSM $ do + doc <- currentDocumentUnchecked + fmap RawNode . toJSVal =<< getElementById doc did + + treatEmpty :: Foldable f => Functor f => a -> (f a -> a) -> (b -> a) -> f b -> a treatEmpty zero plural singular xs = if Prelude.null xs then zero else plural $ singular <$> xs diff --git a/html/Shpadoinkle/WebWorker.hs b/html/Shpadoinkle/WebWorker.hs new file mode 100644 index 0000000000000000000000000000000000000000..d01e57c2bb87689421b5233df49fb6f4f6195984 --- /dev/null +++ b/html/Shpadoinkle/WebWorker.hs @@ -0,0 +1,78 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + + +module Shpadoinkle.WebWorker where + + +import Control.Monad (void) +import Data.Text +import GHCJS.DOM +import Language.Javascript.JSaddle +import Text.RawString.QQ + + +newtype Worker = Worker { unWorker :: JSVal } + deriving (ToJSVal) + + +createWorkerJS :: Text +createWorkerJS = [r|createWorker = function (workerUrl) { + var worker = null; + try { + worker = new Worker(workerUrl); + } catch (e) { + try { + var blob; + try { + blob = new Blob(["importScripts('" + workerUrl + "');"], { "type": 'application/javascript' }); + } catch (e1) { + var blobBuilder = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)(); + blobBuilder.append("importScripts('" + workerUrl + "');"); + blob = blobBuilder.getBlob('application/javascript'); + } + var url = window.URL || window.webkitURL; + var blobUrl = url.createObjectURL(blob); + worker = new Worker(blobUrl); + } catch (e2) { + //if it still fails, there is nothing much we can do + } + } + return worker; +}|] + + +createWorker :: MonadJSM m => Text -> m Worker +createWorker url = liftJSM $ do + _ <- eval createWorkerJS + w <- toJSVal =<< currentWindowUnchecked + u <- toJSVal url + Worker <$> (w # ("createWorker" :: Text) $ [u]) + + +postMessage :: ToJSVal a => MonadJSM m => Worker -> a -> m () +postMessage (Worker worker) msg = liftJSM $ do + v <- toJSVal msg + () <$ (worker # ("postMessage" :: Text) $ [v]) + + +postMessage' :: ToJSVal a => MonadJSM m => a -> m () +postMessage' msg = liftJSM $ do + self <- jsg ("self" :: Text) + m <- toJSVal msg + () <$ (self # ("postMessage" :: Text) $ m) + + +hackWindow :: MonadJSM m => m () +hackWindow = void . liftJSM $ eval ("window = self" :: Text) + + +onMessage :: ToJSVal mailbox => FromJSVal message => MonadJSM m => mailbox -> (Maybe message -> JSM ()) -> m () +onMessage mailbox f = liftJSM $ do + box <- toJSVal mailbox + (box <# ("onmessage" :: Text)) =<< toJSVal (fun (\_ _ -> \case + [v] -> f =<< fromJSVal =<< (v ! ("data" :: Text)) + _ -> return ())) + diff --git a/html/package.yaml b/html/package.yaml index b2109096503e5772c87c6341e55313f011b8421e..3a3fd1ea8112fd37e4c975581efae4338559898a 100644 --- a/html/package.yaml +++ b/html/package.yaml @@ -42,6 +42,7 @@ dependencies: - unliftio - ghcjs-dom - jsaddle >= 0.9.7 && < 0.20 + - raw-strings-qq - Shpadoinkle @@ -67,6 +68,7 @@ library: - Shpadoinkle.Html.LocalStorage - Shpadoinkle.Html.TH - Shpadoinkle.Html.TH.CSS + - Shpadoinkle.WebWorker - Shpadoinkle.Keyboard source-dirs: . diff --git a/router/Shpadoinkle/Router.hs b/router/Shpadoinkle/Router.hs index 9d612dc34c1cb058b176afeeb3df3e59cfad32bd..6b10bded29c5826e76b12c64c83724eabd847bb3 100644 --- a/router/Shpadoinkle/Router.hs +++ b/router/Shpadoinkle/Router.hs @@ -34,7 +34,7 @@ module Shpadoinkle.Router ( -- * Types , Redirect(..), Router(..), View, HTML -- * Shpadoinkle with SPA - , fullPageSPAC, fullPageSPA + , fullPageSPAC, fullPageSPA, fullPageSPA' -- * Navigation , navigate -- * Rehydration @@ -88,7 +88,8 @@ import Servant.Links (Link, URI (..), linkURI, import System.IO.Unsafe (unsafePerformIO) import UnliftIO.Concurrent (MVar, forkIO, newEmptyMVar, putMVar, takeMVar) -import UnliftIO.STM (TVar, newTVarIO) +import UnliftIO.STM (TVar, atomically, newTVarIO, + writeTVar) import Web.HttpApiData (parseQueryParamMaybe, parseUrlPieceMaybe) @@ -245,6 +246,48 @@ fullPageSPA :: forall layout b a r m fullPageSPA a b c v g s = fullPageSPAC @layout a b c v g (fmap (pur . const) . s) +-- | This method wraps @shpadoinkle@ providing for a convenient entrypoint +-- for single page applications. It wires together your normal @shpadoinkle@ +-- app components with a function to respond to route changes, and the route mapping +-- itself. +{-# ANN fullPageSPA' ("HLint: ignore Reduce duplication" :: String) #-} +fullPageSPA' :: forall layout b a r m + . HasRouter layout + => Backend b m a + => Monad (b m) + => Eq a + => Functor m + => (m ~> JSM) + -- ^ how do we get to JSM? + -> (TVar a -> b m ~> m) + -- ^ what backend are we running? + -> TVar a + -- ^ where do we store the state? + -> (r -> m a) + -- ^ what is the initial state? + -> (a -> Html (b m) a) + -- ^ how should the html look? + -> b m RawNode + -- ^ where do we render? + -> (r -> m (Continuation m a)) + -- ^ listen for route changes + -> layout :>> r + -- ^ how shall we relate urls to routes? + -> JSM () +fullPageSPA' toJSM backend model i' view getStage onRoute routes = do + let router = route @layout @r routes + window <- currentWindowUnchecked + getRoute window router $ \case + Nothing -> return () + Just r -> do + i <- toJSM $ i' r + atomically $ writeTVar model i + _ <- listenStateChange router $ writeUpdate model . kleisli . const + . (fmap (hoist toJSM) . toJSM) . onRoute + shpadoinkle toJSM backend i model view getStage + syncPoint + + -- | ?foo=bar&baz=qux -> [("foo","bar"),("baz","qux")] parseQuery :: Text -> [(Text,Text)] parseQuery = (=<<) toKVs . T.splitOn "&" . T.drop 1