| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 // Use of this source code is governed by a BSD-style license that can be |
| 4 // found in the LICENSE file. |
| 5 --> |
| 6 <import src="observe.sky" as="observe" /> |
| 7 |
| 8 <script> |
| 9 var stagingDocument = new Document(); |
| 10 |
| 11 class TemplateInstance { |
| 12 constructor() { |
| 13 this.bindings = []; |
| 14 this.terminator = null; |
| 15 this.fragment = stagingDocument.createDocumentFragment(); |
| 16 Object.preventExtensions(this); |
| 17 } |
| 18 close() { |
| 19 var bindings = this.bindings; |
| 20 for (var i = 0; i < bindings.length; i++) { |
| 21 bindings[i].close(); |
| 22 } |
| 23 } |
| 24 } |
| 25 |
| 26 var emptyInstance = new TemplateInstance(); |
| 27 var directiveCache = new WeakMap(); |
| 28 |
| 29 function createInstance(template, model) { |
| 30 var content = template.content; |
| 31 if (!content.firstChild) |
| 32 return emptyInstance; |
| 33 |
| 34 var directives = directiveCache.get(content); |
| 35 if (!directives) { |
| 36 directives = new NodeDirectives(content); |
| 37 directiveCache.set(content, directives); |
| 38 } |
| 39 |
| 40 var instance = new TemplateInstance(); |
| 41 |
| 42 var length = directives.children.length; |
| 43 for (var i = 0; i < length; ++i) { |
| 44 var clone = directives.children[i].createBoundClone(instance.fragment, |
| 45 model, instance.bindings); |
| 46 |
| 47 // The terminator of the instance is the clone of the last child of the |
| 48 // content. If the last child is an active template, it may produce |
| 49 // instances as a result of production, so simply collecting the last |
| 50 // child of the instance after it has finished producing may be wrong. |
| 51 if (i == length - 1) |
| 52 instance.terminator = clone; |
| 53 } |
| 54 |
| 55 return instance; |
| 56 } |
| 57 |
| 58 function sanitizeValue(value) { |
| 59 return value == null ? '' : value; |
| 60 } |
| 61 |
| 62 function updateText(node, value) { |
| 63 node.data = sanitizeValue(value); |
| 64 } |
| 65 |
| 66 function updateAttribute(element, name, value) { |
| 67 element.setAttribute(name, sanitizeValue(value)); |
| 68 } |
| 69 |
| 70 class BindingExpression { |
| 71 constructor(prefix, path) { |
| 72 this.prefix = prefix; |
| 73 this.path = observe.Path.get(path); |
| 74 Object.preventExtensions(this); |
| 75 } |
| 76 } |
| 77 |
| 78 class PropertyDirective { |
| 79 constructor(name) { |
| 80 this.name = name; |
| 81 this.expressions = []; |
| 82 this.suffix = ""; |
| 83 Object.preventExtensions(this); |
| 84 } |
| 85 createObserver(model) { |
| 86 var expressions = this.expressions; |
| 87 var suffix = this.suffix; |
| 88 |
| 89 if (expressions.length == 1 && expressions[0].prefix == "" && suffix == "") |
| 90 return new observe.PathObserver(model, expressions[0].path); |
| 91 |
| 92 var observer = new observe.CompoundObserver(); |
| 93 |
| 94 for (var i = 0; i < expressions.length; ++i) |
| 95 observer.addPath(model, expressions[i].path); |
| 96 |
| 97 return new observe.ObserverTransform(observer, function(values) { |
| 98 var buffer = ""; |
| 99 for (var i = 0; i < values.length; ++i) { |
| 100 buffer += expressions[i].prefix; |
| 101 buffer += values[i]; |
| 102 } |
| 103 buffer += suffix; |
| 104 return buffer; |
| 105 }); |
| 106 } |
| 107 bindProperty(node, model) { |
| 108 var name = this.name; |
| 109 var observable = this.createObserver(model); |
| 110 if (node instanceof Text) { |
| 111 updateText(node, observable.open(function(value) { |
| 112 return updateText(node, value); |
| 113 })); |
| 114 } else if (name == 'style' || name == 'class') { |
| 115 updateAttribute(node, name, observable.open(function(value) { |
| 116 updateAttribute(node, name, value); |
| 117 })); |
| 118 } else { |
| 119 node[name] = observable.open(function(value) { |
| 120 node[name] = value; |
| 121 }); |
| 122 } |
| 123 return observable; |
| 124 } |
| 125 } |
| 126 |
| 127 function parsePropertyDirective(value, property) { |
| 128 if (!value || !value.length) |
| 129 return; |
| 130 |
| 131 var result; |
| 132 var offset = 0; |
| 133 var firstIndex = 0; |
| 134 var lastIndex = 0; |
| 135 |
| 136 while (offset < value.length) { |
| 137 firstIndex = value.indexOf('{{', offset); |
| 138 if (firstIndex == -1) |
| 139 break; |
| 140 lastIndex = value.indexOf('}}', firstIndex + 2); |
| 141 if (lastIndex == -1) |
| 142 lastIndex = value.length; |
| 143 var prefix = value.substring(offset, firstIndex); |
| 144 var path = value.substring(firstIndex + 2, lastIndex); |
| 145 offset = lastIndex + 2; |
| 146 if (!result) |
| 147 result = new PropertyDirective(property); |
| 148 result.expressions.push(new BindingExpression(prefix, path)); |
| 149 } |
| 150 |
| 151 if (result && offset < value.length) |
| 152 result.suffix = value.substring(offset); |
| 153 |
| 154 return result; |
| 155 } |
| 156 |
| 157 function parseAttributeDirectives(element, directives) { |
| 158 var attributes = element.getAttributes(); |
| 159 |
| 160 for (var i = 0; i < attributes.length; i++) { |
| 161 var attr = attributes[i]; |
| 162 var name = attr.name; |
| 163 var value = attr.value; |
| 164 |
| 165 if (name.startsWith('on-')) { |
| 166 directives.eventHandlers.push(name.substring(3)); |
| 167 continue; |
| 168 } |
| 169 |
| 170 var property = parsePropertyDirective(value, name); |
| 171 if (!property) |
| 172 continue; |
| 173 |
| 174 directives.properties.push(property); |
| 175 } |
| 176 } |
| 177 |
| 178 function eventHandlerCallback(event) { |
| 179 var element = event.currentTarget; |
| 180 var method = element.getAttribute('on-' + event.type); |
| 181 var scope = element.ownerScope; |
| 182 var host = scope.host; |
| 183 var handler = host && host[method]; |
| 184 if (handler instanceof Function) |
| 185 return handler.call(host, event); |
| 186 } |
| 187 |
| 188 class NodeDirectives { |
| 189 constructor(node) { |
| 190 this.eventHandlers = []; |
| 191 this.children = []; |
| 192 this.properties = []; |
| 193 this.node = node; |
| 194 Object.preventExtensions(this); |
| 195 |
| 196 if (node instanceof Element) { |
| 197 parseAttributeDirectives(node, this); |
| 198 } else if (node instanceof Text) { |
| 199 var property = parsePropertyDirective(node.data, 'textContent'); |
| 200 if (property) |
| 201 this.properties.push(property); |
| 202 } |
| 203 |
| 204 for (var child = node.firstChild; child; child = child.nextSibling) { |
| 205 this.children.push(new NodeDirectives(child)); |
| 206 } |
| 207 } |
| 208 findProperty(name) { |
| 209 for (var i = 0; i < this.properties.length; ++i) { |
| 210 if (this.properties[i].name === name) |
| 211 return this.properties[i]; |
| 212 } |
| 213 return null; |
| 214 } |
| 215 createBoundClone(parent, model, bindings) { |
| 216 // TODO(esprehn): In sky instead of needing to use a staging docuemnt per |
| 217 // custom element registry we're going to need to use the current module's |
| 218 // registry. |
| 219 var clone = stagingDocument.importNode(this.node, false); |
| 220 |
| 221 for (var i = 0; i < this.eventHandlers.length; ++i) { |
| 222 clone.addEventListener(this.eventHandlers[i], eventHandlerCallback); |
| 223 } |
| 224 |
| 225 var clone = parent.appendChild(clone); |
| 226 |
| 227 for (var i = 0; i < this.children.length; ++i) { |
| 228 this.children[i].createBoundClone(clone, model, bindings); |
| 229 } |
| 230 |
| 231 for (var i = 0; i < this.properties.length; ++i) { |
| 232 bindings.push(this.properties[i].bindProperty(clone, model)); |
| 233 } |
| 234 |
| 235 if (clone instanceof HTMLTemplateElement) { |
| 236 var iterator = new TemplateIterator(clone); |
| 237 iterator.updateDependencies(this, model); |
| 238 bindings.push(iterator); |
| 239 } |
| 240 |
| 241 return clone; |
| 242 } |
| 243 } |
| 244 |
| 245 var iterators = new WeakMap(); |
| 246 |
| 247 class TemplateIterator { |
| 248 constructor(element) { |
| 249 this.closed = false; |
| 250 this.template = element; |
| 251 this.contentTemplate = null; |
| 252 this.instances = []; |
| 253 this.hasRepeat = false; |
| 254 this.ifObserver = null; |
| 255 this.valueObserver = null; |
| 256 this.iteratedValue = []; |
| 257 this.presentValue = null; |
| 258 this.arrayObserver = null; |
| 259 Object.preventExtensions(this); |
| 260 iterators.set(element, this); |
| 261 } |
| 262 |
| 263 updateDependencies(directives, model) { |
| 264 this.contentTemplate = directives.node; |
| 265 |
| 266 var ifValue = true; |
| 267 var ifProperty = directives.findProperty('if'); |
| 268 if (ifProperty) { |
| 269 this.ifObserver = ifProperty.createObserver(model); |
| 270 ifValue = this.ifObserver.open(this.updateIfValue, this); |
| 271 } |
| 272 |
| 273 var repeatProperty = directives.findProperty('repeat'); |
| 274 if (repeatProperty) { |
| 275 this.hasRepeat = true; |
| 276 this.valueObserver = repeatProperty.createObserver(model); |
| 277 } else { |
| 278 var path = observe.Path.get(""); |
| 279 this.valueObserver = new observe.PathObserver(model, path); |
| 280 } |
| 281 |
| 282 var value = this.valueObserver.open(this.updateIteratedValue, this); |
| 283 this.updateValue(ifValue ? value : null); |
| 284 } |
| 285 |
| 286 getUpdatedValue() { |
| 287 return this.valueObserver.discardChanges(); |
| 288 } |
| 289 |
| 290 updateIfValue(ifValue) { |
| 291 if (!ifValue) { |
| 292 this.valueChanged(); |
| 293 return; |
| 294 } |
| 295 |
| 296 this.updateValue(this.getUpdatedValue()); |
| 297 } |
| 298 |
| 299 updateIteratedValue(value) { |
| 300 if (this.ifObserver) { |
| 301 var ifValue = this.ifObserver.discardChanges(); |
| 302 if (!ifValue) { |
| 303 this.valueChanged(); |
| 304 return; |
| 305 } |
| 306 } |
| 307 |
| 308 this.updateValue(value); |
| 309 } |
| 310 |
| 311 updateValue(value) { |
| 312 if (!this.hasRepeat) |
| 313 value = [value]; |
| 314 var observe = this.hasRepeat && Array.isArray(value); |
| 315 this.valueChanged(value, observe); |
| 316 } |
| 317 |
| 318 valueChanged(value, observeValue) { |
| 319 if (!Array.isArray(value)) |
| 320 value = []; |
| 321 |
| 322 if (value === this.iteratedValue) |
| 323 return; |
| 324 |
| 325 this.unobserve(); |
| 326 this.presentValue = value; |
| 327 if (observeValue) { |
| 328 this.arrayObserver = new observe.ArrayObserver(this.presentValue); |
| 329 this.arrayObserver.open(this.handleSplices, this); |
| 330 } |
| 331 |
| 332 this.handleSplices(observe.ArrayObserver.calculateSplices(this.presentValue, |
| 333 this.iteratedValue)); |
| 334 } |
| 335 |
| 336 getLastInstanceNode(index) { |
| 337 if (index == -1) |
| 338 return this.template; |
| 339 var instance = this.instances[index]; |
| 340 var terminator = instance.terminator; |
| 341 if (!terminator) |
| 342 return this.getLastInstanceNode(index - 1); |
| 343 |
| 344 if (!(terminator instanceof Element) || this.template === terminator) { |
| 345 return terminator; |
| 346 } |
| 347 |
| 348 var subtemplateIterator = iterators.get(terminator); |
| 349 if (!subtemplateIterator) |
| 350 return terminator; |
| 351 |
| 352 return subtemplateIterator.getLastTemplateNode(); |
| 353 } |
| 354 |
| 355 getLastTemplateNode() { |
| 356 return this.getLastInstanceNode(this.instances.length - 1); |
| 357 } |
| 358 |
| 359 insertInstanceAt(index, instance) { |
| 360 var previousInstanceLast = this.getLastInstanceNode(index - 1); |
| 361 var parent = this.template.parentNode; |
| 362 this.instances.splice(index, 0, instance); |
| 363 parent.insertBefore(instance.fragment, previousInstanceLast.nextSibling); |
| 364 } |
| 365 |
| 366 extractInstanceAt(index) { |
| 367 var previousInstanceLast = this.getLastInstanceNode(index - 1); |
| 368 var lastNode = this.getLastInstanceNode(index); |
| 369 var parent = this.template.parentNode; |
| 370 var instance = this.instances.splice(index, 1)[0]; |
| 371 |
| 372 while (lastNode !== previousInstanceLast) { |
| 373 var node = previousInstanceLast.nextSibling; |
| 374 if (node == lastNode) |
| 375 lastNode = previousInstanceLast; |
| 376 |
| 377 instance.fragment.appendChild(parent.removeChild(node)); |
| 378 } |
| 379 |
| 380 return instance; |
| 381 } |
| 382 |
| 383 handleSplices(splices) { |
| 384 if (this.closed || !splices.length) |
| 385 return; |
| 386 |
| 387 var template = this.template; |
| 388 |
| 389 if (!template.parentNode) { |
| 390 this.close(); |
| 391 return; |
| 392 } |
| 393 |
| 394 observe.ArrayObserver.applySplices(this.iteratedValue, this.presentValue, |
| 395 splices); |
| 396 |
| 397 // Instance Removals |
| 398 var instanceCache = new Map; |
| 399 var removeDelta = 0; |
| 400 for (var i = 0; i < splices.length; i++) { |
| 401 var splice = splices[i]; |
| 402 var removed = splice.removed; |
| 403 for (var j = 0; j < removed.length; j++) { |
| 404 var model = removed[j]; |
| 405 var instance = this.extractInstanceAt(splice.index + removeDelta); |
| 406 if (instance !== emptyInstance) { |
| 407 instanceCache.set(model, instance); |
| 408 } |
| 409 } |
| 410 |
| 411 removeDelta -= splice.addedCount; |
| 412 } |
| 413 |
| 414 // Instance Insertions |
| 415 for (var i = 0; i < splices.length; i++) { |
| 416 var splice = splices[i]; |
| 417 var addIndex = splice.index; |
| 418 for (; addIndex < splice.index + splice.addedCount; addIndex++) { |
| 419 var model = this.iteratedValue[addIndex]; |
| 420 var instance = instanceCache.get(model); |
| 421 if (instance) { |
| 422 instanceCache.delete(model); |
| 423 } else { |
| 424 if (model === undefined || model === null) { |
| 425 instance = emptyInstance; |
| 426 } else { |
| 427 instance = createInstance(this.contentTemplate, model); |
| 428 } |
| 429 } |
| 430 |
| 431 this.insertInstanceAt(addIndex, instance); |
| 432 } |
| 433 } |
| 434 |
| 435 instanceCache.forEach(function(instance) { |
| 436 instance.close(); |
| 437 }); |
| 438 } |
| 439 |
| 440 unobserve() { |
| 441 if (!this.arrayObserver) |
| 442 return; |
| 443 |
| 444 this.arrayObserver.close(); |
| 445 this.arrayObserver = null; |
| 446 } |
| 447 |
| 448 close() { |
| 449 if (this.closed) |
| 450 return; |
| 451 this.unobserve(); |
| 452 for (var i = 0; i < this.instances.length; i++) { |
| 453 this.instances[i].close(); |
| 454 } |
| 455 |
| 456 this.instances.length = 0; |
| 457 |
| 458 if (this.ifBinding) |
| 459 this.ifBinding.close(); |
| 460 if (this.binding) |
| 461 this.binding.close(); |
| 462 |
| 463 iterators.delete(this.template); |
| 464 this.closed = true; |
| 465 } |
| 466 } |
| 467 |
| 468 module.exports = { |
| 469 createInstance: createInstance, |
| 470 }; |
| 471 </script> |
| OLD | NEW |