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