git ssb

1+

Matt McKegg / mutant



Tree: bc685ac97855accf26bcdf7168da942ffcdccd1a

Files: bc685ac97855accf26bcdf7168da942ffcdccd1a / html-element.js

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

Built with git-ssb-web