git ssb

1+

Matt McKegg / mutant



Tree: 3bfb6e65a932b5fb11b3c2260c7582699eb17ae5

Files: 3bfb6e65a932b5fb11b3c2260c7582699eb17ae5 / html-element.js

5515 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 replace (oldNodes, newNodes) {
116 // TODO: optmize to not reinsert nodes that are already in correct position!
117 var parent = oldNodes[oldNodes.length - 1].parentNode
118 var marker = oldNodes[oldNodes.length - 1].nextSibling
119 oldNodes.filter(function (node) {
120 return !~newNodes.indexOf(node)
121 }).forEach(function (node) {
122 parent.removeChild(node)
123 walk(node, unbind)
124 })
125 if (marker) {
126 newNodes.forEach(function (node) {
127 parent.insertBefore(node, marker)
128 walk(node, rebind)
129 })
130 } else {
131 newNodes.forEach(function (node) {
132 parent.appendChild(node)
133 walk(node, rebind)
134 })
135 }
136}
137
138function isText (value) {
139 return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
140}
141
142function getNode (document, nodeOrText) {
143 if (nodeOrText == null) {
144 return document.createTextNode('')
145 } else if (isText(nodeOrText)) {
146 return document.createTextNode(nodeOrText.toString())
147 } else {
148 return nodeOrText
149 }
150}
151
152function getNodes (document, nodeOrNodes) {
153 if (Array.isArray(nodeOrNodes)) {
154 if (nodeOrNodes.length) {
155 var result = []
156 nodeOrNodes.forEach(function (item) {
157 if (Array.isArray(item)) {
158 getNodes(document, item).forEach(push, result)
159 } else {
160 result.push(getNode(document, item))
161 }
162 })
163 return result.map(getNode.bind(this, document))
164 } else {
165 return [getNode(document, null)]
166 }
167 } else {
168 return [getNode(document, nodeOrNodes)]
169 }
170}
171
172function rebind (node) {
173 if (node.nodeType === 1) {
174 var data = caches.get(node)
175 if (data) {
176 data.bindings.forEach(invokeBind)
177 }
178 }
179}
180
181function unbind (node) {
182 if (node.nodeType === 1) {
183 var data = caches.get(node)
184 if (data) {
185 data.bindings.forEach(invokeUnbind)
186 }
187 }
188}
189
190function invokeBind (binding) {
191 binding.bind()
192}
193
194function invokeUnbind (binding) {
195 binding.unbind()
196}
197
198function push (item) {
199 this.push(item)
200}
201
202function resolve (source) {
203 return typeof source === 'function' ? source() : source
204}
205
206function Binding (fn, document, obs, data) {
207 this.document = document
208 this.obs = obs
209 this.data = data
210 this.fn = fn
211 this.bound = false
212}
213
214Binding.prototype = {
215 bind: function () {
216 if (!this.bound) {
217 this._release = this.fn(this.document, this.obs, this.data)
218 this.bound = true
219 }
220 },
221 unbind: function () {
222 if (this.bound && typeof this._release === 'function') {
223 this._release()
224 this._release = null
225 this.bound = false
226 }
227 }
228}
229

Built with git-ssb-web