git ssb

1+

Matt McKegg / mutant



Tree: ee6233e21fbc30daa9c23e3a3fdf43734c522096

Files: ee6233e21fbc30daa9c23e3a3fdf43734c522096 / html-element.js

6861 bytesRaw
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 invalidateNextTick = require('./lib/invalidate-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))) {
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 return node
54}
55
56function appendChild (document, target, data, node) {
57 if (Array.isArray(node)) {
58 node.forEach(function (child) {
59 appendChild(document, target, data, child)
60 })
61 } else if (isObservable(node)) {
62 var nodes = getNodes(document, resolve(node))
63 nodes.forEach(append, { target: target, document: document })
64 data.targets.set(node, nodes)
65 data.bindings.push(new Binding(document, node, data))
66 } else {
67 node = getNode(document, node)
68 target.appendChild(node)
69 if (getRootNode(target) === document) {
70 walk(node, rebind)
71 }
72 }
73}
74
75function append (child) {
76 this.target.appendChild(child)
77 maybeBind(child, this)
78}
79
80function maybeBind (node, opts) {
81 bindQueue.push([node, opts])
82 if (!currentlyBinding) {
83 currentlyBinding = true
84 setImmediate(flushBindQueue)
85 }
86}
87
88function flushBindQueue () {
89 currentlyBinding = false
90 while (bindQueue.length) {
91 var item = bindQueue.shift()
92 var node = item[0]
93 var opts = item[1]
94 if (getRootNode(opts.target) === opts.document) {
95 walk(node, rebind)
96 }
97 }
98}
99
100function checkWatcher (document) {
101 if (!watcher && global.MutationObserver) {
102 watcher = new global.MutationObserver(onMutate)
103 watcher.observe(document, {subtree: true, childList: true})
104 }
105}
106
107function onMutate (changes) {
108 changes.forEach(handleChange)
109}
110
111function getRootNode (el) {
112 var element = el
113 while (element.parentNode) {
114 element = element.parentNode
115 }
116 return element
117}
118
119function handleChange (change) {
120 for (var i = 0; i < change.addedNodes.length; i++) {
121 // if parent is a mutant element, then safe to assume it has already been bound
122 var node = change.addedNodes[i]
123 if (!caches.has(node.parentNode)) {
124 walk(node, rebind)
125 }
126 }
127 for (var i = 0; i < change.removedNodes.length; i++) {
128 // if has already been unbound, safe to assume children have also
129 var node = change.removedNodes[i]
130 var data = caches.get(node)
131 if (data && data.bound) {
132 walk(node, unbind)
133 }
134 }
135}
136
137function indexOf (target, item) {
138 return Array.prototype.indexOf.call(target, item)
139}
140
141function replace (oldNodes, newNodes) {
142 var parent = oldNodes[oldNodes.length - 1].parentNode
143 var nodes = parent.childNodes
144 var startIndex = indexOf(nodes, oldNodes[0])
145
146 // avoid reinserting nodes that are already in correct position!
147 for (var i = 0; i < newNodes.length; i++) {
148 if (nodes[i + startIndex] === newNodes[i]) {
149 continue
150 } else if (nodes[i + startIndex + 1] === newNodes[i]) {
151 parent.removeChild(nodes[i + startIndex])
152 continue
153 } else if (nodes[i + startIndex] === newNodes[i + 1] && newNodes[i + 1]) {
154 parent.insertBefore(newNodes[i], nodes[i + startIndex])
155 } else if (nodes[i + startIndex]) {
156 parent.insertBefore(newNodes[i], nodes[i + startIndex])
157 } else {
158 parent.appendChild(newNodes[i])
159 }
160 walk(newNodes[i], rebind)
161 }
162
163 oldNodes.filter(function (node) {
164 return !~newNodes.indexOf(node)
165 }).forEach(function (node) {
166 if (node.parentNode) {
167 parent.removeChild(node)
168 }
169 walk(node, unbind)
170 })
171}
172
173function isText (value) {
174 return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
175}
176
177function getNode (document, nodeOrText) {
178 if (nodeOrText == null) {
179 return document.createTextNode('')
180 } else if (isText(nodeOrText)) {
181 return document.createTextNode(nodeOrText.toString())
182 } else {
183 return nodeOrText
184 }
185}
186
187function getNodes (document, nodeOrNodes) {
188 if (Array.isArray(nodeOrNodes)) {
189 if (nodeOrNodes.length) {
190 var result = []
191 for (var i = 0; i < nodeOrNodes.length; i++) {
192 var item = nodeOrNodes[i]
193 if (Array.isArray(item)) {
194 getNodes(document, item).forEach(push, result)
195 } else {
196 result.push(getNode(document, item))
197 }
198 }
199 return result.map(getNode.bind(this, document))
200 } else {
201 return [getNode(document, null)]
202 }
203 } else {
204 return [getNode(document, nodeOrNodes)]
205 }
206}
207
208function rebind (node) {
209 if (node.nodeType === 1) {
210 var data = caches.get(node)
211 if (data) {
212 data.bindings.forEach(invokeBind)
213 }
214 }
215}
216
217function unbind (node) {
218 if (node.nodeType === 1) {
219 var data = caches.get(node)
220 if (data) {
221 data.bindings.forEach(invokeUnbind)
222 }
223 }
224}
225
226function invokeBind (binding) {
227 binding.bind()
228}
229
230function invokeUnbind (binding) {
231 binding.unbind()
232}
233
234function push (item) {
235 this.push(item)
236}
237
238function resolve (source) {
239 return typeof source === 'function' ? source() : source
240}
241
242function Binding (document, obs, data) {
243 this.document = document
244 this.obs = obs
245 this.data = data
246 this.bound = false
247 this.invalidated = false
248 this.update = function (value) {
249 var oldNodes = data.targets.get(obs)
250 var newNodes = getNodes(document, value)
251 if (oldNodes) {
252 replace(oldNodes, newNodes)
253 data.targets.set(obs, newNodes)
254 }
255 }
256 invalidateNextTick(this)
257}
258
259Binding.prototype = {
260 bind: function () {
261 if (!this.bound) {
262 this._release = this.invalidated
263 ? watch(this.obs, this.update)
264 : this.obs(this.update)
265 this.invalidated = false
266 this.bound = true
267 }
268 },
269 unbind: function () {
270 if (this.bound && typeof this._release === 'function') {
271 this._release()
272 this._release = null
273 this.bound = false
274 invalidateNextTick(this)
275 }
276 }
277}
278

Built with git-ssb-web