├── .gitignore
├── LICENSE
├── README.md
├── build.sbt
├── component
├── index-dev-component.html
└── src
│ └── main
│ └── scala
│ └── im
│ └── vdom
│ └── component
│ ├── Component.scala
│ ├── Test.scala
│ └── package.scala
├── js
└── src
│ ├── main
│ ├── resources
│ │ ├── index-dev-delegate.html
│ │ ├── index-dev-todo.html
│ │ └── index-dev.html
│ └── scala
│ │ └── im
│ │ ├── events
│ │ └── Delegate.scala
│ │ └── vdom
│ │ ├── FunctionAttributes.scala
│ │ ├── Test.scala
│ │ ├── backend
│ │ └── dom
│ │ │ ├── AttributeComponent.scala
│ │ │ ├── DOMBackend.scala
│ │ │ ├── DOMCleanupActions.scala
│ │ │ ├── DOMPatchesComponent.scala
│ │ │ ├── DOMUtils.scala
│ │ │ ├── DelegateComponent.scala
│ │ │ └── RenderToDOMComponent.scala
│ │ └── package.scala
│ └── test
│ └── scala
│ └── im
│ ├── events
│ └── DelegateSpec.scala
│ └── vdom
│ ├── CleanupSpec.scala
│ ├── PatchApplicableSpec.scala
│ ├── RenderSpec.scala
│ └── backend
│ └── dom
│ └── DOMUtilsSpec.scala
├── jvm
└── src
│ ├── main
│ └── scala
│ │ └── im
│ │ └── vdom
│ │ └── ScalaXml.scala
│ └── test
│ └── scala
│ └── im
│ └── vdom
│ ├── PatchSpec.scala
│ ├── ScalaXmlSpec.scala
│ └── backend
│ └── RenderMarkupSpec.scala
├── project
├── Dependencies.scala
├── build.properties
└── plugins.sbt
├── reactive
├── index-dev-reactive.html
└── src
│ └── main
│ └── scala
│ └── im
│ └── vdom
│ └── reactive
│ ├── Test.scala
│ └── package.scala
└── shared
└── src
├── main
└── scala
│ └── im
│ └── vdom
│ ├── Action.scala
│ ├── Attributes.scala
│ ├── Diff.scala
│ ├── Keys.scala
│ ├── Patch.scala
│ ├── VNode.scala
│ ├── backend
│ ├── Backend.scala
│ ├── Hints.scala
│ ├── MarkupBackend.scala
│ └── Utils.scala
│ └── package.scala
└── test
└── scala
└── im
└── vdom
├── DiffSpec.scala
├── OptionSpec.scala
├── VNodeSpec.scala
└── backend
├── BackendSpec.scala
├── MarkupBackendSpec.scala
└── UtilsSpec.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | build
3 | bin
4 | *~
5 | .project
6 | .settings
7 | .classpath
8 | .idea
9 | .cache*
10 | *.iml
11 | *.sjsir
12 | *.class
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import Dependencies._
2 |
3 | name := "scala-vdom root project"
4 |
5 | resolvers := allResolvers
6 |
7 | lazy val commonSettings = Seq(
8 | organization := "org.im.vdom",
9 | version := "0.1.1-SNAPSHOT",
10 | scalaVersion := "2.11.7"
11 | )
12 |
13 | lazy val commonScalacOptions = Seq("-Xlint", "-deprecation", "-Xfatal-warnings", "-feature")
14 |
15 |
16 | lazy val root = (project in file(".")).
17 | aggregate(vdomJS, vdomJVM, component, reactive).
18 | settings(name := "scala-vdom").
19 | settings(
20 | publish := {},
21 | publishLocal := {})
22 |
23 | // crossProject is a Project builder, not a project unto itself
24 | lazy val vdom = crossProject.in(file(".")).
25 | settings(scalacOptions ++= commonScalacOptions).
26 | settings(commonSettings: _*).
27 | settings(libraryDependencies ++= Seq("org.scalatest" %%% "scalatest" % "3.0.0-M15" % "test")).
28 |
29 | jvmSettings(libraryDependencies ++= Seq(
30 | "org.scala-lang.modules" %% "scala-xml" % "1.0.5" % "compile",
31 | "org.scalacheck" %% "scalacheck" % "1.12.5" % "test"
32 | )).
33 |
34 | jsSettings(
35 | relativeSourceMaps := true,
36 | jsDependencies += RuntimeDOM % "test",
37 | libraryDependencies ++= Seq("org.scala-js" %%% "scalajs-dom" % "latest.release"),
38 | persistLauncher := true,
39 | scalaJSStage in Global := FastOptStage
40 | )
41 |
42 | lazy val vdomJVM = vdom.jvm
43 | lazy val vdomJS = vdom.js
44 |
45 | lazy val component = (project in file("component")).
46 | dependsOn(vdomJS).
47 | settings(commonSettings: _*).
48 | enablePlugins(ScalaJSPlugin).
49 | settings(
50 | persistLauncher := true,
51 | scalaJSStage in Global := FastOptStage)
52 |
53 | lazy val reactive = (project in file("reactive")).
54 | dependsOn(vdomJS).
55 | settings(commonSettings: _*).
56 | settings(libraryDependencies += "org.monifu" %%% "monifu" % "latest.release").
57 | enablePlugins(ScalaJSPlugin).
58 | settings(
59 | persistLauncher := true,
60 | scalaJSStage in Global := FastOptStage)
61 |
62 |
--------------------------------------------------------------------------------
/component/index-dev-component.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
48 |
50 |
52 |
54 |
55 |
--------------------------------------------------------------------------------
/component/src/main/scala/im/vdom/component/Component.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package component
19 |
20 | import _root_.org.scalajs.{dom => d}
21 |
22 | /**
23 | * Basic state requires a DOM node and a virtual tree node (VNode).
24 | */
25 | trait State {
26 | val node: d.Node
27 | val tree: VNode
28 | }
29 |
30 | /**
31 | * Component type that links together a method to create virtual trees,
32 | * the native DOM node (the peer) and other parts needed for managing
33 | * rendering.
34 | */
35 | trait Component[S <: State] extends (S => (VNode, S)) {
36 | def map(f: VNode => VNode) =
37 | (state: S) => {
38 | val (vnode, newState) = this(state)
39 | (f(vnode), newState)
40 | }
41 | def flatMap(f: VNode => Component[S]) =
42 | (state: S) => {
43 | val (vnode, newState) = this(state)
44 | f(vnode)(newState)
45 | }
46 | }
47 |
48 | object Component {
49 |
50 | /**
51 | * Easily create a state from a function.
52 | */
53 | def apply[S <: State](r: S => (VNode, S)): Component[S] =
54 | new Component[S] {
55 | def apply(s: S) = r(s)
56 | }
57 |
58 | protected[this] def internalGetID(node: d.Node): Option[NodeIDType] = {
59 | node match {
60 | case e: d.Element => Some(e.getAttribute(ID_ATTRIBUTE_NAME))
61 | case _ => None
62 | }
63 | }
64 |
65 | /**
66 | * Get the NodeID for a DOM node. Ensure that caches are in sync
67 | * if `node` has a NodeID but it has not been cached yet.
68 | */
69 | def getID(node: d.Node): Option[NodeIDType] = {
70 | // extract from node
71 | val id = internalGetID(node)
72 |
73 | // find in cache
74 | val cachedNode = for {
75 | _id <- id
76 | cachedNode <- nodeCache.get(_id)
77 | } yield cachedNode
78 |
79 | id
80 | }
81 |
82 | //
83 | // A variety of caches, for fast lookup
84 | //
85 | val nodeCache: collection.mutable.Map[NodeIDType, Component[_]] =
86 | collection.mutable.Map()
87 | val instancesByRootID: collection.mutable.Map[Int, Component[_]] =
88 | collection.mutable.Map()
89 | val containerByRootID: collection.mutable.Map[Int, d.Node] =
90 | collection.mutable.Map()
91 |
92 | /**
93 | * Register the container. Create root ID if needed.
94 | */
95 | def registerContainer(container: d.Node): Unit = {
96 | }
97 |
98 | /**
99 | * Render a component into a container.
100 | */
101 | def render(component: Component[_], container: d.Node): Unit = {
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/component/src/main/scala/im/vdom/component/Test.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package component
19 |
20 | import scala.concurrent.duration._
21 | import scala.scalajs.js
22 | import scala.scalajs.js.JSApp
23 | import scala.scalajs.js.timers._
24 | import scala.concurrent.{ Future, ExecutionContext }
25 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
26 | import _root_.org.scalajs.{dom => d}
27 | import d.document
28 |
29 | import VNode._
30 | import HTML5Attributes._
31 | import Styles._
32 |
33 | object Test extends JSApp {
34 |
35 | def main(): Unit = {
36 | println("test of scala-vdom component")
37 |
38 | case class CountState(val count: Int, val node: d.Node, val tree: VNode) extends State
39 |
40 | val comp = Component[CountState] { state =>
41 | val newCount = state.count + 1
42 | val render = tag("div", tag("button",
43 | text(s"Click Me - $newCount"), text(s"You have clicked the button $newCount times!")))
44 | (render, state.copy(tree = render, count = newCount))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/component/src/main/scala/im/vdom/component/package.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | package object component {
20 |
21 | type NodeIDType = String
22 | val ID_ATTRIBUTE_NAME = "data-vdom-id"
23 | }
--------------------------------------------------------------------------------
/js/src/main/resources/index-dev-delegate.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 | scalajs-vdom test page
21 |
22 |
35 |
36 |
37 |
38 |
Pure scala event delegate.
39 | Delegate is derived from the ftdelegate source. The source for
40 | ftdelegate is
41 | ftdelegate on
42 | github.
43 |
44 |
45 |
46 |
47 |
48 |
49 |
Test1 - Button click delegate. Look on console for output
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
Test2 - Mouse over event
58 |
59 |
This is text that you can mouse over!
60 |
Feel free to mouse over it!
61 |
62 |
63 |
64 |
65 |
66 |
67 |
test 3 - re-root delegate
68 |
70 |
72 |
73 |
74 |
75 |
76 |
78 | Parent div
79 |
Child div
80 | - Mouse over me
81 | By mousing over the child, the mouse over on the parent
82 | shold fire
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Test queue cleanup actions. Should print cleanup to console when you press
91 | the button.
92 |
93 |
test2: this line should remain and a
47 | new node added
48 |
49 |
50 |
test3: the original test3 node was
51 | replaced
52 |
53 |
54 |
test4 this line
55 | should remain and two new lines of content will be added with
56 | different styling.
57 |
58 |
59 |
60 |
61 | test5 - testing diff that inserts a new child then replaces the
62 | child. If this line is all you see for test5, then the test
63 | failed. Below, you should see some test5 lines that
64 | indicate success.
65 |
66 |
/div>
67 |
68 |
69 |
70 |
71 |
test5a - testing patching using patch paths
72 |
73 |
74 |
75 |
76 |
77 |
test5b - testing patching using patch paths
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
test6 - testing diff when key says its the same VNode
86 |
You should not see this line. If you do its an
87 | error.
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
test8 - add an SVG rect
96 |
97 |
98 |
99 |
100 |
101 | test9:
102 |
103 | Look at js console to see output.
104 |
105 |
106 |
107 |
108 |
109 | test10:
110 |
111 |
112 |
113 |
114 |
115 |
116 | This tests some simple rendering using functional approaches
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
Render to HTML Markup Tests
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/events/Delegate.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package events
18 |
19 | import scalajs.js
20 | import _root_.org.scalajs.dom
21 | import scala.language._
22 |
23 | /**
24 | * When processing events using a Delegate, Matcher is used to determine if the
25 | * delegate function should be called on the node in the traversal
26 | * from the node where the event occurred to the Delegate's root.
27 | * Return true to allow a Delegate to process the current node in the
28 | * traversal.
29 | */
30 | trait Matcher extends Function2[dom.EventTarget, dom.EventTarget, Boolean] {
31 | def and(rhs: Matcher) = AndMatch(this, rhs)
32 | def &&(rhs: Matcher) = and(rhs)
33 | def or(rhs: Matcher) = OrMatch(this, rhs)
34 | def ||(rhs: Matcher) = or(rhs)
35 | def not = NotMatch(this)
36 | def unary_! = not
37 | }
38 |
39 | private[events] case class NotMatch(matcher: Matcher) extends Matcher {
40 | def apply(root: dom.EventTarget, current: dom.EventTarget) = !matcher(root, current)
41 | }
42 |
43 | private[events] case class AndMatch(lhs: Matcher, rhs: Matcher) extends Matcher {
44 | def apply(root: dom.EventTarget, current: dom.EventTarget) = lhs(root, current) && rhs(root, current)
45 | }
46 |
47 | private[events] case class OrMatch(lhs: Matcher, rhs: Matcher) extends Matcher {
48 | def apply(root: dom.EventTarget, current: dom.EventTarget) = lhs(root, current) || rhs(root, current)
49 | }
50 |
51 | /**
52 | * Convenience constructors.
53 | */
54 | object Matcher {
55 |
56 | def apply(f: Function2[dom.EventTarget, dom.EventTarget, Boolean]) =
57 | new Matcher { def apply(root: dom.EventTarget, current: dom.EventTarget) = f(root, current) }
58 |
59 | /**
60 | * Match nothing.
61 | */
62 | val NoMatch = Matcher { (_, _) => false }
63 |
64 | /**
65 | * Match everything.
66 | */
67 | val MatchAll = Matcher { (_, _) => true }
68 |
69 | /**
70 | * Match on the tag. Current node must be an Element.
71 | */
72 | def MatchTag(tagName: String) = Matcher {
73 | (_, _) match {
74 | case (_, el: dom.Element) if (el.tagName.toLowerCase == tagName.toLowerCase) => true
75 | case _ => false
76 | }
77 | }
78 |
79 | /**
80 | * True if the current node is the root.
81 | */
82 | def MatchRoot = Matcher { (root, target) => root == target }
83 |
84 | /**
85 | * Polyfill but not quite as robust as the reference below.
86 | *
87 | * @see [matches](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches)
88 | */
89 | private[this] def matches(el: dom.EventTarget, selector: String): Boolean = {
90 | val ns = dom.document
91 | val qmatches = ns.querySelectorAll(selector)
92 | for (i <- 0 until qmatches.length)
93 | if (qmatches(i) == el) return true
94 | false
95 | }
96 |
97 | /**
98 | * True if the potential target matches a selector.
99 | *
100 | * @see [HTML selectors](http://www.w3.org/TR/CSS21/selector.html%23id-selectors)
101 | */
102 | def MatchSelector(selector: String) = Matcher { (_, el: dom.EventTarget) => matches(el, selector) }
103 |
104 | /**
105 | * True if the target node is an Element and has a matching id.
106 | */
107 | def MatchId(id: String) = Matcher {
108 | (_, _) match {
109 | case (_, el: dom.Element) => el.id == id
110 | case _ => false
111 | }
112 | }
113 | }
114 |
115 | /**
116 | * A listener receives a dom.Event and the current Node that
117 | * is allowed to process the event. Return true if the propagation up
118 | * the tree should continue and false if it should stop. The handler
119 | * is a scala function because the scala machinery eventually
120 | * retrieves the handler and executes it.
121 | *
122 | */
123 | trait Handler extends scala.Function2[dom.Event, dom.Node, Boolean]
124 |
125 | /**
126 | * Importing Handler.Implicits into scope brings in some implicits for automatic
127 | * conversion of scala functions to Handler objects.
128 | *
129 | */
130 | object Handler {
131 | def apply(f: scala.Function2[dom.Event, dom.Node, Boolean]) = new Handler {
132 | def apply(event: dom.Event, node: dom.Node) = f(event, node)
133 | }
134 |
135 | def apply(f: scala.Function1[dom.Event, Boolean]) = new Handler {
136 | def apply(event: dom.Event, node: dom.Node) = f(event)
137 | }
138 |
139 | object Implicits {
140 | implicit def toHandler(f: (dom.Event, dom.Node) => Boolean) = Handler(f)
141 |
142 | implicit def toHandlerUnit(f: (dom.Event, dom.Node) => Unit) = new Handler {
143 | def apply(event: dom.Event, node: dom.Node) = {
144 | f(event, node)
145 | true
146 | }
147 | }
148 | implicit def toHandler1(f: dom.Event => Boolean) = Handler(f)
149 | }
150 | }
151 |
152 | /**
153 | * A handler, a matcher and a capture flag. The matcher and capture flag
154 | * are used to qualify whether a handler is used in processing an event.
155 | * Capture refers to the capturing or bubbling phases of event processing.
156 | */
157 | private[events] case class QualifiedHandler(handler: Handler, matcher: Matcher = Matcher.MatchRoot, capture: Boolean = false)
158 |
159 | /**
160 | * An object that allows a side-effecting call to `cancel`.
161 | * `delegate` is stuck in there for convienence.
162 | */
163 | trait Cancelable {
164 | def cancel(): Unit
165 | def delegate(): Delegate
166 | }
167 |
168 | /**
169 | * Delegate all event calls on the root to registered handlers.
170 | * You can change the root object at any time and the handlers
171 | * are properly deregistered/registered. The delegate is *not*
172 | * attached to the DOM element. The calling program should do that
173 | * if desired. A Delegate is mutable so you can add and remove
174 | * handlers for specific event types and change the root
175 | * object--this follows the design pattern in ftdomdelegate.
176 | *
177 | * The approach used is standard logic in java swing programs
178 | * with jgoodies.
179 | *
180 | * @see [UI EVents](http://www.w3.org/TR/DOM-Level-3-Events/#interface-EventListener)
181 | */
182 | case class Delegate(private[events] var root: Option[dom.EventTarget] = None,
183 | private[events] val handlers: collection.mutable.Map[String, collection.mutable.Set[QualifiedHandler]] = collection.mutable.Map.empty) {
184 | self =>
185 |
186 | /**
187 | * Construct with a specific root.
188 | */
189 | def this(root: dom.EventTarget) = this(Some(root))
190 |
191 | /**
192 | * Handle an event. This is the universal listener attached
193 | * to the root. A mechanism is in place to not process an event
194 | * when it crosses to a different Delegate instances that may have
195 | * attached handlers further up the tree and the event has been
196 | * marked to be ignored by delegate processing.
197 | *
198 | * Handlers can return a false value to indicate that delegates
199 | * should ignore the event.
200 | *
201 | * @see [eventPhase](https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase)
202 | */
203 | @js.annotation.JSExport
204 | protected def handler(event: dom.Event): Unit = {
205 |
206 | if(root.isEmpty) {
207 | println("Delegate root is empty but a handler is firing")
208 | assert(root.isDefined)
209 | }
210 |
211 | import js.DynamicImplicits.truthValue
212 |
213 | // If a special marker is found, other instances of Delegate
214 | // found up the chain should ignore this event as well.
215 | if (truthValue(event.asInstanceOf[js.Dynamic].__DELEGATEIGNORE))
216 | return
217 |
218 | var target =
219 | event.target match {
220 | case d: dom.Node if (d.nodeType == 3 && d.parentNode != null) => d.parentNode
221 | case n@_ => n.asInstanceOf[dom.Node]
222 | }
223 |
224 | // Build a listener list to process based on the event type and phase...
225 | // If eventPhase is defined, use it, otherwise, determine it from the targets...
226 | val phase =
227 | if (truthValue(event.eventPhase.asInstanceOf[js.Dynamic])) event.eventPhase
228 | else if (event.target != event.currentTarget) 3 // bubbling
229 | else 2 // at targetfound
230 |
231 | // filter registered handlers based on whether they are for capture and the processing phase.
232 | val registeredHandlers = handlers.getOrElse(event.`type`, Set.empty).filter { qhandler =>
233 | if (qhandler.capture && (phase == 1 || phase == 2)) true
234 | else if (!qhandler.capture && (phase == 2 || phase == 3)) true
235 | else false
236 | }
237 |
238 | var cont = true
239 | root.foreach { rt =>
240 | while (target != null) {
241 | registeredHandlers.foreach { qhandler =>
242 | if (qhandler.matcher(rt, target)) {
243 | cont = qhandler.handler(event, target)
244 | }
245 | if (!cont) {
246 | event.asInstanceOf[js.Dynamic].__DELEGATEIGNORE = true
247 | event.preventDefault
248 | return // ouch! rework logic so this is not needed in the middle
249 | }
250 | }
251 | if (target == rt)
252 | return
253 | target = target.parentNode
254 | }
255 | }
256 | }
257 |
258 | /**
259 | * List of events that by default should be captured versus bubbled.
260 | */
261 | private[events] val captureList = Seq("abort", "blur",
262 | "error", "focus", "load",
263 | "mouseenter", "mouseleave",
264 | "resize", "scroll", "unload")
265 |
266 | /**
267 | * Whether the event should by default, be processed in the capture phase or not.
268 | */
269 | protected def isDefaultCapture(eventName: String) = captureList.contains(eventName)
270 |
271 | /**
272 | * Apply this configured Delegate to the root and return a new Delegate.
273 | * You can attach to the root `Document.documentElement` to listen to
274 | * events globally.
275 | */
276 | def root(el: Option[dom.EventTarget]): Delegate = {
277 | root.foreach(stopListeningTo(_))
278 | root = el
279 | el.foreach(startListeningTo(_))
280 | this
281 | }
282 |
283 | protected def stopListeningTo(el: dom.EventTarget): Unit = {
284 | require(el != null)
285 | val stops = collection.mutable.ListBuffer[(String, QualifiedHandler)]()
286 | for {
287 | et <- handlers.keys
288 | setOfQL <- handlers.get(et)
289 | ql <- setOfQL
290 | } { stops += ((et, ql)) }
291 | stops.foreach { p =>
292 | el.removeEventListener(p._1, handler _, p._2.capture)
293 | }
294 | }
295 |
296 | protected def startListeningTo(el: dom.EventTarget): Unit = {
297 | require(el != null)
298 | for {
299 | et <- handlers.keys
300 | setOfQL <- handlers.get(et)
301 | ql <- setOfQL
302 | } {
303 | el.addEventListener(et, handler _, ql.capture)
304 | }
305 | }
306 |
307 | /**
308 | * Turn off listening for events for a specific eventType. Individual
309 | * handlers should be cancelled using the Cancelable returned from `on`.
310 | *
311 | * @param eventType The event type or None indicating all handlers for all event types.
312 | */
313 | def off(eventType: Option[String] = None): Delegate = {
314 | import vdom.OptionOps
315 | var removals: collection.mutable.Set[(String, QualifiedHandler)] = collection.mutable.Set.empty
316 |
317 | // Find removals.
318 | for {
319 | et <- handlers.keys
320 | setOfQL <- handlers.get(et)
321 | ql <- setOfQL
322 | } {
323 | if (Some(et) wildcardEq eventType) {
324 | removals += ((et, ql))
325 | setOfQL -= ql
326 | }
327 | }
328 |
329 | // Remove event listeners based on removals.
330 | for { r <- removals } root.foreach(_.removeEventListener(r._1, this.handler _, r._2.capture))
331 |
332 | this
333 | }
334 |
335 | /**
336 | * Add a handler for a specific event. The same handler can be added multiple times.
337 | *
338 | * @return A Cancelable used to cancel the listening of the handler.
339 | */
340 | def on(eventType: String,
341 | handler: Handler,
342 | matcher: Matcher = Matcher.MatchRoot,
343 | useCapture: Option[Boolean] = None): Cancelable = {
344 |
345 | val capture = useCapture.getOrElse(isDefaultCapture(eventType))
346 |
347 | // If root defined, add universal handler to root if this is the
348 | // first time that eventType has been requested to monitor.
349 | if (!handlers.contains(eventType))
350 | root.foreach(_.addEventListener(eventType, this.handler _, capture))
351 |
352 | // Create new listeners set for a specific event by adding the new delegate.
353 | val qhandler = QualifiedHandler(handler, matcher, capture)
354 | val newHandlers = handlers.getOrElse(eventType, collection.mutable.Set.empty) + qhandler
355 |
356 | // Update the handler set.
357 | handlers += (eventType -> newHandlers)
358 |
359 | new Cancelable {
360 | private val _eventType = eventType
361 | private val _qhandler = qhandler
362 | def cancel(): Unit = {
363 | handlers.get(_eventType).foreach(_.remove(_qhandler))
364 | if (handlers.get(_eventType).map(_.size).getOrElse(0) > 0) {
365 | // no need to listen to this event type, no handlers
366 | root.foreach(_.removeEventListener(_eventType, self.handler _, capture))
367 | }
368 | }
369 | def delegate() = self
370 | }
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/FunctionAttributes.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.language._
20 |
21 | /**
22 | * DOM events. If you can define your own convenience methods for defining methods and import
23 | * those into scope.
24 | */
25 | trait UIEvents {
26 | implicit class FunctionToKey(eventType: String) extends RichFuncString(eventType)
27 | val abort = "abort".func
28 | val beforeinput = "beforeinput".func
29 | val blur = "blur".func
30 | val click = "click".func
31 | val compositionstart = "compositionstart".func
32 | val compositionupdate = "compositionupdate".func
33 | val compositionend = "compositionend".func
34 | val dblclick = "dblclick".func
35 | val error = "error".func
36 | val focus = "focus".func
37 | val focusin = "focusin".func
38 | val focusout = "focusout".func
39 | val input = "input".func
40 | val keyup = "keyup".func
41 | val keydown = "keydown".func
42 | val load = "load".func
43 | val mousedown = "mousedown".func
44 | val mouseenter = "mouseenter".func
45 | val mouseleave = "mouseleave".func
46 | val mouseout = "mouseout".func
47 | val mouseover = "mouseover".func
48 | val resize = "resize".func
49 | val scroll = "scroll".func
50 | val select = "select".func
51 | val unload = "unload".func
52 | val wheel = "wheel".func
53 | }
54 |
55 | object UIEvents extends UIEvents
56 |
57 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/AttributeComponent.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 | import _root_.org.scalajs.{dom => d}
30 | import Defaults._
31 | import im.vdom.StyleKey
32 | import im.vdom.ActionLists
33 | import im.vdom.backend.AttrHint
34 | import im.vdom.AttrKey
35 |
36 | /**
37 | * Allows lifting `KeyValue` pairs into a function that can manage DOM attributes.
38 | * Subclass and override and define a new Backend to use your attribute hints
39 | * and side-effecting functions.
40 | *
41 | * This layer does not automatically run named cleanup actions.
42 | */
43 | trait AttributeComponent { self: DOMHints with DelegateComponent with ActionLists[d.Node] =>
44 |
45 | /**
46 | * Call `Element.setAttribute()` with an optional namespace.
47 | */
48 | protected def setAttribute(el: d.Element, name: String,
49 | value: String, namespace: Option[String] = None): Unit = {
50 | namespace.fold(el.setAttribute(name, value))(ns => el.setAttributeNS(ns, name, value))
51 | }
52 |
53 | protected def attr(node: d.Node, kv: KeyValue[_]): Unit = {
54 | val el = node.asInstanceOf[d.Element]
55 | val name = kv.key.name
56 | val hints: AttrHint = attrHint(name).getOrElse(Hints.EmptyAttrHints)
57 | kv.value.fold(el.removeAttribute(name)) { v =>
58 | if (hints.values(Hints.MustUseAttribute))
59 | setAttribute(el, name, v.toString, kv.key.namespace)
60 | else
61 | el.asInstanceOf[js.Dynamic].updateDynamic(name)(v.asInstanceOf[js.Any])
62 | }
63 | }
64 |
65 | protected def style(node: d.Node, kv: KeyValue[_]): Unit = {
66 | val name = kv.key.name
67 | val style = node.asInstanceOf[d.html.Element].style
68 | kv.value.map(_.toString).fold[Unit](style.removeProperty(name))(v => style.setProperty(name, v, ""))
69 | }
70 |
71 | /**
72 | * Add a `Delegate` to a DOM node, if needed, to hande the kv event handler. If
73 | * the handler is None, removes the event type handler but leaves the Delegate.
74 | * Skips adding the handler if it already has been attached.
75 | *
76 | * An action is added to remove the Delegate as part of a cleanup activity.
77 | */
78 | protected def handler(node: d.Node, key: FunctionKey, value: Option[FunctionValue]): Unit = {
79 | import events._
80 |
81 | val name = key.name
82 |
83 | def addit(d: Delegate, v: FunctionValue) = {
84 | val cancelable = d.on(name, v.handler, v.matcher, v.useCapture)
85 | // When this attribute, reperesenting an event, is about to be reset,
86 | // remove this handler so its not registered twice.
87 | addAction(name, node, Action.lift {
88 | cancelable.cancel
89 | node
90 | })
91 | linkDelegate(d, node)
92 | cancelable
93 | }
94 |
95 | value.fold[Unit] {
96 | getDelegate(node).fold() { d => d.off(Some(name)) } // turn off listening for "name" event
97 | } { v =>
98 | getDelegate(node).fold {
99 | val cancelable = addit(Delegate(), v) // create new Delegate
100 | cancelable.delegate.root(Some(node)) // set the node Delegate listens to
101 | } { delegate =>
102 | addit(delegate, v).delegate // use existing Delegate
103 | }
104 | }
105 | }
106 |
107 | /**
108 | * Pure side effect. This can be easily mapped or wrapped into an IOAction.
109 | */
110 | trait KeyValueAction extends (d.Node => Unit)
111 |
112 | /** Lift KeyValue to KeyValueAction */
113 | implicit def toKeyValueAction(kv: KeyValue[_]): KeyValueAction = {
114 | kv match {
115 | case kv@KeyValue(AttrKey(_, _), _) => new KeyValueAction {
116 | def apply(target: d.Node): Unit = attr(target, kv)
117 | }
118 | case kv@KeyValue(StyleKey(_), _) => new KeyValueAction {
119 | def apply(target: d.Node): Unit = style(target, kv)
120 | }
121 | case kv@KeyValue(key@FunctionKey(_), optv) => new KeyValueAction {
122 | def apply(target: d.Node): Unit = handler(target, key, optv.asInstanceOf[Option[FunctionValue]])
123 | }
124 | case _ => throw new IllegalArgumentException("Unknown KeyValue KeyPart")
125 | }
126 | }
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/DOMBackend.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 |
30 | import _root_.org.scalajs.{dom => d}
31 |
32 | import Defaults._
33 |
34 | /**
35 | * A base DOM backend that can be extended with specific components as needed.
36 | * This trait defines the context type.
37 | */
38 | trait BasicDOMBackend extends Backend with DOMUtils {
39 | type This = BasicDOMBackend
40 | type Context = BasicContext
41 |
42 | protected[this] def createContext() = new BasicContext {}
43 | }
44 |
45 | /**
46 | * An example of extending the basic backend with your components.
47 | */
48 | trait DOMBackend extends BasicDOMBackend
49 | with DOMPatchesComponent
50 | with RenderToDOMComponent
51 | with AttributeComponent
52 | with DOMHints
53 | with DOMCleanupActions
54 | with DelegateComponent
55 | with ActionLists[d.Node]
56 |
57 | /**
58 | * Use this backend when a DOM patching process is desired.
59 | */
60 | object DOMBackend extends DOMBackend
61 |
62 |
63 | /**
64 | Note: https://groups.google.com/forum/#!topic/scala-js/qlUJWSQ6ccE
65 |
66 | No Brendon is right: `if (this._map)` tests whether `this._map` is falsy.
67 | To test that according to JS semantics, you can use
68 |
69 | js.DynamicImplicits.truthValue(self._map.asInstanceOf[js.Dynamic])
70 |
71 | which returns false if and only if `self._map` is falsy. This pattern should typically be avoided in Scala code, unless transliterating from JS.
72 |
73 | Cheers,
74 | Sébastien
75 |
76 | */
77 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/DOMCleanupActions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 |
30 | import _root_.org.scalajs.{ dom => d }
31 |
32 | import Defaults._
33 |
34 | /**
35 | * Cleanup actions specific to a DOM backend.
36 | *
37 | * Add a "queue" to DOM nodes for cleanup actions that are run when a DOM node is
38 | * detached from the environment or an attribute's value is about to be changed.
39 | *
40 | * Cleanup queues are available for the d.Node as well as each
41 | * attibute-named cleanup queues. An attribute's queue are
42 | * run before the attribute is set to a new value and then the attribute
43 | * cleanup queue is cleared.
44 | *
45 | * A node property called __namedCleanupActions is added to the DOM node.
46 | *
47 | * Cleanup queues can be used to disconnect event handlers or
48 | * manager external resources.
49 | *
50 | */
51 | trait DOMCleanupActions extends ActionLists[d.Node] { self: DOMBackend =>
52 | import js.DynamicImplicits.truthValue
53 |
54 | val nodeCleanupQueueName = "__node__"
55 |
56 | type NamedCleanupActions = Map[String, IOAction[_]]
57 |
58 | /** Sets the action as the new named cleanup action. */
59 | private[this] def setNamed(node: d.Node, namedActions: NamedCleanupActions): Unit =
60 | node.asInstanceOf[js.Dynamic].__namedCleanupActions = namedActions.asInstanceOf[js.Any]
61 |
62 | private[this] def getNamedQueues(node: d.Node): Option[NamedCleanupActions] = {
63 | val x = node.asInstanceOf[js.Dynamic].__namedCleanupActions
64 | if (truthValue(x)) Some(x.asInstanceOf[NamedCleanupActions])
65 | else None
66 | }
67 |
68 | /**
69 | * Add a cleanup action to run just before an attribute's value is set
70 | * to a new value. The specified actions are run after already registered actions.
71 | */
72 | def addAction(keyName: String, node: d.Node, action: IOAction[_]*): Unit = {
73 | val actions = Action.seq(action: _*)
74 | val named = getNamedQueues(node).getOrElse(Map())
75 | val existingAction = named.get(keyName).getOrElse(Action.successful(()))
76 | val newAction = existingAction andThen actions
77 | setNamed(node, named + (keyName -> newAction))
78 | }
79 |
80 | def addDetachAction(node: d.Node, action: IOAction[_]*): Unit = {
81 | val latest = Action.seq(action: _*)
82 | addAction(nodeCleanupQueueName, node, latest)
83 | }
84 |
85 | /**
86 | * Runs a breadth first pass at running actions. First children recursion,
87 | * then attributes then the node itself. The action list is set to undefined.
88 | */
89 | def runActions(node: d.Node): Unit = {
90 | getNamedQueues(node).fold() { actionMap =>
91 | (0 until node.childNodes.length).foreach{ i => runActions(node.childNodes(i)) }
92 | (actionMap - nodeCleanupQueueName).values.foreach(run(_))
93 | actionMap.get(nodeCleanupQueueName).foreach(run(_))
94 |
95 | }
96 | node.asInstanceOf[js.Dynamic].__namedCleanupActions = js.undefined.asInstanceOf[js.Any]
97 | }
98 |
99 | def runActions(name: String, node: d.Node): Unit = {
100 | getNamedQueues(node).fold() { _.get(name).foreach(run(_)) }
101 | // clear only that queue
102 | getNamedQueues(node).foreach { a => setNamed(node, a - name) }
103 | }
104 |
105 | /**
106 | * Create a `d.Node => IOAction[d.Node]` function that can be used to flatMap
107 | * an existing IOAction. When run, it adds a cleanup method to the d.Node's cleanup queue.
108 | * Returns the d.Node input.
109 | */
110 | private def cleanup(action: IOAction[_]*): (d.Node => IOAction[d.Node]) =
111 | (node: d.Node) => Action.lift {
112 | addDetachAction(node, action: _*)
113 | node
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/DOMPatchesComponent.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 |
30 | import _root_.org.scalajs.{dom => d}
31 |
32 | import Defaults._
33 |
34 |
35 |
36 | /**
37 | * DOM specific Patch processing. Includes an implicit to automatically
38 | * convert Patches to PatchPerformers.
39 | *
40 | * In here be dragons.
41 | *
42 | * I'm not sure that we need to link the vnode to the dom node as the
43 | * events delegate and cleanup queues are independent of the vnode and
44 | * the linkage is not needed for those two areas.
45 | */
46 | trait DOMPatchesComponent extends PatchesComponent {
47 | self: BasicDOMBackend with RenderToDOMComponent with AttributeComponent with ActionLists[d.Node] =>
48 |
49 | type PatchInput = d.Node
50 | type PatchOutput = d.Node
51 |
52 | /** An action that unlinks a VNode from a d.Node. Runs cleanup actions. Returns the input d.Node. */
53 | protected def detach(node: d.Node): IOAction[PatchOutput] = Action.lift {
54 | runActions(node)
55 | DOMEnvironment.unlink(node)
56 | node
57 | }
58 |
59 | /** An action that adds a link between a VNode and d.Node. Returns d.Node. */
60 | protected def attach(vnode: VNode, dnode: d.Node): IOAction[PatchOutput] = Action.lift {
61 | DOMEnvironment.link(vnode, dnode)
62 | dnode
63 | }
64 |
65 | /**
66 | * Automatically convert a Patch to a PatchPerformer so it can be
67 | * applied easily to an input.
68 | */
69 | implicit def toPatchPerformer(patch: Patch)(implicit executor: ExecutionContext) =
70 | makeApplyable(patch)(executor)
71 |
72 | /**
73 | * Convert patches to a runnable patch based on the backend then it can be
74 | * converted to an IOAction to run. The PatchPerformer's are constructed
75 | * to return either the target node, the node that was just changed, or the
76 | * node that was created or changed. This allows you to use combinators
77 | * to run actions on the object returned from the operation.
78 | */
79 | def makeApplyable(patch: Patch)(implicit executor: ExecutionContext): PatchPerformer = {
80 | patch match {
81 |
82 | case PathPatch(patch, path) =>
83 | val pp = makeApplyable(patch)
84 | PatchPerformer { target =>
85 | find(target, path).fold[IOAction[PatchOutput]](
86 | Action.failed(new IllegalArgumentException(s"Unable to find node from $target using path $path")))(
87 | newTarget => pp(newTarget))
88 | }
89 |
90 | case OrderChildrenPatch(i) => PatchPerformer { target =>
91 | require(target != null)
92 | // Process removes correctly, don't delete by index, delete by node object.
93 | // I should not need undefor here, a bad index should throw an exception. It's an API violation.
94 | // The node indexes change as you delete which makes this harder than you think.
95 | val childrenToRemove = i.removes.map(target.childNodes(_).asInstanceOf[UndefOr[d.Node]])
96 | childrenToRemove.foreach { c: UndefOr[d.Node] =>
97 | c.foreach { x =>
98 | target.removeChild(x)
99 | }
100 | }
101 | // Process moves.
102 | // Make copy of current nodes and move via the indexes.
103 | val copyOfCurrent = (0 to target.childNodes.length).map(target.childNodes(_))
104 | i.moves.foreach {
105 | case (from, to) =>
106 | target.childNodes(to) = copyOfCurrent(from)
107 | }
108 | val x = childrenToRemove.filter(_.isDefined).map(_.get).map(detach(_))
109 | Action.seq(x: _*) andThen Action.successful(target)
110 | }
111 |
112 | /**
113 | * Run left then right. Return the original input object.
114 | *
115 | * TODO: Should this return, somehow, the output of the left and right operations?
116 | */
117 | case AndThenPatch(left, right) => {
118 | val pleft = makeApplyable(left)
119 | val pright = makeApplyable(right)
120 | PatchPerformer { target => pleft(target) andThen pright(target) andThen Action.successful(target) }
121 | }
122 |
123 | case KeyValuePatch(keyValues) => PatchPerformer { target =>
124 | require(target != null)
125 | val ele = target.asInstanceOf[d.Element]
126 | keyValues.foreach { a =>
127 | runActions(a.key.name, target) // run cleanup for that key
128 | a(ele) // then apply the keyvalue to the element to add it back
129 | }
130 | Action.successful(target)
131 | }
132 |
133 | case RemovePatch => PatchPerformer { target =>
134 | require(target != null)
135 | target.parentUndefOr.fold[IOAction[PatchOutput]](Action.failed(new IllegalArgumentException(s""))) { p =>
136 | p.removeChild(target)
137 | detach(target)
138 | }
139 | }
140 |
141 | case InsertPatch(vnode, pos) => PatchPerformer { target =>
142 | require(target != null)
143 | render(vnode).map { newNode =>
144 | pos.filter(i => i < target.childNodes.length && i >= 0).fold(target.appendChild(newNode)) { index =>
145 | target.insertBefore(newNode, target.childNodes(index))
146 | }
147 | }.flatMap(attach(vnode, _))
148 | }
149 |
150 | // Not sure this returns el, but maybe does not matter if text node is always a leaf
151 | case TextPatch(content) => PatchPerformer { el =>
152 | require(el != null)
153 | Action.successful {
154 | if (el.nodeType == 3) {
155 | // if already a text node, just replace it
156 | val textEl = el.asInstanceOf[d.Text]
157 | textEl.replaceData(0, textEl.length, content)
158 | //textEl.data = content
159 | textEl
160 | } else {
161 | val t2: UndefOr[d.Text] = createText(content)
162 | replaceRoot(el, t2)
163 | t2.get
164 | }
165 | }
166 | }
167 |
168 | /**
169 | * If a target has a parent, replace target with the
170 | * replacement. Otherwise, throw an exception. Return
171 | * the new node that replaced the target node.
172 | */
173 | case ReplacePatch(replacement) => PatchPerformer { target =>
174 | require(target != null)
175 | // replaceChild returns the old node, not the new one! we want the new one!s
176 | target.parentUndefOr.fold[IOAction[PatchOutput]](Action.failed(
177 | new IllegalArgumentException(s"No parent for target $target"))) { parent =>
178 | detach(target) andThen render(replacement).map { n =>
179 | parent.replaceChild(n, target)
180 | n
181 | }.flatMap(attach(replacement, _))
182 | }
183 | }
184 |
185 | /**
186 | * Don't do anything, but pass along the PatchInput object, whatever it was.
187 | */
188 | case EmptyPatch => PatchPerformer { Action.successful(_) }
189 | }
190 | }
191 | }
192 |
193 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/DOMUtils.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 | package dom
19 |
20 | import scalajs.js
21 | import js.UndefOr
22 | import _root_.org.scalajs.{ dom => d }
23 |
24 | /**
25 | * Utilities to help smooth through the different ways DOM environments
26 | * handle corner cases. Accessing this module forces some minor
27 | * detection processing to occur.
28 | *
29 | */
30 | trait DOMUtils {
31 |
32 | import Defaults._
33 |
34 | private val doc = d.document
35 |
36 | /**
37 | * Create a text node.
38 | */
39 | def createText(content: String) = doc.createTextNode(content)
40 |
41 | /**
42 | * Create a DOM element using selected information from description.
43 | * This function creates the element but does not perform any other
44 | * initialization processing e.g. setting attributes.
45 | */
46 | def createEl(description: VirtualElementNode): d.Element = {
47 | description.namespace.fold(doc.createElement(description.tag))(ns => doc.createElementNS(ns, description.tag))
48 | }
49 |
50 | // val (isWebKit, isFirefox, isTrident) = {
51 | // val uagent = dom.window.navigator.userAgent
52 | // (uagent.contains("WebKit"), uagent.contains("Firefox"),
53 | // uagent.indexOf("Trident"))
54 | // }
55 |
56 | val (normalizes, ignoresEmptyText) = {
57 | // var p: d.Element = dom.document.createElement("p")
58 | // p.appendChild(dom.document.createTextNode("a"))
59 | // p.insertAdjacentHTML("beforeend", "b")
60 | // val normalizes = p.childNodes.length == 1
61 |
62 | // var p = dom.document.createElement("p")
63 | // p.appendChild(dom.document.createTextNode(""))
64 | // p.insertAdjacentHTML("beforeend", "")
65 | // val ignoresEmptyText = p.firstChild.nodeType != 3
66 | // (normalizes, ignoresEmptyText)
67 | (true, true)
68 | }
69 |
70 | /**
71 | * Return falsy value as if calling javascript's `object.member` to see if an object
72 | * has a method or property on it.
73 | */
74 | def has(node: d.Node, property: String): Boolean =
75 | js.DynamicImplicits.truthValue(node.asInstanceOf[js.Dynamic](property).asInstanceOf[js.Dynamic])
76 |
77 | /** Remove all children from the node. */
78 | def removeChildren(node: d.Node): d.Node = {
79 | assert(node != null)
80 | var last: d.Node = node.lastChild
81 | while (last != null) {
82 | node.removeChild(last)
83 | last = node.lastChild
84 | }
85 | node
86 | }
87 |
88 | // /**
89 | // * Replace the oldRoot with the newRoot.There must be a parent to oldRoot otherwise
90 | // * no change is performed.
91 | // */
92 | // def replaceRoot(oldRoot: d.Node, newRoot: d.Node): Unit = {
93 | // if (oldRoot.parentUndefOr.isDefined) {
94 | // oldRoot.parentNode.replaceChild(newRoot, oldRoot)
95 | // }
96 | // }
97 |
98 | /**
99 | * Replace oldRoot with newRoot. Both must exist and oldRoot must have a parent.
100 | * Implicits may help lift regular nodes into UndefOr.
101 | */
102 | def replaceRoot(oldRoot: UndefOr[d.Node], newRoot: UndefOr[d.Node]): Unit = {
103 | val x = for {
104 | p <- oldRoot
105 | parent <- p.parentUndefOr
106 | t <- newRoot
107 | } yield parent.replaceChild(t, p)
108 | }
109 |
110 | /**
111 | * Find a node by navigating through the children based on the child indexes.
112 | * Return None if no node is found, and hence, the path was not in alignment
113 | * with the actual child structure.
114 | */
115 | def find(target: d.Node, path: Seq[Int]): Option[d.Node] = {
116 | path match {
117 | case Nil => Some(target)
118 | case head :: tail =>
119 | if (target.childNodes.length == 0 ||
120 | head >= target.childNodes.length ||
121 | target.childNodes(head) == null) return None
122 | find(target.childNodes(head), path.drop(1))
123 | }
124 | }
125 |
126 | import util.control.Exception
127 |
128 | /**
129 | * If the element has a custom data attribute storing the checksum and its value
130 | * matches the calculated checksum on the markup input, return true. Otherwise,
131 | * return false.
132 | */
133 | def canReuseMarkup(markup: String, n: d.Element) =
134 | n.getAttribute(Utils.ChecksumAttrName) match {
135 | case checksumstr: String =>
136 | val checksum = Exception.catching(classOf[NumberFormatException]) opt (checksumstr.toInt) getOrElse (-1)
137 | if (checksum == Utils.adler32(markup)) true
138 | else false
139 | case null => false
140 | }
141 |
142 | }
143 |
144 | object DOMUtils extends DOMUtils
145 |
146 | /**
147 | * A set of IOActions that can be composed with other IOActions.
148 | */
149 | trait DOMActions {
150 | }
151 |
152 | /** @see [DOMAcions] */
153 | object DOMActions extends DOMActions
154 |
155 | /**
156 | * Link VNodes and DOM Nodes. This can be implemented either in a global map
157 | * or by sticking the vnode into the DOM node using a secret property. The
158 | * secret property is `__vnode`.
159 | *
160 | * Note: truthy test: js.DynamicImplicits.truthValue(self._map.asInstanceOf[js.Dynamic])
161 | */
162 | trait DOMInstanceMap {
163 | import events._
164 |
165 | /**
166 | * Link a VNode to a DOM Node.
167 | */
168 | def link(vnode: VNode, dnode: d.Node): Unit = {
169 | dnode.asInstanceOf[js.Dynamic].__vnode = vnode.asInstanceOf[js.Any]
170 | }
171 |
172 | /**
173 | * Unlink a VNode and DOM Node.
174 | */
175 | def unlink(dnode: d.Node): Unit = {
176 | dnode.asInstanceOf[js.Dynamic].__vnode = js.undefined.asInstanceOf[js.Any]
177 | }
178 |
179 | /**
180 | * Get the linked VNode from a DOM Node, if one was linked previously.
181 | */
182 | def getVNode(dnode: d.Node): Option[VNode] = {
183 | val x = dnode.asInstanceOf[js.Dynamic].__vnode
184 | if (js.DynamicImplicits.truthValue(x)) Some(x.asInstanceOf[VNode])
185 | None
186 | }
187 |
188 | }
189 |
190 | trait DOMEnvironment extends DOMInstanceMap with DOMUtils
191 |
192 | /**
193 | * Singleton that holds virtual DOM state. Mutable state lives somewhere :-)
194 | */
195 | object DOMEnvironment extends DOMEnvironment
196 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/DelegateComponent.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 |
30 | import _root_.org.scalajs.{ dom => d }
31 |
32 | import Defaults._
33 |
34 | /**
35 | * Manage a `Delegate` associated with a single DOM Node.
36 | * Each DOM node should have at most one Delegate since
37 | * a Delegate manages many event types.
38 | *
39 | * Delegate cleanup is handled by adding a node clean up action.
40 | *
41 | * The Delegate is attached under the __delegate property
42 | * of the node and if a delegate detach action has been added on this node
43 | * already, a property __delegate_cleanupaction property is set to true.
44 | *
45 | */
46 | trait DelegateComponent { self: ActionLists[d.Node] =>
47 |
48 | import events._
49 | import js.DynamicImplicits.truthValue
50 |
51 | /**
52 | * Set `node.__delegate` to js.undefined. Set Delegate's root to None.
53 | */
54 | def rmDelegate(dnode: d.Node): d.Node = {
55 | getDelegate(dnode).foreach { d =>
56 | d.root(None) // turn everything off
57 | dnode.asInstanceOf[js.Dynamic].__delegate = js.undefined.asInstanceOf[js.Any]
58 | }
59 | dnode
60 | }
61 |
62 | /**
63 | * Link a Delegate to a DOM Node. It replaces any previous
64 | * Delegate linkage that may exist. Adds a cleanup action
65 | * to disconnect the delegate unless one has already been added.
66 | */
67 | def linkDelegate(del: Delegate, dnode: d.Node): Unit = {
68 | require(dnode != null)
69 | val dyndnode = dnode.asInstanceOf[js.Dynamic]
70 | dyndnode.__delegate = del.asInstanceOf[js.Any]
71 | if (!truthValue(dyndnode.__delegate_cleanupaction)) {
72 | addDetachAction(dnode, cleanupAction(dnode))
73 | dyndnode.__delegate_cleanupaction = true
74 | }
75 | }
76 |
77 | /**
78 | * Get an optional Delegate linked to a DOM node.
79 | */
80 | def getDelegate(dnode: d.Node): Option[Delegate] = {
81 | require(dnode != null)
82 | val x = dnode.asInstanceOf[js.Dynamic].__delegate
83 | if (truthValue(x)) Some(x.asInstanceOf[Delegate])
84 | else None
85 | }
86 |
87 | /** Create a cleanup IOAction that calls rmDelegate and returns the node. */
88 | private def cleanupAction(node: d.Node) = Action.lift { rmDelegate(node) }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/backend/dom/RenderToDOMComponent.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 | package dom
20 |
21 | import scala.concurrent.ExecutionContext
22 | import scala.concurrent.Future
23 | import scala.scalajs.concurrent.JSExecutionContext.queue
24 | import scala.scalajs.js
25 | import scala.scalajs.js._
26 | import scala.scalajs.js.UndefOr
27 | import scala.scalajs.js.UndefOr._
28 | import scala.language._
29 |
30 | import _root_.org.scalajs.{dom => d}
31 |
32 | import Defaults._
33 |
34 |
35 | /**
36 | * Convert a virtual node to a DOM node.
37 | *
38 | * TODO: Fix the execution context. Ensure that the render output future is
39 | * dependent on the children rendering futures.
40 | */
41 | trait RenderToDOMComponent extends RendererComponent {
42 | self: Backend with AttributeComponent =>
43 |
44 | import DOMEnvironment._
45 |
46 | type RenderOutput = d.Node
47 |
48 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput] = {
49 | vnode match {
50 | case v@VirtualText(content) => Action.lift { createText(content) }
51 |
52 | case v@VirtualElementNode(tag, attributes, children, key, namespace) =>
53 | val makeEl: IOAction[d.Node] = Action.lift {
54 | val newNode = createEl(v)
55 | attributes.foreach(_(newNode))
56 | newNode
57 | } flatMap { el =>
58 | val childrenactions = children.map { child =>
59 | val renderChildAction = render(child)
60 | // a bit ugly since appendChild is really a side effoct
61 | renderChildAction.map { c => el.appendChild(c); c }
62 | }
63 | // the child render and append won't happen unless its exposed to be run later
64 | Action.seq(childrenactions: _*) andThen Action.successful(el)
65 | }
66 | makeEl
67 |
68 | case EmptyNode() =>
69 | /** Empty nodes become empty divs */
70 | Action.lift(createEl(VNode.tag("div")))
71 |
72 | case ThunkNode(f) => render(f())
73 |
74 | case CommentNode(c) =>
75 | Action.lift(d.document.createComment(c))
76 | }
77 | }
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/js/src/main/scala/im/vdom/package.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.language._
20 |
21 | import scalajs.js
22 | import js.UndefOr
23 | import UndefOr._
24 | import _root_.org.scalajs.{dom => d}
25 |
26 | trait Defaults {
27 |
28 | /**
29 | * Convenience methods for DOM Nodes and Elements.
30 | */
31 | implicit class RichNode[T <: d.Node](n: T) {
32 | import UndefOr._
33 | def parentUndefOr: UndefOr[d.Node] = {
34 | val p = n.parentNode
35 | if (p == null) js.undefined
36 | p
37 | }
38 | }
39 |
40 | }
41 |
42 | object Defaults extends Defaults
43 |
44 | /**
45 | * KeyValues that holds a few pieces of function configuration
46 | * information relevant to calling a handler when a dom.Event is fired.
47 | */
48 | case class FunctionValue(handler: events.Handler, matcher: events.Matcher = events.Matcher.MatchRoot,
49 | useCapture: Option[Boolean] = None)
50 |
51 | /**
52 | * Value that is a function for an event handler. The entire
53 | * event handling capabilities need to be lifted to a string
54 | * based action framework to allow server side rendering to
55 | * work better on jvm. Need to think this through for awhile.
56 | */
57 | case class FunctionKey(val name: String) extends KeyPart { self =>
58 | def ~~>(v: events.Handler) = KeyValue[FunctionValue](self, Some(FunctionValue(v, events.Matcher.MatchRoot, None)))
59 | def ~~>(v: FunctionValue) = KeyValue[FunctionValue](self, Some(v))
60 | }
61 |
62 | /**
63 | * Convenience class to allow you to define a FunctionKey using '"keyname".func`.
64 | */
65 | class RichFuncString(val name: String) {
66 | def func = FunctionKey(name)
67 | }
--------------------------------------------------------------------------------
/js/src/test/scala/im/events/DelegateSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package events
18 |
19 | import events._
20 | import org.scalatest.FlatSpec
21 | import org.scalatest.Matchers
22 | import org.scalatest.OptionValues
23 | import _root_.org.scalajs.{dom => d}
24 |
25 | /**
26 | * Test Delegates.
27 | */
28 | class DelegateSpec extends FlatSpec
29 | with Matchers
30 | with OptionValues {
31 |
32 | import d.document
33 | import vdom.backend.dom.DOMBackend
34 | import scala.scalajs.js.DynamicImplicits.truthValue
35 |
36 | "delegate component" should "attach a Delegate to a node" in {
37 | val p = document.createElement("p")
38 | val d = Delegate(None)
39 | DOMBackend.linkDelegate(d, p)
40 | DOMBackend.getDelegate(p) should not be (null)
41 | }
42 |
43 | it should "allow removing the Delegate" in {
44 | val p = document.createElement("p")
45 | val d = Delegate(None)
46 | DOMBackend.linkDelegate(d, p)
47 | DOMBackend.getDelegate(p) should not be (null)
48 | DOMBackend.rmDelegate(p)
49 | DOMBackend.getDelegate(p) should be(None)
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/js/src/test/scala/im/vdom/CleanupSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import _root_.org.scalajs.{ dom => d }
20 | import scalajs.js
21 |
22 | import org.scalatest.FlatSpec
23 | import org.scalatest.Matchers
24 | import org.scalatest.OptionValues
25 |
26 | /**
27 | * Test cleanup actions on DOM nodes and properties.
28 | */
29 | class CleanupSpec extends FlatSpec
30 | with Matchers
31 | with OptionValues {
32 |
33 | import d.document
34 | import backend.dom.DOMBackend
35 | import js.DynamicImplicits.truthValue
36 |
37 | "cleanup component" should "attach a cleanup queue to a DOM node" in {
38 | val p = document.createElement("p")
39 | DOMBackend.addDetachAction(p, Action.successful(true))
40 | truthValue(p.asInstanceOf[js.Dynamic].__namedCleanupActions) should be(true)
41 | }
42 |
43 | it should "run the node level cleanup action when asked to cleanup" in {
44 | val p = document.createElement("p")
45 | var counter = 0
46 | DOMBackend.addDetachAction(p, Action.lift {
47 | counter += 1
48 | })
49 | DOMBackend.runActions(p)
50 | counter should be(1)
51 | }
52 |
53 | it should "allow a named cleanup queue to be created" in {
54 | val p = document.createElement("p")
55 | var counter = 0
56 | DOMBackend.addAction("blah", p, Action.lift {
57 | counter += 1
58 | })
59 | val t = p.asInstanceOf[js.Dynamic].__namedCleanupActions.asInstanceOf[Map[String, IOAction[_]]]
60 | t.contains("blah") should be(true)
61 |
62 | DOMBackend.runActions(p)
63 | counter should be(1)
64 | }
65 |
66 | it should "process the hierachy of nodes and run all detach actions" in {
67 | val div = document.createElement("div")
68 | var counter = 0
69 | DOMBackend.addDetachAction(div, Action.lift {
70 | counter += 1
71 | })
72 | val div2 = document.createElement("p")
73 | DOMBackend.addDetachAction(div2, Action.lift {
74 | counter += 1
75 | })
76 | div.appendChild(div2)
77 |
78 | DOMBackend.runActions(div)
79 | counter should be (2)
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/js/src/test/scala/im/vdom/PatchApplicableSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.util._
20 | import _root_.org.scalajs.{ dom => d }
21 | import concurrent._
22 | import scalajs.js
23 | import js._
24 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
25 |
26 | import org.scalatest.AsyncFlatSpec
27 | import org.scalatest.Matchers
28 | import org.scalatest.OptionValues
29 |
30 | /**
31 | * Test rendering virtual nodes to DOM nodes..
32 | */
33 | class PatchApplicableSpec extends AsyncFlatSpec
34 | with Matchers
35 | with OptionValues {
36 |
37 | import js.DynamicImplicits.truthValue
38 | import backend.dom.DOMBackend
39 | import DOMBackend._
40 | import backend.dom.DOMUtils
41 | import VNode._
42 |
43 | private def cleanBody() = DOMUtils.removeChildren(d.document.body)
44 |
45 | "Applying a patch" should "add an element" in {
46 | val vnode = tag("div")
47 | val body = d.document.body
48 | DOMUtils.removeChildren(body)
49 | body should not equal (null)
50 | val x = DOMBackend.run(InsertPatch(vnode)(body))
51 | x.map { newNode =>
52 | newNode.nodeName shouldEqual "DIV"
53 | body.childNodes.length shouldEqual 1
54 | }
55 | }
56 |
57 | it should "insert a vnode at a specific position" in {
58 | val body = DOMUtils.removeChildren(d.document.body)
59 | val node1 = d.document.createElement("h1")
60 | val node2 = d.document.createElement("h2")
61 | val vnode3 = tag("h3")
62 | val node4 = d.document.createElement("span")
63 | Seq(node1, node2, node4).foreach(body.appendChild(_))
64 | val x = DOMBackend.run(InsertPatch(vnode3, Some(2))(body))
65 | x.map { el =>
66 | body.childNodes(2).nodeName shouldEqual "H3"
67 | }
68 | }
69 |
70 | it should "add a 2 level div tree automatically" in {
71 | val body = d.document.body
72 | DOMUtils.removeChildren(body)
73 | val vnode = tag("div", tag("div"))
74 | val x = DOMBackend.run(InsertPatch(vnode)(body))
75 | x.map { el =>
76 | body.childNodes.length shouldEqual 1
77 | val div1 = body.childNodes(0)
78 | div1.nodeName should be("DIV")
79 | div1.childNodes.length shouldEqual 1
80 | val div2 = div1.childNodes(0)
81 | div2.nodeName should be("DIV")
82 | div2.childNodes.length shouldEqual 0
83 | }
84 | }
85 |
86 | it should "remove an element" in {
87 | val body = d.document.body
88 | DOMUtils.removeChildren(body)
89 | body.appendChild(d.document.createElement("div"))
90 | body.childNodes.length shouldEqual 1
91 | val x = DOMBackend.run(RemovePatch(body.childNodes(0)))
92 | x.map { el =>
93 | el should not be (null)
94 | el.nodeName shouldEqual "DIV"
95 | }
96 | }
97 |
98 | it should "replace an element" in {
99 | val body = DOMUtils.removeChildren(d.document.body)
100 | val vnode = tag("div", tag("div"))
101 | val replacement = tag("p")
102 | val y = for {
103 | newNode <- DOMBackend.run(InsertPatch(vnode)(body))
104 | replaced <- DOMBackend.run(ReplacePatch(replacement)(newNode))
105 | } yield replaced
106 | y.map { el =>
107 | el.nodeName shouldEqual "P"
108 | body.childNodes.length shouldEqual 1
109 | }
110 | }
111 |
112 | it should "reorder children correctly" in {
113 | val body = DOMUtils.removeChildren(d.document.body)
114 | val children = Seq("p", "h1", "h2", "span").map(d.document.createElement(_))
115 | val childrenNodeNames = children.map(_.nodeName)
116 | children.foreach(body.appendChild(_))
117 | (0 until body.childNodes.length).map(body.childNodes(_).nodeName) should contain theSameElementsInOrderAs childrenNodeNames
118 | body.childNodes.length shouldEqual children.length
119 |
120 | // Reorder only.
121 | val instr = ReorderInstruction(moves = Seq((0, 1), (1, 0)))
122 | val x = DOMBackend.run(OrderChildrenPatch(instr)(body)) map { el =>
123 | el.childNodes.length shouldEqual children.length
124 | (0 until el.childNodes.length).map(el.childNodes(_).nodeName) should contain theSameElementsInOrderAs Seq("H1", "P", "H2", "SPAN")
125 | }
126 | x
127 | }
128 |
129 | it should "remove and reorder children correctly" in {
130 | val body = DOMUtils.removeChildren(d.document.body)
131 | val children = Seq("p", "h1", "h2", "span").map(d.document.createElement(_))
132 | val childrenNodeNames = children.map(_.nodeName)
133 | children.foreach(body.appendChild(_))
134 |
135 | // Reorder and remove.
136 | val instr2 = ReorderInstruction(moves = Seq((2, 1), (1, 2)), removes = Seq(0))
137 | val y = DOMBackend.run(OrderChildrenPatch(instr2)(body)) map { el =>
138 | el.childNodes.length shouldEqual (children.length - 1)
139 | (0 until el.childNodes.length).map(el.childNodes(_).nodeName) should contain theSameElementsInOrderAs Seq("H1", "SPAN", "H2")
140 | }
141 | y
142 | }
143 |
144 | it should "add text from a text patch" in {
145 | val body = cleanBody()
146 | for {
147 | // we run it this way because we don't want to replace BODY, need a DIV
148 | // and then run the TextPatch
149 | x <- DOMBackend.run(InsertPatch(tag("div"))(body))
150 | y <- DOMBackend.run(TextPatch("my text")(x))
151 | } yield {
152 | val z: UndefOr[d.Text] = y.asInstanceOf[d.Text]
153 | assert(z.isDefined)
154 | z.get.data shouldEqual "my text"
155 | }
156 | }
157 |
158 | }
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/js/src/test/scala/im/vdom/RenderSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import VNode._
20 |
21 | import _root_.org.scalajs.{dom => d}
22 | import concurrent._
23 | import scalajs.js
24 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
25 |
26 | import org.scalatest.AsyncFlatSpec
27 | import org.scalatest.Matchers
28 | import org.scalatest.OptionValues
29 |
30 | /**
31 | * Test rendering virtual nodes to DOM nodes..
32 | */
33 | class RenderSpec extends AsyncFlatSpec
34 | with Matchers
35 | with OptionValues {
36 |
37 | import js.DynamicImplicits.truthValue
38 | import backend.dom.DOMBackend
39 | import DOMBackend._
40 |
41 | "backend rendering" should "create a simple div" in {
42 | val vnode = tag("div")
43 | val elf = DOMBackend.run(render(vnode))
44 | elf.map { e =>
45 | e.nodeName shouldEqual "DIV"
46 | }
47 | }
48 |
49 | it should "render a short, simple tree" in {
50 | val vnode = tag("div", tag("p", text("some text")))
51 | val elf = DOMBackend.run(render(vnode))
52 | elf.map { e =>
53 | // only the
should count
54 | e.childNodes.length shouldEqual 1
55 | }
56 | }
57 |
58 | it should "create a simple text node" in {
59 | val vnode = text("test")
60 | val elf = DOMBackend.run(render(vnode))
61 | elf.map { el =>
62 | el.textContent shouldEqual "test"
63 | }
64 | }
65 |
66 | it should "allow ThunkNode call its function when its asked to" in {
67 | val vnode = ThunkNode(() => {
68 | tag("div")
69 | })
70 | val elf = DOMBackend.run(render(vnode))
71 | elf.map { el =>
72 | el.nodeName shouldEqual "DIV"
73 | }
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/js/src/test/scala/im/vdom/backend/dom/DOMUtilsSpec.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at9
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 | package dom
19 |
20 | import scala.language._
21 | import org.scalatest._
22 | import scala.concurrent.Future
23 | import scala.concurrent.ExecutionContext.Implicits.global
24 | import scala.concurrent.duration._
25 | import java.util.concurrent.TimeUnit
26 | import java.util.concurrent.atomic.AtomicInteger
27 | import _root_.org.scalajs.dom.{ document => d, Element => El }
28 |
29 | /**
30 | * DOMUtils test.
31 | */
32 | class DOMUtilsSpec extends FlatSpec with Matchers with OptionValues {
33 |
34 | import Utils._
35 | import DOMUtils._
36 |
37 | "canReuseMarkup" should "return true when it should return true" in {
38 | val n = d.createElement("div")
39 | val markup = """
blah is cool
"""
40 | val markupWith = """
blah is cool
"""
41 | n.innerHTML = markupWith
42 | canReuseMarkup(markup, n.firstChild.asInstanceOf[El]) should equal(true)
43 | }
44 |
45 | "removeChildren" should "remove all children" in {
46 | val n = d.createElement("div")
47 | n.innerHTML = """
hello
world
"""
48 | n.childNodes.length should be >(0)
49 | removeChildren(n)
50 | n.childNodes.length should be (0)
51 |
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/jvm/src/main/scala/im/vdom/ScalaXml.scala:
--------------------------------------------------------------------------------
1 | package im.vdom
2 |
3 | import scala.xml.{Text, UnprefixedAttribute, Node}
4 |
5 | object ScalaXml {
6 | def nodeToVNode(xml:Node):VNode = {
7 | val attrs = xml.attributes.collect { case UnprefixedAttribute(k, Text(v), _) =>
8 | KeyValue(AttrKey(k), Some(v))
9 | }.toSeq
10 | val children = xml.nonEmptyChildren
11 | .filter(n => n.label != "#PCDATA" || !n.text.trim.isEmpty)
12 | .map(nodeToVNode)
13 |
14 | if(xml.label == "#PCDATA") VNode.text(xml.text)
15 | else VNode.tag(xml.label, attrs, children:_*)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/jvm/src/test/scala/im/vdom/PatchSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import org.scalatest._
20 |
21 | /**
22 | * Patch object testing. Patches are just simple domain classes
23 | * so there is not much to test.
24 | */
25 | class PatchSpec extends FlatSpec
26 | with Matchers
27 | with OptionValues {
28 |
29 | "Patch" should "have only one EmptyPatch" in {
30 | assertResult(EmptyPatch)(EmptyPatch)
31 | }
32 |
33 | it should "not need arguments for RemovePatch" in {
34 | assert(RemovePatch != null)
35 | }
36 |
37 | "andThen" should "sequence left then right" in {
38 | val lhs = EmptyPatch
39 | val rhs = RemovePatch
40 | val andThen = lhs andThen rhs
41 | assertResult(rhs)(andThen.right)
42 | assertResult(lhs)(andThen.left)
43 | }
44 | }
--------------------------------------------------------------------------------
/jvm/src/test/scala/im/vdom/ScalaXmlSpec.scala:
--------------------------------------------------------------------------------
1 | package im.vdom
2 |
3 | import org.scalatest.WordSpec
4 | import ScalaXml._
5 | import VNode._
6 |
7 | class ScalaXmlSpec extends WordSpec {
8 | "ScalaXml.nodeToVNode()" should {
9 | "convert a trivial empty Node into a VNode.tag" in {
10 | val html =
11 | val expected = tag("br")
12 |
13 | assertResult(expected)(nodeToVNode(html))
14 | }
15 |
16 | "convert an empty Node with attributes" in {
17 | val html =
18 | val expected = tag("div", Seq(KeyValue(AttrKey("id"), Some("my-id")), KeyValue(AttrKey("class"), Some("my-class"))))
19 |
20 | assertResult(expected)(nodeToVNode(html))
21 | }
22 |
23 | "convert a Node with text" in {
24 | val html = This is text
25 | val expected = tag("span", text("This is text"))
26 |
27 | assertResult(expected)(nodeToVNode(html))
28 | }
29 |
30 | "convert a Node with children and attributes" in {
31 | val html =
32 |
38 |
39 | val expected =
40 | tag("form", Seq(KeyValue(AttrKey("method"), Some("post"))),
41 | tag("div",
42 | tag("input", Seq(KeyValue(AttrKey("type"), Some("text")), KeyValue(AttrKey("id"), Some("chat-in")), KeyValue(AttrKey("name"), Some("in")))),
43 | tag("input", Seq(KeyValue(AttrKey("type"), Some("submit")), KeyValue(AttrKey("value"), Some("Submit"))))
44 | )
45 | )
46 |
47 | assertResult(expected)(nodeToVNode(html))
48 | }
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/jvm/src/test/scala/im/vdom/backend/RenderMarkupSpec.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 |
19 | import scala.concurrent.Await
20 | import scala.concurrent.ExecutionContext.Implicits.global
21 | import scala.concurrent.duration.DurationInt
22 | import scala.language._
23 |
24 | import org.scalatest.FlatSpec
25 | import org.scalatest.Matchers
26 | import org.scalatest.OptionValues
27 |
28 | class RenderMarkupSpec extends FlatSpec with Matchers
29 | with OptionValues {
30 |
31 | import HTML5Attributes._
32 | import Styles._
33 | import VNode.tag
34 | import VNode.text
35 |
36 | "Backend" should "render markup" in {
37 | // val vdom = tag("div", Seq(cls := "highlighted", data("hah") := "blah", heightA := None,
38 | // width := 30, border := "auto", textAlign := "left"),
39 | // tag("p", text("blah")), tag("br"))
40 | // val action = b.render(vdom)
41 | // val output = b.run(action)
42 | // val markup = Await.result(output, 1 seconds)
43 | // println(s"markup: $markup")
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/project/Dependencies.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | object Dependencies {
4 |
5 | val allResolvers = Seq(
6 | Resolver.url("file://" + Path.userHome.absolutePath + "/.ivy/local"),
7 | "sonatype" at "https://oss.sonatype.org/content/repositories/releases")
8 |
9 | //val frontendDeps = Seq("org.scala-js" %%% "scalajs-dom" % "0.8.1")
10 |
11 | val scalatest = "org.scalatest" %% "scalatest" % "3.0.0-M15"
12 |
13 | //val scalaJsDependencies = Seq("org.scala-js" %%% "scalajs-dom" % "0.8.1")
14 |
15 | val testDeps = Seq(scalatest % Test)
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.6")
2 |
3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.4.0")
4 |
5 |
--------------------------------------------------------------------------------
/reactive/index-dev-reactive.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 | scalajs-vdom test page
21 |
22 |
35 |
36 |
37 |
38 |
scala-vdom reactive test page
39 |
40 |
41 |
42 |
43 | test1
44 |
45 |
46 |
47 |
48 |
49 |
51 |
53 |
55 |
56 |
--------------------------------------------------------------------------------
/reactive/src/main/scala/im/vdom/reactive/Test.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package reactive
19 |
20 | import scala.scalajs.js.JSApp
21 | import scala.concurrent.duration._
22 | import scala.concurrent.Await
23 | import scala.scalajs.js
24 | import scala.scalajs.js.timers._
25 | import scala.concurrent.{ Future, ExecutionContext }
26 | import scala.language._
27 | import scalajs.js
28 | import js.UndefOr
29 | import _root_.org.scalajs.{dom => d}
30 | import d._
31 | import monifu._
32 | import monifu.concurrent.Implicits.globalScheduler
33 | import monifu.reactive._
34 | import channels._
35 | import subjects._
36 | import monifu.concurrent._
37 | import monifu.concurrent.cancelables._
38 | import monifu.reactive.Ack.Continue
39 |
40 | import events.Handler
41 | import Handler.Implicits._
42 | import backend.dom.DOMBackend._
43 | import VNode._
44 | import HTML5Attributes._
45 | import Styles._
46 | import UIEvents._
47 | import vdom.reactive.Implicits._
48 |
49 |
50 | /**
51 | * Derived from: https://github.com/staltz/mvi-example.
52 | */
53 | object Test extends JSApp {
54 |
55 | // Get an element by name.
56 | def elementById[A <: js.Any](id: String): A =
57 | document.getElementById(id).asInstanceOf[A]
58 |
59 | // Convert a d.Element event to an Observable.
60 | def elementEventObservable(el: d.Element, eventType: String): Observable[d.Event] = {
61 | val ps = PublishSubject[d.Event]()
62 | el.addEventListener(eventType, (e: d.Event) => ps.onNext(e))
63 | ps
64 | }
65 |
66 | // Observe stream of VNodes representing view updates.
67 | // We do not use an auto-render system, but this is the equivalent "rendering system".
68 | def renderer(vtree: Observable[VNode], container: d.Element): BooleanCancelable = {
69 | container.innerHTML = ""
70 | var root: Future[d.Node] = Future(document.createElement("div"))
71 | root.foreach(container.appendChild(_))
72 | vtree.
73 | startWith(empty).
74 | slidingWindow(2).
75 | subscribe(new monifu.reactive.Observer[Seq[VNode]] {
76 | def onNext(els: Seq[VNode]): Future[Ack] = {
77 | val oldTree = els(0)
78 | val newTree = els(1)
79 | val patch = diff(oldTree, newTree)
80 | root = root.flatMap(el => run(patch(el)))
81 | Ack.Continue
82 | }
83 | def onError(ex: Throwable): Unit = println(s"Error rendering: $ex")
84 | def onComplete() = ()
85 | })
86 | }
87 |
88 | /**
89 | * Convert view click and low-level events to a semantic, user "intent."
90 | */
91 | def intent(viewInfo: Observable[d.Event]) = viewInfo.map(event => "updateCounter")
92 |
93 | /**
94 | * Convert user "intent" events (like updateCounter) to model state. Expose
95 | * state as an observable. State is kept as 2 fields here for simplicity.
96 | */
97 | var modelState: Int = 0
98 | val modelList = scala.collection.mutable.ListBuffer[String]()
99 | type ModelType = (Int, Seq[String])
100 | def model(intentInfo: Observable[String]) = {
101 | intentInfo.map { eventT =>
102 | modelList += s"Item - $modelState"
103 | modelState += 1
104 | (modelState, modelList)
105 | }
106 | }
107 |
108 | /**
109 | * Convert model changes to view changes. The VNode observable changes
110 | * can be used by a rendering engine to render into the DOM. The user
111 | * then interacts with that view.
112 | */
113 | def view(modelInfo: Observable[ModelType]) = {
114 | val clicks = PublishSubject[d.Event]()
115 | val vtreeObs = modelInfo.map {
116 | case (count, list) =>
117 |
118 | val listOfThings = list.zipWithIndex.map {
119 | case (item, i) =>
120 | tag("li", Some("key" + i.toString), None, Seq(), text(item)) // full form of tag()
121 | }
122 |
123 | tag("div", Some("key1"), None, Seq(),
124 | text(s"count: $count "),
125 | tag("button", Some("buttonkey"), None, Seq(click ~~> Handler{(e: d.Event) => {
126 | clicks.onNext(e)
127 | true
128 | }}), text("Click Me!")),
129 | tag("p", text("Hello World!")),
130 | tag("ul", Some("ulkey"), None, Seq(),
131 | listOfThings: _*))
132 | }
133 | (vtreeObs, clicks)
134 | }
135 |
136 | // val v1 = tag("div", None, None, Seq())
137 | // val v2 = tag("div", None, None, Seq())
138 | // println("v1==v2?: " + (v1 == v2))
139 | // println("v1 close to v2?: " + (v1 closeTo v2))
140 | // println("v1 eq v2?: " + (v1 eq v2))
141 |
142 | def main(): Unit = {
143 | // Dance around circular dependency issues
144 | val placeholder = PublishSubject[ModelType]()
145 | val channel = SubjectChannel(placeholder, OverflowStrategy.Unbounded)
146 |
147 | val (vtree, clicks) = view(placeholder)
148 | val Intent = intent(clicks)
149 | val Model = model(Intent)
150 | renderer(vtree, elementById[d.Element]("test1"))
151 |
152 | // Brake circular dependency and start the "cycle" with the initial state value.
153 | val cancelable = Model.startWith((modelState, Seq())).subscribe(new Observer[ModelType] {
154 | def onNext(p: (Int, Seq[String])) = {
155 | channel.pushNext(p)
156 | Ack.Continue
157 | }
158 | def onError(ex: Throwable): Unit = {
159 | println(s"Error in push: $ex")
160 | channel.pushError(ex)
161 | }
162 | def onComplete() = channel.pushComplete()
163 | })
164 | }
165 | }
--------------------------------------------------------------------------------
/reactive/src/main/scala/im/vdom/reactive/package.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package reactive
19 |
20 | import monifu.reactive._
21 |
22 | object Implicits {
23 |
24 | import scala.concurrent.duration._
25 | import scala.concurrent.{ Future, ExecutionContext }
26 | import scala.language._
27 |
28 | import monifu.reactive._
29 | import monifu.concurrent._
30 | import scala.collection.mutable.ArrayBuffer
31 |
32 | implicit class EnhancedObservable[T](source: Observable[T]) {
33 | def slidingWindow(n: Int): Observable[Seq[T]] = source.whileBusyBuffer(OverflowStrategy.Unbounded).buffer(n, 1)
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/Action.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.concurrent.ExecutionContext
20 | import scala.concurrent.Future
21 | import scala.util.Try
22 | import collection.mutable.ArrayBuffer
23 | import backend.{ Backend, RendererComponent, PatchesComponent }
24 |
25 | /**
26 | * A debuggable object means you can get debuggable information.
27 | */
28 | trait Debuggable {
29 | def getDebugInfo: DebugInfo
30 | }
31 |
32 | /**
33 | * Debug information is just a string for the moment.
34 | */
35 | case class DebugInfo(name: String, msg: Option[String] = None, children: Seq[IOAction[_]] = Seq())
36 |
37 | /**
38 | * Basic action object using the IO monad pattern. The side effect,
39 | * for example changing a DOM element, occurs inside the monad. You
40 | * can represent failure with IOAction so avoid having `R` be another
41 | * wrapper that represents failure.
42 | *
43 | * TODO: Add ExceutionContext as an implicit to handle server side threading.
44 | */
45 | sealed trait IOAction[+R] extends Debuggable {
46 | def map[R2](f: R => R2)(implicit executor: ExecutionContext): IOAction[R2] =
47 | flatMap[R2](r => SuccessAction[R2](f(r)))
48 |
49 | def flatMap[R2](f: R => IOAction[R2])(implicit executor: ExecutionContext): IOAction[R2] =
50 | FlatMapAction[R2, R](this, f, executor)
51 |
52 | /**
53 | * Sequence this before next.
54 | */
55 | def andThen[R2](next: IOAction[R2]): IOAction[R2] = AndThenAction[R2](this, next)
56 |
57 | /**
58 | * Filter the result with p
59 | */
60 | final def filter(p: R => Boolean)(implicit executor: ExecutionContext): IOAction[R] = withFilter(p)
61 |
62 | /**
63 | * Helper for for-comprehensions.
64 | */
65 | def withFilter(p: R => Boolean)(implicit executor: ExecutionContext): IOAction[R] =
66 | flatMap(v => if (p(v)) SuccessAction(v) else throw new NoSuchElementException("IOAction.withFilter failed"))
67 |
68 | /**
69 | * Run another action after this one that processes a potential failure. The cleanup
70 | * action is computed from the result of this action. The cleanup argument is None
71 | * if this action succeeds or a Some containing this action's failure if it failed.
72 | * The default `keepFailure=true` says that this actions exception should be kept even if the cleanup's
73 | * action throws an exception. If false, the exception from the cleanup action is kept.
74 | * @param keepFailure if the cleanup action fails, keep that failure info if false. Otherwise, keep the original failure if true.
75 | */
76 | def cleanUp(f: Option[Throwable] => IOAction[_], keepFailure: Boolean = true)(implicit executor: ExecutionContext): IOAction[R] =
77 | CleanUpAction[R](this, f, keepFailure, executor)
78 |
79 | /**
80 | * Run an action after this action, regardless if this action succeeds or fails. If this
81 | * action fails, always propagate that failure. If this action succeeds and
82 | * the second action fails, return the second action's failure. This is much like
83 | * a 'try { ... } finally { ... }` clause.
84 | */
85 | def andFinally(a: IOAction[_]): IOAction[R] = cleanUp(_ => a)(Action.sameThreadExecutionContext)
86 |
87 | /**
88 | * Return an action that contains the throwable that this action failed with as its result.
89 | * If this action succeeded, the resulting action fails with a NoSuchElementException.
90 | * This is a projection of the action.
91 | */
92 | def failed: IOAction[Throwable] = FailedAction(this)
93 |
94 | /**
95 | * Convert to a Try. Use cleanup and andFinally before using asTry.
96 | */
97 | def asTry: IOAction[Try[R]] = AsTryAction[R](this)
98 |
99 | /**
100 | * Run another action after this action, if it completed successfully, and return the result
101 | * of both actions. If either of the two actions fails, the resulting action also fails.
102 | */
103 | def zip[R2](a: IOAction[R2]): IOAction[(R, R2)] =
104 | SequenceAction[Any, ArrayBuffer[Any]](Vector(this, a)).map { r =>
105 | (r(0).asInstanceOf[R], r(1).asInstanceOf[R2])
106 | }(Action.sameThreadExecutionContext)
107 |
108 | }
109 |
110 | /**
111 | * Helper functions.
112 | */
113 | object Action {
114 |
115 | /** Convert a `Future` to a [[IOAction]]. */
116 | def from[R](f: Future[R]): IOAction[R] = FutureAction[R](f)
117 |
118 | /** Lift a constant value to a [[IOAction]]. This immediately evaluates the argument. Use lift to delay evaluation. */
119 | def successful[R](v: R): IOAction[R] = SuccessAction[R](v)
120 |
121 | /** Create a [[IOAction]] that always fails. */
122 | def failed(t: Throwable): IOAction[Nothing] = FailureAction(t)
123 |
124 | /** Compose actions from the varargs actions, run in sequence using `andThen`, and return Unit. */
125 | def seq(actions: IOAction[_]*): IOAction[Unit] =
126 | (actions :+ SuccessAction(())).reduceLeft(_ andThen _).asInstanceOf[IOAction[Unit]]
127 |
128 | /**
129 | * Create a IOAction that runs some other actions in sequence and combines their results
130 | * with the given function.
131 | */
132 | def fold[T](actions: Seq[IOAction[T]], zero: T)(f: (T, T) => T)(implicit ec: ExecutionContext): IOAction[T] =
133 | actions.foldLeft[IOAction[T]](Action.successful(zero)) { (za, va) => za.flatMap(z => va.map(v => f(z, v))) }
134 |
135 | /**
136 | * An ExecutionContext used internally for executing plumbing operations during IOAction
137 | * composition.
138 | */
139 | private[vdom] object sameThreadExecutionContext extends ExecutionContext {
140 | override def execute(runnable: Runnable): Unit = runnable.run()
141 | override def reportFailure(t: Throwable): Unit = throw t
142 | }
143 |
144 | /**
145 | * Lift a function that takes a context.
146 | */
147 | def withContext[R, B <: Backend](f: B#Context => R) = ContextualAction(f)
148 |
149 | /**
150 | * Lift a by-name value into an IOAction. This delays the computation until it is needed.
151 | */
152 | def lift[R, B <: Backend](f: => R) = ContextualAction(f)
153 | }
154 |
155 | /**
156 | * Context used when running an action.
157 | */
158 | trait ActionContext
159 |
160 | /**
161 | * When applied, performs the patch action. Return the result
162 | * of the action. This action has access to the Backend's
163 | * context while executing.
164 | *
165 | * @tparam R output after applying patch
166 | * @tparam B the `Backend`
167 | *
168 | */
169 | trait ContextualAction[+R, -B <: Backend] extends IOAction[R] { self =>
170 | /**
171 | * Run this action with a context.
172 | */
173 | def run(ctx: B#Context): R
174 | }
175 |
176 | /**
177 | * Create some convenience functions to create `IOAction`s. Generally,
178 | * a layer in between the programmer and the Backend hides the
179 | * creation of actions.
180 | */
181 | object ContextualAction {
182 | /**
183 | * Create a patch action
184 | */
185 | def apply[R, B <: Backend](f: B#Context => R) = new ContextualAction[R, B] {
186 | def run(ctx: B#Context) = f(ctx)
187 | def getDebugInfo = DebugInfo("contextual patch")
188 | }
189 |
190 | /**
191 | * Create a patch action from a simple action. It ignores the context.
192 | */
193 | def apply[R, B <: Backend](f: => R) = new ContextualAction[R, B] {
194 | def run(ctx: B#Context) = f
195 | def getDebugInfo = DebugInfo("contextual patch")
196 | }
197 | }
198 |
199 | /**
200 | * Perform a flatMap using f.
201 | */
202 | case class FlatMapAction[+R, P](base: IOAction[P], f: P => IOAction[R], executor: ExecutionContext) extends IOAction[R] {
203 | def getDebugInfo = DebugInfo("flatMap", children = Seq(base))
204 | }
205 |
206 | /**
207 | * Performs left then right
208 | */
209 | case class AndThenAction[+R](left: IOAction[_], right: IOAction[R]) extends IOAction[R] {
210 | def getDebugInfo = DebugInfo("andThen", children = Seq(left, right))
211 | }
212 |
213 | /**
214 | * Clean up with f after a failure.
215 | */
216 | case class CleanUpAction[+R](base: IOAction[R], f: Option[Throwable] => IOAction[_],
217 | keepFailure: Boolean, executor: ExecutionContext) extends IOAction[R] {
218 | def getDebugInfo = DebugInfo("cleanUp", children = Seq(base))
219 | }
220 |
221 | /**
222 | * An action that returns a constant value.
223 | */
224 | case class SuccessAction[+R](value: R) extends ContextualAction[R, Backend] {
225 | def run(ctx: Backend#Context): R = value
226 | def getDebugInfo = DebugInfo("success", Some(value.toString))
227 | }
228 |
229 | /**
230 | * An action that fails with the given throwable when run.
231 | */
232 | case class FailureAction(t: Throwable) extends ContextualAction[Nothing, Backend] {
233 | def run(ctx: Backend#Context): Nothing = throw t
234 | def getDebugInfo = DebugInfo("failure", Some(t.toString))
235 | }
236 |
237 | /**
238 | * An asynchronous DBIOAction that returns the result of a Future.
239 | */
240 | case class FutureAction[+R](f: Future[R]) extends IOAction[R] {
241 | def getDebugInfo = DebugInfo("future ", Some(f.toString))
242 | }
243 |
244 | /**
245 | * Represents a failed action
246 | */
247 | case class FailedAction(a: IOAction[_]) extends IOAction[Throwable] {
248 | def getDebugInfo = DebugInfo("failed", children = Seq(a))
249 | }
250 |
251 | /**
252 | * Represents conversion to a Try
253 | */
254 | case class AsTryAction[+R](a: IOAction[R]) extends IOAction[Try[R]] {
255 | def getDebugInfo = DebugInfo("asTry", children = Seq(a))
256 | }
257 |
258 | import collection.generic._
259 |
260 | /** A DBIOAction that represents a `sequence` operation for sequencing in the DBIOAction monad. */
261 | case class SequenceAction[R, +R2](as: IndexedSeq[IOAction[R]])(implicit val cbf: CanBuild[R, R2]) extends IOAction[R2] {
262 | def getDebugInfo = DebugInfo("sequence", children = as)
263 | }
264 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/Attributes.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.language._
20 |
21 | /**
22 | * Various constants related to DOM.
23 | */
24 | trait Constants {
25 |
26 | /**
27 | * Namespace URIs
28 | */
29 | object NS {
30 | val HTML5 = "http://www.w3.org/1999/xhtml"
31 | val SVG = "http://www.w3.org/2000/svg"
32 | val XLINK = "http://www.w3.org/1999/xlink"
33 | val XML = "http://www.w3.org/XML/1998/namespace"
34 | }
35 | }
36 |
37 | object Constants extends Constants
38 |
39 | trait HTML5Attributes {
40 |
41 | implicit class StandardStringToKey(name: String) extends RichString(name)
42 |
43 | val accept = "accept".attr
44 | val acceptCharSet = "accept-charset".attr
45 | val accessKey = "accessKey".attr
46 | val action = "action".attr
47 | val allowFullScreen = "allowFullScreen".attr
48 | val allowTransparency = "allowTransparency".attr
49 | val alt = "alt".attr
50 | val checked = "checked".attr
51 | val `class` = "class".attr
52 | val cls = `class`
53 | var contentEditable = "contenteditable".attr
54 | val contextMenu = "contextmenu".attr
55 | val disabled = "disabled".attr
56 | val dir = "dir".attr
57 | val hidden = "hidden".attr
58 | val href = "href".attr
59 | val httpEquiv = "http-equiv".attr
60 | val htmlFor = "for".attr
61 | val innerHTML = "innerHTML".attr
62 | val id = "id".attr
63 | val lang = "lang".attr
64 | val scope = "scope".attr
65 | val scoped = "scoped".attr
66 | val scrolling = "scrolling".attr
67 | val seamless = "seamless".attr
68 | val selected = "selected".attr
69 | val shape = "shape".attr
70 | val size = "size".attr
71 | val sizes = "sizes".attr
72 | val span = "span".attr
73 | val spellCheck = "spellCheck".attr
74 | val src = "src".attr
75 | val srcDoc = "srcDoc".attr
76 | val style = "style".attr
77 | val tabIndex = "tabindex".attr
78 | val title = "title".attr
79 | val value = "value".attr
80 | val widthA = "width".attr // conflicts with style
81 | val heightA = "height".attr // conflicts with style
82 |
83 | // Don't forget these...
84 | /**
85 | * async autoComplete autoFocus autoPlay
86 | *
87 | * cellPadding cellSpacing charSet checked
88 | * classID className colSpan cols content contentEditable contextMenu controls
89 | * coords crossOrigin
90 | * data dateTime defer dir disabled download draggable
91 | * encType
92 | * form formAction formEncType formMethod formNoValidate formTarget frameBorder
93 | *
94 | * headers height hidden high href hrefLang htmlFor httpEquiv
95 | * icon id
96 | * label lang list loop low
97 | * manifest marginHeight marginWidth max maxLength media mediaGroup
98 | * method min multiple muted
99 | *
100 | * name noValidate
101 | * open optimum
102 | * pattern placeholder poster preload
103 | * radioGroup readOnly rel required role rowSpan rows sandbox
104 | *
105 | * srcSet start step style
106 | * tabIndex target title type
107 | * useMap
108 | * value
109 | * width wmode
110 | */
111 | }
112 |
113 | trait CustomHTML5Attributes {
114 | implicit class CustomStringToKey(name: String) extends RichString(name)
115 | def data(name: String) = AttrKey("data-" + name)
116 | def aria(name: String) = AttrKey("aria-" + name)
117 | }
118 |
119 | object CustomHTML5Attributes extends CustomHTML5Attributes
120 |
121 | /**
122 | * Standard HTML5 attributes. Usually, you'll import this.
123 | */
124 | object HTML5Attributes extends HTML5Attributes with CustomHTML5Attributes
125 |
126 | /** Attributes for SVG. */
127 | trait SVGAttributes {
128 | import Constants.NS._
129 | implicit class StandardStringToKey(name: String) extends RichString(name)
130 |
131 | val clipPath = AttrKey(name = "clip-art")
132 | val cx = "cx".attr
133 | val cy = "cx".attr
134 | val d = "d".attr
135 | val dx = "dx".attr
136 | val dy = "dy".attr
137 | val fill = "fill".attr
138 | val fillOpacity = "fill-opacity".attr
139 | val fontFamily = "font-family".attr
140 | val fontSize = "font-size".attr
141 | val fx = "fx".attr
142 | val fy = "fy".attr
143 | val gradientTransform = "gradientTransform".attr
144 | val gradientUnits = "gradientUnits".attr
145 | val markerEnd = "marker-end".attr
146 | val markerMid = "marker-mid".attr
147 | val markerStart = "marker-start".attr
148 | val offset = "offset".attr
149 | val opacity = "opacity".attr
150 | val patternContentUnits = "patternContentUnits".attr
151 | val patternUnits = "patternUnits".attr
152 | val points = "points".attr
153 | val preserveAspectRatio = "preserveAspectRatio".attr
154 | val r = "r".attr
155 | val rx = "rx".attr
156 | val ry = "ry".attr
157 | val spreadMethod = "spreadMethod".attr
158 | val stopColor = "stop-color".attr
159 | val stopOpacity = "stroke-opacity".attr
160 | val stroke = "stroke".attr
161 | val strokeDasharray = "stroke-dasharray".attr
162 | val strokeLinecap = "stoke-linecap".attr
163 | val strokeOpacity = "stroke-opacity".attr
164 | val strokeWidth = "stroke-width".attr
165 | val textAnchor = "text-anchor".attr
166 | val transform = "transform".attr
167 | val version = "version".attr
168 | val viewBox = "view-box".attr
169 | val x1 = "x1".attr
170 | val x2 = "x2".attr
171 | val x = "x".attr
172 | val xlinkActuate = AttrKey("xlink:actuate", Some(XLINK))
173 | val xlinkArcrole = AttrKey("xlink:arcrole", Some(XLINK))
174 | val xlinkHref = AttrKey("xlink:href", Some(XLINK))
175 | val xlinkRole = AttrKey("xlink:role", Some(XLINK))
176 | val xlinkShow = AttrKey("xlink:show", Some(XLINK))
177 | val xlinkTitle = AttrKey("xlink:title", Some(XLINK))
178 | val xlinkType = AttrKey("xlink:type", Some(XLINK))
179 | val xmlBase = AttrKey("xml:base", Some(XML))
180 | val xmlLang = AttrKey("xml:lang", Some(XML))
181 | val xmlSpace = AttrKey("xml:space", Some(XML))
182 | val y1 = "y1".attr
183 | val y2 = "y2".attr
184 | val y = "y".attr
185 | }
186 |
187 | object SVGAttributes extends SVGAttributes
188 |
189 | /**
190 | * Style attributes. These attributes are set on the style property
191 | * of an element. Some of them duplicate attributes found on other
192 | * elements e.g. height and width.
193 | */
194 | trait Styles {
195 | implicit class StyleToKey(name: String) extends RichString(name)
196 | val border = "border".style
197 | val fill = "fill".style
198 | val height = "height".style
199 | val heightS = height
200 | val lineHeight = "lineHeight".style
201 | val opacity = "opacity".style
202 | val order = "order".style
203 | val stroke = "stroke".style
204 | val tabSize = "tabSize".style
205 | val textAlign = "textAlign".style
206 | val width = "width".style
207 | val widthS = width
208 | val zIndex = "zIndex".style
209 | }
210 |
211 | object Styles extends Styles
212 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/Diff.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | /**
20 | * It's not clear why I can't just build this into the VNode trait
21 | * and split this out to subclasses versus having this
22 | * function stuck out here in the middle of nowhere.
23 | */
24 | private[vdom] trait Diff {
25 |
26 | import VNodeUtils._
27 |
28 | /**
29 | * Diff two VNode's and return a patch that makes original look like target.
30 | * This function determines if the original and target are so different
31 | * that it has to be replaced by a new node (e.g. insert, remove or replace)
32 | * versus an update. If they are similar, they are diffed against
33 | * each other.
34 | *
35 | * If the original vnode is empty, an InserttPatch is created.
36 | *
37 | * This is the primary function to call to diff two unknown virtual nodes.
38 | *
39 | * @param original the current VNode description
40 | * @param target the target VNode description
41 | * @return a Patch that would make original look like target
42 | */
43 | def diff(original: VNode, target: VNode): Patch = {
44 | doDiff(original, target, Nil)
45 | }
46 |
47 | def doDiff(original: VNode, target: VNode, path: Seq[Int] = Nil): Patch = {
48 | if (original == target) EmptyPatch
49 | else if (original == VNode.empty) ReplacePatch(target).applyTo(path)
50 | else if (target == VNode.empty) RemovePatch.applyTo(path)
51 | else (original, target) match {
52 | case (o: VirtualText, t: VirtualText) => o.diff(t, path)
53 | case (o: VirtualElementNode, t: VirtualElementNode) => o.diff(t, path)
54 | case (o: ThunkNode, t: ThunkNode) => o.diff(t, path)
55 | case (o: ThunkNode, _) => doDiff(o.f(), target, path)
56 | case (_, t) => ReplacePatch(target).applyTo(path)
57 | }
58 | }
59 |
60 | /**
61 | * Totally unoptimized :-). We should sort and see if any
62 | * attributes are the same, leave those in place and only
63 | * patch to remove attributes.
64 | */
65 | def diffProperties(original: Seq[KeyValue[_]], target: Seq[KeyValue[_]]): Patch = {
66 | val deletes = original.diff(target).map { x =>
67 | KeyValuePatch(Seq(x.unset))
68 | }
69 | val adds = target.diff(original).map { x =>
70 | KeyValuePatch(Seq(x))
71 | }
72 | deletes ++ adds
73 | }
74 |
75 | /**
76 | * Returns moves (Seq[(Int,Int)], removed Seq[Int], added Seq[Int], restdiff Seq[Patch]).
77 | * The moves should operate after the removes on the original and adds on the
78 | * target have been factored out.
79 | *
80 | * TODO Redo the entire algorithm.
81 | */
82 | def diffSeq2(original: Seq[VNode], target: Seq[VNode], path: Seq[Int]): (Seq[(Int, Int)], Seq[Int], Seq[Int], Seq[Patch]) = {
83 | // Find removes
84 | println("original:")
85 | original.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
86 | println("target:")
87 | target.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
88 |
89 | // Find nodes that were removed. Indexes valid against original sequence.
90 | // Seq(index,...)
91 | val removedIndexes = findRemoves(original, target)(_ == _)
92 |
93 | // Create tmp original with removes removed
94 | // Seq((vnode, orig index in original), ...)
95 | val origLessRemoves =
96 | original.zipWithIndex.filterNot { case (node, index) => removedIndexes.contains(index) }
97 |
98 | // Find nodes that are pure adds. Indexes are relative to target sequence.
99 | // Return Seq(index),...)
100 | val addedIndexes = findAdds(original, target)(_ == _)
101 |
102 | // Create tmp target sequence with "adds" removed.
103 | // Seq((vnode, orig index in target), ...)
104 | val targetLessAdded =
105 | target.zipWithIndex.filterNot { case (node, index) => addedIndexes.contains(index) }
106 |
107 | println("origLessRemoves:")
108 | origLessRemoves.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
109 | println("targetLessAdded:")
110 | targetLessAdded.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
111 |
112 | // Find moved items taking into account keys where available.
113 | // Operate on original with removes removed and target with adds removed.
114 | // Remove moves that are to and from the same position since that's not really a move.
115 | // Seq((original index, target index), ...)
116 | val moves = origLessRemoves.map { orig =>
117 | targetLessAdded.indexWhere { tgt => orig._1 == tgt._1 }
118 | }.zipWithIndex.filter(_._1 >= 0).filterNot(p => p._1 == p._2)
119 |
120 | println(s"moves (${moves.length}): $moves")
121 | println(s"removed (${removedIndexes.length}): $removedIndexes")
122 | println(s"adds (${addedIndexes.length}): $addedIndexes")
123 | println(s"targetLessAdded ($targetLessAdded.length}):")
124 | targetLessAdded.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
125 |
126 | //
127 | // For the reduced original sequence, it should patch index-wise to the reduce target
128 | // sequence. Now we can run a pure diff on those to find any diffs that reflect
129 | // "close" node definitions. Those that are the same should work out to an empty diff
130 | // automatically.
131 | //
132 | val origLessRemovesWithMoves = collection.mutable.IndexedSeq(origLessRemoves: _*)
133 | moves.foreach {
134 | case (from, to) =>
135 | origLessRemovesWithMoves(to) = origLessRemoves(from)
136 | }
137 | println(s"origLessRemovesWithMoves:")
138 | origLessRemovesWithMoves.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") }
139 |
140 | assert(origLessRemovesWithMoves.size == targetLessAdded.size)
141 |
142 | val restdiff: Seq[Patch] = origLessRemovesWithMoves.zip(targetLessAdded).zipWithIndex.map {
143 | case ((l, r), index) =>
144 | doDiff(l._1, r._1, path :+ index)
145 | }
146 | println(s"child diffs: $restdiff")
147 |
148 | (moves, removedIndexes, addedIndexes, restdiff)
149 | }
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | /**
158 | * Observes keyed VNodes and tries to identify moved nodes.
159 | * The algorithm has almost no other optimizations :-).
160 | *
161 | * Optimizations it does have,rReordering children.
162 | *
163 | * It does not track keyed objects that are on separate levels.
164 | */
165 | def diffSeq(original: Seq[VNode], target: Seq[VNode], path: Seq[Int]): Patch = {
166 | val tsize = target.size
167 | if (target.size == 0) {
168 | // Remove all the original nodes.
169 | original.zipWithIndex.map {
170 | case (value, index) => doDiff(value, VNode.empty, path :+ index)
171 | }
172 | } else {
173 | val (moves, removedIndexes, addedIndexes, restdiff) = diffSeq2(original, target, path)
174 | (OrderChildrenPatch(ReorderInstruction(moves, removedIndexes)) andThen
175 | restdiff andThen
176 | addedIndexes.map { index => InsertPatch(target(index), Some(index)) }).applyTo(path)
177 | }
178 | }
179 |
180 | }
181 |
182 | private[vdom] object Diff extends Diff
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/Keys.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.language._
20 |
21 | /**
22 | * Keys are really builders of `KeyValue` objects.
23 | */
24 | trait KeyPart { self =>
25 | def name: String
26 | def namespace: Option[String] = None
27 | }
28 |
29 | /**
30 | * Attribute that is part of an element.
31 | */
32 | case class AttrKey(val name: String, override val namespace: Option[String] = None) extends KeyPart { self =>
33 | def :=[T](v: Option[T]): KeyValue[T] = KeyValue[T](this, v)
34 | def :=[T](v: T): KeyValue[T] = :=[T](Some(v))
35 | }
36 |
37 | /**
38 | * Property that is part of a style.
39 | */
40 | case class StyleKey(val name: String) extends KeyPart { self =>
41 | def :=[T](v: Option[T]): KeyValue[T] = KeyValue[T](this, v)
42 | def :=[T](v: T): KeyValue[T] = :=[T](Some(v))
43 | }
44 |
45 | /**
46 | * Combination of keys and values. A value of None should indicate
47 | * that something should be unset or removed. What unset or remove
48 | * means is Backend dependent.
49 | *
50 | */
51 | case class KeyValue[T](val key: KeyPart, val value: Option[T]) {
52 | /**
53 | * Convenience function to create an unset KeyValue.
54 | */
55 | def unset = copy(value = None)
56 | }
57 |
58 | /**
59 | * Create KeyParts easily given a string: `"blahkey".attr` creates
60 | * an `AttrKey`.
61 | */
62 | class RichString(val name: String) {
63 | def attr = AttrKey(name)
64 | def style = StyleKey(name)
65 | }
66 |
67 | /**
68 | * The exception type in this system.
69 | */
70 | class VDomException(msg: String, parent: Throwable = null) extends RuntimeException(msg, parent)
71 |
72 | /**
73 | * Add a queue to an object for named IOAction lists. The actions can be
74 | * run by calling `runActions`. Running actions should be considered a side effect.
75 | * Once added, an IOAction cannot be removed from the list. The API clearly
76 | * shows that the object implementing this trait is mutable.
77 | *
78 | */
79 | trait ActionLists[T] {
80 | /**
81 | * Add a cleanup action to run just before an attribute's value is set
82 | * to a new value. The specified actions are run after already registered actions.
83 | */
84 | def addAction(keyName: String, node: T, action: IOAction[_]*): Unit
85 | /**
86 | * Add an action to run after the Node is detached from its document.
87 | * The specified actions are run after the already registered actions.
88 | * Detaching should invoke hierarchical action running if T contains a hierarchy.
89 | * Whether children action lists are run prior to the top nodes action list
90 | * is implementation dependent.
91 | */
92 | def addDetachAction(node: T, action: IOAction[_]*): Unit
93 | /**
94 | * Run the detach actions for el and named. The actions are run automatically by the PatchesComponent
95 | * at the right time of the lifecycle. Reset all action lists. The named lists are run first
96 | * then the node level queue. Cleanup should recurse through the children nodes as well.
97 | */
98 | def runActions(node: T): Unit
99 |
100 | /** Run the cleanup actions for the named queue. */
101 | def runActions(name: String, node: T): Unit
102 | }
103 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/Patch.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import scala.concurrent.{ Promise, ExecutionContext, Future }
20 | import scala.util.{ Try, Success, Failure }
21 | import scala.util.control.NonFatal
22 |
23 | /**
24 | * A Patch holds "difference" information that can be applied
25 | * to another object (the target) to make it look like another
26 | * (the source) object. The "difference" information must
27 | * be interpreted for each backend environment.
28 | *
29 | * You can create patches either from diffing two virtual DOM
30 | * trees or you can just manually create the patches and compose
31 | * them using a sequence or `andThen` essentially creating a
32 | * patch stream or template language of DOM "updates."
33 | *
34 | * You can create your own DSL to create patches if you want.
35 | */
36 | sealed trait Patch {
37 | /**
38 | * Route this path to another part of the sub-tree when its run.
39 | * The first sequence position represents the first level
40 | * of children that the patch is applied to and so on.
41 | * Nil means to act on the node that the patch is applied to.
42 | */
43 | def applyTo(path: Seq[Int] = Nil) = PathPatch(this, path)
44 |
45 | /**
46 | * Route this path to a specific child index when it is run.
47 | * The index is zero-based. An index outside the child
48 | * list range results in an error.
49 | */
50 | def applyTo(path: Int) = PathPatch(this, Seq(path))
51 |
52 | /**
53 | * Sequence a patch before another.
54 | */
55 | def andThen(right: Patch) = AndThenPatch(this, right)
56 | }
57 |
58 | /**
59 | * Do not do anything. This may translate into some type of "dummy" element.
60 | */
61 | case object EmptyPatch extends Patch
62 |
63 | /**
64 | * Apply a patch to a particular tree child. Indexes navigate children.
65 | */
66 | case class PathPatch(patch: Patch, path: Seq[Int] = Nil) extends Patch
67 |
68 | /**
69 | * Remove a node.
70 | */
71 | case object RemovePatch extends Patch
72 |
73 | /**
74 | * Replace a node. Access to the parent will be required.
75 | */
76 | case class ReplacePatch(replacement: VNode) extends Patch
77 |
78 | /** Insert a new child at the specified index, or append if index is not specified. */
79 | case class InsertPatch(vnode: VNode, index: Option[Int] = None) extends Patch
80 |
81 | /** Create a text node. */
82 | case class TextPatch(content: String) extends Patch
83 |
84 | /**
85 | * Apply attributes/properties to an element.
86 | */
87 | case class KeyValuePatch(elActions: Seq[KeyValue[_]]) extends Patch
88 |
89 | /** Manipulate children. */
90 | case class OrderChildrenPatch(i: ReorderInstruction) extends Patch
91 |
92 | /**
93 | * Combine two patches in sequence.
94 | */
95 | case class AndThenPatch(left: Patch, right: Patch) extends Patch
96 |
97 | /**
98 | * Instruction to re-order children. Removes should be processed first
99 | * then the moves. Duplicate indexes in any of these structures could
100 | * produce surprises. Moves should reflect the removes that are
101 | * processed first.
102 | */
103 | case class ReorderInstruction(moves: Seq[(Int, Int)] = Seq(), removes: Seq[Int] = Seq())
104 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/VNode.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import collection.mutable
20 |
21 | import Diff._
22 |
23 | /**
24 | * An object that can be diff'd to produce a patch that takes this object into that.
25 | *
26 | * We pull the type parameter into an abstract type so we do not have to repeat it
27 | * everywhere which qucikly becomes inconvienent. You only diff with objects
28 | * with the same class as yourself. This uses F-bounded polymorphism so that the diff
29 | * function only works on the same object type it is declared in. You can only
30 | * diff on the same type as this.
31 | */
32 | trait Diffable { self =>
33 | type That >: self.type <: Diffable
34 | /**
35 | * Diff this object with another. The other object needs to be the same class.
36 | * The patch returned should reflect `path` and any additional pathing needed.
37 | */
38 | def diff(that: That, path: Seq[Int]): Patch
39 | }
40 |
41 | /** A type that can provide a key. */
42 | trait Keyable {
43 | /** Default key is None */
44 | def key: Option[VNodeKey] = None
45 | }
46 |
47 | /**
48 | * Virtual node can be diffed and keyed. Some virtual dom libraries
49 | * like to add hooks (hook and unhook) to be called when the vnode
50 | * is associated with an actual DOM node. In this library, there are
51 | * no hooks but you can easily compose actions to map into a patch
52 | * action and add your hook and unhook logic through composition.
53 | *
54 | */
55 | trait VNode extends Keyable with Diffable {
56 | def closeToOrEquals(rhs: VNode): Boolean = VNodeUtils.closeToOrEquals(this, rhs)
57 | }
58 |
59 | /**
60 | * Node composed of text content.
61 | */
62 | case class VirtualText(text: String) extends VNode {
63 | type That = VirtualText
64 |
65 | def diff(that: That, path: Seq[Int] = Nil): Patch = {
66 | val r =
67 | if (text == that.text) EmptyPatch
68 | else TextPatch(that.text)
69 | if (path != Nil) r.applyTo(path)
70 | r
71 | }
72 | }
73 |
74 | object VNodeUtils {
75 | /**
76 | * Find removed els. Return indexes of removes. Indexes may not be sorted.
77 | * Uses `closToOrEquals` to use fuzzy node comparison.
78 | */
79 | def findRemoves[T](source: Seq[T], target: Seq[T])(compare: (T, T) => Boolean): Seq[Int] =
80 | // add index to each item, create (index, deleted) flags, filter to keep deletes, return only indexes
81 | source.zipWithIndex.map {
82 | case (vnode, index) =>
83 | (index, kindOfContains(target, vnode)(compare))
84 | }.filterNot(_._2).map(_._1)
85 |
86 | def findRemovesCloseEnough[T] = findRemoves[T](_: Seq[T], _: Seq[T])(closeToOrEquals)
87 |
88 | /**
89 | * Find added els. Return indexes of adds relative to the target sequence. Indexes may not be sorted.
90 | */
91 | def findAdds[T](source: Seq[T], target: Seq[T])(compare: (T, T) => Boolean = closeToOrEquals _): Seq[Int] =
92 | findRemoves(target, source)(compare)
93 |
94 | def findAddsCloseEnough[T] = findAdds[T](_: Seq[T], _: Seq[T])(closeToOrEquals)
95 |
96 | /**
97 | * Like "Sequence.contains` but uses the specified comparison for the test.
98 | */
99 | def kindOfContains[T](seq: Seq[T], vnode: T)(compare: (T, T) => Boolean): Boolean =
100 | seq.find { compare(_, vnode) }.fold(false)(_ => true)
101 |
102 | /**
103 | * Like `Sequence.contains` but uses `closeToOrEqual` for the test.
104 | */
105 | def kindOfContainsCloseEnough[T] = kindOfContains[T](_: Seq[T], _: T)(closeToOrEquals)
106 |
107 | /**
108 | * Determine if two nodes are equal using standard equals, or for VirtualElementNodes, close to each other
109 | * using `VirtualElementNode.closeTo`.
110 | */
111 | def closeToOrEquals[T](lhs: T, rhs: T): Boolean = {
112 | (lhs, rhs) match {
113 | case (o: VirtualElementNode, t: VirtualElementNode) => t.closeTo(o)
114 | case p@(_, _) => p._1 == p._2
115 | }
116 | }
117 |
118 | }
119 |
120 | /**
121 | * Virtual node representing an element. Various hooks and hacks help capture
122 | * programmer intent while still allowing a DOM node to be properly configured.
123 | *
124 | * @param tag the element tag
125 | * @param properties key-value pairs. Use "attributes" to enforce using get/set-Attribute otherwise
126 | * property access is used.
127 | * @param children list of children vnodes
128 | * @param key a value used to minimize DOM element node creation. Must be unique among siblings.
129 | */
130 | case class VirtualElementNode(val tag: String,
131 | val attributes: Seq[KeyValue[_]] = Seq(),
132 | val children: Seq[VNode] = Seq(),
133 | override val key: Option[VNodeKey] = None,
134 | val namespace: Option[String] = None) extends VNode {
135 |
136 | import VNodeUtils._
137 |
138 | type That = VirtualElementNode
139 |
140 | /**
141 | * Compare to That using only the tag, key, potentially the namespace
142 | * and the number of children if checkChildren is true.
143 | *
144 | */
145 | def closeTo(that: That, checkChildren: Boolean = false): Boolean = {
146 | val b = tag == that.tag &&
147 | (key == that.key) &&
148 | (namespace == that.namespace)
149 | if (checkChildren) b && (children.length == that.children.length)
150 | else b
151 | }
152 |
153 | /**
154 | * Diff properties then children and compose the resulting patches.
155 | */
156 | def diff(that: That, path: Seq[Int] = Nil): Patch = {
157 | if (closeTo(that)) {
158 | // diff properties and children
159 | diffProperties(attributes, that.attributes).applyTo(path) andThen
160 | diffSeq(children, that.children, path)
161 | } else {
162 | // It's not the same node, so just replace it. Very unoptimized :-)
163 | ReplacePatch(that).applyTo(path)
164 | }
165 | }
166 | }
167 |
168 | /** An empty node that renders into something that is backend specific. */
169 | case class EmptyNode() extends VNode {
170 | type That = EmptyNode
171 | def diff(that: That, path: Seq[Int]) = PathPatch(EmptyPatch, path)
172 | }
173 |
174 | /**
175 | * Control VNode creation from within a VNode. Instead of
176 | * composing your VTree externally using a function, you
177 | * can have subtree generation occur inside the VNode itself.
178 | * This allows you compose tree generation logic
179 | * to another tree without relying on function composition
180 | * external to the tree. Yeah, that sounds useless but it is
181 | * helpful sometimes.
182 | */
183 | case class ThunkNode(val f: () => VNode) extends VNode {
184 | type That = ThunkNode
185 | def diff(that: That, path: Seq[Int]) = Diff.doDiff(f(), that.f(), path)
186 | }
187 |
188 | /**
189 | * A comment node. Sometimes, comments need to be
190 | * interested into a rendering process e.g. markup generation.
191 | *
192 | * TODO: Comments are all mapped to EmptyPatch.
193 | */
194 | case class CommentNode(val content: String) extends VNode {
195 | type That = CommentNode
196 | def diff(that: That, path: Seq[Int]) = {
197 | val p =
198 | if (content == that.content) EmptyPatch
199 | else EmptyPatch
200 | if (p != null) p.applyTo(path)
201 | else p
202 | }
203 | }
204 |
205 | /**
206 | * Smart constructors.
207 | */
208 | object VNode {
209 |
210 | /**
211 | * Create a constant ThunkNode. `f` is evaluated immediately.
212 | */
213 | def constant(f: => VNode) = {
214 | val c = f
215 | ThunkNode(() => c)
216 | }
217 |
218 | /**
219 | * Create a ThunkNode.
220 | */
221 | def thunk(f: => VNode) = ThunkNode(() => f)
222 |
223 | /** Create a new virtual text node */
224 | def tag(text: String) = VirtualElementNode(text)
225 |
226 | /** Create a new virtual element with the given tag */
227 | def tag(tag: String, attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode =
228 | VirtualElementNode(tag, attributes, children)
229 |
230 | /** Create a new virtual element with the given tag and key */
231 | def tag(tag: String, key: VNodeKey, attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode =
232 | VirtualElementNode(tag, attributes, children, Some(key))
233 |
234 | /** Create a new virtual element with the given tag and key */
235 | def tag(tag: String, key: VNodeKey, children: VNode*): VirtualElementNode =
236 | VirtualElementNode(tag, Seq(), children, Some(key))
237 |
238 | /** Create a new virtual element with the given tag, key and namespace */
239 | def tag(tag: String, key: Option[VNodeKey], namespace: Option[String], attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode =
240 | VirtualElementNode(tag, attributes, children, key, namespace)
241 |
242 | /** Create a new virtual element with children, but no attributes. */
243 | def tag(tag: String, children: VNode*): VirtualElementNode =
244 | VirtualElementNode(tag, Seq(), children)
245 |
246 | /** Create a SVG element. */
247 | def svg(attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode =
248 | VirtualElementNode("svg", attributes, children, None, Some(Constants.NS.SVG))
249 |
250 | /**
251 | * An empty VNode.
252 | */
253 | val empty = EmptyNode()
254 |
255 | /**
256 | * Alias for creating a text node.
257 | */
258 | def text(content: String) = VirtualText(content)
259 |
260 | /**
261 | * Insert a comment.
262 | */
263 | def comment(content: String) = CommentNode(content)
264 | }
265 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/backend/Backend.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 |
20 | import scala.concurrent.{ Promise, ExecutionContext, Future }
21 | import scala.util.control.NonFatal
22 |
23 | import util.{ Success, Failure, Try }
24 |
25 | /**
26 | * A backend that can run IOActions.
27 | *
28 | * Yes, this looks a little like typesafe's slick.
29 | */
30 | trait Backend { self =>
31 | type Context >: Null <: BasicContext
32 | protected[this]type This >: this.type <: Backend
33 |
34 | final def run[R](a: IOAction[R]): Future[R] = runInternal(a)
35 |
36 | private[vdom] def runInternal[R](a: IOAction[R]): Future[R] =
37 | try runInContext(a, createContext()) catch { case NonFatal(ex) => Future.failed(ex) }
38 |
39 | /**
40 | * Create the default Context for this backend.
41 | */
42 | protected[this] def createContext(): Context
43 |
44 | /**
45 | * Handle compositional actions and send out patch actions to a subclass overridable
46 | * method.
47 | */
48 | protected[this] def runInContext[R](a: IOAction[R], ctx: Context): Future[R] = {
49 | logAction(a, ctx)
50 | a match {
51 | case FlatMapAction(base, f, ec) => runInContext(base, ctx).flatMap(v => runInContext(f(v), ctx))(ctx.getEC(ec))
52 | case AndThenAction(a1, a2) => runInContext(a1, ctx).flatMap(_ => runInContext(a2, ctx))(Action.sameThreadExecutionContext)
53 | case SuccessAction(v) => Future.successful(v)
54 | case FailureAction(t) => Future.failed(t)
55 | case FutureAction(f) => f
56 | case AsTryAction(t) =>
57 | val p = Promise[R]()
58 | runInContext(a, ctx).onComplete(v => p.success(v.asInstanceOf[R]))(Action.sameThreadExecutionContext)
59 | p.future
60 | case sa@SequenceAction(actions) =>
61 | import java.util.concurrent.atomic.AtomicReferenceArray
62 | val len = actions.length
63 | val results = new AtomicReferenceArray[Any](len)
64 | def run(pos: Int): Future[Any] = {
65 | if (pos == len) Future.successful {
66 | val b = sa.cbf()
67 | var i = 0
68 | while (i < len) {
69 | b += results.get(i)
70 | i += 1
71 | }
72 | b.result()
73 | }
74 | else runInContext(actions(pos), ctx).flatMap { (v: Any) =>
75 | results.set(pos, v)
76 | run(pos + 1)
77 | }(Action.sameThreadExecutionContext)
78 | }
79 | run(0).asInstanceOf[Future[R]]
80 |
81 | case CleanUpAction(base, f, keepFailure, ec) =>
82 | val p = Promise[R]()
83 | runInContext(base, ctx).onComplete { t1 =>
84 | try {
85 | // transform base action result to a flipped Option
86 | val a2 = f(t1 match {
87 | case Success(_) => None
88 | case Failure(t) => Some(t)
89 | })
90 | // Run the user function that transforms the error
91 | runInContext(a2, ctx).onComplete { t2 =>
92 | if (t2.isFailure && (t1.isSuccess || !keepFailure)) p.complete(t2.asInstanceOf[Failure[R]])
93 | else p.complete(t1)
94 | }(Action.sameThreadExecutionContext)
95 | } catch {
96 | case NonFatal(ex) =>
97 | throw (t1 match {
98 | case Failure(t) if keepFailure => t
99 | case _ => ex
100 | })
101 | }
102 | }(ctx.getEC(ec))
103 | p.future
104 | case FailedAction(a) => runInContext(a, ctx).failed.asInstanceOf[Future[R]]
105 | case a: ContextualAction[_, _] => runPatchAction(a.asInstanceOf[ContextualAction[R, This]], ctx)
106 | case x@_ => throw new VDomException(s"Unknown action type $x for $this")
107 | }
108 | }
109 |
110 | protected[this] def runPatchAction[R](a: ContextualAction[R, This], ctx: Context): Future[R] = {
111 | val promise = Promise[R]()
112 | try {
113 | val r = a.run(ctx)
114 | promise.success(r)
115 | } catch {
116 | case NonFatal(ex) => promise.tryFailure(ex)
117 | }
118 | promise.future
119 | }
120 |
121 | protected[this] def logAction(a: IOAction[_], ctx: Context): Unit = {
122 | ctx.sequenceCounter += 1
123 | //println(s"#${ctx.sequenceCounter}: $a")
124 | }
125 |
126 | trait BasicContext extends ActionContext {
127 | @volatile private[Backend] var sequenceCounter: Long = 0
128 |
129 | /**
130 | * Given an ExcecutionContext return one, perhaps the modified original or a different one.
131 | */
132 | private[backend] def getEC(ec: ExecutionContext): ExecutionContext = ec
133 | }
134 | }
135 |
136 | /**
137 | * Backend rendering of VNodes. Since rendering may involve side effects
138 | * or asynchronous activities as a VNode is converted into the output,
139 | * return an IOAction wrapper around the output. Rendering creates
140 | * a description of what should be rendered and the actual processing
141 | * may be delayed until the rendered value is needed.
142 | */
143 | trait RendererComponent { self =>
144 |
145 | /**
146 | * The type output from the rendering.
147 | */
148 | type RenderOutput
149 |
150 | /**
151 | * Render a VNode producing RenderOutput objects.
152 | *
153 | * The action is a recipe and running it multiple times
154 | * could produce different output values.
155 | */
156 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput]
157 | }
158 |
159 | /**
160 | * Convert Patches to functions that can be applied to
161 | * Backend specific elements to create IOActions to be run
162 | * by the bBackend. Concrete backends will create ways
163 | * to implicitly create PatchPerformers and
164 | * have transparent syntax to run a Patch, but you can
165 | * also create a PatchPerformer explicitly so you can
166 | * add before and after actions to run using IOAction
167 | * composition.
168 | *
169 | * PatchInput does not necessary have to be
170 | * a specific Element type for the visual UI tree, it could
171 | * be a wrapped object that contains additional information
172 | * that is used to create the IOAction. For example, it could
173 | * contain different types of interceptors/callbacks around
174 | * creating Backend specific elements.
175 | */
176 | trait PatchesComponent { self =>
177 | type PatchInput
178 | type PatchOutput
179 |
180 | /**
181 | * Convert PatchInput into a wrapped PatchOutput. A PatchPerformer
182 | * can be applied to a PatchInput by the user. The output IOAction
183 | * will still need to be run.
184 | */
185 | trait PatchPerformer extends (PatchInput => IOAction[PatchOutput])
186 |
187 | object PatchPerformer {
188 | /** Create PatchPerformers more easily */
189 | def apply(f: PatchInput => IOAction[PatchOutput]) =
190 | new PatchPerformer {
191 | def apply(pi: PatchInput) = f(pi)
192 | }
193 | }
194 |
195 | /**
196 | * Make a patch able to be applied to `PatchInput` to create
197 | * a runnable action. Generally, this function must process
198 | * all Patch types and produce a PatchPerformer object. Concrete
199 | * backends will need to match against the Patch type and
200 | * then create a specialized PatchPerformer.
201 | */
202 | def makeApplyable(patch: Patch)(implicit executor: ExecutionContext): PatchPerformer
203 | }
204 |
205 |
206 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/backend/Hints.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 |
20 | import scala.collection.immutable.BitSet
21 | import scala.language._
22 |
23 | /**
24 | * Basic hint structure. Hints are encoded as bit flags..Sub-classes
25 | * are made for different hint usage types so that they can
26 | * be customized with additional usage specific hint information.
27 | * This is essentially like React's DOMProperty hint scaffolding.
28 | */
29 | trait Hints {
30 | /** Hint information for working with values */
31 | def values: BitSet
32 | }
33 |
34 | /**
35 | * Hints for working with Elements
36 | */
37 | case class AttrHint(
38 | val values: BitSet) extends Hints
39 |
40 | /** Hints for Styles. */
41 | case class StyleHint(
42 | val values: BitSet) extends Hints
43 |
44 | /**
45 | * Hints for Elements.
46 | */
47 | case class ElHint(
48 | val values: BitSet) extends Hints
49 |
50 | /**
51 | * Some of these hints are only relevant with certain backends,
52 | * such as the DOM backend e.g. MustUseProperty. Some, are
53 | * related to multiple backends, such as HasPositiveNumericValue.
54 | *
55 | * Hints can be combined through '+'.
56 | */
57 | object Hints {
58 |
59 | /** Convert single BitSet to an AttrHint */
60 | implicit def hintToAttrHint(hints: BitSet) = AttrHint(values = hints)
61 |
62 | /** Convert multiple BitSets to an AttrHint */
63 | implicit def hintsToAttrHints(hints: BitSet*) =
64 | AttrHint(values = hints.fold(EmptyHints)(_ & _))
65 |
66 | /** Empty hint set. */
67 | val EmptyHints = BitSet.empty
68 |
69 | /** Convenience converter. */
70 | implicit def toBitSet(i: Int) = BitSet(i)
71 |
72 | /** Should use set/get/remove*Property */
73 | val MustUseAttribute = 1
74 |
75 | /** Must set value NOt using the attribute API */
76 | val MustUseProperty = 2
77 |
78 | /** Setting the value has side effects. */
79 | val HasSideEffects = 3
80 |
81 | /** Whether property should be removed when set to a falsy value. */
82 | val HasBooleanValue = 4
83 |
84 | /** Not sure what this means in react. */
85 | val HasOverloadedBooleanValue = 5
86 |
87 | /** Whether property must be numeric or parse as numeric and should be removed when set to a falsy value */
88 | val HasNumericValue = 6
89 |
90 | /** Numeric and positive. */
91 | val HasPositiveNumericValue = 7
92 |
93 | // Element hints are separately enumerated from attribute hints.
94 | val OmitClosingTag = 1
95 | val NewlineEating = 2
96 |
97 | // Style hints
98 | val Unitless = 1
99 |
100 | /**
101 | * Empty hints with empty value hints.
102 | */
103 | val EmptyAttrHints = AttrHint(values = EmptyHints)
104 | }
105 |
106 | trait DOMStyleHints {
107 | import Hints._
108 |
109 | private val styleHints: Map[String, StyleHint] = Map(
110 | "animationIterationCount" -> StyleHint(values = Unitless),
111 | "boxFlex" -> StyleHint(values = Unitless),
112 | "boxFlexGroup" -> StyleHint(values = Unitless),
113 | "boxOrdinalGroup" -> StyleHint(values = Unitless),
114 | "columnCount" -> StyleHint(values = Unitless),
115 | "flex" -> StyleHint(values = Unitless),
116 | "flexGrow" -> StyleHint(values = Unitless),
117 | "flexPositive" -> StyleHint(values = Unitless),
118 | "flexShrink" -> StyleHint(values = Unitless),
119 | "flexNegative" -> StyleHint(values = Unitless),
120 | "flexOrder" -> StyleHint(values = Unitless),
121 | "gridRow" -> StyleHint(values = Unitless),
122 | "gridColumn" -> StyleHint(values = Unitless),
123 | "fontWeight" -> StyleHint(values = Unitless),
124 | "lineClamp" -> StyleHint(values = Unitless),
125 | "lineHeight" -> StyleHint(values = Unitless),
126 | "opacity" -> StyleHint(values = Unitless),
127 | "order" -> StyleHint(values = Unitless),
128 | "orphans" -> StyleHint(values = Unitless),
129 | "tabSize" -> StyleHint(values = Unitless),
130 | "widows" -> StyleHint(values = Unitless),
131 | "zIndex" -> StyleHint(values = Unitless),
132 | "zoom" -> StyleHint(values = Unitless),
133 |
134 | // SVG-related properties
135 | "fillOpacity" -> StyleHint(values = Unitless),
136 | "stopOpacity" -> StyleHint(values = Unitless),
137 | "strokeDashoffset" -> StyleHint(values = Unitless),
138 | "strokeOpacity" -> StyleHint(values = Unitless),
139 | "strokeWidth" -> StyleHint(values = Unitless))
140 |
141 | def styleHint(name: String) = styleHints.get(name)
142 | }
143 | protected[backend] object DOMStyleHints extends DOMStyleHints
144 |
145 | trait DOMElHints {
146 | import Hints._
147 |
148 | private val elHints: Map[String, ElHint] = Map(
149 | "area" -> ElHint(values = OmitClosingTag),
150 | "base" -> ElHint(values = OmitClosingTag),
151 | "br" -> ElHint(values = OmitClosingTag),
152 | "col" -> ElHint(values = OmitClosingTag),
153 | "hr" -> ElHint(values = OmitClosingTag),
154 | "img" -> ElHint(values = OmitClosingTag),
155 | "input" -> ElHint(values = OmitClosingTag),
156 | "keygen" -> ElHint(values = OmitClosingTag),
157 | "listing" -> ElHint(values = NewlineEating),
158 | "link" -> ElHint(values = OmitClosingTag),
159 | "meta" -> ElHint(values = OmitClosingTag),
160 | "param" -> ElHint(values = OmitClosingTag),
161 | "pre" -> ElHint(values = NewlineEating),
162 | "source" -> ElHint(values = OmitClosingTag),
163 | "textarea" -> ElHint(values = NewlineEating),
164 | "track" -> ElHint(values = OmitClosingTag),
165 | "wbr" -> ElHint(values = OmitClosingTag))
166 |
167 | /** Get a hint or None. */
168 | def elHint(name: String) = elHints.get(name)
169 | }
170 |
171 | protected[backend] object DOMElHints extends DOMElHints
172 |
173 | trait DOMAttrHints {
174 | import Hints._
175 | import Constants.NS._
176 |
177 | private implicit def toAttrHint(i: Int) = AttrHint(values = BitSet(i))
178 |
179 | private val attrHints: Map[String, AttrHint] = Map(
180 | "allowFullScreen" -> AttrHint(values = BitSet(HasBooleanValue) | BitSet(MustUseAttribute)),
181 | "async" -> AttrHint(values = HasBooleanValue),
182 | "autoPlay" -> AttrHint(values = HasBooleanValue),
183 | "checked" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)),
184 | "class" -> AttrHint(values = MustUseAttribute),
185 | "disabled" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)),
186 | "download" -> AttrHint(values = HasOverloadedBooleanValue),
187 | "formNoValidate" -> AttrHint(values = HasBooleanValue),
188 | "hidden" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)),
189 | "height" -> AttrHint(MustUseAttribute),
190 | "hidden" -> AttrHint(MustUseAttribute),
191 | "id" -> AttrHint(MustUseProperty),
192 | "readOnly" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)),
193 | "required" -> AttrHint(values = HasBooleanValue),
194 | "reversed" -> AttrHint(values = HasBooleanValue),
195 | "scoped" -> AttrHint(values = HasBooleanValue),
196 | "seamless" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)),
197 | "selected" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)),
198 | "value" -> AttrHint(BitSet(MustUseProperty) | BitSet(HasSideEffects)),
199 | "width" -> AttrHint(MustUseAttribute))
200 |
201 | private val svgHints: Map[String, AttrHint] = Map(
202 | "clipPath" -> AttrHint(MustUseAttribute),
203 | "cx" -> MustUseAttribute,
204 | "cy" -> MustUseAttribute,
205 | "d" -> MustUseAttribute,
206 | "dx" -> MustUseAttribute,
207 | "dy" -> MustUseAttribute,
208 | "fill" -> MustUseAttribute,
209 | "fillOpacity" -> AttrHint(MustUseAttribute),
210 | "fontFamily" -> AttrHint(MustUseAttribute),
211 | "fontSize" -> AttrHint(MustUseAttribute),
212 | "fx" -> MustUseAttribute,
213 | "fy" -> MustUseAttribute,
214 | "gradientTransform" -> AttrHint(MustUseAttribute),
215 | "gradientUnits" -> AttrHint(MustUseAttribute),
216 | "markerEnd" -> AttrHint(MustUseAttribute),
217 | "markerMid" -> AttrHint(MustUseAttribute),
218 | "markerStart" -> AttrHint(MustUseAttribute),
219 | "offset" -> MustUseAttribute,
220 | "opacity" -> MustUseAttribute,
221 | "patternContentUnits" -> MustUseAttribute,
222 | "patternUnits" -> MustUseAttribute,
223 | "points" -> MustUseAttribute,
224 | "preserveAspectRatio" -> MustUseAttribute,
225 | "r" -> MustUseAttribute,
226 | "rx" -> MustUseAttribute,
227 | "ry" -> MustUseAttribute,
228 | "spreadMethod" -> MustUseAttribute,
229 | "stopColor" -> AttrHint(MustUseAttribute),
230 | "stopOpacity" -> AttrHint(MustUseAttribute),
231 | "stroke" -> MustUseAttribute,
232 | "strokeDasharray" -> AttrHint(MustUseAttribute),
233 | "strokeLinecap" -> AttrHint(MustUseAttribute),
234 | "strokeOpacity" -> AttrHint(MustUseAttribute),
235 | "strokeWidth" -> AttrHint(MustUseAttribute),
236 | "textAnchor" -> AttrHint(MustUseAttribute),
237 | "transform" -> MustUseAttribute,
238 | "version" -> MustUseAttribute,
239 | "viewBox" -> AttrHint(MustUseAttribute),
240 | "x1" -> MustUseAttribute,
241 | "x2" -> MustUseAttribute,
242 | "x" -> MustUseAttribute,
243 | "xlinkActuate" -> AttrHint(MustUseAttribute),
244 | "xlinkArcrole" -> AttrHint(MustUseAttribute),
245 | "xlinkHref" -> AttrHint(MustUseAttribute),
246 | "xlinkRole" -> AttrHint(MustUseAttribute),
247 | "xlinkShow" -> AttrHint(MustUseAttribute),
248 | "xlinkTitle" -> AttrHint(MustUseAttribute),
249 | "xlinkType" -> AttrHint(MustUseAttribute),
250 | "xmlBase" -> AttrHint(MustUseAttribute),
251 | "xmlLang" -> AttrHint(MustUseAttribute),
252 | "xmlSpace" -> AttrHint(MustUseAttribute),
253 | "y1" -> MustUseAttribute,
254 | "y2" -> MustUseAttribute,
255 | "y" -> MustUseAttribute)
256 |
257 | /** Get a hint or None. */
258 | def attrHint(name: String) = attrHints.get(name) orElse svgHints.get(name)
259 |
260 | }
261 |
262 | protected[backend] object DOMAttrHints extends DOMAttrHints
263 |
264 | /** All DOM related hints. */
265 | trait DOMHints extends DOMStyleHints with DOMElHints with DOMAttrHints
266 |
267 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/backend/MarkupBackend.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 |
20 | import scala.concurrent.ExecutionContext
21 | import scala.concurrent.Future
22 |
23 | /**
24 | * Render to HTML strings.
25 | *
26 | */
27 | trait MarkupRendererComponent extends RendererComponent {
28 | self: Backend with DOMHints =>
29 |
30 | import Utils._
31 |
32 | type RenderOutput = String
33 |
34 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput] = {
35 | val ctx = createContext().getEC(executor)
36 |
37 | vnode match {
38 | case v@VirtualText(content) =>
39 | Action.successful(content)
40 |
41 | case v@VirtualElementNode(tag, attributes, children, key, namespace) =>
42 |
43 | // find style "attributes", then add
44 | val processStyles = Action.lift {
45 | val valstr = attributes.filter(keepStyles).map { kv =>
46 | createMarkupForStyles(kv, styleHint(kv.key.name)).getOrElse("")
47 | }.mkString(";")
48 | valstr match {
49 | case "" => ""
50 | case v => s"""style="$v;""""
51 | }
52 | }
53 |
54 | // process remaining attributes
55 | val processAttributes = Action.lift {
56 | attributes.filter(keepAttributes).map { kv =>
57 | createMarkupForProperty(kv, attrHint(kv.key.name)).getOrElse("")
58 | }.mkString(" ")
59 | }
60 |
61 | // create tag content
62 | val childMarkup = Action.fold(children.map { child =>
63 | render(child)
64 | }, "") { stringAppend }
65 |
66 | val elHint = self.elHint(tag)
67 |
68 | // If omit closing tag, just close tag, don't generate middle content.
69 | val middleEnd = elHint.filter(h => h.values(Hints.OmitClosingTag)).fold {
70 | Seq(Action.successful(">"),
71 | childMarkup,
72 | Action.successful("" + tag + ">"))
73 | } { h => Seq(Action.successful("/>")) }
74 |
75 | Action.fold(Seq(Action.successful("<" + tag + " "),
76 | processStyles,
77 | processAttributes) ++ middleEnd, "") { stringAppend }
78 |
79 | case EmptyNode() =>
80 | Action.successful("")
81 | case CommentNode(content) =>
82 | Action.successful("")
83 | case ThunkNode(f) =>
84 | render(f())
85 | case x@_ =>
86 | Action.failed(new VDomException(s"Unknown VNode type $x for $this"))
87 | }
88 | }
89 | }
90 |
91 | trait MarkupBackend extends Backend with MarkupRendererComponent with DOMHints {
92 | type This = MarkupBackend
93 | type Context = BasicContext
94 |
95 | protected[this] def createContext() = new BasicContext {}
96 | }
97 |
98 | object MarkupBackend extends MarkupBackend
99 |
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/backend/Utils.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 | package backend
19 |
20 | /**
21 | * Adler32 checksum.
22 | *
23 | * @see http://patterngazer.blogspot.com/2012/01/naive-adler32-example-in-clojure-and.html
24 | */
25 | private[backend] trait Adler32 {
26 | val base = 65521
27 |
28 | def rebased(value: Int) = value % base
29 |
30 | def cumulated(acc: (Int, Int), item: Byte): (Int, Int) = {
31 | val a = rebased(acc._1 + (item & 0xff))
32 | (a, (a + acc._2) % base)
33 | }
34 |
35 | def checksum(data: Traversable[Byte]): Int
36 |
37 | def checksumText(data: Traversable[Char]): Int
38 | }
39 |
40 | protected[backend] object Adler32 extends Adler32 {
41 |
42 | override def checksum(data: Traversable[Byte]): Int = {
43 | val result = data.foldLeft((1, 0)) { cumulated(_, _) }
44 | (result._2 << 16) | result._1
45 | }
46 |
47 | def checksumText(data: Traversable[Char]) = {
48 | checksum(data.toSeq.map(_.toByte))
49 | }
50 | }
51 |
52 | protected[backend] trait Utils {
53 |
54 | /**
55 | * Return Adler32 checksum.
56 | */
57 | def adler32(str: String): Int = Adler32.checksumText(str)
58 |
59 | /**
60 | * Name of the custom data attribute that stores a checksum.
61 | */
62 | val ChecksumAttrName = "data-scala-vdom-checksum"
63 |
64 | /**
65 | * Add checksum to end of markup using complete adhoc string regex
66 | * to find the end tag. Assumes the first set of characters
67 | * is the start of an element and the tag ends in ">" or "/>".
68 | */
69 | def addChecksumToMarkup(markup: String): String = {
70 | val checksum = adler32(markup)
71 | markup.replaceFirst("(/?>)", " " + ChecksumAttrName + "=\"" + checksum + "\"$1")
72 | }
73 |
74 | /** Append two strings */
75 | def stringAppend(left: String, right: String) = left + right
76 |
77 | /** Filter for filtering attribute lists. Keeps StyleKey attributes. */
78 | def keepStyles(el: KeyValue[_]) = el match {
79 | case KeyValue(StyleKey(_), _) => true
80 | case _ => false
81 | }
82 |
83 | /** Filter for filtering attribute lists. Keeps AttrKey attributes. */
84 | def keepAttributes(el: KeyValue[_]) = el match {
85 | case KeyValue(AttrKey(_, _), _) => true
86 | case _ => false
87 | }
88 |
89 | /**
90 | * Take a value and convert it to a quoted string suitable for use
91 | * as an attribute's value in markup.
92 | */
93 | def quoteValueForBrowser[T](v: T): String =
94 | "\"" + escapeTextForBrowser(v) + "\""
95 |
96 | private[this] val escapeTable = Seq(
97 | ("&".r, "&"),
98 | (">".r, ">"),
99 | ("<".r, "<"),
100 | ("\"".r, """),
101 | ("'".r, "'"))
102 |
103 | /**
104 | * Escape certain char sequences to avoid scripting attacks.
105 | *
106 | * TODO: Horribly inefficient!
107 | */
108 | def escapeTextForBrowser[T](v: T): String = {
109 | var rval = v.toString
110 | for (p <- escapeTable)
111 | rval = p._1.replaceAllIn(rval, p._2)
112 | rval
113 | }
114 |
115 | /**
116 | * Check hint structure and apply business rule about whether this
117 | * value should be ignored and does not need to be set into a DOM object.
118 | */
119 | def ignoreValue[T](key: KeyPart, hintopt: Option[AttrHint], value: T): Boolean = {
120 | (hintopt.map(_.values).getOrElse(Hints.EmptyHints), value) match {
121 | case (hints, false) if (hints(Hints.HasBooleanValue)) => true
122 | case _ => false
123 | }
124 | }
125 |
126 | /**
127 | * Create markup for a key value pair. It considers both the hint and the value
128 | * when generating markup.
129 | *
130 | * If no hint is found, generate a simple `name = 'value'` and convert the
131 | * value to a quoted string. If the value is None then it
132 | * generates a None return value.
133 | *
134 | * @return None if no markup was generated or a string of markup.
135 | */
136 | def createMarkupForProperty(kv: KeyValue[_], hintopt: Option[AttrHint]): Option[String] = {
137 | kv.value.
138 | filterNot(ignoreValue(kv.key, hintopt, _)).
139 | map { v =>
140 | (hintopt.map(_.values).getOrElse(Hints.EmptyHints), v) match {
141 | case (hints, true) if (hints(Hints.HasBooleanValue) || hints(Hints.HasOverloadedBooleanValue)) =>
142 | kv.key.name + """="""""
143 | case _ => kv.key.name + "=" + quoteValueForBrowser(v)
144 | }
145 | }
146 | }
147 |
148 | /**
149 | * Take a style value and prepare it to be inserted into markup.
150 | */
151 | def quoteStyleValueForBrowser[T](hintopt: Option[StyleHint], v: T) = {
152 | (hintopt.map(_.values).getOrElse(collection.BitSet.empty), v) match {
153 | case (_, null) => ""
154 | case (_, true) => ""
155 | case (_, false) => ""
156 | case (_, "") => ""
157 | case (hints, x@_) if (hints(Hints.Unitless)) => v.toString
158 | case _ => v.toString.trim + "px"
159 | }
160 | }
161 |
162 | /** Convert camel cased to a hyphenated name. */
163 | def hyphenate(name: String) = name.replaceAll("([A-Z])", "-$1").toLowerCase
164 |
165 | /**
166 | * Process a style name for proper formation. Hyphenate and fix
167 | * some.
168 | *
169 | * For "ms-" prefix convert to "-ms-" per react.
170 | */
171 | def processStyleName(name: String) = {
172 | hyphenate(name).trim.replaceAll("^ms-", "-ms-")
173 | }
174 |
175 | /**
176 | * Create style markup. This does NOT include `style=` or
177 | * surrounding quotes. None values conceptually indicate
178 | * we should ignore the value so no markup is generated for
179 | * keys with None values.
180 | *
181 | * @return None if no markup was generated or a string of markup.
182 | *
183 | * TODO Allow the specification of a hint source.
184 | */
185 | def createMarkupForStyles(kv: KeyValue[_], hintopt: Option[StyleHint]): Option[String] =
186 | kv.value.map { v => processStyleName(kv.key.name) + ":" + quoteStyleValueForBrowser(hintopt, v) }
187 |
188 | /**
189 | * Norm the string for comparison purposes. Remove repeated whitespace,
190 | * remove whitespace before non-alpha characters, remove leading/trailing
191 | * whitespace. Note that newlines are stripped as well and cannot
192 | * detect correctness if a newline MUST be in the normalized string.
193 | */
194 | def norm(input: String): String = {
195 | input.trim().
196 | replaceAll("(\\s)+", " ").
197 | replaceAll("\\s(\\W)", "$1").
198 | replaceAll("(\\W)\\s", "$1").
199 | toUpperCase
200 | }
201 |
202 | }
203 |
204 | protected[backend] object Utils extends Utils
--------------------------------------------------------------------------------
/shared/src/main/scala/im/vdom/package.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 |
18 | import scala.language._
19 |
20 | package object vdom {
21 |
22 | /**
23 | * Virtual nodes may have keys to improve diff'ing performance.
24 | */
25 | type VNodeKey = String
26 |
27 | /**
28 | * Reduction to a single patch object using composition. All
29 | * patches will apply to the same input object when applied.
30 | */
31 | implicit def seqPatchToPatch(seq: Seq[Patch]): Patch = seq.fold(EmptyPatch)((p, n) => p andThen n)
32 |
33 | /**
34 | * Generate a Patch that describes the differences between original and target.
35 | */
36 | def diff(original: VNode, target: VNode): Patch = Diff.diff(original, target)
37 |
38 | /**
39 | * Helpers for comparing Option values.
40 | */
41 | implicit class OptionOps[T](lhs: Option[T]) {
42 |
43 | /**
44 | * rhs = None acts like a wildcard and matches anything
45 | * on lhs. But lhs = None only matches a rhs None.
46 | */
47 | def wildcardEq(rhs: Option[T]) = (lhs, rhs) match {
48 | case (None, None) => true
49 | case (None, _) => false
50 | case (_, None) => true
51 | case (Some(l), Some(r)) => l == r
52 | }
53 |
54 | /**
55 | * Negated `===`
56 | */
57 | def /==(rhs: Option[T]) = !(===(rhs))
58 | /**
59 | * Equal only if they are both defined and the defined values are equal.
60 | */
61 | def ===(rhs: Option[T]) = lhs.toRight(false) == rhs.toRight(true)
62 | }
63 |
64 | /**
65 | * Enable explicit `.toPatch` notation on a sequence of patches.
66 | */
67 | implicit class ToPatch(seq: Seq[Patch]) {
68 | def to1Patch = seqPatchToPatch(seq)
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/DiffSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import org.scalatest._
20 |
21 | /**
22 | * Test diffing algorithm.
23 | */
24 | class DiffSpec extends FlatSpec
25 | with Matchers
26 | with OptionValues {
27 |
28 | import VNode._
29 | import VNodeUtils._
30 |
31 | val nodea = Seq(
32 | tag("li", text("one")),
33 | tag("li", text("two")),
34 | tag("li", text("three")))
35 |
36 | val nodeb = Seq(
37 | tag("li", text("one")),
38 | tag("li", text("two")),
39 | tag("li", text("three")),
40 | tag("li", text("four")))
41 |
42 | val nodeBChangeLastVNode = Seq(
43 | tag("li", text("one")),
44 | tag("li", text("two")),
45 | tag("li", text("three")),
46 | tag("p"))
47 |
48 | "General vnode tests" should "show two lists of line items to be not equal if they have different content" in {
49 | nodea should not equal (nodeb)
50 | }
51 |
52 | it should "show two ul with the same number of children to be close to but may not be equal" in {
53 | val nodea = tag("ul",
54 | tag("li", text("foo")),
55 | tag("li", text("fi")),
56 | tag("li", text("fee")))
57 | val nodeb = tag("ul",
58 | tag("li", text("foo")),
59 | tag("li", text("fi")),
60 | tag("li", text("fee")))
61 | assert(nodea closeTo nodeb)
62 | }
63 |
64 | it should "two ul with the same number of children but different keys to not be close to" in {
65 | val nodea = tag("ul", "key1", Seq(),
66 | tag("li", text("foo")),
67 | tag("li", text("fi")),
68 | tag("li", text("fee")))
69 | val nodeb = tag("ul", "key2", Seq(),
70 | tag("li", text("foo")),
71 | tag("li", text("fi")),
72 | tag("li", text("fee")))
73 | nodea closeTo nodeb should not be (true)
74 | }
75 |
76 | it should "correctly find or not find a vnode in a list of vnodes using kindOfContains equals and closeEnough" in {
77 | val x = kindOfContains(nodeb, nodeb(3))(_ == _)
78 | x should be(true)
79 |
80 | val y = kindOfContainsCloseEnough(nodeb, nodeb(3))
81 | y should be(true)
82 |
83 | val z = kindOfContains(nodeb, tag("li", text("boom")))(_ == _)
84 | z should be(false)
85 |
86 | val ztoo = kindOfContainsCloseEnough(nodeb, tag("li", text("boom")))
87 | ztoo should be(true)
88 |
89 | val znot = kindOfContainsCloseEnough(nodeb, tag("li"))
90 | znot should be(true)
91 | }
92 |
93 | it should "find removes using equality but not find any using closeEnough when using similar vnodes" in {
94 | val x = findRemoves(nodeb, nodea)(_ == _)
95 | x should have length 1
96 | x should contain(3)
97 |
98 | val y = findRemovesCloseEnough(nodeb, nodea)
99 | withClue("find removes using closeEnough") {
100 | y should have length 0
101 | }
102 | }
103 |
104 | it should "find removes using equality and closeEnough when using non-similar vnodes" in {
105 | val x = findRemoves(nodeBChangeLastVNode, nodea)(_ == _)
106 | x should have length 1
107 | x should contain(3)
108 |
109 | val y = findRemovesCloseEnough(nodeBChangeLastVNode, nodeb)
110 | withClue("find removes using closeEnough") {
111 | y should have length 1
112 | }
113 | }
114 |
115 | it should "not find removes comparing with equality and closeEnough and comparing a sequence to itself" in {
116 | val x = findRemoves(nodeb, nodeb)(_ == _)
117 | x should have length 0
118 |
119 | val y = findRemovesCloseEnough(nodeb, nodeb)
120 | withClue("find removes using closeEnough") {
121 | y should have length 0
122 | }
123 | }
124 |
125 | it should "find adds using exact matches but not with closeEnough" in {
126 | val x = findAdds(nodea, nodeb)(_ == _)
127 | x should have length 1
128 | x(0) should equal(3)
129 |
130 | val y = findRemovesCloseEnough(nodea, nodeb)
131 | withClue("find adds using closeEnough") {
132 | y should have length 0
133 | }
134 | }
135 |
136 | "diffSeq" should "should find the removes when one vnode is removed in a sequence" in {
137 | val (moves, removes, adds, rest) = Diff.diffSeq2(nodeb, nodea, Nil)
138 | removes should have length 1
139 | }
140 |
141 | it should "should find the adds when one vnode is removed in a sequence" in {
142 | val (moves, removes, adds, rest) = Diff.diffSeq2(nodea, nodeb, Nil)
143 | adds should have length 1
144 | }
145 |
146 | it should "find simple moves if two children swap places" in {
147 | val lhs = Seq(nodea(0), nodea(1))
148 | val rhs = Seq(nodea(1), nodea(0))
149 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil)
150 | withClue("moves") { moves should have length 2 }
151 | withClue("removes") { removes should have length 0 }
152 | withClue("adds") { adds should have length 0 }
153 | }
154 |
155 | it should "find simple moves if two children change places" in {
156 | val lhs = Seq(nodea(0), nodea(1), nodea(2))
157 | val rhs = Seq(nodea(2), nodea(1), nodea(0))
158 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil)
159 | withClue("moves") { moves should have length 2 }
160 | withClue("removes") { removes should have length 0 }
161 | withClue("adds") { adds should have length 0 }
162 | }
163 |
164 | it should "find simple removes and moves if two children change place and one child is dropped" in {
165 | val lhs = Seq(nodea(0), nodea(1), nodea(2))
166 | val rhs = Seq(nodea(2), nodea(1))
167 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil)
168 | withClue("moves") {
169 | moves should have length 2
170 | moves(0) should equal((1, 0))
171 | moves(1) should equal((0, 1))
172 | }
173 | withClue("removes") {
174 | removes should contain only (0)
175 | }
176 | withClue("adds") {
177 | adds should have length 0
178 | }
179 | }
180 |
181 | it should "find simple adds and moves if two children change place and one child is dropped" in {
182 | val lhs = Seq(nodea(0), nodea(1))
183 | val rhs = Seq(nodea(1), nodea(0), nodea(2))
184 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil)
185 | withClue("moves") {
186 | moves should have length 2
187 | moves(0) should equal((1, 0))
188 | moves(1) should equal((0, 1))
189 | }
190 | withClue("removes") {
191 | removes should have length 0
192 | }
193 | withClue("adds") {
194 | adds should contain only (2)
195 | }
196 | }
197 |
198 | "Diff" should "create an empty patch of both vnodes are the same" in {
199 | val nodea = tag("p")
200 | val nodeb = tag("p")
201 | val d = diff(nodea, nodeb)
202 | d should equal(EmptyPatch)
203 | }
204 |
205 | it should "create an empty patch with a multi-level vnode tree when the trees are the same" in {
206 | val d = diff(tag("ul", nodea: _*), tag("ul", nodea: _*))
207 | d should equal(EmptyPatch)
208 | }
209 |
210 | }
211 |
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/OptionSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | //import _root_.org.scalajs.{ dom => d }
20 |
21 | import org.scalatest.FlatSpec
22 | import org.scalatest.Matchers
23 | import org.scalatest.OptionValues
24 | import org.scalatest.prop
25 |
26 | /**
27 | * Test OptionOpps.
28 | */
29 | class OptionSpec extends FlatSpec
30 | with Matchers
31 | with OptionValues
32 | with prop.TableDrivenPropertyChecks {
33 |
34 | import im.vdom.OptionOps
35 |
36 | val goodwildcard = Table(
37 | ("lhs", "rhs"),
38 | (None, None),
39 | (Some(1), None),
40 | (Some("blah"), None))
41 |
42 | it should "find good wildCardEq" in {
43 | forAll(goodwildcard) { (lhs: Option[Any], rhs: Option[Any]) =>
44 | lhs wildcardEq rhs should equal(true)
45 | }
46 | }
47 |
48 | val badwildcard = Table(
49 | ("lhs", "rhs"),
50 | (None, Some(1)),
51 | (Some(3), Some(4)))
52 |
53 | it should "find bad wildCardEq" in {
54 | forAll(badwildcard) { (lhs: Option[Any], rhs: Option[Any]) =>
55 | lhs wildcardEq rhs should equal(false)
56 | }
57 | }
58 |
59 | it should "handle ===" in {
60 | Some(3) === Some(3) should equal (true)
61 | None === None should equal (false)
62 | None === Some(3) should equal (false)
63 | Some(4) === Some(5) should equal (false)
64 | }
65 |
66 |
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/VNodeSpec.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Devon Miller
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package im
17 | package vdom
18 |
19 | import org.scalatest._
20 |
21 | /**
22 | * Patches do not need much testing...so this is really
23 | * just a dummy test class.
24 | */
25 | class VNodeSpec extends FlatSpec
26 | with Matchers
27 | with OptionValues {
28 |
29 | import VNode._
30 |
31 | "VNode" should "have equals that works" in {
32 | val lhs = tag("div", Some("key1"), None, Seq())
33 | val rhs = tag("div", Some("key1"), None, Seq())
34 | val rhs2 = tag("div", Some("key2"), None, Seq())
35 |
36 | assertResult(true)(lhs == rhs)
37 | assertResult(false)(rhs == rhs2)
38 | }
39 |
40 | it should "equals on a simple tag" in {
41 | val lhs = tag("div")
42 | val rhs = tag("div")
43 | assertResult(true)(lhs == rhs)
44 | assertResult(false)(lhs eq rhs)
45 | }
46 |
47 | "closeTo" should "test on tag, key and namespace" in {
48 | val lhs = tag("div", None, None, Seq())
49 | val rhs = tag("div", None, None, Seq())
50 | assertResult(true)(lhs.closeTo(rhs))
51 |
52 | val lhs1 = tag("div", Some("k1"), None, Seq())
53 | val rhs1 = tag("div", Some("k1"), None, Seq())
54 | assertResult(true)(lhs1.closeTo(rhs1))
55 |
56 | val rhs2 = tag("div", None, None, Seq())
57 | assertResult(false)(lhs1.closeTo(rhs2))
58 |
59 | val lhs3 = tag("div", Some("k1"), Some("ns"), Seq())
60 | val rhs3 = tag("div", Some("k1"), Some("ns"), Seq())
61 | assertResult(true)(lhs3.closeTo(rhs3))
62 |
63 | val rhs4 = tag("div", Some("k1"), None, Seq())
64 | assertResult(false)(lhs3.closeTo(rhs4))
65 | }
66 |
67 | "closeToOrEqual" should "use closeTo when a VirtualElementNode and == when anything else" in {
68 | import VNodeUtils.closeToOrEquals
69 | val lhs = text("test")
70 | val rhs = text("test")
71 | assertResult(true)(closeToOrEquals(lhs, rhs))
72 |
73 | val rhs2 = comment("comment")
74 | assertResult(false)(lhs.closeToOrEquals(rhs2))
75 |
76 | val lhs3 = tag("div", Some("k1"), Some("ns"), Seq())
77 | val rhs3 = tag("div", Some("k1"), Some("ns"), Seq())
78 | assertResult(true)(closeToOrEquals(lhs3, rhs3))
79 |
80 | assertResult(false)(closeToOrEquals(lhs, rhs3))
81 | }
82 |
83 | "kindOfContains" should "find something close in the list" in {
84 | val seq = Seq(text("text"), tag("div", Some("k1"), None, Seq()), comment("comment"))
85 | assertResult(true)(VNodeUtils.kindOfContainsCloseEnough(seq, tag("div", Some("k1"), None, Seq())))
86 | assertResult(true)(VNodeUtils.kindOfContainsCloseEnough(seq, comment("comment")))
87 | assertResult(false)(VNodeUtils.kindOfContainsCloseEnough(seq, tag("div", Some("k1"), Some("ns"), Seq())))
88 | }
89 |
90 | "findRemoves" should "find removes in easy vnode list" in {
91 | val lhs = Seq(text("t1"), text("t2"), text("t3"))
92 | val rhs = Seq(text("t1"), text("t3"))
93 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs)
94 | assertResult(1)(r.size)
95 | assertResult(1)(r(0))
96 | }
97 |
98 | it should "not remove nodes if they are close to each other with keys" in {
99 | val lhs = Seq(text("t1"), tag("div", Some("k1"), None, Seq(), text("text")), text("t3"))
100 | val rhs = Seq(text("t1"), tag("div", Some("k1"), None, Seq()), text("t3"))
101 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs)
102 | assertResult(0)(r.size)
103 | }
104 |
105 | it should "not remove nodes if they are close to each without keys" in {
106 | val lhs = Seq(text("t1"), tag("div", None, None, Seq(), text("text")), text("t3"))
107 | val rhs = Seq(text("t1"), tag("div", None, None, Seq()), text("t3"))
108 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs)
109 | assertResult(0)(r.size)
110 | }
111 |
112 | it should "not find any removes on empty lists" in {
113 | val rhs: Seq[VNode] = Seq.empty
114 | val lhs: Seq[VNode] = Seq.empty
115 | assertResult(0)(VNodeUtils.findRemovesCloseEnough(lhs, rhs).size)
116 | }
117 |
118 | it should "find 1 remove when the target is empty and source has 1 el" in {
119 | val lhs = Seq(text("t"))
120 | assertResult(1)(VNodeUtils.findRemovesCloseEnough(lhs, Seq.empty).size)
121 | }
122 |
123 | it should "find 1 remove when the target has 1 el different from the source" in {
124 | val rhs = Seq(text("tnew"))
125 | val lhs = Seq(text("told"))
126 | assertResult(1)(VNodeUtils.findRemovesCloseEnough(lhs, rhs).size)
127 | }
128 |
129 | "findAdds" should "find adds in easy vnode list" in {
130 | val rhs = Seq(text("t1"), text("t2"), text("t3"))
131 | val lhs = Seq(text("t1"), text("t3"))
132 | val r = VNodeUtils.findAddsCloseEnough(lhs, rhs)
133 | assertResult(1)(r.size)
134 | assertResult(1)(r(0))
135 | }
136 |
137 | }
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/backend/BackendSpec.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 |
19 | import scala.language._
20 | import org.scalatest._
21 | import scala.concurrent.Future
22 | import scala.concurrent.ExecutionContext.Implicits.global
23 | import scala.concurrent.duration._
24 | import java.util.concurrent.TimeUnit
25 | import java.util.concurrent.atomic.AtomicInteger
26 |
27 | /**
28 | * A base DOM backend that can be extended with specific components as needed.
29 | * This trait defines the context type.
30 | */
31 | trait TestBackend extends Backend {
32 | type This = TestBackend
33 | type Context = BasicContext
34 |
35 | protected[this] def createContext() = new BasicContext {}
36 | }
37 |
38 | // Could just do `new TestBackend {}`
39 | class MyTestBackend extends TestBackend
40 |
41 | /**
42 | * A test backend is used to test IOAction run funcitonality.
43 | */
44 | class BackendSpec extends AsyncFlatSpec with Matchers with OptionValues {
45 |
46 | val b = new MyTestBackend()
47 |
48 | "Backend" should "run a successful action" in {
49 | val action = Action.successful(true)
50 | val result = b.run(action)
51 | result.map { result =>
52 | result should be(true)
53 | }
54 | }
55 |
56 | it should "contain the exception when projecting with action.failed " in {
57 | val ex = new IllegalArgumentException("bad arg")
58 | val action = Action.failed(ex)
59 | recoverToSucceededIf[IllegalArgumentException] { b.run(action) }
60 | }
61 |
62 | it should "throw a NoSuchElement exception when the result in action.failed does not contain an exception" in {
63 | recoverToSucceededIf[NoSuchElementException] { b.run(Action.successful(10).failed) }
64 | }
65 |
66 | it should "return a future value" in {
67 | val f = Future(1)
68 | val action = Action.from(f)
69 | val result = b.run(action)
70 | result.map { r =>
71 | r should be(1)
72 | }
73 | }
74 |
75 | it should "map" in {
76 | val action = Action.successful(1).map(_ + 1)
77 | val result = b.run(action)
78 | result.map { r =>
79 | r should be(2)
80 | }
81 | }
82 |
83 | it should "flatMap" in {
84 | val action = Action.successful(1)
85 | val action2 = action.flatMap(n => Action.successful(n + 1))
86 | val result = b.run(action2)
87 | result.map { r =>
88 | r should be(2)
89 | }
90 | }
91 |
92 | it should "sequence actions, run them, return Unit" in {
93 | val r = new AtomicInteger(0)
94 | val actions = (1 to 10).map(n => ContextualAction {
95 | r.incrementAndGet()
96 | n
97 | })
98 | val result = b.run(Action.seq(actions: _*))
99 | result.map { res =>
100 | res should be(())
101 | }
102 | r.get should be(10)
103 |
104 | val actions2 = (1 to 10).map(n =>
105 | ContextualAction[Int, MyTestBackend] { ctx: MyTestBackend#Context =>
106 | r.incrementAndGet()
107 | n
108 | })
109 | val action2 = Action.seq(actions2: _*)
110 | val result2 = b.run(action2)
111 | result2.map { res =>
112 | res should be(())
113 | r.get should be(20)
114 | }
115 | }
116 |
117 | it should "allow easy extraction to a value" in {
118 | val x = Action.successful(Some(10))
119 | val y = x.map(_.get)
120 | val r = b.run(y)
121 | r.map { res =>
122 | res should be(10)
123 | }
124 |
125 | val extracted = x.flatMap { opt =>
126 | opt match {
127 | case Some(x) => Action.successful(x)
128 | case _ => Action.failed(new NoSuchElementException("no el"))
129 | }
130 | }
131 | b.run(extracted).map { res =>
132 | res should equal(10)
133 | }
134 | }
135 |
136 | it should "project using action.failed correctly" in {
137 | val x = Action.failed(new IllegalArgumentException("blah")).failed
138 | b.run(x).map { ex =>
139 | ex.getMessage should equal("blah")
140 | }
141 | }
142 |
143 | it should "project using action.failed correctly when there is no exception thrown" in {
144 | val x = Action.successful(10).failed
145 | recoverToSucceededIf[NoSuchElementException] {
146 | b.run(x)
147 | }
148 | }
149 |
150 | it should "always call finally if action was successful anyway" in {
151 | val counter = new AtomicInteger(0)
152 |
153 | val af = Action.successful(10).andFinally(Action.lift {
154 | counter.incrementAndGet()
155 | })
156 | b.run(af).map { res =>
157 | counter.get should equal(1)
158 | res should equal(10)
159 | }
160 | }
161 | it should "always call finally if action was failed" in {
162 | val counter = new AtomicInteger(0)
163 |
164 | val a2 = Action.failed(new IllegalArgumentException("blah"))
165 | val af2 = a2.andFinally(ContextualAction {
166 | counter.incrementAndGet()
167 | })
168 | recoverToSucceededIf[IllegalArgumentException] {
169 | val f = b.run(af2)
170 | counter.get should equal(1)
171 | f
172 | }
173 | }
174 |
175 | it should "propagate base action's failure when using finally" in {
176 | val counter = new AtomicInteger(0)
177 |
178 | val a: IOAction[Int] = Action.failed(new IllegalArgumentException("blah"))
179 | val af = a.andFinally(ContextualAction {
180 | counter.incrementAndGet()
181 | })
182 | val f = b.run(af)
183 | recoverToSucceededIf[IllegalArgumentException] {
184 | counter.get should be(1)
185 | f
186 | }
187 | }
188 |
189 | it should "call cleanup with None when successful" in {
190 | val counter = new AtomicInteger(0)
191 |
192 | val x = Action.successful(Some(10))
193 | val y = x.map(_.get)
194 |
195 | val cleanup = y.cleanUp { err: Option[Throwable] =>
196 | err match {
197 | case Some(t) =>
198 | fail("base action was successful so this should not be called")
199 | case _ =>
200 | counter.incrementAndGet() // should run
201 | Action.successful(-1) // should never see it!
202 | }
203 | }
204 | b.run(cleanup).map { res =>
205 | res should be(10)
206 | counter.get should be(1)
207 | }
208 | }
209 |
210 | it should "call cleanup with Some(ex) when failure occurs" in {
211 | val counter = new AtomicInteger(0)
212 | // Acually throw an exception, could use Action.failed to create this...
213 | val x = Action.lift { throw new IllegalArgumentException("blah") }
214 | val cleanup = x.cleanUp { err: Option[Throwable] =>
215 | err match {
216 | case Some(t) =>
217 | counter.incrementAndGet() // should run
218 | Action.successful(10)
219 | case None =>
220 | fail("base action was failed so this should not be called")
221 | }
222 | }
223 | // Small track, you need the projection, otherwise the exception would be thrown during run!
224 | b.run(cleanup.failed).map { ex =>
225 | counter.get should be(1)
226 | }
227 | }
228 |
229 | it should "run the cleanup action even if base action was successful" in {
230 | val counter = new AtomicInteger(0)
231 | val cleanup = Action.successful(10).cleanUp { err: Option[Throwable] =>
232 | err match {
233 | case Some(t) =>
234 | fail("base action was successful so this should not be called")
235 | case None =>
236 | Action.lift(counter.incrementAndGet)
237 | }
238 | }
239 | b.run(cleanup).map { res =>
240 | counter.get should be(1)
241 | }
242 | }
243 |
244 | it should "run the cleanup action even if base action fails" in {
245 | val counter = new AtomicInteger(0)
246 | val cleanup = Action.lift{ throw new IllegalArgumentException("blah")}.cleanUp { err: Option[Throwable] =>
247 | err match {
248 | case Some(t) =>
249 | Action.lift(counter.incrementAndGet)
250 | case None =>
251 | fail("base action was successful so this should not be called")
252 | }
253 | }
254 | b.run(cleanup.failed).map { res =>
255 | counter.get should be(1)
256 | }
257 | }
258 |
259 | it should "pass exception from base action to cleanup when keepFailure=true" in {
260 | // slight change in style, we actually throw the exception instead of using Action.failed
261 | val baseAction: IOAction[Int] = Action.lift { throw new IllegalArgumentException("ouch!") }
262 | val cleanup: IOAction[Int] = baseAction
263 | .cleanUp { err: Option[Throwable] =>
264 | err match {
265 | case Some(t) =>
266 | Action.failed(new NoSuchElementException())
267 | case _ =>
268 | fail("base action was failed so this should not be called")
269 | }
270 | }
271 | recoverToSucceededIf[IllegalArgumentException] {
272 | b.run(cleanup)
273 | }
274 | }
275 |
276 | it should "return cleanup action's exception if base fails, cleanUp fails and keepFailure=false" in {
277 | // lazy many style to create a failed action
278 | val baseAction: IOAction[Int] = Action.failed(new IllegalArgumentException("ouch!"))
279 | val cleanup: IOAction[Int] = baseAction.cleanUp({
280 | _ match {
281 | case Some(t) => Action.failed(new NoSuchElementException())
282 | case _ => fail("base action was failed so this should not be called")
283 | }
284 | }, false)
285 | recoverToSucceededIf[NoSuchElementException] { b.run(cleanup) }
286 | }
287 |
288 | }
289 |
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/backend/MarkupBackendSpec.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at9
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 |
19 | import scala.language._
20 | import org.scalatest._
21 | import scala.concurrent.Future
22 | import scala.concurrent.ExecutionContext.Implicits.global
23 | import scala.concurrent.duration._
24 | import java.util.concurrent.TimeUnit
25 | import java.util.concurrent.atomic.AtomicInteger
26 |
27 | /**
28 | * A test backend is used to test IOAction run funcitonality.
29 | */
30 | class MarkupBackendSpec extends AsyncFlatSpec with Matchers with OptionValues {
31 |
32 | import Utils._
33 | import VNode._
34 |
35 | val b = MarkupBackend
36 | import HTML5Attributes._
37 |
38 | "MarkupBackend" should "run a successful action" in {
39 | val action = Action.successful(true)
40 | val result = b.run(action)
41 | result.map { result =>
42 | result should be(true)
43 | }
44 | }
45 |
46 | it should "render a simple tag" in {
47 | val v = tag("div")
48 | b.run(b.render(v)).map { html =>
49 | norm(html) should be("")
50 | }
51 | }
52 |
53 | it should "render a tag with insane whitespace around it" in {
54 | val v = tag(" div \t\n ")
55 | b.run(b.render(v)).map { html =>
56 | norm(html) should be("")
57 | }
58 | }
59 |
60 | it should "render a tag with a string attribute" in {
61 | val v = tag("div", Seq(id := "foo"))
62 | b.run(b.render(v)).map { html =>
63 | norm(html) should be("""""")
64 | }
65 | }
66 |
67 | it should "render a tag with a string attribute and a text node" in {
68 | val v = tag("div", Seq(id := "foo"), text("FOO"))
69 | b.run(b.render(v)).map { html =>
70 | norm(html) should equal("""
FOO
""")
71 | }
72 | }
73 |
74 | it should "render a tage with child elements" in {
75 | val v = tag("div", tag("p", text("foo")), tag("div", text("bar")))
76 | b.run(b.render(v)).map { html =>
77 | norm(html) should equal("""
FOO
BAR
""")
78 | }
79 | }
80 |
81 | it should "omit a closing tag for br" in {
82 | val v = tag("br")
83 | b.run(b.render(v)).map { html =>
84 | norm(html) should equal(""" """)
85 | }
86 | }
87 |
88 | it should "not render an HasBooleanValue=true attribute when the attribute is false" in {
89 | val v = tag("div", Seq(disabled := false))
90 | b.run(b.render(v)).map { html =>
91 | norm(html) should equal("""""")
92 | }
93 | }
94 |
95 | // Not sure this should render to ="" or to just the name of the attribute per HTML5 spec
96 | it should "render a HasBooleanValue=true attribute when the attribute is true" in {
97 | val v = tag("div", Seq(disabled := true))
98 | b.run(b.render(v)).map { html =>
99 | norm(html) should equal("""""")
100 | }
101 | }
102 |
103 | it should "render a simple style" in {
104 | val v = tag("div", Seq(Styles.height := 200))
105 | b.run(b.render(v)).map { html =>
106 | norm(html) should equal ("""""")
107 | }
108 | }
109 |
110 | it should "render multple styles" in {
111 | val v = tag("div", Seq(Styles.height := 200, Styles.width := 400, Styles.zIndex := 3))
112 | b.run(b.render(v)).map { html =>
113 | norm(html) should equal ("""""")
114 | }
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/shared/src/test/scala/im/vdom/backend/UtilsSpec.scala:
--------------------------------------------------------------------------------
1 | /* Copyright 2015 Devon Miller
2 | *
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at9
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 | package im
16 | package vdom
17 | package backend
18 |
19 | import scala.language._
20 | import org.scalatest._
21 | import scala.concurrent.Future
22 | import scala.concurrent.ExecutionContext.Implicits.global
23 | import scala.concurrent.duration._
24 | import java.util.concurrent.TimeUnit
25 | import java.util.concurrent.atomic.AtomicInteger
26 |
27 | /**
28 | * Utils test.
29 | */
30 | class UtilsSpec extends FlatSpec with Matchers with OptionValues {
31 |
32 | import Utils._
33 | import collection.immutable.BitSet
34 | import im.vdom.backend.Hints;
35 |
36 | "norm" should "should not change an already normed string" in {
37 | norm("
") should equal ("
")
38 | }
39 |
40 | it should "norm an empty string to an empty string" in {
41 | norm("") should equal("")
42 | }
43 |
44 | it should "remove leading and trailing space" in {
45 | norm(" a \n") should equal ("A")
46 | }
47 |
48 | it should "remove repeated whitespace" in {
49 | norm(" a\n b ") should equal ("A B")
50 | }
51 |
52 | it should "norm a trailing space before a non-alpha character" in {
53 | norm("a >") should equal ("A>")
54 | }
55 |
56 | it should "norm a crazy div" in {
57 | norm(" < div \n>
\n") should equal ("")
58 | }
59 |
60 | it should "norm text node correctly" in {
61 | norm("
this is \na text block
") should equal ("
THIS IS A TEXT BLOCK
")
62 | }
63 |
64 | "hyphenate" should "convert camel cased to hyphens" in {
65 | hyphenate("blahHah") should equal ("blah-hah")
66 | }
67 |
68 | it should "work on non camel cased names" in {
69 | hyphenate("blah") should equal ("blah")
70 | }
71 |
72 | it should "remove leading and trailing spaces" in {
73 | hyphenate("blahHah") should equal ("blah-hah")
74 | }
75 |
76 | "processStyleName" should "camel case the name" in {
77 | processStyleName("blahHah") should equal ("blah-hah")
78 | }
79 |
80 | it should "remove leading trailing spaces" in {
81 | processStyleName(" \nblahHah \n\t") should equal ("blah-hah")
82 | }
83 |
84 | it should "process the special mozilla prefix special" in {
85 | processStyleName(" \n msBlahHah") should equal ("-ms-blah-hah")
86 | }
87 |
88 | "quoteValueForStyle" should "return empty string for a boolean" in {
89 | quoteStyleValueForBrowser(None, true) should equal ("")
90 | }
91 |
92 | it should "return empty string for a null value" in {
93 | quoteStyleValueForBrowser(None, null) should equal ("")
94 | }
95 |
96 | it should "return a unitless string if the hint says its unitless" in {
97 | quoteStyleValueForBrowser(Some(StyleHint(values = BitSet(Hints.Unitless))), "200") should equal ("200")
98 | }
99 |
100 | it should "return a string with units if the hint says its not unitless" in {
101 | quoteStyleValueForBrowser(None, "200") should equal ("200px")
102 | }
103 |
104 | "adler32" should "calculate a checksum" in {
105 | val checksum = adler32("
blah is cool
")
106 | //println(s"$checksum")
107 | checksum should be (1628178442)
108 | }
109 |
110 | it should "add it to markup correctly" in {
111 | val markup= addChecksumToMarkup("