miso 0.21.2.0 → 1.0.0.0
raw patch · 17 files changed
+599/−651 lines, 17 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Miso.Html: data_ :: MisoString -> MisoString -> Attribute action
+ Miso.Html.Property: data_ :: MisoString -> MisoString -> Attribute action
+ Miso.Router: instance Miso.Router.HasRouter sublayout => Miso.Router.HasRouter (Servant.API.Header.Header sym x Servant.API.Sub.:> sublayout)
+ Miso.Router: type family RouteT layout a :: *
+ Miso.String: instance Miso.String.ToMisoString GHC.Types.Double
+ Miso.String: instance Miso.String.ToMisoString GHC.Types.Float
+ Miso.String: instance Miso.String.ToMisoString GHC.Types.Int
+ Miso.String: instance Miso.String.ToMisoString GHC.Types.Word
+ Miso.TypeLevel: type family ToServerRoutes (layout :: k) (wrapper :: * -> *) (action :: *) :: k
- Miso.Event.Decoder: Decoder :: Value -> Parser a -> DecodeTarget -> Decoder a
+ Miso.Event.Decoder: Decoder :: (Value -> Parser a) -> DecodeTarget -> Decoder a
- Miso.Html: [VNode] :: {vType :: Text Element type (i.e. "div", "a", "p"), vNs :: NS HTML or SVG, vProps :: Props Fields present on DOM Node, vKey :: Maybe Key Key used for child swap patch, vChildren :: Vector (VTree action) Child nodes} -> VTree action
+ Miso.Html: [VNode] :: {vType :: Text " Element type (i.e. "div", "a", "p")", vNs :: NS " HTML or SVG", vProps :: Props " Fields present on DOM Node", vKey :: Maybe Key " Key used for child swap patch", vChildren :: Vector (VTree action) " Child nodes"} -> VTree action
- Miso.Html: [VText] :: {vText :: Text TextNode content} -> VTree action
+ Miso.Html: [VText] :: {vText :: Text " TextNode content"} -> VTree action
Files
- README.md +57/−241
- frontend-src/Miso.hs +6/−2
- frontend-src/Miso/Html/Internal.hs +6/−0
- frontend-src/Miso/String.hs +1/−1
- frontend-src/Miso/Subscription/Keyboard.hs +9/−0
- ghc-src/Miso/String.hs +14/−0
- ghcjs-ffi/Miso/FFI.hs +15/−12
- jsaddle-ffi/Miso/FFI.hs +2/−2
- jsbits/delegate.js +70/−74
- jsbits/diff.js +292/−293
- jsbits/isomorphic.js +54/−20
- jsbits/util.js +2/−2
- miso.cabal +53/−1
- src/Miso/Html/Element.hs +1/−1
- src/Miso/Html/Property.hs +11/−0
- src/Miso/Router.hs +6/−1
- tests/Main.hs +0/−1
README.md view
@@ -1,5 +1,6 @@ <h1 align="center">miso</h1> <p align="center">+ <a href="https://haskell-miso.org"> <img width=10% src="https://emojipedia-us.s3.amazonaws.com/thumbs/240/apple/96/steaming-bowl_1f35c.png"> </a>@@ -27,21 +28,12 @@ </a> </p> -<p align="center">- <a href="https://saucelabs.com/u/dmjio">- <img src="https://saucelabs.com/browser-matrix/dmjio.svg" alt="Sauce Test Status"/>- </a>-</p>- **Miso** is a small "[isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/)" [Haskell](https://www.haskell.org/) front-end framework for quickly building highly interactive single-page web applications. It features a virtual-dom, diffing / patching algorithm, attribute and property normalization, event delegation, event batching, SVG, Server-sent events, Websockets, type-safe [servant](https://haskell-servant.github.io/)-style routing and an extensible Subscription-based subsystem. Inspired by [Elm](http://elm-lang.org/), [Redux](http://redux.js.org/) and [Bobril](http://github.com/bobris/bobril). **Miso** is pure by default, but side effects (like `XHR`) can be introduced into the system via the `Effect` data type. **Miso** makes heavy use of the [GHCJS](https://github.com/ghcjs/ghcjs) FFI and therefore has minimal dependencies. **Miso** can be considered a shallow [embedded domain-specific language](https://wiki.haskell.org/Embedded_domain_specific_language) for modern web programming. ## Table of Contents - [Quick Start](#quick-start) - [Begin](#begin)- - [Stack](#stack) - [Nix](#nix)- - [Cabal](#cabal)- - [GHCJSi Caveats](#ghcjsi-caveats) - [Architecture](#architecture) - [Examples](#examples) - [TodoMVC](#todomvc)@@ -49,6 +41,7 @@ - [2048](#2048) - [Snake](#snake) - [Mario](#mario)+ - [Miso Plane (Flappy Birds)](#miso-plane-flappy-birds) - [Websocket](#websocket) - [SSE](#sse) - [XHR](#xhr)@@ -74,152 +67,35 @@ - [License](#license) ## Quick start-To get started quickly building applications, we recommend using the [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`nix`](https://nixos.org/nix) package managers. Obtaining [`GHCJS`](https://github.com/ghcjs/ghcjs) is required as a prerequisite. `stack` and `nix` make this process easy, if you're using `cabal` we assume you have [obtained `GHCJS`](https://github.com/ghcjs/ghcjs#installation) by other means.--All source code depicted below for the quick start app is available [here](https://github.com/dmjio/miso/tree/master/sample-app).+To get started quickly building applications, we recommend using the [`nix`](https://nixos.org/nix) package manager with miso's binary cache provided by [`cachix`](https://haskell-miso.cachix.org/). It is possible to use [`stack`](https://docs.haskellstack.org/en/stable/README/) to build GHCJS projects, but support for procuring `GHCJS` has been removed [as of stack 2.0](https://github.com/commercialhaskell/stack/issues/4086). `nix` is used to procure a working version of `GHCJS`. If you're using `cabal` we assume you have [obtained `GHCJS`](https://github.com/ghcjs/ghcjs#installation) by other means. All source code depicted below for the quick start app is available [here](https://github.com/dmjio/miso/tree/master/sample-app). ### Begin-We recommend using `nix` when working with `miso`, but it is just as fine to use `stack`.-To build the sample-app with `nix`, execute the command below:--```bash-git clone https://github.com/dmjio/miso && cd miso/sample-app && nix-build-```--To develop with `nix` run the below command (this will put you into a shell where you can iteratively develop the project with `cabal build`).--```bash-git clone https://github.com/dmjio/miso && cd miso/sample-app && nix-shell -A env-```--For more information on using `nix` w/ `miso`, see the [`nix` section below](#nix)--To build the sample-app with `stack`, execute the command below:-```bash-git clone https://github.com/dmjio/miso && cd miso/sample-app && stack setup && stack build-```--Note: It's important to ensure that you don't have a global `cabal-install` present on your system. This could cause build problems. If you see an error like this (below), try deleting your global `cabal-install`.+To build the sample-app with `nix`, execute the commands below: ```bash-exit status: 1-stderr: solver must be one of: modular-CallStack (from HasCallStack):- error, called at libraries/Cabal/Cabal/Distribution/ReadE.hs:46:24 in Cabal-2.0.1.0:Distribution.ReadE-```---For more information on using `stack` w/ `miso`, see the [`stack` section below](#stack)---### Stack-In the `miso` repository there is a [folder named `stack`](https://github.com/dmjio/miso/tree/master/stack) with "known to work" configurations for `GHCJS`. One stack file exists for both the `7.10.3` and `8.0.1` versions of `GHCJS`. In general, we recommend developing with the `7.10.3` version since it currently supports `GHCJSi` (a REPL that connects to the browser by way of a [`nodejs`](https://nodejs.org/en/) web server using [`socket.io`](https://socket.io/)) and building with the `8.0.1` version (if possible). For more information on using `stack` with `GHCJS`, please consult the [GHCJS section of the `stack` docs](https://docs.haskellstack.org/en/stable/ghcjs/).--To begin, create the following directory layout-```bash-➜ mkdir app && touch app/{Main.hs,app.cabal,stack.yaml} && tree app-app-|-- Main.hs-|-- app.cabal-`-- stack.yaml-```--Add a `stack.yaml` file that uses a recent version of `miso`.-```bash-➜ cat app/stack.yaml-resolver: lts-6.30-compiler: ghcjs-0.2.0.9006030_ghc-7.10.3-compiler-check: match-exact--packages:- - '.'-extra-deps:- - miso-0.18.0.0--setup-info:- ghcjs:- source:- ghcjs-0.2.0.9006030_ghc-7.10.3:- url: http://ghcjs.tolysz.org/lts-6.30-9006030.tar.gz- sha1: 2371e2ffe9e8781808b7a04313e6a0065b64ee51-```--Add a `cabal` file-```bash-➜ cat app/*.cabal-name: app-version: 0.1.0.0-synopsis: First miso app-category: Web-build-type: Simple-cabal-version: >=1.10--executable app- main-is: Main.hs- build-depends: base, miso- default-language: Haskell2010-```--Add the source from [Sample Application](#sample-application) to `app/Main.hs`--Run `stack setup`. This might take a long time, since it will have to build `GHCJS`.-```-stack setup-```--Run `stack build` to get the static assets-```-stack build-```--See the result-```-open $(stack path --local-install-root)/bin/app.jsexe/index.html-```--Using GHCJSi-```-stack ghci-```--If that warns with `socket.io not found, browser session not available`, you'll need to install `socket.io`-```-npm install socket.io-```--and update your `NODE_PATH`-```-export NODE_PATH=$(pwd)/node_modules+(optional) nix-env -iA cachix -f https://cachix.org/api/v1/install+(optional) cachix use haskell-miso+git clone https://github.com/dmjio/miso+cd miso/sample-app+nix-build+open ./result/bin/app.jsexe/index.html ``` -Now you should be connected, and the app viewable in `GHCJSi` (open http://localhost:6400).-```bash-➜ stack ghci-app-0.1.0.0: initial-build-steps (exe)-Configuring GHCi with the following packages: app-GHCJSi, version 0.2.0.9006020-7.10.3: http://www.github.com/ghcjs/ghcjs/ :? for help-[1 of 1] Compiling Main ( /Users/david/Desktop/miso/sample-app/Main.hs, interpreted )-socket.io found, browser session available at http://localhost:6400-Ok, modules loaded: Main.-*Main> main-browser connected, code runs in browser from now on-```+The above commands will add miso's binary cache to your nix installation (support for both Linux and OSX).+`nix-build` will fetch the dependencies from miso's cache and build the sample application. ### Nix `Nix` is a more powerful option for building web applications with `miso` since it encompasses development workflow, configuration management, and deployment. The source code for [`haskell-miso.org`](https://github.com/dmjio/miso/tree/master/examples/haskell-miso.org) is an example of this. If unfamiliar with `nix`, we recommend [@Gabriel439](https://github.com/Gabriel439)'s ["Nix and Haskell in production"](https://github.com/Gabriel439/haskell-nix) guide. -To get started, we will use the [`cabal2nix`](https://github.com/NixOS/cabal2nix) tool to convert our `Cabal` file into a `nix` derivation (named `app.nix`). We'll then write a file named `default.nix`, which is used for building our project (via `nix-build`) and development (via `nix-shell`).- To begin, make the following directory layout: ```bash-➜ mkdir app && touch app/{Main.hs,app.cabal,default.nix,app.nix} && tree app+➜ mkdir app && touch app/{Main.hs,app.cabal,default.nix} && tree app app |-- Main.hs |-- app.cabal-|-- default.nix-`-- app.nix+`-- default.nix ``` Add a `cabal` file@@ -234,50 +110,21 @@ executable app main-is: Main.hs+ ghcjs-options:+ -dedupe build-depends: base, miso default-language: Haskell2010 ``` -Use [`cabal2nix`](https://github.com/NixOS/cabal2nix) to generate a file named `app.nix`-that looks like below.-```bash-➜ cabal2nix . --compiler ghcjs > app.nix-➜ cat app.nix-```+Write a `default.nix` (this will fetch a recent version of `miso`). `miso` will provide you with a working `nixpkgs` named `pkgs`. `callCabal2nix` will automatically produce a nix expression that builds your cabal file. ```nix-{ mkDerivation, base, miso, stdenv }:-mkDerivation {- pname = "app";- version = "0.1.0.0";- src = ./.;- isLibrary = false;- isExecutable = true;- executableHaskellDepends = [ base miso ];- description = "First miso app";- license = stdenv.lib.licenses.bsd3;-}-```--Write a `default.nix` (which calls `app.nix`), this fetches a recent version of `miso`.-```nix-{ pkgs ? import ((import <nixpkgs> {}).fetchFromGitHub {- owner = "NixOS";- repo = "nixpkgs";- rev = "a0aeb23";- sha256 = "04dgg0f2839c1kvlhc45hcksmjzr8a22q1bgfnrx71935ilxl33d";- }){}-}:-let- result = import (pkgs.fetchFromGitHub {- owner = "dmjio";- repo = "miso";- sha256 = "1l1gwzzqlvvcmg70jjrwc5ijv1vb6y5ljqkh7rxxq7hkyxpjyx9q";- rev = "95f6bc9b1ae6230b110358a82b6a573806f272c2";- }) {};-in pkgs.haskell.packages.ghcjs.callPackage ./app.nix {- miso = result.miso-ghcjs;-}+with (import (builtins.fetchTarball {+ url = "https://github.com/dmjio/miso/archive/b4c473f3ed6d6251ea7b8b489fc50076ac8d9b70.tar.gz";+ sha256 = "11rby2s0hxbl28a4fcwdm9lcbjfysv862xd6b9jy0rgl63dh51i3";+}) {});+with pkgs.haskell.packages;+ghcjs.callCabal2nix "app" ./. {} ``` Add the source from [Sample Application](#sample-application) to `app/Main.hs`@@ -292,7 +139,7 @@ open ./result/bin/app.jsexe/index.html ``` -For development with `nix`, it's important to have `cabal` present for building. This command will make it available in your `PATH`.+For development with `nix`, it can be nice to have `cabal` present for building. This command will make it available in your `PATH`. ``` nix-env -iA cabal-install -f '<nixpkgs>' ```@@ -302,37 +149,20 @@ nix-shell -A env ``` -To open `GHCJSi` (`NODE_PATH` should already be set properly)+To view the dependencies for your project, call `ghcjs-pkg list` when inside the shell. ```-$ cabal configure --ghcjs-$ cabal repl-Package has never been configured. Configuring with default flags. If this-fails, please run configure manually.-Resolving dependencies...-Configuring app-0.1.0.0...-Preprocessing executable 'app' for app-0.1.0.0...-GHCJSi, version 0.2.0-7.10.3: http://www.github.com/ghcjs/ghcjs/ :? for help-[1 of 1] Compiling Main ( Main.hs, interpreted )-Ok, modules loaded: Main.-*Main>-browser connected, code runs in browser from now on+nix-shell -A env --run 'ghcjs-pkg list' ``` -### Cabal-The latest stable version of `miso` will be available on Hackage.-To build with cabal, we assume `ghcjs` is in your `PATH` and `ghcjs-base` is present in your `ghcjs-pkg` list.-```bash-cabal sandbox init-cabal install --ghcjs-cabal build-open dist/build/app/app.jsexe/index.html+To build the project with `cabal` after entering the `nix-shell` ```+nix-shell -A env --run 'cabal configure --ghcjs && cabal build`+``` -### GHCJSi Caveats-If you run `main` in `GHCJSi`, interrupt it and then run it again, you-will end up with two copies of your app displayed above each other. As-a workaround, you can use `clearBody >> main` which will completely-clear the document body before rendering your application.+For incremental development inside of the `nix-shell` we recommend using a tool like [`entr`](http://eradman.com/entrproject/) to automatically rebuild on file changes, or roll your own solution with `inotify`.+```+ag -l | entr 'cabal build'+``` ### Architecture For constructing client and server applications, we recommend using one `cabal` file with two executable sections, where the `buildable` attribute set is contingent on the compiler. An example of this layout is [here](https://github.com/dmjio/miso/blob/master/examples/haskell-miso.org/haskell-miso.cabal#L16-L60). For more info on how to use `stack` with a `client`/`server` setup, see this [link](https://docs.haskellstack.org/en/stable/ghcjs/#project-with-both-client-and-server). For more information on how to use `nix` with a `client`/`server` setup, see the [nix scripts](https://github.com/dmjio/miso/blob/master/examples/haskell-miso.org/default.nix) for [https://haskell-miso.org](https://haskell-miso.org).@@ -354,6 +184,9 @@ ### Mario - [Link](https://mario.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/mario/Main.hs) +### Miso Plane (Flappy Birds)+ - [Link](https://miso-plane.lermex.net/) / [Source](https://github.com/Lermex/miso-plane)+ ### Websocket - [Link](https://websocket.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/websocket/Main.hs) @@ -449,39 +282,33 @@ The easiest way to build the examples is with the [`nix`](https://nixos.org/nix/) package manager ```-git clone https://github.com/dmjio/miso && cd miso && nix-build+git clone https://github.com/dmjio/miso && cd miso && nix-build -A miso-ghcjs ``` This will build all examples and documentation into a folder named `result` ```-➜ miso git:(master) ✗ tree result -d-result-|-- doc-| |-- x86_64-osx-ghc-8.0.2-| | `-- miso-0.2.0.0-| | `-- html-| | `-- src-| `-- x86_64-osx-ghcjs-0.2.0-ghc7_10_3-| `-- miso-0.2.0.0-| `-- html-| `-- src-|-- examples-| |-- mario.jsexe-| | `-- imgs-| | |-- jump-| | |-- stand-| | `-- walk-| |-- router.jsexe-| |-- simple.jsexe-| |-- tests.jsexe-| |-- todo-mvc.jsexe-| `-- websocket.jsexe+➜ miso git:(master) ✗ tree -d ./result/bin+./result/bin+|-- canvas2d.jsexe+|-- compose-update.jsexe+|-- file-reader.jsexe+|-- mario.jsexe+| `-- imgs+|-- mathml.jsexe+|-- router.jsexe+|-- simple.jsexe+|-- svg.jsexe+|-- tests.jsexe+|-- threejs.jsexe+|-- todo-mvc.jsexe+|-- websocket.jsexe+`-- xhr.jsexe ``` To see examples, we recommend hosting them with a webserver ```-cd result/examples/todo-mvc.jsexe && python -m SimpleHTTPServer+cd result/bin/todo-mvc.jsexe && nix-shell -p python --run 'python -m SimpleHTTPServer' Serving HTTP on 0.0.0.0 port 8000 ... ``` @@ -510,25 +337,14 @@ ## Pinning nixpkgs -By default `miso` uses a known-to-work, pinned version of [`nixpkgs`](https://github.com/dmjio/miso/blob/master/default.nix#L1-L6).-To override this to your system's version of `nixpkgs` write:--```-nix-build --arg nixpkgs 'import <nixpkgs> {}'-```+By default `miso` uses a known-to-work, pinned version of [`nixpkgs`](https://github.com/dmjio/miso/blob/master/nixpkgs.json). ## Binary cache -`nix` users on a Linux distro can take advantage of a [binary cache](https://hydra.dmj.io/nix-cache-info) for faster builds. To use the binary cache simply append `https://hydra.dmj.io/nix-cache-info` during all `nix-shell` and/or `nix-build` invocations.+`nix` users on a Linux or OSX distro can take advantage of a [binary cache](https://hydra.dmj.io/nix-cache-info) for faster builds. To use the binary cache follow the instructions on `https://haskell-miso.cachix.org/`. ```bash-nix-build --option extra-binary-caches https://hydra.dmj.io-```--Alternatively, add `https://hydra.dmj.io` to your list of local binary caches in `nix.conf` (usually found in `/etc/nix/nix.conf`), and it will automatically be used on all invocations of `nix-build` and/or `nix-shell`.--```-binary-caches = https://hydra.dmj.io/ https://cache.nixos.org/+cachix use haskell-miso ``` ## Benchmarks
frontend-src/Miso.hs view
@@ -34,6 +34,7 @@ import Data.IORef import Data.List import Data.Sequence ((|>))+import System.Mem.StableName import qualified Data.Sequence as S import qualified JavaScript.Object.Internal as OI @@ -102,7 +103,9 @@ let (Acc newModel effects) = foldl' (foldEffects writeEvent update) (Acc oldModel (pure ())) actions effects- when (oldModel /= newModel) $ do+ oldName <- liftIO $ oldModel `seq` makeStableName oldModel+ newName <- liftIO $ newModel `seq` makeStableName newModel+ when (oldName /= newName && oldModel /= newModel) $ do swapCallbacks newVTree <- runView (view newModel) writeEvent oldVTree <- liftIO (readIORef viewRef)@@ -122,8 +125,9 @@ common app model $ \writeEvent -> do let initialView = view model VTree (OI.Object iv) <- flip runView writeEvent initialView+ mountEl <- mountElement mountPoint -- Initial diff can be bypassed, just copy DOM into VTree- copyDOMIntoVTree iv+ copyDOMIntoVTree mountEl iv let initialVTree = VTree (OI.Object iv) -- Create virtual dom, perform initial diff liftIO (newIORef initialVTree)
frontend-src/Miso/Html/Internal.hs view
@@ -258,6 +258,9 @@ -- | @onCreated action@ is an event that gets called after the actual DOM -- element is created.+--+-- Important note: Any node that uses this event MUST have a unique @Key@,+-- otherwise the event may not be reliably called! onCreated :: action -> Attribute action onCreated action = Attribute $ \sink n -> do@@ -268,6 +271,9 @@ -- | @onDestroyed action@ is an event that gets called after the DOM element -- is removed from the DOM. The @action@ is given the DOM element that was -- removed from the DOM tree.+--+-- Important note: Any node that uses this event MUST have a unique @Key@,+-- otherwise the event may not be reliably called! onDestroyed :: action -> Attribute action onDestroyed action = Attribute $ \sink n -> do
frontend-src/Miso/String.hs view
@@ -84,7 +84,7 @@ fromMisoString = realToFrac . jsStringToDouble instance ToMisoString Double where toMisoString = realFloatToJSString- fromMisoString = jsStringToDouble+ fromMisoString = jsStringToDouble instance ToMisoString Int where toMisoString = integralToJSString fromMisoString = round . jsStringToDouble
frontend-src/Miso/Subscription/Keyboard.hs view
@@ -81,6 +81,7 @@ keySetRef <- liftIO (newIORef mempty) windowAddEventListener "keyup" $ keyUpCallback keySetRef windowAddEventListener "keydown" $ keyDownCallback keySetRef+ windowAddEventListener "blur" $ blurCallback keySetRef where keyDownCallback keySetRef = \keyDownEvent -> do Just key <- fromJSVal =<< getProp "keyCode" (Object keyDownEvent)@@ -94,4 +95,12 @@ newKeys <- liftIO $ atomicModifyIORef' keySetRef $ \keys -> let !new = S.delete key keys in (new, new)+ liftIO (sink (f newKeys))++ -- Assume keys are released the moment focus is lost. Otherwise going+ -- back and forth to the app can cause keys to get stuck.+ blurCallback keySetRef = \_ -> do+ newKeys <- liftIO $ atomicModifyIORef' keySetRef $ \_ ->+ let !new = S.empty+ in (new, new) liftIO (sink (f newKeys))
ghc-src/Miso/String.hs view
@@ -54,3 +54,17 @@ instance ToMisoString BL.ByteString where toMisoString = toMisoString . LT.decodeUtf8 fromMisoString = LT.encodeUtf8 . fromMisoString+instance ToMisoString Float where+ toMisoString = T.pack . show+ fromMisoString = read . T.unpack+instance ToMisoString Double where+ toMisoString = T.pack . show+ fromMisoString = read . T.unpack+instance ToMisoString Int where+ toMisoString = T.pack . show+ -- Replicate frontend behavior+ fromMisoString = round . (read :: String -> Double) . T.unpack+instance ToMisoString Word where+ toMisoString = T.pack . show+ -- Replicate frontend behavior+ fromMisoString = round . (read :: String -> Double) . T.unpack
ghcjs-ffi/Miso/FFI.hs view
@@ -119,11 +119,11 @@ -- | Retrieves inner height-foreign import javascript unsafe "$r = window.innerHeight;"+foreign import javascript unsafe "$r = window['innerHeight'];" windowInnerHeight :: IO Int -- | Retrieves outer height-foreign import javascript unsafe "$r = window.innerWidth;"+foreign import javascript unsafe "$r = window['innerWidth'];" windowInnerWidth :: IO Int -- | Retrieve high performance time stamp@@ -162,7 +162,7 @@ clearBody :: IO () -foreign import javascript unsafe "$r = objectToJSON($1,$2);"+foreign import javascript unsafe "$r = window['objectToJSON']($1,$2);" objectToJSON :: JSVal -- ^ decodeAt :: [JSString] -> JSVal -- ^ object with impure references to the DOM@@ -201,7 +201,7 @@ callFunction continuation res delegateEvent' mountPoint events cb' -foreign import javascript unsafe "delegate($1, $2, $3);"+foreign import javascript unsafe "window['delegate']($1, $2, $3);" delegateEvent' :: JSVal -- ^ mountPoint element -> JSVal -- ^ Events@@ -213,35 +213,38 @@ -- | Copies DOM pointers into virtual dom -- entry point into isomorphic javascript-foreign import javascript unsafe "copyDOMIntoVTree($1);"- copyDOMIntoVTree :: JSVal -> IO ()+foreign import javascript unsafe "window['copyDOMIntoVTree']($1, $2);"+ copyDOMIntoVTree+ :: JSVal -- ^ mountPoint element of the isomorphic app+ -> JSVal -- ^ VDom object+ -> IO () -- | Pins down the current callbacks for clearing later-foreign import javascript unsafe "swapCallbacks();"+foreign import javascript unsafe "window['swapCallbacks']();" swapCallbacks :: IO () -- | Releases callbacks registered by the virtual DOM.-foreign import javascript unsafe "releaseCallbacks();"+foreign import javascript unsafe "window['releaseCallbacks']();" releaseCallbacks :: IO () -foreign import javascript unsafe "registerCallback($1);"+foreign import javascript unsafe "window['registerCallback']($1);" registerCallback :: JSVal -> IO () -- | Fails silently if the element is not found. -- -- Analogous to @document.getElementById(id).focus()@.-foreign import javascript unsafe "callFocus($1);"+foreign import javascript unsafe "window['callFocus']($1);" focus :: JSString -> JSM () -- | Fails silently if the element is not found. -- -- Analogous to @document.getElementById(id).blur()@-foreign import javascript unsafe "callBlur($1);"+foreign import javascript unsafe "window['callBlur']($1);" blur :: JSString -> JSM () -- | Calls @document.getElementById(id).scrollIntoView()@ foreign import javascript unsafe- "document.getElementById($1).scrollIntoView();"+ "document.getElementById($1)['scrollIntoView']();" scrollIntoView :: JSString -> IO () -- | Calls the @alert()@ function.
jsaddle-ffi/Miso/FFI.hs view
@@ -199,8 +199,8 @@ -- | Copies DOM pointers into virtual dom -- entry point into isomorphic javascript-copyDOMIntoVTree :: JSVal -> JSM ()-copyDOMIntoVTree a = () <$ jsg1 "copyDOMIntoVTree" a+copyDOMIntoVTree :: JSVal -> JSVal -> JSM ()+copyDOMIntoVTree mountPoint a = () <$ jsg1 "copyDOMIntoVTree" mountPoint a -- TODO For now, we do not free callbacks when compiling with JSaddle
jsbits/delegate.js view
@@ -1,111 +1,107 @@-window.oldCallbacks = [];-window.currentCallbacks = [];+window['oldCallbacks'] = [];+window['currentCallbacks'] = []; /* Callbacks in ghcjs need to be released. With this function one can register callbacks that should be released right before diffing. */-window.registerCallback = function registerCallback(cb) {- currentCallbacks.push(cb);+window['registerCallback'] = function registerCallback(cb) {+ window['currentCallbacks'].push(cb); }; /* Swaps out the new calbacks for old callbacks. The old callbacks should be cleared once the new callbacks have replaced them. */-window.swapCallbacks = function swapCallbacks() {- oldCallbacks = currentCallbacks;- currentCallbacks = [];+window['swapCallbacks'] = function swapCallbacks() {+ window['oldCallbacks'] = window['currentCallbacks'];+ window['currentCallbacks'] = []; }; /* This releases the old callbacks. */-window.releaseCallbacks = function releaseCallbacks() {- for (var i in oldCallbacks)- h$release(oldCallbacks[i]);+window['releaseCallbacks'] = function releaseCallbacks() {+ for (var i in window['oldCallbacks'])+ h$release(window['oldCallbacks'][i]); - oldCallbacks = [];+ window['oldCallbacks'] = []; }; /* event delegation algorithm */-window.delegate = function delegate(mountPointElement, events, getVTree) {- for (var event in events) {- mountPointElement.addEventListener(events[event][0], function(e) {- getVTree(function (obj) {- delegateEvent ( e- , obj- , buildTargetToElement(mountPointElement, e.target)- , []- );- });- }, events[event][1]);- }+window['delegate'] = function delegate(mountPointElement, events, getVTree) {+ for (var event in events) {+ mountPointElement.addEventListener(events[event][0], function(e) {+ getVTree(function (obj) {+ window['delegateEvent'](e, obj, window['buildTargetToElement'](mountPointElement, e.target), []);+ });+ }, events[event][1]);+ } }; /* Accumulate parent stack as well for propagation */-window.delegateEvent = function delegateEvent (event, obj, stack, parentStack) {+window['delegateEvent'] = function delegateEvent (event, obj, stack, parentStack) { - /* base case, not found */- if (!stack.length) return;+ /* base case, not found */+ if (!stack.length) return; - /* stack not length 1, recurse */- else if (stack.length > 1) {- if (obj.domRef === stack[0]) parentStack.unshift(obj);- for (var o = 0; o < obj.children.length; o++) {- if (obj.children[o].type === "vtext") continue;- delegateEvent ( event- , obj.children[o]- , stack.slice(1)- , parentStack- );- }- }+ /* stack not length 1, recurse */+ else if (stack.length > 1) {+ if (obj['domRef'] === stack[0]) parentStack.unshift(obj);+ for (var o = 0; o < obj.children.length; o++) {+ if (obj.children[o]['type'] === "vtext") continue;+ delegateEvent ( event+ , obj.children[o]+ , stack.slice(1)+ , parentStack+ );+ }+ } - /* stack.length == 1 */- else {- if (obj.domRef === stack[0]) {- var eventObj = obj.events[event.type];- if (eventObj) {- var options = eventObj.options;- if (options.preventDefault)- event.preventDefault();- eventObj.runEvent(event);- if (!options.stopPropagation)- propogateWhileAble (parentStack, event);- } else {- /* still propagate to parent handlers even if event not defined */- propogateWhileAble (parentStack, event);- }- }+ /* stack.length == 1 */+ else {+ if (obj['domRef'] === stack[0]) {+ var eventObj = obj['events'][event.type];+ if (eventObj) {+ var options = eventObj.options;+ if (options['preventDefault'])+ event.preventDefault();+ eventObj['runEvent'](event);+ if (!options['stopPropagation'])+ window['propogateWhileAble'] (parentStack, event);+ } else {+ /* still propagate to parent handlers even if event not defined */+ window['propogateWhileAble'] (parentStack, event);+ } }+ } }; -window.buildTargetToElement = function buildTargetToElement (element, target) {- var stack = [];- while (element !== target) {- stack.unshift (target);- target = target.parentNode;- }- return stack;+window['buildTargetToElement'] = function buildTargetToElement (element, target) {+ var stack = [];+ while (element !== target) {+ stack.unshift (target);+ target = target.parentNode;+ }+ return stack; }; -window.propogateWhileAble = function propogateWhileAble (parentStack, event) {+window['propogateWhileAble'] = function propogateWhileAble (parentStack, event) { for (var i = 0; i < parentStack.length; i++) {- if (parentStack[i].events[event.type]) {- var eventObj = parentStack[i].events[event.type],- options = eventObj.options;- if (options.preventDefault) event.preventDefault();- eventObj.runEvent(event);- if (options.stopPropagation) break;+ if (parentStack[i]['events'][event.type]) {+ var eventObj = parentStack[i]['events'][event.type],+ options = eventObj['options'];+ if (options['preventDefault']) event.preventDefault();+ eventObj['runEvent'](event);+ if (options['stopPropagation']) break; } } }; /* Walks down obj following the path described by `at`, then filters primitive values (string, numbers and booleans)*/-window.objectToJSON = function objectToJSON (at, obj) {+window['objectToJSON'] = function objectToJSON (at, obj) { /* If at is of type [[MisoString]] */ if (typeof at[0] == "object") { var ret = []; for (var i = 0; i < at.length; i++)- ret.push(objectToJSON(at[i], obj));+ ret.push(window['objectToJSON'](at[i], obj)); return (ret); } @@ -115,13 +111,13 @@ if (obj instanceof Array) { var newObj = []; for (var i = 0; i < obj.length; i++)- newObj.push(objectToJSON([], obj[i]));+ newObj.push(window['objectToJSON']([], obj[i])); return (newObj); } /* If obj is a non-list-like object */ var newObj = {};- for (var i in obj){+ for (var i in obj){ /* bug in safari, throws TypeError if the following fields are referenced on a checkbox */ /* https://stackoverflow.com/a/25569117/453261 */ /* https://html.spec.whatwg.org/multipage/input.html#do-not-apply */@@ -129,6 +125,6 @@ continue; if (typeof obj[i] == "string" || typeof obj[i] == "number" || typeof obj[i] == "boolean") newObj[i] = obj[i];- }- return newObj;+ }+ return newObj; };
jsbits/diff.js view
@@ -1,347 +1,346 @@ /* virtual-dom diffing algorithm, applies patches as detected */-window.diff = function diff(currentObj, newObj, parent, doc) {- if (!currentObj && !newObj) return;- else if (!currentObj && newObj) createNode(newObj, parent, doc);- else if (currentObj && !newObj) destroyNode(currentObj, parent);- else {- if (currentObj.type === "vtext") {- if (newObj.type === "vnode") replaceTextWithElement(currentObj, newObj, parent, doc);- else diffTextNodes(currentObj, newObj);- } else {- if (newObj.type === "vnode") diffVNodes(currentObj, newObj, parent, doc);- else replaceElementWithText(currentObj, newObj, parent, doc);- }+window['diff'] = function diff(currentObj, newObj, parent, doc) {+ if (!currentObj && !newObj) return;+ else if (!currentObj && newObj) window['createNode'](newObj, parent, doc);+ else if (currentObj && !newObj) window['destroyNode'](currentObj, parent);+ else {+ if (currentObj.type === "vtext") {+ if (newObj.type === "vnode") window['replaceTextWithElement'](currentObj, newObj, parent, doc);+ else window['diffTextNodes'](currentObj, newObj);+ } else {+ if (newObj.type === "vnode") window['diffVNodes'](currentObj, newObj, parent, doc);+ else window['replaceElementWithText'](currentObj, newObj, parent, doc); }+ } }; -window.destroyNode = function destroyNode(obj, parent) {- parent.removeChild(obj.domRef);- callDestroyedRecursive(obj);+window['destroyNode'] = function destroyNode(obj, parent) {+ parent.removeChild(obj['domRef']);+ window['callDestroyedRecursive'](obj); }; -window.callDestroyedRecursive = function callDestroyedRecursive(obj) {- callDestroyed(obj);- for (var i in obj.children)- callDestroyedRecursive(obj.children[i]);+window['callDestroyedRecursive'] = function callDestroyedRecursive(obj) {+ window['callDestroyed'](obj);+ for (var i in obj.children)+ window['callDestroyedRecursive'](obj.children[i]); }; -window.callDestroyed = function callDestroyed(obj) {- if (obj.onDestroyed) obj.onDestroyed();+window['callDestroyed'] = function callDestroyed(obj) {+ if (obj['onDestroyed']) obj['onDestroyed'](); }; -window.diffTextNodes = function diffTextNodes(c, n) {- if (c.text !== n.text) c.domRef.textContent = n.text;- n.domRef = c.domRef;+window['diffTextNodes'] = function diffTextNodes(c, n) {+ if (c['text'] !== n['text']) c['domRef'].textContent = n['text'];+ n['domRef'] = c['domRef']; }; -window.replaceElementWithText = function replaceElementWithText(c, n, parent, doc) {- n.domRef = doc.createTextNode(n.text);- parent.replaceChild(n.domRef, c.domRef);- callDestroyedRecursive(c);+window['replaceElementWithText'] = function replaceElementWithText(c, n, parent, doc) {+ n['domRef'] = doc.createTextNode(n['text']);+ parent.replaceChild(n['domRef'], c['domRef']);+ window['callDestroyedRecursive'](c); }; -window.replaceTextWithElement = function replaceTextWithElement(c, n, parent, doc) {- createElement(n, doc);- parent.replaceChild(n.domRef, c.domRef);- callCreated(n);+window['replaceTextWithElement'] = function replaceTextWithElement(c, n, parent, doc) {+ window['createElement'](n, doc);+ parent.replaceChild(n['domRef'], c['domRef']);+ window['callCreated'](n); }; -window.callCreated = function callCreated(obj) {- if (obj.onCreated) obj.onCreated();+window['callCreated'] = function callCreated(obj) {+ if (obj['onCreated']) obj['onCreated'](); }; -window.populate = function populate(c, n, doc) {- if (!c) c = {- props: null,- css: null,- children: []- }- diffProps(c.props, n.props, n.domRef, n.ns === "svg");- diffCss(c.css, n.css, n.domRef);- diffChildren(c.children, n.children, n.domRef, doc);+window['populate'] = function populate(c, n, doc) {+ if (!c) c = {+ props: null,+ css: null,+ children: []+ }+ window['diffProps'](c['props'], n['props'], n['domRef'], n['ns'] === "svg");+ window['diffCss'](c['css'], n['css'], n['domRef']);+ window['diffChildren'](c['children'], n['children'], n['domRef'], doc); }; -window.diffVNodes = function diffVNodes(c, n, parent, doc) {- if (c.tag === n.tag && n.key === c.key) {- n.domRef = c.domRef;- populate(c, n, doc);- } else {- createElement(n, doc);- parent.replaceChild(n.domRef, c.domRef);- callDestroyedRecursive(c);- callCreated(n);- }+window['diffVNodes'] = function diffVNodes(c, n, parent, doc) {+ if (c['tag'] === n['tag'] && n['key'] === c['key']) {+ n['domRef'] = c['domRef'];+ window['populate'](c, n, doc);+ } else {+ window['createElement'](n, doc);+ parent.replaceChild(n['domRef'], c['domRef']);+ window['callDestroyedRecursive'](c);+ window['callCreated'](n);+ } }; -window.diffProps = function diffProps(cProps, nProps, node, isSvg) {- var result, newProp, domProp;- /* Is current prop in new prop list? */- for (var c in cProps) {- newProp = nProps[c];- /* If current property no longer exists, remove it */- if (!newProp) {- /* current key is not in node, remove it from DOM, if SVG, remove attribute */- if (isSvg || !(c in node))- node.removeAttribute(c, cProps[c]);- else- node[c] = '';- } else {- /* Already on DOM from previous diff, continue */- if (newProp === cProps[c]) continue;- domProp = node[c];- if (isSvg) {- if (c === "href")- node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);- else- node.setAttribute(c, newProp);- } else if (c in node && !(c === "list" || c === "form")) {- node[c] = newProp;- } else {- node.setAttribute(c, newProp);- }- }+window['diffProps'] = function diffProps(cProps, nProps, node, isSvg) {+ var result, newProp;+ /* Is current prop in new prop list? */+ for (var c in cProps) {+ newProp = nProps[c];+ /* If current property no longer exists, remove it */+ if (!newProp) {+ /* current key is not in node, remove it from DOM, if SVG, remove attribute */+ if (isSvg || !(c in node))+ node.removeAttribute(c, cProps[c]);+ else+ node[c] = '';+ } else {+ /* Already on DOM from previous diff, continue */+ if (newProp === cProps[c]) continue;+ if (isSvg) {+ if (c === "href")+ node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);+ else+ node.setAttribute(c, newProp);+ } else if (c in node && !(c === "list" || c === "form")) {+ node[c] = newProp;+ } else {+ node.setAttribute(c, newProp);+ } }- /* add remaining */- for (var n in nProps) {- if (cProps && cProps[n]) continue;- newProp = nProps[n];- /* Only add new properties, skip (continue) if they already exist in current property map */- if (isSvg) {- if (n === "href")- node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);- else- node.setAttribute(n, newProp);- } else if (n in node && !(n === "list" || n === "form")) {- node[n] = nProps[n];- } else {- node.setAttribute(n, newProp);- }+ }+ /* add remaining */+ for (var n in nProps) {+ if (cProps && cProps[n]) continue;+ newProp = nProps[n];+ /* Only add new properties, skip (continue) if they already exist in current property map */+ if (isSvg) {+ if (n === "href")+ node.setAttributeNS("http://www.w3.org/1999/xlink", "href", newProp);+ else+ node.setAttribute(n, newProp);+ } else if (n in node && !(n === "list" || n === "form")) {+ node[n] = nProps[n];+ } else {+ node.setAttribute(n, newProp); }+ } }; -window.diffCss = function diffCss(cCss, nCss, node) {- var result;- /* is current attribute in new attribute list? */- for (var c in cCss) {- result = nCss[c];- if (!result) {- /* current key is not in node */- node.style[c] = null;- } else if (result !== cCss[c]) {- node.style[c] = result;- }- }- /* add remaining */- for (var n in nCss) {- if (cCss && cCss[n]) continue;- node.style[n] = nCss[n];+window['diffCss'] = function diffCss(cCss, nCss, node) {+ var result;+ /* is current attribute in new attribute list? */+ for (var c in cCss) {+ result = nCss[c];+ if (!result) {+ /* current key is not in node */+ node.style[c] = null;+ } else if (result !== cCss[c]) {+ node.style[c] = result; }+ }+ /* add remaining */+ for (var n in nCss) {+ if (cCss && cCss[n]) continue;+ node.style[n] = nCss[n];+ } }; -window.hasKeys = function hasKeys(ns, cs) {- return ns.length > 0 && cs.length > 0 && ns[0].key != null && cs[0].key != null;+window['hasKeys'] = function hasKeys(ns, cs) {+ return ns.length > 0 && cs.length > 0 && ns[0]['key'] != null && cs[0]['key'] != null; }; -window.diffChildren = function diffChildren(cs, ns, parent, doc) {- var longest = ns.length > cs.length ? ns.length : cs.length;- if (hasKeys(ns, cs)) {- syncChildren(cs, ns, parent, doc);- } else {- for (var i = 0; i < longest; i++)- diff(cs[i], ns[i], parent, doc);- }+window['diffChildren'] = function diffChildren(cs, ns, parent, doc) {+ var longest = ns.length > cs.length ? ns.length : cs.length;+ if (window['hasKeys'](ns, cs)) {+ window['syncChildren'](cs, ns, parent, doc);+ } else {+ for (var i = 0; i < longest; i++)+ window['diff'](cs[i], ns[i], parent, doc);+ } }; -window.createElement = function createElement(obj, doc) {- if (obj.ns === "svg") {- obj.domRef = doc.createElementNS("http://www.w3.org/2000/svg", obj.tag);- } else if (obj.ns === "mathml") {- obj.domRef = doc.createElementNS("http://www.w3.org/1998/Math/MathML", obj.tag);- } else {- obj.domRef = doc.createElement(obj.tag);- }- populate(null, obj, doc);+window['createElement'] = function createElement(obj, doc) {+ if (obj['ns'] === "svg") {+ obj['domRef'] = doc.createElementNS("http://www.w3.org/2000/svg", obj['tag']);+ } else if (obj['ns'] === "mathml") {+ obj['domRef'] = doc.createElementNS("http://www.w3.org/1998/Math/MathML", obj['tag']);+ } else {+ obj['domRef'] = doc.createElement(obj['tag']);+ }+ window['populate'](null, obj, doc); }; -window.createNode = function createNode(obj, parent, doc) {- if (obj.type === "vnode") createElement(obj, doc);- else obj.domRef = doc.createTextNode(obj.text);- parent.appendChild(obj.domRef);- callCreated(obj);+window['createNode'] = function createNode(obj, parent, doc) {+ if (obj.type === "vnode") window['createElement'](obj, doc);+ else obj['domRef'] = doc.createTextNode(obj['text']);+ parent.appendChild(obj['domRef']);+ window['callCreated'](obj); }; /* Child reconciliation algorithm, inspired by kivi and Bobril */-window.syncChildren = function syncChildren(os, ns, parent, doc) {- var oldFirstIndex = 0,- newFirstIndex = 0,- oldLastIndex = os.length - 1,- newLastIndex = ns.length - 1,- nFirst, nLast, oLast, oFirst, tmp, found, node;- for (;;) {- /* check base case, first > last for both new and old- [ ] -- old children empty (fully-swapped)- [ ] -- new children empty (fully-swapped)- */- if (newFirstIndex > newLastIndex && oldFirstIndex > oldLastIndex) {- break;- }+window['syncChildren'] = function syncChildren(os, ns, parent, doc) {+ var oldFirstIndex = 0,+ newFirstIndex = 0,+ oldLastIndex = os.length - 1,+ newLastIndex = ns.length - 1,+ nFirst, nLast, oLast, oFirst, tmp, found, node;+ for (;;) {+ /* check base case, first > last for both new and old+ [ ] -- old children empty (fully-swapped)+ [ ] -- new children empty (fully-swapped)+ */+ if (newFirstIndex > newLastIndex && oldFirstIndex > oldLastIndex) {+ break;+ } - /* Initialize */- nFirst = ns[newFirstIndex];- nLast = ns[newLastIndex];- oFirst = os[oldFirstIndex];- oLast = os[oldLastIndex];- /* No more old nodes, create and insert all remaining nodes- -> [ ] <- old children- -> [ a b c ] <- new children- */- if (oldFirstIndex > oldLastIndex) {- diff(null, nFirst, parent, doc);- /* insertBefore's semantics will append a node if the second argument provided is `null` or `undefined`.- Otherwise, it will insert node.domRef before oLast.domRef. */- parent.insertBefore(oLast.domRef, nFirst.domRef);- os.splice(newFirstIndex, 0, nFirst);- newFirstIndex++;- }- /* No more new nodes, delete all remaining nodes in old list- -> [ a b c ] <- old children- -> [ ] <- new children- */- else if (newFirstIndex > newLastIndex) {- tmp = oldLastIndex - oldFirstIndex;- while (tmp >= 0) {- parent.removeChild(os[oldFirstIndex].domRef);- os.splice(oldFirstIndex, 1);- tmp--;- }- break;- }- /* happy path, everything aligns, we continue- -> oldFirstIndex -> [ a b c ] <- oldLastIndex- -> newFirstIndex -> [ a b c ] <- newLastIndex- check if nFirst and oFirst align, if so, check nLast and oLast- */- else if (oFirst.key === nFirst.key) {- diff(os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);- } else if (oLast.key === nLast.key) {- diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);- }- /* flip-flop case, nodes have been swapped, in some way or another- both could have been swapped.- -> [ a b c ] <- old children- -> [ c b a ] <- new children- */- else if (oFirst.key === nLast.key && nFirst.key === oLast.key) {- swapDomRefs(node, oFirst.domRef, oLast.domRef, parent);- swap(os, oldFirstIndex, oldLastIndex);- diff(os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);- diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);- }- /* Or just one could be swapped (d's align here)- This is top left and bottom right match case.- We move d to end of list, mutate old vdom to reflect the change- We then continue without affecting indexes, hoping to land in a better case- -> [ d a b ] <- old children- -> [ a b d ] <- new children- becomes- -> [ a b d ] <- old children- -> [ a b d ] <- new children- and now we happy path- */- else if (oFirst.key === nLast.key) {- /* insertAfter */- parent.insertBefore(oFirst.domRef, oLast.domRef.nextSibling);- /* swap positions in old vdom */- os.splice(oldLastIndex,0,os.splice(oldFirstIndex,1)[0]);- diff(os[oldLastIndex--], ns[newLastIndex--], parent, doc);- }- /* This is top right and bottom lefts match case.- We move d to end of list, mutate old vdom to reflect the change- -> [ b a d ] <- old children- -> [ d b a ] <- new children- becomes- -> [ d b a ] <- old children- -> [ d b a ] <- new children- and now we happy path- */- else if (oLast.key === nFirst.key) {- /* insertAfter */- parent.insertBefore(oLast.domRef, oFirst.domRef);- /* swap positions in old vdom */- os.splice(oldFirstIndex,0, os.splice(oldLastIndex,1)[0]);- diff(os[oldFirstIndex++], nFirst, parent, doc);- newFirstIndex++;- }+ /* Initialize */+ nFirst = ns[newFirstIndex];+ nLast = ns[newLastIndex];+ oFirst = os[oldFirstIndex];+ oLast = os[oldLastIndex];+ /* No more old nodes, create and insert all remaining nodes+ -> [ ] <- old children+ -> [ a b c ] <- new children+ */+ if (oldFirstIndex > oldLastIndex) {+ window['diff'](null, nFirst, parent, doc);+ /* insertBefore's semantics will append a node if the second argument provided is `null` or `undefined`.+ Otherwise, it will insert node['domRef'] before oLast['domRef']. */+ parent.insertBefore(oLast['domRef'], nFirst['domRef']);+ os.splice(newFirstIndex, 0, nFirst);+ newFirstIndex++;+ }+ /* No more new nodes, delete all remaining nodes in old list+ -> [ a b c ] <- old children+ -> [ ] <- new children+ */+ else if (newFirstIndex > newLastIndex) {+ tmp = oldLastIndex - oldFirstIndex;+ while (tmp >= 0) {+ parent.removeChild(os[oldFirstIndex]['domRef']);+ os.splice(oldFirstIndex, 1);+ tmp--;+ }+ break;+ }+ /* happy path, everything aligns, we continue+ -> oldFirstIndex -> [ a b c ] <- oldLastIndex+ -> newFirstIndex -> [ a b c ] <- newLastIndex+ check if nFirst and oFirst align, if so, check nLast and oLast+ */+ else if (oFirst['key'] === nFirst['key']) {+ window['diff'](os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);+ } else if (oLast['key'] === nLast['key']) {+ window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);+ }+ /* flip-flop case, nodes have been swapped, in some way or another+ both could have been swapped.+ -> [ a b c ] <- old children+ -> [ c b a ] <- new children+ */+ else if (oFirst['key'] === nLast['key'] && nFirst['key'] === oLast['key']) {+ window['swapDomRefs'](node, oFirst['domRef'], oLast['domRef'], parent);+ window['swap'](os, oldFirstIndex, oldLastIndex);+ window['diff'](os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);+ window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);+ }+ /* Or just one could be swapped (d's align here)+ This is top left and bottom right match case.+ We move d to end of list, mutate old vdom to reflect the change+ We then continue without affecting indexes, hoping to land in a better case+ -> [ d a b ] <- old children+ -> [ a b d ] <- new children+ becomes+ -> [ a b d ] <- old children+ -> [ a b d ] <- new children+ and now we happy path+ */+ else if (oFirst['key'] === nLast['key']) {+ /* insertAfter */+ parent.insertBefore(oFirst['domRef'], oLast['domRef'].nextSibling);+ /* swap positions in old vdom */+ os.splice(oldLastIndex,0,os.splice(oldFirstIndex,1)[0]);+ window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);+ }+ /* This is top right and bottom lefts match case.+ We move d to end of list, mutate old vdom to reflect the change+ -> [ b a d ] <- old children+ -> [ d b a ] <- new children+ becomes+ -> [ d b a ] <- old children+ -> [ d b a ] <- new children+ and now we happy path+ */+ else if (oLast['key'] === nFirst['key']) {+ /* insertAfter */+ parent.insertBefore(oLast['domRef'], oFirst['domRef']);+ /* swap positions in old vdom */+ os.splice(oldFirstIndex,0, os.splice(oldLastIndex,1)[0]);+ window['diff'](os[oldFirstIndex++], nFirst, parent, doc);+ newFirstIndex++;+ } - /* The "you're screwed" case, nothing aligns, pull the ripcord, do something more fancy- This can happen when the list is sorted, for example.- -> [ a e c ] <- old children- -> [ b e d ] <- new children- */- else {- /* final case, perform linear search to check if new key exists in old map, decide what to do from there */- found = false;- tmp = oldFirstIndex;- while (tmp <= oldLastIndex) {- if (os[tmp].key === nFirst.key) {- found = true;- node = os[tmp];- break;- }- tmp++;- }- /* If new key was found in old map this means it was moved, hypothetically as below- -> [ a e b c ] <- old children- -> [ b e a j ] <- new children- ^- In the above case 'b' has been moved, so we need to insert 'b' before 'a' in both vDOM and DOM- We also increase oldFirstIndex and newFirstIndex.+ /* The "you're screwed" case, nothing aligns, pull the ripcord, do something more fancy+ This can happen when the list is sorted, for example.+ -> [ a e c ] <- old children+ -> [ b e d ] <- new children+ */+ else {+ /* final case, perform linear search to check if new key exists in old map, decide what to do from there */+ found = false;+ tmp = oldFirstIndex;+ while (tmp <= oldLastIndex) {+ if (os[tmp]['key'] === nFirst['key']) {+ found = true;+ node = os[tmp];+ break;+ }+ tmp++;+ }+ /* If new key was found in old map this means it was moved, hypothetically as below+ -> [ a e b c ] <- old children+ -> [ b e a j ] <- new children+ ^+ In the above case 'b' has been moved, so we need to insert 'b' before 'a' in both vDOM and DOM+ We also increase oldFirstIndex and newFirstIndex. - This results in new list below w/ updated index position- -> [ b a e c ] <- old children- -> [ b e a j ] <- new children- ^- */- if (found) {- /* Move item to correct position */- os.splice(oldFirstIndex,0, os.splice(tmp,1)[0]);- /* Swap DOM references */- parent.insertBefore(node.domRef, os[oldFirstIndex].domRef);- /* optionally perform `diff` here */- diff(os[oldFirstIndex++], nFirst, parent, doc);- /* increment counters */- newFirstIndex++;- }- /* If new key was *not* found in the old map this means it must now be created, example below- -> [ a e d c ] <- old children- -> [ b e a j ] <- new children- ^+ This results in new list below w/ updated index position+ -> [ b a e c ] <- old children+ -> [ b e a j ] <- new children+ ^+ */+ if (found) {+ /* Move item to correct position */+ os.splice(oldFirstIndex,0, os.splice(tmp,1)[0]);+ /* Swap DOM references */+ parent.insertBefore(node['domRef'], os[oldFirstIndex]['domRef']);+ /* optionally perform `diff` here */+ window['diff'](os[oldFirstIndex++], nFirst, parent, doc);+ /* increment counters */+ newFirstIndex++;+ }+ /* If new key was *not* found in the old map this means it must now be created, example below+ -> [ a e d c ] <- old children+ -> [ b e a j ] <- new children+ ^ - In the above case 'b' does not exist in the old map, so we create a new element and DOM reference.- We then insertBefore in both vDOM and DOM.+ In the above case 'b' does not exist in the old map, so we create a new element and DOM reference.+ We then insertBefore in both vDOM and DOM. - -> [ b a e d c ] <- old children- -> [ b e a j ] <- new children- ^- */- else {- createElement(nFirst, doc);- parent.insertBefore(nFirst.domRef, oFirst.domRef);- os.splice(oldFirstIndex++, 0, nFirst);- newFirstIndex++;- oldLastIndex++;- }- }+ -> [ b a e d c ] <- old children+ -> [ b e a j ] <- new children+ ^+ */+ else {+ window['createElement'](nFirst, doc);+ parent.insertBefore(nFirst['domRef'], oFirst['domRef']);+ os.splice(oldFirstIndex++, 0, nFirst);+ newFirstIndex++;+ oldLastIndex++;+ } }+ } }; -window.swapDomRefs = function swapDomRefs(tmp,a,b,p) {+window['swapDomRefs'] = function swapDomRefs(tmp,a,b,p) { tmp = a.nextSibling; p.insertBefore(a,b); p.insertBefore(b,tmp); }; -window.swap= function swap(os,l,r) {+window['swap']= function swap(os,l,r) { var k = os[l]; os[l] = os[r]; os[r] = k;
jsbits/isomorphic.js view
@@ -1,23 +1,57 @@-window.copyDOMIntoVTree = function copyDOMIntoVTree (vtree) {- walk (vtree, document.body.firstChild);-};+window['copyDOMIntoVTree'] = function copyDOMIntoVTree(mountPoint, vtree, doc = window.document) {+ var node = mountPoint ? mountPoint.firstChild : doc.body.firstChild;+ if (!window['walk'](vtree, node, doc)) {+ console.warn('Could not copy DOM into virtual DOM, falling back to diff');+ // Remove all children before rebuilding DOM+ while (node.firstChild)+ node.removeChild(node.lastChild);+ window['diff'](null, vtree, node, doc);+ return false;+ }+ return true;+} -window.walk = function walk (vtree, node) {- var i = 0, vdomChild, domChild;- vtree.domRef = node;+window['walk'] = function walk(vtree, node, doc) {+ // This is slightly more complicated than one might expect since+ // browsers will collapse consecutive text nodes into a single text node.+ // There can thus be fewer DOM nodes than VDOM nodes.+ var vdomChild,+ domChild; - // Fire onCreated events as though the elements had just been created.- callCreated(vtree);+ vtree['domRef'] = node; - while (i < vtree.children.length) {- vdomChild = vtree.children[i];- domChild = node.childNodes[i];- if (vdomChild.type === "vtext") {- vdomChild.domRef = domChild;- i++;- continue;- }- walk(vdomChild, domChild);- i++;- }-};+ // Fire onCreated events as though the elements had just been created.+ window['callCreated'](vtree);++ for (var i = 0; i < vtree.children.length; i++) {+ vdomChild = vtree['children'][i];+ domChild = node.childNodes[i];+ if (vdomChild.type === "vtext") {+ if (domChild.nodeType !== Node.TEXT_NODE) return false;++ if (vdomChild['text'] === domChild.textContent) {+ vdomChild['domRef'] = domChild;+ } else {+ var len = vdomChild.text.length,+ domNodeText = domChild.textContent.substring(0, len);+ if (domNodeText !== vdomChild.text) return false;++ // There are more VDOM nodes than DOM nodes+ // Create new DOM node to ensure synchrony between VDom and DOM+ var partialTxt = doc.createTextNode(domNodeText);+ node.insertBefore(partialTxt, domChild);+ vdomChild['domRef'] = partialTxt;+ domChild.textContent = domChild.textContent.substring(len);+ }+ } else {+ if (domChild.nodeType !== Node.ELEMENT_NODE) return false;+ window['walk'](vdomChild, domChild, doc);+ }++ }+ // After walking the sizes of VDom and DOM should be equal+ // Otherwise there are DOM nodes unaccounted for+ if (vtree.children.length !== node.childNodes.length) return false;++ return true;+}
jsbits/util.js view
@@ -1,11 +1,11 @@-window.callFocus = function callFocus(id) {+window['callFocus'] = function callFocus(id) { setTimeout(function(){ var ele = document.getElementById(id); if (ele && ele.focus) ele.focus() }, 50); } -window.callBlur = function callBlur(id) {+window['callBlur'] = function callBlur(id) { setTimeout(function(){ var ele = document.getElementById(id); if (ele && ele.blur) ele.blur()
miso.cabal view
@@ -1,5 +1,5 @@ name: miso-version: 0.21.2.0+version: 1.0.0.0 category: Web, Miso, Data Structures license: BSD3 license-file: LICENSE@@ -44,6 +44,10 @@ if (!impl(ghcjs) && !flag(jsaddle)) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/todo-mvc build-depends:@@ -62,6 +66,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/three build-depends:@@ -79,6 +87,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/file-reader build-depends:@@ -96,6 +108,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/xhr build-depends:@@ -113,6 +129,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/canvas2d build-depends:@@ -129,6 +149,10 @@ if (!impl(ghcjs) && !flag(jsaddle)) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/router build-depends:@@ -148,6 +172,10 @@ if (!impl(ghcjs) && !flag(jsaddle)) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/websocket build-depends:@@ -166,6 +194,10 @@ if (!impl(ghcjs) && !flag(jsaddle)) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/mario build-depends:@@ -188,6 +220,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/svg other-modules:@@ -206,6 +242,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/compose-update build-depends:@@ -220,6 +260,10 @@ if !impl(ghcjs) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: examples/mathml build-depends:@@ -234,6 +278,10 @@ if (!impl(ghcjs) && !flag(jsaddle)) || !flag(examples) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: exe build-depends:@@ -252,6 +300,10 @@ if !impl(ghcjs) || !flag(tests) buildable: False else+ ghcjs-options:+ -dedupe+ cpp-options:+ -DGHCJS_BROWSER hs-source-dirs: tests build-depends:
src/Miso/Html/Element.hs view
@@ -126,7 +126,7 @@ , bdi_ , bdo_ , wbr_- -- * Interactive elemnts+ -- * Interactive elements , details_ , summary_ , menuitem_
src/Miso/Html/Property.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} ----------------------------------------------------------------------------- -- | -- Module : Miso.Html.Property@@ -114,11 +115,17 @@ , httpEquiv_ , language_ , scoped_+ -- * Data+ , data_ ) where import Miso.Html.Internal import Miso.String (MisoString, intercalate) +#if __GLASGOW_HASKELL__ < 841+import Data.Monoid ((<>))+#endif+ -- | Set field to `Bool` value boolProp :: MisoString -> Bool -> Attribute action boolProp = prop@@ -383,3 +390,7 @@ -- <https://developer.mozilla.org/en-US/docs/Web/API/Element/className> class_ :: MisoString -> Attribute action class_ = textProp "class"+-- | Set "data-*" property+-- https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*+data_ :: MisoString -> MisoString -> Attribute action+data_ k v = textProp ("data-" <> k) v
src/Miso/Router.hs view
@@ -36,7 +36,7 @@ import qualified Data.Text as T import Data.Text.Encoding import GHC.TypeLits-import Network.HTTP.Types+import Network.HTTP.Types hiding (Header) import Network.URI import Servant.API import Web.HttpApiData@@ -117,6 +117,11 @@ mkRouter _ a f = RQueryFlag (Proxy :: Proxy sym) (\x -> mkRouter (Proxy :: Proxy sublayout) a (f x))++-- | Header+instance HasRouter sublayout => HasRouter (Header sym (x :: *) :> sublayout) where+ type RouteT (Header sym x :> sublayout) a = Maybe x -> RouteT sublayout a+ mkRouter _ a f = mkRouter (Proxy :: Proxy sublayout) a (f Nothing) -- | Path instance (HasRouter sublayout, KnownSymbol path)
tests/Main.hs view
@@ -95,7 +95,6 @@ GaveUp { output = o } -> (False, o) Failure { output = o } -> (False, o) NoExpectedFailure { output = o } -> (False, o)- InsufficientCoverage { output = o } -> (False, o) foreign import javascript unsafe "window.global_test_results = $1;" writeToGlobalObject :: JSVal -> IO ()