├── .gitignore
├── LICENSE
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
├── server
└── src
│ └── main
│ ├── resources
│ └── index.html
│ └── scala
│ └── doodlebot
│ ├── action
│ └── store.scala
│ ├── endpoint
│ ├── chat.scala
│ ├── login.scala
│ ├── signup.scala
│ └── static.scala
│ ├── model.scala
│ ├── server.scala
│ ├── syntax
│ └── validation.scala
│ └── validation
│ ├── input-error.scala
│ └── predicate.scala
└── ui
└── src
└── main
├── resources
└── virtual-dom.js
└── scala
└── doodlebot
├── basic-auth.scala
├── circuit.scala
├── doodlebot.scala
├── effect.scala
├── form
└── input.scala
├── message
└── message.scala
├── model
└── model.scala
├── view
├── chat.scala
├── login.scala
└── signup.scala
└── virtualDom
├── dom.scala
└── virtual-dom.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | .ensime
2 | .ensime_cache
3 | target
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Doodlebot
2 |
3 | An Essential Scala case study using Finch and Scala.js
4 |
5 | To run:
6 |
7 | 1. From `sbt`, `project server`, and `console`. Opening the console will automatically build the UI and the server, and then start the server. Closing the console with shutdown the server.
8 | 2. Navigate to `http://localhost:8080/index` in your web browser of choice.
9 | 3. If you don't want to create an account you can login with name "tester" and password "password".
10 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name in ThisBuild := "doodlebot"
2 | version in ThisBuild := "0.0.1"
3 | organization in ThisBuild := "underscoreio"
4 | scalaVersion in ThisBuild := "2.11.8"
5 |
6 | scalacOptions in ThisBuild ++= Seq("-feature", "-Xfatal-warnings", "-deprecation", "-unchecked", "-Ywarn-unused-import")
7 |
8 |
9 | lazy val server = project.
10 | settings(
11 | scalacOptions in (Compile, console) := Seq("-feature", "-Xfatal-warnings", "-deprecation", "-unchecked"),
12 |
13 | licenses += ("Apache-2.0", url("http://apache.org/licenses/LICENSE-2.0")),
14 |
15 | resolvers += Resolver.sonatypeRepo("snapshots"),
16 |
17 | libraryDependencies ++= Seq(
18 | "com.github.finagle" %% "finch-core" % "0.11.0-M2",
19 | "com.github.finagle" %% "finch-circe" % "0.11.0-M2",
20 | "io.circe" %% "circe-core" % "0.4.1",
21 | "io.circe" %% "circe-generic" % "0.4.1",
22 | "io.circe" %% "circe-parser" % "0.4.1",
23 | "org.scalatest" %% "scalatest" % "2.2.6" % "test",
24 | "org.scalacheck" %% "scalacheck" % "1.12.5" % "test"
25 | ),
26 |
27 | resourceGenerators in Compile += Def.task {
28 | val code = (fastOptJS in Compile in ui).value.data
29 | val sourceMap = code.getParentFile / (code.getName + ".map")
30 | val launcher = (packageScalaJSLauncher in Compile in ui).value.data
31 | val dependencies = (packageJSDependencies in Compile in ui).value
32 | Seq(code, sourceMap, launcher, dependencies)
33 | }.taskValue,
34 |
35 | initialCommands in console := """
36 | |doodlebot.DoodleBot.server
37 | """.trim.stripMargin,
38 |
39 | cleanupCommands in console := """
40 | |doodlebot.DoodleBot.server.close()
41 | """.trim.stripMargin
42 | )
43 |
44 |
45 | lazy val ui = project.
46 | enablePlugins(ScalaJSPlugin).
47 | settings(
48 | libraryDependencies ++= Seq(
49 | "org.scala-js" %%% "scalajs-dom" % "0.9.1",
50 | "com.lihaoyi" %%% "scalatags" % "0.5.5",
51 | "be.doeraene" %%% "scalajs-jquery" % "0.9.0"
52 | ),
53 | jsDependencies ++= Seq(
54 | "org.webjars" % "jquery" % "2.1.3" / "2.1.3/jquery.js",
55 | ProvidedJS / "virtual-dom.js"
56 | ),
57 | persistLauncher := true,
58 | skip in packageJSDependencies := false
59 | )
60 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.11
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.10")
2 |
--------------------------------------------------------------------------------
/server/src/main/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DoodleBot
5 |
6 |
73 |
74 |
75 | The app should be displayed here.
76 |
77 |
78 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/action/store.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package action
3 |
4 | import cats.data.{NonEmptyList,ValidatedNel,Xor}
5 | import cats.std.list._
6 | import cats.std.option._
7 | import cats.syntax.validated._
8 | import cats.syntax.cartesian._
9 | import cats.syntax.xor._
10 |
11 | object Store {
12 | import doodlebot.model._
13 | import scala.collection.mutable
14 |
15 | sealed abstract class SignupError extends Product with Serializable
16 | final case class EmailAlreadyExists(email: Email) extends SignupError
17 | final case class NameAlreadyExists(name: Name) extends SignupError
18 |
19 | sealed abstract class LoginError extends Product with Serializable
20 | final case class NameDoesNotExist(name: Name) extends LoginError
21 | final case object PasswordIncorrect extends LoginError
22 |
23 | def emailAlreadyExists(email: Email): SignupError =
24 | EmailAlreadyExists(email)
25 |
26 | def nameAlreadyExists(name: Name): SignupError =
27 | NameAlreadyExists(name)
28 |
29 | def nameDoesNotExist(name: Name): LoginError =
30 | NameDoesNotExist(name)
31 |
32 | val passwordIncorrect: LoginError =
33 | PasswordIncorrect
34 |
35 |
36 | private var emails: mutable.Set[Email] = mutable.Set.empty
37 | private var names: mutable.Set[Name] = mutable.Set.empty
38 | private var accounts: mutable.Map[Name, User] = mutable.Map.empty
39 | private var sessionsBySession: mutable.Map[Session, Name] = mutable.Map.empty
40 | private var sessionsByName: mutable.Map[Name, Session] = mutable.Map.empty
41 | // This will gobble memory without limit, but is ok for our simple use case
42 | private var messages: mutable.ArrayBuffer[Message] = new mutable.ArrayBuffer(1024)
43 |
44 | def signup(user: User): Xor[NonEmptyList[SignupError],Session] = {
45 | Store.synchronized {
46 | val emailCheck: ValidatedNel[SignupError,Unit] =
47 | if(emails(user.email))
48 | emailAlreadyExists(user.email).invalidNel[Unit]
49 | else
50 | ().validNel[SignupError]
51 |
52 | val nameCheck: ValidatedNel[SignupError,Unit] =
53 | if(names(user.name))
54 | nameAlreadyExists(user.name).invalidNel[Unit]
55 | else
56 | ().validNel
57 |
58 | (emailCheck |@| nameCheck).map { (_,_) =>
59 | emails += user.email
60 | names += user.name
61 | accounts += (user.name -> user)
62 | makeSession(user.name)
63 | }.toXor
64 | }
65 | }
66 |
67 | def login(login: Login): Xor[LoginError,Session] = {
68 | Store.synchronized {
69 | for {
70 | user <- Xor.fromOption(accounts.get(login.name), nameDoesNotExist(login.name))
71 | session <- if(user.password == login.password)
72 | makeSession(login.name).right
73 | else
74 | passwordIncorrect.left
75 | } yield session
76 | }
77 | }
78 |
79 | def makeSession(name: Name): Session = {
80 | Store.synchronized {
81 | sessionsByName.get(name).getOrElse {
82 | val session = Session()
83 | sessionsByName += (name -> session)
84 | sessionsBySession += (session -> name)
85 | session
86 | }
87 | }
88 | }
89 |
90 | def poll(offset: Int): Log =
91 | Store.synchronized {
92 | if(offset > messages.size)
93 | Log(messages.length, List.empty)
94 | else {
95 | Log(messages.length, messages.takeRight(messages.length - offset).toList)
96 | }
97 | }
98 |
99 | def message(message: Message): Unit =
100 | Store.synchronized {
101 | messages += message
102 | }
103 |
104 | def authenticated(name: Name, session: Session): Boolean =
105 | Store.synchronized {
106 | (sessionsBySession.get(session) |@| sessionsByName.get(name)).map { (n, s) =>
107 | n == name && s == session
108 | }.getOrElse(false)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/endpoint/chat.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package endpoint
3 |
4 | import io.finch._
5 | import com.twitter.util.Base64StringEncoder
6 | import doodlebot.action.Store
7 | import java.util.UUID
8 |
9 | object Chat {
10 | import doodlebot.model._
11 |
12 | val authorized: Endpoint0 =
13 | header("authorization") { header: String =>
14 | val authorized =
15 | header.split(" ", 2) match {
16 | case Array(scheme, params) =>
17 | if(scheme.toLowerCase == "basic") {
18 | new String(Base64StringEncoder.decode(params)).split(":", 2) match {
19 | case Array(name, session) =>
20 | try {
21 | val n = Name(name)
22 | val s = Session(UUID.fromString(session))
23 | Store.authenticated(n, s)
24 | } catch {
25 | case exn: IllegalArgumentException =>
26 | false
27 | }
28 |
29 | case _ => false
30 | }
31 | } else {
32 | false
33 | }
34 |
35 | case _ => false
36 | }
37 |
38 | if(authorized)
39 | Ok(shapeless.HNil : shapeless.HNil)
40 | else
41 | NotAcceptable(BasicAuthFailed)
42 | }
43 |
44 | val message: Endpoint[Unit] =
45 | post("message" :: authorized :: param("name") :: param("message")) { (name: String, message: String) =>
46 | val msg = model.Message(name, message)
47 | Ok(Store.message(msg))
48 | }
49 |
50 | val poll: Endpoint[Log] =
51 | post("poll" :: authorized :: param("offset").as[Int]) { (offset: Int) =>
52 | Ok(Store.poll(offset))
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/endpoint/login.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package endpoint
3 |
4 | import io.finch._
5 | import cats.data.Xor
6 | import cats.syntax.xor._
7 | import doodlebot.action.Store
8 | import doodlebot.validation.InputError
9 |
10 | object Login {
11 | import doodlebot.model._
12 |
13 | val login: Endpoint[Authenticated] = post("login" :: param("name") :: param("password")) { (name: String, password: String) =>
14 | val login = model.Login(Name(name), Password(password))
15 | val result: Xor[FormErrors,Authenticated] =
16 | Store.login(login).fold(
17 | fa = error => {
18 | FormErrors(
19 | error match {
20 | case Store.NameDoesNotExist(name) =>
21 | InputError("name", "Nobody has signed up with this name")
22 | case Store.PasswordIncorrect =>
23 | InputError("password", "Your password is incorrect")
24 | }
25 | ).left
26 | },
27 |
28 | fb = session => {
29 | Authenticated(name, session.get.toString).right
30 | }
31 | )
32 |
33 | result.fold(BadRequest, Ok)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/endpoint/signup.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package endpoint
3 |
4 | import io.finch._
5 | import cats.data.Xor
6 | import cats.std.list._
7 | import cats.syntax.cartesian._
8 | import cats.syntax.semigroup._
9 | import cats.syntax.xor._
10 | import doodlebot.action.Store
11 | import doodlebot.validation.InputError
12 |
13 | object Signup {
14 | import model._
15 |
16 | val signup: Endpoint[Authenticated] =
17 | post("signup" :: param("name") :: param("email") :: param("password")) { (name: String, email: String, password: String) =>
18 | import doodlebot.syntax.validation._
19 |
20 | val validatedUser: Xor[FormErrors,User] =
21 | (
22 | Name.validate(name).forInput("name") |@|
23 | Email.validate(email).forInput("email") |@|
24 | Password.validate(password).forInput("password")
25 | ).map { (n, e, p) => User(n, e, p) }.toXor.leftMap(errs => FormErrors(errs))
26 |
27 | val result: Xor[FormErrors,Authenticated] =
28 | validatedUser.flatMap { user =>
29 | Store.signup(user).fold(
30 | fa = errors => {
31 | val errs =
32 | errors.foldLeft(InputError.empty){ (accum, elt) =>
33 | elt match {
34 | case Store.EmailAlreadyExists(email) =>
35 | accum |+| InputError("email", "This email is already taken")
36 | case Store.NameAlreadyExists(name) =>
37 | accum |+| InputError("name", "This name is already taken")
38 | }
39 | }
40 |
41 | FormErrors(errs).left
42 | },
43 | fb = session => {
44 | Authenticated(name, session.get.toString).right
45 | }
46 | )
47 | }
48 |
49 | result.fold(BadRequest, Ok)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/endpoint/static.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package endpoint
3 |
4 | import io.finch._
5 | import com.twitter.io.Reader
6 | import com.twitter.finagle.http.Response
7 |
8 | object Static {
9 | val resourceLoader = this.getClass()
10 |
11 | val index: Endpoint[Response] = get("index") {
12 | val stream = resourceLoader.getResourceAsStream("/index.html")
13 | val reader = Reader.fromStream(stream)
14 | val data = Reader.readAll(reader)
15 |
16 | data.map { buf =>
17 | val response: Response = new Response.Ok()
18 | response.content = buf
19 | response.contentType = "text/html"
20 | Ok(response)
21 | }
22 | }
23 |
24 | val ui: Endpoint[Response] = get("ui" :: strings) { (tail: Seq[String]) =>
25 | val path = tail.mkString("/","/","")
26 | println(s"Loading resource $path")
27 | val stream = resourceLoader.getResourceAsStream(path)
28 | val reader = Reader.fromStream(stream)
29 | val data = Reader.readAll(reader)
30 |
31 | data.map { buf =>
32 | val response: Response = new Response.Ok()
33 | response.content = buf
34 | response.contentType = "application/javascript"
35 | Ok(response)
36 | }
37 | }
38 |
39 | val static = index :+: ui
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/model.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import cats.data.ValidatedNel
4 | import cats.std.list._
5 | import cats.syntax.cartesian._
6 | import java.util.UUID
7 |
8 | object model {
9 | import doodlebot.validation._
10 | import doodlebot.syntax.validation._
11 | import doodlebot.validation.Predicate._
12 |
13 | // Messages to the client
14 | final case class Authenticated(name: String, session: String)
15 | final case class Log(offset: Int, messages: List[Message])
16 | final case class Message(author: String, message: String)
17 | final case class FormErrors(errors: InputError) extends Exception
18 |
19 | // Messages from the client
20 | final case class Login(name: Name, password: Password)
21 |
22 | // Wrappers
23 | final case class Name(get: String) extends AnyVal
24 | object Name {
25 | def validate(name: String): ValidatedNel[String,Name] = {
26 | name.validate(lengthAtLeast(6) and onlyLettersOrDigits).map(n => Name(n))
27 | }
28 | }
29 | final case class Email(get: String) extends AnyVal
30 | object Email {
31 | def validate(email: String): ValidatedNel[String,Email] = {
32 | email.validate(containsAllChars("@.")).map(e => Email(e))
33 | }
34 | }
35 | final case class Password(get: String) extends AnyVal
36 | object Password {
37 | def validate(password: String): ValidatedNel[String,Password] = {
38 | password.validate(lengthAtLeast(8)).map(p => Password(p))
39 | }
40 | }
41 | final case class Session(get: UUID = UUID.randomUUID()) extends AnyVal
42 |
43 | // State
44 | final case class User(name: Name, email: Email, password: Password)
45 | object User {
46 | def validate(name: String, email: String, password: String): ValidatedNel[String,User] = {
47 | (Name.validate(name) |@| Email.validate(email) |@| Password.validate(password)).map { User.apply _ }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/server.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import com.twitter.finagle.Http
4 | import io.circe._
5 | import io.circe.syntax._
6 | import io.circe.generic.auto._
7 | import io.finch._
8 | import io.finch.circe._
9 |
10 | object DoodleBot {
11 | import doodlebot.endpoint._
12 | import doodlebot.model.FormErrors
13 |
14 | implicit val encodeException: Encoder[Exception] = Encoder.instance {
15 | case fe @ FormErrors(_) => fe.asJson
16 | case e => Json.obj(
17 | "message" -> Json.fromString(s"""Exception was thrown: ${e.getMessage}""")
18 | )
19 | }
20 |
21 | val service =
22 | Static.static :+: Signup.signup :+: Login.login :+: Chat.message :+: Chat.poll
23 |
24 | val server = {
25 | import doodlebot.model._
26 | import doodlebot.action.Store
27 | Store.signup(
28 | User(Name("tester"), Email("tester@example.com"), Password("password"))
29 | )
30 | Http.serve(":8080", service.toServiceAs[Application.Json])
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/syntax/validation.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package syntax
3 |
4 | import cats.data.{Validated,ValidatedNel}
5 |
6 | object validation {
7 | import doodlebot.validation._
8 |
9 | implicit class ValidationOps[A](a: A) {
10 | def validate(predicate: Predicate[A]): ValidatedNel[String,A] =
11 | predicate(a)
12 | }
13 |
14 | implicit class FormValidatedOps[A](validated: ValidatedNel[String,A]) {
15 | def forInput(id: String): Validated[InputError,A] =
16 | validated.leftMap { messages => InputError(id, messages) }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/validation/input-error.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package validation
3 |
4 | import cats.Monoid
5 | import cats.data.NonEmptyList
6 | import cats.std.list._
7 | import cats.std.map._
8 | import cats.syntax.semigroup._
9 |
10 | final case class InputError(messages: Map[String,NonEmptyList[String]])
11 | object InputError {
12 | val empty: InputError = InputError(Map.empty)
13 |
14 | def apply(name: String, messages: NonEmptyList[String]): InputError =
15 | InputError(Map(name -> messages))
16 |
17 | def apply(name: String, message: String): InputError =
18 | InputError(Map(name -> NonEmptyList(message)))
19 |
20 | implicit object inputErrorInstances extends Monoid[InputError] {
21 | override def combine(a1: InputError, a2: InputError): InputError =
22 | InputError(a1.messages |+| a2.messages)
23 |
24 | override def empty: InputError =
25 | InputError(Map.empty)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/main/scala/doodlebot/validation/predicate.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package validation
3 |
4 | import cats.data.{NonEmptyList,Validated,ValidatedNel}
5 | import cats.std.list._
6 | import cats.syntax.validated._
7 | import cats.syntax.semigroup._
8 |
9 | final case class Predicate[A](messages: NonEmptyList[String], check: A => Boolean) {
10 | def apply(a: A): ValidatedNel[String,A] =
11 | if(check(a))
12 | a.validNel
13 | else
14 | Validated.invalid(messages)
15 |
16 | def and(that: Predicate[A]): Predicate[A] =
17 | Predicate(this.messages |+| that.messages, (a: A) => this.check(a) && that.check(a))
18 | }
19 | object Predicate {
20 | def lift[A](message: String)(f: A => Boolean): Predicate[A] =
21 | Predicate(NonEmptyList(message), f)
22 |
23 | def lengthAtLeast(length: Int): Predicate[String] =
24 | Predicate.lift(s"Must be at least $length characters."){ string =>
25 | string.length >= length
26 | }
27 |
28 | val onlyLettersOrDigits: Predicate[String] =
29 | Predicate.lift("Must contain only letters or digits."){ string =>
30 | string.forall(_.isLetterOrDigit)
31 | }
32 |
33 | def containsAllChars(chars: String): Predicate[String] =
34 | Predicate.lift(s"Must contain all of $chars"){ string =>
35 | val chs = chars.toList
36 | chs.foldLeft(true){ (accum, elt) =>
37 | accum && string.contains(elt)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ui/src/main/resources/virtual-dom.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | /* jshint unused: false */
3 | /**
4 | * @typedef module
5 | * @type {object}
6 | * @property {string} id - the identifier for the module.
7 | * @property {string} filename - the fully resolved filename to the module.
8 | * @property {module} parent - the module that required this one.
9 | * @property {module[]} children - the module objects required by this one.
10 | * @property {boolean} loaded - whether or not the module is done loading, or is in the process of loading
11 | */
12 | /**
13 | *
14 | * Define scope for `require`
15 | */
16 | var _require = (function(){
17 | var /**
18 | * Store modules (types assigned to module.exports)
19 | * @type {module[]}
20 | */
21 | imports = [],
22 | /**
23 | * Store the code that constructs a module (and assigns to exports)
24 | * @type {*[]}
25 | */
26 | factories = [],
27 | /**
28 | * @type {module}
29 | */
30 | module = {},
31 | /**
32 | * Implement CommonJS `require`
33 | * http://wiki.commonjs.org/wiki/Modules/1.1.1
34 | * @param {string} filename
35 | * @returns {*}
36 | */
37 | __require = function( filename ) {
38 |
39 | if ( typeof imports[ filename ] !== "undefined" ) {
40 | return imports[ filename ].exports;
41 | }
42 | module = {
43 | id: filename,
44 | filename: filename,
45 | parent: module,
46 | children: [],
47 | exports: {},
48 | loaded: false
49 | };
50 | if ( typeof factories[ filename ] === "undefined" ) {
51 | throw new Error( "The factory of " + filename + " module not found" );
52 | }
53 | // Called first time, so let's run code constructing (exporting) the module
54 | imports[ filename ] = factories[ filename ]( _require, module.exports, module,
55 | typeof window !== "undefined" ? window : global );
56 | imports[ filename ].loaded = true;
57 | if ( imports[ filename ].parent.children ) {
58 | imports[ filename ].parent.children.push( imports[ filename ] );
59 | }
60 | return imports[ filename ].exports;
61 | };
62 | /**
63 | * Register module
64 | * @param {string} filename
65 | * @param {function(module, *)} moduleFactory
66 | */
67 | __require.def = function( filename, moduleFactory ) {
68 | factories[ filename ] = moduleFactory;
69 | };
70 | return __require;
71 | }());
72 | // Must run for UMD, but under CommonJS do not conflict with global require
73 | if ( typeof require === "undefined" ) {
74 | require = _require;
75 | }
76 | _require.def( "main.js", function( _require, exports, module, global ){
77 | var h = _require( "node_modules/virtual-dom/h.js" );
78 | var diff = _require( "node_modules/virtual-dom/diff.js" );
79 | var patch = _require( "node_modules/virtual-dom/patch.js" );
80 | var createElement = _require( "node_modules/virtual-dom/create-element.js" );
81 |
82 |
83 |
84 | return module;
85 | });
86 |
87 | _require.def( "node_modules/virtual-dom/h.js", function( _require, exports, module, global ){
88 | var h = _require( "node_modules/virtual-dom/virtual-hyperscript/index.js" )
89 |
90 | module.exports = h
91 |
92 |
93 |
94 | return module;
95 | });
96 |
97 | _require.def( "node_modules/virtual-dom/diff.js", function( _require, exports, module, global ){
98 | var diff = _require( "node_modules/virtual-dom/vtree/diff.js" )
99 |
100 | module.exports = diff
101 |
102 |
103 |
104 | return module;
105 | });
106 |
107 | _require.def( "node_modules/virtual-dom/patch.js", function( _require, exports, module, global ){
108 | var patch = _require( "node_modules/virtual-dom/vdom/patch.js" )
109 |
110 | module.exports = patch
111 |
112 |
113 |
114 | return module;
115 | });
116 |
117 | _require.def( "node_modules/virtual-dom/create-element.js", function( _require, exports, module, global ){
118 | var createElement = _require( "node_modules/virtual-dom/vdom/create-element.js" )
119 |
120 | module.exports = createElement
121 |
122 |
123 |
124 | return module;
125 | });
126 |
127 | _require.def( "node_modules/virtual-dom/virtual-hyperscript/index.js", function( _require, exports, module, global ){
128 | 'use strict';
129 |
130 | var isArray = _require( "node_modules/x-is-array/index.js" );
131 |
132 | var VNode = _require( "node_modules/virtual-dom/vnode/vnode.js" );
133 | var VText = _require( "node_modules/virtual-dom/vnode/vtext.js" );
134 | var isVNode = _require( "node_modules/virtual-dom/vnode/is-vnode.js" );
135 | var isVText = _require( "node_modules/virtual-dom/vnode/is-vtext.js" );
136 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" );
137 | var isHook = _require( "node_modules/virtual-dom/vnode/is-vhook.js" );
138 | var isVThunk = _require( "node_modules/virtual-dom/vnode/is-thunk.js" );
139 |
140 | var parseTag = _require( "node_modules/virtual-dom/virtual-hyperscript/parse-tag.js" );
141 | var softSetHook = _require( "node_modules/virtual-dom/virtual-hyperscript/hooks/soft-set-hook.js" );
142 | var evHook = _require( "node_modules/virtual-dom/virtual-hyperscript/hooks/ev-hook.js" );
143 |
144 | module.exports = h;
145 |
146 | function h(tagName, properties, children) {
147 | var childNodes = [];
148 | var tag, props, key, namespace;
149 |
150 | if (!children && isChildren(properties)) {
151 | children = properties;
152 | props = {};
153 | }
154 |
155 | props = props || properties || {};
156 | tag = parseTag(tagName, props);
157 |
158 | // support keys
159 | if (props.hasOwnProperty('key')) {
160 | key = props.key;
161 | props.key = undefined;
162 | }
163 |
164 | // support namespace
165 | if (props.hasOwnProperty('namespace')) {
166 | namespace = props.namespace;
167 | props.namespace = undefined;
168 | }
169 |
170 | // fix cursor bug
171 | if (tag === 'INPUT' &&
172 | !namespace &&
173 | props.hasOwnProperty('value') &&
174 | props.value !== undefined &&
175 | !isHook(props.value)
176 | ) {
177 | props.value = softSetHook(props.value);
178 | }
179 |
180 | transformProperties(props);
181 |
182 | if (children !== undefined && children !== null) {
183 | addChild(children, childNodes, tag, props);
184 | }
185 |
186 |
187 | return new VNode(tag, props, childNodes, key, namespace);
188 | }
189 |
190 | function addChild(c, childNodes, tag, props) {
191 | if (typeof c === 'string') {
192 | childNodes.push(new VText(c));
193 | } else if (typeof c === 'number') {
194 | childNodes.push(new VText(String(c)));
195 | } else if (isChild(c)) {
196 | childNodes.push(c);
197 | } else if (isArray(c)) {
198 | for (var i = 0; i < c.length; i++) {
199 | addChild(c[i], childNodes, tag, props);
200 | }
201 | } else if (c === null || c === undefined) {
202 | return;
203 | } else {
204 | throw UnexpectedVirtualElement({
205 | foreignObject: c,
206 | parentVnode: {
207 | tagName: tag,
208 | properties: props
209 | }
210 | });
211 | }
212 | }
213 |
214 | function transformProperties(props) {
215 | for (var propName in props) {
216 | if (props.hasOwnProperty(propName)) {
217 | var value = props[propName];
218 |
219 | if (isHook(value)) {
220 | continue;
221 | }
222 |
223 | if (propName.substr(0, 3) === 'ev-') {
224 | // add ev-foo support
225 | props[propName] = evHook(value);
226 | }
227 | }
228 | }
229 | }
230 |
231 | function isChild(x) {
232 | return isVNode(x) || isVText(x) || isWidget(x) || isVThunk(x);
233 | }
234 |
235 | function isChildren(x) {
236 | return typeof x === 'string' || isArray(x) || isChild(x);
237 | }
238 |
239 | function UnexpectedVirtualElement(data) {
240 | var err = new Error();
241 |
242 | err.type = 'virtual-hyperscript.unexpected.virtual-element';
243 | err.message = 'Unexpected virtual child passed to h().\n' +
244 | 'Expected a VNode / Vthunk / VWidget / string but:\n' +
245 | 'got:\n' +
246 | errorString(data.foreignObject) +
247 | '.\n' +
248 | 'The parent vnode is:\n' +
249 | errorString(data.parentVnode)
250 | '\n' +
251 | 'Suggested fix: change your `h(..., [ ... ])` callsite.';
252 | err.foreignObject = data.foreignObject;
253 | err.parentVnode = data.parentVnode;
254 |
255 | return err;
256 | }
257 |
258 | function errorString(obj) {
259 | try {
260 | return JSON.stringify(obj, null, ' ');
261 | } catch (e) {
262 | return String(obj);
263 | }
264 | }
265 |
266 |
267 |
268 | return module;
269 | });
270 |
271 | _require.def( "node_modules/virtual-dom/vtree/diff.js", function( _require, exports, module, global ){
272 | var isArray = _require( "node_modules/x-is-array/index.js" )
273 |
274 | var VPatch = _require( "node_modules/virtual-dom/vnode/vpatch.js" )
275 | var isVNode = _require( "node_modules/virtual-dom/vnode/is-vnode.js" )
276 | var isVText = _require( "node_modules/virtual-dom/vnode/is-vtext.js" )
277 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
278 | var isThunk = _require( "node_modules/virtual-dom/vnode/is-thunk.js" )
279 | var handleThunk = _require( "node_modules/virtual-dom/vnode/handle-thunk.js" )
280 |
281 | var diffProps = _require( "node_modules/virtual-dom/vtree/diff-props.js" )
282 |
283 | module.exports = diff
284 |
285 | function diff(a, b) {
286 | var patch = { a: a }
287 | walk(a, b, patch, 0)
288 | return patch
289 | }
290 |
291 | function walk(a, b, patch, index) {
292 | if (a === b) {
293 | return
294 | }
295 |
296 | var apply = patch[index]
297 | var applyClear = false
298 |
299 | if (isThunk(a) || isThunk(b)) {
300 | thunks(a, b, patch, index)
301 | } else if (b == null) {
302 |
303 | // If a is a widget we will add a remove patch for it
304 | // Otherwise any child widgets/hooks must be destroyed.
305 | // This prevents adding two remove patches for a widget.
306 | if (!isWidget(a)) {
307 | clearState(a, patch, index)
308 | apply = patch[index]
309 | }
310 |
311 | apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
312 | } else if (isVNode(b)) {
313 | if (isVNode(a)) {
314 | if (a.tagName === b.tagName &&
315 | a.namespace === b.namespace &&
316 | a.key === b.key) {
317 | var propsPatch = diffProps(a.properties, b.properties)
318 | if (propsPatch) {
319 | apply = appendPatch(apply,
320 | new VPatch(VPatch.PROPS, a, propsPatch))
321 | }
322 | apply = diffChildren(a, b, patch, apply, index)
323 | } else {
324 | apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
325 | applyClear = true
326 | }
327 | } else {
328 | apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
329 | applyClear = true
330 | }
331 | } else if (isVText(b)) {
332 | if (!isVText(a)) {
333 | apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
334 | applyClear = true
335 | } else if (a.text !== b.text) {
336 | apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
337 | }
338 | } else if (isWidget(b)) {
339 | if (!isWidget(a)) {
340 | applyClear = true
341 | }
342 |
343 | apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
344 | }
345 |
346 | if (apply) {
347 | patch[index] = apply
348 | }
349 |
350 | if (applyClear) {
351 | clearState(a, patch, index)
352 | }
353 | }
354 |
355 | function diffChildren(a, b, patch, apply, index) {
356 | var aChildren = a.children
357 | var orderedSet = reorder(aChildren, b.children)
358 | var bChildren = orderedSet.children
359 |
360 | var aLen = aChildren.length
361 | var bLen = bChildren.length
362 | var len = aLen > bLen ? aLen : bLen
363 |
364 | for (var i = 0; i < len; i++) {
365 | var leftNode = aChildren[i]
366 | var rightNode = bChildren[i]
367 | index += 1
368 |
369 | if (!leftNode) {
370 | if (rightNode) {
371 | // Excess nodes in b need to be added
372 | apply = appendPatch(apply,
373 | new VPatch(VPatch.INSERT, null, rightNode))
374 | }
375 | } else {
376 | walk(leftNode, rightNode, patch, index)
377 | }
378 |
379 | if (isVNode(leftNode) && leftNode.count) {
380 | index += leftNode.count
381 | }
382 | }
383 |
384 | if (orderedSet.moves) {
385 | // Reorder nodes last
386 | apply = appendPatch(apply, new VPatch(
387 | VPatch.ORDER,
388 | a,
389 | orderedSet.moves
390 | ))
391 | }
392 |
393 | return apply
394 | }
395 |
396 | function clearState(vNode, patch, index) {
397 | // TODO: Make this a single walk, not two
398 | unhook(vNode, patch, index)
399 | destroyWidgets(vNode, patch, index)
400 | }
401 |
402 | // Patch records for all destroyed widgets must be added because we need
403 | // a DOM node reference for the destroy function
404 | function destroyWidgets(vNode, patch, index) {
405 | if (isWidget(vNode)) {
406 | if (typeof vNode.destroy === "function") {
407 | patch[index] = appendPatch(
408 | patch[index],
409 | new VPatch(VPatch.REMOVE, vNode, null)
410 | )
411 | }
412 | } else if (isVNode(vNode) && (vNode.hasWidgets || vNode.hasThunks)) {
413 | var children = vNode.children
414 | var len = children.length
415 | for (var i = 0; i < len; i++) {
416 | var child = children[i]
417 | index += 1
418 |
419 | destroyWidgets(child, patch, index)
420 |
421 | if (isVNode(child) && child.count) {
422 | index += child.count
423 | }
424 | }
425 | } else if (isThunk(vNode)) {
426 | thunks(vNode, null, patch, index)
427 | }
428 | }
429 |
430 | // Create a sub-patch for thunks
431 | function thunks(a, b, patch, index) {
432 | var nodes = handleThunk(a, b)
433 | var thunkPatch = diff(nodes.a, nodes.b)
434 | if (hasPatches(thunkPatch)) {
435 | patch[index] = new VPatch(VPatch.THUNK, null, thunkPatch)
436 | }
437 | }
438 |
439 | function hasPatches(patch) {
440 | for (var index in patch) {
441 | if (index !== "a") {
442 | return true
443 | }
444 | }
445 |
446 | return false
447 | }
448 |
449 | // Execute hooks when two nodes are identical
450 | function unhook(vNode, patch, index) {
451 | if (isVNode(vNode)) {
452 | if (vNode.hooks) {
453 | patch[index] = appendPatch(
454 | patch[index],
455 | new VPatch(
456 | VPatch.PROPS,
457 | vNode,
458 | undefinedKeys(vNode.hooks)
459 | )
460 | )
461 | }
462 |
463 | if (vNode.descendantHooks || vNode.hasThunks) {
464 | var children = vNode.children
465 | var len = children.length
466 | for (var i = 0; i < len; i++) {
467 | var child = children[i]
468 | index += 1
469 |
470 | unhook(child, patch, index)
471 |
472 | if (isVNode(child) && child.count) {
473 | index += child.count
474 | }
475 | }
476 | }
477 | } else if (isThunk(vNode)) {
478 | thunks(vNode, null, patch, index)
479 | }
480 | }
481 |
482 | function undefinedKeys(obj) {
483 | var result = {}
484 |
485 | for (var key in obj) {
486 | result[key] = undefined
487 | }
488 |
489 | return result
490 | }
491 |
492 | // List diff, naive left to right reordering
493 | function reorder(aChildren, bChildren) {
494 | // O(M) time, O(M) memory
495 | var bChildIndex = keyIndex(bChildren)
496 | var bKeys = bChildIndex.keys
497 | var bFree = bChildIndex.free
498 |
499 | if (bFree.length === bChildren.length) {
500 | return {
501 | children: bChildren,
502 | moves: null
503 | }
504 | }
505 |
506 | // O(N) time, O(N) memory
507 | var aChildIndex = keyIndex(aChildren)
508 | var aKeys = aChildIndex.keys
509 | var aFree = aChildIndex.free
510 |
511 | if (aFree.length === aChildren.length) {
512 | return {
513 | children: bChildren,
514 | moves: null
515 | }
516 | }
517 |
518 | // O(MAX(N, M)) memory
519 | var newChildren = []
520 |
521 | var freeIndex = 0
522 | var freeCount = bFree.length
523 | var deletedItems = 0
524 |
525 | // Iterate through a and match a node in b
526 | // O(N) time,
527 | for (var i = 0 ; i < aChildren.length; i++) {
528 | var aItem = aChildren[i]
529 | var itemIndex
530 |
531 | if (aItem.key) {
532 | if (bKeys.hasOwnProperty(aItem.key)) {
533 | // Match up the old keys
534 | itemIndex = bKeys[aItem.key]
535 | newChildren.push(bChildren[itemIndex])
536 |
537 | } else {
538 | // Remove old keyed items
539 | itemIndex = i - deletedItems++
540 | newChildren.push(null)
541 | }
542 | } else {
543 | // Match the item in a with the next free item in b
544 | if (freeIndex < freeCount) {
545 | itemIndex = bFree[freeIndex++]
546 | newChildren.push(bChildren[itemIndex])
547 | } else {
548 | // There are no free items in b to match with
549 | // the free items in a, so the extra free nodes
550 | // are deleted.
551 | itemIndex = i - deletedItems++
552 | newChildren.push(null)
553 | }
554 | }
555 | }
556 |
557 | var lastFreeIndex = freeIndex >= bFree.length ?
558 | bChildren.length :
559 | bFree[freeIndex]
560 |
561 | // Iterate through b and append any new keys
562 | // O(M) time
563 | for (var j = 0; j < bChildren.length; j++) {
564 | var newItem = bChildren[j]
565 |
566 | if (newItem.key) {
567 | if (!aKeys.hasOwnProperty(newItem.key)) {
568 | // Add any new keyed items
569 | // We are adding new items to the end and then sorting them
570 | // in place. In future we should insert new items in place.
571 | newChildren.push(newItem)
572 | }
573 | } else if (j >= lastFreeIndex) {
574 | // Add any leftover non-keyed items
575 | newChildren.push(newItem)
576 | }
577 | }
578 |
579 | var simulate = newChildren.slice()
580 | var simulateIndex = 0
581 | var removes = []
582 | var inserts = []
583 | var simulateItem
584 |
585 | for (var k = 0; k < bChildren.length;) {
586 | var wantedItem = bChildren[k]
587 | simulateItem = simulate[simulateIndex]
588 |
589 | // remove items
590 | while (simulateItem === null && simulate.length) {
591 | removes.push(remove(simulate, simulateIndex, null))
592 | simulateItem = simulate[simulateIndex]
593 | }
594 |
595 | if (!simulateItem || simulateItem.key !== wantedItem.key) {
596 | // if we need a key in this position...
597 | if (wantedItem.key) {
598 | if (simulateItem && simulateItem.key) {
599 | // if an insert doesn't put this key in place, it needs to move
600 | if (bKeys[simulateItem.key] !== k + 1) {
601 | removes.push(remove(simulate, simulateIndex, simulateItem.key))
602 | simulateItem = simulate[simulateIndex]
603 | // if the remove didn't put the wanted item in place, we need to insert it
604 | if (!simulateItem || simulateItem.key !== wantedItem.key) {
605 | inserts.push({key: wantedItem.key, to: k})
606 | }
607 | // items are matching, so skip ahead
608 | else {
609 | simulateIndex++
610 | }
611 | }
612 | else {
613 | inserts.push({key: wantedItem.key, to: k})
614 | }
615 | }
616 | else {
617 | inserts.push({key: wantedItem.key, to: k})
618 | }
619 | k++
620 | }
621 | // a key in simulate has no matching wanted key, remove it
622 | else if (simulateItem && simulateItem.key) {
623 | removes.push(remove(simulate, simulateIndex, simulateItem.key))
624 | }
625 | }
626 | else {
627 | simulateIndex++
628 | k++
629 | }
630 | }
631 |
632 | // remove all the remaining nodes from simulate
633 | while(simulateIndex < simulate.length) {
634 | simulateItem = simulate[simulateIndex]
635 | removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key))
636 | }
637 |
638 | // If the only moves we have are deletes then we can just
639 | // let the delete patch remove these items.
640 | if (removes.length === deletedItems && !inserts.length) {
641 | return {
642 | children: newChildren,
643 | moves: null
644 | }
645 | }
646 |
647 | return {
648 | children: newChildren,
649 | moves: {
650 | removes: removes,
651 | inserts: inserts
652 | }
653 | }
654 | }
655 |
656 | function remove(arr, index, key) {
657 | arr.splice(index, 1)
658 |
659 | return {
660 | from: index,
661 | key: key
662 | }
663 | }
664 |
665 | function keyIndex(children) {
666 | var keys = {}
667 | var free = []
668 | var length = children.length
669 |
670 | for (var i = 0; i < length; i++) {
671 | var child = children[i]
672 |
673 | if (child.key) {
674 | keys[child.key] = i
675 | } else {
676 | free.push(i)
677 | }
678 | }
679 |
680 | return {
681 | keys: keys, // A hash of key name to index
682 | free: free // An array of unkeyed item indices
683 | }
684 | }
685 |
686 | function appendPatch(apply, patch) {
687 | if (apply) {
688 | if (isArray(apply)) {
689 | apply.push(patch)
690 | } else {
691 | apply = [apply, patch]
692 | }
693 |
694 | return apply
695 | } else {
696 | return patch
697 | }
698 | }
699 |
700 |
701 |
702 | return module;
703 | });
704 |
705 | _require.def( "node_modules/virtual-dom/vdom/patch.js", function( _require, exports, module, global ){
706 | var document = _require( "node_modules/global/document.js" )
707 | var isArray = _require( "node_modules/x-is-array/index.js" )
708 |
709 | var render = _require( "node_modules/virtual-dom/vdom/create-element.js" )
710 | var domIndex = _require( "node_modules/virtual-dom/vdom/dom-index.js" )
711 | var patchOp = _require( "node_modules/virtual-dom/vdom/patch-op.js" )
712 | module.exports = patch
713 |
714 | function patch(rootNode, patches, renderOptions) {
715 | renderOptions = renderOptions || {}
716 | renderOptions.patch = renderOptions.patch && renderOptions.patch !== patch
717 | ? renderOptions.patch
718 | : patchRecursive
719 | renderOptions.render = renderOptions.render || render
720 |
721 | return renderOptions.patch(rootNode, patches, renderOptions)
722 | }
723 |
724 | function patchRecursive(rootNode, patches, renderOptions) {
725 | var indices = patchIndices(patches)
726 |
727 | if (indices.length === 0) {
728 | return rootNode
729 | }
730 |
731 | var index = domIndex(rootNode, patches.a, indices)
732 | var ownerDocument = rootNode.ownerDocument
733 |
734 | if (!renderOptions.document && ownerDocument !== document) {
735 | renderOptions.document = ownerDocument
736 | }
737 |
738 | for (var i = 0; i < indices.length; i++) {
739 | var nodeIndex = indices[i]
740 | rootNode = applyPatch(rootNode,
741 | index[nodeIndex],
742 | patches[nodeIndex],
743 | renderOptions)
744 | }
745 |
746 | return rootNode
747 | }
748 |
749 | function applyPatch(rootNode, domNode, patchList, renderOptions) {
750 | if (!domNode) {
751 | return rootNode
752 | }
753 |
754 | var newNode
755 |
756 | if (isArray(patchList)) {
757 | for (var i = 0; i < patchList.length; i++) {
758 | newNode = patchOp(patchList[i], domNode, renderOptions)
759 |
760 | if (domNode === rootNode) {
761 | rootNode = newNode
762 | }
763 | }
764 | } else {
765 | newNode = patchOp(patchList, domNode, renderOptions)
766 |
767 | if (domNode === rootNode) {
768 | rootNode = newNode
769 | }
770 | }
771 |
772 | return rootNode
773 | }
774 |
775 | function patchIndices(patches) {
776 | var indices = []
777 |
778 | for (var key in patches) {
779 | if (key !== "a") {
780 | indices.push(Number(key))
781 | }
782 | }
783 |
784 | return indices
785 | }
786 |
787 |
788 |
789 | return module;
790 | });
791 |
792 | _require.def( "node_modules/virtual-dom/vdom/create-element.js", function( _require, exports, module, global ){
793 | var document = _require( "node_modules/global/document.js" )
794 |
795 | var applyProperties = _require( "node_modules/virtual-dom/vdom/apply-properties.js" )
796 |
797 | var isVNode = _require( "node_modules/virtual-dom/vnode/is-vnode.js" )
798 | var isVText = _require( "node_modules/virtual-dom/vnode/is-vtext.js" )
799 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
800 | var handleThunk = _require( "node_modules/virtual-dom/vnode/handle-thunk.js" )
801 |
802 | module.exports = createElement
803 |
804 | function createElement(vnode, opts) {
805 | var doc = opts ? opts.document || document : document
806 | var warn = opts ? opts.warn : null
807 |
808 | vnode = handleThunk(vnode).a
809 |
810 | if (isWidget(vnode)) {
811 | return vnode.init()
812 | } else if (isVText(vnode)) {
813 | return doc.createTextNode(vnode.text)
814 | } else if (!isVNode(vnode)) {
815 | if (warn) {
816 | warn("Item is not a valid virtual dom node", vnode)
817 | }
818 | return null
819 | }
820 |
821 | var node = (vnode.namespace === null) ?
822 | doc.createElement(vnode.tagName) :
823 | doc.createElementNS(vnode.namespace, vnode.tagName)
824 |
825 | var props = vnode.properties
826 | applyProperties(node, props)
827 |
828 | var children = vnode.children
829 |
830 | for (var i = 0; i < children.length; i++) {
831 | var childNode = createElement(children[i], opts)
832 | if (childNode) {
833 | node.appendChild(childNode)
834 | }
835 | }
836 |
837 | return node
838 | }
839 |
840 |
841 |
842 | return module;
843 | });
844 |
845 | _require.def( "node_modules/x-is-array/index.js", function( _require, exports, module, global ){
846 | var nativeIsArray = Array.isArray
847 | var toString = Object.prototype.toString
848 |
849 | module.exports = nativeIsArray || isArray
850 |
851 | function isArray(obj) {
852 | return toString.call(obj) === "[object Array]"
853 | }
854 |
855 |
856 |
857 | return module;
858 | });
859 |
860 | _require.def( "node_modules/virtual-dom/vnode/vnode.js", function( _require, exports, module, global ){
861 | var version = _require( "node_modules/virtual-dom/vnode/version.js" )
862 | var isVNode = _require( "node_modules/virtual-dom/vnode/is-vnode.js" )
863 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
864 | var isThunk = _require( "node_modules/virtual-dom/vnode/is-thunk.js" )
865 | var isVHook = _require( "node_modules/virtual-dom/vnode/is-vhook.js" )
866 |
867 | module.exports = VirtualNode
868 |
869 | var noProperties = {}
870 | var noChildren = []
871 |
872 | function VirtualNode(tagName, properties, children, key, namespace) {
873 | this.tagName = tagName
874 | this.properties = properties || noProperties
875 | this.children = children || noChildren
876 | this.key = key != null ? String(key) : undefined
877 | this.namespace = (typeof namespace === "string") ? namespace : null
878 |
879 | var count = (children && children.length) || 0
880 | var descendants = 0
881 | var hasWidgets = false
882 | var hasThunks = false
883 | var descendantHooks = false
884 | var hooks
885 |
886 | for (var propName in properties) {
887 | if (properties.hasOwnProperty(propName)) {
888 | var property = properties[propName]
889 | if (isVHook(property) && property.unhook) {
890 | if (!hooks) {
891 | hooks = {}
892 | }
893 |
894 | hooks[propName] = property
895 | }
896 | }
897 | }
898 |
899 | for (var i = 0; i < count; i++) {
900 | var child = children[i]
901 | if (isVNode(child)) {
902 | descendants += child.count || 0
903 |
904 | if (!hasWidgets && child.hasWidgets) {
905 | hasWidgets = true
906 | }
907 |
908 | if (!hasThunks && child.hasThunks) {
909 | hasThunks = true
910 | }
911 |
912 | if (!descendantHooks && (child.hooks || child.descendantHooks)) {
913 | descendantHooks = true
914 | }
915 | } else if (!hasWidgets && isWidget(child)) {
916 | if (typeof child.destroy === "function") {
917 | hasWidgets = true
918 | }
919 | } else if (!hasThunks && isThunk(child)) {
920 | hasThunks = true;
921 | }
922 | }
923 |
924 | this.count = count + descendants
925 | this.hasWidgets = hasWidgets
926 | this.hasThunks = hasThunks
927 | this.hooks = hooks
928 | this.descendantHooks = descendantHooks
929 | }
930 |
931 | VirtualNode.prototype.version = version
932 | VirtualNode.prototype.type = "VirtualNode"
933 |
934 |
935 |
936 | return module;
937 | });
938 |
939 | _require.def( "node_modules/virtual-dom/vnode/vtext.js", function( _require, exports, module, global ){
940 | var version = _require( "node_modules/virtual-dom/vnode/version.js" )
941 |
942 | module.exports = VirtualText
943 |
944 | function VirtualText(text) {
945 | this.text = String(text)
946 | }
947 |
948 | VirtualText.prototype.version = version
949 | VirtualText.prototype.type = "VirtualText"
950 |
951 |
952 |
953 | return module;
954 | });
955 |
956 | _require.def( "node_modules/virtual-dom/vnode/is-vnode.js", function( _require, exports, module, global ){
957 | var version = _require( "node_modules/virtual-dom/vnode/version.js" )
958 |
959 | module.exports = isVirtualNode
960 |
961 | function isVirtualNode(x) {
962 | return x && x.type === "VirtualNode" && x.version === version
963 | }
964 |
965 |
966 |
967 | return module;
968 | });
969 |
970 | _require.def( "node_modules/virtual-dom/vnode/is-vtext.js", function( _require, exports, module, global ){
971 | var version = _require( "node_modules/virtual-dom/vnode/version.js" )
972 |
973 | module.exports = isVirtualText
974 |
975 | function isVirtualText(x) {
976 | return x && x.type === "VirtualText" && x.version === version
977 | }
978 |
979 |
980 |
981 | return module;
982 | });
983 |
984 | _require.def( "node_modules/virtual-dom/vnode/is-vhook.js", function( _require, exports, module, global ){
985 | module.exports = isHook
986 |
987 | function isHook(hook) {
988 | return hook &&
989 | (typeof hook.hook === "function" && !hook.hasOwnProperty("hook") ||
990 | typeof hook.unhook === "function" && !hook.hasOwnProperty("unhook"))
991 | }
992 |
993 |
994 |
995 | return module;
996 | });
997 |
998 | _require.def( "node_modules/virtual-dom/vnode/is-thunk.js", function( _require, exports, module, global ){
999 | module.exports = isThunk
1000 |
1001 | function isThunk(t) {
1002 | return t && t.type === "Thunk"
1003 | }
1004 |
1005 |
1006 |
1007 | return module;
1008 | });
1009 |
1010 | _require.def( "node_modules/virtual-dom/vnode/is-widget.js", function( _require, exports, module, global ){
1011 | module.exports = isWidget
1012 |
1013 | function isWidget(w) {
1014 | return w && w.type === "Widget"
1015 | }
1016 |
1017 |
1018 |
1019 | return module;
1020 | });
1021 |
1022 | _require.def( "node_modules/virtual-dom/virtual-hyperscript/parse-tag.js", function( _require, exports, module, global ){
1023 | 'use strict';
1024 |
1025 | var split = _require( "node_modules/browser-split/index.js" );
1026 |
1027 | var classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/;
1028 | var notClassId = /^\.|#/;
1029 |
1030 | module.exports = parseTag;
1031 |
1032 | function parseTag(tag, props) {
1033 | if (!tag) {
1034 | return 'DIV';
1035 | }
1036 |
1037 | var noId = !(props.hasOwnProperty('id'));
1038 |
1039 | var tagParts = split(tag, classIdSplit);
1040 | var tagName = null;
1041 |
1042 | if (notClassId.test(tagParts[1])) {
1043 | tagName = 'DIV';
1044 | }
1045 |
1046 | var classes, part, type, i;
1047 |
1048 | for (i = 0; i < tagParts.length; i++) {
1049 | part = tagParts[i];
1050 |
1051 | if (!part) {
1052 | continue;
1053 | }
1054 |
1055 | type = part.charAt(0);
1056 |
1057 | if (!tagName) {
1058 | tagName = part;
1059 | } else if (type === '.') {
1060 | classes = classes || [];
1061 | classes.push(part.substring(1, part.length));
1062 | } else if (type === '#' && noId) {
1063 | props.id = part.substring(1, part.length);
1064 | }
1065 | }
1066 |
1067 | if (classes) {
1068 | if (props.className) {
1069 | classes.push(props.className);
1070 | }
1071 |
1072 | props.className = classes.join(' ');
1073 | }
1074 |
1075 | return props.namespace ? tagName : tagName.toUpperCase();
1076 | }
1077 |
1078 |
1079 |
1080 | return module;
1081 | });
1082 |
1083 | _require.def( "node_modules/virtual-dom/virtual-hyperscript/hooks/soft-set-hook.js", function( _require, exports, module, global ){
1084 | 'use strict';
1085 |
1086 | module.exports = SoftSetHook;
1087 |
1088 | function SoftSetHook(value) {
1089 | if (!(this instanceof SoftSetHook)) {
1090 | return new SoftSetHook(value);
1091 | }
1092 |
1093 | this.value = value;
1094 | }
1095 |
1096 | SoftSetHook.prototype.hook = function (node, propertyName) {
1097 | if (node[propertyName] !== this.value) {
1098 | node[propertyName] = this.value;
1099 | }
1100 | };
1101 |
1102 |
1103 |
1104 | return module;
1105 | });
1106 |
1107 | _require.def( "node_modules/virtual-dom/virtual-hyperscript/hooks/ev-hook.js", function( _require, exports, module, global ){
1108 | 'use strict';
1109 |
1110 | var EvStore = _require( "node_modules/ev-store/index.js" );
1111 |
1112 | module.exports = EvHook;
1113 |
1114 | function EvHook(value) {
1115 | if (!(this instanceof EvHook)) {
1116 | return new EvHook(value);
1117 | }
1118 |
1119 | this.value = value;
1120 | }
1121 |
1122 | EvHook.prototype.hook = function (node, propertyName) {
1123 | var es = EvStore(node);
1124 | var propName = propertyName.substr(3);
1125 |
1126 | es[propName] = this.value;
1127 | };
1128 |
1129 | EvHook.prototype.unhook = function(node, propertyName) {
1130 | var es = EvStore(node);
1131 | var propName = propertyName.substr(3);
1132 |
1133 | es[propName] = undefined;
1134 | };
1135 |
1136 |
1137 |
1138 | return module;
1139 | });
1140 |
1141 | _require.def( "node_modules/virtual-dom/vnode/vpatch.js", function( _require, exports, module, global ){
1142 | var version = _require( "node_modules/virtual-dom/vnode/version.js" )
1143 |
1144 | VirtualPatch.NONE = 0
1145 | VirtualPatch.VTEXT = 1
1146 | VirtualPatch.VNODE = 2
1147 | VirtualPatch.WIDGET = 3
1148 | VirtualPatch.PROPS = 4
1149 | VirtualPatch.ORDER = 5
1150 | VirtualPatch.INSERT = 6
1151 | VirtualPatch.REMOVE = 7
1152 | VirtualPatch.THUNK = 8
1153 |
1154 | module.exports = VirtualPatch
1155 |
1156 | function VirtualPatch(type, vNode, patch) {
1157 | this.type = Number(type)
1158 | this.vNode = vNode
1159 | this.patch = patch
1160 | }
1161 |
1162 | VirtualPatch.prototype.version = version
1163 | VirtualPatch.prototype.type = "VirtualPatch"
1164 |
1165 |
1166 |
1167 | return module;
1168 | });
1169 |
1170 | _require.def( "node_modules/virtual-dom/vnode/handle-thunk.js", function( _require, exports, module, global ){
1171 | var isVNode = _require( "node_modules/virtual-dom/vnode/is-vnode.js" )
1172 | var isVText = _require( "node_modules/virtual-dom/vnode/is-vtext.js" )
1173 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
1174 | var isThunk = _require( "node_modules/virtual-dom/vnode/is-thunk.js" )
1175 |
1176 | module.exports = handleThunk
1177 |
1178 | function handleThunk(a, b) {
1179 | var renderedA = a
1180 | var renderedB = b
1181 |
1182 | if (isThunk(b)) {
1183 | renderedB = renderThunk(b, a)
1184 | }
1185 |
1186 | if (isThunk(a)) {
1187 | renderedA = renderThunk(a, null)
1188 | }
1189 |
1190 | return {
1191 | a: renderedA,
1192 | b: renderedB
1193 | }
1194 | }
1195 |
1196 | function renderThunk(thunk, previous) {
1197 | var renderedThunk = thunk.vnode
1198 |
1199 | if (!renderedThunk) {
1200 | renderedThunk = thunk.vnode = thunk.render(previous)
1201 | }
1202 |
1203 | if (!(isVNode(renderedThunk) ||
1204 | isVText(renderedThunk) ||
1205 | isWidget(renderedThunk))) {
1206 | throw new Error("thunk did not return a valid node");
1207 | }
1208 |
1209 | return renderedThunk
1210 | }
1211 |
1212 |
1213 |
1214 | return module;
1215 | });
1216 |
1217 | _require.def( "node_modules/virtual-dom/vtree/diff-props.js", function( _require, exports, module, global ){
1218 | var isObject = _require( "node_modules/is-object/index.js" )
1219 | var isHook = _require( "node_modules/virtual-dom/vnode/is-vhook.js" )
1220 |
1221 | module.exports = diffProps
1222 |
1223 | function diffProps(a, b) {
1224 | var diff
1225 |
1226 | for (var aKey in a) {
1227 | if (!(aKey in b)) {
1228 | diff = diff || {}
1229 | diff[aKey] = undefined
1230 | }
1231 |
1232 | var aValue = a[aKey]
1233 | var bValue = b[aKey]
1234 |
1235 | if (aValue === bValue) {
1236 | continue
1237 | } else if (isObject(aValue) && isObject(bValue)) {
1238 | if (getPrototype(bValue) !== getPrototype(aValue)) {
1239 | diff = diff || {}
1240 | diff[aKey] = bValue
1241 | } else if (isHook(bValue)) {
1242 | diff = diff || {}
1243 | diff[aKey] = bValue
1244 | } else {
1245 | var objectDiff = diffProps(aValue, bValue)
1246 | if (objectDiff) {
1247 | diff = diff || {}
1248 | diff[aKey] = objectDiff
1249 | }
1250 | }
1251 | } else {
1252 | diff = diff || {}
1253 | diff[aKey] = bValue
1254 | }
1255 | }
1256 |
1257 | for (var bKey in b) {
1258 | if (!(bKey in a)) {
1259 | diff = diff || {}
1260 | diff[bKey] = b[bKey]
1261 | }
1262 | }
1263 |
1264 | return diff
1265 | }
1266 |
1267 | function getPrototype(value) {
1268 | if (Object.getPrototypeOf) {
1269 | return Object.getPrototypeOf(value)
1270 | } else if (value.__proto__) {
1271 | return value.__proto__
1272 | } else if (value.constructor) {
1273 | return value.constructor.prototype
1274 | }
1275 | }
1276 |
1277 |
1278 |
1279 | return module;
1280 | });
1281 |
1282 | _require.def( "node_modules/global/document.js", function( _require, exports, module, global ){
1283 | var topLevel = typeof global !== 'undefined' ? global :
1284 | typeof window !== 'undefined' ? window : {}
1285 | var minDoc = _require( "node_modules/min-document/index.js" );
1286 |
1287 | if (typeof document !== 'undefined') {
1288 | module.exports = document;
1289 | } else {
1290 | var doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
1291 |
1292 | if (!doccy) {
1293 | doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
1294 | }
1295 |
1296 | module.exports = doccy;
1297 | }
1298 |
1299 |
1300 |
1301 | return module;
1302 | });
1303 |
1304 | _require.def( "node_modules/virtual-dom/vdom/dom-index.js", function( _require, exports, module, global ){
1305 | // Maps a virtual DOM tree onto a real DOM tree in an efficient manner.
1306 | // We don't want to read all of the DOM nodes in the tree so we use
1307 | // the in-order tree indexing to eliminate recursion down certain branches.
1308 | // We only recurse into a DOM node if we know that it contains a child of
1309 | // interest.
1310 |
1311 | var noChild = {}
1312 |
1313 | module.exports = domIndex
1314 |
1315 | function domIndex(rootNode, tree, indices, nodes) {
1316 | if (!indices || indices.length === 0) {
1317 | return {}
1318 | } else {
1319 | indices.sort(ascending)
1320 | return recurse(rootNode, tree, indices, nodes, 0)
1321 | }
1322 | }
1323 |
1324 | function recurse(rootNode, tree, indices, nodes, rootIndex) {
1325 | nodes = nodes || {}
1326 |
1327 |
1328 | if (rootNode) {
1329 | if (indexInRange(indices, rootIndex, rootIndex)) {
1330 | nodes[rootIndex] = rootNode
1331 | }
1332 |
1333 | var vChildren = tree.children
1334 |
1335 | if (vChildren) {
1336 |
1337 | var childNodes = rootNode.childNodes
1338 |
1339 | for (var i = 0; i < tree.children.length; i++) {
1340 | rootIndex += 1
1341 |
1342 | var vChild = vChildren[i] || noChild
1343 | var nextIndex = rootIndex + (vChild.count || 0)
1344 |
1345 | // skip recursion down the tree if there are no nodes down here
1346 | if (indexInRange(indices, rootIndex, nextIndex)) {
1347 | recurse(childNodes[i], vChild, indices, nodes, rootIndex)
1348 | }
1349 |
1350 | rootIndex = nextIndex
1351 | }
1352 | }
1353 | }
1354 |
1355 | return nodes
1356 | }
1357 |
1358 | // Binary search for an index in the interval [left, right]
1359 | function indexInRange(indices, left, right) {
1360 | if (indices.length === 0) {
1361 | return false
1362 | }
1363 |
1364 | var minIndex = 0
1365 | var maxIndex = indices.length - 1
1366 | var currentIndex
1367 | var currentItem
1368 |
1369 | while (minIndex <= maxIndex) {
1370 | currentIndex = ((maxIndex + minIndex) / 2) >> 0
1371 | currentItem = indices[currentIndex]
1372 |
1373 | if (minIndex === maxIndex) {
1374 | return currentItem >= left && currentItem <= right
1375 | } else if (currentItem < left) {
1376 | minIndex = currentIndex + 1
1377 | } else if (currentItem > right) {
1378 | maxIndex = currentIndex - 1
1379 | } else {
1380 | return true
1381 | }
1382 | }
1383 |
1384 | return false;
1385 | }
1386 |
1387 | function ascending(a, b) {
1388 | return a > b ? 1 : -1
1389 | }
1390 |
1391 |
1392 |
1393 | return module;
1394 | });
1395 |
1396 | _require.def( "node_modules/virtual-dom/vdom/patch-op.js", function( _require, exports, module, global ){
1397 | var applyProperties = _require( "node_modules/virtual-dom/vdom/apply-properties.js" )
1398 |
1399 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
1400 | var VPatch = _require( "node_modules/virtual-dom/vnode/vpatch.js" )
1401 |
1402 | var updateWidget = _require( "node_modules/virtual-dom/vdom/update-widget.js" )
1403 |
1404 | module.exports = applyPatch
1405 |
1406 | function applyPatch(vpatch, domNode, renderOptions) {
1407 | var type = vpatch.type
1408 | var vNode = vpatch.vNode
1409 | var patch = vpatch.patch
1410 |
1411 | switch (type) {
1412 | case VPatch.REMOVE:
1413 | return removeNode(domNode, vNode)
1414 | case VPatch.INSERT:
1415 | return insertNode(domNode, patch, renderOptions)
1416 | case VPatch.VTEXT:
1417 | return stringPatch(domNode, vNode, patch, renderOptions)
1418 | case VPatch.WIDGET:
1419 | return widgetPatch(domNode, vNode, patch, renderOptions)
1420 | case VPatch.VNODE:
1421 | return vNodePatch(domNode, vNode, patch, renderOptions)
1422 | case VPatch.ORDER:
1423 | reorderChildren(domNode, patch)
1424 | return domNode
1425 | case VPatch.PROPS:
1426 | applyProperties(domNode, patch, vNode.properties)
1427 | return domNode
1428 | case VPatch.THUNK:
1429 | return replaceRoot(domNode,
1430 | renderOptions.patch(domNode, patch, renderOptions))
1431 | default:
1432 | return domNode
1433 | }
1434 | }
1435 |
1436 | function removeNode(domNode, vNode) {
1437 | var parentNode = domNode.parentNode
1438 |
1439 | if (parentNode) {
1440 | parentNode.removeChild(domNode)
1441 | }
1442 |
1443 | destroyWidget(domNode, vNode);
1444 |
1445 | return null
1446 | }
1447 |
1448 | function insertNode(parentNode, vNode, renderOptions) {
1449 | var newNode = renderOptions.render(vNode, renderOptions)
1450 |
1451 | if (parentNode) {
1452 | parentNode.appendChild(newNode)
1453 | }
1454 |
1455 | return parentNode
1456 | }
1457 |
1458 | function stringPatch(domNode, leftVNode, vText, renderOptions) {
1459 | var newNode
1460 |
1461 | if (domNode.nodeType === 3) {
1462 | domNode.replaceData(0, domNode.length, vText.text)
1463 | newNode = domNode
1464 | } else {
1465 | var parentNode = domNode.parentNode
1466 | newNode = renderOptions.render(vText, renderOptions)
1467 |
1468 | if (parentNode && newNode !== domNode) {
1469 | parentNode.replaceChild(newNode, domNode)
1470 | }
1471 | }
1472 |
1473 | return newNode
1474 | }
1475 |
1476 | function widgetPatch(domNode, leftVNode, widget, renderOptions) {
1477 | var updating = updateWidget(leftVNode, widget)
1478 | var newNode
1479 |
1480 | if (updating) {
1481 | newNode = widget.update(leftVNode, domNode) || domNode
1482 | } else {
1483 | newNode = renderOptions.render(widget, renderOptions)
1484 | }
1485 |
1486 | var parentNode = domNode.parentNode
1487 |
1488 | if (parentNode && newNode !== domNode) {
1489 | parentNode.replaceChild(newNode, domNode)
1490 | }
1491 |
1492 | if (!updating) {
1493 | destroyWidget(domNode, leftVNode)
1494 | }
1495 |
1496 | return newNode
1497 | }
1498 |
1499 | function vNodePatch(domNode, leftVNode, vNode, renderOptions) {
1500 | var parentNode = domNode.parentNode
1501 | var newNode = renderOptions.render(vNode, renderOptions)
1502 |
1503 | if (parentNode && newNode !== domNode) {
1504 | parentNode.replaceChild(newNode, domNode)
1505 | }
1506 |
1507 | return newNode
1508 | }
1509 |
1510 | function destroyWidget(domNode, w) {
1511 | if (typeof w.destroy === "function" && isWidget(w)) {
1512 | w.destroy(domNode)
1513 | }
1514 | }
1515 |
1516 | function reorderChildren(domNode, moves) {
1517 | var childNodes = domNode.childNodes
1518 | var keyMap = {}
1519 | var node
1520 | var remove
1521 | var insert
1522 |
1523 | for (var i = 0; i < moves.removes.length; i++) {
1524 | remove = moves.removes[i]
1525 | node = childNodes[remove.from]
1526 | if (remove.key) {
1527 | keyMap[remove.key] = node
1528 | }
1529 | domNode.removeChild(node)
1530 | }
1531 |
1532 | var length = childNodes.length
1533 | for (var j = 0; j < moves.inserts.length; j++) {
1534 | insert = moves.inserts[j]
1535 | node = keyMap[insert.key]
1536 | // this is the weirdest bug i've ever seen in webkit
1537 | domNode.insertBefore(node, insert.to >= length++ ? null : childNodes[insert.to])
1538 | }
1539 | }
1540 |
1541 | function replaceRoot(oldRoot, newRoot) {
1542 | if (oldRoot && newRoot && oldRoot !== newRoot && oldRoot.parentNode) {
1543 | oldRoot.parentNode.replaceChild(newRoot, oldRoot)
1544 | }
1545 |
1546 | return newRoot;
1547 | }
1548 |
1549 |
1550 |
1551 | return module;
1552 | });
1553 |
1554 | _require.def( "node_modules/virtual-dom/vdom/apply-properties.js", function( _require, exports, module, global ){
1555 | var isObject = _require( "node_modules/is-object/index.js" )
1556 | var isHook = _require( "node_modules/virtual-dom/vnode/is-vhook.js" )
1557 |
1558 | module.exports = applyProperties
1559 |
1560 | function applyProperties(node, props, previous) {
1561 | for (var propName in props) {
1562 | var propValue = props[propName]
1563 |
1564 | if (propValue === undefined) {
1565 | removeProperty(node, propName, propValue, previous);
1566 | } else if (isHook(propValue)) {
1567 | removeProperty(node, propName, propValue, previous)
1568 | if (propValue.hook) {
1569 | propValue.hook(node,
1570 | propName,
1571 | previous ? previous[propName] : undefined)
1572 | }
1573 | } else {
1574 | if (isObject(propValue)) {
1575 | patchObject(node, props, previous, propName, propValue);
1576 | } else {
1577 | node[propName] = propValue
1578 | }
1579 | }
1580 | }
1581 | }
1582 |
1583 | function removeProperty(node, propName, propValue, previous) {
1584 | if (previous) {
1585 | var previousValue = previous[propName]
1586 |
1587 | if (!isHook(previousValue)) {
1588 | if (propName === "attributes") {
1589 | for (var attrName in previousValue) {
1590 | node.removeAttribute(attrName)
1591 | }
1592 | } else if (propName === "style") {
1593 | for (var i in previousValue) {
1594 | node.style[i] = ""
1595 | }
1596 | } else if (typeof previousValue === "string") {
1597 | node[propName] = ""
1598 | } else {
1599 | node[propName] = null
1600 | }
1601 | } else if (previousValue.unhook) {
1602 | previousValue.unhook(node, propName, propValue)
1603 | }
1604 | }
1605 | }
1606 |
1607 | function patchObject(node, props, previous, propName, propValue) {
1608 | var previousValue = previous ? previous[propName] : undefined
1609 |
1610 | // Set attributes
1611 | if (propName === "attributes") {
1612 | for (var attrName in propValue) {
1613 | var attrValue = propValue[attrName]
1614 |
1615 | if (attrValue === undefined) {
1616 | node.removeAttribute(attrName)
1617 | } else {
1618 | node.setAttribute(attrName, attrValue)
1619 | }
1620 | }
1621 |
1622 | return
1623 | }
1624 |
1625 | if(previousValue && isObject(previousValue) &&
1626 | getPrototype(previousValue) !== getPrototype(propValue)) {
1627 | node[propName] = propValue
1628 | return
1629 | }
1630 |
1631 | if (!isObject(node[propName])) {
1632 | node[propName] = {}
1633 | }
1634 |
1635 | var replacer = propName === "style" ? "" : undefined
1636 |
1637 | for (var k in propValue) {
1638 | var value = propValue[k]
1639 | node[propName][k] = (value === undefined) ? replacer : value
1640 | }
1641 | }
1642 |
1643 | function getPrototype(value) {
1644 | if (Object.getPrototypeOf) {
1645 | return Object.getPrototypeOf(value)
1646 | } else if (value.__proto__) {
1647 | return value.__proto__
1648 | } else if (value.constructor) {
1649 | return value.constructor.prototype
1650 | }
1651 | }
1652 |
1653 |
1654 |
1655 | return module;
1656 | });
1657 |
1658 | _require.def( "node_modules/virtual-dom/vnode/version.js", function( _require, exports, module, global ){
1659 | module.exports = "2"
1660 |
1661 |
1662 |
1663 | return module;
1664 | });
1665 |
1666 | _require.def( "node_modules/browser-split/index.js", function( _require, exports, module, global ){
1667 | /*!
1668 | * Cross-Browser Split 1.1.1
1669 | * Copyright 2007-2012 Steven Levithan
1670 | * Available under the MIT License
1671 | * ECMAScript compliant, uniform cross-browser split method
1672 | */
1673 |
1674 | /**
1675 | * Splits a string into an array of strings using a regex or string separator. Matches of the
1676 | * separator are not included in the result array. However, if `separator` is a regex that contains
1677 | * capturing groups, backreferences are spliced into the result each time `separator` is matched.
1678 | * Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
1679 | * cross-browser.
1680 | * @param {String} str String to split.
1681 | * @param {RegExp|String} separator Regex or string to use for separating the string.
1682 | * @param {Number} [limit] Maximum number of items to include in the result array.
1683 | * @returns {Array} Array of substrings.
1684 | * @example
1685 | *
1686 | * // Basic use
1687 | * split('a b c d', ' ');
1688 | * // -> ['a', 'b', 'c', 'd']
1689 | *
1690 | * // With limit
1691 | * split('a b c d', ' ', 2);
1692 | * // -> ['a', 'b']
1693 | *
1694 | * // Backreferences in result array
1695 | * split('..word1 word2..', /([a-z]+)(\d+)/i);
1696 | * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
1697 | */
1698 | module.exports = (function split(undef) {
1699 |
1700 | var nativeSplit = String.prototype.split,
1701 | compliantExecNpcg = /()??/.exec("")[1] === undef,
1702 | // NPCG: nonparticipating capturing group
1703 | self;
1704 |
1705 | self = function(str, separator, limit) {
1706 | // If `separator` is not a regex, use `nativeSplit`
1707 | if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
1708 | return nativeSplit.call(str, separator, limit);
1709 | }
1710 | var output = [],
1711 | flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6
1712 | (separator.sticky ? "y" : ""),
1713 | // Firefox 3+
1714 | lastLastIndex = 0,
1715 | // Make `global` and avoid `lastIndex` issues by working with a copy
1716 | separator = new RegExp(separator.source, flags + "g"),
1717 | separator2, match, lastIndex, lastLength;
1718 | str += ""; // Type-convert
1719 | if (!compliantExecNpcg) {
1720 | // Doesn't need flags gy, but they don't hurt
1721 | separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
1722 | }
1723 | /* Values for `limit`, per the spec:
1724 | * If undefined: 4294967295 // Math.pow(2, 32) - 1
1725 | * If 0, Infinity, or NaN: 0
1726 | * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
1727 | * If negative number: 4294967296 - Math.floor(Math.abs(limit))
1728 | * If other: Type-convert, then use the above rules
1729 | */
1730 | limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1
1731 | limit >>> 0; // ToUint32(limit)
1732 | while (match = separator.exec(str)) {
1733 | // `separator.lastIndex` is not reliable cross-browser
1734 | lastIndex = match.index + match[0].length;
1735 | if (lastIndex > lastLastIndex) {
1736 | output.push(str.slice(lastLastIndex, match.index));
1737 | // Fix browsers whose `exec` methods don't consistently return `undefined` for
1738 | // nonparticipating capturing groups
1739 | if (!compliantExecNpcg && match.length > 1) {
1740 | match[0].replace(separator2, function() {
1741 | for (var i = 1; i < arguments.length - 2; i++) {
1742 | if (arguments[i] === undef) {
1743 | match[i] = undef;
1744 | }
1745 | }
1746 | });
1747 | }
1748 | if (match.length > 1 && match.index < str.length) {
1749 | Array.prototype.push.apply(output, match.slice(1));
1750 | }
1751 | lastLength = match[0].length;
1752 | lastLastIndex = lastIndex;
1753 | if (output.length >= limit) {
1754 | break;
1755 | }
1756 | }
1757 | if (separator.lastIndex === match.index) {
1758 | separator.lastIndex++; // Avoid an infinite loop
1759 | }
1760 | }
1761 | if (lastLastIndex === str.length) {
1762 | if (lastLength || !separator.test("")) {
1763 | output.push("");
1764 | }
1765 | } else {
1766 | output.push(str.slice(lastLastIndex));
1767 | }
1768 | return output.length > limit ? output.slice(0, limit) : output;
1769 | };
1770 |
1771 | return self;
1772 | })();
1773 |
1774 |
1775 |
1776 | return module;
1777 | });
1778 |
1779 | _require.def( "node_modules/ev-store/index.js", function( _require, exports, module, global ){
1780 | 'use strict';
1781 |
1782 | var OneVersionConstraint = _require( "node_modules/individual/one-version.js" );
1783 |
1784 | var MY_VERSION = '7';
1785 | OneVersionConstraint('ev-store', MY_VERSION);
1786 |
1787 | var hashKey = '__EV_STORE_KEY@' + MY_VERSION;
1788 |
1789 | module.exports = EvStore;
1790 |
1791 | function EvStore(elem) {
1792 | var hash = elem[hashKey];
1793 |
1794 | if (!hash) {
1795 | hash = elem[hashKey] = {};
1796 | }
1797 |
1798 | return hash;
1799 | }
1800 |
1801 |
1802 |
1803 | return module;
1804 | });
1805 |
1806 | _require.def( "node_modules/is-object/index.js", function( _require, exports, module, global ){
1807 | "use strict";
1808 |
1809 | module.exports = function isObject(x) {
1810 | return typeof x === "object" && x !== null;
1811 | };
1812 |
1813 |
1814 |
1815 | return module;
1816 | });
1817 |
1818 | _require.def( "node_modules/min-document/index.js", function( _require, exports, module, global ){
1819 | var Document = _require( "node_modules/min-document/document.js" );
1820 |
1821 | module.exports = new Document();
1822 |
1823 |
1824 |
1825 | return module;
1826 | });
1827 |
1828 | _require.def( "node_modules/virtual-dom/vdom/update-widget.js", function( _require, exports, module, global ){
1829 | var isWidget = _require( "node_modules/virtual-dom/vnode/is-widget.js" )
1830 |
1831 | module.exports = updateWidget
1832 |
1833 | function updateWidget(a, b) {
1834 | if (isWidget(a) && isWidget(b)) {
1835 | if ("name" in a && "name" in b) {
1836 | return a.id === b.id
1837 | } else {
1838 | return a.init === b.init
1839 | }
1840 | }
1841 |
1842 | return false
1843 | }
1844 |
1845 |
1846 |
1847 | return module;
1848 | });
1849 |
1850 | _require.def( "node_modules/individual/one-version.js", function( _require, exports, module, global ){
1851 | 'use strict';
1852 |
1853 | var Individual = _require( "node_modules/individual/index.js" );
1854 |
1855 | module.exports = OneVersion;
1856 |
1857 | function OneVersion(moduleName, version, defaultValue) {
1858 | var key = '__INDIVIDUAL_ONE_VERSION_' + moduleName;
1859 | var enforceKey = key + '_ENFORCE_SINGLETON';
1860 |
1861 | var versionValue = Individual(enforceKey, version);
1862 |
1863 | if (versionValue !== version) {
1864 | throw new Error('Can only have one copy of ' +
1865 | moduleName + '.\n' +
1866 | 'You already have version ' + versionValue +
1867 | ' installed.\n' +
1868 | 'This means you cannot install version ' + version);
1869 | }
1870 |
1871 | return Individual(key, defaultValue);
1872 | }
1873 |
1874 |
1875 |
1876 | return module;
1877 | });
1878 |
1879 | _require.def( "node_modules/min-document/document.js", function( _require, exports, module, global ){
1880 | var domWalk = _require( "node_modules/dom-walk/index.js" )
1881 |
1882 | var Comment = _require( "node_modules/min-document/dom-comment.js" )
1883 | var DOMText = _require( "node_modules/min-document/dom-text.js" )
1884 | var DOMElement = _require( "node_modules/min-document/dom-element.js" )
1885 | var DocumentFragment = _require( "node_modules/min-document/dom-fragment.js" )
1886 | var Event = _require( "node_modules/min-document/event.js" )
1887 | var dispatchEvent = _require( "node_modules/min-document/event/dispatch-event.js" )
1888 | var addEventListener = _require( "node_modules/min-document/event/add-event-listener.js" )
1889 | var removeEventListener = _require( "node_modules/min-document/event/remove-event-listener.js" )
1890 |
1891 | module.exports = Document;
1892 |
1893 | function Document() {
1894 | if (!(this instanceof Document)) {
1895 | return new Document();
1896 | }
1897 |
1898 | this.head = this.createElement("head")
1899 | this.body = this.createElement("body")
1900 | this.documentElement = this.createElement("html")
1901 | this.documentElement.appendChild(this.head)
1902 | this.documentElement.appendChild(this.body)
1903 | this.childNodes = [this.documentElement]
1904 | this.nodeType = 9
1905 | }
1906 |
1907 | var proto = Document.prototype;
1908 | proto.createTextNode = function createTextNode(value) {
1909 | return new DOMText(value, this)
1910 | }
1911 |
1912 | proto.createElementNS = function createElementNS(namespace, tagName) {
1913 | var ns = namespace === null ? null : String(namespace)
1914 | return new DOMElement(tagName, this, ns)
1915 | }
1916 |
1917 | proto.createElement = function createElement(tagName) {
1918 | return new DOMElement(tagName, this)
1919 | }
1920 |
1921 | proto.createDocumentFragment = function createDocumentFragment() {
1922 | return new DocumentFragment(this)
1923 | }
1924 |
1925 | proto.createEvent = function createEvent(family) {
1926 | return new Event(family)
1927 | }
1928 |
1929 | proto.createComment = function createComment(data) {
1930 | return new Comment(data, this)
1931 | }
1932 |
1933 | proto.getElementById = function getElementById(id) {
1934 | id = String(id)
1935 |
1936 | var result = domWalk(this.childNodes, function (node) {
1937 | if (String(node.id) === id) {
1938 | return node
1939 | }
1940 | })
1941 |
1942 | return result || null
1943 | }
1944 |
1945 | proto.getElementsByClassName = DOMElement.prototype.getElementsByClassName
1946 | proto.getElementsByTagName = DOMElement.prototype.getElementsByTagName
1947 | proto.contains = DOMElement.prototype.contains
1948 |
1949 | proto.removeEventListener = removeEventListener
1950 | proto.addEventListener = addEventListener
1951 | proto.dispatchEvent = dispatchEvent
1952 |
1953 |
1954 |
1955 | return module;
1956 | });
1957 |
1958 | _require.def( "node_modules/min-document/dom-comment.js", function( _require, exports, module, global ){
1959 | module.exports = Comment
1960 |
1961 | function Comment(data, owner) {
1962 | if (!(this instanceof Comment)) {
1963 | return new Comment(data, owner)
1964 | }
1965 |
1966 | this.data = data
1967 | this.nodeValue = data
1968 | this.length = data.length
1969 | this.ownerDocument = owner || null
1970 | }
1971 |
1972 | Comment.prototype.nodeType = 8
1973 | Comment.prototype.nodeName = "#comment"
1974 |
1975 | Comment.prototype.toString = function _Comment_toString() {
1976 | return "[object Comment]"
1977 | }
1978 |
1979 |
1980 |
1981 | return module;
1982 | });
1983 |
1984 | _require.def( "node_modules/min-document/dom-text.js", function( _require, exports, module, global ){
1985 | module.exports = DOMText
1986 |
1987 | function DOMText(value, owner) {
1988 | if (!(this instanceof DOMText)) {
1989 | return new DOMText(value)
1990 | }
1991 |
1992 | this.data = value || ""
1993 | this.length = this.data.length
1994 | this.ownerDocument = owner || null
1995 | }
1996 |
1997 | DOMText.prototype.type = "DOMTextNode"
1998 | DOMText.prototype.nodeType = 3
1999 |
2000 | DOMText.prototype.toString = function _Text_toString() {
2001 | return this.data
2002 | }
2003 |
2004 | DOMText.prototype.replaceData = function replaceData(index, length, value) {
2005 | var current = this.data
2006 | var left = current.substring(0, index)
2007 | var right = current.substring(index + length, current.length)
2008 | this.data = left + value + right
2009 | this.length = this.data.length
2010 | }
2011 |
2012 |
2013 |
2014 | return module;
2015 | });
2016 |
2017 | _require.def( "node_modules/individual/index.js", function( _require, exports, module, global ){
2018 | 'use strict';
2019 |
2020 | /*global window, global*/
2021 |
2022 | var root = typeof window !== 'undefined' ?
2023 | window : typeof global !== 'undefined' ?
2024 | global : {};
2025 |
2026 | module.exports = Individual;
2027 |
2028 | function Individual(key, value) {
2029 | if (key in root) {
2030 | return root[key];
2031 | }
2032 |
2033 | root[key] = value;
2034 |
2035 | return value;
2036 | }
2037 |
2038 |
2039 |
2040 | return module;
2041 | });
2042 |
2043 | _require.def( "node_modules/dom-walk/index.js", function( _require, exports, module, global ){
2044 | var slice = Array.prototype.slice
2045 |
2046 | module.exports = iterativelyWalk
2047 |
2048 | function iterativelyWalk(nodes, cb) {
2049 | if (!('length' in nodes)) {
2050 | nodes = [nodes]
2051 | }
2052 |
2053 | nodes = slice.call(nodes)
2054 |
2055 | while(nodes.length) {
2056 | var node = nodes.shift(),
2057 | ret = cb(node)
2058 |
2059 | if (ret) {
2060 | return ret
2061 | }
2062 |
2063 | if (node.childNodes && node.childNodes.length) {
2064 | nodes = slice.call(node.childNodes).concat(nodes)
2065 | }
2066 | }
2067 | }
2068 |
2069 |
2070 |
2071 | return module;
2072 | });
2073 |
2074 | _require.def( "node_modules/min-document/dom-fragment.js", function( _require, exports, module, global ){
2075 | var DOMElement = _require( "node_modules/min-document/dom-element.js" )
2076 |
2077 | module.exports = DocumentFragment
2078 |
2079 | function DocumentFragment(owner) {
2080 | if (!(this instanceof DocumentFragment)) {
2081 | return new DocumentFragment()
2082 | }
2083 |
2084 | this.childNodes = []
2085 | this.parentNode = null
2086 | this.ownerDocument = owner || null
2087 | }
2088 |
2089 | DocumentFragment.prototype.type = "DocumentFragment"
2090 | DocumentFragment.prototype.nodeType = 11
2091 | DocumentFragment.prototype.nodeName = "#document-fragment"
2092 |
2093 | DocumentFragment.prototype.appendChild = DOMElement.prototype.appendChild
2094 | DocumentFragment.prototype.replaceChild = DOMElement.prototype.replaceChild
2095 | DocumentFragment.prototype.removeChild = DOMElement.prototype.removeChild
2096 |
2097 | DocumentFragment.prototype.toString =
2098 | function _DocumentFragment_toString() {
2099 | return this.childNodes.map(function (node) {
2100 | return String(node)
2101 | }).join("")
2102 | }
2103 |
2104 |
2105 |
2106 | return module;
2107 | });
2108 |
2109 | _require.def( "node_modules/min-document/event.js", function( _require, exports, module, global ){
2110 | module.exports = Event
2111 |
2112 | function Event(family) {}
2113 |
2114 | Event.prototype.initEvent = function _Event_initEvent(type, bubbles, cancelable) {
2115 | this.type = type
2116 | this.bubbles = bubbles
2117 | this.cancelable = cancelable
2118 | }
2119 |
2120 | Event.prototype.preventDefault = function _Event_preventDefault() {
2121 |
2122 | }
2123 |
2124 |
2125 |
2126 | return module;
2127 | });
2128 |
2129 | _require.def( "node_modules/min-document/dom-element.js", function( _require, exports, module, global ){
2130 | var domWalk = _require( "node_modules/dom-walk/index.js" )
2131 | var dispatchEvent = _require( "node_modules/min-document/event/dispatch-event.js" )
2132 | var addEventListener = _require( "node_modules/min-document/event/add-event-listener.js" )
2133 | var removeEventListener = _require( "node_modules/min-document/event/remove-event-listener.js" )
2134 | var serializeNode = _require( "node_modules/min-document/serialize.js" )
2135 |
2136 | var htmlns = "http://www.w3.org/1999/xhtml"
2137 |
2138 | module.exports = DOMElement
2139 |
2140 | function DOMElement(tagName, owner, namespace) {
2141 | if (!(this instanceof DOMElement)) {
2142 | return new DOMElement(tagName)
2143 | }
2144 |
2145 | var ns = namespace === undefined ? htmlns : (namespace || null)
2146 |
2147 | this.tagName = ns === htmlns ? String(tagName).toUpperCase() : tagName
2148 | this.className = ""
2149 | this.dataset = {}
2150 | this.childNodes = []
2151 | this.parentNode = null
2152 | this.style = {}
2153 | this.ownerDocument = owner || null
2154 | this.namespaceURI = ns
2155 | this._attributes = {}
2156 |
2157 | if (this.tagName === 'INPUT') {
2158 | this.type = 'text'
2159 | }
2160 | }
2161 |
2162 | DOMElement.prototype.type = "DOMElement"
2163 | DOMElement.prototype.nodeType = 1
2164 |
2165 | DOMElement.prototype.appendChild = function _Element_appendChild(child) {
2166 | if (child.parentNode) {
2167 | child.parentNode.removeChild(child)
2168 | }
2169 |
2170 | this.childNodes.push(child)
2171 | child.parentNode = this
2172 |
2173 | return child
2174 | }
2175 |
2176 | DOMElement.prototype.replaceChild =
2177 | function _Element_replaceChild(elem, needle) {
2178 | // TODO: Throw NotFoundError if needle.parentNode !== this
2179 |
2180 | if (elem.parentNode) {
2181 | elem.parentNode.removeChild(elem)
2182 | }
2183 |
2184 | var index = this.childNodes.indexOf(needle)
2185 |
2186 | needle.parentNode = null
2187 | this.childNodes[index] = elem
2188 | elem.parentNode = this
2189 |
2190 | return needle
2191 | }
2192 |
2193 | DOMElement.prototype.removeChild = function _Element_removeChild(elem) {
2194 | // TODO: Throw NotFoundError if elem.parentNode !== this
2195 |
2196 | var index = this.childNodes.indexOf(elem)
2197 | this.childNodes.splice(index, 1)
2198 |
2199 | elem.parentNode = null
2200 | return elem
2201 | }
2202 |
2203 | DOMElement.prototype.insertBefore =
2204 | function _Element_insertBefore(elem, needle) {
2205 | // TODO: Throw NotFoundError if referenceElement is a dom node
2206 | // and parentNode !== this
2207 |
2208 | if (elem.parentNode) {
2209 | elem.parentNode.removeChild(elem)
2210 | }
2211 |
2212 | var index = needle === null || needle === undefined ?
2213 | -1 :
2214 | this.childNodes.indexOf(needle)
2215 |
2216 | if (index > -1) {
2217 | this.childNodes.splice(index, 0, elem)
2218 | } else {
2219 | this.childNodes.push(elem)
2220 | }
2221 |
2222 | elem.parentNode = this
2223 | return elem
2224 | }
2225 |
2226 | DOMElement.prototype.setAttributeNS =
2227 | function _Element_setAttributeNS(namespace, name, value) {
2228 | var prefix = null
2229 | var localName = name
2230 | var colonPosition = name.indexOf(":")
2231 | if (colonPosition > -1) {
2232 | prefix = name.substr(0, colonPosition)
2233 | localName = name.substr(colonPosition + 1)
2234 | }
2235 | var attributes = this._attributes[namespace] || (this._attributes[namespace] = {})
2236 | attributes[localName] = {value: value, prefix: prefix}
2237 | }
2238 |
2239 | DOMElement.prototype.getAttributeNS =
2240 | function _Element_getAttributeNS(namespace, name) {
2241 | var attributes = this._attributes[namespace];
2242 | var value = attributes && attributes[name] && attributes[name].value
2243 | if (typeof value !== "string") {
2244 | return null
2245 | }
2246 |
2247 | return value
2248 | }
2249 |
2250 | DOMElement.prototype.removeAttributeNS =
2251 | function _Element_removeAttributeNS(namespace, name) {
2252 | var attributes = this._attributes[namespace];
2253 | if (attributes) {
2254 | delete attributes[name]
2255 | }
2256 | }
2257 |
2258 | DOMElement.prototype.hasAttributeNS =
2259 | function _Element_hasAttributeNS(namespace, name) {
2260 | var attributes = this._attributes[namespace]
2261 | return !!attributes && name in attributes;
2262 | }
2263 |
2264 | DOMElement.prototype.setAttribute = function _Element_setAttribute(name, value) {
2265 | return this.setAttributeNS(null, name, value)
2266 | }
2267 |
2268 | DOMElement.prototype.getAttribute = function _Element_getAttribute(name) {
2269 | return this.getAttributeNS(null, name)
2270 | }
2271 |
2272 | DOMElement.prototype.removeAttribute = function _Element_removeAttribute(name) {
2273 | return this.removeAttributeNS(null, name)
2274 | }
2275 |
2276 | DOMElement.prototype.hasAttribute = function _Element_hasAttribute(name) {
2277 | return this.hasAttributeNS(null, name)
2278 | }
2279 |
2280 | DOMElement.prototype.removeEventListener = removeEventListener
2281 | DOMElement.prototype.addEventListener = addEventListener
2282 | DOMElement.prototype.dispatchEvent = dispatchEvent
2283 |
2284 | // Un-implemented
2285 | DOMElement.prototype.focus = function _Element_focus() {
2286 | return void 0
2287 | }
2288 |
2289 | DOMElement.prototype.toString = function _Element_toString() {
2290 | return serializeNode(this)
2291 | }
2292 |
2293 | DOMElement.prototype.getElementsByClassName = function _Element_getElementsByClassName(classNames) {
2294 | var classes = classNames.split(" ");
2295 | var elems = []
2296 |
2297 | domWalk(this, function (node) {
2298 | if (node.nodeType === 1) {
2299 | var nodeClassName = node.className || ""
2300 | var nodeClasses = nodeClassName.split(" ")
2301 |
2302 | if (classes.every(function (item) {
2303 | return nodeClasses.indexOf(item) !== -1
2304 | })) {
2305 | elems.push(node)
2306 | }
2307 | }
2308 | })
2309 |
2310 | return elems
2311 | }
2312 |
2313 | DOMElement.prototype.getElementsByTagName = function _Element_getElementsByTagName(tagName) {
2314 | tagName = tagName.toLowerCase()
2315 | var elems = []
2316 |
2317 | domWalk(this.childNodes, function (node) {
2318 | if (node.nodeType === 1 && (tagName === '*' || node.tagName.toLowerCase() === tagName)) {
2319 | elems.push(node)
2320 | }
2321 | })
2322 |
2323 | return elems
2324 | }
2325 |
2326 | DOMElement.prototype.contains = function _Element_contains(element) {
2327 | return domWalk(this, function (node) {
2328 | return element === node
2329 | }) || false
2330 | }
2331 |
2332 |
2333 |
2334 | return module;
2335 | });
2336 |
2337 | _require.def( "node_modules/min-document/event/dispatch-event.js", function( _require, exports, module, global ){
2338 | module.exports = dispatchEvent
2339 |
2340 | function dispatchEvent(ev) {
2341 | var elem = this
2342 | var type = ev.type
2343 |
2344 | if (!ev.target) {
2345 | ev.target = elem
2346 | }
2347 |
2348 | if (!elem.listeners) {
2349 | elem.listeners = {}
2350 | }
2351 |
2352 | var listeners = elem.listeners[type]
2353 |
2354 | if (listeners) {
2355 | return listeners.forEach(function (listener) {
2356 | ev.currentTarget = elem
2357 | if (typeof listener === 'function') {
2358 | listener(ev)
2359 | } else {
2360 | listener.handleEvent(ev)
2361 | }
2362 | })
2363 | }
2364 |
2365 | if (elem.parentNode) {
2366 | elem.parentNode.dispatchEvent(ev)
2367 | }
2368 | }
2369 |
2370 |
2371 |
2372 | return module;
2373 | });
2374 |
2375 | _require.def( "node_modules/min-document/event/remove-event-listener.js", function( _require, exports, module, global ){
2376 | module.exports = removeEventListener
2377 |
2378 | function removeEventListener(type, listener) {
2379 | var elem = this
2380 |
2381 | if (!elem.listeners) {
2382 | return
2383 | }
2384 |
2385 | if (!elem.listeners[type]) {
2386 | return
2387 | }
2388 |
2389 | var list = elem.listeners[type]
2390 | var index = list.indexOf(listener)
2391 | if (index !== -1) {
2392 | list.splice(index, 1)
2393 | }
2394 | }
2395 |
2396 |
2397 |
2398 | return module;
2399 | });
2400 |
2401 | _require.def( "node_modules/min-document/event/add-event-listener.js", function( _require, exports, module, global ){
2402 | module.exports = addEventListener
2403 |
2404 | function addEventListener(type, listener) {
2405 | var elem = this
2406 |
2407 | if (!elem.listeners) {
2408 | elem.listeners = {}
2409 | }
2410 |
2411 | if (!elem.listeners[type]) {
2412 | elem.listeners[type] = []
2413 | }
2414 |
2415 | if (elem.listeners[type].indexOf(listener) === -1) {
2416 | elem.listeners[type].push(listener)
2417 | }
2418 | }
2419 |
2420 |
2421 |
2422 | return module;
2423 | });
2424 |
2425 | _require.def( "node_modules/min-document/serialize.js", function( _require, exports, module, global ){
2426 | module.exports = serializeNode
2427 |
2428 | var voidElements = /area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr/i;
2429 |
2430 | function serializeNode(node) {
2431 | switch (node.nodeType) {
2432 | case 3:
2433 | return escapeText(node.data)
2434 | case 8:
2435 | return ""
2436 | default:
2437 | return serializeElement(node)
2438 | }
2439 | }
2440 |
2441 | function serializeElement(elem) {
2442 | var strings = []
2443 |
2444 | var tagname = elem.tagName
2445 |
2446 | if (elem.namespaceURI === "http://www.w3.org/1999/xhtml") {
2447 | tagname = tagname.toLowerCase()
2448 | }
2449 |
2450 | strings.push("<" + tagname + properties(elem) + datasetify(elem))
2451 |
2452 | if (voidElements.test(tagname)) {
2453 | strings.push(" />")
2454 | } else {
2455 | strings.push(">")
2456 |
2457 | if (elem.childNodes.length) {
2458 | strings.push.apply(strings, elem.childNodes.map(serializeNode))
2459 | } else if (elem.textContent || elem.innerText) {
2460 | strings.push(escapeText(elem.textContent || elem.innerText))
2461 | } else if (elem.innerHTML) {
2462 | strings.push(elem.innerHTML)
2463 | }
2464 |
2465 | strings.push("" + tagname + ">")
2466 | }
2467 |
2468 | return strings.join("")
2469 | }
2470 |
2471 | function isProperty(elem, key) {
2472 | var type = typeof elem[key]
2473 |
2474 | if (key === "style" && Object.keys(elem.style).length > 0) {
2475 | return true
2476 | }
2477 |
2478 | return elem.hasOwnProperty(key) &&
2479 | (type === "string" || type === "boolean" || type === "number") &&
2480 | key !== "nodeName" && key !== "className" && key !== "tagName" &&
2481 | key !== "textContent" && key !== "innerText" && key !== "namespaceURI" && key !== "innerHTML"
2482 | }
2483 |
2484 | function stylify(styles) {
2485 | var attr = ""
2486 | Object.keys(styles).forEach(function (key) {
2487 | var value = styles[key]
2488 | key = key.replace(/[A-Z]/g, function(c) {
2489 | return "-" + c.toLowerCase();
2490 | })
2491 | attr += key + ":" + value + ";"
2492 | })
2493 | return attr
2494 | }
2495 |
2496 | function datasetify(elem) {
2497 | var ds = elem.dataset
2498 | var props = []
2499 |
2500 | for (var key in ds) {
2501 | props.push({ name: "data-" + key, value: ds[key] })
2502 | }
2503 |
2504 | return props.length ? stringify(props) : ""
2505 | }
2506 |
2507 | function stringify(list) {
2508 | var attributes = []
2509 | list.forEach(function (tuple) {
2510 | var name = tuple.name
2511 | var value = tuple.value
2512 |
2513 | if (name === "style") {
2514 | value = stylify(value)
2515 | }
2516 |
2517 | attributes.push(name + "=" + "\"" + escapeAttributeValue(value) + "\"")
2518 | })
2519 |
2520 | return attributes.length ? " " + attributes.join(" ") : ""
2521 | }
2522 |
2523 | function properties(elem) {
2524 | var props = []
2525 | for (var key in elem) {
2526 | if (isProperty(elem, key)) {
2527 | props.push({ name: key, value: elem[key] })
2528 | }
2529 | }
2530 |
2531 | for (var ns in elem._attributes) {
2532 | for (var attribute in elem._attributes[ns]) {
2533 | var prop = elem._attributes[ns][attribute]
2534 | var name = (prop.prefix ? prop.prefix + ":" : "") + attribute
2535 | props.push({ name: name, value: prop.value })
2536 | }
2537 | }
2538 |
2539 | if (elem.className) {
2540 | props.push({ name: "class", value: elem.className })
2541 | }
2542 |
2543 | return props.length ? stringify(props) : ""
2544 | }
2545 |
2546 | function escapeText(str) {
2547 | return str
2548 | .replace(/&/g, "&")
2549 | .replace(//g, ">")
2551 | }
2552 |
2553 | function escapeAttributeValue(str) {
2554 | return escapeText(str).replace(/"/g, """)
2555 | }
2556 |
2557 |
2558 |
2559 | return module;
2560 | });
2561 |
2562 | (function(){
2563 | _require( "main.js" );
2564 | }());
2565 | }());
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/basic-auth.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import org.scalajs.dom
4 |
5 | object BasicAuth {
6 | def header(name: String, password: String): (String, String) = {
7 | val encoded = dom.window.btoa(s"$name:$password")
8 | "Authorization" -> s"Basic $encoded"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/circuit.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import org.scalajs.dom
4 |
5 | object Circuit {
6 | import doodlebot.model._
7 | import doodlebot.model.Model._
8 | import doodlebot.message._
9 | import doodlebot.virtualDom._
10 | import doodlebot.virtualDom.Dom._
11 |
12 | def update(message: Message, model: Model): Model =
13 | (message, model) match {
14 | case (Message.NotAuthenticated, _) =>
15 | NotAuthenticated(Signup.empty, Login.empty)
16 |
17 | case (Message.Authenticated(n, c), _) =>
18 | val (model, effect) = view.Chat.init(n, c)
19 | Effect.run(effect)
20 | Authenticated(n, c, model)
21 |
22 | case (Message.SignupError(errors), NotAuthenticated(signup, login)) =>
23 | NotAuthenticated(signup.withErrors(errors), login)
24 |
25 | case (Message.LoginError(errors), NotAuthenticated(signup, login)) =>
26 | NotAuthenticated(signup, login.withErrors(errors))
27 |
28 | case (Message.Signup(signup), NotAuthenticated(_, login)) =>
29 | NotAuthenticated(signup, login)
30 |
31 | case (Message.Login(login), NotAuthenticated(signup, _)) =>
32 | NotAuthenticated(signup, login)
33 |
34 | case (Message.Chat(msg), Authenticated(name, session, chat)) =>
35 | val (model, effect) = view.Chat.update(chat, msg)
36 | Effect.run(effect)
37 | Authenticated(name, session, model)
38 |
39 | case (_, _) =>
40 | dom.console.log("Unexpected message and model combination")
41 | model
42 | }
43 |
44 | def render(model: Model): VTree = {
45 | model match {
46 | case NotAuthenticated(signup, login) =>
47 | div(
48 | h1("DoodleBot"),
49 |
50 | element("div.forms")(
51 | view.Signup.render(signup),
52 | view.Login.render(login)
53 | )
54 | )
55 | case Authenticated(n, s, c) =>
56 | div(
57 | h1("DoodleBot"),
58 | view.Chat.render(c)
59 | )
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/doodlebot.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSExport
5 | import org.scalajs.dom
6 | import doodlebot.virtualDom._
7 |
8 | object DoodleBot extends js.JSApp {
9 | import doodlebot.model._
10 | import doodlebot.message._
11 |
12 | var model: Model = Model.NotAuthenticated(Model.Signup.empty, Model.Login.empty)
13 | var current: VTree = null
14 | var rendered: dom.Element = null
15 |
16 | def main(): Unit = {
17 | current = Circuit.render(model)
18 | rendered = VirtualDom.createElement(current)
19 | val root = dom.document.getElementById("app")
20 | root.removeChild(root.firstChild)
21 | root.appendChild(rendered)
22 | }
23 |
24 | def loop(message: Message): Unit = {
25 | val newModel = Circuit.update(message, model)
26 | model = newModel
27 | this.render(Circuit.render(newModel))
28 | }
29 |
30 | def render(update: VTree): Unit = {
31 | val patches = VirtualDom.diff(current, update)
32 | rendered = VirtualDom.patch(rendered, patches)
33 | current = update
34 | }
35 |
36 | @JSExport
37 | def onLogin(evt: dom.Event): Unit = {
38 | val name = dom.document.getElementById("login-name").asInstanceOf[dom.html.Input].value
39 | val password = dom.document.getElementById("login-password").asInstanceOf[dom.html.Input].value
40 |
41 | evt.preventDefault()
42 |
43 | val message = Message.NotAuthenticated
44 | loop(message)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/effect.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 |
3 | import doodlebot.message.{Message => Msg}
4 | import scala.scalajs.js
5 | import org.scalajs.dom
6 | import org.scalajs.jquery
7 | import scala.scalajs.js.JSON
8 |
9 | sealed abstract class Effect extends Product with Serializable
10 | object Effect {
11 | final case object NoEffect extends Effect
12 | final case class Message(message: Msg) extends Effect
13 | final case class Request(
14 | path: String,
15 | payload: js.Dictionary[String],
16 | success: js.Dictionary[js.Any] => Msg,
17 | failure: Map[String, List[String]] => Msg,
18 | headers: List[(String, String)]
19 | ) extends Effect
20 | final case class Tick(message: Msg) extends Effect
21 |
22 | val noEffect: Effect =
23 | NoEffect
24 | def message(message: Msg): Effect =
25 | Message(message)
26 | def request(
27 | path: String,
28 | payload: js.Dictionary[String],
29 | success: js.Dictionary[js.Any] => Msg,
30 | failure: Map[String, List[String]] => Msg,
31 | headers: List[(String, String)] = List.empty
32 | ) =
33 | Request(path, payload, success, failure, headers)
34 | def tick(message: Msg): Effect =
35 | Tick(message)
36 |
37 | def run(effect: Effect): Unit =
38 | effect match {
39 | case NoEffect =>
40 | ()
41 |
42 | case Message(message) =>
43 | DoodleBot.loop(message)
44 |
45 | case Request(path, payload, success, failure, hdrs) =>
46 | dom.console.log("Sending", payload, " to ", path, " with headers", hdrs.toString)
47 |
48 | val callbackFailure = (data: js.Dictionary[js.Any]) => {
49 | dom.console.log("Ajax request failed with", data)
50 |
51 | val raw = data.asInstanceOf[js.Dictionary[js.Dictionary[js.Dictionary[js.Array[String]]]]]
52 | val converted = raw("errors")("messages").toMap.mapValues(_.toList)
53 |
54 | DoodleBot.loop(failure(converted))
55 | }
56 |
57 | val callbackSuccess = (data: js.Dictionary[js.Any]) => {
58 | dom.console.log("Ajax request succeeded with", data)
59 | DoodleBot.loop(success(data))
60 | }
61 |
62 | val theHeaders: js.Dictionary[String] = js.Dictionary()
63 | hdrs.foreach { hdr => theHeaders += hdr }
64 |
65 | val settings: jquery.JQueryAjaxSettings =
66 | js.Dynamic.literal (
67 | headers = theHeaders,
68 | method = "POST",
69 | url = path,
70 | data = payload,
71 | dataType = "json",
72 | success = (data: js.Any, textStatus: String, jqXHR: jquery.JQueryXHR) =>
73 | callbackSuccess(data.asInstanceOf[js.Dictionary[js.Any]]) ,
74 | error = (jqXHR: jquery.JQueryXHR, textStatus: String, errorThrow: String) =>
75 | callbackFailure(JSON.parse(jqXHR.responseText).asInstanceOf[js.Dictionary[js.Any]])
76 | ).asInstanceOf[jquery.JQueryAjaxSettings]
77 |
78 | jquery.jQuery.ajax(settings)
79 |
80 | case Tick(message) =>
81 | dom.window.setInterval(() => DoodleBot.loop(message), 1000)
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/form/input.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package form
3 |
4 | import org.scalajs.dom
5 | import doodlebot.virtualDom._
6 | import doodlebot.virtualDom.Dom._
7 |
8 | object Input {
9 | def email(
10 | name: String,
11 | placeholder: String = "",
12 | value: String = "",
13 | help: String = "",
14 | onInput: (String) => Effect = (string) => Effect.noEffect
15 | ): VTree =
16 | apply(name, "email", placeholder, value, help, onInput)
17 |
18 | def text(
19 | name: String,
20 | placeholder: String = "",
21 | value: String = "",
22 | help: String = "",
23 | onInput: (String) => Effect = (string) => Effect.noEffect
24 | ): VTree =
25 | apply(name, "text", placeholder, value, help, onInput)
26 |
27 | def password(
28 | name: String,
29 | placeholder: String = "",
30 | value: String = "",
31 | help: String = "",
32 | onInput: (String) => Effect = (string) => Effect.noEffect
33 | ): VTree =
34 | apply(name, "password", placeholder, value, help, onInput)
35 |
36 | def apply(
37 | name: String,
38 | `type`: String,
39 | placeholder: String = "",
40 | value: String = "",
41 | help: String = "",
42 | onInput: (String) => Effect = (string) => Effect.noEffect
43 | ): VTree = {
44 | val handler = eventHandler((event: dom.Event) =>
45 | Effect.run(onInput(event.target.asInstanceOf[dom.raw.HTMLInputElement].value)))
46 |
47 | element("div.form-group")(
48 | input(
49 | "type":=`type`,
50 | "name":=name,
51 | "placeholder":=placeholder,
52 | "value":=value,
53 | "oninput":=handler
54 | )(),
55 | if(help.isEmpty)
56 | span()
57 | else
58 | element("span.help-block")(help)
59 | )
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/message/message.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package message
3 |
4 | import scala.scalajs.js
5 |
6 | sealed abstract class Message extends Product with Serializable
7 | object Message {
8 | // Messages that indicate the view should change
9 | sealed abstract class View extends Message
10 | final case object NotAuthenticated extends View
11 | final case class Authenticated(name: String, session: String) extends View
12 | object Authenticated {
13 | def deserialize(data: js.Dictionary[js.Any]): Authenticated =
14 | Authenticated(
15 | data("name").asInstanceOf[String],
16 | data("session").asInstanceOf[String]
17 | )
18 | }
19 |
20 | // Messages that indicate an error has occurred
21 | sealed abstract class Error extends Message
22 | final case class SignupError(errors: Map[String,List[String]]) extends Error
23 | final case class LoginError(errors: Map[String,List[String]]) extends Error
24 | final case class ChatError(errors: Map[String,List[String]]) extends Error
25 |
26 | // Messages that indicate an update to the current model
27 | sealed abstract class Update extends Message
28 | final case object NoChange extends Message
29 | final case class Signup(model: doodlebot.model.Model.Signup) extends Update
30 | final case class Login(model: doodlebot.model.Model.Login) extends Update
31 | final case class Chat(message: view.Chat.Msg) extends Update
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/model/model.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package model
3 |
4 | sealed abstract class Model extends Product with Serializable
5 | object Model {
6 | final case class NotAuthenticated(signup: Signup, login: Login) extends Model
7 | final case class Authenticated(name: String, session: String, chat: view.Chat.Model) extends Model
8 |
9 | final case class Signup(email: String, name: String, password: String, errors: Map[String, List[String]] = Map.empty) {
10 | def withErrors(errors: Map[String, List[String]]): Signup =
11 | this.copy(errors = errors)
12 | }
13 | object Signup {
14 | val empty = Signup("","","")
15 | }
16 |
17 | final case class Login(name: String, password: String, errors: Map[String, List[String]] = Map.empty) {
18 | def withErrors(errors: Map[String, List[String]]): Login =
19 | this.copy(errors = errors)
20 | }
21 | object Login {
22 | val empty = Login("","")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/view/chat.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package view
3 |
4 | import doodlebot.form.Input
5 | import doodlebot.message.{Message => GlobalMessage}
6 | import org.scalajs.dom
7 | import scala.scalajs.js
8 |
9 | object Chat {
10 | import doodlebot.virtualDom._
11 | import doodlebot.virtualDom.Dom._
12 |
13 | final case class Model(name: String, password: String, log: Log, message: String)
14 | final case class Log(offset: Int, messages: List[Message])
15 | final case class Message(author: String, message: String)
16 | object Message {
17 | def deserialize(data: js.Dictionary[js.Any]): Message = {
18 | val author = data("author").asInstanceOf[String]
19 | val msg = data("message").asInstanceOf[String]
20 | Message(author, msg)
21 | }
22 | }
23 |
24 | sealed abstract class Msg extends Product with Serializable
25 | final case class LogMsg(log: Log) extends Msg
26 | object LogMsg {
27 | def deserialize(data: js.Dictionary[js.Any]): LogMsg = {
28 | val offset = data("offset").asInstanceOf[Int]
29 | val messages = data("messages").asInstanceOf[js.Array[js.Any]].toList.map { msg =>
30 | Message.deserialize(msg.asInstanceOf[js.Dictionary[js.Any]])
31 | }
32 | LogMsg(Log(offset, messages))
33 | }
34 | }
35 | final case class MessageMsg(message: String) extends Msg
36 | final case class Errors(errors: Map[String, List[String]]) extends Msg
37 | final case object Poll extends Msg
38 |
39 |
40 | def init(name: String, password: String): (Model, Effect) = {
41 | (Model(name, password, Log(0, List.empty), ""), Effect.tick(GlobalMessage.Chat(Poll)))
42 | }
43 |
44 |
45 | def update(model: Model, msg: Msg): (Model, Effect) =
46 | msg match {
47 | case LogMsg(l) =>
48 | val range = l.offset - model.log.offset
49 | val m =
50 | if(range > 0) {
51 | val newLog =
52 | model.log.copy(
53 | offset = l.offset, messages = l.messages.take(range) ++ model.log.messages
54 | )
55 | model.copy(log = newLog)
56 | }
57 | else
58 | model
59 |
60 | (m, Effect.noEffect)
61 |
62 | case MessageMsg(m) =>
63 | (model.copy(message = m), Effect.noEffect)
64 |
65 | case Poll =>
66 | (
67 | model,
68 | Effect.request(
69 | "/poll",
70 | js.Dictionary("offset" -> model.log.offset.toString),
71 | (data) => GlobalMessage.Chat(LogMsg.deserialize(data)),
72 | (errors) => GlobalMessage.Chat(Errors.apply(errors)),
73 | List(BasicAuth.header(model.name, model.password))
74 | )
75 | )
76 |
77 | case Errors(errors) =>
78 | dom.console.log(errors)
79 | (model, Effect.noEffect)
80 | }
81 |
82 |
83 | def render(model: Model): VTree = {
84 | def onSubmit(event: dom.Event): Unit = {
85 | event.preventDefault()
86 |
87 | val payload = js.Dictionary("name" -> model.name, "message" -> model.message)
88 | val success = (data: js.Dictionary[js.Any]) => {
89 | // Clear the message input now that we've sent it to the server
90 | GlobalMessage.Chat(MessageMsg(""))
91 | }
92 | val failure =
93 | (errors: Map[String, List[String]]) => GlobalMessage.Chat(Errors(errors))
94 |
95 | val effect =
96 | Effect.request(
97 | "/message",
98 | payload,
99 | success,
100 | failure,
101 | List(BasicAuth.header(model.name, model.password))
102 | )
103 |
104 | Effect.run(effect)
105 | }
106 |
107 | element("div#chat")(
108 | h2("Chat"),
109 | div(
110 | model.log.messages.reverse.map { case Message(author, message) =>
111 | span(span(s"$author: "), span(message), br)
112 | }:_*
113 | ),
114 | form("onsubmit":=eventHandler(onSubmit _))(
115 | Input.text(
116 | name=messageInput.name,
117 | placeholder="Say something",
118 | value=model.message,
119 | onInput=(m) => Effect.message(GlobalMessage.Chat(MessageMsg(m)))
120 | )
121 | )
122 | )
123 | }
124 |
125 | object messageInput {
126 | val name = "message"
127 | val selector = s"#chat input[name=$name]"
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/view/login.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package view
3 |
4 | import doodlebot.form.Input
5 | import doodlebot.model.Model
6 | import doodlebot.message.Message
7 | import org.scalajs.dom
8 | import scala.scalajs.js
9 |
10 | object Login {
11 | import doodlebot.virtualDom._
12 | import doodlebot.virtualDom.Dom._
13 |
14 | object name {
15 | val name = "name"
16 | val selector = s"#login input[name=$name]"
17 | }
18 | object password {
19 | val name = "password"
20 | val selector = s"#login input[name=$name]"
21 | }
22 |
23 | def onLogin(event: dom.Event): Unit = {
24 | event.preventDefault()
25 |
26 | val n = dom.document.querySelector(name.selector).asInstanceOf[dom.html.Input].value
27 | val p = dom.document.querySelector(password.selector).asInstanceOf[dom.html.Input].value
28 |
29 | val payload = js.Dictionary("name" -> n, "password" -> p)
30 | val success = Message.Authenticated.deserialize _
31 | val failure =
32 | (errors: Map[String, List[String]]) => Message.LoginError(errors)
33 |
34 | val effect = Effect.request("/login", payload, success, failure)
35 | Effect.run(effect)
36 | }
37 |
38 | def render(login: Model.Login): VTree = {
39 | dom.console.log(s"Rendering $login")
40 | element("div#login")(
41 | h2("Login"),
42 | form("onsubmit":=eventHandler(onLogin _))(
43 | Input.text(
44 | name=name.name,
45 | placeholder="Your name",
46 | value=login.name,
47 | help=login.errors.get(name.name).map(_.mkString(" ")).getOrElse(""),
48 | onInput=(n) => Effect.message(Message.Login(login.copy(name=n)))
49 | ),
50 | Input.password(
51 | name=password.name,
52 | placeholder="Your password",
53 | value=login.password,
54 | help=login.errors.get(password.name).map(_.mkString(" ")).getOrElse(""),
55 | onInput=(p) => Effect.message(Message.Login(login.copy(password=p)))
56 | ),
57 | button("type":="submit")("Login")
58 | )
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/view/signup.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package view
3 |
4 | import doodlebot.form.Input
5 | import doodlebot.model.Model
6 | import doodlebot.message.Message
7 | import org.scalajs.dom
8 | import scala.scalajs.js
9 |
10 | object Signup {
11 | import doodlebot.virtualDom._
12 | import doodlebot.virtualDom.Dom._
13 |
14 | object email {
15 | val name = "email"
16 | val selector = s"#signup input[name=$name]"
17 | }
18 | object name {
19 | val name = "name"
20 | val selector = s"#signup input[name=$name]"
21 | }
22 | object password {
23 | val name = "password"
24 | val selector = s"#signup input[name=$name]"
25 | }
26 |
27 | def onSignUp(event: dom.Event): Unit = {
28 | event.preventDefault()
29 |
30 | val e = dom.document.querySelector(email.selector).asInstanceOf[dom.html.Input].value
31 | val u = dom.document.querySelector(name.selector).asInstanceOf[dom.html.Input].value
32 | val p = dom.document.querySelector(password.selector).asInstanceOf[dom.html.Input].value
33 |
34 | val payload = js.Dictionary("email" -> e, "name" -> u, "password" -> p)
35 | val success = Message.Authenticated.deserialize _
36 | val failure =
37 | (errors: Map[String, List[String]]) => Message.SignupError(errors)
38 |
39 | val effect = Effect.request("/signup", payload, success, failure)
40 | Effect.run(effect)
41 | }
42 |
43 | def render(signup: Model.Signup): VTree = {
44 | dom.console.log(s"Rendering $signup")
45 | element("div#signup")(
46 | h2("Sign Up"),
47 | form("onsubmit":=eventHandler(onSignUp _))(
48 | Input.email(
49 | name=email.name,
50 | placeholder="Your email address",
51 | value=signup.email,
52 | help=signup.errors.get(email.name).map(_.mkString(" ")).getOrElse(""),
53 | onInput=(e) => Effect.message(Message.Signup(signup.copy(email=e)))
54 | ),
55 | Input.text(
56 | name=name.name,
57 | placeholder="Your name",
58 | value=signup.name,
59 | help=signup.errors.get(name.name).map(_.mkString(" ")).getOrElse(""),
60 | onInput=(u) => Effect.message(Message.Signup(signup.copy(name=u)))
61 | ),
62 | Input.password(
63 | name=password.name,
64 | placeholder="Your password",
65 | value=signup.password,
66 | help=signup.errors.get(password.name).map(_.mkString(" ")).getOrElse(""),
67 | onInput=(p) => Effect.message(Message.Signup(signup.copy(password=p)))
68 | ),
69 | button("type":="submit")("Sign up")
70 | )
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/virtualDom/dom.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package virtualDom
3 |
4 | import scala.scalajs.js
5 | import org.scalajs.dom
6 |
7 | object Dom {
8 |
9 | /** Utility type definition */
10 | type EventHandler = js.Function1[dom.Event,Unit]
11 |
12 | def eventHandler[A](f: dom.Event => Unit): EventHandler =
13 | f
14 |
15 | final case class DomAttribute(name: String, value: js.Any)
16 | implicit class DomAttributeOps(name: String) {
17 | def :=(value: String): DomAttribute =
18 | DomAttribute(name, value)
19 |
20 | def :=(value: EventHandler): DomAttribute =
21 | DomAttribute(name, eventHandler(value))
22 | }
23 |
24 | import js.JSConverters._
25 |
26 | final case class Element(name: String) {
27 | class WithAttributes(attrs: js.Dictionary[js.Any]) {
28 | def apply(): VTree = {
29 | VirtualDom.h(name, attrs, js.Array[VTree]())
30 | }
31 |
32 | def apply(child: VTree*): VTree = {
33 | VirtualDom.h(name, attrs, child.toJSArray)
34 | }
35 |
36 | def apply(text: String): VTree = {
37 | VirtualDom.h(name, attrs, text)
38 | }
39 | }
40 |
41 | def apply(attr: DomAttribute*): WithAttributes = {
42 | val attrs = (js.Dictionary.apply(attr.map { case DomAttribute(k,v) => k -> v }:_*))
43 | new WithAttributes(attrs)
44 | }
45 |
46 | def apply(child: VTree*): VTree = {
47 | VirtualDom.h(name, js.Dictionary[js.Any](), child.toJSArray)
48 | }
49 |
50 | def apply(text: String): VTree = {
51 | VirtualDom.h(name, js.Dictionary[js.Any](), text)
52 | }
53 | }
54 |
55 | /** Generic utility */
56 | def element(name: String): Element =
57 | Element(name)
58 |
59 | val div = Element("div")
60 | val span = Element("span")
61 | val h1 = Element("h1")
62 | val h2 = Element("h2")
63 | val form = Element("form")
64 | val p = Element("p")
65 | val input = Element("input")
66 | val button = Element("button")
67 | val br = VirtualDom.h("br", js.Array[VTree]())
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/main/scala/doodlebot/virtualDom/virtual-dom.scala:
--------------------------------------------------------------------------------
1 | package doodlebot
2 | package virtualDom
3 |
4 | import scala.scalajs.js
5 | import org.scalajs.dom
6 |
7 | @js.native
8 | trait VTree extends js.Object {}
9 |
10 | @js.native
11 | trait VPatch extends js.Object {}
12 |
13 | @js.native
14 | trait H extends js.Object {
15 | def apply(selector: String, children: js.Array[VTree]): VTree
16 | def apply(selector: String, properties: js.Dictionary[js.Any], children: js.Array[VTree]): VTree
17 | def apply(selector: String, text: String): VTree
18 | def apply(selector: String, properties: js.Dictionary[js.Any], text: String): VTree
19 | }
20 |
21 | @js.native
22 | object VirtualDom extends js.GlobalScope {
23 | val h: H = js.native
24 | def createElement(vtree: VTree): dom.Element = js.native
25 | def diff(a: VTree, b: VTree): js.Array[VPatch] = js.native
26 | def patch(elt: dom.Element, patches: js.Array[VPatch]): dom.Element = js.native
27 | }
28 |
--------------------------------------------------------------------------------