diff --git a/aio/src/assets/js/native-shim.js b/aio/src/assets/js/native-shim.js new file mode 100644 index 0000000000..2011d29a66 --- /dev/null +++ b/aio/src/assets/js/native-shim.js @@ -0,0 +1,164 @@ +/** + * @license + * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/** + * This shim allows elements written in, or compiled to, ES5 to work on native + * implementations of Custom Elements. + * + * ES5-style classes don't work with native Custom Elements because the + * HTMLElement constructor uses the value of `new.target` to look up the custom + * element definition for the currently called constructor. `new.target` is only + * set when `new` is called and is only propagated via super() calls. super() + * is not emulatable in ES5. The pattern of `SuperClass.call(this)`` only works + * when extending other ES5-style classes, and does not propagate `new.target`. + * + * This shim allows the native HTMLElement constructor to work by generating and + * registering a stand-in class instead of the users custom element class. This + * stand-in class's constructor has an actual call to super(). + * `customElements.define()` and `customElements.get()` are both overridden to + * hide this stand-in class from users. + * + * In order to create instance of the user-defined class, rather than the stand + * in, the stand-in's constructor swizzles its instances prototype and invokes + * the user-defined constructor. When the user-defined constructor is called + * directly it creates an instance of the stand-in class to get a real extension + * of HTMLElement and returns that. + * + * There are two important constructors: A patched HTMLElement constructor, and + * the StandInElement constructor. They both will be called to create an element + * but which is called first depends on whether the browser creates the element + * or the user-defined constructor is called directly. The variables + * `browserConstruction` and `userConstruction` control the flow between the + * two constructors. + * + * This shim should be better than forcing the polyfill because: + * 1. It's smaller + * 2. All reaction timings are the same as native (mostly synchronous) + * 3. All reaction triggering DOM operations are automatically supported + * + * There are some restrictions and requirements on ES5 constructors: + * 1. All constructors in a inheritance hierarchy must be ES5-style, so that + * they can be called with Function.call(). This effectively means that the + * whole application must be compiled to ES5. + * 2. Constructors must return the value of the emulated super() call. Like + * `return SuperClass.call(this)` + * 3. The `this` reference should not be used before the emulated super() call + * just like `this` is illegal to use before super() in ES6. + * 4. Constructors should not create other custom elements before the emulated + * super() call. This is the same restriction as with native custom + * elements. + * + * Compiling valid class-based custom elements to ES5 will satisfy these + * requirements with the latest version of popular transpilers. + */ +(() => { + 'use strict'; + + // Do nothing if `customElements` does not exist. + if (!window.customElements) return; + + const NativeHTMLElement = window.HTMLElement; + const nativeDefine = window.customElements.define; + const nativeGet = window.customElements.get; + + /** + * Map of user-provided constructors to tag names. + * + * @type {Map} + */ + const tagnameByConstructor = new Map(); + + /** + * Map of tag names to user-provided constructors. + * + * @type {Map} + */ + const constructorByTagname = new Map(); + + + /** + * Whether the constructors are being called by a browser process, ie parsing + * or createElement. + */ + let browserConstruction = false; + + /** + * Whether the constructors are being called by a user-space process, ie + * calling an element constructor. + */ + let userConstruction = false; + + window.HTMLElement = function() { + if (!browserConstruction) { + const tagname = tagnameByConstructor.get(this.constructor); + const fakeClass = nativeGet.call(window.customElements, tagname); + + // Make sure that the fake constructor doesn't call back to this constructor + userConstruction = true; + const instance = new (fakeClass)(); + return instance; + } + // Else do nothing. This will be reached by ES5-style classes doing + // HTMLElement.call() during initialization + browserConstruction = false; + }; + // By setting the patched HTMLElement's prototype property to the native + // HTMLElement's prototype we make sure that: + // document.createElement('a') instanceof HTMLElement + // works because instanceof uses HTMLElement.prototype, which is on the + // ptototype chain of built-in elements. + window.HTMLElement.prototype = NativeHTMLElement.prototype; + + const define = (tagname, elementClass) => { + const elementProto = elementClass.prototype; + const StandInElement = class extends NativeHTMLElement { + constructor() { + // Call the native HTMLElement constructor, this gives us the + // under-construction instance as `this`: + super(); + + // The prototype will be wrong up because the browser used our fake + // class, so fix it: + Object.setPrototypeOf(this, elementProto); + + if (!userConstruction) { + // Make sure that user-defined constructor bottom's out to a do-nothing + // HTMLElement() call + browserConstruction = true; + // Call the user-defined constructor on our instance: + elementClass.call(this); + } + userConstruction = false; + } + }; + const standInProto = StandInElement.prototype; + StandInElement.observedAttributes = elementClass.observedAttributes; + standInProto.connectedCallback = elementProto.connectedCallback; + standInProto.disconnectedCallback = elementProto.disconnectedCallback; + standInProto.attributeChangedCallback = elementProto.attributeChangedCallback; + standInProto.adoptedCallback = elementProto.adoptedCallback; + + tagnameByConstructor.set(elementClass, tagname); + constructorByTagname.set(tagname, elementClass); + nativeDefine.call(window.customElements, tagname, StandInElement); + }; + + const get = (tagname) => constructorByTagname.get(tagname); + + // Workaround for Safari bug where patching customElements can be lost, likely + // due to native wrapper garbage collection issue + Object.defineProperty(window, 'customElements', + {value: window.customElements, configurable: true, writable: true}); + Object.defineProperty(window.customElements, 'define', + {value: define, configurable: true, writable: true}); + Object.defineProperty(window.customElements, 'get', + {value: get, configurable: true, writable: true}); + +})();