├── demo ├── dependencies.css ├── import.html ├── index.html └── dependencies.js ├── .gitignore ├── .gitmodules ├── README.md ├── bower.json ├── package.json ├── xtag.json ├── Gruntfile.js └── src └── html-imports.js /demo/dependencies.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /demo/import.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "HTMLImports"] 2 | path = HTMLImports 3 | url = https://github.com/Polymer/HTMLImports.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About HTMLImports 2 | 3 | This project is a Bower wrapper around https://github.com/Polymer/HTMLImports so that it is easily consumable without relying on the polymer module loader. 4 | 5 | 6 | ``` 7 | bower install HTMLImports 8 | ``` 9 | 10 | 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML-Imports", 3 | "description": "HTML Imports polyfill for Web Components.", 4 | "version": "0.2.3", 5 | "author": "Arron Schaar", 6 | "keywords": [ 7 | "polyfill", 8 | "web-components", 9 | "x-tag" 10 | ], 11 | "main": [ 12 | "src/html-imports.js" 13 | ], 14 | "dependencies": { 15 | "MutationObserver": "~0.2.0", 16 | "WeakMap": "~0.2.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmlimports", 3 | "description": "Makes Polymer/HTMLImports Bower friendly", 4 | "version": "0.2.3", 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-smush-components": "~0.2.0", 15 | "grunt-tagrelease": "~0.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /xtag.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmlimports", 3 | "tagName": "h", 4 | "version": "0.2.3", 5 | "author": "Arron Schaar", 6 | "description": "Makes Polymer/HTMLImports Bower friendly", 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": "Makes Polymer/HTMLImports bower friendly", 18 | "attributes": {}, 19 | "events": {}, 20 | "methods": {}, 21 | "getters": {}, 22 | "setters": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HTMLImports - X-Tag 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /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 | 'HTMLImports/src/scope.js', 38 | 'HTMLImports/src/Loader.js', 39 | 'HTMLImports/src/Parser.js', 40 | 'HTMLImports/src/HTMLImports.js', 41 | 'HTMLImports/src/Observer.js', 42 | 'HTMLImports/src/boot.js' 43 | ], 44 | dest: 'src/html-imports.js', 45 | } 46 | } 47 | }); 48 | 49 | grunt.loadNpmTasks('grunt-contrib-concat'); 50 | grunt.loadNpmTasks('grunt-contrib-connect'); 51 | grunt.loadNpmTasks('grunt-contrib-jshint'); 52 | grunt.loadNpmTasks('grunt-smush-components'); 53 | grunt.loadNpmTasks('grunt-tagrelease'); 54 | grunt.loadNpmTasks('grunt-bumpup'); 55 | 56 | grunt.registerTask('build', ['smush-components','concat:dist']); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/html-imports.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 | window.HTMLImports = window.HTMLImports || {flags:{}}; 7 | /* 8 | * Copyright 2013 The Polymer Authors. All rights reserved. 9 | * Use of this source code is governed by a BSD-style 10 | * license that can be found in the LICENSE file. 11 | */ 12 | 13 | (function(scope) { 14 | 15 | // imports 16 | var path = scope.path; 17 | var xhr = scope.xhr; 18 | var flags = scope.flags; 19 | 20 | // TODO(sorvell): this loader supports a dynamic list of urls 21 | // and an oncomplete callback that is called when the loader is done. 22 | // The polyfill currently does *not* need this dynamism or the onComplete 23 | // concept. Because of this, the loader could be simplified quite a bit. 24 | var Loader = function(onLoad, onComplete) { 25 | this.cache = {}; 26 | this.onload = onLoad; 27 | this.oncomplete = onComplete; 28 | this.inflight = 0; 29 | this.pending = {}; 30 | }; 31 | 32 | Loader.prototype = { 33 | addNodes: function(nodes) { 34 | // number of transactions to complete 35 | this.inflight += nodes.length; 36 | // commence transactions 37 | for (var i=0, l=nodes.length, n; (i -1) { 91 | body = atob(body); 92 | } else { 93 | body = decodeURIComponent(body); 94 | } 95 | setTimeout(function() { 96 | this.receive(url, elt, null, body); 97 | }.bind(this), 0); 98 | } else { 99 | var receiveXhr = function(err, resource) { 100 | this.receive(url, elt, err, resource); 101 | }.bind(this); 102 | xhr.load(url, receiveXhr); 103 | // TODO(sorvell): blocked on) 104 | // https://code.google.com/p/chromium/issues/detail?id=257221 105 | // xhr'ing for a document makes scripts in imports runnable; otherwise 106 | // they are not; however, it requires that we have doctype=html in 107 | // the import which is unacceptable. This is only needed on Chrome 108 | // to avoid the bug above. 109 | /* 110 | if (isDocumentLink(elt)) { 111 | xhr.loadDocument(url, receiveXhr); 112 | } else { 113 | xhr.load(url, receiveXhr); 114 | } 115 | */ 116 | } 117 | }, 118 | receive: function(url, elt, err, resource) { 119 | this.cache[url] = resource; 120 | var $p = this.pending[url]; 121 | for (var i=0, l=$p.length, p; (i= 200 && request.status < 300) 144 | || (request.status === 304) 145 | || (request.status === 0); 146 | }, 147 | load: function(url, next, nextContext) { 148 | var request = new XMLHttpRequest(); 149 | if (scope.flags.debug || scope.flags.bust) { 150 | url += '?' + Math.random(); 151 | } 152 | request.open('GET', url, xhr.async); 153 | request.addEventListener('readystatechange', function(e) { 154 | if (request.readyState === 4) { 155 | next.call(nextContext, !xhr.ok(request) && request, 156 | request.response || request.responseText, url); 157 | } 158 | }); 159 | request.send(); 160 | return request; 161 | }, 162 | loadDocument: function(url, next, nextContext) { 163 | this.load(url, next, nextContext).responseType = 'document'; 164 | } 165 | }; 166 | 167 | // exports 168 | scope.xhr = xhr; 169 | scope.Loader = Loader; 170 | 171 | })(window.HTMLImports); 172 | 173 | /* 174 | * Copyright 2013 The Polymer Authors. All rights reserved. 175 | * Use of this source code is governed by a BSD-style 176 | * license that can be found in the LICENSE file. 177 | */ 178 | 179 | (function(scope) { 180 | 181 | var IMPORT_LINK_TYPE = 'import'; 182 | var flags = scope.flags; 183 | var isIe = /Trident/.test(navigator.userAgent); 184 | // TODO(sorvell): SD polyfill intrusion 185 | var mainDoc = window.ShadowDOMPolyfill ? 186 | window.ShadowDOMPolyfill.wrapIfNeeded(document) : document; 187 | 188 | // importParser 189 | // highlander object to manage parsing of imports 190 | // parses import related elements 191 | // and ensures proper parse order 192 | // parse order is enforced by crawling the tree and monitoring which elements 193 | // have been parsed; async parsing is also supported. 194 | 195 | // highlander object for parsing a document tree 196 | var importParser = { 197 | // parse selectors for main document elements 198 | documentSelectors: 'link[rel=' + IMPORT_LINK_TYPE + ']', 199 | // parse selectors for import document elements 200 | importsSelectors: [ 201 | 'link[rel=' + IMPORT_LINK_TYPE + ']', 202 | 'link[rel=stylesheet]', 203 | 'style', 204 | 'script:not([type])', 205 | 'script[type="text/javascript"]' 206 | ].join(','), 207 | map: { 208 | link: 'parseLink', 209 | script: 'parseScript', 210 | style: 'parseStyle' 211 | }, 212 | // try to parse the next import in the tree 213 | parseNext: function() { 214 | var next = this.nextToParse(); 215 | if (next) { 216 | this.parse(next); 217 | } 218 | }, 219 | parse: function(elt) { 220 | if (this.isParsed(elt)) { 221 | flags.parse && console.log('[%s] is already parsed', elt.localName); 222 | return; 223 | } 224 | var fn = this[this.map[elt.localName]]; 225 | if (fn) { 226 | this.markParsing(elt); 227 | fn.call(this, elt); 228 | } 229 | }, 230 | // only 1 element may be parsed at a time; parsing is async so, each 231 | // parsing implementation must inform the system that parsing is complete 232 | // via markParsingComplete. 233 | markParsing: function(elt) { 234 | flags.parse && console.log('parsing', elt); 235 | this.parsingElement = elt; 236 | }, 237 | markParsingComplete: function(elt) { 238 | elt.__importParsed = true; 239 | if (elt.__importElement) { 240 | elt.__importElement.__importParsed = true; 241 | } 242 | this.parsingElement = null; 243 | flags.parse && console.log('completed', elt); 244 | this.parseNext(); 245 | }, 246 | parseImport: function(elt) { 247 | elt.import.__importParsed = true; 248 | // TODO(sorvell): consider if there's a better way to do this; 249 | // expose an imports parsing hook; this is needed, for example, by the 250 | // CustomElements polyfill. 251 | if (HTMLImports.__importsParsingHook) { 252 | HTMLImports.__importsParsingHook(elt); 253 | } 254 | // fire load event 255 | if (elt.__resource) { 256 | elt.dispatchEvent(new CustomEvent('load', {bubbles: false})); 257 | } else { 258 | elt.dispatchEvent(new CustomEvent('error', {bubbles: false})); 259 | } 260 | // TODO(sorvell): workaround for Safari addEventListener not working 261 | // for elements not in the main document. 262 | if (elt.__pending) { 263 | var fn; 264 | while (elt.__pending.length) { 265 | fn = elt.__pending.shift(); 266 | if (fn) { 267 | fn({target: elt}); 268 | } 269 | } 270 | } 271 | this.markParsingComplete(elt); 272 | }, 273 | parseLink: function(linkElt) { 274 | if (nodeIsImport(linkElt)) { 275 | this.parseImport(linkElt); 276 | } else { 277 | // make href absolute 278 | linkElt.href = linkElt.href; 279 | this.parseGeneric(linkElt); 280 | } 281 | }, 282 | parseStyle: function(elt) { 283 | // TODO(sorvell): style element load event can just not fire so clone styles 284 | var src = elt; 285 | elt = cloneStyle(elt); 286 | elt.__importElement = src; 287 | this.parseGeneric(elt); 288 | }, 289 | parseGeneric: function(elt) { 290 | this.trackElement(elt); 291 | document.head.appendChild(elt); 292 | }, 293 | // tracks when a loadable element has loaded 294 | trackElement: function(elt, callback) { 295 | var self = this; 296 | var done = function(e) { 297 | if (callback) { 298 | callback(e); 299 | } 300 | self.markParsingComplete(elt); 301 | }; 302 | elt.addEventListener('load', done); 303 | elt.addEventListener('error', done); 304 | 305 | // NOTE: IE does not fire "load" event for styles that have already loaded 306 | // This is in violation of the spec, so we try our hardest to work around it 307 | if (isIe && elt.localName === 'style') { 308 | var fakeLoad = false; 309 | // If there's not @import in the textContent, assume it has loaded 310 | if (elt.textContent.indexOf('@import') == -1) { 311 | fakeLoad = true; 312 | // if we have a sheet, we have been parsed 313 | } else if (elt.sheet) { 314 | fakeLoad = true; 315 | var csr = elt.sheet.cssRules; 316 | var len = csr ? csr.length : 0; 317 | // search the rules for @import's 318 | for (var i = 0, r; (i < len) && (r = csr[i]); i++) { 319 | if (r.type === CSSRule.IMPORT_RULE) { 320 | // if every @import has resolved, fake the load 321 | fakeLoad = fakeLoad && Boolean(r.styleSheet); 322 | } 323 | } 324 | } 325 | // dispatch a fake load event and continue parsing 326 | if (fakeLoad) { 327 | elt.dispatchEvent(new CustomEvent('load', {bubbles: false})); 328 | } 329 | } 330 | }, 331 | // NOTE: execute scripts by injecting them and watching for the load/error 332 | // event. Inline scripts are handled via dataURL's because browsers tend to 333 | // provide correct parsing errors in this case. If this has any compatibility 334 | // issues, we can switch to injecting the inline script with textContent. 335 | // Scripts with dataURL's do not appear to generate load events and therefore 336 | // we assume they execute synchronously. 337 | parseScript: function(scriptElt) { 338 | var script = document.createElement('script'); 339 | script.__importElement = scriptElt; 340 | script.src = scriptElt.src ? scriptElt.src : 341 | generateScriptDataUrl(scriptElt); 342 | scope.currentScript = scriptElt; 343 | this.trackElement(script, function(e) { 344 | script.parentNode.removeChild(script); 345 | scope.currentScript = null; 346 | }); 347 | document.head.appendChild(script); 348 | }, 349 | // determine the next element in the tree which should be parsed 350 | nextToParse: function() { 351 | return !this.parsingElement && this.nextToParseInDoc(mainDoc); 352 | }, 353 | nextToParseInDoc: function(doc, link) { 354 | var nodes = doc.querySelectorAll(this.parseSelectorsForNode(doc)); 355 | for (var i=0, l=nodes.length, p=0, n; (i.content 526 | // see https://code.google.com/p/chromium/issues/detail?id=249381. 527 | elt.__resource = resource; 528 | if (isDocumentLink(elt)) { 529 | var doc = this.documents[url]; 530 | // if we've never seen a document at this url 531 | if (!doc) { 532 | // generate an HTMLDocument from data 533 | doc = makeDocument(resource, url); 534 | doc.__importLink = elt; 535 | // TODO(sorvell): we cannot use MO to detect parsed nodes because 536 | // SD polyfill does not report these as mutations. 537 | this.bootDocument(doc); 538 | // cache document 539 | this.documents[url] = doc; 540 | } 541 | // don't store import record until we're actually loaded 542 | // store document resource 543 | elt.import = doc; 544 | } 545 | parser.parseNext(); 546 | }, 547 | bootDocument: function(doc) { 548 | this.loadSubtree(doc); 549 | this.observe(doc); 550 | parser.parseNext(); 551 | }, 552 | loadedAll: function() { 553 | parser.parseNext(); 554 | } 555 | }; 556 | 557 | // loader singleton 558 | var importLoader = new Loader(importer.loaded.bind(importer), 559 | importer.loadedAll.bind(importer)); 560 | 561 | function isDocumentLink(elt) { 562 | return isLinkRel(elt, IMPORT_LINK_TYPE); 563 | } 564 | 565 | function isLinkRel(elt, rel) { 566 | return elt.localName === 'link' && elt.getAttribute('rel') === rel; 567 | } 568 | 569 | function isScript(elt) { 570 | return elt.localName === 'script'; 571 | } 572 | 573 | function makeDocument(resource, url) { 574 | // create a new HTML document 575 | var doc = resource; 576 | if (!(doc instanceof Document)) { 577 | doc = document.implementation.createHTMLDocument(IMPORT_LINK_TYPE); 578 | } 579 | // cache the new document's source url 580 | doc._URL = url; 581 | // establish a relative path via 582 | var base = doc.createElement('base'); 583 | base.setAttribute('href', url); 584 | // add baseURI support to browsers (IE) that lack it. 585 | if (!doc.baseURI) { 586 | doc.baseURI = url; 587 | } 588 | // ensure UTF-8 charset 589 | var meta = doc.createElement('meta'); 590 | meta.setAttribute('charset', 'utf-8'); 591 | 592 | doc.head.appendChild(meta); 593 | doc.head.appendChild(base); 594 | // install HTML last as it may trigger CustomElement upgrades 595 | // TODO(sjmiles): problem wrt to template boostrapping below, 596 | // template bootstrapping must (?) come before element upgrade 597 | // but we cannot bootstrap templates until they are in a document 598 | // which is too late 599 | if (!(resource instanceof Document)) { 600 | // install html 601 | doc.body.innerHTML = resource; 602 | } 603 | // TODO(sorvell): ideally this code is not aware of Template polyfill, 604 | // but for now the polyfill needs help to bootstrap these templates 605 | if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) { 606 | HTMLTemplateElement.bootstrap(doc); 607 | } 608 | return doc; 609 | } 610 | } else { 611 | // do nothing if using native imports 612 | var importer = {}; 613 | } 614 | 615 | // NOTE: We cannot polyfill document.currentScript because it's not possible 616 | // both to override and maintain the ability to capture the native value; 617 | // therefore we choose to expose _currentScript both when native imports 618 | // and the polyfill are in use. 619 | var currentScriptDescriptor = { 620 | get: function() { 621 | return HTMLImports.currentScript || document.currentScript; 622 | }, 623 | configurable: true 624 | }; 625 | 626 | Object.defineProperty(document, '_currentScript', currentScriptDescriptor); 627 | Object.defineProperty(mainDoc, '_currentScript', currentScriptDescriptor); 628 | 629 | // Polyfill document.baseURI for browsers without it. 630 | if (!document.baseURI) { 631 | var baseURIDescriptor = { 632 | get: function() { 633 | return window.location.href; 634 | }, 635 | configurable: true 636 | }; 637 | 638 | Object.defineProperty(document, 'baseURI', baseURIDescriptor); 639 | Object.defineProperty(mainDoc, 'baseURI', baseURIDescriptor); 640 | } 641 | 642 | // call a callback when all HTMLImports in the document at call (or at least 643 | // document ready) time have loaded. 644 | // 1. ensure the document is in a ready state (has dom), then 645 | // 2. watch for loading of imports and call callback when done 646 | function whenImportsReady(callback, doc) { 647 | doc = doc || mainDoc; 648 | // if document is loading, wait and try again 649 | whenDocumentReady(function() { 650 | watchImportsLoad(callback, doc); 651 | }, doc); 652 | } 653 | 654 | // call the callback when the document is in a ready state (has dom) 655 | var requiredReadyState = HTMLImports.isIE ? 'complete' : 'interactive'; 656 | var READY_EVENT = 'readystatechange'; 657 | function isDocumentReady(doc) { 658 | return (doc.readyState === 'complete' || 659 | doc.readyState === requiredReadyState); 660 | } 661 | 662 | // call when we ensure the document is in a ready state 663 | function whenDocumentReady(callback, doc) { 664 | if (!isDocumentReady(doc)) { 665 | var checkReady = function() { 666 | if (doc.readyState === 'complete' || 667 | doc.readyState === requiredReadyState) { 668 | doc.removeEventListener(READY_EVENT, checkReady); 669 | whenDocumentReady(callback, doc); 670 | } 671 | } 672 | doc.addEventListener(READY_EVENT, checkReady); 673 | } else if (callback) { 674 | callback(); 675 | } 676 | } 677 | 678 | // call when we ensure all imports have loaded 679 | function watchImportsLoad(callback, doc) { 680 | var imports = doc.querySelectorAll('link[rel=import]'); 681 | var loaded = 0, l = imports.length; 682 | function checkDone(d) { 683 | if (loaded == l) { 684 | // go async to ensure parser isn't stuck on a script tag 685 | requestAnimationFrame(callback); 686 | } 687 | } 688 | function loadedImport(e) { 689 | loaded++; 690 | checkDone(); 691 | } 692 | if (l) { 693 | for (var i=0, imp; (i