-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathnative-shim.js
164 lines (147 loc) · 6.93 KB
/
native-shim.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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<Function, string>}
*/
const tagnameByConstructor = new Map();
/**
* Map of tag names to user-provided constructors.
*
* @type {Map<string, Function>}
*/
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});
})();