1 /** 2 * @license 3 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 4 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7 * Code distributed by Google as part of the polymer project is also 8 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9 */ 10 // @version 0.5.5 11 window.PolymerGestures = {}; 12 13 (function(scope) { 14 var hasFullPath = false; 15 16 // test for full event path support 17 var pathTest = document.createElement('meta'); 18 if (pathTest.createShadowRoot) { 19 var sr = pathTest.createShadowRoot(); 20 var s = document.createElement('span'); 21 sr.appendChild(s); 22 pathTest.addEventListener('testpath', function(ev) { 23 if (ev.path) { 24 // if the span is in the event path, then path[0] is the real source for all events 25 hasFullPath = ev.path[0] === s; 26 } 27 ev.stopPropagation(); 28 }); 29 var ev = new CustomEvent('testpath', {bubbles: true}); 30 // must add node to DOM to trigger event listener 31 document.head.appendChild(pathTest); 32 s.dispatchEvent(ev); 33 pathTest.parentNode.removeChild(pathTest); 34 sr = s = null; 35 } 36 pathTest = null; 37 38 var target = { 39 shadow: function(inEl) { 40 if (inEl) { 41 return inEl.shadowRoot || inEl.webkitShadowRoot; 42 } 43 }, 44 canTarget: function(shadow) { 45 return shadow && Boolean(shadow.elementFromPoint); 46 }, 47 targetingShadow: function(inEl) { 48 var s = this.shadow(inEl); 49 if (this.canTarget(s)) { 50 return s; 51 } 52 }, 53 olderShadow: function(shadow) { 54 var os = shadow.olderShadowRoot; 55 if (!os) { 56 var se = shadow.querySelector('shadow'); 57 if (se) { 58 os = se.olderShadowRoot; 59 } 60 } 61 return os; 62 }, 63 allShadows: function(element) { 64 var shadows = [], s = this.shadow(element); 65 while(s) { 66 shadows.push(s); 67 s = this.olderShadow(s); 68 } 69 return shadows; 70 }, 71 searchRoot: function(inRoot, x, y) { 72 var t, st, sr, os; 73 if (inRoot) { 74 t = inRoot.elementFromPoint(x, y); 75 if (t) { 76 // found element, check if it has a ShadowRoot 77 sr = this.targetingShadow(t); 78 } else if (inRoot !== document) { 79 // check for sibling roots 80 sr = this.olderShadow(inRoot); 81 } 82 // search other roots, fall back to light dom element 83 return this.searchRoot(sr, x, y) || t; 84 } 85 }, 86 owner: function(element) { 87 if (!element) { 88 return document; 89 } 90 var s = element; 91 // walk up until you hit the shadow root or document 92 while (s.parentNode) { 93 s = s.parentNode; 94 } 95 // the owner element is expected to be a Document or ShadowRoot 96 if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) { 97 s = document; 98 } 99 return s; 100 }, 101 findTarget: function(inEvent) { 102 if (hasFullPath && inEvent.path && inEvent.path.length) { 103 return inEvent.path[0]; 104 } 105 var x = inEvent.clientX, y = inEvent.clientY; 106 // if the listener is in the shadow root, it is much faster to start there 107 var s = this.owner(inEvent.target); 108 // if x, y is not in this root, fall back to document search 109 if (!s.elementFromPoint(x, y)) { 110 s = document; 111 } 112 return this.searchRoot(s, x, y); 113 }, 114 findTouchAction: function(inEvent) { 115 var n; 116 if (hasFullPath && inEvent.path && inEvent.path.length) { 117 var path = inEvent.path; 118 for (var i = 0; i < path.length; i++) { 119 n = path[i]; 120 if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { 121 return n.getAttribute('touch-action'); 122 } 123 } 124 } else { 125 n = inEvent.target; 126 while(n) { 127 if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { 128 return n.getAttribute('touch-action'); 129 } 130 n = n.parentNode || n.host; 131 } 132 } 133 // auto is default 134 return "auto"; 135 }, 136 LCA: function(a, b) { 137 if (a === b) { 138 return a; 139 } 140 if (a && !b) { 141 return a; 142 } 143 if (b && !a) { 144 return b; 145 } 146 if (!b && !a) { 147 return document; 148 } 149 // fast case, a is a direct descendant of b or vice versa 150 if (a.contains && a.contains(b)) { 151 return a; 152 } 153 if (b.contains && b.contains(a)) { 154 return b; 155 } 156 var adepth = this.depth(a); 157 var bdepth = this.depth(b); 158 var d = adepth - bdepth; 159 if (d >= 0) { 160 a = this.walk(a, d); 161 } else { 162 b = this.walk(b, -d); 163 } 164 while (a && b && a !== b) { 165 a = a.parentNode || a.host; 166 b = b.parentNode || b.host; 167 } 168 return a; 169 }, 170 walk: function(n, u) { 171 for (var i = 0; n && (i < u); i++) { 172 n = n.parentNode || n.host; 173 } 174 return n; 175 }, 176 depth: function(n) { 177 var d = 0; 178 while(n) { 179 d++; 180 n = n.parentNode || n.host; 181 } 182 return d; 183 }, 184 deepContains: function(a, b) { 185 var common = this.LCA(a, b); 186 // if a is the common ancestor, it must "deeply" contain b 187 return common === a; 188 }, 189 insideNode: function(node, x, y) { 190 var rect = node.getBoundingClientRect(); 191 return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom); 192 }, 193 path: function(event) { 194 var p; 195 if (hasFullPath && event.path && event.path.length) { 196 p = event.path; 197 } else { 198 p = []; 199 var n = this.findTarget(event); 200 while (n) { 201 p.push(n); 202 n = n.parentNode || n.host; 203 } 204 } 205 return p; 206 } 207 }; 208 scope.targetFinding = target; 209 /** 210 * Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting 211 * 212 * @param {Event} Event An event object with clientX and clientY properties 213 * @return {Element} The probable event origninator 214 */ 215 scope.findTarget = target.findTarget.bind(target); 216 /** 217 * Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM 218 * roots. 219 * 220 * @param {Node} container 221 * @param {Node} containee 222 * @return {Boolean} 223 */ 224 scope.deepContains = target.deepContains.bind(target); 225 226 /** 227 * Determines if the x/y position is inside the given node. 228 * 229 * Example: 230 * 231 * function upHandler(event) { 232 * var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY); 233 * if (innode) { 234 * // wait for tap? 235 * } else { 236 * // tap will never happen 237 * } 238 * } 239 * 240 * @param {Node} node 241 * @param {Number} x Screen X position 242 * @param {Number} y screen Y position 243 * @return {Boolean} 244 */ 245 scope.insideNode = target.insideNode; 246 247 })(window.PolymerGestures); 248 249 (function() { 250 function shadowSelector(v) { 251 return 'html /deep/ ' + selector(v); 252 } 253 function selector(v) { 254 return '[touch-action="' + v + '"]'; 255 } 256 function rule(v) { 257 return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + ';}'; 258 } 259 var attrib2css = [ 260 'none', 261 'auto', 262 'pan-x', 263 'pan-y', 264 { 265 rule: 'pan-x pan-y', 266 selectors: [ 267 'pan-x pan-y', 268 'pan-y pan-x' 269 ] 270 }, 271 'manipulation' 272 ]; 273 var styles = ''; 274 // only install stylesheet if the browser has touch action support 275 var hasTouchAction = typeof document.head.style.touchAction === 'string'; 276 // only add shadow selectors if shadowdom is supported 277 var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot; 278 279 if (hasTouchAction) { 280 attrib2css.forEach(function(r) { 281 if (String(r) === r) { 282 styles += selector(r) + rule(r) + '\n'; 283 if (hasShadowRoot) { 284 styles += shadowSelector(r) + rule(r) + '\n'; 285 } 286 } else { 287 styles += r.selectors.map(selector) + rule(r.rule) + '\n'; 288 if (hasShadowRoot) { 289 styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n'; 290 } 291 } 292 }); 293 294 var el = document.createElement('style'); 295 el.textContent = styles; 296 document.head.appendChild(el); 297 } 298 })(); 299 300 /** 301 * This is the constructor for new PointerEvents. 302 * 303 * New Pointer Events must be given a type, and an optional dictionary of 304 * initialization properties. 305 * 306 * Due to certain platform requirements, events returned from the constructor 307 * identify as MouseEvents. 308 * 309 * @constructor 310 * @param {String} inType The type of the event to create. 311 * @param {Object} [inDict] An optional dictionary of initial event properties. 312 * @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`. 313 */ 314 (function(scope) { 315 316 var MOUSE_PROPS = [ 317 'bubbles', 318 'cancelable', 319 'view', 320 'detail', 321 'screenX', 322 'screenY', 323 'clientX', 324 'clientY', 325 'ctrlKey', 326 'altKey', 327 'shiftKey', 328 'metaKey', 329 'button', 330 'relatedTarget', 331 'pageX', 332 'pageY' 333 ]; 334 335 var MOUSE_DEFAULTS = [ 336 false, 337 false, 338 null, 339 null, 340 0, 341 0, 342 0, 343 0, 344 false, 345 false, 346 false, 347 false, 348 0, 349 null, 350 0, 351 0 352 ]; 353 354 var NOP_FACTORY = function(){ return function(){}; }; 355 356 var eventFactory = { 357 // TODO(dfreedm): this is overridden by tap recognizer, needs review 358 preventTap: NOP_FACTORY, 359 makeBaseEvent: function(inType, inDict) { 360 var e = document.createEvent('Event'); 361 e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false); 362 e.preventTap = eventFactory.preventTap(e); 363 return e; 364 }, 365 makeGestureEvent: function(inType, inDict) { 366 inDict = inDict || Object.create(null); 367 368 var e = this.makeBaseEvent(inType, inDict); 369 for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) { 370 k = keys[i]; 371 if( k !== 'bubbles' && k !== 'cancelable' ) { 372 e[k] = inDict[k]; 373 } 374 } 375 return e; 376 }, 377 makePointerEvent: function(inType, inDict) { 378 inDict = inDict || Object.create(null); 379 380 var e = this.makeBaseEvent(inType, inDict); 381 // define inherited MouseEvent properties 382 for(var i = 2, p; i < MOUSE_PROPS.length; i++) { 383 p = MOUSE_PROPS[i]; 384 e[p] = inDict[p] || MOUSE_DEFAULTS[i]; 385 } 386 e.buttons = inDict.buttons || 0; 387 388 // Spec requires that pointers without pressure specified use 0.5 for down 389 // state and 0 for up state. 390 var pressure = 0; 391 if (inDict.pressure) { 392 pressure = inDict.pressure; 393 } else { 394 pressure = e.buttons ? 0.5 : 0; 395 } 396 397 // add x/y properties aliased to clientX/Y 398 e.x = e.clientX; 399 e.y = e.clientY; 400 401 // define the properties of the PointerEvent interface 402 e.pointerId = inDict.pointerId || 0; 403 e.width = inDict.width || 0; 404 e.height = inDict.height || 0; 405 e.pressure = pressure; 406 e.tiltX = inDict.tiltX || 0; 407 e.tiltY = inDict.tiltY || 0; 408 e.pointerType = inDict.pointerType || ''; 409 e.hwTimestamp = inDict.hwTimestamp || 0; 410 e.isPrimary = inDict.isPrimary || false; 411 e._source = inDict._source || ''; 412 return e; 413 } 414 }; 415 416 scope.eventFactory = eventFactory; 417 })(window.PolymerGestures); 418 419 /** 420 * This module implements an map of pointer states 421 */ 422 (function(scope) { 423 var USE_MAP = window.Map && window.Map.prototype.forEach; 424 var POINTERS_FN = function(){ return this.size; }; 425 function PointerMap() { 426 if (USE_MAP) { 427 var m = new Map(); 428 m.pointers = POINTERS_FN; 429 return m; 430 } else { 431 this.keys = []; 432 this.values = []; 433 } 434 } 435 436 PointerMap.prototype = { 437 set: function(inId, inEvent) { 438 var i = this.keys.indexOf(inId); 439 if (i > -1) { 440 this.values[i] = inEvent; 441 } else { 442 this.keys.push(inId); 443 this.values.push(inEvent); 444 } 445 }, 446 has: function(inId) { 447 return this.keys.indexOf(inId) > -1; 448 }, 449 'delete': function(inId) { 450 var i = this.keys.indexOf(inId); 451 if (i > -1) { 452 this.keys.splice(i, 1); 453 this.values.splice(i, 1); 454 } 455 }, 456 get: function(inId) { 457 var i = this.keys.indexOf(inId); 458 return this.values[i]; 459 }, 460 clear: function() { 461 this.keys.length = 0; 462 this.values.length = 0; 463 }, 464 // return value, key, map 465 forEach: function(callback, thisArg) { 466 this.values.forEach(function(v, i) { 467 callback.call(thisArg, v, this.keys[i], this); 468 }, this); 469 }, 470 pointers: function() { 471 return this.keys.length; 472 } 473 }; 474 475 scope.PointerMap = PointerMap; 476 })(window.PolymerGestures); 477 478 (function(scope) { 479 var CLONE_PROPS = [ 480 // MouseEvent 481 'bubbles', 482 'cancelable', 483 'view', 484 'detail', 485 'screenX', 486 'screenY', 487 'clientX', 488 'clientY', 489 'ctrlKey', 490 'altKey', 491 'shiftKey', 492 'metaKey', 493 'button', 494 'relatedTarget', 495 // DOM Level 3 496 'buttons', 497 // PointerEvent 498 'pointerId', 499 'width', 500 'height', 501 'pressure', 502 'tiltX', 503 'tiltY', 504 'pointerType', 505 'hwTimestamp', 506 'isPrimary', 507 // event instance 508 'type', 509 'target', 510 'currentTarget', 511 'which', 512 'pageX', 513 'pageY', 514 'timeStamp', 515 // gesture addons 516 'preventTap', 517 'tapPrevented', 518 '_source' 519 ]; 520 521 var CLONE_DEFAULTS = [ 522 // MouseEvent 523 false, 524 false, 525 null, 526 null, 527 0, 528 0, 529 0, 530 0, 531 false, 532 false, 533 false, 534 false, 535 0, 536 null, 537 // DOM Level 3 538 0, 539 // PointerEvent 540 0, 541 0, 542 0, 543 0, 544 0, 545 0, 546 '', 547 0, 548 false, 549 // event instance 550 '', 551 null, 552 null, 553 0, 554 0, 555 0, 556 0, 557 function(){}, 558 false 559 ]; 560 561 var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined'); 562 563 var eventFactory = scope.eventFactory; 564 565 // set of recognizers to run for the currently handled event 566 var currentGestures; 567 568 /** 569 * This module is for normalizing events. Mouse and Touch events will be 570 * collected here, and fire PointerEvents that have the same semantics, no 571 * matter the source. 572 * Events fired: 573 * - pointerdown: a pointing is added 574 * - pointerup: a pointer is removed 575 * - pointermove: a pointer is moved 576 * - pointerover: a pointer crosses into an element 577 * - pointerout: a pointer leaves an element 578 * - pointercancel: a pointer will no longer generate events 579 */ 580 var dispatcher = { 581 IS_IOS: false, 582 pointermap: new scope.PointerMap(), 583 requiredGestures: new scope.PointerMap(), 584 eventMap: Object.create(null), 585 // Scope objects for native events. 586 // This exists for ease of testing. 587 eventSources: Object.create(null), 588 eventSourceList: [], 589 gestures: [], 590 // map gesture event -> {listeners: int, index: gestures[int]} 591 dependencyMap: { 592 // make sure down and up are in the map to trigger "register" 593 down: {listeners: 0, index: -1}, 594 up: {listeners: 0, index: -1} 595 }, 596 gestureQueue: [], 597 /** 598 * Add a new event source that will generate pointer events. 599 * 600 * `inSource` must contain an array of event names named `events`, and 601 * functions with the names specified in the `events` array. 602 * @param {string} name A name for the event source 603 * @param {Object} source A new source of platform events. 604 */ 605 registerSource: function(name, source) { 606 var s = source; 607 var newEvents = s.events; 608 if (newEvents) { 609 newEvents.forEach(function(e) { 610 if (s[e]) { 611 this.eventMap[e] = s[e].bind(s); 612 } 613 }, this); 614 this.eventSources[name] = s; 615 this.eventSourceList.push(s); 616 } 617 }, 618 registerGesture: function(name, source) { 619 var obj = Object.create(null); 620 obj.listeners = 0; 621 obj.index = this.gestures.length; 622 for (var i = 0, g; i < source.exposes.length; i++) { 623 g = source.exposes[i].toLowerCase(); 624 this.dependencyMap[g] = obj; 625 } 626 this.gestures.push(source); 627 }, 628 register: function(element, initial) { 629 var l = this.eventSourceList.length; 630 for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { 631 // call eventsource register 632 es.register.call(es, element, initial); 633 } 634 }, 635 unregister: function(element) { 636 var l = this.eventSourceList.length; 637 for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { 638 // call eventsource register 639 es.unregister.call(es, element); 640 } 641 }, 642 // EVENTS 643 down: function(inEvent) { 644 this.requiredGestures.set(inEvent.pointerId, currentGestures); 645 this.fireEvent('down', inEvent); 646 }, 647 move: function(inEvent) { 648 // pipe move events into gesture queue directly 649 inEvent.type = 'move'; 650 this.fillGestureQueue(inEvent); 651 }, 652 up: function(inEvent) { 653 this.fireEvent('up', inEvent); 654 this.requiredGestures.delete(inEvent.pointerId); 655 }, 656 cancel: function(inEvent) { 657 inEvent.tapPrevented = true; 658 this.fireEvent('up', inEvent); 659 this.requiredGestures.delete(inEvent.pointerId); 660 }, 661 addGestureDependency: function(node, currentGestures) { 662 var gesturesWanted = node._pgEvents; 663 if (gesturesWanted && currentGestures) { 664 var gk = Object.keys(gesturesWanted); 665 for (var i = 0, r, ri, g; i < gk.length; i++) { 666 // gesture 667 g = gk[i]; 668 if (gesturesWanted[g] > 0) { 669 // lookup gesture recognizer 670 r = this.dependencyMap[g]; 671 // recognizer index 672 ri = r ? r.index : -1; 673 currentGestures[ri] = true; 674 } 675 } 676 } 677 }, 678 // LISTENER LOGIC 679 eventHandler: function(inEvent) { 680 // This is used to prevent multiple dispatch of events from 681 // platform events. This can happen when two elements in different scopes 682 // are set up to create pointer events, which is relevant to Shadow DOM. 683 684 var type = inEvent.type; 685 686 // only generate the list of desired events on "down" 687 if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') { 688 if (!inEvent._handledByPG) { 689 currentGestures = {}; 690 } 691 692 // in IOS mode, there is only a listener on the document, so this is not re-entrant 693 if (this.IS_IOS) { 694 var ev = inEvent; 695 if (type === 'touchstart') { 696 var ct = inEvent.changedTouches[0]; 697 // set up a fake event to give to the path builder 698 ev = {target: inEvent.target, clientX: ct.clientX, clientY: ct.clientY, path: inEvent.path}; 699 } 700 // use event path if available, otherwise build a path from target finding 701 var nodes = inEvent.path || scope.targetFinding.path(ev); 702 for (var i = 0, n; i < nodes.length; i++) { 703 n = nodes[i]; 704 this.addGestureDependency(n, currentGestures); 705 } 706 } else { 707 this.addGestureDependency(inEvent.currentTarget, currentGestures); 708 } 709 } 710 711 if (inEvent._handledByPG) { 712 return; 713 } 714 var fn = this.eventMap && this.eventMap[type]; 715 if (fn) { 716 fn(inEvent); 717 } 718 inEvent._handledByPG = true; 719 }, 720 // set up event listeners 721 listen: function(target, events) { 722 for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { 723 this.addEvent(target, e); 724 } 725 }, 726 // remove event listeners 727 unlisten: function(target, events) { 728 for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { 729 this.removeEvent(target, e); 730 } 731 }, 732 addEvent: function(target, eventName) { 733 target.addEventListener(eventName, this.boundHandler); 734 }, 735 removeEvent: function(target, eventName) { 736 target.removeEventListener(eventName, this.boundHandler); 737 }, 738 // EVENT CREATION AND TRACKING 739 /** 740 * Creates a new Event of type `inType`, based on the information in 741 * `inEvent`. 742 * 743 * @param {string} inType A string representing the type of event to create 744 * @param {Event} inEvent A platform event with a target 745 * @return {Event} A PointerEvent of type `inType` 746 */ 747 makeEvent: function(inType, inEvent) { 748 var e = eventFactory.makePointerEvent(inType, inEvent); 749 e.preventDefault = inEvent.preventDefault; 750 e.tapPrevented = inEvent.tapPrevented; 751 e._target = e._target || inEvent.target; 752 return e; 753 }, 754 // make and dispatch an event in one call 755 fireEvent: function(inType, inEvent) { 756 var e = this.makeEvent(inType, inEvent); 757 return this.dispatchEvent(e); 758 }, 759 /** 760 * Returns a snapshot of inEvent, with writable properties. 761 * 762 * @param {Event} inEvent An event that contains properties to copy. 763 * @return {Object} An object containing shallow copies of `inEvent`'s 764 * properties. 765 */ 766 cloneEvent: function(inEvent) { 767 var eventCopy = Object.create(null), p; 768 for (var i = 0; i < CLONE_PROPS.length; i++) { 769 p = CLONE_PROPS[i]; 770 eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i]; 771 // Work around SVGInstanceElement shadow tree 772 // Return the <use> element that is represented by the instance for Safari, Chrome, IE. 773 // This is the behavior implemented by Firefox. 774 if (p === 'target' || p === 'relatedTarget') { 775 if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) { 776 eventCopy[p] = eventCopy[p].correspondingUseElement; 777 } 778 } 779 } 780 // keep the semantics of preventDefault 781 eventCopy.preventDefault = function() { 782 inEvent.preventDefault(); 783 }; 784 return eventCopy; 785 }, 786 /** 787 * Dispatches the event to its target. 788 * 789 * @param {Event} inEvent The event to be dispatched. 790 * @return {Boolean} True if an event handler returns true, false otherwise. 791 */ 792 dispatchEvent: function(inEvent) { 793 var t = inEvent._target; 794 if (t) { 795 t.dispatchEvent(inEvent); 796 // clone the event for the gesture system to process 797 // clone after dispatch to pick up gesture prevention code 798 var clone = this.cloneEvent(inEvent); 799 clone.target = t; 800 this.fillGestureQueue(clone); 801 } 802 }, 803 gestureTrigger: function() { 804 // process the gesture queue 805 for (var i = 0, e, rg; i < this.gestureQueue.length; i++) { 806 e = this.gestureQueue[i]; 807 rg = e._requiredGestures; 808 if (rg) { 809 for (var j = 0, g, fn; j < this.gestures.length; j++) { 810 // only run recognizer if an element in the source event's path is listening for those gestures 811 if (rg[j]) { 812 g = this.gestures[j]; 813 fn = g[e.type]; 814 if (fn) { 815 fn.call(g, e); 816 } 817 } 818 } 819 } 820 } 821 this.gestureQueue.length = 0; 822 }, 823 fillGestureQueue: function(ev) { 824 // only trigger the gesture queue once 825 if (!this.gestureQueue.length) { 826 requestAnimationFrame(this.boundGestureTrigger); 827 } 828 ev._requiredGestures = this.requiredGestures.get(ev.pointerId); 829 this.gestureQueue.push(ev); 830 } 831 }; 832 dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher); 833 dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher); 834 scope.dispatcher = dispatcher; 835 836 /** 837 * Listen for `gesture` on `node` with the `handler` function 838 * 839 * If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled. 840 * 841 * @param {Element} node 842 * @param {string} gesture 843 * @return Boolean `gesture` is a valid gesture 844 */ 845 scope.activateGesture = function(node, gesture) { 846 var g = gesture.toLowerCase(); 847 var dep = dispatcher.dependencyMap[g]; 848 if (dep) { 849 var recognizer = dispatcher.gestures[dep.index]; 850 if (!node._pgListeners) { 851 dispatcher.register(node); 852 node._pgListeners = 0; 853 } 854 // TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes 855 if (recognizer) { 856 var touchAction = recognizer.defaultActions && recognizer.defaultActions[g]; 857 var actionNode; 858 switch(node.nodeType) { 859 case Node.ELEMENT_NODE: 860 actionNode = node; 861 break; 862 case Node.DOCUMENT_FRAGMENT_NODE: 863 actionNode = node.host; 864 break; 865 default: 866 actionNode = null; 867 break; 868 } 869 if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) { 870 actionNode.setAttribute('touch-action', touchAction); 871 } 872 } 873 if (!node._pgEvents) { 874 node._pgEvents = {}; 875 } 876 node._pgEvents[g] = (node._pgEvents[g] || 0) + 1; 877 node._pgListeners++; 878 } 879 return Boolean(dep); 880 }; 881 882 /** 883 * 884 * Listen for `gesture` from `node` with `handler` function. 885 * 886 * @param {Element} node 887 * @param {string} gesture 888 * @param {Function} handler 889 * @param {Boolean} capture 890 */ 891 scope.addEventListener = function(node, gesture, handler, capture) { 892 if (handler) { 893 scope.activateGesture(node, gesture); 894 node.addEventListener(gesture, handler, capture); 895 } 896 }; 897 898 /** 899 * Tears down the gesture configuration for `node` 900 * 901 * If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled. 902 * 903 * @param {Element} node 904 * @param {string} gesture 905 * @return Boolean `gesture` is a valid gesture 906 */ 907 scope.deactivateGesture = function(node, gesture) { 908 var g = gesture.toLowerCase(); 909 var dep = dispatcher.dependencyMap[g]; 910 if (dep) { 911 if (node._pgListeners > 0) { 912 node._pgListeners--; 913 } 914 if (node._pgListeners === 0) { 915 dispatcher.unregister(node); 916 } 917 if (node._pgEvents) { 918 if (node._pgEvents[g] > 0) { 919 node._pgEvents[g]--; 920 } else { 921 node._pgEvents[g] = 0; 922 } 923 } 924 } 925 return Boolean(dep); 926 }; 927 928 /** 929 * Stop listening for `gesture` from `node` with `handler` function. 930 * 931 * @param {Element} node 932 * @param {string} gesture 933 * @param {Function} handler 934 * @param {Boolean} capture 935 */ 936 scope.removeEventListener = function(node, gesture, handler, capture) { 937 if (handler) { 938 scope.deactivateGesture(node, gesture); 939 node.removeEventListener(gesture, handler, capture); 940 } 941 }; 942 })(window.PolymerGestures); 943 944 (function(scope) { 945 var dispatcher = scope.dispatcher; 946 var pointermap = dispatcher.pointermap; 947 // radius around touchend that swallows mouse events 948 var DEDUP_DIST = 25; 949 950 var WHICH_TO_BUTTONS = [0, 1, 4, 2]; 951 952 var currentButtons = 0; 953 954 var FIREFOX_LINUX = /Linux.*Firefox\//i; 955 956 var HAS_BUTTONS = (function() { 957 // firefox on linux returns spec-incorrect values for mouseup.buttons 958 // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also 959 // https://codereview.chromium.org/727593003/#msg16 960 if (FIREFOX_LINUX.test(navigator.userAgent)) { 961 return false; 962 } 963 try { 964 return new MouseEvent('test', {buttons: 1}).buttons === 1; 965 } catch (e) { 966 return false; 967 } 968 })(); 969 970 // handler block for native mouse events 971 var mouseEvents = { 972 POINTER_ID: 1, 973 POINTER_TYPE: 'mouse', 974 events: [ 975 'mousedown', 976 'mousemove', 977 'mouseup' 978 ], 979 exposes: [ 980 'down', 981 'up', 982 'move' 983 ], 984 register: function(target) { 985 dispatcher.listen(target, this.events); 986 }, 987 unregister: function(target) { 988 if (target.nodeType === Node.DOCUMENT_NODE) { 989 return; 990 } 991 dispatcher.unlisten(target, this.events); 992 }, 993 lastTouches: [], 994 // collide with the global mouse listener 995 isEventSimulatedFromTouch: function(inEvent) { 996 var lts = this.lastTouches; 997 var x = inEvent.clientX, y = inEvent.clientY; 998 for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { 999 // simulated mouse events will be swallowed near a primary touchend 1000 var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y); 1001 if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { 1002 return true; 1003 } 1004 } 1005 }, 1006 prepareEvent: function(inEvent) { 1007 var e = dispatcher.cloneEvent(inEvent); 1008 e.pointerId = this.POINTER_ID; 1009 e.isPrimary = true; 1010 e.pointerType = this.POINTER_TYPE; 1011 e._source = 'mouse'; 1012 if (!HAS_BUTTONS) { 1013 var type = inEvent.type; 1014 var bit = WHICH_TO_BUTTONS[inEvent.which] || 0; 1015 if (type === 'mousedown') { 1016 currentButtons |= bit; 1017 } else if (type === 'mouseup') { 1018 currentButtons &= ~bit; 1019 } 1020 e.buttons = currentButtons; 1021 } 1022 return e; 1023 }, 1024 mousedown: function(inEvent) { 1025 if (!this.isEventSimulatedFromTouch(inEvent)) { 1026 var p = pointermap.has(this.POINTER_ID); 1027 var e = this.prepareEvent(inEvent); 1028 e.target = scope.findTarget(inEvent); 1029 pointermap.set(this.POINTER_ID, e.target); 1030 dispatcher.down(e); 1031 } 1032 }, 1033 mousemove: function(inEvent) { 1034 if (!this.isEventSimulatedFromTouch(inEvent)) { 1035 var target = pointermap.get(this.POINTER_ID); 1036 if (target) { 1037 var e = this.prepareEvent(inEvent); 1038 e.target = target; 1039 // handle case where we missed a mouseup 1040 if ((HAS_BUTTONS ? e.buttons : e.which) === 0) { 1041 if (!HAS_BUTTONS) { 1042 currentButtons = e.buttons = 0; 1043 } 1044 dispatcher.cancel(e); 1045 this.cleanupMouse(e.buttons); 1046 } else { 1047 dispatcher.move(e); 1048 } 1049 } 1050 } 1051 }, 1052 mouseup: function(inEvent) { 1053 if (!this.isEventSimulatedFromTouch(inEvent)) { 1054 var e = this.prepareEvent(inEvent); 1055 e.relatedTarget = scope.findTarget(inEvent); 1056 e.target = pointermap.get(this.POINTER_ID); 1057 dispatcher.up(e); 1058 this.cleanupMouse(e.buttons); 1059 } 1060 }, 1061 cleanupMouse: function(buttons) { 1062 if (buttons === 0) { 1063 pointermap.delete(this.POINTER_ID); 1064 } 1065 } 1066 }; 1067 1068 scope.mouseEvents = mouseEvents; 1069 })(window.PolymerGestures); 1070 1071 (function(scope) { 1072 var dispatcher = scope.dispatcher; 1073 var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding); 1074 var pointermap = dispatcher.pointermap; 1075 var touchMap = Array.prototype.map.call.bind(Array.prototype.map); 1076 // This should be long enough to ignore compat mouse events made by touch 1077 var DEDUP_TIMEOUT = 2500; 1078 var DEDUP_DIST = 25; 1079 var CLICK_COUNT_TIMEOUT = 200; 1080 var HYSTERESIS = 20; 1081 var ATTRIB = 'touch-action'; 1082 // TODO(dfreedm): disable until http://crbug.com/399765 is resolved 1083 // var HAS_TOUCH_ACTION = ATTRIB in document.head.style; 1084 var HAS_TOUCH_ACTION = false; 1085 1086 // handler block for native touch events 1087 var touchEvents = { 1088 IS_IOS: false, 1089 events: [ 1090 'touchstart', 1091 'touchmove', 1092 'touchend', 1093 'touchcancel' 1094 ], 1095 exposes: [ 1096 'down', 1097 'up', 1098 'move' 1099 ], 1100 register: function(target, initial) { 1101 if (this.IS_IOS ? initial : !initial) { 1102 dispatcher.listen(target, this.events); 1103 } 1104 }, 1105 unregister: function(target) { 1106 if (!this.IS_IOS) { 1107 dispatcher.unlisten(target, this.events); 1108 } 1109 }, 1110 scrollTypes: { 1111 EMITTER: 'none', 1112 XSCROLLER: 'pan-x', 1113 YSCROLLER: 'pan-y', 1114 }, 1115 touchActionToScrollType: function(touchAction) { 1116 var t = touchAction; 1117 var st = this.scrollTypes; 1118 if (t === st.EMITTER) { 1119 return 'none'; 1120 } else if (t === st.XSCROLLER) { 1121 return 'X'; 1122 } else if (t === st.YSCROLLER) { 1123 return 'Y'; 1124 } else { 1125 return 'XY'; 1126 } 1127 }, 1128 POINTER_TYPE: 'touch', 1129 firstTouch: null, 1130 isPrimaryTouch: function(inTouch) { 1131 return this.firstTouch === inTouch.identifier; 1132 }, 1133 setPrimaryTouch: function(inTouch) { 1134 // set primary touch if there no pointers, or the only pointer is the mouse 1135 if (pointermap.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) { 1136 this.firstTouch = inTouch.identifier; 1137 this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY}; 1138 this.firstTarget = inTouch.target; 1139 this.scrolling = null; 1140 this.cancelResetClickCount(); 1141 } 1142 }, 1143 removePrimaryPointer: function(inPointer) { 1144 if (inPointer.isPrimary) { 1145 this.firstTouch = null; 1146 this.firstXY = null; 1147 this.resetClickCount(); 1148 } 1149 }, 1150 clickCount: 0, 1151 resetId: null, 1152 resetClickCount: function() { 1153 var fn = function() { 1154 this.clickCount = 0; 1155 this.resetId = null; 1156 }.bind(this); 1157 this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT); 1158 }, 1159 cancelResetClickCount: function() { 1160 if (this.resetId) { 1161 clearTimeout(this.resetId); 1162 } 1163 }, 1164 typeToButtons: function(type) { 1165 var ret = 0; 1166 if (type === 'touchstart' || type === 'touchmove') { 1167 ret = 1; 1168 } 1169 return ret; 1170 }, 1171 findTarget: function(touch, id) { 1172 if (this.currentTouchEvent.type === 'touchstart') { 1173 if (this.isPrimaryTouch(touch)) { 1174 var fastPath = { 1175 clientX: touch.clientX, 1176 clientY: touch.clientY, 1177 path: this.currentTouchEvent.path, 1178 target: this.currentTouchEvent.target 1179 }; 1180 return scope.findTarget(fastPath); 1181 } else { 1182 return scope.findTarget(touch); 1183 } 1184 } 1185 // reuse target we found in touchstart 1186 return pointermap.get(id); 1187 }, 1188 touchToPointer: function(inTouch) { 1189 var cte = this.currentTouchEvent; 1190 var e = dispatcher.cloneEvent(inTouch); 1191 // Spec specifies that pointerId 1 is reserved for Mouse. 1192 // Touch identifiers can start at 0. 1193 // Add 2 to the touch identifier for compatibility. 1194 var id = e.pointerId = inTouch.identifier + 2; 1195 e.target = this.findTarget(inTouch, id); 1196 e.bubbles = true; 1197 e.cancelable = true; 1198 e.detail = this.clickCount; 1199 e.buttons = this.typeToButtons(cte.type); 1200 e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; 1201 e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; 1202 e.pressure = inTouch.webkitForce || inTouch.force || 0.5; 1203 e.isPrimary = this.isPrimaryTouch(inTouch); 1204 e.pointerType = this.POINTER_TYPE; 1205 e._source = 'touch'; 1206 // forward touch preventDefaults 1207 var self = this; 1208 e.preventDefault = function() { 1209 self.scrolling = false; 1210 self.firstXY = null; 1211 cte.preventDefault(); 1212 }; 1213 return e; 1214 }, 1215 processTouches: function(inEvent, inFunction) { 1216 var tl = inEvent.changedTouches; 1217 this.currentTouchEvent = inEvent; 1218 for (var i = 0, t, p; i < tl.length; i++) { 1219 t = tl[i]; 1220 p = this.touchToPointer(t); 1221 if (inEvent.type === 'touchstart') { 1222 pointermap.set(p.pointerId, p.target); 1223 } 1224 if (pointermap.has(p.pointerId)) { 1225 inFunction.call(this, p); 1226 } 1227 if (inEvent.type === 'touchend' || inEvent._cancel) { 1228 this.cleanUpPointer(p); 1229 } 1230 } 1231 }, 1232 // For single axis scrollers, determines whether the element should emit 1233 // pointer events or behave as a scroller 1234 shouldScroll: function(inEvent) { 1235 if (this.firstXY) { 1236 var ret; 1237 var touchAction = scope.targetFinding.findTouchAction(inEvent); 1238 var scrollAxis = this.touchActionToScrollType(touchAction); 1239 if (scrollAxis === 'none') { 1240 // this element is a touch-action: none, should never scroll 1241 ret = false; 1242 } else if (scrollAxis === 'XY') { 1243 // this element should always scroll 1244 ret = true; 1245 } else { 1246 var t = inEvent.changedTouches[0]; 1247 // check the intended scroll axis, and other axis 1248 var a = scrollAxis; 1249 var oa = scrollAxis === 'Y' ? 'X' : 'Y'; 1250 var da = Math.abs(t['client' + a] - this.firstXY[a]); 1251 var doa = Math.abs(t['client' + oa] - this.firstXY[oa]); 1252 // if delta in the scroll axis > delta other axis, scroll instead of 1253 // making events 1254 ret = da >= doa; 1255 } 1256 return ret; 1257 } 1258 }, 1259 findTouch: function(inTL, inId) { 1260 for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) { 1261 if (t.identifier === inId) { 1262 return true; 1263 } 1264 } 1265 }, 1266 // In some instances, a touchstart can happen without a touchend. This 1267 // leaves the pointermap in a broken state. 1268 // Therefore, on every touchstart, we remove the touches that did not fire a 1269 // touchend event. 1270 // To keep state globally consistent, we fire a 1271 // pointercancel for this "abandoned" touch 1272 vacuumTouches: function(inEvent) { 1273 var tl = inEvent.touches; 1274 // pointermap.pointers() should be < tl.length here, as the touchstart has not 1275 // been processed yet. 1276 if (pointermap.pointers() >= tl.length) { 1277 var d = []; 1278 pointermap.forEach(function(value, key) { 1279 // Never remove pointerId == 1, which is mouse. 1280 // Touch identifiers are 2 smaller than their pointerId, which is the 1281 // index in pointermap. 1282 if (key !== 1 && !this.findTouch(tl, key - 2)) { 1283 var p = value; 1284 d.push(p); 1285 } 1286 }, this); 1287 d.forEach(function(p) { 1288 this.cancel(p); 1289 pointermap.delete(p.pointerId); 1290 }, this); 1291 } 1292 }, 1293 touchstart: function(inEvent) { 1294 this.vacuumTouches(inEvent); 1295 this.setPrimaryTouch(inEvent.changedTouches[0]); 1296 this.dedupSynthMouse(inEvent); 1297 if (!this.scrolling) { 1298 this.clickCount++; 1299 this.processTouches(inEvent, this.down); 1300 } 1301 }, 1302 down: function(inPointer) { 1303 dispatcher.down(inPointer); 1304 }, 1305 touchmove: function(inEvent) { 1306 if (HAS_TOUCH_ACTION) { 1307 // touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36 1308 // https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ 1309 if (inEvent.cancelable) { 1310 this.processTouches(inEvent, this.move); 1311 } 1312 } else { 1313 if (!this.scrolling) { 1314 if (this.scrolling === null && this.shouldScroll(inEvent)) { 1315 this.scrolling = true; 1316 } else { 1317 this.scrolling = false; 1318 inEvent.preventDefault(); 1319 this.processTouches(inEvent, this.move); 1320 } 1321 } else if (this.firstXY) { 1322 var t = inEvent.changedTouches[0]; 1323 var dx = t.clientX - this.firstXY.X; 1324 var dy = t.clientY - this.firstXY.Y; 1325 var dd = Math.sqrt(dx * dx + dy * dy); 1326 if (dd >= HYSTERESIS) { 1327 this.touchcancel(inEvent); 1328 this.scrolling = true; 1329 this.firstXY = null; 1330 } 1331 } 1332 } 1333 }, 1334 move: function(inPointer) { 1335 dispatcher.move(inPointer); 1336 }, 1337 touchend: function(inEvent) { 1338 this.dedupSynthMouse(inEvent); 1339 this.processTouches(inEvent, this.up); 1340 }, 1341 up: function(inPointer) { 1342 inPointer.relatedTarget = scope.findTarget(inPointer); 1343 dispatcher.up(inPointer); 1344 }, 1345 cancel: function(inPointer) { 1346 dispatcher.cancel(inPointer); 1347 }, 1348 touchcancel: function(inEvent) { 1349 inEvent._cancel = true; 1350 this.processTouches(inEvent, this.cancel); 1351 }, 1352 cleanUpPointer: function(inPointer) { 1353 pointermap['delete'](inPointer.pointerId); 1354 this.removePrimaryPointer(inPointer); 1355 }, 1356 // prevent synth mouse events from creating pointer events 1357 dedupSynthMouse: function(inEvent) { 1358 var lts = scope.mouseEvents.lastTouches; 1359 var t = inEvent.changedTouches[0]; 1360 // only the primary finger will synth mouse events 1361 if (this.isPrimaryTouch(t)) { 1362 // remember x/y of last touch 1363 var lt = {x: t.clientX, y: t.clientY}; 1364 lts.push(lt); 1365 var fn = (function(lts, lt){ 1366 var i = lts.indexOf(lt); 1367 if (i > -1) { 1368 lts.splice(i, 1); 1369 } 1370 }).bind(null, lts, lt); 1371 setTimeout(fn, DEDUP_TIMEOUT); 1372 } 1373 } 1374 }; 1375 1376 // prevent "ghost clicks" that come from elements that were removed in a touch handler 1377 var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation; 1378 document.addEventListener('click', function(ev) { 1379 var x = ev.clientX, y = ev.clientY; 1380 // check if a click is within DEDUP_DIST px radius of the touchstart 1381 var closeTo = function(touch) { 1382 var dx = Math.abs(x - touch.x), dy = Math.abs(y - touch.y); 1383 return (dx <= DEDUP_DIST && dy <= DEDUP_DIST); 1384 }; 1385 // if click coordinates are close to touch coordinates, assume the click came from a touch 1386 var wasTouched = scope.mouseEvents.lastTouches.some(closeTo); 1387 // if the click came from touch, and the touchstart target is not in the path of the click event, 1388 // then the touchstart target was probably removed, and the click should be "busted" 1389 var path = scope.targetFinding.path(ev); 1390 if (wasTouched) { 1391 for (var i = 0; i < path.length; i++) { 1392 if (path[i] === touchEvents.firstTarget) { 1393 return; 1394 } 1395 } 1396 ev.preventDefault(); 1397 STOP_PROP_FN.call(ev); 1398 } 1399 }, true); 1400 1401 scope.touchEvents = touchEvents; 1402 })(window.PolymerGestures); 1403 1404 (function(scope) { 1405 var dispatcher = scope.dispatcher; 1406 var pointermap = dispatcher.pointermap; 1407 var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number'; 1408 var msEvents = { 1409 events: [ 1410 'MSPointerDown', 1411 'MSPointerMove', 1412 'MSPointerUp', 1413 'MSPointerCancel', 1414 ], 1415 register: function(target) { 1416 dispatcher.listen(target, this.events); 1417 }, 1418 unregister: function(target) { 1419 if (target.nodeType === Node.DOCUMENT_NODE) { 1420 return; 1421 } 1422 dispatcher.unlisten(target, this.events); 1423 }, 1424 POINTER_TYPES: [ 1425 '', 1426 'unavailable', 1427 'touch', 1428 'pen', 1429 'mouse' 1430 ], 1431 prepareEvent: function(inEvent) { 1432 var e = inEvent; 1433 e = dispatcher.cloneEvent(inEvent); 1434 if (HAS_BITMAP_TYPE) { 1435 e.pointerType = this.POINTER_TYPES[inEvent.pointerType]; 1436 } 1437 e._source = 'ms'; 1438 return e; 1439 }, 1440 cleanup: function(id) { 1441 pointermap['delete'](id); 1442 }, 1443 MSPointerDown: function(inEvent) { 1444 var e = this.prepareEvent(inEvent); 1445 e.target = scope.findTarget(inEvent); 1446 pointermap.set(inEvent.pointerId, e.target); 1447 dispatcher.down(e); 1448 }, 1449 MSPointerMove: function(inEvent) { 1450 var target = pointermap.get(inEvent.pointerId); 1451 if (target) { 1452 var e = this.prepareEvent(inEvent); 1453 e.target = target; 1454 dispatcher.move(e); 1455 } 1456 }, 1457 MSPointerUp: function(inEvent) { 1458 var e = this.prepareEvent(inEvent); 1459 e.relatedTarget = scope.findTarget(inEvent); 1460 e.target = pointermap.get(e.pointerId); 1461 dispatcher.up(e); 1462 this.cleanup(inEvent.pointerId); 1463 }, 1464 MSPointerCancel: function(inEvent) { 1465 var e = this.prepareEvent(inEvent); 1466 e.relatedTarget = scope.findTarget(inEvent); 1467 e.target = pointermap.get(e.pointerId); 1468 dispatcher.cancel(e); 1469 this.cleanup(inEvent.pointerId); 1470 } 1471 }; 1472 1473 scope.msEvents = msEvents; 1474 })(window.PolymerGestures); 1475 1476 (function(scope) { 1477 var dispatcher = scope.dispatcher; 1478 var pointermap = dispatcher.pointermap; 1479 var pointerEvents = { 1480 events: [ 1481 'pointerdown', 1482 'pointermove', 1483 'pointerup', 1484 'pointercancel' 1485 ], 1486 prepareEvent: function(inEvent) { 1487 var e = dispatcher.cloneEvent(inEvent); 1488 e._source = 'pointer'; 1489 return e; 1490 }, 1491 register: function(target) { 1492 dispatcher.listen(target, this.events); 1493 }, 1494 unregister: function(target) { 1495 if (target.nodeType === Node.DOCUMENT_NODE) { 1496 return; 1497 } 1498 dispatcher.unlisten(target, this.events); 1499 }, 1500 cleanup: function(id) { 1501 pointermap['delete'](id); 1502 }, 1503 pointerdown: function(inEvent) { 1504 var e = this.prepareEvent(inEvent); 1505 e.target = scope.findTarget(inEvent); 1506 pointermap.set(e.pointerId, e.target); 1507 dispatcher.down(e); 1508 }, 1509 pointermove: function(inEvent) { 1510 var target = pointermap.get(inEvent.pointerId); 1511 if (target) { 1512 var e = this.prepareEvent(inEvent); 1513 e.target = target; 1514 dispatcher.move(e); 1515 } 1516 }, 1517 pointerup: function(inEvent) { 1518 var e = this.prepareEvent(inEvent); 1519 e.relatedTarget = scope.findTarget(inEvent); 1520 e.target = pointermap.get(e.pointerId); 1521 dispatcher.up(e); 1522 this.cleanup(inEvent.pointerId); 1523 }, 1524 pointercancel: function(inEvent) { 1525 var e = this.prepareEvent(inEvent); 1526 e.relatedTarget = scope.findTarget(inEvent); 1527 e.target = pointermap.get(e.pointerId); 1528 dispatcher.cancel(e); 1529 this.cleanup(inEvent.pointerId); 1530 } 1531 }; 1532 1533 scope.pointerEvents = pointerEvents; 1534 })(window.PolymerGestures); 1535 1536 /** 1537 * This module contains the handlers for native platform events. 1538 * From here, the dispatcher is called to create unified pointer events. 1539 * Included are touch events (v1), mouse events, and MSPointerEvents. 1540 */ 1541 (function(scope) { 1542 1543 var dispatcher = scope.dispatcher; 1544 var nav = window.navigator; 1545 1546 if (window.PointerEvent) { 1547 dispatcher.registerSource('pointer', scope.pointerEvents); 1548 } else if (nav.msPointerEnabled) { 1549 dispatcher.registerSource('ms', scope.msEvents); 1550 } else { 1551 dispatcher.registerSource('mouse', scope.mouseEvents); 1552 if (window.ontouchstart !== undefined) { 1553 dispatcher.registerSource('touch', scope.touchEvents); 1554 } 1555 } 1556 1557 // Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506 1558 var ua = navigator.userAgent; 1559 var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window; 1560 1561 dispatcher.IS_IOS = IS_IOS; 1562 scope.touchEvents.IS_IOS = IS_IOS; 1563 1564 dispatcher.register(document, true); 1565 })(window.PolymerGestures); 1566 1567 /** 1568 * This event denotes the beginning of a series of tracking events. 1569 * 1570 * @module PointerGestures 1571 * @submodule Events 1572 * @class trackstart 1573 */ 1574 /** 1575 * Pixels moved in the x direction since trackstart. 1576 * @type Number 1577 * @property dx 1578 */ 1579 /** 1580 * Pixes moved in the y direction since trackstart. 1581 * @type Number 1582 * @property dy 1583 */ 1584 /** 1585 * Pixels moved in the x direction since the last track. 1586 * @type Number 1587 * @property ddx 1588 */ 1589 /** 1590 * Pixles moved in the y direction since the last track. 1591 * @type Number 1592 * @property ddy 1593 */ 1594 /** 1595 * The clientX position of the track gesture. 1596 * @type Number 1597 * @property clientX 1598 */ 1599 /** 1600 * The clientY position of the track gesture. 1601 * @type Number 1602 * @property clientY 1603 */ 1604 /** 1605 * The pageX position of the track gesture. 1606 * @type Number 1607 * @property pageX 1608 */ 1609 /** 1610 * The pageY position of the track gesture. 1611 * @type Number 1612 * @property pageY 1613 */ 1614 /** 1615 * The screenX position of the track gesture. 1616 * @type Number 1617 * @property screenX 1618 */ 1619 /** 1620 * The screenY position of the track gesture. 1621 * @type Number 1622 * @property screenY 1623 */ 1624 /** 1625 * The last x axis direction of the pointer. 1626 * @type Number 1627 * @property xDirection 1628 */ 1629 /** 1630 * The last y axis direction of the pointer. 1631 * @type Number 1632 * @property yDirection 1633 */ 1634 /** 1635 * A shared object between all tracking events. 1636 * @type Object 1637 * @property trackInfo 1638 */ 1639 /** 1640 * The element currently under the pointer. 1641 * @type Element 1642 * @property relatedTarget 1643 */ 1644 /** 1645 * The type of pointer that make the track gesture. 1646 * @type String 1647 * @property pointerType 1648 */ 1649 /** 1650 * 1651 * This event fires for all pointer movement being tracked. 1652 * 1653 * @class track 1654 * @extends trackstart 1655 */ 1656 /** 1657 * This event fires when the pointer is no longer being tracked. 1658 * 1659 * @class trackend 1660 * @extends trackstart 1661 */ 1662 1663 (function(scope) { 1664 var dispatcher = scope.dispatcher; 1665 var eventFactory = scope.eventFactory; 1666 var pointermap = new scope.PointerMap(); 1667 var track = { 1668 events: [ 1669 'down', 1670 'move', 1671 'up', 1672 ], 1673 exposes: [ 1674 'trackstart', 1675 'track', 1676 'trackx', 1677 'tracky', 1678 'trackend' 1679 ], 1680 defaultActions: { 1681 'track': 'none', 1682 'trackx': 'pan-y', 1683 'tracky': 'pan-x' 1684 }, 1685 WIGGLE_THRESHOLD: 4, 1686 clampDir: function(inDelta) { 1687 return inDelta > 0 ? 1 : -1; 1688 }, 1689 calcPositionDelta: function(inA, inB) { 1690 var x = 0, y = 0; 1691 if (inA && inB) { 1692 x = inB.pageX - inA.pageX; 1693 y = inB.pageY - inA.pageY; 1694 } 1695 return {x: x, y: y}; 1696 }, 1697 fireTrack: function(inType, inEvent, inTrackingData) { 1698 var t = inTrackingData; 1699 var d = this.calcPositionDelta(t.downEvent, inEvent); 1700 var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent); 1701 if (dd.x) { 1702 t.xDirection = this.clampDir(dd.x); 1703 } else if (inType === 'trackx') { 1704 return; 1705 } 1706 if (dd.y) { 1707 t.yDirection = this.clampDir(dd.y); 1708 } else if (inType === 'tracky') { 1709 return; 1710 } 1711 var gestureProto = { 1712 bubbles: true, 1713 cancelable: true, 1714 trackInfo: t.trackInfo, 1715 relatedTarget: inEvent.relatedTarget, 1716 pointerType: inEvent.pointerType, 1717 pointerId: inEvent.pointerId, 1718 _source: 'track' 1719 }; 1720 if (inType !== 'tracky') { 1721 gestureProto.x = inEvent.x; 1722 gestureProto.dx = d.x; 1723 gestureProto.ddx = dd.x; 1724 gestureProto.clientX = inEvent.clientX; 1725 gestureProto.pageX = inEvent.pageX; 1726 gestureProto.screenX = inEvent.screenX; 1727 gestureProto.xDirection = t.xDirection; 1728 } 1729 if (inType !== 'trackx') { 1730 gestureProto.dy = d.y; 1731 gestureProto.ddy = dd.y; 1732 gestureProto.y = inEvent.y; 1733 gestureProto.clientY = inEvent.clientY; 1734 gestureProto.pageY = inEvent.pageY; 1735 gestureProto.screenY = inEvent.screenY; 1736 gestureProto.yDirection = t.yDirection; 1737 } 1738 var e = eventFactory.makeGestureEvent(inType, gestureProto); 1739 t.downTarget.dispatchEvent(e); 1740 }, 1741 down: function(inEvent) { 1742 if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) { 1743 var p = { 1744 downEvent: inEvent, 1745 downTarget: inEvent.target, 1746 trackInfo: {}, 1747 lastMoveEvent: null, 1748 xDirection: 0, 1749 yDirection: 0, 1750 tracking: false 1751 }; 1752 pointermap.set(inEvent.pointerId, p); 1753 } 1754 }, 1755 move: function(inEvent) { 1756 var p = pointermap.get(inEvent.pointerId); 1757 if (p) { 1758 if (!p.tracking) { 1759 var d = this.calcPositionDelta(p.downEvent, inEvent); 1760 var move = d.x * d.x + d.y * d.y; 1761 // start tracking only if finger moves more than WIGGLE_THRESHOLD 1762 if (move > this.WIGGLE_THRESHOLD) { 1763 p.tracking = true; 1764 p.lastMoveEvent = p.downEvent; 1765 this.fireTrack('trackstart', inEvent, p); 1766 } 1767 } 1768 if (p.tracking) { 1769 this.fireTrack('track', inEvent, p); 1770 this.fireTrack('trackx', inEvent, p); 1771 this.fireTrack('tracky', inEvent, p); 1772 } 1773 p.lastMoveEvent = inEvent; 1774 } 1775 }, 1776 up: function(inEvent) { 1777 var p = pointermap.get(inEvent.pointerId); 1778 if (p) { 1779 if (p.tracking) { 1780 this.fireTrack('trackend', inEvent, p); 1781 } 1782 pointermap.delete(inEvent.pointerId); 1783 } 1784 } 1785 }; 1786 dispatcher.registerGesture('track', track); 1787 })(window.PolymerGestures); 1788 1789 /** 1790 * This event is fired when a pointer is held down for 200ms. 1791 * 1792 * @module PointerGestures 1793 * @submodule Events 1794 * @class hold 1795 */ 1796 /** 1797 * Type of pointer that made the holding event. 1798 * @type String 1799 * @property pointerType 1800 */ 1801 /** 1802 * Screen X axis position of the held pointer 1803 * @type Number 1804 * @property clientX 1805 */ 1806 /** 1807 * Screen Y axis position of the held pointer 1808 * @type Number 1809 * @property clientY 1810 */ 1811 /** 1812 * Type of pointer that made the holding event. 1813 * @type String 1814 * @property pointerType 1815 */ 1816 /** 1817 * This event is fired every 200ms while a pointer is held down. 1818 * 1819 * @class holdpulse 1820 * @extends hold 1821 */ 1822 /** 1823 * Milliseconds pointer has been held down. 1824 * @type Number 1825 * @property holdTime 1826 */ 1827 /** 1828 * This event is fired when a held pointer is released or moved. 1829 * 1830 * @class release 1831 */ 1832 1833 (function(scope) { 1834 var dispatcher = scope.dispatcher; 1835 var eventFactory = scope.eventFactory; 1836 var hold = { 1837 // wait at least HOLD_DELAY ms between hold and pulse events 1838 HOLD_DELAY: 200, 1839 // pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold 1840 WIGGLE_THRESHOLD: 16, 1841 events: [ 1842 'down', 1843 'move', 1844 'up', 1845 ], 1846 exposes: [ 1847 'hold', 1848 'holdpulse', 1849 'release' 1850 ], 1851 heldPointer: null, 1852 holdJob: null, 1853 pulse: function() { 1854 var hold = Date.now() - this.heldPointer.timeStamp; 1855 var type = this.held ? 'holdpulse' : 'hold'; 1856 this.fireHold(type, hold); 1857 this.held = true; 1858 }, 1859 cancel: function() { 1860 clearInterval(this.holdJob); 1861 if (this.held) { 1862 this.fireHold('release'); 1863 } 1864 this.held = false; 1865 this.heldPointer = null; 1866 this.target = null; 1867 this.holdJob = null; 1868 }, 1869 down: function(inEvent) { 1870 if (inEvent.isPrimary && !this.heldPointer) { 1871 this.heldPointer = inEvent; 1872 this.target = inEvent.target; 1873 this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY); 1874 } 1875 }, 1876 up: function(inEvent) { 1877 if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { 1878 this.cancel(); 1879 } 1880 }, 1881 move: function(inEvent) { 1882 if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { 1883 var x = inEvent.clientX - this.heldPointer.clientX; 1884 var y = inEvent.clientY - this.heldPointer.clientY; 1885 if ((x * x + y * y) > this.WIGGLE_THRESHOLD) { 1886 this.cancel(); 1887 } 1888 } 1889 }, 1890 fireHold: function(inType, inHoldTime) { 1891 var p = { 1892 bubbles: true, 1893 cancelable: true, 1894 pointerType: this.heldPointer.pointerType, 1895 pointerId: this.heldPointer.pointerId, 1896 x: this.heldPointer.clientX, 1897 y: this.heldPointer.clientY, 1898 _source: 'hold' 1899 }; 1900 if (inHoldTime) { 1901 p.holdTime = inHoldTime; 1902 } 1903 var e = eventFactory.makeGestureEvent(inType, p); 1904 this.target.dispatchEvent(e); 1905 } 1906 }; 1907 dispatcher.registerGesture('hold', hold); 1908 })(window.PolymerGestures); 1909 1910 /** 1911 * This event is fired when a pointer quickly goes down and up, and is used to 1912 * denote activation. 1913 * 1914 * Any gesture event can prevent the tap event from being created by calling 1915 * `event.preventTap`. 1916 * 1917 * Any pointer event can prevent the tap by setting the `tapPrevented` property 1918 * on itself. 1919 * 1920 * @module PointerGestures 1921 * @submodule Events 1922 * @class tap 1923 */ 1924 /** 1925 * X axis position of the tap. 1926 * @property x 1927 * @type Number 1928 */ 1929 /** 1930 * Y axis position of the tap. 1931 * @property y 1932 * @type Number 1933 */ 1934 /** 1935 * Type of the pointer that made the tap. 1936 * @property pointerType 1937 * @type String 1938 */ 1939 (function(scope) { 1940 var dispatcher = scope.dispatcher; 1941 var eventFactory = scope.eventFactory; 1942 var pointermap = new scope.PointerMap(); 1943 var tap = { 1944 events: [ 1945 'down', 1946 'up' 1947 ], 1948 exposes: [ 1949 'tap' 1950 ], 1951 down: function(inEvent) { 1952 if (inEvent.isPrimary && !inEvent.tapPrevented) { 1953 pointermap.set(inEvent.pointerId, { 1954 target: inEvent.target, 1955 buttons: inEvent.buttons, 1956 x: inEvent.clientX, 1957 y: inEvent.clientY 1958 }); 1959 } 1960 }, 1961 shouldTap: function(e, downState) { 1962 var tap = true; 1963 if (e.pointerType === 'mouse') { 1964 // only allow left click to tap for mouse 1965 tap = (e.buttons ^ 1) && (downState.buttons & 1); 1966 } 1967 return tap && !e.tapPrevented; 1968 }, 1969 up: function(inEvent) { 1970 var start = pointermap.get(inEvent.pointerId); 1971 if (start && this.shouldTap(inEvent, start)) { 1972 // up.relatedTarget is target currently under finger 1973 var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget); 1974 if (t) { 1975 var e = eventFactory.makeGestureEvent('tap', { 1976 bubbles: true, 1977 cancelable: true, 1978 x: inEvent.clientX, 1979 y: inEvent.clientY, 1980 detail: inEvent.detail, 1981 pointerType: inEvent.pointerType, 1982 pointerId: inEvent.pointerId, 1983 altKey: inEvent.altKey, 1984 ctrlKey: inEvent.ctrlKey, 1985 metaKey: inEvent.metaKey, 1986 shiftKey: inEvent.shiftKey, 1987 _source: 'tap' 1988 }); 1989 t.dispatchEvent(e); 1990 } 1991 } 1992 pointermap.delete(inEvent.pointerId); 1993 } 1994 }; 1995 // patch eventFactory to remove id from tap's pointermap for preventTap calls 1996 eventFactory.preventTap = function(e) { 1997 return function() { 1998 e.tapPrevented = true; 1999 pointermap.delete(e.pointerId); 2000 }; 2001 }; 2002 dispatcher.registerGesture('tap', tap); 2003 })(window.PolymerGestures); 2004 2005 /* 2006 * Basic strategy: find the farthest apart points, use as diameter of circle 2007 * react to size change and rotation of the chord 2008 */ 2009 2010 /** 2011 * @module pointer-gestures 2012 * @submodule Events 2013 * @class pinch 2014 */ 2015 /** 2016 * Scale of the pinch zoom gesture 2017 * @property scale 2018 * @type Number 2019 */ 2020 /** 2021 * Center X position of pointers causing pinch 2022 * @property centerX 2023 * @type Number 2024 */ 2025 /** 2026 * Center Y position of pointers causing pinch 2027 * @property centerY 2028 * @type Number 2029 */ 2030 2031 /** 2032 * @module pointer-gestures 2033 * @submodule Events 2034 * @class rotate 2035 */ 2036 /** 2037 * Angle (in degrees) of rotation. Measured from starting positions of pointers. 2038 * @property angle 2039 * @type Number 2040 */ 2041 /** 2042 * Center X position of pointers causing rotation 2043 * @property centerX 2044 * @type Number 2045 */ 2046 /** 2047 * Center Y position of pointers causing rotation 2048 * @property centerY 2049 * @type Number 2050 */ 2051 (function(scope) { 2052 var dispatcher = scope.dispatcher; 2053 var eventFactory = scope.eventFactory; 2054 var pointermap = new scope.PointerMap(); 2055 var RAD_TO_DEG = 180 / Math.PI; 2056 var pinch = { 2057 events: [ 2058 'down', 2059 'up', 2060 'move', 2061 'cancel' 2062 ], 2063 exposes: [ 2064 'pinchstart', 2065 'pinch', 2066 'pinchend', 2067 'rotate' 2068 ], 2069 defaultActions: { 2070 'pinch': 'none', 2071 'rotate': 'none' 2072 }, 2073 reference: {}, 2074 down: function(inEvent) { 2075 pointermap.set(inEvent.pointerId, inEvent); 2076 if (pointermap.pointers() == 2) { 2077 var points = this.calcChord(); 2078 var angle = this.calcAngle(points); 2079 this.reference = { 2080 angle: angle, 2081 diameter: points.diameter, 2082 target: scope.targetFinding.LCA(points.a.target, points.b.target) 2083 }; 2084 2085 this.firePinch('pinchstart', points.diameter, points); 2086 } 2087 }, 2088 up: function(inEvent) { 2089 var p = pointermap.get(inEvent.pointerId); 2090 var num = pointermap.pointers(); 2091 if (p) { 2092 if (num === 2) { 2093 // fire 'pinchend' before deleting pointer 2094 var points = this.calcChord(); 2095 this.firePinch('pinchend', points.diameter, points); 2096 } 2097 pointermap.delete(inEvent.pointerId); 2098 } 2099 }, 2100 move: function(inEvent) { 2101 if (pointermap.has(inEvent.pointerId)) { 2102 pointermap.set(inEvent.pointerId, inEvent); 2103 if (pointermap.pointers() > 1) { 2104 this.calcPinchRotate(); 2105 } 2106 } 2107 }, 2108 cancel: function(inEvent) { 2109 this.up(inEvent); 2110 }, 2111 firePinch: function(type, diameter, points) { 2112 var zoom = diameter / this.reference.diameter; 2113 var e = eventFactory.makeGestureEvent(type, { 2114 bubbles: true, 2115 cancelable: true, 2116 scale: zoom, 2117 centerX: points.center.x, 2118 centerY: points.center.y, 2119 _source: 'pinch' 2120 }); 2121 this.reference.target.dispatchEvent(e); 2122 }, 2123 fireRotate: function(angle, points) { 2124 var diff = Math.round((angle - this.reference.angle) % 360); 2125 var e = eventFactory.makeGestureEvent('rotate', { 2126 bubbles: true, 2127 cancelable: true, 2128 angle: diff, 2129 centerX: points.center.x, 2130 centerY: points.center.y, 2131 _source: 'pinch' 2132 }); 2133 this.reference.target.dispatchEvent(e); 2134 }, 2135 calcPinchRotate: function() { 2136 var points = this.calcChord(); 2137 var diameter = points.diameter; 2138 var angle = this.calcAngle(points); 2139 if (diameter != this.reference.diameter) { 2140 this.firePinch('pinch', diameter, points); 2141 } 2142 if (angle != this.reference.angle) { 2143 this.fireRotate(angle, points); 2144 } 2145 }, 2146 calcChord: function() { 2147 var pointers = []; 2148 pointermap.forEach(function(p) { 2149 pointers.push(p); 2150 }); 2151 var dist = 0; 2152 // start with at least two pointers 2153 var points = {a: pointers[0], b: pointers[1]}; 2154 var x, y, d; 2155 for (var i = 0; i < pointers.length; i++) { 2156 var a = pointers[i]; 2157 for (var j = i + 1; j < pointers.length; j++) { 2158 var b = pointers[j]; 2159 x = Math.abs(a.clientX - b.clientX); 2160 y = Math.abs(a.clientY - b.clientY); 2161 d = x + y; 2162 if (d > dist) { 2163 dist = d; 2164 points = {a: a, b: b}; 2165 } 2166 } 2167 } 2168 x = Math.abs(points.a.clientX + points.b.clientX) / 2; 2169 y = Math.abs(points.a.clientY + points.b.clientY) / 2; 2170 points.center = { x: x, y: y }; 2171 points.diameter = dist; 2172 return points; 2173 }, 2174 calcAngle: function(points) { 2175 var x = points.a.clientX - points.b.clientX; 2176 var y = points.a.clientY - points.b.clientY; 2177 return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360; 2178 } 2179 }; 2180 dispatcher.registerGesture('pinch', pinch); 2181 })(window.PolymerGestures); 2182 2183 (function (global) { 2184 'use strict'; 2185 2186 var Token, 2187 TokenName, 2188 Syntax, 2189 Messages, 2190 source, 2191 index, 2192 length, 2193 delegate, 2194 lookahead, 2195 state; 2196 2197 Token = { 2198 BooleanLiteral: 1, 2199 EOF: 2, 2200 Identifier: 3, 2201 Keyword: 4, 2202 NullLiteral: 5, 2203 NumericLiteral: 6, 2204 Punctuator: 7, 2205 StringLiteral: 8 2206 }; 2207 2208 TokenName = {}; 2209 TokenName[Token.BooleanLiteral] = 'Boolean'; 2210 TokenName[Token.EOF] = '<end>'; 2211 TokenName[Token.Identifier] = 'Identifier'; 2212 TokenName[Token.Keyword] = 'Keyword'; 2213 TokenName[Token.NullLiteral] = 'Null'; 2214 TokenName[Token.NumericLiteral] = 'Numeric'; 2215 TokenName[Token.Punctuator] = 'Punctuator'; 2216 TokenName[Token.StringLiteral] = 'String'; 2217 2218 Syntax = { 2219 ArrayExpression: 'ArrayExpression', 2220 BinaryExpression: 'BinaryExpression', 2221 CallExpression: 'CallExpression', 2222 ConditionalExpression: 'ConditionalExpression', 2223 EmptyStatement: 'EmptyStatement', 2224 ExpressionStatement: 'ExpressionStatement', 2225 Identifier: 'Identifier', 2226 Literal: 'Literal', 2227 LabeledStatement: 'LabeledStatement', 2228 LogicalExpression: 'LogicalExpression', 2229 MemberExpression: 'MemberExpression', 2230 ObjectExpression: 'ObjectExpression', 2231 Program: 'Program', 2232 Property: 'Property', 2233 ThisExpression: 'ThisExpression', 2234 UnaryExpression: 'UnaryExpression' 2235 }; 2236 2237 // Error messages should be identical to V8. 2238 Messages = { 2239 UnexpectedToken: 'Unexpected token %0', 2240 UnknownLabel: 'Undefined label \'%0\'', 2241 Redeclaration: '%0 \'%1\' has already been declared' 2242 }; 2243 2244 // Ensure the condition is true, otherwise throw an error. 2245 // This is only to have a better contract semantic, i.e. another safety net 2246 // to catch a logic error. The condition shall be fulfilled in normal case. 2247 // Do NOT use this to enforce a certain condition on any user input. 2248 2249 function assert(condition, message) { 2250 if (!condition) { 2251 throw new Error('ASSERT: ' + message); 2252 } 2253 } 2254 2255 function isDecimalDigit(ch) { 2256 return (ch >= 48 && ch <= 57); // 0..9 2257 } 2258 2259 2260 // 7.2 White Space 2261 2262 function isWhiteSpace(ch) { 2263 return (ch === 32) || // space 2264 (ch === 9) || // tab 2265 (ch === 0xB) || 2266 (ch === 0xC) || 2267 (ch === 0xA0) || 2268 (ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0); 2269 } 2270 2271 // 7.3 Line Terminators 2272 2273 function isLineTerminator(ch) { 2274 return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029); 2275 } 2276 2277 // 7.6 Identifier Names and Identifiers 2278 2279 function isIdentifierStart(ch) { 2280 return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) 2281 (ch >= 65 && ch <= 90) || // A..Z 2282 (ch >= 97 && ch <= 122); // a..z 2283 } 2284 2285 function isIdentifierPart(ch) { 2286 return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) 2287 (ch >= 65 && ch <= 90) || // A..Z 2288 (ch >= 97 && ch <= 122) || // a..z 2289 (ch >= 48 && ch <= 57); // 0..9 2290 } 2291 2292 // 7.6.1.1 Keywords 2293 2294 function isKeyword(id) { 2295 return (id === 'this') 2296 } 2297 2298 // 7.4 Comments 2299 2300 function skipWhitespace() { 2301 while (index < length && isWhiteSpace(source.charCodeAt(index))) { 2302 ++index; 2303 } 2304 } 2305 2306 function getIdentifier() { 2307 var start, ch; 2308 2309 start = index++; 2310 while (index < length) { 2311 ch = source.charCodeAt(index); 2312 if (isIdentifierPart(ch)) { 2313 ++index; 2314 } else { 2315 break; 2316 } 2317 } 2318 2319 return source.slice(start, index); 2320 } 2321 2322 function scanIdentifier() { 2323 var start, id, type; 2324 2325 start = index; 2326 2327 id = getIdentifier(); 2328 2329 // There is no keyword or literal with only one character. 2330 // Thus, it must be an identifier. 2331 if (id.length === 1) { 2332 type = Token.Identifier; 2333 } else if (isKeyword(id)) { 2334 type = Token.Keyword; 2335 } else if (id === 'null') { 2336 type = Token.NullLiteral; 2337 } else if (id === 'true' || id === 'false') { 2338 type = Token.BooleanLiteral; 2339 } else { 2340 type = Token.Identifier; 2341 } 2342 2343 return { 2344 type: type, 2345 value: id, 2346 range: [start, index] 2347 }; 2348 } 2349 2350 2351 // 7.7 Punctuators 2352 2353 function scanPunctuator() { 2354 var start = index, 2355 code = source.charCodeAt(index), 2356 code2, 2357 ch1 = source[index], 2358 ch2; 2359 2360 switch (code) { 2361 2362 // Check for most common single-character punctuators. 2363 case 46: // . dot 2364 case 40: // ( open bracket 2365 case 41: // ) close bracket 2366 case 59: // ; semicolon 2367 case 44: // , comma 2368 case 123: // { open curly brace 2369 case 125: // } close curly brace 2370 case 91: // [ 2371 case 93: // ] 2372 case 58: // : 2373 case 63: // ? 2374 ++index; 2375 return { 2376 type: Token.Punctuator, 2377 value: String.fromCharCode(code), 2378 range: [start, index] 2379 }; 2380 2381 default: 2382 code2 = source.charCodeAt(index + 1); 2383 2384 // '=' (char #61) marks an assignment or comparison operator. 2385 if (code2 === 61) { 2386 switch (code) { 2387 case 37: // % 2388 case 38: // & 2389 case 42: // *: 2390 case 43: // + 2391 case 45: // - 2392 case 47: // / 2393 case 60: // < 2394 case 62: // > 2395 case 124: // | 2396 index += 2; 2397 return { 2398 type: Token.Punctuator, 2399 value: String.fromCharCode(code) + String.fromCharCode(code2), 2400 range: [start, index] 2401 }; 2402 2403 case 33: // ! 2404 case 61: // = 2405 index += 2; 2406 2407 // !== and === 2408 if (source.charCodeAt(index) === 61) { 2409 ++index; 2410 } 2411 return { 2412 type: Token.Punctuator, 2413 value: source.slice(start, index), 2414 range: [start, index] 2415 }; 2416 default: 2417 break; 2418 } 2419 } 2420 break; 2421 } 2422 2423 // Peek more characters. 2424 2425 ch2 = source[index + 1]; 2426 2427 // Other 2-character punctuators: && || 2428 2429 if (ch1 === ch2 && ('&|'.indexOf(ch1) >= 0)) { 2430 index += 2; 2431 return { 2432 type: Token.Punctuator, 2433 value: ch1 + ch2, 2434 range: [start, index] 2435 }; 2436 } 2437 2438 if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { 2439 ++index; 2440 return { 2441 type: Token.Punctuator, 2442 value: ch1, 2443 range: [start, index] 2444 }; 2445 } 2446 2447 throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); 2448 } 2449 2450 // 7.8.3 Numeric Literals 2451 function scanNumericLiteral() { 2452 var number, start, ch; 2453 2454 ch = source[index]; 2455 assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), 2456 'Numeric literal must start with a decimal digit or a decimal point'); 2457 2458 start = index; 2459 number = ''; 2460 if (ch !== '.') { 2461 number = source[index++]; 2462 ch = source[index]; 2463 2464 // Hex number starts with '0x'. 2465 // Octal number starts with '0'. 2466 if (number === '0') { 2467 // decimal number starts with '0' such as '09' is illegal. 2468 if (ch && isDecimalDigit(ch.charCodeAt(0))) { 2469 throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); 2470 } 2471 } 2472 2473 while (isDecimalDigit(source.charCodeAt(index))) { 2474 number += source[index++]; 2475 } 2476 ch = source[index]; 2477 } 2478 2479 if (ch === '.') { 2480 number += source[index++]; 2481 while (isDecimalDigit(source.charCodeAt(index))) { 2482 number += source[index++]; 2483 } 2484 ch = source[index]; 2485 } 2486 2487 if (ch === 'e' || ch === 'E') { 2488 number += source[index++]; 2489 2490 ch = source[index]; 2491 if (ch === '+' || ch === '-') { 2492 number += source[index++]; 2493 } 2494 if (isDecimalDigit(source.charCodeAt(index))) { 2495 while (isDecimalDigit(source.charCodeAt(index))) { 2496 number += source[index++]; 2497 } 2498 } else { 2499 throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); 2500 } 2501 } 2502 2503 if (isIdentifierStart(source.charCodeAt(index))) { 2504 throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); 2505 } 2506 2507 return { 2508 type: Token.NumericLiteral, 2509 value: parseFloat(number), 2510 range: [start, index] 2511 }; 2512 } 2513 2514 // 7.8.4 String Literals 2515 2516 function scanStringLiteral() { 2517 var str = '', quote, start, ch, octal = false; 2518 2519 quote = source[index]; 2520 assert((quote === '\'' || quote === '"'), 2521 'String literal must starts with a quote'); 2522 2523 start = index; 2524 ++index; 2525 2526 while (index < length) { 2527 ch = source[index++]; 2528 2529 if (ch === quote) { 2530 quote = ''; 2531 break; 2532 } else if (ch === '\\') { 2533 ch = source[index++]; 2534 if (!ch || !isLineTerminator(ch.charCodeAt(0))) { 2535 switch (ch) { 2536 case 'n': 2537 str += '\n'; 2538 break; 2539 case 'r': 2540 str += '\r'; 2541 break; 2542 case 't': 2543 str += '\t'; 2544 break; 2545 case 'b': 2546 str += '\b'; 2547 break; 2548 case 'f': 2549 str += '\f'; 2550 break; 2551 case 'v': 2552 str += '\x0B'; 2553 break; 2554 2555 default: 2556 str += ch; 2557 break; 2558 } 2559 } else { 2560 if (ch === '\r' && source[index] === '\n') { 2561 ++index; 2562 } 2563 } 2564 } else if (isLineTerminator(ch.charCodeAt(0))) { 2565 break; 2566 } else { 2567 str += ch; 2568 } 2569 } 2570 2571 if (quote !== '') { 2572 throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); 2573 } 2574 2575 return { 2576 type: Token.StringLiteral, 2577 value: str, 2578 octal: octal, 2579 range: [start, index] 2580 }; 2581 } 2582 2583 function isIdentifierName(token) { 2584 return token.type === Token.Identifier || 2585 token.type === Token.Keyword || 2586 token.type === Token.BooleanLiteral || 2587 token.type === Token.NullLiteral; 2588 } 2589 2590 function advance() { 2591 var ch; 2592 2593 skipWhitespace(); 2594 2595 if (index >= length) { 2596 return { 2597 type: Token.EOF, 2598 range: [index, index] 2599 }; 2600 } 2601 2602 ch = source.charCodeAt(index); 2603 2604 // Very common: ( and ) and ; 2605 if (ch === 40 || ch === 41 || ch === 58) { 2606 return scanPunctuator(); 2607 } 2608 2609 // String literal starts with single quote (#39) or double quote (#34). 2610 if (ch === 39 || ch === 34) { 2611 return scanStringLiteral(); 2612 } 2613 2614 if (isIdentifierStart(ch)) { 2615 return scanIdentifier(); 2616 } 2617 2618 // Dot (.) char #46 can also start a floating-point number, hence the need 2619 // to check the next character. 2620 if (ch === 46) { 2621 if (isDecimalDigit(source.charCodeAt(index + 1))) { 2622 return scanNumericLiteral(); 2623 } 2624 return scanPunctuator(); 2625 } 2626 2627 if (isDecimalDigit(ch)) { 2628 return scanNumericLiteral(); 2629 } 2630 2631 return scanPunctuator(); 2632 } 2633 2634 function lex() { 2635 var token; 2636 2637 token = lookahead; 2638 index = token.range[1]; 2639 2640 lookahead = advance(); 2641 2642 index = token.range[1]; 2643 2644 return token; 2645 } 2646 2647 function peek() { 2648 var pos; 2649 2650 pos = index; 2651 lookahead = advance(); 2652 index = pos; 2653 } 2654 2655 // Throw an exception 2656 2657 function throwError(token, messageFormat) { 2658 var error, 2659 args = Array.prototype.slice.call(arguments, 2), 2660 msg = messageFormat.replace( 2661 /%(\d)/g, 2662 function (whole, index) { 2663 assert(index < args.length, 'Message reference must be in range'); 2664 return args[index]; 2665 } 2666 ); 2667 2668 error = new Error(msg); 2669 error.index = index; 2670 error.description = msg; 2671 throw error; 2672 } 2673 2674 // Throw an exception because of the token. 2675 2676 function throwUnexpected(token) { 2677 throwError(token, Messages.UnexpectedToken, token.value); 2678 } 2679 2680 // Expect the next token to match the specified punctuator. 2681 // If not, an exception will be thrown. 2682 2683 function expect(value) { 2684 var token = lex(); 2685 if (token.type !== Token.Punctuator || token.value !== value) { 2686 throwUnexpected(token); 2687 } 2688 } 2689 2690 // Return true if the next token matches the specified punctuator. 2691 2692 function match(value) { 2693 return lookahead.type === Token.Punctuator && lookahead.value === value; 2694 } 2695 2696 // Return true if the next token matches the specified keyword 2697 2698 function matchKeyword(keyword) { 2699 return lookahead.type === Token.Keyword && lookahead.value === keyword; 2700 } 2701 2702 function consumeSemicolon() { 2703 // Catch the very common case first: immediately a semicolon (char #59). 2704 if (source.charCodeAt(index) === 59) { 2705 lex(); 2706 return; 2707 } 2708 2709 skipWhitespace(); 2710 2711 if (match(';')) { 2712 lex(); 2713 return; 2714 } 2715 2716 if (lookahead.type !== Token.EOF && !match('}')) { 2717 throwUnexpected(lookahead); 2718 } 2719 } 2720 2721 // 11.1.4 Array Initialiser 2722 2723 function parseArrayInitialiser() { 2724 var elements = []; 2725 2726 expect('['); 2727 2728 while (!match(']')) { 2729 if (match(',')) { 2730 lex(); 2731 elements.push(null); 2732 } else { 2733 elements.push(parseExpression()); 2734 2735 if (!match(']')) { 2736 expect(','); 2737 } 2738 } 2739 } 2740 2741 expect(']'); 2742 2743 return delegate.createArrayExpression(elements); 2744 } 2745 2746 // 11.1.5 Object Initialiser 2747 2748 function parseObjectPropertyKey() { 2749 var token; 2750 2751 skipWhitespace(); 2752 token = lex(); 2753 2754 // Note: This function is called only from parseObjectProperty(), where 2755 // EOF and Punctuator tokens are already filtered out. 2756 if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { 2757 return delegate.createLiteral(token); 2758 } 2759 2760 return delegate.createIdentifier(token.value); 2761 } 2762 2763 function parseObjectProperty() { 2764 var token, key; 2765 2766 token = lookahead; 2767 skipWhitespace(); 2768 2769 if (token.type === Token.EOF || token.type === Token.Punctuator) { 2770 throwUnexpected(token); 2771 } 2772 2773 key = parseObjectPropertyKey(); 2774 expect(':'); 2775 return delegate.createProperty('init', key, parseExpression()); 2776 } 2777 2778 function parseObjectInitialiser() { 2779 var properties = []; 2780 2781 expect('{'); 2782 2783 while (!match('}')) { 2784 properties.push(parseObjectProperty()); 2785 2786 if (!match('}')) { 2787 expect(','); 2788 } 2789 } 2790 2791 expect('}'); 2792 2793 return delegate.createObjectExpression(properties); 2794 } 2795 2796 // 11.1.6 The Grouping Operator 2797 2798 function parseGroupExpression() { 2799 var expr; 2800 2801 expect('('); 2802 2803 expr = parseExpression(); 2804 2805 expect(')'); 2806 2807 return expr; 2808 } 2809 2810 2811 // 11.1 Primary Expressions 2812 2813 function parsePrimaryExpression() { 2814 var type, token, expr; 2815 2816 if (match('(')) { 2817 return parseGroupExpression(); 2818 } 2819 2820 type = lookahead.type; 2821 2822 if (type === Token.Identifier) { 2823 expr = delegate.createIdentifier(lex().value); 2824 } else if (type === Token.StringLiteral || type === Token.NumericLiteral) { 2825 expr = delegate.createLiteral(lex()); 2826 } else if (type === Token.Keyword) { 2827 if (matchKeyword('this')) { 2828 lex(); 2829 expr = delegate.createThisExpression(); 2830 } 2831 } else if (type === Token.BooleanLiteral) { 2832 token = lex(); 2833 token.value = (token.value === 'true'); 2834 expr = delegate.createLiteral(token); 2835 } else if (type === Token.NullLiteral) { 2836 token = lex(); 2837 token.value = null; 2838 expr = delegate.createLiteral(token); 2839 } else if (match('[')) { 2840 expr = parseArrayInitialiser(); 2841 } else if (match('{')) { 2842 expr = parseObjectInitialiser(); 2843 } 2844 2845 if (expr) { 2846 return expr; 2847 } 2848 2849 throwUnexpected(lex()); 2850 } 2851 2852 // 11.2 Left-Hand-Side Expressions 2853 2854 function parseArguments() { 2855 var args = []; 2856 2857 expect('('); 2858 2859 if (!match(')')) { 2860 while (index < length) { 2861 args.push(parseExpression()); 2862 if (match(')')) { 2863 break; 2864 } 2865 expect(','); 2866 } 2867 } 2868 2869 expect(')'); 2870 2871 return args; 2872 } 2873 2874 function parseNonComputedProperty() { 2875 var token; 2876 2877 token = lex(); 2878 2879 if (!isIdentifierName(token)) { 2880 throwUnexpected(token); 2881 } 2882 2883 return delegate.createIdentifier(token.value); 2884 } 2885 2886 function parseNonComputedMember() { 2887 expect('.'); 2888 2889 return parseNonComputedProperty(); 2890 } 2891 2892 function parseComputedMember() { 2893 var expr; 2894 2895 expect('['); 2896 2897 expr = parseExpression(); 2898 2899 expect(']'); 2900 2901 return expr; 2902 } 2903 2904 function parseLeftHandSideExpression() { 2905 var expr, args, property; 2906 2907 expr = parsePrimaryExpression(); 2908 2909 while (true) { 2910 if (match('[')) { 2911 property = parseComputedMember(); 2912 expr = delegate.createMemberExpression('[', expr, property); 2913 } else if (match('.')) { 2914 property = parseNonComputedMember(); 2915 expr = delegate.createMemberExpression('.', expr, property); 2916 } else if (match('(')) { 2917 args = parseArguments(); 2918 expr = delegate.createCallExpression(expr, args); 2919 } else { 2920 break; 2921 } 2922 } 2923 2924 return expr; 2925 } 2926 2927 // 11.3 Postfix Expressions 2928 2929 var parsePostfixExpression = parseLeftHandSideExpression; 2930 2931 // 11.4 Unary Operators 2932 2933 function parseUnaryExpression() { 2934 var token, expr; 2935 2936 if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { 2937 expr = parsePostfixExpression(); 2938 } else if (match('+') || match('-') || match('!')) { 2939 token = lex(); 2940 expr = parseUnaryExpression(); 2941 expr = delegate.createUnaryExpression(token.value, expr); 2942 } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { 2943 throwError({}, Messages.UnexpectedToken); 2944 } else { 2945 expr = parsePostfixExpression(); 2946 } 2947 2948 return expr; 2949 } 2950 2951 function binaryPrecedence(token) { 2952 var prec = 0; 2953 2954 if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { 2955 return 0; 2956 } 2957 2958 switch (token.value) { 2959 case '||': 2960 prec = 1; 2961 break; 2962 2963 case '&&': 2964 prec = 2; 2965 break; 2966 2967 case '==': 2968 case '!=': 2969 case '===': 2970 case '!==': 2971 prec = 6; 2972 break; 2973 2974 case '<': 2975 case '>': 2976 case '<=': 2977 case '>=': 2978 case 'instanceof': 2979 prec = 7; 2980 break; 2981 2982 case 'in': 2983 prec = 7; 2984 break; 2985 2986 case '+': 2987 case '-': 2988 prec = 9; 2989 break; 2990 2991 case '*': 2992 case '/': 2993 case '%': 2994 prec = 11; 2995 break; 2996 2997 default: 2998 break; 2999 } 3000 3001 return prec; 3002 } 3003 3004 // 11.5 Multiplicative Operators 3005 // 11.6 Additive Operators 3006 // 11.7 Bitwise Shift Operators 3007 // 11.8 Relational Operators 3008 // 11.9 Equality Operators 3009 // 11.10 Binary Bitwise Operators 3010 // 11.11 Binary Logical Operators 3011 3012 function parseBinaryExpression() { 3013 var expr, token, prec, stack, right, operator, left, i; 3014 3015 left = parseUnaryExpression(); 3016 3017 token = lookahead; 3018 prec = binaryPrecedence(token); 3019 if (prec === 0) { 3020 return left; 3021 } 3022 token.prec = prec; 3023 lex(); 3024 3025 right = parseUnaryExpression(); 3026 3027 stack = [left, token, right]; 3028 3029 while ((prec = binaryPrecedence(lookahead)) > 0) { 3030 3031 // Reduce: make a binary expression from the three topmost entries. 3032 while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { 3033 right = stack.pop(); 3034 operator = stack.pop().value; 3035 left = stack.pop(); 3036 expr = delegate.createBinaryExpression(operator, left, right); 3037 stack.push(expr); 3038 } 3039 3040 // Shift. 3041 token = lex(); 3042 token.prec = prec; 3043 stack.push(token); 3044 expr = parseUnaryExpression(); 3045 stack.push(expr); 3046 } 3047 3048 // Final reduce to clean-up the stack. 3049 i = stack.length - 1; 3050 expr = stack[i]; 3051 while (i > 1) { 3052 expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); 3053 i -= 2; 3054 } 3055 3056 return expr; 3057 } 3058 3059 3060 // 11.12 Conditional Operator 3061 3062 function parseConditionalExpression() { 3063 var expr, consequent, alternate; 3064 3065 expr = parseBinaryExpression(); 3066 3067 if (match('?')) { 3068 lex(); 3069 consequent = parseConditionalExpression(); 3070 expect(':'); 3071 alternate = parseConditionalExpression(); 3072 3073 expr = delegate.createConditionalExpression(expr, consequent, alternate); 3074 } 3075 3076 return expr; 3077 } 3078 3079 // Simplification since we do not support AssignmentExpression. 3080 var parseExpression = parseConditionalExpression; 3081 3082 // Polymer Syntax extensions 3083 3084 // Filter :: 3085 // Identifier 3086 // Identifier "(" ")" 3087 // Identifier "(" FilterArguments ")" 3088 3089 function parseFilter() { 3090 var identifier, args; 3091 3092 identifier = lex(); 3093 3094 if (identifier.type !== Token.Identifier) { 3095 throwUnexpected(identifier); 3096 } 3097 3098 args = match('(') ? parseArguments() : []; 3099 3100 return delegate.createFilter(identifier.value, args); 3101 } 3102 3103 // Filters :: 3104 // "|" Filter 3105 // Filters "|" Filter 3106 3107 function parseFilters() { 3108 while (match('|')) { 3109 lex(); 3110 parseFilter(); 3111 } 3112 } 3113 3114 // TopLevel :: 3115 // LabelledExpressions 3116 // AsExpression 3117 // InExpression 3118 // FilterExpression 3119 3120 // AsExpression :: 3121 // FilterExpression as Identifier 3122 3123 // InExpression :: 3124 // Identifier, Identifier in FilterExpression 3125 // Identifier in FilterExpression 3126 3127 // FilterExpression :: 3128 // Expression 3129 // Expression Filters 3130 3131 function parseTopLevel() { 3132 skipWhitespace(); 3133 peek(); 3134 3135 var expr = parseExpression(); 3136 if (expr) { 3137 if (lookahead.value === ',' || lookahead.value == 'in' && 3138 expr.type === Syntax.Identifier) { 3139 parseInExpression(expr); 3140 } else { 3141 parseFilters(); 3142 if (lookahead.value === 'as') { 3143 parseAsExpression(expr); 3144 } else { 3145 delegate.createTopLevel(expr); 3146 } 3147 } 3148 } 3149 3150 if (lookahead.type !== Token.EOF) { 3151 throwUnexpected(lookahead); 3152 } 3153 } 3154 3155 function parseAsExpression(expr) { 3156 lex(); // as 3157 var identifier = lex().value; 3158 delegate.createAsExpression(expr, identifier); 3159 } 3160 3161 function parseInExpression(identifier) { 3162 var indexName; 3163 if (lookahead.value === ',') { 3164 lex(); 3165 if (lookahead.type !== Token.Identifier) 3166 throwUnexpected(lookahead); 3167 indexName = lex().value; 3168 } 3169 3170 lex(); // in 3171 var expr = parseExpression(); 3172 parseFilters(); 3173 delegate.createInExpression(identifier.name, indexName, expr); 3174 } 3175 3176 function parse(code, inDelegate) { 3177 delegate = inDelegate; 3178 source = code; 3179 index = 0; 3180 length = source.length; 3181 lookahead = null; 3182 state = { 3183 labelSet: {} 3184 }; 3185 3186 return parseTopLevel(); 3187 } 3188 3189 global.esprima = { 3190 parse: parse 3191 }; 3192 })(this); 3193 3194 // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 3195 // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 3196 // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 3197 // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 3198 // Code distributed by Google as part of the polymer project is also 3199 // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 3200 3201 (function (global) { 3202 'use strict'; 3203 3204 function prepareBinding(expressionText, name, node, filterRegistry) { 3205 var expression; 3206 try { 3207 expression = getExpression(expressionText); 3208 if (expression.scopeIdent && 3209 (node.nodeType !== Node.ELEMENT_NODE || 3210 node.tagName !== 'TEMPLATE' || 3211 (name !== 'bind' && name !== 'repeat'))) { 3212 throw Error('as and in can only be used within <template bind/repeat>'); 3213 } 3214 } catch (ex) { 3215 console.error('Invalid expression syntax: ' + expressionText, ex); 3216 return; 3217 } 3218 3219 return function(model, node, oneTime) { 3220 var binding = expression.getBinding(model, filterRegistry, oneTime); 3221 if (expression.scopeIdent && binding) { 3222 node.polymerExpressionScopeIdent_ = expression.scopeIdent; 3223 if (expression.indexIdent) 3224 node.polymerExpressionIndexIdent_ = expression.indexIdent; 3225 } 3226 3227 return binding; 3228 } 3229 } 3230 3231 // TODO(rafaelw): Implement simple LRU. 3232 var expressionParseCache = Object.create(null); 3233 3234 function getExpression(expressionText) { 3235 var expression = expressionParseCache[expressionText]; 3236 if (!expression) { 3237 var delegate = new ASTDelegate(); 3238 esprima.parse(expressionText, delegate); 3239 expression = new Expression(delegate); 3240 expressionParseCache[expressionText] = expression; 3241 } 3242 return expression; 3243 } 3244 3245 function Literal(value) { 3246 this.value = value; 3247 this.valueFn_ = undefined; 3248 } 3249 3250 Literal.prototype = { 3251 valueFn: function() { 3252 if (!this.valueFn_) { 3253 var value = this.value; 3254 this.valueFn_ = function() { 3255 return value; 3256 } 3257 } 3258 3259 return this.valueFn_; 3260 } 3261 } 3262 3263 function IdentPath(name) { 3264 this.name = name; 3265 this.path = Path.get(name); 3266 } 3267 3268 IdentPath.prototype = { 3269 valueFn: function() { 3270 if (!this.valueFn_) { 3271 var name = this.name; 3272 var path = this.path; 3273 this.valueFn_ = function(model, observer) { 3274 if (observer) 3275 observer.addPath(model, path); 3276 3277 return path.getValueFrom(model); 3278 } 3279 } 3280 3281 return this.valueFn_; 3282 }, 3283 3284 setValue: function(model, newValue) { 3285 if (this.path.length == 1) 3286 model = findScope(model, this.path[0]); 3287 3288 return this.path.setValueFrom(model, newValue); 3289 } 3290 }; 3291 3292 function MemberExpression(object, property, accessor) { 3293 this.computed = accessor == '['; 3294 3295 this.dynamicDeps = typeof object == 'function' || 3296 object.dynamicDeps || 3297 (this.computed && !(property instanceof Literal)); 3298 3299 this.simplePath = 3300 !this.dynamicDeps && 3301 (property instanceof IdentPath || property instanceof Literal) && 3302 (object instanceof MemberExpression || object instanceof IdentPath); 3303 3304 this.object = this.simplePath ? object : getFn(object); 3305 this.property = !this.computed || this.simplePath ? 3306 property : getFn(property); 3307 } 3308 3309 MemberExpression.prototype = { 3310 get fullPath() { 3311 if (!this.fullPath_) { 3312 3313 var parts = this.object instanceof MemberExpression ? 3314 this.object.fullPath.slice() : [this.object.name]; 3315 parts.push(this.property instanceof IdentPath ? 3316 this.property.name : this.property.value); 3317 this.fullPath_ = Path.get(parts); 3318 } 3319 3320 return this.fullPath_; 3321 }, 3322 3323 valueFn: function() { 3324 if (!this.valueFn_) { 3325 var object = this.object; 3326 3327 if (this.simplePath) { 3328 var path = this.fullPath; 3329 3330 this.valueFn_ = function(model, observer) { 3331 if (observer) 3332 observer.addPath(model, path); 3333 3334 return path.getValueFrom(model); 3335 }; 3336 } else if (!this.computed) { 3337 var path = Path.get(this.property.name); 3338 3339 this.valueFn_ = function(model, observer, filterRegistry) { 3340 var context = object(model, observer, filterRegistry); 3341 3342 if (observer) 3343 observer.addPath(context, path); 3344 3345 return path.getValueFrom(context); 3346 } 3347 } else { 3348 // Computed property. 3349 var property = this.property; 3350 3351 this.valueFn_ = function(model, observer, filterRegistry) { 3352 var context = object(model, observer, filterRegistry); 3353 var propName = property(model, observer, filterRegistry); 3354 if (observer) 3355 observer.addPath(context, [propName]); 3356 3357 return context ? context[propName] : undefined; 3358 }; 3359 } 3360 } 3361 return this.valueFn_; 3362 }, 3363 3364 setValue: function(model, newValue) { 3365 if (this.simplePath) { 3366 this.fullPath.setValueFrom(model, newValue); 3367 return newValue; 3368 } 3369 3370 var object = this.object(model); 3371 var propName = this.property instanceof IdentPath ? this.property.name : 3372 this.property(model); 3373 return object[propName] = newValue; 3374 } 3375 }; 3376 3377 function Filter(name, args) { 3378 this.name = name; 3379 this.args = []; 3380 for (var i = 0; i < args.length; i++) { 3381 this.args[i] = getFn(args[i]); 3382 } 3383 } 3384 3385 Filter.prototype = { 3386 transform: function(model, observer, filterRegistry, toModelDirection, 3387 initialArgs) { 3388 var context = model; 3389 var fn = context[this.name]; 3390 3391 if (!fn) { 3392 fn = filterRegistry[this.name]; 3393 if (!fn) { 3394 console.error('Cannot find function or filter: ' + this.name); 3395 return; 3396 } 3397 } 3398 3399 // If toModelDirection is falsey, then the "normal" (dom-bound) direction 3400 // is used. Otherwise, it looks for a 'toModel' property function on the 3401 // object. 3402 if (toModelDirection) { 3403 fn = fn.toModel; 3404 } else if (typeof fn.toDOM == 'function') { 3405 fn = fn.toDOM; 3406 } 3407 3408 if (typeof fn != 'function') { 3409 console.error('Cannot find function or filter: ' + this.name); 3410 return; 3411 } 3412 3413 var args = initialArgs || []; 3414 for (var i = 0; i < this.args.length; i++) { 3415 args.push(getFn(this.args[i])(model, observer, filterRegistry)); 3416 } 3417 3418 return fn.apply(context, args); 3419 } 3420 }; 3421 3422 function notImplemented() { throw Error('Not Implemented'); } 3423 3424 var unaryOperators = { 3425 '+': function(v) { return +v; }, 3426 '-': function(v) { return -v; }, 3427 '!': function(v) { return !v; } 3428 }; 3429 3430 var binaryOperators = { 3431 '+': function(l, r) { return l+r; }, 3432 '-': function(l, r) { return l-r; }, 3433 '*': function(l, r) { return l*r; }, 3434 '/': function(l, r) { return l/r; }, 3435 '%': function(l, r) { return l%r; }, 3436 '<': function(l, r) { return l<r; }, 3437 '>': function(l, r) { return l>r; }, 3438 '<=': function(l, r) { return l<=r; }, 3439 '>=': function(l, r) { return l>=r; }, 3440 '==': function(l, r) { return l==r; }, 3441 '!=': function(l, r) { return l!=r; }, 3442 '===': function(l, r) { return l===r; }, 3443 '!==': function(l, r) { return l!==r; }, 3444 '&&': function(l, r) { return l&&r; }, 3445 '||': function(l, r) { return l||r; }, 3446 }; 3447 3448 function getFn(arg) { 3449 return typeof arg == 'function' ? arg : arg.valueFn(); 3450 } 3451 3452 function ASTDelegate() { 3453 this.expression = null; 3454 this.filters = []; 3455 this.deps = {}; 3456 this.currentPath = undefined; 3457 this.scopeIdent = undefined; 3458 this.indexIdent = undefined; 3459 this.dynamicDeps = false; 3460 } 3461 3462 ASTDelegate.prototype = { 3463 createUnaryExpression: function(op, argument) { 3464 if (!unaryOperators[op]) 3465 throw Error('Disallowed operator: ' + op); 3466 3467 argument = getFn(argument); 3468 3469 return function(model, observer, filterRegistry) { 3470 return unaryOperators[op](argument(model, observer, filterRegistry)); 3471 }; 3472 }, 3473 3474 createBinaryExpression: function(op, left, right) { 3475 if (!binaryOperators[op]) 3476 throw Error('Disallowed operator: ' + op); 3477 3478 left = getFn(left); 3479 right = getFn(right); 3480 3481 switch (op) { 3482 case '||': 3483 this.dynamicDeps = true; 3484 return function(model, observer, filterRegistry) { 3485 return left(model, observer, filterRegistry) || 3486 right(model, observer, filterRegistry); 3487 }; 3488 case '&&': 3489 this.dynamicDeps = true; 3490 return function(model, observer, filterRegistry) { 3491 return left(model, observer, filterRegistry) && 3492 right(model, observer, filterRegistry); 3493 }; 3494 } 3495 3496 return function(model, observer, filterRegistry) { 3497 return binaryOperators[op](left(model, observer, filterRegistry), 3498 right(model, observer, filterRegistry)); 3499 }; 3500 }, 3501 3502 createConditionalExpression: function(test, consequent, alternate) { 3503 test = getFn(test); 3504 consequent = getFn(consequent); 3505 alternate = getFn(alternate); 3506 3507 this.dynamicDeps = true; 3508 3509 return function(model, observer, filterRegistry) { 3510 return test(model, observer, filterRegistry) ? 3511 consequent(model, observer, filterRegistry) : 3512 alternate(model, observer, filterRegistry); 3513 } 3514 }, 3515 3516 createIdentifier: function(name) { 3517 var ident = new IdentPath(name); 3518 ident.type = 'Identifier'; 3519 return ident; 3520 }, 3521 3522 createMemberExpression: function(accessor, object, property) { 3523 var ex = new MemberExpression(object, property, accessor); 3524 if (ex.dynamicDeps) 3525 this.dynamicDeps = true; 3526 return ex; 3527 }, 3528 3529 createCallExpression: function(expression, args) { 3530 if (!(expression instanceof IdentPath)) 3531 throw Error('Only identifier function invocations are allowed'); 3532 3533 var filter = new Filter(expression.name, args); 3534 3535 return function(model, observer, filterRegistry) { 3536 return filter.transform(model, observer, filterRegistry, false); 3537 }; 3538 }, 3539 3540 createLiteral: function(token) { 3541 return new Literal(token.value); 3542 }, 3543 3544 createArrayExpression: function(elements) { 3545 for (var i = 0; i < elements.length; i++) 3546 elements[i] = getFn(elements[i]); 3547 3548 return function(model, observer, filterRegistry) { 3549 var arr = [] 3550 for (var i = 0; i < elements.length; i++) 3551 arr.push(elements[i](model, observer, filterRegistry)); 3552 return arr; 3553 } 3554 }, 3555 3556 createProperty: function(kind, key, value) { 3557 return { 3558 key: key instanceof IdentPath ? key.name : key.value, 3559 value: value 3560 }; 3561 }, 3562 3563 createObjectExpression: function(properties) { 3564 for (var i = 0; i < properties.length; i++) 3565 properties[i].value = getFn(properties[i].value); 3566 3567 return function(model, observer, filterRegistry) { 3568 var obj = {}; 3569 for (var i = 0; i < properties.length; i++) 3570 obj[properties[i].key] = 3571 properties[i].value(model, observer, filterRegistry); 3572 return obj; 3573 } 3574 }, 3575 3576 createFilter: function(name, args) { 3577 this.filters.push(new Filter(name, args)); 3578 }, 3579 3580 createAsExpression: function(expression, scopeIdent) { 3581 this.expression = expression; 3582 this.scopeIdent = scopeIdent; 3583 }, 3584 3585 createInExpression: function(scopeIdent, indexIdent, expression) { 3586 this.expression = expression; 3587 this.scopeIdent = scopeIdent; 3588 this.indexIdent = indexIdent; 3589 }, 3590 3591 createTopLevel: function(expression) { 3592 this.expression = expression; 3593 }, 3594 3595 createThisExpression: notImplemented 3596 } 3597 3598 function ConstantObservable(value) { 3599 this.value_ = value; 3600 } 3601 3602 ConstantObservable.prototype = { 3603 open: function() { return this.value_; }, 3604 discardChanges: function() { return this.value_; }, 3605 deliver: function() {}, 3606 close: function() {}, 3607 } 3608 3609 function Expression(delegate) { 3610 this.scopeIdent = delegate.scopeIdent; 3611 this.indexIdent = delegate.indexIdent; 3612 3613 if (!delegate.expression) 3614 throw Error('No expression found.'); 3615 3616 this.expression = delegate.expression; 3617 getFn(this.expression); // forces enumeration of path dependencies 3618 3619 this.filters = delegate.filters; 3620 this.dynamicDeps = delegate.dynamicDeps; 3621 } 3622 3623 Expression.prototype = { 3624 getBinding: function(model, filterRegistry, oneTime) { 3625 if (oneTime) 3626 return this.getValue(model, undefined, filterRegistry); 3627 3628 var observer = new CompoundObserver(); 3629 // captures deps. 3630 var firstValue = this.getValue(model, observer, filterRegistry); 3631 var firstTime = true; 3632 var self = this; 3633 3634 function valueFn() { 3635 // deps cannot have changed on first value retrieval. 3636 if (firstTime) { 3637 firstTime = false; 3638 return firstValue; 3639 } 3640 3641 if (self.dynamicDeps) 3642 observer.startReset(); 3643 3644 var value = self.getValue(model, 3645 self.dynamicDeps ? observer : undefined, 3646 filterRegistry); 3647 if (self.dynamicDeps) 3648 observer.finishReset(); 3649 3650 return value; 3651 } 3652 3653 function setValueFn(newValue) { 3654 self.setValue(model, newValue, filterRegistry); 3655 return newValue; 3656 } 3657 3658 return new ObserverTransform(observer, valueFn, setValueFn, true); 3659 }, 3660 3661 getValue: function(model, observer, filterRegistry) { 3662 var value = getFn(this.expression)(model, observer, filterRegistry); 3663 for (var i = 0; i < this.filters.length; i++) { 3664 value = this.filters[i].transform(model, observer, filterRegistry, 3665 false, [value]); 3666 } 3667 3668 return value; 3669 }, 3670 3671 setValue: function(model, newValue, filterRegistry) { 3672 var count = this.filters ? this.filters.length : 0; 3673 while (count-- > 0) { 3674 newValue = this.filters[count].transform(model, undefined, 3675 filterRegistry, true, [newValue]); 3676 } 3677 3678 if (this.expression.setValue) 3679 return this.expression.setValue(model, newValue); 3680 } 3681 } 3682 3683 /** 3684 * Converts a style property name to a css property name. For example: 3685 * "WebkitUserSelect" to "-webkit-user-select" 3686 */ 3687 function convertStylePropertyName(name) { 3688 return String(name).replace(/[A-Z]/g, function(c) { 3689 return '-' + c.toLowerCase(); 3690 }); 3691 } 3692 3693 var parentScopeName = '@' + Math.random().toString(36).slice(2); 3694 3695 // Single ident paths must bind directly to the appropriate scope object. 3696 // I.e. Pushed values in two-bindings need to be assigned to the actual model 3697 // object. 3698 function findScope(model, prop) { 3699 while (model[parentScopeName] && 3700 !Object.prototype.hasOwnProperty.call(model, prop)) { 3701 model = model[parentScopeName]; 3702 } 3703 3704 return model; 3705 } 3706 3707 function isLiteralExpression(pathString) { 3708 switch (pathString) { 3709 case '': 3710 return false; 3711 3712 case 'false': 3713 case 'null': 3714 case 'true': 3715 return true; 3716 } 3717 3718 if (!isNaN(Number(pathString))) 3719 return true; 3720 3721 return false; 3722 }; 3723 3724 function PolymerExpressions() {} 3725 3726 PolymerExpressions.prototype = { 3727 // "built-in" filters 3728 styleObject: function(value) { 3729 var parts = []; 3730 for (var key in value) { 3731 parts.push(convertStylePropertyName(key) + ': ' + value[key]); 3732 } 3733 return parts.join('; '); 3734 }, 3735 3736 tokenList: function(value) { 3737 var tokens = []; 3738 for (var key in value) { 3739 if (value[key]) 3740 tokens.push(key); 3741 } 3742 return tokens.join(' '); 3743 }, 3744 3745 // binding delegate API 3746 prepareInstancePositionChanged: function(template) { 3747 var indexIdent = template.polymerExpressionIndexIdent_; 3748 if (!indexIdent) 3749 return; 3750 3751 return function(templateInstance, index) { 3752 templateInstance.model[indexIdent] = index; 3753 }; 3754 }, 3755 3756 prepareBinding: function(pathString, name, node) { 3757 var path = Path.get(pathString); 3758 3759 if (!isLiteralExpression(pathString) && path.valid) { 3760 if (path.length == 1) { 3761 return function(model, node, oneTime) { 3762 if (oneTime) 3763 return path.getValueFrom(model); 3764 3765 var scope = findScope(model, path[0]); 3766 return new PathObserver(scope, path); 3767 }; 3768 } 3769 return; // bail out early if pathString is simple path. 3770 } 3771 3772 return prepareBinding(pathString, name, node, this); 3773 }, 3774 3775 prepareInstanceModel: function(template) { 3776 var scopeName = template.polymerExpressionScopeIdent_; 3777 if (!scopeName) 3778 return; 3779 3780 var parentScope = template.templateInstance ? 3781 template.templateInstance.model : 3782 template.model; 3783 3784 var indexName = template.polymerExpressionIndexIdent_; 3785 3786 return function(model) { 3787 return createScopeObject(parentScope, model, scopeName, indexName); 3788 }; 3789 } 3790 }; 3791 3792 var createScopeObject = ('__proto__' in {}) ? 3793 function(parentScope, model, scopeName, indexName) { 3794 var scope = {}; 3795 scope[scopeName] = model; 3796 scope[indexName] = undefined; 3797 scope[parentScopeName] = parentScope; 3798 scope.__proto__ = parentScope; 3799 return scope; 3800 } : 3801 function(parentScope, model, scopeName, indexName) { 3802 var scope = Object.create(parentScope); 3803 Object.defineProperty(scope, scopeName, 3804 { value: model, configurable: true, writable: true }); 3805 Object.defineProperty(scope, indexName, 3806 { value: undefined, configurable: true, writable: true }); 3807 Object.defineProperty(scope, parentScopeName, 3808 { value: parentScope, configurable: true, writable: true }); 3809 return scope; 3810 }; 3811 3812 global.PolymerExpressions = PolymerExpressions; 3813 PolymerExpressions.getExpression = getExpression; 3814 })(this); 3815 3816 Polymer = { 3817 version: '0.5.5' 3818 }; 3819 3820 // TODO(sorvell): this ensures Polymer is an object and not a function 3821 // Platform is currently defining it as a function to allow for async loading 3822 // of polymer; once we refine the loading process this likely goes away. 3823 if (typeof window.Polymer === 'function') { 3824 Polymer = {}; 3825 } 3826 3827 3828 (function(scope) { 3829 3830 function withDependencies(task, depends) { 3831 depends = depends || []; 3832 if (!depends.map) { 3833 depends = [depends]; 3834 } 3835 return task.apply(this, depends.map(marshal)); 3836 } 3837 3838 function module(name, dependsOrFactory, moduleFactory) { 3839 var module; 3840 switch (arguments.length) { 3841 case 0: 3842 return; 3843 case 1: 3844 module = null; 3845 break; 3846 case 2: 3847 // dependsOrFactory is `factory` in this case 3848 module = dependsOrFactory.apply(this); 3849 break; 3850 default: 3851 // dependsOrFactory is `depends` in this case 3852 module = withDependencies(moduleFactory, dependsOrFactory); 3853 break; 3854 } 3855 modules[name] = module; 3856 }; 3857 3858 function marshal(name) { 3859 return modules[name]; 3860 } 3861 3862 var modules = {}; 3863 3864 function using(depends, task) { 3865 HTMLImports.whenImportsReady(function() { 3866 withDependencies(task, depends); 3867 }); 3868 }; 3869 3870 // exports 3871 3872 scope.marshal = marshal; 3873 // `module` confuses commonjs detectors 3874 scope.modularize = module; 3875 scope.using = using; 3876 3877 })(window); 3878 3879 /* 3880 Build only script. 3881 3882 Ensures scripts needed for basic x-platform compatibility 3883 will be run when platform.js is not loaded. 3884 */ 3885 if (!window.WebComponents) { 3886 3887 /* 3888 On supported platforms, platform.js is not needed. To retain compatibility 3889 with the polyfills, we stub out minimal functionality. 3890 */ 3891 if (!window.WebComponents) { 3892 3893 WebComponents = { 3894 flush: function() {}, 3895 flags: {log: {}} 3896 }; 3897 3898 Platform = WebComponents; 3899 3900 CustomElements = { 3901 useNative: true, 3902 ready: true, 3903 takeRecords: function() {}, 3904 instanceof: function(obj, base) { 3905 return obj instanceof base; 3906 } 3907 }; 3908 3909 HTMLImports = { 3910 useNative: true 3911 }; 3912 3913 3914 addEventListener('HTMLImportsLoaded', function() { 3915 document.dispatchEvent( 3916 new CustomEvent('WebComponentsReady', {bubbles: true}) 3917 ); 3918 }); 3919 3920 3921 // ShadowDOM 3922 ShadowDOMPolyfill = null; 3923 wrap = unwrap = function(n){ 3924 return n; 3925 }; 3926 3927 } 3928 3929 /* 3930 Create polyfill scope and feature detect native support. 3931 */ 3932 window.HTMLImports = window.HTMLImports || {flags:{}}; 3933 3934 (function(scope) { 3935 3936 /** 3937 Basic setup and simple module executer. We collect modules and then execute 3938 the code later, only if it's necessary for polyfilling. 3939 */ 3940 var IMPORT_LINK_TYPE = 'import'; 3941 var useNative = Boolean(IMPORT_LINK_TYPE in document.createElement('link')); 3942 3943 /** 3944 Support `currentScript` on all browsers as `document._currentScript.` 3945 3946 NOTE: We cannot polyfill `document.currentScript` because it's not possible 3947 both to override and maintain the ability to capture the native value. 3948 Therefore we choose to expose `_currentScript` both when native imports 3949 and the polyfill are in use. 3950 */ 3951 // NOTE: ShadowDOMPolyfill intrusion. 3952 var hasShadowDOMPolyfill = Boolean(window.ShadowDOMPolyfill); 3953 var wrap = function(node) { 3954 return hasShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node) : node; 3955 }; 3956 var rootDocument = wrap(document); 3957 3958 var currentScriptDescriptor = { 3959 get: function() { 3960 var script = HTMLImports.currentScript || document.currentScript || 3961 // NOTE: only works when called in synchronously executing code. 3962 // readyState should check if `loading` but IE10 is 3963 // interactive when scripts run so we cheat. 3964 (document.readyState !== 'complete' ? 3965 document.scripts[document.scripts.length - 1] : null); 3966 return wrap(script); 3967 }, 3968 configurable: true 3969 }; 3970 3971 Object.defineProperty(document, '_currentScript', currentScriptDescriptor); 3972 Object.defineProperty(rootDocument, '_currentScript', currentScriptDescriptor); 3973 3974 /** 3975 Add support for the `HTMLImportsLoaded` event and the `HTMLImports.whenReady` 3976 method. This api is necessary because unlike the native implementation, 3977 script elements do not force imports to resolve. Instead, users should wrap 3978 code in either an `HTMLImportsLoaded` hander or after load time in an 3979 `HTMLImports.whenReady(callback)` call. 3980 3981 NOTE: This module also supports these apis under the native implementation. 3982 Therefore, if this file is loaded, the same code can be used under both 3983 the polyfill and native implementation. 3984 */ 3985 3986 var isIE = /Trident/.test(navigator.userAgent); 3987 3988 // call a callback when all HTMLImports in the document at call time 3989 // (or at least document ready) have loaded. 3990 // 1. ensure the document is in a ready state (has dom), then 3991 // 2. watch for loading of imports and call callback when done 3992 function whenReady(callback, doc) { 3993 doc = doc || rootDocument; 3994 // if document is loading, wait and try again 3995 whenDocumentReady(function() { 3996 watchImportsLoad(callback, doc); 3997 }, doc); 3998 } 3999 4000 // call the callback when the document is in a ready state (has dom) 4001 var requiredReadyState = isIE ? 'complete' : 'interactive'; 4002 var READY_EVENT = 'readystatechange'; 4003 function isDocumentReady(doc) { 4004 return (doc.readyState === 'complete' || 4005 doc.readyState === requiredReadyState); 4006 } 4007 4008 // call <callback> when we ensure the document is in a ready state 4009 function whenDocumentReady(callback, doc) { 4010 if (!isDocumentReady(doc)) { 4011 var checkReady = function() { 4012 if (doc.readyState === 'complete' || 4013 doc.readyState === requiredReadyState) { 4014 doc.removeEventListener(READY_EVENT, checkReady); 4015 whenDocumentReady(callback, doc); 4016 } 4017 }; 4018 doc.addEventListener(READY_EVENT, checkReady); 4019 } else if (callback) { 4020 callback(); 4021 } 4022 } 4023 4024 function markTargetLoaded(event) { 4025 event.target.__loaded = true; 4026 } 4027 4028 // call <callback> when we ensure all imports have loaded 4029 function watchImportsLoad(callback, doc) { 4030 var imports = doc.querySelectorAll('link[rel=import]'); 4031 var loaded = 0, l = imports.length; 4032 function checkDone(d) { 4033 if ((loaded == l) && callback) { 4034 callback(); 4035 } 4036 } 4037 function loadedImport(e) { 4038 markTargetLoaded(e); 4039 loaded++; 4040 checkDone(); 4041 } 4042 if (l) { 4043 for (var i=0, imp; (i<l) && (imp=imports[i]); i++) { 4044 if (isImportLoaded(imp)) { 4045 loadedImport.call(imp, {target: imp}); 4046 } else { 4047 imp.addEventListener('load', loadedImport); 4048 imp.addEventListener('error', loadedImport); 4049 } 4050 } 4051 } else { 4052 checkDone(); 4053 } 4054 } 4055 4056 // NOTE: test for native imports loading is based on explicitly watching 4057 // all imports (see below). 4058 // However, we cannot rely on this entirely without watching the entire document 4059 // for import links. For perf reasons, currently only head is watched. 4060 // Instead, we fallback to checking if the import property is available 4061 // and the document is not itself loading. 4062 function isImportLoaded(link) { 4063 return useNative ? link.__loaded || 4064 (link.import && link.import.readyState !== 'loading') : 4065 link.__importParsed; 4066 } 4067 4068 // TODO(sorvell): Workaround for 4069 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25007, should be removed when 4070 // this bug is addressed. 4071 // (1) Install a mutation observer to see when HTMLImports have loaded 4072 // (2) if this script is run during document load it will watch any existing 4073 // imports for loading. 4074 // 4075 // NOTE: The workaround has restricted functionality: (1) it's only compatible 4076 // with imports that are added to document.head since the mutation observer 4077 // watches only head for perf reasons, (2) it requires this script 4078 // to run before any imports have completed loading. 4079 if (useNative) { 4080 new MutationObserver(function(mxns) { 4081 for (var i=0, l=mxns.length, m; (i < l) && (m=mxns[i]); i++) { 4082 if (m.addedNodes) { 4083 handleImports(m.addedNodes); 4084 } 4085 } 4086 }).observe(document.head, {childList: true}); 4087 4088 function handleImports(nodes) { 4089 for (var i=0, l=nodes.length, n; (i<l) && (n=nodes[i]); i++) { 4090 if (isImport(n)) { 4091 handleImport(n); 4092 } 4093 } 4094 } 4095 4096 function isImport(element) { 4097 return element.localName === 'link' && element.rel === 'import'; 4098 } 4099 4100 function handleImport(element) { 4101 var loaded = element.import; 4102 if (loaded) { 4103 markTargetLoaded({target: element}); 4104 } else { 4105 element.addEventListener('load', markTargetLoaded); 4106 element.addEventListener('error', markTargetLoaded); 4107 } 4108 } 4109 4110 // make sure to catch any imports that are in the process of loading 4111 // when this script is run. 4112 (function() { 4113 if (document.readyState === 'loading') { 4114 var imports = document.querySelectorAll('link[rel=import]'); 4115 for (var i=0, l=imports.length, imp; (i<l) && (imp=imports[i]); i++) { 4116 handleImport(imp); 4117 } 4118 } 4119 })(); 4120 4121 } 4122 4123 // Fire the 'HTMLImportsLoaded' event when imports in document at load time 4124 // have loaded. This event is required to simulate the script blocking 4125 // behavior of native imports. A main document script that needs to be sure 4126 // imports have loaded should wait for this event. 4127 whenReady(function() { 4128 HTMLImports.ready = true; 4129 HTMLImports.readyTime = new Date().getTime(); 4130 rootDocument.dispatchEvent( 4131 new CustomEvent('HTMLImportsLoaded', {bubbles: true}) 4132 ); 4133 }); 4134 4135 // exports 4136 scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE; 4137 scope.useNative = useNative; 4138 scope.rootDocument = rootDocument; 4139 scope.whenReady = whenReady; 4140 scope.isIE = isIE; 4141 4142 })(HTMLImports); 4143 4144 (function(scope) { 4145 4146 // TODO(sorvell): It's desireable to provide a default stylesheet 4147 // that's convenient for styling unresolved elements, but 4148 // it's cumbersome to have to include this manually in every page. 4149 // It would make sense to put inside some HTMLImport but 4150 // the HTMLImports polyfill does not allow loading of stylesheets 4151 // that block rendering. Therefore this injection is tolerated here. 4152 var style = document.createElement('style'); 4153 style.textContent = '' 4154 + 'body {' 4155 + 'transition: opacity ease-in 0.2s;' 4156 + ' } \n' 4157 + 'body[unresolved] {' 4158 + 'opacity: 0; display: block; overflow: hidden;' 4159 + ' } \n' 4160 ; 4161 var head = document.querySelector('head'); 4162 head.insertBefore(style, head.firstChild); 4163 4164 })(Platform); 4165 4166 /* 4167 Build only script. 4168 4169 Ensures scripts needed for basic x-platform compatibility 4170 will be run when platform.js is not loaded. 4171 */ 4172 } 4173 (function(global) { 4174 'use strict'; 4175 4176 var testingExposeCycleCount = global.testingExposeCycleCount; 4177 4178 // Detect and do basic sanity checking on Object/Array.observe. 4179 function detectObjectObserve() { 4180 if (typeof Object.observe !== 'function' || 4181 typeof Array.observe !== 'function') { 4182 return false; 4183 } 4184 4185 var records = []; 4186 4187 function callback(recs) { 4188 records = recs; 4189 } 4190 4191 var test = {}; 4192 var arr = []; 4193 Object.observe(test, callback); 4194 Array.observe(arr, callback); 4195 test.id = 1; 4196 test.id = 2; 4197 delete test.id; 4198 arr.push(1, 2); 4199 arr.length = 0; 4200 4201 Object.deliverChangeRecords(callback); 4202 if (records.length !== 5) 4203 return false; 4204 4205 if (records[0].type != 'add' || 4206 records[1].type != 'update' || 4207 records[2].type != 'delete' || 4208 records[3].type != 'splice' || 4209 records[4].type != 'splice') { 4210 return false; 4211 } 4212 4213 Object.unobserve(test, callback); 4214 Array.unobserve(arr, callback); 4215 4216 return true; 4217 } 4218 4219 var hasObserve = detectObjectObserve(); 4220 4221 function detectEval() { 4222 // Don't test for eval if we're running in a Chrome App environment. 4223 // We check for APIs set that only exist in a Chrome App context. 4224 if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { 4225 return false; 4226 } 4227 4228 // Firefox OS Apps do not allow eval. This feature detection is very hacky 4229 // but even if some other platform adds support for this function this code 4230 // will continue to work. 4231 if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { 4232 return false; 4233 } 4234 4235 try { 4236 var f = new Function('', 'return true;'); 4237 return f(); 4238 } catch (ex) { 4239 return false; 4240 } 4241 } 4242 4243 var hasEval = detectEval(); 4244 4245 function isIndex(s) { 4246 return +s === s >>> 0 && s !== ''; 4247 } 4248 4249 function toNumber(s) { 4250 return +s; 4251 } 4252 4253 function isObject(obj) { 4254 return obj === Object(obj); 4255 } 4256 4257 var numberIsNaN = global.Number.isNaN || function(value) { 4258 return typeof value === 'number' && global.isNaN(value); 4259 } 4260 4261 function areSameValue(left, right) { 4262 if (left === right) 4263 return left !== 0 || 1 / left === 1 / right; 4264 if (numberIsNaN(left) && numberIsNaN(right)) 4265 return true; 4266 4267 return left !== left && right !== right; 4268 } 4269 4270 var createObject = ('__proto__' in {}) ? 4271 function(obj) { return obj; } : 4272 function(obj) { 4273 var proto = obj.__proto__; 4274 if (!proto) 4275 return obj; 4276 var newObject = Object.create(proto); 4277 Object.getOwnPropertyNames(obj).forEach(function(name) { 4278 Object.defineProperty(newObject, name, 4279 Object.getOwnPropertyDescriptor(obj, name)); 4280 }); 4281 return newObject; 4282 }; 4283 4284 var identStart = '[\$_a-zA-Z]'; 4285 var identPart = '[\$_a-zA-Z0-9]'; 4286 var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); 4287 4288 function getPathCharType(char) { 4289 if (char === undefined) 4290 return 'eof'; 4291 4292 var code = char.charCodeAt(0); 4293 4294 switch(code) { 4295 case 0x5B: // [ 4296 case 0x5D: // ] 4297 case 0x2E: // . 4298 case 0x22: // " 4299 case 0x27: // ' 4300 case 0x30: // 0 4301 return char; 4302 4303 case 0x5F: // _ 4304 case 0x24: // $ 4305 return 'ident'; 4306 4307 case 0x20: // Space 4308 case 0x09: // Tab 4309 case 0x0A: // Newline 4310 case 0x0D: // Return 4311 case 0xA0: // No-break space 4312 case 0xFEFF: // Byte Order Mark 4313 case 0x2028: // Line Separator 4314 case 0x2029: // Paragraph Separator 4315 return 'ws'; 4316 } 4317 4318 // a-z, A-Z 4319 if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) 4320 return 'ident'; 4321 4322 // 1-9 4323 if (0x31 <= code && code <= 0x39) 4324 return 'number'; 4325 4326 return 'else'; 4327 } 4328 4329 var pathStateMachine = { 4330 'beforePath': { 4331 'ws': ['beforePath'], 4332 'ident': ['inIdent', 'append'], 4333 '[': ['beforeElement'], 4334 'eof': ['afterPath'] 4335 }, 4336 4337 'inPath': { 4338 'ws': ['inPath'], 4339 '.': ['beforeIdent'], 4340 '[': ['beforeElement'], 4341 'eof': ['afterPath'] 4342 }, 4343 4344 'beforeIdent': { 4345 'ws': ['beforeIdent'], 4346 'ident': ['inIdent', 'append'] 4347 }, 4348 4349 'inIdent': { 4350 'ident': ['inIdent', 'append'], 4351 '0': ['inIdent', 'append'], 4352 'number': ['inIdent', 'append'], 4353 'ws': ['inPath', 'push'], 4354 '.': ['beforeIdent', 'push'], 4355 '[': ['beforeElement', 'push'], 4356 'eof': ['afterPath', 'push'] 4357 }, 4358 4359 'beforeElement': { 4360 'ws': ['beforeElement'], 4361 '0': ['afterZero', 'append'], 4362 'number': ['inIndex', 'append'], 4363 "'": ['inSingleQuote', 'append', ''], 4364 '"': ['inDoubleQuote', 'append', ''] 4365 }, 4366 4367 'afterZero': { 4368 'ws': ['afterElement', 'push'], 4369 ']': ['inPath', 'push'] 4370 }, 4371 4372 'inIndex': { 4373 '0': ['inIndex', 'append'], 4374 'number': ['inIndex', 'append'], 4375 'ws': ['afterElement'], 4376 ']': ['inPath', 'push'] 4377 }, 4378 4379 'inSingleQuote': { 4380 "'": ['afterElement'], 4381 'eof': ['error'], 4382 'else': ['inSingleQuote', 'append'] 4383 }, 4384 4385 'inDoubleQuote': { 4386 '"': ['afterElement'], 4387 'eof': ['error'], 4388 'else': ['inDoubleQuote', 'append'] 4389 }, 4390 4391 'afterElement': { 4392 'ws': ['afterElement'], 4393 ']': ['inPath', 'push'] 4394 } 4395 } 4396 4397 function noop() {} 4398 4399 function parsePath(path) { 4400 var keys = []; 4401 var index = -1; 4402 var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; 4403 4404 var actions = { 4405 push: function() { 4406 if (key === undefined) 4407 return; 4408 4409 keys.push(key); 4410 key = undefined; 4411 }, 4412 4413 append: function() { 4414 if (key === undefined) 4415 key = newChar 4416 else 4417 key += newChar; 4418 } 4419 }; 4420 4421 function maybeUnescapeQuote() { 4422 if (index >= path.length) 4423 return; 4424 4425 var nextChar = path[index + 1]; 4426 if ((mode == 'inSingleQuote' && nextChar == "'") || 4427 (mode == 'inDoubleQuote' && nextChar == '"')) { 4428 index++; 4429 newChar = nextChar; 4430 actions.append(); 4431 return true; 4432 } 4433 } 4434 4435 while (mode) { 4436 index++; 4437 c = path[index]; 4438 4439 if (c == '\\' && maybeUnescapeQuote(mode)) 4440 continue; 4441 4442 type = getPathCharType(c); 4443 typeMap = pathStateMachine[mode]; 4444 transition = typeMap[type] || typeMap['else'] || 'error'; 4445 4446 if (transition == 'error') 4447 return; // parse error; 4448 4449 mode = transition[0]; 4450 action = actions[transition[1]] || noop; 4451 newChar = transition[2] === undefined ? c : transition[2]; 4452 action(); 4453 4454 if (mode === 'afterPath') { 4455 return keys; 4456 } 4457 } 4458 4459 return; // parse error 4460 } 4461 4462 function isIdent(s) { 4463 return identRegExp.test(s); 4464 } 4465 4466 var constructorIsPrivate = {}; 4467 4468 function Path(parts, privateToken) { 4469 if (privateToken !== constructorIsPrivate) 4470 throw Error('Use Path.get to retrieve path objects'); 4471 4472 for (var i = 0; i < parts.length; i++) { 4473 this.push(String(parts[i])); 4474 } 4475 4476 if (hasEval && this.length) { 4477 this.getValueFrom = this.compiledGetValueFromFn(); 4478 } 4479 } 4480 4481 // TODO(rafaelw): Make simple LRU cache 4482 var pathCache = {}; 4483 4484 function getPath(pathString) { 4485 if (pathString instanceof Path) 4486 return pathString; 4487 4488 if (pathString == null || pathString.length == 0) 4489 pathString = ''; 4490 4491 if (typeof pathString != 'string') { 4492 if (isIndex(pathString.length)) { 4493 // Constructed with array-like (pre-parsed) keys 4494 return new Path(pathString, constructorIsPrivate); 4495 } 4496 4497 pathString = String(pathString); 4498 } 4499 4500 var path = pathCache[pathString]; 4501 if (path) 4502 return path; 4503 4504 var parts = parsePath(pathString); 4505 if (!parts) 4506 return invalidPath; 4507 4508 var path = new Path(parts, constructorIsPrivate); 4509 pathCache[pathString] = path; 4510 return path; 4511 } 4512 4513 Path.get = getPath; 4514 4515 function formatAccessor(key) { 4516 if (isIndex(key)) { 4517 return '[' + key + ']'; 4518 } else { 4519 return '["' + key.replace(/"/g, '\\"') + '"]'; 4520 } 4521 } 4522 4523 Path.prototype = createObject({ 4524 __proto__: [], 4525 valid: true, 4526 4527 toString: function() { 4528 var pathString = ''; 4529 for (var i = 0; i < this.length; i++) { 4530 var key = this[i]; 4531 if (isIdent(key)) { 4532 pathString += i ? '.' + key : key; 4533 } else { 4534 pathString += formatAccessor(key); 4535 } 4536 } 4537 4538 return pathString; 4539 }, 4540 4541 getValueFrom: function(obj, directObserver) { 4542 for (var i = 0; i < this.length; i++) { 4543 if (obj == null) 4544 return; 4545 obj = obj[this[i]]; 4546 } 4547 return obj; 4548 }, 4549 4550 iterateObjects: function(obj, observe) { 4551 for (var i = 0; i < this.length; i++) { 4552 if (i) 4553 obj = obj[this[i - 1]]; 4554 if (!isObject(obj)) 4555 return; 4556 observe(obj, this[i]); 4557 } 4558 }, 4559 4560 compiledGetValueFromFn: function() { 4561 var str = ''; 4562 var pathString = 'obj'; 4563 str += 'if (obj != null'; 4564 var i = 0; 4565 var key; 4566 for (; i < (this.length - 1); i++) { 4567 key = this[i]; 4568 pathString += isIdent(key) ? '.' + key : formatAccessor(key); 4569 str += ' &&\n ' + pathString + ' != null'; 4570 } 4571 str += ')\n'; 4572 4573 var key = this[i]; 4574 pathString += isIdent(key) ? '.' + key : formatAccessor(key); 4575 4576 str += ' return ' + pathString + ';\nelse\n return undefined;'; 4577 return new Function('obj', str); 4578 }, 4579 4580 setValueFrom: function(obj, value) { 4581 if (!this.length) 4582 return false; 4583 4584 for (var i = 0; i < this.length - 1; i++) { 4585 if (!isObject(obj)) 4586 return false; 4587 obj = obj[this[i]]; 4588 } 4589 4590 if (!isObject(obj)) 4591 return false; 4592 4593 obj[this[i]] = value; 4594 return true; 4595 } 4596 }); 4597 4598 var invalidPath = new Path('', constructorIsPrivate); 4599 invalidPath.valid = false; 4600 invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; 4601 4602 var MAX_DIRTY_CHECK_CYCLES = 1000; 4603 4604 function dirtyCheck(observer) { 4605 var cycles = 0; 4606 while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { 4607 cycles++; 4608 } 4609 if (testingExposeCycleCount) 4610 global.dirtyCheckCycleCount = cycles; 4611 4612 return cycles > 0; 4613 } 4614 4615 function objectIsEmpty(object) { 4616 for (var prop in object) 4617 return false; 4618 return true; 4619 } 4620 4621 function diffIsEmpty(diff) { 4622 return objectIsEmpty(diff.added) && 4623 objectIsEmpty(diff.removed) && 4624 objectIsEmpty(diff.changed); 4625 } 4626 4627 function diffObjectFromOldObject(object, oldObject) { 4628 var added = {}; 4629 var removed = {}; 4630 var changed = {}; 4631 4632 for (var prop in oldObject) { 4633 var newValue = object[prop]; 4634 4635 if (newValue !== undefined && newValue === oldObject[prop]) 4636 continue; 4637 4638 if (!(prop in object)) { 4639 removed[prop] = undefined; 4640 continue; 4641 } 4642 4643 if (newValue !== oldObject[prop]) 4644 changed[prop] = newValue; 4645 } 4646 4647 for (var prop in object) { 4648 if (prop in oldObject) 4649 continue; 4650 4651 added[prop] = object[prop]; 4652 } 4653 4654 if (Array.isArray(object) && object.length !== oldObject.length) 4655 changed.length = object.length; 4656 4657 return { 4658 added: added, 4659 removed: removed, 4660 changed: changed 4661 }; 4662 } 4663 4664 var eomTasks = []; 4665 function runEOMTasks() { 4666 if (!eomTasks.length) 4667 return false; 4668 4669 for (var i = 0; i < eomTasks.length; i++) { 4670 eomTasks[i](); 4671 } 4672 eomTasks.length = 0; 4673 return true; 4674 } 4675 4676 var runEOM = hasObserve ? (function(){ 4677 return function(fn) { 4678 return Promise.resolve().then(fn); 4679 } 4680 })() : 4681 (function() { 4682 return function(fn) { 4683 eomTasks.push(fn); 4684 }; 4685 })(); 4686 4687 var observedObjectCache = []; 4688 4689 function newObservedObject() { 4690 var observer; 4691 var object; 4692 var discardRecords = false; 4693 var first = true; 4694 4695 function callback(records) { 4696 if (observer && observer.state_ === OPENED && !discardRecords) 4697 observer.check_(records); 4698 } 4699 4700 return { 4701 open: function(obs) { 4702 if (observer) 4703 throw Error('ObservedObject in use'); 4704 4705 if (!first) 4706 Object.deliverChangeRecords(callback); 4707 4708 observer = obs; 4709 first = false; 4710 }, 4711 observe: function(obj, arrayObserve) { 4712 object = obj; 4713 if (arrayObserve) 4714 Array.observe(object, callback); 4715 else 4716 Object.observe(object, callback); 4717 }, 4718 deliver: function(discard) { 4719 discardRecords = discard; 4720 Object.deliverChangeRecords(callback); 4721 discardRecords = false; 4722 }, 4723 close: function() { 4724 observer = undefined; 4725 Object.unobserve(object, callback); 4726 observedObjectCache.push(this); 4727 } 4728 }; 4729 } 4730 4731 /* 4732 * The observedSet abstraction is a perf optimization which reduces the total 4733 * number of Object.observe observations of a set of objects. The idea is that 4734 * groups of Observers will have some object dependencies in common and this 4735 * observed set ensures that each object in the transitive closure of 4736 * dependencies is only observed once. The observedSet acts as a write barrier 4737 * such that whenever any change comes through, all Observers are checked for 4738 * changed values. 4739 * 4740 * Note that this optimization is explicitly moving work from setup-time to 4741 * change-time. 4742 * 4743 * TODO(rafaelw): Implement "garbage collection". In order to move work off 4744 * the critical path, when Observers are closed, their observed objects are 4745 * not Object.unobserve(d). As a result, it's possible that if the observedSet 4746 * is kept open, but some Observers have been closed, it could cause "leaks" 4747 * (prevent otherwise collectable objects from being collected). At some 4748 * point, we should implement incremental "gc" which keeps a list of 4749 * observedSets which may need clean-up and does small amounts of cleanup on a 4750 * timeout until all is clean. 4751 */ 4752 4753 function getObservedObject(observer, object, arrayObserve) { 4754 var dir = observedObjectCache.pop() || newObservedObject(); 4755 dir.open(observer); 4756 dir.observe(object, arrayObserve); 4757 return dir; 4758 } 4759 4760 var observedSetCache = []; 4761 4762 function newObservedSet() { 4763 var observerCount = 0; 4764 var observers = []; 4765 var objects = []; 4766 var rootObj; 4767 var rootObjProps; 4768 4769 function observe(obj, prop) { 4770 if (!obj) 4771 return; 4772 4773 if (obj === rootObj) 4774 rootObjProps[prop] = true; 4775 4776 if (objects.indexOf(obj) < 0) { 4777 objects.push(obj); 4778 Object.observe(obj, callback); 4779 } 4780 4781 observe(Object.getPrototypeOf(obj), prop); 4782 } 4783 4784 function allRootObjNonObservedProps(recs) { 4785 for (var i = 0; i < recs.length; i++) { 4786 var rec = recs[i]; 4787 if (rec.object !== rootObj || 4788 rootObjProps[rec.name] || 4789 rec.type === 'setPrototype') { 4790 return false; 4791 } 4792 } 4793 return true; 4794 } 4795 4796 function callback(recs) { 4797 if (allRootObjNonObservedProps(recs)) 4798 return; 4799 4800 var observer; 4801 for (var i = 0; i < observers.length; i++) { 4802 observer = observers[i]; 4803 if (observer.state_ == OPENED) { 4804 observer.iterateObjects_(observe); 4805 } 4806 } 4807 4808 for (var i = 0; i < observers.length; i++) { 4809 observer = observers[i]; 4810 if (observer.state_ == OPENED) { 4811 observer.check_(); 4812 } 4813 } 4814 } 4815 4816 var record = { 4817 objects: objects, 4818 get rootObject() { return rootObj; }, 4819 set rootObject(value) { 4820 rootObj = value; 4821 rootObjProps = {}; 4822 }, 4823 open: function(obs, object) { 4824 observers.push(obs); 4825 observerCount++; 4826 obs.iterateObjects_(observe); 4827 }, 4828 close: function(obs) { 4829 observerCount--; 4830 if (observerCount > 0) { 4831 return; 4832 } 4833 4834 for (var i = 0; i < objects.length; i++) { 4835 Object.unobserve(objects[i], callback); 4836 Observer.unobservedCount++; 4837 } 4838 4839 observers.length = 0; 4840 objects.length = 0; 4841 rootObj = undefined; 4842 rootObjProps = undefined; 4843 observedSetCache.push(this); 4844 if (lastObservedSet === this) 4845 lastObservedSet = null; 4846 }, 4847 }; 4848 4849 return record; 4850 } 4851 4852 var lastObservedSet; 4853 4854 function getObservedSet(observer, obj) { 4855 if (!lastObservedSet || lastObservedSet.rootObject !== obj) { 4856 lastObservedSet = observedSetCache.pop() || newObservedSet(); 4857 lastObservedSet.rootObject = obj; 4858 } 4859 lastObservedSet.open(observer, obj); 4860 return lastObservedSet; 4861 } 4862 4863 var UNOPENED = 0; 4864 var OPENED = 1; 4865 var CLOSED = 2; 4866 var RESETTING = 3; 4867 4868 var nextObserverId = 1; 4869 4870 function Observer() { 4871 this.state_ = UNOPENED; 4872 this.callback_ = undefined; 4873 this.target_ = undefined; // TODO(rafaelw): Should be WeakRef 4874 this.directObserver_ = undefined; 4875 this.value_ = undefined; 4876 this.id_ = nextObserverId++; 4877 } 4878 4879 Observer.prototype = { 4880 open: function(callback, target) { 4881 if (this.state_ != UNOPENED) 4882 throw Error('Observer has already been opened.'); 4883 4884 addToAll(this); 4885 this.callback_ = callback; 4886 this.target_ = target; 4887 this.connect_(); 4888 this.state_ = OPENED; 4889 return this.value_; 4890 }, 4891 4892 close: function() { 4893 if (this.state_ != OPENED) 4894 return; 4895 4896 removeFromAll(this); 4897 this.disconnect_(); 4898 this.value_ = undefined; 4899 this.callback_ = undefined; 4900 this.target_ = undefined; 4901 this.state_ = CLOSED; 4902 }, 4903 4904 deliver: function() { 4905 if (this.state_ != OPENED) 4906 return; 4907 4908 dirtyCheck(this); 4909 }, 4910 4911 report_: function(changes) { 4912 try { 4913 this.callback_.apply(this.target_, changes); 4914 } catch (ex) { 4915 Observer._errorThrownDuringCallback = true; 4916 console.error('Exception caught during observer callback: ' + 4917 (ex.stack || ex)); 4918 } 4919 }, 4920 4921 discardChanges: function() { 4922 this.check_(undefined, true); 4923 return this.value_; 4924 } 4925 } 4926 4927 var collectObservers = !hasObserve; 4928 var allObservers; 4929 Observer._allObserversCount = 0; 4930 4931 if (collectObservers) { 4932 allObservers = []; 4933 } 4934 4935 function addToAll(observer) { 4936 Observer._allObserversCount++; 4937 if (!collectObservers) 4938 return; 4939 4940 allObservers.push(observer); 4941 } 4942 4943 function removeFromAll(observer) { 4944 Observer._allObserversCount--; 4945 } 4946 4947 var runningMicrotaskCheckpoint = false; 4948 4949 global.Platform = global.Platform || {}; 4950 4951 global.Platform.performMicrotaskCheckpoint = function() { 4952 if (runningMicrotaskCheckpoint) 4953 return; 4954 4955 if (!collectObservers) 4956 return; 4957 4958 runningMicrotaskCheckpoint = true; 4959 4960 var cycles = 0; 4961 var anyChanged, toCheck; 4962 4963 do { 4964 cycles++; 4965 toCheck = allObservers; 4966 allObservers = []; 4967 anyChanged = false; 4968 4969 for (var i = 0; i < toCheck.length; i++) { 4970 var observer = toCheck[i]; 4971 if (observer.state_ != OPENED) 4972 continue; 4973 4974 if (observer.check_()) 4975 anyChanged = true; 4976 4977 allObservers.push(observer); 4978 } 4979 if (runEOMTasks()) 4980 anyChanged = true; 4981 } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); 4982 4983 if (testingExposeCycleCount) 4984 global.dirtyCheckCycleCount = cycles; 4985 4986 runningMicrotaskCheckpoint = false; 4987 }; 4988 4989 if (collectObservers) { 4990 global.Platform.clearObservers = function() { 4991 allObservers = []; 4992 }; 4993 } 4994 4995 function ObjectObserver(object) { 4996 Observer.call(this); 4997 this.value_ = object; 4998 this.oldObject_ = undefined; 4999 } 5000 5001 ObjectObserver.prototype = createObject({ 5002 __proto__: Observer.prototype, 5003 5004 arrayObserve: false, 5005 5006 connect_: function(callback, target) { 5007 if (hasObserve) { 5008 this.directObserver_ = getObservedObject(this, this.value_, 5009 this.arrayObserve); 5010 } else { 5011 this.oldObject_ = this.copyObject(this.value_); 5012 } 5013 5014 }, 5015 5016 copyObject: function(object) { 5017 var copy = Array.isArray(object) ? [] : {}; 5018 for (var prop in object) { 5019 copy[prop] = object[prop]; 5020 }; 5021 if (Array.isArray(object)) 5022 copy.length = object.length; 5023 return copy; 5024 }, 5025 5026 check_: function(changeRecords, skipChanges) { 5027 var diff; 5028 var oldValues; 5029 if (hasObserve) { 5030 if (!changeRecords) 5031 return false; 5032 5033 oldValues = {}; 5034 diff = diffObjectFromChangeRecords(this.value_, changeRecords, 5035 oldValues); 5036 } else { 5037 oldValues = this.oldObject_; 5038 diff = diffObjectFromOldObject(this.value_, this.oldObject_); 5039 } 5040 5041 if (diffIsEmpty(diff)) 5042 return false; 5043 5044 if (!hasObserve) 5045 this.oldObject_ = this.copyObject(this.value_); 5046 5047 this.report_([ 5048 diff.added || {}, 5049 diff.removed || {}, 5050 diff.changed || {}, 5051 function(property) { 5052 return oldValues[property]; 5053 } 5054 ]); 5055 5056 return true; 5057 }, 5058 5059 disconnect_: function() { 5060 if (hasObserve) { 5061 this.directObserver_.close(); 5062 this.directObserver_ = undefined; 5063 } else { 5064 this.oldObject_ = undefined; 5065 } 5066 }, 5067 5068 deliver: function() { 5069 if (this.state_ != OPENED) 5070 return; 5071 5072 if (hasObserve) 5073 this.directObserver_.deliver(false); 5074 else 5075 dirtyCheck(this); 5076 }, 5077 5078 discardChanges: function() { 5079 if (this.directObserver_) 5080 this.directObserver_.deliver(true); 5081 else 5082 this.oldObject_ = this.copyObject(this.value_); 5083 5084 return this.value_; 5085 } 5086 }); 5087 5088 function ArrayObserver(array) { 5089 if (!Array.isArray(array)) 5090 throw Error('Provided object is not an Array'); 5091 ObjectObserver.call(this, array); 5092 } 5093 5094 ArrayObserver.prototype = createObject({ 5095 5096 __proto__: ObjectObserver.prototype, 5097 5098 arrayObserve: true, 5099 5100 copyObject: function(arr) { 5101 return arr.slice(); 5102 }, 5103 5104 check_: function(changeRecords) { 5105 var splices; 5106 if (hasObserve) { 5107 if (!changeRecords) 5108 return false; 5109 splices = projectArraySplices(this.value_, changeRecords); 5110 } else { 5111 splices = calcSplices(this.value_, 0, this.value_.length, 5112 this.oldObject_, 0, this.oldObject_.length); 5113 } 5114 5115 if (!splices || !splices.length) 5116 return false; 5117 5118 if (!hasObserve) 5119 this.oldObject_ = this.copyObject(this.value_); 5120 5121 this.report_([splices]); 5122 return true; 5123 } 5124 }); 5125 5126 ArrayObserver.applySplices = function(previous, current, splices) { 5127 splices.forEach(function(splice) { 5128 var spliceArgs = [splice.index, splice.removed.length]; 5129 var addIndex = splice.index; 5130 while (addIndex < splice.index + splice.addedCount) { 5131 spliceArgs.push(current[addIndex]); 5132 addIndex++; 5133 } 5134 5135 Array.prototype.splice.apply(previous, spliceArgs); 5136 }); 5137 }; 5138 5139 function PathObserver(object, path) { 5140 Observer.call(this); 5141 5142 this.object_ = object; 5143 this.path_ = getPath(path); 5144 this.directObserver_ = undefined; 5145 } 5146 5147 PathObserver.prototype = createObject({ 5148 __proto__: Observer.prototype, 5149 5150 get path() { 5151 return this.path_; 5152 }, 5153 5154 connect_: function() { 5155 if (hasObserve) 5156 this.directObserver_ = getObservedSet(this, this.object_); 5157 5158 this.check_(undefined, true); 5159 }, 5160 5161 disconnect_: function() { 5162 this.value_ = undefined; 5163 5164 if (this.directObserver_) { 5165 this.directObserver_.close(this); 5166 this.directObserver_ = undefined; 5167 } 5168 }, 5169 5170 iterateObjects_: function(observe) { 5171 this.path_.iterateObjects(this.object_, observe); 5172 }, 5173 5174 check_: function(changeRecords, skipChanges) { 5175 var oldValue = this.value_; 5176 this.value_ = this.path_.getValueFrom(this.object_); 5177 if (skipChanges || areSameValue(this.value_, oldValue)) 5178 return false; 5179 5180 this.report_([this.value_, oldValue, this]); 5181 return true; 5182 }, 5183 5184 setValue: function(newValue) { 5185 if (this.path_) 5186 this.path_.setValueFrom(this.object_, newValue); 5187 } 5188 }); 5189 5190 function CompoundObserver(reportChangesOnOpen) { 5191 Observer.call(this); 5192 5193 this.reportChangesOnOpen_ = reportChangesOnOpen; 5194 this.value_ = []; 5195 this.directObserver_ = undefined; 5196 this.observed_ = []; 5197 } 5198 5199 var observerSentinel = {}; 5200 5201 CompoundObserver.prototype = createObject({ 5202 __proto__: Observer.prototype, 5203 5204 connect_: function() { 5205 if (hasObserve) { 5206 var object; 5207 var needsDirectObserver = false; 5208 for (var i = 0; i < this.observed_.length; i += 2) { 5209 object = this.observed_[i] 5210 if (object !== observerSentinel) { 5211 needsDirectObserver = true; 5212 break; 5213 } 5214 } 5215 5216 if (needsDirectObserver) 5217 this.directObserver_ = getObservedSet(this, object); 5218 } 5219 5220 this.check_(undefined, !this.reportChangesOnOpen_); 5221 }, 5222 5223 disconnect_: function() { 5224 for (var i = 0; i < this.observed_.length; i += 2) { 5225 if (this.observed_[i] === observerSentinel) 5226 this.observed_[i + 1].close(); 5227 } 5228 this.observed_.length = 0; 5229 this.value_.length = 0; 5230 5231 if (this.directObserver_) { 5232 this.directObserver_.close(this); 5233 this.directObserver_ = undefined; 5234 } 5235 }, 5236 5237 addPath: function(object, path) { 5238 if (this.state_ != UNOPENED && this.state_ != RESETTING) 5239 throw Error('Cannot add paths once started.'); 5240 5241 var path = getPath(path); 5242 this.observed_.push(object, path); 5243 if (!this.reportChangesOnOpen_) 5244 return; 5245 var index = this.observed_.length / 2 - 1; 5246 this.value_[index] = path.getValueFrom(object); 5247 }, 5248 5249 addObserver: function(observer) { 5250 if (this.state_ != UNOPENED && this.state_ != RESETTING) 5251 throw Error('Cannot add observers once started.'); 5252 5253 this.observed_.push(observerSentinel, observer); 5254 if (!this.reportChangesOnOpen_) 5255 return; 5256 var index = this.observed_.length / 2 - 1; 5257 this.value_[index] = observer.open(this.deliver, this); 5258 }, 5259 5260 startReset: function() { 5261 if (this.state_ != OPENED) 5262 throw Error('Can only reset while open'); 5263 5264 this.state_ = RESETTING; 5265 this.disconnect_(); 5266 }, 5267 5268 finishReset: function() { 5269 if (this.state_ != RESETTING) 5270 throw Error('Can only finishReset after startReset'); 5271 this.state_ = OPENED; 5272 this.connect_(); 5273 5274 return this.value_; 5275 }, 5276 5277 iterateObjects_: function(observe) { 5278 var object; 5279 for (var i = 0; i < this.observed_.length; i += 2) { 5280 object = this.observed_[i] 5281 if (object !== observerSentinel) 5282 this.observed_[i + 1].iterateObjects(object, observe) 5283 } 5284 }, 5285 5286 check_: function(changeRecords, skipChanges) { 5287 var oldValues; 5288 for (var i = 0; i < this.observed_.length; i += 2) { 5289 var object = this.observed_[i]; 5290 var path = this.observed_[i+1]; 5291 var value; 5292 if (object === observerSentinel) { 5293 var observable = path; 5294 value = this.state_ === UNOPENED ? 5295 observable.open(this.deliver, this) : 5296 observable.discardChanges(); 5297 } else { 5298 value = path.getValueFrom(object); 5299 } 5300 5301 if (skipChanges) { 5302 this.value_[i / 2] = value; 5303 continue; 5304 } 5305 5306 if (areSameValue(value, this.value_[i / 2])) 5307 continue; 5308 5309 oldValues = oldValues || []; 5310 oldValues[i / 2] = this.value_[i / 2]; 5311 this.value_[i / 2] = value; 5312 } 5313 5314 if (!oldValues) 5315 return false; 5316 5317 // TODO(rafaelw): Having observed_ as the third callback arg here is 5318 // pretty lame API. Fix. 5319 this.report_([this.value_, oldValues, this.observed_]); 5320 return true; 5321 } 5322 }); 5323 5324 function identFn(value) { return value; } 5325 5326 function ObserverTransform(observable, getValueFn, setValueFn, 5327 dontPassThroughSet) { 5328 this.callback_ = undefined; 5329 this.target_ = undefined; 5330 this.value_ = undefined; 5331 this.observable_ = observable; 5332 this.getValueFn_ = getValueFn || identFn; 5333 this.setValueFn_ = setValueFn || identFn; 5334 // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this 5335 // at the moment because of a bug in it's dependency tracking. 5336 this.dontPassThroughSet_ = dontPassThroughSet; 5337 } 5338 5339 ObserverTransform.prototype = { 5340 open: function(callback, target) { 5341 this.callback_ = callback; 5342 this.target_ = target; 5343 this.value_ = 5344 this.getValueFn_(this.observable_.open(this.observedCallback_, this)); 5345 return this.value_; 5346 }, 5347 5348 observedCallback_: function(value) { 5349 value = this.getValueFn_(value); 5350 if (areSameValue(value, this.value_)) 5351 return; 5352 var oldValue = this.value_; 5353 this.value_ = value; 5354 this.callback_.call(this.target_, this.value_, oldValue); 5355 }, 5356 5357 discardChanges: function() { 5358 this.value_ = this.getValueFn_(this.observable_.discardChanges()); 5359 return this.value_; 5360 }, 5361 5362 deliver: function() { 5363 return this.observable_.deliver(); 5364 }, 5365 5366 setValue: function(value) { 5367 value = this.setValueFn_(value); 5368 if (!this.dontPassThroughSet_ && this.observable_.setValue) 5369 return this.observable_.setValue(value); 5370 }, 5371 5372 close: function() { 5373 if (this.observable_) 5374 this.observable_.close(); 5375 this.callback_ = undefined; 5376 this.target_ = undefined; 5377 this.observable_ = undefined; 5378 this.value_ = undefined; 5379 this.getValueFn_ = undefined; 5380 this.setValueFn_ = undefined; 5381 } 5382 } 5383 5384 var expectedRecordTypes = { 5385 add: true, 5386 update: true, 5387 delete: true 5388 }; 5389 5390 function diffObjectFromChangeRecords(object, changeRecords, oldValues) { 5391 var added = {}; 5392 var removed = {}; 5393 5394 for (var i = 0; i < changeRecords.length; i++) { 5395 var record = changeRecords[i]; 5396 if (!expectedRecordTypes[record.type]) { 5397 console.error('Unknown changeRecord type: ' + record.type); 5398 console.error(record); 5399 continue; 5400 } 5401 5402 if (!(record.name in oldValues)) 5403 oldValues[record.name] = record.oldValue; 5404 5405 if (record.type == 'update') 5406 continue; 5407 5408 if (record.type == 'add') { 5409 if (record.name in removed) 5410 delete removed[record.name]; 5411 else 5412 added[record.name] = true; 5413 5414 continue; 5415 } 5416 5417 // type = 'delete' 5418 if (record.name in added) { 5419 delete added[record.name]; 5420 delete oldValues[record.name]; 5421 } else { 5422 removed[record.name] = true; 5423 } 5424 } 5425 5426 for (var prop in added) 5427 added[prop] = object[prop]; 5428 5429 for (var prop in removed) 5430 removed[prop] = undefined; 5431 5432 var changed = {}; 5433 for (var prop in oldValues) { 5434 if (prop in added || prop in removed) 5435 continue; 5436 5437 var newValue = object[prop]; 5438 if (oldValues[prop] !== newValue) 5439 changed[prop] = newValue; 5440 } 5441 5442 return { 5443 added: added, 5444 removed: removed, 5445 changed: changed 5446 }; 5447 } 5448 5449 function newSplice(index, removed, addedCount) { 5450 return { 5451 index: index, 5452 removed: removed, 5453 addedCount: addedCount 5454 }; 5455 } 5456 5457 var EDIT_LEAVE = 0; 5458 var EDIT_UPDATE = 1; 5459 var EDIT_ADD = 2; 5460 var EDIT_DELETE = 3; 5461 5462 function ArraySplice() {} 5463 5464 ArraySplice.prototype = { 5465 5466 // Note: This function is *based* on the computation of the Levenshtein 5467 // "edit" distance. The one change is that "updates" are treated as two 5468 // edits - not one. With Array splices, an update is really a delete 5469 // followed by an add. By retaining this, we optimize for "keeping" the 5470 // maximum array items in the original array. For example: 5471 // 5472 // 'xxxx123' -> '123yyyy' 5473 // 5474 // With 1-edit updates, the shortest path would be just to update all seven 5475 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This 5476 // leaves the substring '123' intact. 5477 calcEditDistances: function(current, currentStart, currentEnd, 5478 old, oldStart, oldEnd) { 5479 // "Deletion" columns 5480 var rowCount = oldEnd - oldStart + 1; 5481 var columnCount = currentEnd - currentStart + 1; 5482 var distances = new Array(rowCount); 5483 5484 // "Addition" rows. Initialize null column. 5485 for (var i = 0; i < rowCount; i++) { 5486 distances[i] = new Array(columnCount); 5487 distances[i][0] = i; 5488 } 5489 5490 // Initialize null row 5491 for (var j = 0; j < columnCount; j++) 5492 distances[0][j] = j; 5493 5494 for (var i = 1; i < rowCount; i++) { 5495 for (var j = 1; j < columnCount; j++) { 5496 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) 5497 distances[i][j] = distances[i - 1][j - 1]; 5498 else { 5499 var north = distances[i - 1][j] + 1; 5500 var west = distances[i][j - 1] + 1; 5501 distances[i][j] = north < west ? north : west; 5502 } 5503 } 5504 } 5505 5506 return distances; 5507 }, 5508 5509 // This starts at the final weight, and walks "backward" by finding 5510 // the minimum previous weight recursively until the origin of the weight 5511 // matrix. 5512 spliceOperationsFromEditDistances: function(distances) { 5513 var i = distances.length - 1; 5514 var j = distances[0].length - 1; 5515 var current = distances[i][j]; 5516 var edits = []; 5517 while (i > 0 || j > 0) { 5518 if (i == 0) { 5519 edits.push(EDIT_ADD); 5520 j--; 5521 continue; 5522 } 5523 if (j == 0) { 5524 edits.push(EDIT_DELETE); 5525 i--; 5526 continue; 5527 } 5528 var northWest = distances[i - 1][j - 1]; 5529 var west = distances[i - 1][j]; 5530 var north = distances[i][j - 1]; 5531 5532 var min; 5533 if (west < north) 5534 min = west < northWest ? west : northWest; 5535 else 5536 min = north < northWest ? north : northWest; 5537 5538 if (min == northWest) { 5539 if (northWest == current) { 5540 edits.push(EDIT_LEAVE); 5541 } else { 5542 edits.push(EDIT_UPDATE); 5543 current = northWest; 5544 } 5545 i--; 5546 j--; 5547 } else if (min == west) { 5548 edits.push(EDIT_DELETE); 5549 i--; 5550 current = west; 5551 } else { 5552 edits.push(EDIT_ADD); 5553 j--; 5554 current = north; 5555 } 5556 } 5557 5558 edits.reverse(); 5559 return edits; 5560 }, 5561 5562 /** 5563 * Splice Projection functions: 5564 * 5565 * A splice map is a representation of how a previous array of items 5566 * was transformed into a new array of items. Conceptually it is a list of 5567 * tuples of 5568 * 5569 * <index, removed, addedCount> 5570 * 5571 * which are kept in ascending index order of. The tuple represents that at 5572 * the |index|, |removed| sequence of items were removed, and counting forward 5573 * from |index|, |addedCount| items were added. 5574 */ 5575 5576 /** 5577 * Lacking individual splice mutation information, the minimal set of 5578 * splices can be synthesized given the previous state and final state of an 5579 * array. The basic approach is to calculate the edit distance matrix and 5580 * choose the shortest path through it. 5581 * 5582 * Complexity: O(l * p) 5583 * l: The length of the current array 5584 * p: The length of the old array 5585 */ 5586 calcSplices: function(current, currentStart, currentEnd, 5587 old, oldStart, oldEnd) { 5588 var prefixCount = 0; 5589 var suffixCount = 0; 5590 5591 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); 5592 if (currentStart == 0 && oldStart == 0) 5593 prefixCount = this.sharedPrefix(current, old, minLength); 5594 5595 if (currentEnd == current.length && oldEnd == old.length) 5596 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); 5597 5598 currentStart += prefixCount; 5599 oldStart += prefixCount; 5600 currentEnd -= suffixCount; 5601 oldEnd -= suffixCount; 5602 5603 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) 5604 return []; 5605 5606 if (currentStart == currentEnd) { 5607 var splice = newSplice(currentStart, [], 0); 5608 while (oldStart < oldEnd) 5609 splice.removed.push(old[oldStart++]); 5610 5611 return [ splice ]; 5612 } else if (oldStart == oldEnd) 5613 return [ newSplice(currentStart, [], currentEnd - currentStart) ]; 5614 5615 var ops = this.spliceOperationsFromEditDistances( 5616 this.calcEditDistances(current, currentStart, currentEnd, 5617 old, oldStart, oldEnd)); 5618 5619 var splice = undefined; 5620 var splices = []; 5621 var index = currentStart; 5622 var oldIndex = oldStart; 5623 for (var i = 0; i < ops.length; i++) { 5624 switch(ops[i]) { 5625 case EDIT_LEAVE: 5626 if (splice) { 5627 splices.push(splice); 5628 splice = undefined; 5629 } 5630 5631 index++; 5632 oldIndex++; 5633 break; 5634 case EDIT_UPDATE: 5635 if (!splice) 5636 splice = newSplice(index, [], 0); 5637 5638 splice.addedCount++; 5639 index++; 5640 5641 splice.removed.push(old[oldIndex]); 5642 oldIndex++; 5643 break; 5644 case EDIT_ADD: 5645 if (!splice) 5646 splice = newSplice(index, [], 0); 5647 5648 splice.addedCount++; 5649 index++; 5650 break; 5651 case EDIT_DELETE: 5652 if (!splice) 5653 splice = newSplice(index, [], 0); 5654 5655 splice.removed.push(old[oldIndex]); 5656 oldIndex++; 5657 break; 5658 } 5659 } 5660 5661 if (splice) { 5662 splices.push(splice); 5663 } 5664 return splices; 5665 }, 5666 5667 sharedPrefix: function(current, old, searchLength) { 5668 for (var i = 0; i < searchLength; i++) 5669 if (!this.equals(current[i], old[i])) 5670 return i; 5671 return searchLength; 5672 }, 5673 5674 sharedSuffix: function(current, old, searchLength) { 5675 var index1 = current.length; 5676 var index2 = old.length; 5677 var count = 0; 5678 while (count < searchLength && this.equals(current[--index1], old[--index2])) 5679 count++; 5680 5681 return count; 5682 }, 5683 5684 calculateSplices: function(current, previous) { 5685 return this.calcSplices(current, 0, current.length, previous, 0, 5686 previous.length); 5687 }, 5688 5689 equals: function(currentValue, previousValue) { 5690 return currentValue === previousValue; 5691 } 5692 }; 5693 5694 var arraySplice = new ArraySplice(); 5695 5696 function calcSplices(current, currentStart, currentEnd, 5697 old, oldStart, oldEnd) { 5698 return arraySplice.calcSplices(current, currentStart, currentEnd, 5699 old, oldStart, oldEnd); 5700 } 5701 5702 function intersect(start1, end1, start2, end2) { 5703 // Disjoint 5704 if (end1 < start2 || end2 < start1) 5705 return -1; 5706 5707 // Adjacent 5708 if (end1 == start2 || end2 == start1) 5709 return 0; 5710 5711 // Non-zero intersect, span1 first 5712 if (start1 < start2) { 5713 if (end1 < end2) 5714 return end1 - start2; // Overlap 5715 else 5716 return end2 - start2; // Contained 5717 } else { 5718 // Non-zero intersect, span2 first 5719 if (end2 < end1) 5720 return end2 - start1; // Overlap 5721 else 5722 return end1 - start1; // Contained 5723 } 5724 } 5725 5726 function mergeSplice(splices, index, removed, addedCount) { 5727 5728 var splice = newSplice(index, removed, addedCount); 5729 5730 var inserted = false; 5731 var insertionOffset = 0; 5732 5733 for (var i = 0; i < splices.length; i++) { 5734 var current = splices[i]; 5735 current.index += insertionOffset; 5736 5737 if (inserted) 5738 continue; 5739 5740 var intersectCount = intersect(splice.index, 5741 splice.index + splice.removed.length, 5742 current.index, 5743 current.index + current.addedCount); 5744 5745 if (intersectCount >= 0) { 5746 // Merge the two splices 5747 5748 splices.splice(i, 1); 5749 i--; 5750 5751 insertionOffset -= current.addedCount - current.removed.length; 5752 5753 splice.addedCount += current.addedCount - intersectCount; 5754 var deleteCount = splice.removed.length + 5755 current.removed.length - intersectCount; 5756 5757 if (!splice.addedCount && !deleteCount) { 5758 // merged splice is a noop. discard. 5759 inserted = true; 5760 } else { 5761 var removed = current.removed; 5762 5763 if (splice.index < current.index) { 5764 // some prefix of splice.removed is prepended to current.removed. 5765 var prepend = splice.removed.slice(0, current.index - splice.index); 5766 Array.prototype.push.apply(prepend, removed); 5767 removed = prepend; 5768 } 5769 5770 if (splice.index + splice.removed.length > current.index + current.addedCount) { 5771 // some suffix of splice.removed is appended to current.removed. 5772 var append = splice.removed.slice(current.index + current.addedCount - splice.index); 5773 Array.prototype.push.apply(removed, append); 5774 } 5775 5776 splice.removed = removed; 5777 if (current.index < splice.index) { 5778 splice.index = current.index; 5779 } 5780 } 5781 } else if (splice.index < current.index) { 5782 // Insert splice here. 5783 5784 inserted = true; 5785 5786 splices.splice(i, 0, splice); 5787 i++; 5788 5789 var offset = splice.addedCount - splice.removed.length 5790 current.index += offset; 5791 insertionOffset += offset; 5792 } 5793 } 5794 5795 if (!inserted) 5796 splices.push(splice); 5797 } 5798 5799 function createInitialSplices(array, changeRecords) { 5800 var splices = []; 5801 5802 for (var i = 0; i < changeRecords.length; i++) { 5803 var record = changeRecords[i]; 5804 switch(record.type) { 5805 case 'splice': 5806 mergeSplice(splices, record.index, record.removed.slice(), record.addedCount); 5807 break; 5808 case 'add': 5809 case 'update': 5810 case 'delete': 5811 if (!isIndex(record.name)) 5812 continue; 5813 var index = toNumber(record.name); 5814 if (index < 0) 5815 continue; 5816 mergeSplice(splices, index, [record.oldValue], 1); 5817 break; 5818 default: 5819 console.error('Unexpected record type: ' + JSON.stringify(record)); 5820 break; 5821 } 5822 } 5823 5824 return splices; 5825 } 5826 5827 function projectArraySplices(array, changeRecords) { 5828 var splices = []; 5829 5830 createInitialSplices(array, changeRecords).forEach(function(splice) { 5831 if (splice.addedCount == 1 && splice.removed.length == 1) { 5832 if (splice.removed[0] !== array[splice.index]) 5833 splices.push(splice); 5834 5835 return 5836 }; 5837 5838 splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount, 5839 splice.removed, 0, splice.removed.length)); 5840 }); 5841 5842 return splices; 5843 } 5844 5845 // Export the observe-js object for **Node.js**, with backwards-compatibility 5846 // for the old `require()` API. Also ensure `exports` is not a DOM Element. 5847 // If we're in the browser, export as a global object. 5848 5849 var expose = global; 5850 5851 if (typeof exports !== 'undefined' && !exports.nodeType) { 5852 if (typeof module !== 'undefined' && module.exports) { 5853 exports = module.exports; 5854 } 5855 expose = exports; 5856 } 5857 5858 expose.Observer = Observer; 5859 expose.Observer.runEOM_ = runEOM; 5860 expose.Observer.observerSentinel_ = observerSentinel; // for testing. 5861 expose.Observer.hasObjectObserve = hasObserve; 5862 expose.ArrayObserver = ArrayObserver; 5863 expose.ArrayObserver.calculateSplices = function(current, previous) { 5864 return arraySplice.calculateSplices(current, previous); 5865 }; 5866 5867 expose.ArraySplice = ArraySplice; 5868 expose.ObjectObserver = ObjectObserver; 5869 expose.PathObserver = PathObserver; 5870 expose.CompoundObserver = CompoundObserver; 5871 expose.Path = Path; 5872 expose.ObserverTransform = ObserverTransform; 5873 5874 })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window); 5875 5876 // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 5877 // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5878 // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 5879 // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 5880 // Code distributed by Google as part of the polymer project is also 5881 // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 5882 5883 (function(global) { 5884 'use strict'; 5885 5886 var filter = Array.prototype.filter.call.bind(Array.prototype.filter); 5887 5888 function getTreeScope(node) { 5889 while (node.parentNode) { 5890 node = node.parentNode; 5891 } 5892 5893 return typeof node.getElementById === 'function' ? node : null; 5894 } 5895 5896 Node.prototype.bind = function(name, observable) { 5897 console.error('Unhandled binding to Node: ', this, name, observable); 5898 }; 5899 5900 Node.prototype.bindFinished = function() {}; 5901 5902 function updateBindings(node, name, binding) { 5903 var bindings = node.bindings_; 5904 if (!bindings) 5905 bindings = node.bindings_ = {}; 5906 5907 if (bindings[name]) 5908 binding[name].close(); 5909 5910 return bindings[name] = binding; 5911 } 5912 5913 function returnBinding(node, name, binding) { 5914 return binding; 5915 } 5916 5917 function sanitizeValue(value) { 5918 return value == null ? '' : value; 5919 } 5920 5921 function updateText(node, value) { 5922 node.data = sanitizeValue(value); 5923 } 5924 5925 function textBinding(node) { 5926 return function(value) { 5927 return updateText(node, value); 5928 }; 5929 } 5930 5931 var maybeUpdateBindings = returnBinding; 5932 5933 Object.defineProperty(Platform, 'enableBindingsReflection', { 5934 get: function() { 5935 return maybeUpdateBindings === updateBindings; 5936 }, 5937 set: function(enable) { 5938 maybeUpdateBindings = enable ? updateBindings : returnBinding; 5939 return enable; 5940 }, 5941 configurable: true 5942 }); 5943 5944 Text.prototype.bind = function(name, value, oneTime) { 5945 if (name !== 'textContent') 5946 return Node.prototype.bind.call(this, name, value, oneTime); 5947 5948 if (oneTime) 5949 return updateText(this, value); 5950 5951 var observable = value; 5952 updateText(this, observable.open(textBinding(this))); 5953 return maybeUpdateBindings(this, name, observable); 5954 } 5955 5956 function updateAttribute(el, name, conditional, value) { 5957 if (conditional) { 5958 if (value) 5959 el.setAttribute(name, ''); 5960 else 5961 el.removeAttribute(name); 5962 return; 5963 } 5964 5965 el.setAttribute(name, sanitizeValue(value)); 5966 } 5967 5968 function attributeBinding(el, name, conditional) { 5969 return function(value) { 5970 updateAttribute(el, name, conditional, value); 5971 }; 5972 } 5973 5974 Element.prototype.bind = function(name, value, oneTime) { 5975 var conditional = name[name.length - 1] == '?'; 5976 if (conditional) { 5977 this.removeAttribute(name); 5978 name = name.slice(0, -1); 5979 } 5980 5981 if (oneTime) 5982 return updateAttribute(this, name, conditional, value); 5983 5984 5985 var observable = value; 5986 updateAttribute(this, name, conditional, 5987 observable.open(attributeBinding(this, name, conditional))); 5988 5989 return maybeUpdateBindings(this, name, observable); 5990 }; 5991 5992 var checkboxEventType; 5993 (function() { 5994 // Attempt to feature-detect which event (change or click) is fired first 5995 // for checkboxes. 5996 var div = document.createElement('div'); 5997 var checkbox = div.appendChild(document.createElement('input')); 5998 checkbox.setAttribute('type', 'checkbox'); 5999 var first; 6000 var count = 0; 6001 checkbox.addEventListener('click', function(e) { 6002 count++; 6003 first = first || 'click'; 6004 }); 6005 checkbox.addEventListener('change', function() { 6006 count++; 6007 first = first || 'change'; 6008 }); 6009 6010 var event = document.createEvent('MouseEvent'); 6011 event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, 6012 false, false, false, 0, null); 6013 checkbox.dispatchEvent(event); 6014 // WebKit/Blink don't fire the change event if the element is outside the 6015 // document, so assume 'change' for that case. 6016 checkboxEventType = count == 1 ? 'change' : first; 6017 })(); 6018 6019 function getEventForInputType(element) { 6020 switch (element.type) { 6021 case 'checkbox': 6022 return checkboxEventType; 6023 case 'radio': 6024 case 'select-multiple': 6025 case 'select-one': 6026 return 'change'; 6027 case 'range': 6028 if (/Trident|MSIE/.test(navigator.userAgent)) 6029 return 'change'; 6030 default: 6031 return 'input'; 6032 } 6033 } 6034 6035 function updateInput(input, property, value, santizeFn) { 6036 input[property] = (santizeFn || sanitizeValue)(value); 6037 } 6038 6039 function inputBinding(input, property, santizeFn) { 6040 return function(value) { 6041 return updateInput(input, property, value, santizeFn); 6042 } 6043 } 6044 6045 function noop() {} 6046 6047 function bindInputEvent(input, property, observable, postEventFn) { 6048 var eventType = getEventForInputType(input); 6049 6050 function eventHandler() { 6051 var isNum = property == 'value' && input.type == 'number'; 6052 observable.setValue(isNum ? input.valueAsNumber : input[property]); 6053 observable.discardChanges(); 6054 (postEventFn || noop)(input); 6055 Platform.performMicrotaskCheckpoint(); 6056 } 6057 input.addEventListener(eventType, eventHandler); 6058 6059 return { 6060 close: function() { 6061 input.removeEventListener(eventType, eventHandler); 6062 observable.close(); 6063 }, 6064 6065 observable_: observable 6066 } 6067 } 6068 6069 function booleanSanitize(value) { 6070 return Boolean(value); 6071 } 6072 6073 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. 6074 // Returns an array containing all radio buttons other than |element| that 6075 // have the same |name|, either in the form that |element| belongs to or, 6076 // if no form, in the document tree to which |element| belongs. 6077 // 6078 // This implementation is based upon the HTML spec definition of a 6079 // "radio button group": 6080 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group 6081 // 6082 function getAssociatedRadioButtons(element) { 6083 if (element.form) { 6084 return filter(element.form.elements, function(el) { 6085 return el != element && 6086 el.tagName == 'INPUT' && 6087 el.type == 'radio' && 6088 el.name == element.name; 6089 }); 6090 } else { 6091 var treeScope = getTreeScope(element); 6092 if (!treeScope) 6093 return []; 6094 var radios = treeScope.querySelectorAll( 6095 'input[type="radio"][name="' + element.name + '"]'); 6096 return filter(radios, function(el) { 6097 return el != element && !el.form; 6098 }); 6099 } 6100 } 6101 6102 function checkedPostEvent(input) { 6103 // Only the radio button that is getting checked gets an event. We 6104 // therefore find all the associated radio buttons and update their 6105 // check binding manually. 6106 if (input.tagName === 'INPUT' && 6107 input.type === 'radio') { 6108 getAssociatedRadioButtons(input).forEach(function(radio) { 6109 var checkedBinding = radio.bindings_.checked; 6110 if (checkedBinding) { 6111 // Set the value directly to avoid an infinite call stack. 6112 checkedBinding.observable_.setValue(false); 6113 } 6114 }); 6115 } 6116 } 6117 6118 HTMLInputElement.prototype.bind = function(name, value, oneTime) { 6119 if (name !== 'value' && name !== 'checked') 6120 return HTMLElement.prototype.bind.call(this, name, value, oneTime); 6121 6122 this.removeAttribute(name); 6123 var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; 6124 var postEventFn = name == 'checked' ? checkedPostEvent : noop; 6125 6126 if (oneTime) 6127 return updateInput(this, name, value, sanitizeFn); 6128 6129 6130 var observable = value; 6131 var binding = bindInputEvent(this, name, observable, postEventFn); 6132 updateInput(this, name, 6133 observable.open(inputBinding(this, name, sanitizeFn)), 6134 sanitizeFn); 6135 6136 // Checkboxes may need to update bindings of other checkboxes. 6137 return updateBindings(this, name, binding); 6138 } 6139 6140 HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) { 6141 if (name !== 'value') 6142 return HTMLElement.prototype.bind.call(this, name, value, oneTime); 6143 6144 this.removeAttribute('value'); 6145 6146 if (oneTime) 6147 return updateInput(this, 'value', value); 6148 6149 var observable = value; 6150 var binding = bindInputEvent(this, 'value', observable); 6151 updateInput(this, 'value', 6152 observable.open(inputBinding(this, 'value', sanitizeValue))); 6153 return maybeUpdateBindings(this, name, binding); 6154 } 6155 6156 function updateOption(option, value) { 6157 var parentNode = option.parentNode;; 6158 var select; 6159 var selectBinding; 6160 var oldValue; 6161 if (parentNode instanceof HTMLSelectElement && 6162 parentNode.bindings_ && 6163 parentNode.bindings_.value) { 6164 select = parentNode; 6165 selectBinding = select.bindings_.value; 6166 oldValue = select.value; 6167 } 6168 6169 option.value = sanitizeValue(value); 6170 6171 if (select && select.value != oldValue) { 6172 selectBinding.observable_.setValue(select.value); 6173 selectBinding.observable_.discardChanges(); 6174 Platform.performMicrotaskCheckpoint(); 6175 } 6176 } 6177 6178 function optionBinding(option) { 6179 return function(value) { 6180 updateOption(option, value); 6181 } 6182 } 6183 6184 HTMLOptionElement.prototype.bind = function(name, value, oneTime) { 6185 if (name !== 'value') 6186 return HTMLElement.prototype.bind.call(this, name, value, oneTime); 6187 6188 this.removeAttribute('value'); 6189 6190 if (oneTime) 6191 return updateOption(this, value); 6192 6193 var observable = value; 6194 var binding = bindInputEvent(this, 'value', observable); 6195 updateOption(this, observable.open(optionBinding(this))); 6196 return maybeUpdateBindings(this, name, binding); 6197 } 6198 6199 HTMLSelectElement.prototype.bind = function(name, value, oneTime) { 6200 if (name === 'selectedindex') 6201 name = 'selectedIndex'; 6202 6203 if (name !== 'selectedIndex' && name !== 'value') 6204 return HTMLElement.prototype.bind.call(this, name, value, oneTime); 6205 6206 this.removeAttribute(name); 6207 6208 if (oneTime) 6209 return updateInput(this, name, value); 6210 6211 var observable = value; 6212 var binding = bindInputEvent(this, name, observable); 6213 updateInput(this, name, 6214 observable.open(inputBinding(this, name))); 6215 6216 // Option update events may need to access select bindings. 6217 return updateBindings(this, name, binding); 6218 } 6219 })(this); 6220 6221 // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 6222 // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 6223 // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6224 // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 6225 // Code distributed by Google as part of the polymer project is also 6226 // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 6227 6228 (function(global) { 6229 'use strict'; 6230 6231 function assert(v) { 6232 if (!v) 6233 throw new Error('Assertion failed'); 6234 } 6235 6236 var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); 6237 6238 function getFragmentRoot(node) { 6239 var p; 6240 while (p = node.parentNode) { 6241 node = p; 6242 } 6243 6244 return node; 6245 } 6246 6247 function searchRefId(node, id) { 6248 if (!id) 6249 return; 6250 6251 var ref; 6252 var selector = '#' + id; 6253 while (!ref) { 6254 node = getFragmentRoot(node); 6255 6256 if (node.protoContent_) 6257 ref = node.protoContent_.querySelector(selector); 6258 else if (node.getElementById) 6259 ref = node.getElementById(id); 6260 6261 if (ref || !node.templateCreator_) 6262 break 6263 6264 node = node.templateCreator_; 6265 } 6266 6267 return ref; 6268 } 6269 6270 function getInstanceRoot(node) { 6271 while (node.parentNode) { 6272 node = node.parentNode; 6273 } 6274 return node.templateCreator_ ? node : null; 6275 } 6276 6277 var Map; 6278 if (global.Map && typeof global.Map.prototype.forEach === 'function') { 6279 Map = global.Map; 6280 } else { 6281 Map = function() { 6282 this.keys = []; 6283 this.values = []; 6284 }; 6285 6286 Map.prototype = { 6287 set: function(key, value) { 6288 var index = this.keys.indexOf(key); 6289 if (index < 0) { 6290 this.keys.push(key); 6291 this.values.push(value); 6292 } else { 6293 this.values[index] = value; 6294 } 6295 }, 6296 6297 get: function(key) { 6298 var index = this.keys.indexOf(key); 6299 if (index < 0) 6300 return; 6301 6302 return this.values[index]; 6303 }, 6304 6305 delete: function(key, value) { 6306 var index = this.keys.indexOf(key); 6307 if (index < 0) 6308 return false; 6309 6310 this.keys.splice(index, 1); 6311 this.values.splice(index, 1); 6312 return true; 6313 }, 6314 6315 forEach: function(f, opt_this) { 6316 for (var i = 0; i < this.keys.length; i++) 6317 f.call(opt_this || this, this.values[i], this.keys[i], this); 6318 } 6319 }; 6320 } 6321 6322 // JScript does not have __proto__. We wrap all object literals with 6323 // createObject which uses Object.create, Object.defineProperty and 6324 // Object.getOwnPropertyDescriptor to create a new object that does the exact 6325 // same thing. The main downside to this solution is that we have to extract 6326 // all those property descriptors for IE. 6327 var createObject = ('__proto__' in {}) ? 6328 function(obj) { return obj; } : 6329 function(obj) { 6330 var proto = obj.__proto__; 6331 if (!proto) 6332 return obj; 6333 var newObject = Object.create(proto); 6334 Object.getOwnPropertyNames(obj).forEach(function(name) { 6335 Object.defineProperty(newObject, name, 6336 Object.getOwnPropertyDescriptor(obj, name)); 6337 }); 6338 return newObject; 6339 }; 6340 6341 // IE does not support have Document.prototype.contains. 6342 if (typeof document.contains != 'function') { 6343 Document.prototype.contains = function(node) { 6344 if (node === this || node.parentNode === this) 6345 return true; 6346 return this.documentElement.contains(node); 6347 } 6348 } 6349 6350 var BIND = 'bind'; 6351 var REPEAT = 'repeat'; 6352 var IF = 'if'; 6353 6354 var templateAttributeDirectives = { 6355 'template': true, 6356 'repeat': true, 6357 'bind': true, 6358 'ref': true, 6359 'if': true 6360 }; 6361 6362 var semanticTemplateElements = { 6363 'THEAD': true, 6364 'TBODY': true, 6365 'TFOOT': true, 6366 'TH': true, 6367 'TR': true, 6368 'TD': true, 6369 'COLGROUP': true, 6370 'COL': true, 6371 'CAPTION': true, 6372 'OPTION': true, 6373 'OPTGROUP': true 6374 }; 6375 6376 var hasTemplateElement = typeof HTMLTemplateElement !== 'undefined'; 6377 if (hasTemplateElement) { 6378 // TODO(rafaelw): Remove when fix for 6379 // https://codereview.chromium.org/164803002/ 6380 // makes it to Chrome release. 6381 (function() { 6382 var t = document.createElement('template'); 6383 var d = t.content.ownerDocument; 6384 var html = d.appendChild(d.createElement('html')); 6385 var head = html.appendChild(d.createElement('head')); 6386 var base = d.createElement('base'); 6387 base.href = document.baseURI; 6388 head.appendChild(base); 6389 })(); 6390 } 6391 6392 var allTemplatesSelectors = 'template, ' + 6393 Object.keys(semanticTemplateElements).map(function(tagName) { 6394 return tagName.toLowerCase() + '[template]'; 6395 }).join(', '); 6396 6397 function isSVGTemplate(el) { 6398 return el.tagName == 'template' && 6399 el.namespaceURI == 'http://www.w3.org/2000/svg'; 6400 } 6401 6402 function isHTMLTemplate(el) { 6403 return el.tagName == 'TEMPLATE' && 6404 el.namespaceURI == 'http://www.w3.org/1999/xhtml'; 6405 } 6406 6407 function isAttributeTemplate(el) { 6408 return Boolean(semanticTemplateElements[el.tagName] && 6409 el.hasAttribute('template')); 6410 } 6411 6412 function isTemplate(el) { 6413 if (el.isTemplate_ === undefined) 6414 el.isTemplate_ = el.tagName == 'TEMPLATE' || isAttributeTemplate(el); 6415 6416 return el.isTemplate_; 6417 } 6418 6419 // FIXME: Observe templates being added/removed from documents 6420 // FIXME: Expose imperative API to decorate and observe templates in 6421 // "disconnected tress" (e.g. ShadowRoot) 6422 document.addEventListener('DOMContentLoaded', function(e) { 6423 bootstrapTemplatesRecursivelyFrom(document); 6424 // FIXME: Is this needed? Seems like it shouldn't be. 6425 Platform.performMicrotaskCheckpoint(); 6426 }, false); 6427 6428 function forAllTemplatesFrom(node, fn) { 6429 var subTemplates = node.querySelectorAll(allTemplatesSelectors); 6430 6431 if (isTemplate(node)) 6432 fn(node) 6433 forEach(subTemplates, fn); 6434 } 6435 6436 function bootstrapTemplatesRecursivelyFrom(node) { 6437 function bootstrap(template) { 6438 if (!HTMLTemplateElement.decorate(template)) 6439 bootstrapTemplatesRecursivelyFrom(template.content); 6440 } 6441 6442 forAllTemplatesFrom(node, bootstrap); 6443 } 6444 6445 if (!hasTemplateElement) { 6446 /** 6447 * This represents a <template> element. 6448 * @constructor 6449 * @extends {HTMLElement} 6450 */ 6451 global.HTMLTemplateElement = function() { 6452 throw TypeError('Illegal constructor'); 6453 }; 6454 } 6455 6456 var hasProto = '__proto__' in {}; 6457 6458 function mixin(to, from) { 6459 Object.getOwnPropertyNames(from).forEach(function(name) { 6460 Object.defineProperty(to, name, 6461 Object.getOwnPropertyDescriptor(from, name)); 6462 }); 6463 } 6464 6465 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner 6466 function getOrCreateTemplateContentsOwner(template) { 6467 var doc = template.ownerDocument 6468 if (!doc.defaultView) 6469 return doc; 6470 var d = doc.templateContentsOwner_; 6471 if (!d) { 6472 // TODO(arv): This should either be a Document or HTMLDocument depending 6473 // on doc. 6474 d = doc.implementation.createHTMLDocument(''); 6475 while (d.lastChild) { 6476 d.removeChild(d.lastChild); 6477 } 6478 doc.templateContentsOwner_ = d; 6479 } 6480 return d; 6481 } 6482 6483 function getTemplateStagingDocument(template) { 6484 if (!template.stagingDocument_) { 6485 var owner = template.ownerDocument; 6486 if (!owner.stagingDocument_) { 6487 owner.stagingDocument_ = owner.implementation.createHTMLDocument(''); 6488 owner.stagingDocument_.isStagingDocument = true; 6489 // TODO(rafaelw): Remove when fix for 6490 // https://codereview.chromium.org/164803002/ 6491 // makes it to Chrome release. 6492 var base = owner.stagingDocument_.createElement('base'); 6493 base.href = document.baseURI; 6494 owner.stagingDocument_.head.appendChild(base); 6495 6496 owner.stagingDocument_.stagingDocument_ = owner.stagingDocument_; 6497 } 6498 6499 template.stagingDocument_ = owner.stagingDocument_; 6500 } 6501 6502 return template.stagingDocument_; 6503 } 6504 6505 // For non-template browsers, the parser will disallow <template> in certain 6506 // locations, so we allow "attribute templates" which combine the template 6507 // element with the top-level container node of the content, e.g. 6508 // 6509 // <tr template repeat="{{ foo }}"" class="bar"><td>Bar</td></tr> 6510 // 6511 // becomes 6512 // 6513 // <template repeat="{{ foo }}"> 6514 // + #document-fragment 6515 // + <tr class="bar"> 6516 // + <td>Bar</td> 6517 // 6518 function extractTemplateFromAttributeTemplate(el) { 6519 var template = el.ownerDocument.createElement('template'); 6520 el.parentNode.insertBefore(template, el); 6521 6522 var attribs = el.attributes; 6523 var count = attribs.length; 6524 while (count-- > 0) { 6525 var attrib = attribs[count]; 6526 if (templateAttributeDirectives[attrib.name]) { 6527 if (attrib.name !== 'template') 6528 template.setAttribute(attrib.name, attrib.value); 6529 el.removeAttribute(attrib.name); 6530 } 6531 } 6532 6533 return template; 6534 } 6535 6536 function extractTemplateFromSVGTemplate(el) { 6537 var template = el.ownerDocument.createElement('template'); 6538 el.parentNode.insertBefore(template, el); 6539 6540 var attribs = el.attributes; 6541 var count = attribs.length; 6542 while (count-- > 0) { 6543 var attrib = attribs[count]; 6544 template.setAttribute(attrib.name, attrib.value); 6545 el.removeAttribute(attrib.name); 6546 } 6547 6548 el.parentNode.removeChild(el); 6549 return template; 6550 } 6551 6552 function liftNonNativeTemplateChildrenIntoContent(template, el, useRoot) { 6553 var content = template.content; 6554 if (useRoot) { 6555 content.appendChild(el); 6556 return; 6557 } 6558 6559 var child; 6560 while (child = el.firstChild) { 6561 content.appendChild(child); 6562 } 6563 } 6564 6565 var templateObserver; 6566 if (typeof MutationObserver == 'function') { 6567 templateObserver = new MutationObserver(function(records) { 6568 for (var i = 0; i < records.length; i++) { 6569 records[i].target.refChanged_(); 6570 } 6571 }); 6572 } 6573 6574 /** 6575 * Ensures proper API and content model for template elements. 6576 * @param {HTMLTemplateElement} opt_instanceRef The template element which 6577 * |el| template element will return as the value of its ref(), and whose 6578 * content will be used as source when createInstance() is invoked. 6579 */ 6580 HTMLTemplateElement.decorate = function(el, opt_instanceRef) { 6581 if (el.templateIsDecorated_) 6582 return false; 6583 6584 var templateElement = el; 6585 templateElement.templateIsDecorated_ = true; 6586 6587 var isNativeHTMLTemplate = isHTMLTemplate(templateElement) && 6588 hasTemplateElement; 6589 var bootstrapContents = isNativeHTMLTemplate; 6590 var liftContents = !isNativeHTMLTemplate; 6591 var liftRoot = false; 6592 6593 if (!isNativeHTMLTemplate) { 6594 if (isAttributeTemplate(templateElement)) { 6595 assert(!opt_instanceRef); 6596 templateElement = extractTemplateFromAttributeTemplate(el); 6597 templateElement.templateIsDecorated_ = true; 6598 isNativeHTMLTemplate = hasTemplateElement; 6599 liftRoot = true; 6600 } else if (isSVGTemplate(templateElement)) { 6601 templateElement = extractTemplateFromSVGTemplate(el); 6602 templateElement.templateIsDecorated_ = true; 6603 isNativeHTMLTemplate = hasTemplateElement; 6604 } 6605 } 6606 6607 if (!isNativeHTMLTemplate) { 6608 fixTemplateElementPrototype(templateElement); 6609 var doc = getOrCreateTemplateContentsOwner(templateElement); 6610 templateElement.content_ = doc.createDocumentFragment(); 6611 } 6612 6613 if (opt_instanceRef) { 6614 // template is contained within an instance, its direct content must be 6615 // empty 6616 templateElement.instanceRef_ = opt_instanceRef; 6617 } else if (liftContents) { 6618 liftNonNativeTemplateChildrenIntoContent(templateElement, 6619 el, 6620 liftRoot); 6621 } else if (bootstrapContents) { 6622 bootstrapTemplatesRecursivelyFrom(templateElement.content); 6623 } 6624 6625 return true; 6626 }; 6627 6628 // TODO(rafaelw): This used to decorate recursively all templates from a given 6629 // node. This happens by default on 'DOMContentLoaded', but may be needed 6630 // in subtrees not descendent from document (e.g. ShadowRoot). 6631 // Review whether this is the right public API. 6632 HTMLTemplateElement.bootstrap = bootstrapTemplatesRecursivelyFrom; 6633 6634 var htmlElement = global.HTMLUnknownElement || HTMLElement; 6635 6636 var contentDescriptor = { 6637 get: function() { 6638 return this.content_; 6639 }, 6640 enumerable: true, 6641 configurable: true 6642 }; 6643 6644 if (!hasTemplateElement) { 6645 // Gecko is more picky with the prototype than WebKit. Make sure to use the 6646 // same prototype as created in the constructor. 6647 HTMLTemplateElement.prototype = Object.create(htmlElement.prototype); 6648 6649 Object.defineProperty(HTMLTemplateElement.prototype, 'content', 6650 contentDescriptor); 6651 } 6652 6653 function fixTemplateElementPrototype(el) { 6654 if (hasProto) 6655 el.__proto__ = HTMLTemplateElement.prototype; 6656 else 6657 mixin(el, HTMLTemplateElement.prototype); 6658 } 6659 6660 function ensureSetModelScheduled(template) { 6661 if (!template.setModelFn_) { 6662 template.setModelFn_ = function() { 6663 template.setModelFnScheduled_ = false; 6664 var map = getBindings(template, 6665 template.delegate_ && template.delegate_.prepareBinding); 6666 processBindings(template, map, template.model_); 6667 }; 6668 } 6669 6670 if (!template.setModelFnScheduled_) { 6671 template.setModelFnScheduled_ = true; 6672 Observer.runEOM_(template.setModelFn_); 6673 } 6674 } 6675 6676 mixin(HTMLTemplateElement.prototype, { 6677 bind: function(name, value, oneTime) { 6678 if (name != 'ref') 6679 return Element.prototype.bind.call(this, name, value, oneTime); 6680 6681 var self = this; 6682 var ref = oneTime ? value : value.open(function(ref) { 6683 self.setAttribute('ref', ref); 6684 self.refChanged_(); 6685 }); 6686 6687 this.setAttribute('ref', ref); 6688 this.refChanged_(); 6689 if (oneTime) 6690 return; 6691 6692 if (!this.bindings_) { 6693 this.bindings_ = { ref: value }; 6694 } else { 6695 this.bindings_.ref = value; 6696 } 6697 6698 return value; 6699 }, 6700 6701 processBindingDirectives_: function(directives) { 6702 if (this.iterator_) 6703 this.iterator_.closeDeps(); 6704 6705 if (!directives.if && !directives.bind && !directives.repeat) { 6706 if (this.iterator_) { 6707 this.iterator_.close(); 6708 this.iterator_ = undefined; 6709 } 6710 6711 return; 6712 } 6713 6714 if (!this.iterator_) { 6715 this.iterator_ = new TemplateIterator(this); 6716 } 6717 6718 this.iterator_.updateDependencies(directives, this.model_); 6719 6720 if (templateObserver) { 6721 templateObserver.observe(this, { attributes: true, 6722 attributeFilter: ['ref'] }); 6723 } 6724 6725 return this.iterator_; 6726 }, 6727 6728 createInstance: function(model, bindingDelegate, delegate_) { 6729 if (bindingDelegate) 6730 delegate_ = this.newDelegate_(bindingDelegate); 6731 else if (!delegate_) 6732 delegate_ = this.delegate_; 6733 6734 if (!this.refContent_) 6735 this.refContent_ = this.ref_.content; 6736 var content = this.refContent_; 6737 if (content.firstChild === null) 6738 return emptyInstance; 6739 6740 var map = getInstanceBindingMap(content, delegate_); 6741 var stagingDocument = getTemplateStagingDocument(this); 6742 var instance = stagingDocument.createDocumentFragment(); 6743 instance.templateCreator_ = this; 6744 instance.protoContent_ = content; 6745 instance.bindings_ = []; 6746 instance.terminator_ = null; 6747 var instanceRecord = instance.templateInstance_ = { 6748 firstNode: null, 6749 lastNode: null, 6750 model: model 6751 }; 6752 6753 var i = 0; 6754 var collectTerminator = false; 6755 for (var child = content.firstChild; child; child = child.nextSibling) { 6756 // The terminator of the instance is the clone of the last child of the 6757 // content. If the last child is an active template, it may produce 6758 // instances as a result of production, so simply collecting the last 6759 // child of the instance after it has finished producing may be wrong. 6760 if (child.nextSibling === null) 6761 collectTerminator = true; 6762 6763 var clone = cloneAndBindInstance(child, instance, stagingDocument, 6764 map.children[i++], 6765 model, 6766 delegate_, 6767 instance.bindings_); 6768 clone.templateInstance_ = instanceRecord; 6769 if (collectTerminator) 6770 instance.terminator_ = clone; 6771 } 6772 6773 instanceRecord.firstNode = instance.firstChild; 6774 instanceRecord.lastNode = instance.lastChild; 6775 instance.templateCreator_ = undefined; 6776 instance.protoContent_ = undefined; 6777 return instance; 6778 }, 6779 6780 get model() { 6781 return this.model_; 6782 }, 6783 6784 set model(model) { 6785 this.model_ = model; 6786 ensureSetModelScheduled(this); 6787 }, 6788 6789 get bindingDelegate() { 6790 return this.delegate_ && this.delegate_.raw; 6791 }, 6792 6793 refChanged_: function() { 6794 if (!this.iterator_ || this.refContent_ === this.ref_.content) 6795 return; 6796 6797 this.refContent_ = undefined; 6798 this.iterator_.valueChanged(); 6799 this.iterator_.updateIteratedValue(this.iterator_.getUpdatedValue()); 6800 }, 6801 6802 clear: function() { 6803 this.model_ = undefined; 6804 this.delegate_ = undefined; 6805 if (this.bindings_ && this.bindings_.ref) 6806 this.bindings_.ref.close() 6807 this.refContent_ = undefined; 6808 if (!this.iterator_) 6809 return; 6810 this.iterator_.valueChanged(); 6811 this.iterator_.close() 6812 this.iterator_ = undefined; 6813 }, 6814 6815 setDelegate_: function(delegate) { 6816 this.delegate_ = delegate; 6817 this.bindingMap_ = undefined; 6818 if (this.iterator_) { 6819 this.iterator_.instancePositionChangedFn_ = undefined; 6820 this.iterator_.instanceModelFn_ = undefined; 6821 } 6822 }, 6823 6824 newDelegate_: function(bindingDelegate) { 6825 if (!bindingDelegate) 6826 return; 6827 6828 function delegateFn(name) { 6829 var fn = bindingDelegate && bindingDelegate[name]; 6830 if (typeof fn != 'function') 6831 return; 6832 6833 return function() { 6834 return fn.apply(bindingDelegate, arguments); 6835 }; 6836 } 6837 6838 return { 6839 bindingMaps: {}, 6840 raw: bindingDelegate, 6841 prepareBinding: delegateFn('prepareBinding'), 6842 prepareInstanceModel: delegateFn('prepareInstanceModel'), 6843 prepareInstancePositionChanged: 6844 delegateFn('prepareInstancePositionChanged') 6845 }; 6846 }, 6847 6848 set bindingDelegate(bindingDelegate) { 6849 if (this.delegate_) { 6850 throw Error('Template must be cleared before a new bindingDelegate ' + 6851 'can be assigned'); 6852 } 6853 6854 this.setDelegate_(this.newDelegate_(bindingDelegate)); 6855 }, 6856 6857 get ref_() { 6858 var ref = searchRefId(this, this.getAttribute('ref')); 6859 if (!ref) 6860 ref = this.instanceRef_; 6861 6862 if (!ref) 6863 return this; 6864 6865 var nextRef = ref.ref_; 6866 return nextRef ? nextRef : ref; 6867 } 6868 }); 6869 6870 // Returns 6871 // a) undefined if there are no mustaches. 6872 // b) [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one mustache. 6873 function parseMustaches(s, name, node, prepareBindingFn) { 6874 if (!s || !s.length) 6875 return; 6876 6877 var tokens; 6878 var length = s.length; 6879 var startIndex = 0, lastIndex = 0, endIndex = 0; 6880 var onlyOneTime = true; 6881 while (lastIndex < length) { 6882 var startIndex = s.indexOf('{{', lastIndex); 6883 var oneTimeStart = s.indexOf('[[', lastIndex); 6884 var oneTime = false; 6885 var terminator = '}}'; 6886 6887 if (oneTimeStart >= 0 && 6888 (startIndex < 0 || oneTimeStart < startIndex)) { 6889 startIndex = oneTimeStart; 6890 oneTime = true; 6891 terminator = ']]'; 6892 } 6893 6894 endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2); 6895 6896 if (endIndex < 0) { 6897 if (!tokens) 6898 return; 6899 6900 tokens.push(s.slice(lastIndex)); // TEXT 6901 break; 6902 } 6903 6904 tokens = tokens || []; 6905 tokens.push(s.slice(lastIndex, startIndex)); // TEXT 6906 var pathString = s.slice(startIndex + 2, endIndex).trim(); 6907 tokens.push(oneTime); // ONE_TIME? 6908 onlyOneTime = onlyOneTime && oneTime; 6909 var delegateFn = prepareBindingFn && 6910 prepareBindingFn(pathString, name, node); 6911 // Don't try to parse the expression if there's a prepareBinding function 6912 if (delegateFn == null) { 6913 tokens.push(Path.get(pathString)); // PATH 6914 } else { 6915 tokens.push(null); 6916 } 6917 tokens.push(delegateFn); // DELEGATE_FN 6918 lastIndex = endIndex + 2; 6919 } 6920 6921 if (lastIndex === length) 6922 tokens.push(''); // TEXT 6923 6924 tokens.hasOnePath = tokens.length === 5; 6925 tokens.isSimplePath = tokens.hasOnePath && 6926 tokens[0] == '' && 6927 tokens[4] == ''; 6928 tokens.onlyOneTime = onlyOneTime; 6929 6930 tokens.combinator = function(values) { 6931 var newValue = tokens[0]; 6932 6933 for (var i = 1; i < tokens.length; i += 4) { 6934 var value = tokens.hasOnePath ? values : values[(i - 1) / 4]; 6935 if (value !== undefined) 6936 newValue += value; 6937 newValue += tokens[i + 3]; 6938 } 6939 6940 return newValue; 6941 } 6942 6943 return tokens; 6944 }; 6945 6946 function processOneTimeBinding(name, tokens, node, model) { 6947 if (tokens.hasOnePath) { 6948 var delegateFn = tokens[3]; 6949 var value = delegateFn ? delegateFn(model, node, true) : 6950 tokens[2].getValueFrom(model); 6951 return tokens.isSimplePath ? value : tokens.combinator(value); 6952 } 6953 6954 var values = []; 6955 for (var i = 1; i < tokens.length; i += 4) { 6956 var delegateFn = tokens[i + 2]; 6957 values[(i - 1) / 4] = delegateFn ? delegateFn(model, node) : 6958 tokens[i + 1].getValueFrom(model); 6959 } 6960 6961 return tokens.combinator(values); 6962 } 6963 6964 function processSinglePathBinding(name, tokens, node, model) { 6965 var delegateFn = tokens[3]; 6966 var observer = delegateFn ? delegateFn(model, node, false) : 6967 new PathObserver(model, tokens[2]); 6968 6969 return tokens.isSimplePath ? observer : 6970 new ObserverTransform(observer, tokens.combinator); 6971 } 6972 6973 function processBinding(name, tokens, node, model) { 6974 if (tokens.onlyOneTime) 6975 return processOneTimeBinding(name, tokens, node, model); 6976 6977 if (tokens.hasOnePath) 6978 return processSinglePathBinding(name, tokens, node, model); 6979 6980 var observer = new CompoundObserver(); 6981 6982 for (var i = 1; i < tokens.length; i += 4) { 6983 var oneTime = tokens[i]; 6984 var delegateFn = tokens[i + 2]; 6985 6986 if (delegateFn) { 6987 var value = delegateFn(model, node, oneTime); 6988 if (oneTime) 6989 observer.addPath(value) 6990 else 6991 observer.addObserver(value); 6992 continue; 6993 } 6994 6995 var path = tokens[i + 1]; 6996 if (oneTime) 6997 observer.addPath(path.getValueFrom(model)) 6998 else 6999 observer.addPath(model, path); 7000 } 7001 7002 return new ObserverTransform(observer, tokens.combinator); 7003 } 7004 7005 function processBindings(node, bindings, model, instanceBindings) { 7006 for (var i = 0; i < bindings.length; i += 2) { 7007 var name = bindings[i] 7008 var tokens = bindings[i + 1]; 7009 var value = processBinding(name, tokens, node, model); 7010 var binding = node.bind(name, value, tokens.onlyOneTime); 7011 if (binding && instanceBindings) 7012 instanceBindings.push(binding); 7013 } 7014 7015 node.bindFinished(); 7016 if (!bindings.isTemplate) 7017 return; 7018 7019 node.model_ = model; 7020 var iter = node.processBindingDirectives_(bindings); 7021 if (instanceBindings && iter) 7022 instanceBindings.push(iter); 7023 } 7024 7025 function parseWithDefault(el, name, prepareBindingFn) { 7026 var v = el.getAttribute(name); 7027 return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn); 7028 } 7029 7030 function parseAttributeBindings(element, prepareBindingFn) { 7031 assert(element); 7032 7033 var bindings = []; 7034 var ifFound = false; 7035 var bindFound = false; 7036 7037 for (var i = 0; i < element.attributes.length; i++) { 7038 var attr = element.attributes[i]; 7039 var name = attr.name; 7040 var value = attr.value; 7041 7042 // Allow bindings expressed in attributes to be prefixed with underbars. 7043 // We do this to allow correct semantics for browsers that don't implement 7044 // <template> where certain attributes might trigger side-effects -- and 7045 // for IE which sanitizes certain attributes, disallowing mustache 7046 // replacements in their text. 7047 while (name[0] === '_') { 7048 name = name.substring(1); 7049 } 7050 7051 if (isTemplate(element) && 7052 (name === IF || name === BIND || name === REPEAT)) { 7053 continue; 7054 } 7055 7056 var tokens = parseMustaches(value, name, element, 7057 prepareBindingFn); 7058 if (!tokens) 7059 continue; 7060 7061 bindings.push(name, tokens); 7062 } 7063 7064 if (isTemplate(element)) { 7065 bindings.isTemplate = true; 7066 bindings.if = parseWithDefault(element, IF, prepareBindingFn); 7067 bindings.bind = parseWithDefault(element, BIND, prepareBindingFn); 7068 bindings.repeat = parseWithDefault(element, REPEAT, prepareBindingFn); 7069 7070 if (bindings.if && !bindings.bind && !bindings.repeat) 7071 bindings.bind = parseMustaches('{{}}', BIND, element, prepareBindingFn); 7072 } 7073 7074 return bindings; 7075 } 7076 7077 function getBindings(node, prepareBindingFn) { 7078 if (node.nodeType === Node.ELEMENT_NODE) 7079 return parseAttributeBindings(node, prepareBindingFn); 7080 7081 if (node.nodeType === Node.TEXT_NODE) { 7082 var tokens = parseMustaches(node.data, 'textContent', node, 7083 prepareBindingFn); 7084 if (tokens) 7085 return ['textContent', tokens]; 7086 } 7087 7088 return []; 7089 } 7090 7091 function cloneAndBindInstance(node, parent, stagingDocument, bindings, model, 7092 delegate, 7093 instanceBindings, 7094 instanceRecord) { 7095 var clone = parent.appendChild(stagingDocument.importNode(node, false)); 7096 7097 var i = 0; 7098 for (var child = node.firstChild; child; child = child.nextSibling) { 7099 cloneAndBindInstance(child, clone, stagingDocument, 7100 bindings.children[i++], 7101 model, 7102 delegate, 7103 instanceBindings); 7104 } 7105 7106 if (bindings.isTemplate) { 7107 HTMLTemplateElement.decorate(clone, node); 7108 if (delegate) 7109 clone.setDelegate_(delegate); 7110 } 7111 7112 processBindings(clone, bindings, model, instanceBindings); 7113 return clone; 7114 } 7115 7116 function createInstanceBindingMap(node, prepareBindingFn) { 7117 var map = getBindings(node, prepareBindingFn); 7118 map.children = {}; 7119 var index = 0; 7120 for (var child = node.firstChild; child; child = child.nextSibling) { 7121 map.children[index++] = createInstanceBindingMap(child, prepareBindingFn); 7122 } 7123 7124 return map; 7125 } 7126 7127 var contentUidCounter = 1; 7128 7129 // TODO(rafaelw): Setup a MutationObserver on content which clears the id 7130 // so that bindingMaps regenerate when the template.content changes. 7131 function getContentUid(content) { 7132 var id = content.id_; 7133 if (!id) 7134 id = content.id_ = contentUidCounter++; 7135 return id; 7136 } 7137 7138 // Each delegate is associated with a set of bindingMaps, one for each 7139 // content which may be used by a template. The intent is that each binding 7140 // delegate gets the opportunity to prepare the instance (via the prepare* 7141 // delegate calls) once across all uses. 7142 // TODO(rafaelw): Separate out the parse map from the binding map. In the 7143 // current implementation, if two delegates need a binding map for the same 7144 // content, the second will have to reparse. 7145 function getInstanceBindingMap(content, delegate_) { 7146 var contentId = getContentUid(content); 7147 if (delegate_) { 7148 var map = delegate_.bindingMaps[contentId]; 7149 if (!map) { 7150 map = delegate_.bindingMaps[contentId] = 7151 createInstanceBindingMap(content, delegate_.prepareBinding) || []; 7152 } 7153 return map; 7154 } 7155 7156 var map = content.bindingMap_; 7157 if (!map) { 7158 map = content.bindingMap_ = 7159 createInstanceBindingMap(content, undefined) || []; 7160 } 7161 return map; 7162 } 7163 7164 Object.defineProperty(Node.prototype, 'templateInstance', { 7165 get: function() { 7166 var instance = this.templateInstance_; 7167 return instance ? instance : 7168 (this.parentNode ? this.parentNode.templateInstance : undefined); 7169 } 7170 }); 7171 7172 var emptyInstance = document.createDocumentFragment(); 7173 emptyInstance.bindings_ = []; 7174 emptyInstance.terminator_ = null; 7175 7176 function TemplateIterator(templateElement) { 7177 this.closed = false; 7178 this.templateElement_ = templateElement; 7179 this.instances = []; 7180 this.deps = undefined; 7181 this.iteratedValue = []; 7182 this.presentValue = undefined; 7183 this.arrayObserver = undefined; 7184 } 7185 7186 TemplateIterator.prototype = { 7187 closeDeps: function() { 7188 var deps = this.deps; 7189 if (deps) { 7190 if (deps.ifOneTime === false) 7191 deps.ifValue.close(); 7192 if (deps.oneTime === false) 7193 deps.value.close(); 7194 } 7195 }, 7196 7197 updateDependencies: function(directives, model) { 7198 this.closeDeps(); 7199 7200 var deps = this.deps = {}; 7201 var template = this.templateElement_; 7202 7203 var ifValue = true; 7204 if (directives.if) { 7205 deps.hasIf = true; 7206 deps.ifOneTime = directives.if.onlyOneTime; 7207 deps.ifValue = processBinding(IF, directives.if, template, model); 7208 7209 ifValue = deps.ifValue; 7210 7211 // oneTime if & predicate is false. nothing else to do. 7212 if (deps.ifOneTime && !ifValue) { 7213 this.valueChanged(); 7214 return; 7215 } 7216 7217 if (!deps.ifOneTime) 7218 ifValue = ifValue.open(this.updateIfValue, this); 7219 } 7220 7221 if (directives.repeat) { 7222 deps.repeat = true; 7223 deps.oneTime = directives.repeat.onlyOneTime; 7224 deps.value = processBinding(REPEAT, directives.repeat, template, model); 7225 } else { 7226 deps.repeat = false; 7227 deps.oneTime = directives.bind.onlyOneTime; 7228 deps.value = processBinding(BIND, directives.bind, template, model); 7229 } 7230 7231 var value = deps.value; 7232 if (!deps.oneTime) 7233 value = value.open(this.updateIteratedValue, this); 7234 7235 if (!ifValue) { 7236 this.valueChanged(); 7237 return; 7238 } 7239 7240 this.updateValue(value); 7241 }, 7242 7243 /** 7244 * Gets the updated value of the bind/repeat. This can potentially call 7245 * user code (if a bindingDelegate is set up) so we try to avoid it if we 7246 * already have the value in hand (from Observer.open). 7247 */ 7248 getUpdatedValue: function() { 7249 var value = this.deps.value; 7250 if (!this.deps.oneTime) 7251 value = value.discardChanges(); 7252 return value; 7253 }, 7254 7255 updateIfValue: function(ifValue) { 7256 if (!ifValue) { 7257 this.valueChanged(); 7258 return; 7259 } 7260 7261 this.updateValue(this.getUpdatedValue()); 7262 }, 7263 7264 updateIteratedValue: function(value) { 7265 if (this.deps.hasIf) { 7266 var ifValue = this.deps.ifValue; 7267 if (!this.deps.ifOneTime) 7268 ifValue = ifValue.discardChanges(); 7269 if (!ifValue) { 7270 this.valueChanged(); 7271 return; 7272 } 7273 } 7274 7275 this.updateValue(value); 7276 }, 7277 7278 updateValue: function(value) { 7279 if (!this.deps.repeat) 7280 value = [value]; 7281 var observe = this.deps.repeat && 7282 !this.deps.oneTime && 7283 Array.isArray(value); 7284 this.valueChanged(value, observe); 7285 }, 7286 7287 valueChanged: function(value, observeValue) { 7288 if (!Array.isArray(value)) 7289 value = []; 7290 7291 if (value === this.iteratedValue) 7292 return; 7293 7294 this.unobserve(); 7295 this.presentValue = value; 7296 if (observeValue) { 7297 this.arrayObserver = new ArrayObserver(this.presentValue); 7298 this.arrayObserver.open(this.handleSplices, this); 7299 } 7300 7301 this.handleSplices(ArrayObserver.calculateSplices(this.presentValue, 7302 this.iteratedValue)); 7303 }, 7304 7305 getLastInstanceNode: function(index) { 7306 if (index == -1) 7307 return this.templateElement_; 7308 var instance = this.instances[index]; 7309 var terminator = instance.terminator_; 7310 if (!terminator) 7311 return this.getLastInstanceNode(index - 1); 7312 7313 if (terminator.nodeType !== Node.ELEMENT_NODE || 7314 this.templateElement_ === terminator) { 7315 return terminator; 7316 } 7317 7318 var subtemplateIterator = terminator.iterator_; 7319 if (!subtemplateIterator) 7320 return terminator; 7321 7322 return subtemplateIterator.getLastTemplateNode(); 7323 }, 7324 7325 getLastTemplateNode: function() { 7326 return this.getLastInstanceNode(this.instances.length - 1); 7327 }, 7328 7329 insertInstanceAt: function(index, fragment) { 7330 var previousInstanceLast = this.getLastInstanceNode(index - 1); 7331 var parent = this.templateElement_.parentNode; 7332 this.instances.splice(index, 0, fragment); 7333 7334 parent.insertBefore(fragment, previousInstanceLast.nextSibling); 7335 }, 7336 7337 extractInstanceAt: function(index) { 7338 var previousInstanceLast = this.getLastInstanceNode(index - 1); 7339 var lastNode = this.getLastInstanceNode(index); 7340 var parent = this.templateElement_.parentNode; 7341 var instance = this.instances.splice(index, 1)[0]; 7342 7343 while (lastNode !== previousInstanceLast) { 7344 var node = previousInstanceLast.nextSibling; 7345 if (node == lastNode) 7346 lastNode = previousInstanceLast; 7347 7348 instance.appendChild(parent.removeChild(node)); 7349 } 7350 7351 return instance; 7352 }, 7353 7354 getDelegateFn: function(fn) { 7355 fn = fn && fn(this.templateElement_); 7356 return typeof fn === 'function' ? fn : null; 7357 }, 7358 7359 handleSplices: function(splices) { 7360 if (this.closed || !splices.length) 7361 return; 7362 7363 var template = this.templateElement_; 7364 7365 if (!template.parentNode) { 7366 this.close(); 7367 return; 7368 } 7369 7370 ArrayObserver.applySplices(this.iteratedValue, this.presentValue, 7371 splices); 7372 7373 var delegate = template.delegate_; 7374 if (this.instanceModelFn_ === undefined) { 7375 this.instanceModelFn_ = 7376 this.getDelegateFn(delegate && delegate.prepareInstanceModel); 7377 } 7378 7379 if (this.instancePositionChangedFn_ === undefined) { 7380 this.instancePositionChangedFn_ = 7381 this.getDelegateFn(delegate && 7382 delegate.prepareInstancePositionChanged); 7383 } 7384 7385 // Instance Removals 7386 var instanceCache = new Map; 7387 var removeDelta = 0; 7388 for (var i = 0; i < splices.length; i++) { 7389 var splice = splices[i]; 7390 var removed = splice.removed; 7391 for (var j = 0; j < removed.length; j++) { 7392 var model = removed[j]; 7393 var instance = this.extractInstanceAt(splice.index + removeDelta); 7394 if (instance !== emptyInstance) { 7395 instanceCache.set(model, instance); 7396 } 7397 } 7398 7399 removeDelta -= splice.addedCount; 7400 } 7401 7402 // Instance Insertions 7403 for (var i = 0; i < splices.length; i++) { 7404 var splice = splices[i]; 7405 var addIndex = splice.index; 7406 for (; addIndex < splice.index + splice.addedCount; addIndex++) { 7407