git ssb

1+

Matt McKegg / mutant



Tree: 5be49af9cc03aa1519439c1576fd40202cf243eb

Files: 5be49af9cc03aa1519439c1576fd40202cf243eb / html-element.js

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

Built with git-ssb-web