Files: bc685ac97855accf26bcdf7168da942ffcdccd1a / html-element.js
6553 bytesRaw
1 | var applyProperties = require('./lib/apply-properties') |
2 | var isObservable = require('./is-observable') |
3 | var parseTag = require('./lib/parse-tag') |
4 | var walk = require('./lib/walk') |
5 | var watch = require('./watch') |
6 | var caches = new global.WeakMap() |
7 | var watcher = null |
8 | var invalidateNextTick = require('./lib/invalidate-next-tick') |
9 | |
10 | module.exports = function (tag, attributes, children) { |
11 | return Element(global.document, null, tag, attributes, children) |
12 | } |
13 | |
14 | module.exports.forDocument = function (document, namespace) { |
15 | return Element.bind(this, document, namespace) |
16 | } |
17 | |
18 | function 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 | |
54 | function 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 | |
73 | function append (child) { |
74 | this.target.appendChild(child) |
75 | maybeBind(child, this) |
76 | } |
77 | |
78 | function maybeBind (node, opts) { |
79 | setImmediate(function () { |
80 | if (getRootNode(opts.target) === opts.document) { |
81 | walk(node, rebind) |
82 | } |
83 | }) |
84 | } |
85 | |
86 | function checkWatcher (document) { |
87 | if (!watcher && global.MutationObserver) { |
88 | watcher = new global.MutationObserver(onMutate) |
89 | watcher.observe(document, {subtree: true, childList: true}) |
90 | } |
91 | } |
92 | |
93 | function onMutate (changes) { |
94 | changes.forEach(handleChange) |
95 | } |
96 | |
97 | function getRootNode (el) { |
98 | var element = el |
99 | while (element.parentNode) { |
100 | element = element.parentNode |
101 | } |
102 | return element |
103 | } |
104 | |
105 | function 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 | |
123 | function indexOf (target, item) { |
124 | return Array.prototype.indexOf.call(target, item) |
125 | } |
126 | |
127 | function 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 | |
159 | function isText (value) { |
160 | return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' |
161 | } |
162 | |
163 | function 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 | |
173 | function 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 | |
194 | function 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 | |
203 | function 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 | |
212 | function invokeBind (binding) { |
213 | binding.bind() |
214 | } |
215 | |
216 | function invokeUnbind (binding) { |
217 | binding.unbind() |
218 | } |
219 | |
220 | function push (item) { |
221 | this.push(item) |
222 | } |
223 | |
224 | function resolve (source) { |
225 | return typeof source === 'function' ? source() : source |
226 | } |
227 | |
228 | function 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 | |
245 | Binding.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