git ssb

1+

Matt McKegg / mutant



Tree: 65803d088ea063bf6463b25ec11c25f754196413

Files: 65803d088ea063bf6463b25ec11c25f754196413 / html-element.js

6878 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) || isNode(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 return value instanceof Node
178}
179
180function getNode (document, nodeOrText) {
181 if (nodeOrText == null) {
182 return document.createTextNode('')
183 } else if (isText(nodeOrText)) {
184 return document.createTextNode(nodeOrText.toString())
185 } else {
186 return nodeOrText
187 }
188}
189
190function getNodes (document, nodeOrNodes) {
191 if (Array.isArray(nodeOrNodes)) {
192 if (nodeOrNodes.length) {
193 var result = []
194 for (var i = 0; i < nodeOrNodes.length; i++) {
195 var item = nodeOrNodes[i]
196 if (Array.isArray(item)) {
197 getNodes(document, item).forEach(push, result)
198 } else {
199 result.push(getNode(document, item))
200 }
201 }
202 return result.map(getNode.bind(this, document))
203 } else {
204 return [getNode(document, null)]
205 }
206 } else {
207 return [getNode(document, nodeOrNodes)]
208 }
209}
210
211function rebind (node) {
212 if (node.nodeType === 1) {
213 var data = caches.get(node)
214 if (data) {
215 data.bindings.forEach(invokeBind)
216 }
217 }
218}
219
220function unbind (node) {
221 if (node.nodeType === 1) {
222 var data = caches.get(node)
223 if (data) {
224 data.bindings.forEach(invokeUnbind)
225 }
226 }
227}
228
229function invokeBind (binding) {
230 binding.bind()
231}
232
233function invokeUnbind (binding) {
234 binding.unbind()
235}
236
237function push (item) {
238 this.push(item)
239}
240
241function resolve (source) {
242 return typeof source === 'function' ? source() : source
243}
244
245function Binding (document, obs, data) {
246 this.document = document
247 this.obs = obs
248 this.data = data
249 this.bound = false
250 this.invalidated = false
251 this.update = function (value) {
252 var oldNodes = data.targets.get(obs)
253 var newNodes = getNodes(document, value)
254 if (oldNodes) {
255 replace(oldNodes, newNodes)
256 data.targets.set(obs, newNodes)
257 }
258 }
259 invalidateNextTick(this)
260}
261
262Binding.prototype = {
263 bind: function () {
264 if (!this.bound) {
265 this._release = this.invalidated
266 ? watch(this.obs, this.update)
267 : this.obs(this.update)
268 this.invalidated = false
269 this.bound = true
270 }
271 },
272 unbind: function () {
273 if (this.bound && typeof this._release === 'function') {
274 this._release()
275 this._release = null
276 this.bound = false
277 invalidateNextTick(this)
278 }
279 }
280}
281

Built with git-ssb-web