git ssb

1+

Matt McKegg / mutant



Tree: b588b1cbc0c0e85d0288a7fa484660bf8225a915

Files: b588b1cbc0c0e85d0288a7fa484660bf8225a915 / html-element.js

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

Built with git-ssb-web