├── demo ├── dependencies.css ├── index.html └── dependencies.js ├── .gitignore ├── .gitmodules ├── src ├── bootstrap.js └── custom-elements.js ├── bower.json ├── package.json ├── xtag.json ├── README.md └── Gruntfile.js /demo/dependencies.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CustomElements"] 2 | path = CustomElements 3 | url = https://github.com/Polymer/CustomElements.git 4 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 The Polymer Authors. All rights reserved. 3 | Use of this source code is governed by a BSD-style 4 | license that can be found in the LICENSE file. 5 | */ 6 | 7 | window.CustomElements = window.CustomElements || {flags:{}}; 8 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom-Elements", 3 | "description": "Custom Element polyfill for Web Components.", 4 | "version": "0.2.4", 5 | "author": "Arron Schaar", 6 | "keywords": [ 7 | "polyfill", 8 | "web-components", 9 | "x-tag" 10 | ], 11 | "main": "src/custom-elements.js", 12 | "dependencies": { 13 | "MutationObserver": "~0.2.0", 14 | "classlist": "*" 15 | }, 16 | "ignore": [ 17 | ".gitignore", 18 | ".gitmodules", 19 | "CustomElements/", 20 | "Gruntfile.js", 21 | "demo/", 22 | "package.json", 23 | "xtag.json" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-elements-polyfill", 3 | "description": "Wraps the CustomElement polyfill and makes it Bower consumable.", 4 | "version": "0.2.4", 5 | "author": "Arron Schaar", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "bower": "~0.7.0", 9 | "grunt": "~0.4.0", 10 | "grunt-bumpup": "~0.2.0", 11 | "grunt-contrib-concat": "~0.4.0", 12 | "grunt-contrib-connect": "~0.7.0", 13 | "grunt-contrib-jshint": "~0.10.0", 14 | "grunt-contrib-stylus": "~0.7.0", 15 | "grunt-smush-components": "~0.2.1", 16 | "grunt-tagrelease": "~0.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /xtag.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-elements-polyfill", 3 | "tagName": "custom-elements", 4 | "version": "0.2.4", 5 | "author": "Arron Schaar", 6 | "description": "Wraps the CustomElement polyfill and makes it Bower consumable.", 7 | "demo": "demo/index.html", 8 | "compatibility": { 9 | "firefox": 5, 10 | "chrome": 4, 11 | "ie": 9, 12 | "opera": 12, 13 | "android": 2.1, 14 | "ios": 4 15 | }, 16 | "documentation": { 17 | "description": "Wraps the CustomElement polyfill and makes it Bower consumable.", 18 | "attributes": {}, 19 | "events": {}, 20 | "methods": {}, 21 | "getters": {}, 22 | "setters": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About Custom Elements Polyfill 2 | 3 | This repo is a wrapper for https://github.com/Polymer/CustomElements. 4 | 5 | # Bower 6 | 7 | ``` 8 | bower install CustomElements 9 | ``` 10 | 11 | # Example 12 | 13 | ``` 14 | 15 | // Basic usage 16 | 17 | 18 | 19 | 20 | 21 | 41 | 42 | ``` 43 | 44 | 45 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Elements Polyfill - X-Tag 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | connect: { 6 | demo: { 7 | options:{ 8 | port: 3001, 9 | base: '', 10 | keepalive: true 11 | } 12 | } 13 | }, 14 | jshint:{ 15 | options:{ 16 | jshintrc: true 17 | }, 18 | all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'] 19 | }, 20 | 'smush-components': { 21 | options: { 22 | fileMap: { 23 | js: 'demo/dependencies.js', 24 | css: 'demo/dependencies.css' 25 | } 26 | } 27 | }, 28 | bumpup: ['bower.json', 'package.json', 'xtag.json'], 29 | tagrelease: { 30 | file: 'package.json', 31 | prefix: '', 32 | commit: true 33 | }, 34 | concat:{ 35 | dist: { 36 | src: [ 37 | 'src/bootstrap.js', 38 | 'CustomElements/src/Observer.js', 39 | 'CustomElements/src/CustomElements.js', 40 | 'CustomElements/src/Parser.js', 41 | 'CustomElements/src/boot.js' 42 | ], 43 | dest: 'src/custom-elements.js', 44 | } 45 | } 46 | }); 47 | 48 | grunt.loadNpmTasks('grunt-contrib-concat'); 49 | grunt.loadNpmTasks('grunt-contrib-connect'); 50 | grunt.loadNpmTasks('grunt-contrib-jshint'); 51 | grunt.loadNpmTasks('grunt-smush-components'); 52 | grunt.loadNpmTasks('grunt-tagrelease'); 53 | grunt.loadNpmTasks('grunt-bumpup'); 54 | 55 | grunt.registerTask('build', ['smush-components','concat:dist']); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /demo/dependencies.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Polymer Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style 4 | * license that can be found in the LICENSE file. 5 | */ 6 | 7 | if (typeof WeakMap === 'undefined') { 8 | (function() { 9 | var defineProperty = Object.defineProperty; 10 | var counter = Date.now() % 1e9; 11 | 12 | var WeakMap = function() { 13 | this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); 14 | }; 15 | 16 | WeakMap.prototype = { 17 | set: function(key, value) { 18 | var entry = key[this.name]; 19 | if (entry && entry[0] === key) 20 | entry[1] = value; 21 | else 22 | defineProperty(key, this.name, {value: [key, value], writable: true}); 23 | }, 24 | get: function(key) { 25 | var entry; 26 | return (entry = key[this.name]) && entry[0] === key ? 27 | entry[1] : undefined; 28 | }, 29 | delete: function(key) { 30 | this.set(key, undefined); 31 | } 32 | }; 33 | 34 | window.WeakMap = WeakMap; 35 | })(); 36 | } 37 | 38 | /* 39 | * Copyright 2012 The Polymer Authors. All rights reserved. 40 | * Use of this source code is goverened by a BSD-style 41 | * license that can be found in the LICENSE file. 42 | */ 43 | 44 | (function(global) { 45 | 46 | var registrationsTable = new WeakMap(); 47 | 48 | // We use setImmediate or postMessage for our future callback. 49 | var setImmediate = window.msSetImmediate; 50 | 51 | // Use post message to emulate setImmediate. 52 | if (!setImmediate) { 53 | var setImmediateQueue = []; 54 | var sentinel = String(Math.random()); 55 | window.addEventListener('message', function(e) { 56 | if (e.data === sentinel) { 57 | var queue = setImmediateQueue; 58 | setImmediateQueue = []; 59 | queue.forEach(function(func) { 60 | func(); 61 | }); 62 | } 63 | }); 64 | setImmediate = function(func) { 65 | setImmediateQueue.push(func); 66 | window.postMessage(sentinel, '*'); 67 | }; 68 | } 69 | 70 | // This is used to ensure that we never schedule 2 callas to setImmediate 71 | var isScheduled = false; 72 | 73 | // Keep track of observers that needs to be notified next time. 74 | var scheduledObservers = []; 75 | 76 | /** 77 | * Schedules |dispatchCallback| to be called in the future. 78 | * @param {MutationObserver} observer 79 | */ 80 | function scheduleCallback(observer) { 81 | scheduledObservers.push(observer); 82 | if (!isScheduled) { 83 | isScheduled = true; 84 | setImmediate(dispatchCallbacks); 85 | } 86 | } 87 | 88 | function wrapIfNeeded(node) { 89 | return window.ShadowDOMPolyfill && 90 | window.ShadowDOMPolyfill.wrapIfNeeded(node) || 91 | node; 92 | } 93 | 94 | function dispatchCallbacks() { 95 | // http://dom.spec.whatwg.org/#mutation-observers 96 | 97 | isScheduled = false; // Used to allow a new setImmediate call above. 98 | 99 | var observers = scheduledObservers; 100 | scheduledObservers = []; 101 | // Sort observers based on their creation UID (incremental). 102 | observers.sort(function(o1, o2) { 103 | return o1.uid_ - o2.uid_; 104 | }); 105 | 106 | var anyNonEmpty = false; 107 | observers.forEach(function(observer) { 108 | 109 | // 2.1, 2.2 110 | var queue = observer.takeRecords(); 111 | // 2.3. Remove all transient registered observers whose observer is mo. 112 | removeTransientObserversFor(observer); 113 | 114 | // 2.4 115 | if (queue.length) { 116 | observer.callback_(queue, observer); 117 | anyNonEmpty = true; 118 | } 119 | }); 120 | 121 | // 3. 122 | if (anyNonEmpty) 123 | dispatchCallbacks(); 124 | } 125 | 126 | function removeTransientObserversFor(observer) { 127 | observer.nodes_.forEach(function(node) { 128 | var registrations = registrationsTable.get(node); 129 | if (!registrations) 130 | return; 131 | registrations.forEach(function(registration) { 132 | if (registration.observer === observer) 133 | registration.removeTransientObservers(); 134 | }); 135 | }); 136 | } 137 | 138 | /** 139 | * This function is used for the "For each registered observer observer (with 140 | * observer's options as options) in target's list of registered observers, 141 | * run these substeps:" and the "For each ancestor ancestor of target, and for 142 | * each registered observer observer (with options options) in ancestor's list 143 | * of registered observers, run these substeps:" part of the algorithms. The 144 | * |options.subtree| is checked to ensure that the callback is called 145 | * correctly. 146 | * 147 | * @param {Node} target 148 | * @param {function(MutationObserverInit):MutationRecord} callback 149 | */ 150 | function forEachAncestorAndObserverEnqueueRecord(target, callback) { 151 | for (var node = target; node; node = node.parentNode) { 152 | var registrations = registrationsTable.get(node); 153 | 154 | if (registrations) { 155 | for (var j = 0; j < registrations.length; j++) { 156 | var registration = registrations[j]; 157 | var options = registration.options; 158 | 159 | // Only target ignores subtree. 160 | if (node !== target && !options.subtree) 161 | continue; 162 | 163 | var record = callback(options); 164 | if (record) 165 | registration.enqueue(record); 166 | } 167 | } 168 | } 169 | } 170 | 171 | var uidCounter = 0; 172 | 173 | /** 174 | * The class that maps to the DOM MutationObserver interface. 175 | * @param {Function} callback. 176 | * @constructor 177 | */ 178 | function JsMutationObserver(callback) { 179 | this.callback_ = callback; 180 | this.nodes_ = []; 181 | this.records_ = []; 182 | this.uid_ = ++uidCounter; 183 | } 184 | 185 | JsMutationObserver.prototype = { 186 | observe: function(target, options) { 187 | target = wrapIfNeeded(target); 188 | 189 | // 1.1 190 | if (!options.childList && !options.attributes && !options.characterData || 191 | 192 | // 1.2 193 | options.attributeOldValue && !options.attributes || 194 | 195 | // 1.3 196 | options.attributeFilter && options.attributeFilter.length && 197 | !options.attributes || 198 | 199 | // 1.4 200 | options.characterDataOldValue && !options.characterData) { 201 | 202 | throw new SyntaxError(); 203 | } 204 | 205 | var registrations = registrationsTable.get(target); 206 | if (!registrations) 207 | registrationsTable.set(target, registrations = []); 208 | 209 | // 2 210 | // If target's list of registered observers already includes a registered 211 | // observer associated with the context object, replace that registered 212 | // observer's options with options. 213 | var registration; 214 | for (var i = 0; i < registrations.length; i++) { 215 | if (registrations[i].observer === this) { 216 | registration = registrations[i]; 217 | registration.removeListeners(); 218 | registration.options = options; 219 | break; 220 | } 221 | } 222 | 223 | // 3. 224 | // Otherwise, add a new registered observer to target's list of registered 225 | // observers with the context object as the observer and options as the 226 | // options, and add target to context object's list of nodes on which it 227 | // is registered. 228 | if (!registration) { 229 | registration = new Registration(this, target, options); 230 | registrations.push(registration); 231 | this.nodes_.push(target); 232 | } 233 | 234 | registration.addListeners(); 235 | }, 236 | 237 | disconnect: function() { 238 | this.nodes_.forEach(function(node) { 239 | var registrations = registrationsTable.get(node); 240 | for (var i = 0; i < registrations.length; i++) { 241 | var registration = registrations[i]; 242 | if (registration.observer === this) { 243 | registration.removeListeners(); 244 | registrations.splice(i, 1); 245 | // Each node can only have one registered observer associated with 246 | // this observer. 247 | break; 248 | } 249 | } 250 | }, this); 251 | this.records_ = []; 252 | }, 253 | 254 | takeRecords: function() { 255 | var copyOfRecords = this.records_; 256 | this.records_ = []; 257 | return copyOfRecords; 258 | } 259 | }; 260 | 261 | /** 262 | * @param {string} type 263 | * @param {Node} target 264 | * @constructor 265 | */ 266 | function MutationRecord(type, target) { 267 | this.type = type; 268 | this.target = target; 269 | this.addedNodes = []; 270 | this.removedNodes = []; 271 | this.previousSibling = null; 272 | this.nextSibling = null; 273 | this.attributeName = null; 274 | this.attributeNamespace = null; 275 | this.oldValue = null; 276 | } 277 | 278 | function copyMutationRecord(original) { 279 | var record = new MutationRecord(original.type, original.target); 280 | record.addedNodes = original.addedNodes.slice(); 281 | record.removedNodes = original.removedNodes.slice(); 282 | record.previousSibling = original.previousSibling; 283 | record.nextSibling = original.nextSibling; 284 | record.attributeName = original.attributeName; 285 | record.attributeNamespace = original.attributeNamespace; 286 | record.oldValue = original.oldValue; 287 | return record; 288 | }; 289 | 290 | // We keep track of the two (possibly one) records used in a single mutation. 291 | var currentRecord, recordWithOldValue; 292 | 293 | /** 294 | * Creates a record without |oldValue| and caches it as |currentRecord| for 295 | * later use. 296 | * @param {string} oldValue 297 | * @return {MutationRecord} 298 | */ 299 | function getRecord(type, target) { 300 | return currentRecord = new MutationRecord(type, target); 301 | } 302 | 303 | /** 304 | * Gets or creates a record with |oldValue| based in the |currentRecord| 305 | * @param {string} oldValue 306 | * @return {MutationRecord} 307 | */ 308 | function getRecordWithOldValue(oldValue) { 309 | if (recordWithOldValue) 310 | return recordWithOldValue; 311 | recordWithOldValue = copyMutationRecord(currentRecord); 312 | recordWithOldValue.oldValue = oldValue; 313 | return recordWithOldValue; 314 | } 315 | 316 | function clearRecords() { 317 | currentRecord = recordWithOldValue = undefined; 318 | } 319 | 320 | /** 321 | * @param {MutationRecord} record 322 | * @return {boolean} Whether the record represents a record from the current 323 | * mutation event. 324 | */ 325 | function recordRepresentsCurrentMutation(record) { 326 | return record === recordWithOldValue || record === currentRecord; 327 | } 328 | 329 | /** 330 | * Selects which record, if any, to replace the last record in the queue. 331 | * This returns |null| if no record should be replaced. 332 | * 333 | * @param {MutationRecord} lastRecord 334 | * @param {MutationRecord} newRecord 335 | * @param {MutationRecord} 336 | */ 337 | function selectRecord(lastRecord, newRecord) { 338 | if (lastRecord === newRecord) 339 | return lastRecord; 340 | 341 | // Check if the the record we are adding represents the same record. If 342 | // so, we keep the one with the oldValue in it. 343 | if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord)) 344 | return recordWithOldValue; 345 | 346 | return null; 347 | } 348 | 349 | /** 350 | * Class used to represent a registered observer. 351 | * @param {MutationObserver} observer 352 | * @param {Node} target 353 | * @param {MutationObserverInit} options 354 | * @constructor 355 | */ 356 | function Registration(observer, target, options) { 357 | this.observer = observer; 358 | this.target = target; 359 | this.options = options; 360 | this.transientObservedNodes = []; 361 | } 362 | 363 | Registration.prototype = { 364 | enqueue: function(record) { 365 | var records = this.observer.records_; 366 | var length = records.length; 367 | 368 | // There are cases where we replace the last record with the new record. 369 | // For example if the record represents the same mutation we need to use 370 | // the one with the oldValue. If we get same record (this can happen as we 371 | // walk up the tree) we ignore the new record. 372 | if (records.length > 0) { 373 | var lastRecord = records[length - 1]; 374 | var recordToReplaceLast = selectRecord(lastRecord, record); 375 | if (recordToReplaceLast) { 376 | records[length - 1] = recordToReplaceLast; 377 | return; 378 | } 379 | } else { 380 | scheduleCallback(this.observer); 381 | } 382 | 383 | records[length] = record; 384 | }, 385 | 386 | addListeners: function() { 387 | this.addListeners_(this.target); 388 | }, 389 | 390 | addListeners_: function(node) { 391 | var options = this.options; 392 | if (options.attributes) 393 | node.addEventListener('DOMAttrModified', this, true); 394 | 395 | if (options.characterData) 396 | node.addEventListener('DOMCharacterDataModified', this, true); 397 | 398 | if (options.childList) 399 | node.addEventListener('DOMNodeInserted', this, true); 400 | 401 | if (options.childList || options.subtree) 402 | node.addEventListener('DOMNodeRemoved', this, true); 403 | }, 404 | 405 | removeListeners: function() { 406 | this.removeListeners_(this.target); 407 | }, 408 | 409 | removeListeners_: function(node) { 410 | var options = this.options; 411 | if (options.attributes) 412 | node.removeEventListener('DOMAttrModified', this, true); 413 | 414 | if (options.characterData) 415 | node.removeEventListener('DOMCharacterDataModified', this, true); 416 | 417 | if (options.childList) 418 | node.removeEventListener('DOMNodeInserted', this, true); 419 | 420 | if (options.childList || options.subtree) 421 | node.removeEventListener('DOMNodeRemoved', this, true); 422 | }, 423 | 424 | /** 425 | * Adds a transient observer on node. The transient observer gets removed 426 | * next time we deliver the change records. 427 | * @param {Node} node 428 | */ 429 | addTransientObserver: function(node) { 430 | // Don't add transient observers on the target itself. We already have all 431 | // the required listeners set up on the target. 432 | if (node === this.target) 433 | return; 434 | 435 | this.addListeners_(node); 436 | this.transientObservedNodes.push(node); 437 | var registrations = registrationsTable.get(node); 438 | if (!registrations) 439 | registrationsTable.set(node, registrations = []); 440 | 441 | // We know that registrations does not contain this because we already 442 | // checked if node === this.target. 443 | registrations.push(this); 444 | }, 445 | 446 | removeTransientObservers: function() { 447 | var transientObservedNodes = this.transientObservedNodes; 448 | this.transientObservedNodes = []; 449 | 450 | transientObservedNodes.forEach(function(node) { 451 | // Transient observers are never added to the target. 452 | this.removeListeners_(node); 453 | 454 | var registrations = registrationsTable.get(node); 455 | for (var i = 0; i < registrations.length; i++) { 456 | if (registrations[i] === this) { 457 | registrations.splice(i, 1); 458 | // Each node can only have one registered observer associated with 459 | // this observer. 460 | break; 461 | } 462 | } 463 | }, this); 464 | }, 465 | 466 | handleEvent: function(e) { 467 | // Stop propagation since we are managing the propagation manually. 468 | // This means that other mutation events on the page will not work 469 | // correctly but that is by design. 470 | e.stopImmediatePropagation(); 471 | 472 | switch (e.type) { 473 | case 'DOMAttrModified': 474 | // http://dom.spec.whatwg.org/#concept-mo-queue-attributes 475 | 476 | var name = e.attrName; 477 | var namespace = e.relatedNode.namespaceURI; 478 | var target = e.target; 479 | 480 | // 1. 481 | var record = new getRecord('attributes', target); 482 | record.attributeName = name; 483 | record.attributeNamespace = namespace; 484 | 485 | // 2. 486 | var oldValue = 487 | e.attrChange === MutationEvent.ADDITION ? null : e.prevValue; 488 | 489 | forEachAncestorAndObserverEnqueueRecord(target, function(options) { 490 | // 3.1, 4.2 491 | if (!options.attributes) 492 | return; 493 | 494 | // 3.2, 4.3 495 | if (options.attributeFilter && options.attributeFilter.length && 496 | options.attributeFilter.indexOf(name) === -1 && 497 | options.attributeFilter.indexOf(namespace) === -1) { 498 | return; 499 | } 500 | // 3.3, 4.4 501 | if (options.attributeOldValue) 502 | return getRecordWithOldValue(oldValue); 503 | 504 | // 3.4, 4.5 505 | return record; 506 | }); 507 | 508 | break; 509 | 510 | case 'DOMCharacterDataModified': 511 | // http://dom.spec.whatwg.org/#concept-mo-queue-characterdata 512 | var target = e.target; 513 | 514 | // 1. 515 | var record = getRecord('characterData', target); 516 | 517 | // 2. 518 | var oldValue = e.prevValue; 519 | 520 | 521 | forEachAncestorAndObserverEnqueueRecord(target, function(options) { 522 | // 3.1, 4.2 523 | if (!options.characterData) 524 | return; 525 | 526 | // 3.2, 4.3 527 | if (options.characterDataOldValue) 528 | return getRecordWithOldValue(oldValue); 529 | 530 | // 3.3, 4.4 531 | return record; 532 | }); 533 | 534 | break; 535 | 536 | case 'DOMNodeRemoved': 537 | this.addTransientObserver(e.target); 538 | // Fall through. 539 | case 'DOMNodeInserted': 540 | // http://dom.spec.whatwg.org/#concept-mo-queue-childlist 541 | var target = e.relatedNode; 542 | var changedNode = e.target; 543 | var addedNodes, removedNodes; 544 | if (e.type === 'DOMNodeInserted') { 545 | addedNodes = [changedNode]; 546 | removedNodes = []; 547 | } else { 548 | 549 | addedNodes = []; 550 | removedNodes = [changedNode]; 551 | } 552 | var previousSibling = changedNode.previousSibling; 553 | var nextSibling = changedNode.nextSibling; 554 | 555 | // 1. 556 | var record = getRecord('childList', target); 557 | record.addedNodes = addedNodes; 558 | record.removedNodes = removedNodes; 559 | record.previousSibling = previousSibling; 560 | record.nextSibling = nextSibling; 561 | 562 | forEachAncestorAndObserverEnqueueRecord(target, function(options) { 563 | // 2.1, 3.2 564 | if (!options.childList) 565 | return; 566 | 567 | // 2.2, 3.3 568 | return record; 569 | }); 570 | 571 | } 572 | 573 | clearRecords(); 574 | } 575 | }; 576 | 577 | global.JsMutationObserver = JsMutationObserver; 578 | 579 | if (!global.MutationObserver) 580 | global.MutationObserver = JsMutationObserver; 581 | 582 | 583 | })(this); 584 | 585 | /* 586 | * classList.js: Cross-browser full element.classList implementation. 587 | * 2012-11-15 588 | * 589 | * By Eli Grey, http://eligrey.com 590 | * Public Domain. 591 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 592 | */ 593 | 594 | /*global self, document, DOMException */ 595 | 596 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ 597 | 598 | if (typeof document !== "undefined" && !("classList" in document.createElement("a"))) { 599 | 600 | (function (view) { 601 | 602 | "use strict"; 603 | 604 | if (!('HTMLElement' in view) && !('Element' in view)) return; 605 | 606 | var 607 | classListProp = "classList" 608 | , protoProp = "prototype" 609 | , elemCtrProto = (view.HTMLElement || view.Element)[protoProp] 610 | , objCtr = Object 611 | , strTrim = String[protoProp].trim || function () { 612 | return this.replace(/^\s+|\s+$/g, ""); 613 | } 614 | , arrIndexOf = Array[protoProp].indexOf || function (item) { 615 | var 616 | i = 0 617 | , len = this.length 618 | ; 619 | for (; i < len; i++) { 620 | if (i in this && this[i] === item) { 621 | return i; 622 | } 623 | } 624 | return -1; 625 | } 626 | // Vendors: please allow content code to instantiate DOMExceptions 627 | , DOMEx = function (type, message) { 628 | this.name = type; 629 | this.code = DOMException[type]; 630 | this.message = message; 631 | } 632 | , checkTokenAndGetIndex = function (classList, token) { 633 | if (token === "") { 634 | throw new DOMEx( 635 | "SYNTAX_ERR" 636 | , "An invalid or illegal string was specified" 637 | ); 638 | } 639 | if (/\s/.test(token)) { 640 | throw new DOMEx( 641 | "INVALID_CHARACTER_ERR" 642 | , "String contains an invalid character" 643 | ); 644 | } 645 | return arrIndexOf.call(classList, token); 646 | } 647 | , ClassList = function (elem) { 648 | var 649 | trimmedClasses = strTrim.call(elem.className) 650 | , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] 651 | , i = 0 652 | , len = classes.length 653 | ; 654 | for (; i < len; i++) { 655 | this.push(classes[i]); 656 | } 657 | this._updateClassName = function () { 658 | elem.className = this.toString(); 659 | }; 660 | } 661 | , classListProto = ClassList[protoProp] = [] 662 | , classListGetter = function () { 663 | return new ClassList(this); 664 | } 665 | ; 666 | // Most DOMException implementations don't allow calling DOMException's toString() 667 | // on non-DOMExceptions. Error's toString() is sufficient here. 668 | DOMEx[protoProp] = Error[protoProp]; 669 | classListProto.item = function (i) { 670 | return this[i] || null; 671 | }; 672 | classListProto.contains = function (token) { 673 | token += ""; 674 | return checkTokenAndGetIndex(this, token) !== -1; 675 | }; 676 | classListProto.add = function () { 677 | var 678 | tokens = arguments 679 | , i = 0 680 | , l = tokens.length 681 | , token 682 | , updated = false 683 | ; 684 | do { 685 | token = tokens[i] + ""; 686 | if (checkTokenAndGetIndex(this, token) === -1) { 687 | this.push(token); 688 | updated = true; 689 | } 690 | } 691 | while (++i < l); 692 | 693 | if (updated) { 694 | this._updateClassName(); 695 | } 696 | }; 697 | classListProto.remove = function () { 698 | var 699 | tokens = arguments 700 | , i = 0 701 | , l = tokens.length 702 | , token 703 | , updated = false 704 | ; 705 | do { 706 | token = tokens[i] + ""; 707 | var index = checkTokenAndGetIndex(this, token); 708 | if (index !== -1) { 709 | this.splice(index, 1); 710 | updated = true; 711 | } 712 | } 713 | while (++i < l); 714 | 715 | if (updated) { 716 | this._updateClassName(); 717 | } 718 | }; 719 | classListProto.toggle = function (token, forse) { 720 | token += ""; 721 | 722 | var 723 | result = this.contains(token) 724 | , method = result ? 725 | forse !== true && "remove" 726 | : 727 | forse !== false && "add" 728 | ; 729 | 730 | if (method) { 731 | this[method](token); 732 | } 733 | 734 | return !result; 735 | }; 736 | classListProto.toString = function () { 737 | return this.join(" "); 738 | }; 739 | 740 | if (objCtr.defineProperty) { 741 | var classListPropDesc = { 742 | get: classListGetter 743 | , enumerable: true 744 | , configurable: true 745 | }; 746 | try { 747 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 748 | } catch (ex) { // IE 8 doesn't support enumerable:true 749 | if (ex.number === -0x7FF5EC54) { 750 | classListPropDesc.enumerable = false; 751 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 752 | } 753 | } 754 | } else if (objCtr[protoProp].__defineGetter__) { 755 | elemCtrProto.__defineGetter__(classListProp, classListGetter); 756 | } 757 | 758 | }(self)); 759 | 760 | } 761 | -------------------------------------------------------------------------------- /src/custom-elements.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 The Polymer Authors. All rights reserved. 3 | Use of this source code is governed by a BSD-style 4 | license that can be found in the LICENSE file. 5 | */ 6 | 7 | window.CustomElements = window.CustomElements || {flags:{}}; 8 | 9 | /* 10 | Copyright 2013 The Polymer Authors. All rights reserved. 11 | Use of this source code is governed by a BSD-style 12 | license that can be found in the LICENSE file. 13 | */ 14 | 15 | (function(scope){ 16 | 17 | var logFlags = window.logFlags || {}; 18 | var IMPORT_LINK_TYPE = window.HTMLImports ? HTMLImports.IMPORT_LINK_TYPE : 'none'; 19 | 20 | // walk the subtree rooted at node, applying 'find(element, data)' function 21 | // to each element 22 | // if 'find' returns true for 'element', do not search element's subtree 23 | function findAll(node, find, data) { 24 | var e = node.firstElementChild; 25 | if (!e) { 26 | e = node.firstChild; 27 | while (e && e.nodeType !== Node.ELEMENT_NODE) { 28 | e = e.nextSibling; 29 | } 30 | } 31 | while (e) { 32 | if (find(e, data) !== true) { 33 | findAll(e, find, data); 34 | } 35 | e = e.nextElementSibling; 36 | } 37 | return null; 38 | } 39 | 40 | // walk all shadowRoots on a given node. 41 | function forRoots(node, cb) { 42 | var root = node.shadowRoot; 43 | while(root) { 44 | forSubtree(root, cb); 45 | root = root.olderShadowRoot; 46 | } 47 | } 48 | 49 | // walk the subtree rooted at node, including descent into shadow-roots, 50 | // applying 'cb' to each element 51 | function forSubtree(node, cb) { 52 | //logFlags.dom && node.childNodes && node.childNodes.length && console.group('subTree: ', node); 53 | findAll(node, function(e) { 54 | if (cb(e)) { 55 | return true; 56 | } 57 | forRoots(e, cb); 58 | }); 59 | forRoots(node, cb); 60 | //logFlags.dom && node.childNodes && node.childNodes.length && console.groupEnd(); 61 | } 62 | 63 | // manage lifecycle on added node 64 | function added(node) { 65 | if (upgrade(node)) { 66 | insertedNode(node); 67 | return true; 68 | } 69 | inserted(node); 70 | } 71 | 72 | // manage lifecycle on added node's subtree only 73 | function addedSubtree(node) { 74 | forSubtree(node, function(e) { 75 | if (added(e)) { 76 | return true; 77 | } 78 | }); 79 | } 80 | 81 | // manage lifecycle on added node and it's subtree 82 | function addedNode(node) { 83 | return added(node) || addedSubtree(node); 84 | } 85 | 86 | // upgrade custom elements at node, if applicable 87 | function upgrade(node) { 88 | if (!node.__upgraded__ && node.nodeType === Node.ELEMENT_NODE) { 89 | var type = node.getAttribute('is') || node.localName; 90 | var definition = scope.registry[type]; 91 | if (definition) { 92 | logFlags.dom && console.group('upgrade:', node.localName); 93 | scope.upgrade(node); 94 | logFlags.dom && console.groupEnd(); 95 | return true; 96 | } 97 | } 98 | } 99 | 100 | function insertedNode(node) { 101 | inserted(node); 102 | if (inDocument(node)) { 103 | forSubtree(node, function(e) { 104 | inserted(e); 105 | }); 106 | } 107 | } 108 | 109 | // TODO(sorvell): on platforms without MutationObserver, mutations may not be 110 | // reliable and therefore attached/detached are not reliable. 111 | // To make these callbacks less likely to fail, we defer all inserts and removes 112 | // to give a chance for elements to be inserted into dom. 113 | // This ensures attachedCallback fires for elements that are created and 114 | // immediately added to dom. 115 | var hasPolyfillMutations = (!window.MutationObserver || 116 | (window.MutationObserver === window.JsMutationObserver)); 117 | scope.hasPolyfillMutations = hasPolyfillMutations; 118 | 119 | var isPendingMutations = false; 120 | var pendingMutations = []; 121 | function deferMutation(fn) { 122 | pendingMutations.push(fn); 123 | if (!isPendingMutations) { 124 | isPendingMutations = true; 125 | var async = (window.Platform && window.Platform.endOfMicrotask) || 126 | setTimeout; 127 | async(takeMutations); 128 | } 129 | } 130 | 131 | function takeMutations() { 132 | isPendingMutations = false; 133 | var $p = pendingMutations; 134 | for (var i=0, l=$p.length, p; (i 1) { 172 | logFlags.dom && console.warn('inserted:', element.localName, 173 | 'insert/remove count:', element.__inserted) 174 | } else if (element.attachedCallback) { 175 | logFlags.dom && console.log('inserted:', element.localName); 176 | element.attachedCallback(); 177 | } 178 | } 179 | logFlags.dom && console.groupEnd(); 180 | } 181 | } 182 | 183 | function removedNode(node) { 184 | removed(node); 185 | forSubtree(node, function(e) { 186 | removed(e); 187 | }); 188 | } 189 | 190 | function removed(element) { 191 | if (hasPolyfillMutations) { 192 | deferMutation(function() { 193 | _removed(element); 194 | }); 195 | } else { 196 | _removed(element); 197 | } 198 | } 199 | 200 | function _removed(element) { 201 | // TODO(sjmiles): temporary: do work on all custom elements so we can track 202 | // behavior even when callbacks not defined 203 | if (element.attachedCallback || element.detachedCallback || (element.__upgraded__ && logFlags.dom)) { 204 | logFlags.dom && console.group('removed:', element.localName); 205 | if (!inDocument(element)) { 206 | element.__inserted = (element.__inserted || 0) - 1; 207 | // if we are in a 'inserted' state, bluntly adjust to an 'removed' state 208 | if (element.__inserted > 0) { 209 | element.__inserted = 0; 210 | } 211 | // if we are 'over removed', squelch the callback 212 | if (element.__inserted < 0) { 213 | logFlags.dom && console.warn('removed:', element.localName, 214 | 'insert/remove count:', element.__inserted) 215 | } else if (element.detachedCallback) { 216 | element.detachedCallback(); 217 | } 218 | } 219 | logFlags.dom && console.groupEnd(); 220 | } 221 | } 222 | 223 | // SD polyfill intrustion due mainly to the fact that 'document' 224 | // is not entirely wrapped 225 | function wrapIfNeeded(node) { 226 | return window.ShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node) 227 | : node; 228 | } 229 | 230 | function inDocument(element) { 231 | var p = element; 232 | var doc = wrapIfNeeded(document); 233 | while (p) { 234 | if (p == doc) { 235 | return true; 236 | } 237 | p = p.parentNode || p.host; 238 | } 239 | } 240 | 241 | function watchShadow(node) { 242 | if (node.shadowRoot && !node.shadowRoot.__watched) { 243 | logFlags.dom && console.log('watching shadow-root for: ', node.localName); 244 | // watch all unwatched roots... 245 | var root = node.shadowRoot; 246 | while (root) { 247 | watchRoot(root); 248 | root = root.olderShadowRoot; 249 | } 250 | } 251 | } 252 | 253 | function watchRoot(root) { 254 | if (!root.__watched) { 255 | observe(root); 256 | root.__watched = true; 257 | } 258 | } 259 | 260 | function handler(mutations) { 261 | // 262 | if (logFlags.dom) { 263 | var mx = mutations[0]; 264 | if (mx && mx.type === 'childList' && mx.addedNodes) { 265 | if (mx.addedNodes) { 266 | var d = mx.addedNodes[0]; 267 | while (d && d !== document && !d.host) { 268 | d = d.parentNode; 269 | } 270 | var u = d && (d.URL || d._URL || (d.host && d.host.localName)) || ''; 271 | u = u.split('/?').shift().split('/').pop(); 272 | } 273 | } 274 | console.group('mutations (%d) [%s]', mutations.length, u || ''); 275 | } 276 | // 277 | mutations.forEach(function(mx) { 278 | //logFlags.dom && console.group('mutation'); 279 | if (mx.type === 'childList') { 280 | forEach(mx.addedNodes, function(n) { 281 | //logFlags.dom && console.log(n.localName); 282 | if (!n.localName) { 283 | return; 284 | } 285 | // nodes added may need lifecycle management 286 | addedNode(n); 287 | }); 288 | // removed nodes may need lifecycle management 289 | forEach(mx.removedNodes, function(n) { 290 | //logFlags.dom && console.log(n.localName); 291 | if (!n.localName) { 292 | return; 293 | } 294 | removedNode(n); 295 | }); 296 | } 297 | //logFlags.dom && console.groupEnd(); 298 | }); 299 | logFlags.dom && console.groupEnd(); 300 | }; 301 | 302 | var observer = new MutationObserver(handler); 303 | 304 | function takeRecords() { 305 | // TODO(sjmiles): ask Raf why we have to call handler ourselves 306 | handler(observer.takeRecords()); 307 | takeMutations(); 308 | } 309 | 310 | var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); 311 | 312 | function observe(inRoot) { 313 | observer.observe(inRoot, {childList: true, subtree: true}); 314 | } 315 | 316 | function observeDocument(doc) { 317 | observe(doc); 318 | } 319 | 320 | function upgradeDocument(doc) { 321 | logFlags.dom && console.group('upgradeDocument: ', (doc.baseURI).split('/').pop()); 322 | addedNode(doc); 323 | logFlags.dom && console.groupEnd(); 324 | } 325 | 326 | function upgradeDocumentTree(doc) { 327 | doc = wrapIfNeeded(doc); 328 | //console.log('upgradeDocumentTree: ', (doc.baseURI).split('/').pop()); 329 | // upgrade contained imported documents 330 | var imports = doc.querySelectorAll('link[rel=' + IMPORT_LINK_TYPE + ']'); 331 | for (var i=0, l=imports.length, n; (i= 0) { 739 | implement(element, HTMLElement); 740 | } 741 | return element; 742 | } 743 | 744 | function upgradeElement(element) { 745 | if (!element.__upgraded__ && (element.nodeType === Node.ELEMENT_NODE)) { 746 | var is = element.getAttribute('is'); 747 | var definition = getRegisteredDefinition(is || element.localName); 748 | if (definition) { 749 | if (is && definition.tag == element.localName) { 750 | return upgrade(element, definition); 751 | } else if (!is && !definition.extends) { 752 | return upgrade(element, definition); 753 | } 754 | } 755 | } 756 | } 757 | 758 | function cloneNode(deep) { 759 | // call original clone 760 | var n = domCloneNode.call(this, deep); 761 | // upgrade the element and subtree 762 | scope.upgradeAll(n); 763 | // return the clone 764 | return n; 765 | } 766 | // capture native createElement before we override it 767 | 768 | var domCreateElement = document.createElement.bind(document); 769 | var domCreateElementNS = document.createElementNS.bind(document); 770 | 771 | // capture native cloneNode before we override it 772 | 773 | var domCloneNode = Node.prototype.cloneNode; 774 | 775 | // exports 776 | 777 | document.registerElement = register; 778 | document.createElement = createElement; // override 779 | document.createElementNS = createElementNS; // override 780 | Node.prototype.cloneNode = cloneNode; // override 781 | 782 | scope.registry = registry; 783 | 784 | /** 785 | * Upgrade an element to a custom element. Upgrading an element 786 | * causes the custom prototype to be applied, an `is` attribute 787 | * to be attached (as needed), and invocation of the `readyCallback`. 788 | * `upgrade` does nothing if the element is already upgraded, or 789 | * if it matches no registered custom tag name. 790 | * 791 | * @method ugprade 792 | * @param {Element} element The element to upgrade. 793 | * @return {Element} The upgraded element. 794 | */ 795 | scope.upgrade = upgradeElement; 796 | } 797 | 798 | // Create a custom 'instanceof'. This is necessary when CustomElements 799 | // are implemented via a mixin strategy, as for example on IE10. 800 | var isInstance; 801 | if (!Object.__proto__ && !useNative) { 802 | isInstance = function(obj, ctor) { 803 | var p = obj; 804 | while (p) { 805 | // NOTE: this is not technically correct since we're not checking if 806 | // an object is an instance of a constructor; however, this should 807 | // be good enough for the mixin strategy. 808 | if (p === ctor.prototype) { 809 | return true; 810 | } 811 | p = p.__proto__; 812 | } 813 | return false; 814 | } 815 | } else { 816 | isInstance = function(obj, base) { 817 | return obj instanceof base; 818 | } 819 | } 820 | 821 | // exports 822 | scope.instanceof = isInstance; 823 | scope.reservedTagList = reservedTagList; 824 | 825 | // bc 826 | document.register = document.registerElement; 827 | 828 | scope.hasNative = hasNative; 829 | scope.useNative = useNative; 830 | 831 | })(window.CustomElements); 832 | 833 | /* 834 | * Copyright 2013 The Polymer Authors. All rights reserved. 835 | * Use of this source code is governed by a BSD-style 836 | * license that can be found in the LICENSE file. 837 | */ 838 | 839 | (function(scope) { 840 | 841 | // import 842 | 843 | var IMPORT_LINK_TYPE = scope.IMPORT_LINK_TYPE; 844 | 845 | // highlander object for parsing a document tree 846 | 847 | var parser = { 848 | selectors: [ 849 | 'link[rel=' + IMPORT_LINK_TYPE + ']' 850 | ], 851 | map: { 852 | link: 'parseLink' 853 | }, 854 | parse: function(inDocument) { 855 | if (!inDocument.__parsed) { 856 | // only parse once 857 | inDocument.__parsed = true; 858 | // all parsable elements in inDocument (depth-first pre-order traversal) 859 | var elts = inDocument.querySelectorAll(parser.selectors); 860 | // for each parsable node type, call the mapped parsing method 861 | forEach(elts, function(e) { 862 | parser[parser.map[e.localName]](e); 863 | }); 864 | // upgrade all upgradeable static elements, anything dynamically 865 | // created should be caught by observer 866 | CustomElements.upgradeDocument(inDocument); 867 | // observe document for dom changes 868 | CustomElements.observeDocument(inDocument); 869 | } 870 | }, 871 | parseLink: function(linkElt) { 872 | // imports 873 | if (isDocumentLink(linkElt)) { 874 | this.parseImport(linkElt); 875 | } 876 | }, 877 | parseImport: function(linkElt) { 878 | if (linkElt.import) { 879 | parser.parse(linkElt.import); 880 | } 881 | } 882 | }; 883 | 884 | function isDocumentLink(inElt) { 885 | return (inElt.localName === 'link' 886 | && inElt.getAttribute('rel') === IMPORT_LINK_TYPE); 887 | } 888 | 889 | var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); 890 | 891 | // exports 892 | 893 | scope.parser = parser; 894 | scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE; 895 | 896 | })(window.CustomElements); 897 | /* 898 | * Copyright 2013 The Polymer Authors. All rights reserved. 899 | * Use of this source code is governed by a BSD-style 900 | * license that can be found in the LICENSE file. 901 | */ 902 | (function(scope){ 903 | 904 | // bootstrap parsing 905 | function bootstrap() { 906 | // parse document 907 | CustomElements.parser.parse(document); 908 | // one more pass before register is 'live' 909 | CustomElements.upgradeDocument(document); 910 | // choose async 911 | var async = window.Platform && Platform.endOfMicrotask ? 912 | Platform.endOfMicrotask : 913 | setTimeout; 914 | async(function() { 915 | // set internal 'ready' flag, now document.registerElement will trigger 916 | // synchronous upgrades 917 | CustomElements.ready = true; 918 | // capture blunt profiling data 919 | CustomElements.readyTime = Date.now(); 920 | if (window.HTMLImports) { 921 | CustomElements.elapsed = CustomElements.readyTime - HTMLImports.readyTime; 922 | } 923 | // notify the system that we are bootstrapped 924 | document.dispatchEvent( 925 | new CustomEvent('WebComponentsReady', {bubbles: true}) 926 | ); 927 | 928 | // install upgrade hook if HTMLImports are available 929 | if (window.HTMLImports) { 930 | HTMLImports.__importsParsingHook = function(elt) { 931 | CustomElements.parser.parse(elt.import); 932 | } 933 | } 934 | }); 935 | } 936 | 937 | // CustomEvent shim for IE 938 | if (typeof window.CustomEvent !== 'function') { 939 | window.CustomEvent = function(inType) { 940 | var e = document.createEvent('HTMLEvents'); 941 | e.initEvent(inType, true, true); 942 | return e; 943 | }; 944 | } 945 | 946 | // When loading at readyState complete time (or via flag), boot custom elements 947 | // immediately. 948 | // If relevant, HTMLImports must already be loaded. 949 | if (document.readyState === 'complete' || scope.flags.eager) { 950 | bootstrap(); 951 | // When loading at readyState interactive time, bootstrap only if HTMLImports 952 | // are not pending. Also avoid IE as the semantics of this state are unreliable. 953 | } else if (document.readyState === 'interactive' && !window.attachEvent && 954 | (!window.HTMLImports || window.HTMLImports.ready)) { 955 | bootstrap(); 956 | // When loading at other readyStates, wait for the appropriate DOM event to 957 | // bootstrap. 958 | } else { 959 | var loadEvent = window.HTMLImports && !HTMLImports.ready ? 960 | 'HTMLImportsLoaded' : 'DOMContentLoaded'; 961 | window.addEventListener(loadEvent, bootstrap); 962 | } 963 | 964 | })(window.CustomElements); 965 | --------------------------------------------------------------------------------