git ssb

1+

Matt McKegg / mutant



Tree: 0d72eea7c7d75e11ba0f148f6a9db9db0eeccdeb

Files: 0d72eea7c7d75e11ba0f148f6a9db9db0eeccdeb / html-element.js

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

Built with git-ssb-web