1 | var applyProperties = require('./lib/apply-properties')
|
2 | var isObservable = require('./is-observable')
|
3 | var parseTag = require('./lib/parse-tag')
|
4 | var walk = require('./lib/walk')
|
5 | var watch = require('./watch')
|
6 | var caches = new global.WeakMap()
|
7 | var bindQueue = []
|
8 | var currentlyBinding = false
|
9 | var watcher = null
|
10 | var releaseNextTick = require('./lib/release-next-tick')
|
11 |
|
12 | module.exports = function (tag, attributes, children) {
|
13 | return Element(global.document, null, tag, attributes, children)
|
14 | }
|
15 |
|
16 | module.exports.forDocument = function (document, namespace) {
|
17 | return Element.bind(this, document, namespace)
|
18 | }
|
19 |
|
20 | function Element (document, namespace, tagName, properties, children) {
|
21 | if (!children && (Array.isArray(properties) || isText(properties) || isNode(properties) || isObservable(properties))) {
|
22 | children = properties
|
23 | properties = null
|
24 | }
|
25 |
|
26 | checkWatcher(document)
|
27 | properties = properties || {}
|
28 |
|
29 | var tag = parseTag(tagName, properties, namespace)
|
30 | var node = namespace
|
31 | ? document.createElementNS(namespace, tag.tagName)
|
32 | : document.createElement(tag.tagName)
|
33 |
|
34 | if (tag.id) {
|
35 | node.id = tag.id
|
36 | }
|
37 |
|
38 | if (tag.classes && tag.classes.length) {
|
39 | node.className = tag.classes.join(' ')
|
40 | }
|
41 |
|
42 | var data = {
|
43 | targets: new Map(),
|
44 | bindings: []
|
45 | }
|
46 |
|
47 | caches.set(node, data)
|
48 | applyProperties(node, properties, data)
|
49 | if (children != null) {
|
50 | appendChild(document, node, data, children)
|
51 | }
|
52 |
|
53 | maybeBind(document, node)
|
54 | return node
|
55 | }
|
56 |
|
57 | function appendChild (document, target, data, node) {
|
58 | if (Array.isArray(node)) {
|
59 | node.forEach(function (child) {
|
60 | appendChild(document, target, data, child)
|
61 | })
|
62 | } else if (isObservable(node)) {
|
63 | var nodes = getNodes(document, resolve(node))
|
64 | nodes.forEach(append, { target: target, document: document })
|
65 | data.targets.set(node, nodes)
|
66 | data.bindings.push(new Binding(document, node, data))
|
67 | } else {
|
68 | node = getNode(document, node)
|
69 | target.appendChild(node)
|
70 | if (getRootNode(node) === document) {
|
71 | walk(node, rebind)
|
72 | }
|
73 | }
|
74 | }
|
75 |
|
76 | function append (child) {
|
77 | this.target.appendChild(child)
|
78 | if (getRootNode(child) === this.document) {
|
79 | walk(child, rebind)
|
80 | }
|
81 | }
|
82 |
|
83 | function maybeBind (document, node) {
|
84 | bindQueue.push([document, node])
|
85 | if (!currentlyBinding) {
|
86 | currentlyBinding = true
|
87 | setImmediate(flushBindQueue)
|
88 | }
|
89 | }
|
90 |
|
91 | function flushBindQueue () {
|
92 | currentlyBinding = false
|
93 | while (bindQueue.length) {
|
94 | var item = bindQueue.shift()
|
95 | var document = item[0]
|
96 | var node = item[1]
|
97 | if (getRootNode(node) === document) {
|
98 | walk(node, rebind)
|
99 | }
|
100 | }
|
101 | }
|
102 |
|
103 | function checkWatcher (document) {
|
104 | if (!watcher && global.MutationObserver) {
|
105 | watcher = new global.MutationObserver(onMutate)
|
106 | watcher.observe(document, {subtree: true, childList: true})
|
107 | }
|
108 | }
|
109 |
|
110 | function onMutate (changes) {
|
111 | changes.forEach(handleChange)
|
112 | }
|
113 |
|
114 | function getRootNode (el) {
|
115 | var element = el
|
116 | while (element.parentNode) {
|
117 | element = element.parentNode
|
118 | }
|
119 | return element
|
120 | }
|
121 |
|
122 | function handleChange (change) {
|
123 | for (var i = 0; i < change.addedNodes.length; i++) {
|
124 |
|
125 | var node = change.addedNodes[i]
|
126 | if (!caches.has(node.parentNode)) {
|
127 | walk(node, rebind)
|
128 | }
|
129 | }
|
130 | for (var i = 0; i < change.removedNodes.length; i++) {
|
131 | var node = change.removedNodes[i]
|
132 | walk(node, unbind)
|
133 | }
|
134 | }
|
135 |
|
136 | function indexOf (target, item) {
|
137 | return Array.prototype.indexOf.call(target, item)
|
138 | }
|
139 |
|
140 | function replace (oldNodes, newNodes) {
|
141 | var parent = oldNodes[oldNodes.length - 1].parentNode
|
142 | var nodes = parent.childNodes
|
143 | var startIndex = indexOf(nodes, oldNodes[0])
|
144 |
|
145 |
|
146 | for (var i = 0; i < newNodes.length; i++) {
|
147 | if (nodes[i + startIndex] === newNodes[i]) {
|
148 | continue
|
149 | } else if (nodes[i + startIndex + 1] === newNodes[i]) {
|
150 | parent.removeChild(nodes[i + startIndex])
|
151 | continue
|
152 | } else if (nodes[i + startIndex] === newNodes[i + 1] && newNodes[i + 1]) {
|
153 | parent.insertBefore(newNodes[i], nodes[i + startIndex])
|
154 | } else if (nodes[i + startIndex]) {
|
155 | parent.insertBefore(newNodes[i], nodes[i + startIndex])
|
156 | } else {
|
157 | parent.appendChild(newNodes[i])
|
158 | }
|
159 | walk(newNodes[i], rebind)
|
160 | }
|
161 |
|
162 | oldNodes.filter(function (node) {
|
163 | return !~newNodes.indexOf(node)
|
164 | }).forEach(function (node) {
|
165 | if (node.parentNode) {
|
166 | parent.removeChild(node)
|
167 | }
|
168 | walk(node, unbind)
|
169 | })
|
170 | }
|
171 |
|
172 | function isText (value) {
|
173 | return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
174 | }
|
175 |
|
176 | function isNode (value) {
|
177 |
|
178 | return value instanceof global.Node || (global.HTMLElement && value instanceof global.HTMLElement)
|
179 | }
|
180 |
|
181 | function getNode (document, nodeOrText) {
|
182 | if (nodeOrText == null) {
|
183 | return document.createTextNode('')
|
184 | } else if (isText(nodeOrText)) {
|
185 | return document.createTextNode(nodeOrText.toString())
|
186 | } else {
|
187 | return nodeOrText
|
188 | }
|
189 | }
|
190 |
|
191 | function getNodes (document, nodeOrNodes) {
|
192 | if (Array.isArray(nodeOrNodes)) {
|
193 | if (nodeOrNodes.length) {
|
194 | var result = []
|
195 | for (var i = 0; i < nodeOrNodes.length; i++) {
|
196 | var item = nodeOrNodes[i]
|
197 | if (Array.isArray(item)) {
|
198 | getNodes(document, item).forEach(push, result)
|
199 | } else {
|
200 | result.push(getNode(document, item))
|
201 | }
|
202 | }
|
203 | return result.map(getNode.bind(this, document))
|
204 | } else {
|
205 | return [getNode(document, null)]
|
206 | }
|
207 | } else {
|
208 | return [getNode(document, nodeOrNodes)]
|
209 | }
|
210 | }
|
211 |
|
212 | function rebind (node) {
|
213 | if (node.nodeType === 1) {
|
214 | var data = caches.get(node)
|
215 | if (data) {
|
216 | data.bindings.forEach(invokeBind)
|
217 | }
|
218 | }
|
219 | }
|
220 |
|
221 | function unbind (node) {
|
222 | if (node.nodeType === 1) {
|
223 | var data = caches.get(node)
|
224 | if (data) {
|
225 | data.bindings.forEach(invokeUnbind)
|
226 | }
|
227 | }
|
228 | }
|
229 |
|
230 | function invokeBind (binding) {
|
231 | binding.bind()
|
232 | }
|
233 |
|
234 | function invokeUnbind (binding) {
|
235 | binding.unbind()
|
236 | }
|
237 |
|
238 | function push (item) {
|
239 | this.push(item)
|
240 | }
|
241 |
|
242 | function resolve (source) {
|
243 | return typeof source === 'function' ? source() : source
|
244 | }
|
245 |
|
246 | function Binding (document, obs, data) {
|
247 | this.document = document
|
248 | this.obs = obs
|
249 | this.data = data
|
250 | this.bound = false
|
251 |
|
252 | this.update = function (value) {
|
253 | var oldNodes = data.targets.get(obs)
|
254 | var newNodes = getNodes(document, value)
|
255 | if (oldNodes) {
|
256 | replace(oldNodes, newNodes)
|
257 | data.targets.set(obs, newNodes)
|
258 | }
|
259 | }
|
260 |
|
261 |
|
262 | this.release = obs(this.update)
|
263 | releaseNextTick(this)
|
264 | }
|
265 |
|
266 | Binding.prototype = {
|
267 | bind: function () {
|
268 | if (!this.bound) {
|
269 | if (!this.release) {
|
270 | this.release = watch(this.obs, this.update)
|
271 | }
|
272 | this.bound = true
|
273 | }
|
274 | },
|
275 | unbind: function () {
|
276 | if (this.bound) {
|
277 | this.bound = false
|
278 | releaseNextTick(this)
|
279 | }
|
280 | }
|
281 | }
|