git ssb

1+

Matt McKegg / mutant



Tree: 07b9ddc6b2950f50f5820053fa89145a5d96e1f2

Files: 07b9ddc6b2950f50f5820053fa89145a5d96e1f2 / html-element.js

6721 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 var node = change.removedNodes[i]
129 walk(node, unbind)
130 }
131}
132
133function indexOf (target, item) {
134 return Array.prototype.indexOf.call(target, item)
135}
136
137function replace (oldNodes, newNodes) {
138 var parent = oldNodes[oldNodes.length - 1].parentNode
139 var nodes = parent.childNodes
140 var startIndex = indexOf(nodes, oldNodes[0])
141
142 // avoid reinserting nodes that are already in correct position!
143 for (var i = 0; i < newNodes.length; i++) {
144 if (nodes[i + startIndex] === newNodes[i]) {
145 continue
146 } else if (nodes[i + startIndex + 1] === newNodes[i]) {
147 parent.removeChild(nodes[i + startIndex])
148 continue
149 } else if (nodes[i + startIndex] === newNodes[i + 1] && newNodes[i + 1]) {
150 parent.insertBefore(newNodes[i], nodes[i + startIndex])
151 } else if (nodes[i + startIndex]) {
152 parent.insertBefore(newNodes[i], nodes[i + startIndex])
153 } else {
154 parent.appendChild(newNodes[i])
155 }
156 walk(newNodes[i], rebind)
157 }
158
159 oldNodes.filter(function (node) {
160 return !~newNodes.indexOf(node)
161 }).forEach(function (node) {
162 if (node.parentNode) {
163 parent.removeChild(node)
164 }
165 walk(node, unbind)
166 })
167}
168
169function isText (value) {
170 return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
171}
172
173function getNode (document, nodeOrText) {
174 if (nodeOrText == null) {
175 return document.createTextNode('')
176 } else if (isText(nodeOrText)) {
177 return document.createTextNode(nodeOrText.toString())
178 } else {
179 return nodeOrText
180 }
181}
182
183function getNodes (document, nodeOrNodes) {
184 if (Array.isArray(nodeOrNodes)) {
185 if (nodeOrNodes.length) {
186 var result = []
187 for (var i = 0; i < nodeOrNodes.length; i++) {
188 var item = nodeOrNodes[i]
189 if (Array.isArray(item)) {
190 getNodes(document, item).forEach(push, result)
191 } else {
192 result.push(getNode(document, item))
193 }
194 }
195 return result.map(getNode.bind(this, document))
196 } else {
197 return [getNode(document, null)]
198 }
199 } else {
200 return [getNode(document, nodeOrNodes)]
201 }
202}
203
204function rebind (node) {
205 if (node.nodeType === 1) {
206 var data = caches.get(node)
207 if (data) {
208 data.bindings.forEach(invokeBind)
209 }
210 }
211}
212
213function unbind (node) {
214 if (node.nodeType === 1) {
215 var data = caches.get(node)
216 if (data) {
217 data.bindings.forEach(invokeUnbind)
218 }
219 }
220}
221
222function invokeBind (binding) {
223 binding.bind()
224}
225
226function invokeUnbind (binding) {
227 binding.unbind()
228}
229
230function push (item) {
231 this.push(item)
232}
233
234function resolve (source) {
235 return typeof source === 'function' ? source() : source
236}
237
238function Binding (document, obs, data) {
239 this.document = document
240 this.obs = obs
241 this.data = data
242 this.bound = false
243 this.invalidated = false
244 this.update = function (value) {
245 var oldNodes = data.targets.get(obs)
246 var newNodes = getNodes(document, value)
247 if (oldNodes) {
248 replace(oldNodes, newNodes)
249 data.targets.set(obs, newNodes)
250 }
251 }
252 invalidateNextTick(this)
253}
254
255Binding.prototype = {
256 bind: function () {
257 if (!this.bound) {
258 this._release = this.invalidated
259 ? watch(this.obs, this.update)
260 : this.obs(this.update)
261 this.invalidated = false
262 this.bound = true
263 }
264 },
265 unbind: function () {
266 if (this.bound && typeof this._release === 'function') {
267 this._release()
268 this._release = null
269 this.bound = false
270 invalidateNextTick(this)
271 }
272 }
273}
274

Built with git-ssb-web