UNPKG

6.96 kBJavaScriptView Raw
1var applyProperties = require('./lib/apply-properties')
2var isObservable = require('./is-observable')
3var parseTag = require('./lib/parse-tag')
4var walk = require('./lib/walk')
5var watch = require('./watch')
6var caches = new global.WeakMap()
7var bindQueue = []
8var currentlyBinding = false
9var watcher = null
10var releaseNextTick = require('./lib/release-next-tick')
11
12module.exports = function (tag, attributes, children) {
13 return Element(global.document, null, tag, attributes, children)
14}
15
16module.exports.forDocument = function (document, namespace) {
17 return Element.bind(this, document, namespace)
18}
19
20function 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
57function 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
76function append (child) {
77 this.target.appendChild(child)
78 if (getRootNode(child) === this.document) {
79 walk(child, rebind)
80 }
81}
82
83function maybeBind (document, node) {
84 bindQueue.push([document, node])
85 if (!currentlyBinding) {
86 currentlyBinding = true
87 setImmediate(flushBindQueue)
88 }
89}
90
91function 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
103function checkWatcher (document) {
104 if (!watcher && global.MutationObserver) {
105 watcher = new global.MutationObserver(onMutate)
106 watcher.observe(document, {subtree: true, childList: true})
107 }
108}
109
110function onMutate (changes) {
111 changes.forEach(handleChange)
112}
113
114function getRootNode (el) {
115 var element = el
116 while (element.parentNode) {
117 element = element.parentNode
118 }
119 return element
120}
121
122function handleChange (change) {
123 for (var i = 0; i < change.addedNodes.length; i++) {
124 // if parent is a mutant element, then safe to assume it has already been bound
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
136function indexOf (target, item) {
137 return Array.prototype.indexOf.call(target, item)
138}
139
140function 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 // avoid reinserting nodes that are already in correct position!
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
172function isText (value) {
173 return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
174}
175
176function isNode (value) {
177 // for some reason, img elements are not instances of Node
178 return value instanceof global.Node || (global.HTMLElement && value instanceof global.HTMLElement)
179}
180
181function 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
191function 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
212function 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
221function 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
230function invokeBind (binding) {
231 binding.bind()
232}
233
234function invokeUnbind (binding) {
235 binding.unbind()
236}
237
238function push (item) {
239 this.push(item)
240}
241
242function resolve (source) {
243 return typeof source === 'function' ? source() : source
244}
245
246function 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 // listen immediately, but if not bound before next tick, release
262 this.release = obs(this.update)
263 releaseNextTick(this)
264}
265
266Binding.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}