-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
custom-elements.js
932 lines (837 loc) · 26.4 KB
/
custom-elements.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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
import {Deferred} from '#core/data-structures/promise';
import {rethrowAsync} from '#core/error';
import * as mode from '#core/mode';
import {map} from '#core/types/object';
/**
* For type anotations where Element is a local variable.
* @typedef {!Element}
*/
let ElementOrigDef;
/** @typedef {!typeof HTMLElement} */
let CustomElementConstructorDef;
/**
* @typedef {{
* name: string,
* ctor: !CustomElementConstructorDef,
* }}
*/
let CustomElementDef;
/**
* Validates the custom element's name.
* This intentionally ignores "valid" higher Unicode Code Points.
* https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
*/
const VALID_NAME = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
const INVALID_NAMES = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph',
];
/**
* A MutationObserverInit dictionary to track subtree modifications.
*/
const TRACK_SUBTREE = {
'childList': true,
'subtree': true,
};
/**
* Asserts that the custom element name conforms to the spec.
*
* @param {!typeof SyntaxError} SyntaxError
* @param {string} name
*/
function assertValidName(SyntaxError, name) {
if (!VALID_NAME.test(name) || INVALID_NAMES.includes(name)) {
throw new SyntaxError(`invalid custom element name "${name}"`);
}
}
/**
* Does win have a full Custom Elements registry?
*
* @param {!Window} win
* @return {boolean}
*/
function hasCustomElements(win) {
const {customElements} = win;
return !!(
customElements &&
customElements.define &&
customElements.get &&
customElements.whenDefined
);
}
/**
* Was HTMLElement already patched for this window?
*
* @param {!Window} win
* @return {boolean}
*/
function isPatched(win) {
const tag = win.HTMLElement.toString();
return tag.indexOf('[native code]') === -1;
}
/**
* The public Custom Elements API.
*/
class CustomElementRegistry {
/**
* @param {!Window} win
* @param {!Registry} registry
*/
constructor(win, registry) {
/** @const @private */
this.win_ = win;
/** @const @private */
this.registry_ = registry;
/** @private @const @type {!Object<string, !Deferred>} */
this.pendingDefines_ = map();
}
/**
* Register the custom element.
*
* @param {string} name
* @param {!CustomElementConstructorDef} ctor
* @param {!Object=} options
*/
define(name, ctor, options) {
this.registry_.define(name, ctor, options);
// If anyone is waiting for this custom element to be defined, resolve
// their promise.
const pending = this.pendingDefines_;
const deferred = pending[name];
if (deferred) {
deferred.resolve();
delete pending[name];
}
}
/**
* Get the constructor of the (already defined) custom element.
*
* @param {string} name
* @return {!CustomElementConstructorDef|undefined}
*/
get(name) {
const def = this.registry_.getByName(name);
if (def) {
return def.ctor;
}
}
/**
* Returns a promise that waits until the custom element is defined.
* If the custom element is already defined, returns a resolved promise.
*
* @param {string} name
* @return {!Promise<undefined>}
*/
whenDefined(name) {
const {Promise, SyntaxError} = this.win_;
assertValidName(SyntaxError, name);
if (this.registry_.getByName(name)) {
return Promise.resolve();
}
const pending = this.pendingDefines_;
let deferred = pending[name];
if (!deferred) {
deferred = new Deferred();
pending[name] = deferred;
}
return deferred.promise;
}
/**
* Upgrade all custom elements inside root.
*
* @param {!Node} root
*/
upgrade(root) {
this.registry_.upgrade(root);
}
}
/**
* This internal APIs necessary to run the CustomElementRegistry.
* Since Registry is never exposed externally, all methods are actually
* available on the instance.
*/
class Registry {
/**
* @param {!Window} win
*/
constructor(win) {
/** @private @const */
this.win_ = win;
/** @private @const @type {!Object<string, !CustomElementDef>} */
this.definitions_ = map();
/**
* A up-to-date DOM selector for all custom elements.
* @type {string}
*/
this.query_ = '';
/**
* The currently upgrading element.
* @private {?Element}
*/
this.current_ = null;
/**
* Once started (after the first Custom Element definition), this tracks
* DOM append and removals.
*
* @private {?MutationObserver}
*/
this.mutationObserver_ = null;
/**
* All the observed DOM trees, including shadow trees.
*
* @private @const {!Array<!Node>}
*/
this.roots_ = [win.document];
}
/**
* The currently-being-upgraded custom element.
*
* When an already created (through the DOM parsing APIs, or innerHTML)
* custom element node is being upgraded, we can't just create a new node
* (it's illegal in the spec). But we still need to run the custom element's
* constructor code on the node. We avoid this conundrum by running the
* constructor while returning this current node in the HTMLElement
* class constructor (the base class of all custom elements).
*
* @return {?Element}
*/
current() {
const current = this.current_;
this.current_ = null;
return current;
}
/**
* Finds the custom element definition by name.
*
* @param {string} name
* @return {!CustomElementDef|undefined}
*/
getByName(name) {
const definition = this.definitions_[name];
if (definition) {
return definition;
}
}
/**
* Finds the custom element definition by constructor instance.
*
* @param {!CustomElementConstructorDef} ctor
* @return {!CustomElementDef|undefined}
*/
getByConstructor(ctor) {
const definitions = this.definitions_;
for (const name in definitions) {
const def = definitions[name];
if (def.ctor === ctor) {
return def;
}
}
}
/**
* Registers the custom element definition, and upgrades all elements by that
* name in the root document.
*
* @param {string} name
* @param {!CustomElementConstructorDef} ctor
* @param {!Object|undefined} options
*/
define(name, ctor, options) {
const {Error, SyntaxError} = this.win_;
if (options) {
throw new Error('Extending native custom elements is not supported');
}
assertValidName(SyntaxError, name);
if (this.getByName(name) || this.getByConstructor(ctor)) {
throw new Error(`duplicate definition "${name}"`);
}
// TODO(jridgewell): Record connectedCallback, disconnectedCallback,
// adoptedCallback, attributeChangedCallback, and observedAttributes.
// TODO(jridgewell): If attributeChangedCallback, gather observedAttributes
this.definitions_[name] = {
name,
ctor,
};
this.observe_(name);
for (const tree of this.roots_) {
this.upgrade(tree, name);
}
}
/**
* Upgrades custom elements descendants of root (but not including root).
*
* When called with an opt_query, it both upgrades and connects the custom
* elements (this is used during the custom element define algorithm).
*
* @param {!Node} root
* @param {string=} opt_query
*/
upgrade(root, opt_query) {
// Only CustomElementRegistry.p.define provides a query (the newly defined
// custom element). In this case, we are both upgrading _and_ connecting
// the custom elements.
const newlyDefined = !!opt_query;
const query = opt_query || this.query_;
const upgradeCandidates = this.queryAll_(root, query);
for (const candidate of upgradeCandidates) {
if (newlyDefined) {
this.connectedCallback_(candidate);
} else {
this.upgradeSelf(candidate);
}
}
}
/**
* Upgrades the custom element node, if a custom element has been registered
* by this name.
*
* @param {!Node} node
*/
upgradeSelf(node) {
const def = this.getByName(node.localName);
if (!def) {
return;
}
this.upgradeSelf_(/** @type {!Element} */ (node), def);
}
/**
* @param {!Node} root
* @param {string} query
* @return {!Array|!NodeList}
*/
queryAll_(root, query) {
if (!query || !root.querySelectorAll) {
// Nothing to do...
return [];
}
return root.querySelectorAll(query);
}
/**
* Upgrades the (already created via DOM parsing) custom element.
*
* @param {!Element} node
* @param {!CustomElementDef} def
*/
upgradeSelf_(node, def) {
const {ctor} = def;
if (node instanceof ctor) {
return;
}
// Despite how it looks, this is not a useless construction.
// HTMLElementPolyfill (the base class of all custom elements) will return
// the current node, allowing the custom element's subclass constructor to
// run on the node. The node itself is already constructed, so the return
// value is just the node.
this.current_ = node;
try {
const el = new ctor();
if (el !== node) {
throw new this.win_.Error(
'Constructor illegally returned a different instance.'
);
}
} catch (e) {
rethrowAsync(e);
}
}
/**
* Fires connectedCallback on the custom element, if it has one.
* This also upgrades the custom element, since it may not have been
* accessible via the root document before (a detached DOM tree).
*
* @param {!Node} node
*/
connectedCallback_(node) {
const def = this.getByName(node.localName);
if (!def) {
return;
}
node = /** @type {!HTMLElement} */ (node);
this.upgradeSelf_(node, def);
// TODO(jridgewell): It may be appropriate to adoptCallback, if the node
// used to be in another doc.
// TODO(jridgewell): I should be calling the definitions connectedCallback
// with node as the context.
if (node.connectedCallback) {
try {
node.connectedCallback();
} catch (e) {
rethrowAsync(e);
}
}
}
/**
* Fires disconnectedCallback on the custom element, if it has one.
*
* @param {!Node} node
*/
disconnectedCallback_(node) {
// TODO(jridgewell): I should be calling the definitions connectedCallback
// with node as the context.
node = /** @type {!HTMLElement} */ (node);
if (node.disconnectedCallback) {
try {
node.disconnectedCallback();
} catch (e) {
rethrowAsync(e);
}
}
}
/**
* Records name as a registered custom element to observe.
*
* Starts the Mutation Observer if this is the first registered custom
* element. This is deferred until the first custom element is defined to
* speed up initial rendering of the page.
*
* Mutation Observers are conveniently available in every browser we care
* about. When a node is connected to the root document, all custom
* elements (including that node iteself) will be upgraded and call
* connectedCallback. When a node is disconnectedCallback from the root
* document, all custom elements will call disconnectedCallback.
*
* @param {string} name
*/
observe_(name) {
if (this.query_) {
this.query_ += `,${name}`;
return;
}
this.query_ = name;
// The first registered name starts the mutation observer.
const mo = new this.win_.MutationObserver((records) => {
if (records) {
this.handleRecords_(records);
}
});
this.mutationObserver_ = mo;
// I would love to not have to hold onto all of the roots, since it's a
// memory leak. Unfortunately, there's no way to iterate a list and hold
// onto its contents weakly.
for (const tree of this.roots_) {
mo.observe(tree, TRACK_SUBTREE);
}
installPatches(this.win_, this);
}
/**
* Adds the shadow tree to be observed by the polyfill.
*
* @param {!Node} tree
*/
observe(tree) {
this.roots_.push(tree);
if (this.mutationObserver_) {
this.mutationObserver_.observe(tree, TRACK_SUBTREE);
}
}
/**
* This causes a synchronous handling of all the Mutation Observer's tracked
* mutations. This does nothing until the mutation observer is actually
* registered on the first Custom Element definition.
*/
sync() {
if (this.mutationObserver_) {
this.handleRecords_(this.mutationObserver_.takeRecords());
}
}
/**
* Handle all the Mutation Observer's Mutation Records.
* All added custom elements will be upgraded (if not already) and call
* connectedCallback. All removed custom elements will call
* disconnectedCallback.
*
* @param {!Array<!MutationRecord>} records
*/
handleRecords_(records) {
for (const record of records) {
if (!record) {
continue;
}
const {addedNodes, removedNodes} = record;
for (const node of addedNodes) {
const connectedCandidates = this.queryAll_(node, this.query_);
this.connectedCallback_(node);
for (const candidate of connectedCandidates) {
this.connectedCallback_(candidate);
}
}
for (const node of removedNodes) {
const disconnectedCandidates = this.queryAll_(node, this.query_);
this.disconnectedCallback_(node);
for (const candidate of disconnectedCandidates) {
this.disconnectedCallback_(candidate);
}
}
}
}
}
/**
* Patches the DOM APIs to support synchronous Custom Elements.
* @param {!Window} win
* @param {!Registry} registry
*/
function installPatches(win, registry) {
const {Document, Element, Node, document} = win;
const docProto = Document.prototype;
const elProto = Element.prototype;
const nodeProto = Node.prototype;
const {createElement, importNode} = docProto;
const {appendChild, cloneNode, insertBefore, removeChild, replaceChild} =
nodeProto;
// Patch createElement to immediately upgrade the custom element.
// This has the added benefit that it avoids the "already created but needs
// constructor code run" chicken-and-egg problem.
docProto.createElement = function (name) {
const def = registry.getByName(name);
if (def) {
return new def.ctor();
}
return createElement.apply(this, arguments);
};
// Patch importNode to immediately upgrade custom elements.
// TODO(jridgewell): Can fire adoptedCallback for cross doc imports.
docProto.importNode = function () {
const imported = importNode.apply(this, arguments);
// Only upgrade elements if the document that the nodes were imported into
// is _this_ document. If it's another document, then that document's
// element registry must do the upgrade.
// Eg, when importing from a <template>, the cloned document fragment
// should be upgraded. But importing from document into the <template>
// should not.
if (imported && this === document) {
registry.upgradeSelf(imported);
registry.upgrade(imported);
}
return imported;
};
// Patch appendChild to upgrade custom elements before returning.
nodeProto.appendChild = function () {
const appended = appendChild.apply(this, arguments);
registry.sync();
return appended;
};
// Patch insertBefore to upgrade custom elements before returning.
nodeProto.insertBefore = function () {
const inserted = insertBefore.apply(this, arguments);
registry.sync();
return inserted;
};
// Patch removeChild to upgrade custom elements before returning.
nodeProto.removeChild = function () {
const removed = removeChild.apply(this, arguments);
registry.sync();
return removed;
};
// Patch replaceChild to upgrade and detach custom elements before returning.
nodeProto.replaceChild = function () {
const replaced = replaceChild.apply(this, arguments);
registry.sync();
return replaced;
};
// Patch cloneNode to immediately upgrade custom elements.
nodeProto.cloneNode = function () {
const cloned = cloneNode.apply(this, arguments);
// Only upgrade elements if the cloned node belonged to _this_ document.
// Eg, when cloning a <template>'s content, the cloned document fragment
// does not belong to this document.
if (cloned.ownerDocument === document) {
registry.upgradeSelf(cloned);
registry.upgrade(cloned);
}
return cloned;
};
// Patch the innerHTML setter to immediately upgrade custom elements.
// Note, this could technically fire connectedCallbacks if this node was
// connected, but we leave that to the Mutation Observer.
let innerHTMLProto = elProto;
let innerHTMLDesc = Object.getOwnPropertyDescriptor(
innerHTMLProto,
'innerHTML'
);
if (!innerHTMLDesc) {
// Sigh... IE11 puts innerHTML desciptor on HTMLElement. But, we've
// replaced HTMLElement with a polyfill wrapper, so have to get its proto.
innerHTMLProto = Object.getPrototypeOf(win.HTMLElement.prototype);
innerHTMLDesc = Object.getOwnPropertyDescriptor(
innerHTMLProto,
'innerHTML'
);
}
if (innerHTMLDesc?.configurable) {
const innerHTMLSetter = innerHTMLDesc.set;
innerHTMLDesc.set = function (html) {
innerHTMLSetter.call(this, html);
registry.upgrade(this);
};
Object.defineProperty(
/** @type {!Object} */ (innerHTMLProto),
'innerHTML',
innerHTMLDesc
);
}
}
/**
* Does the polyfilling.
* @param {!Window} win
*/
function polyfill(win) {
const {Element, HTMLElement, document} = win;
const {createElement} = document;
const registry = new Registry(win);
const customElements = new CustomElementRegistry(win, registry);
// Expose the custom element registry.
// Object.getOwnPropertyDescriptor(window, 'customElements')
// {get: ƒ, set: undefined, enumerable: true, configurable: true}
Object.defineProperty(win, 'customElements', {
enumerable: true,
configurable: true,
// writable: false,
value: customElements,
});
// Have to patch shadow methods now, since there's no way to find shadow trees
// later.
const elProto = Element.prototype;
const {attachShadow, createShadowRoot} = elProto;
if (attachShadow) {
/**
* @param {{mode: string}} unused
* @return {!ShadowRoot}
*/
elProto.attachShadow = function (unused) {
const shadow = attachShadow.apply(this, arguments);
registry.observe(shadow);
return shadow;
};
// Necessary for Shadow AMP
elProto.attachShadow.toString = function () {
return attachShadow.toString();
};
}
if (createShadowRoot) {
/** @return {!ShadowRoot} */
elProto.createShadowRoot = function () {
const shadow = createShadowRoot.apply(this, arguments);
registry.observe(shadow);
return shadow;
};
// Necessary for Shadow AMP
elProto.createShadowRoot.toString = function () {
return createShadowRoot.toString();
};
}
/**
* You can't use the real HTMLElement constructor, because you can't subclass
* it without using native classes. So, mock its approximation using
* createElement.
* @return {!ElementOrigDef}
*/
function HTMLElementPolyfill() {
const {constructor} = this;
// If we're upgrading an already created custom element, we can't create
// another new node (by the spec, it must be the same node).
let el = registry.current();
// If there's not a already created custom element, we're being invoked via
// `new`ing the constructor.
//
// Technically, we could get here via createElement, but we patched that.
// If it the custom element was registered, the patch turned it into a
// `new` call.
// If it was not registered, the native createElement is used. And if
// native createElement is being used and we got to this code, we're really
// in an infinite loop (a native createElement call just below) so we've
// got bigger problems.
//
// So just take my word we got here via `new`.
if (!el) {
// The custom element definition is an invariant. If the custom element
// is registered, everything works. If it's not, it throws in the member
// property access (only defined custom elements can be directly
// constructed via `new`).
const def = registry.getByConstructor(constructor);
el = createElement.call(document, def.name);
}
// Finally, if the node was already constructed, we need to reset its
// prototype to the custom element prototype. And if it wasn't already
// constructed, we created a new node via native createElement, and we need
// to reset its prototype. Basically always reset the prototype.
setPrototypeOf(el, constructor.prototype);
return el;
}
subClass(HTMLElement, HTMLElementPolyfill);
// Expose the polyfilled HTMLElement constructor for everyone to extend from.
win.HTMLElementOrig = win.HTMLElement;
win.HTMLElement = HTMLElementPolyfill;
// When we transpile `super` in Custom Element subclasses, we change it to
// `superClass.call(this)` (where `superClass` is `HTMLElementPolyfill`).
// That `.call` value is inherited from `Function.prototype`.
// But, IE11's native HTMLElement hierarchy doesn't extend from Function!
// And because `HTMLElementPolyfill` extends from `HTMLElement`, it doesn't
// have a `.call`! So we need to manually install it.
if (!HTMLElementPolyfill.call) {
HTMLElementPolyfill.apply = win.Function.apply;
HTMLElementPolyfill.bind = win.Function.bind;
HTMLElementPolyfill.call = win.Function.call;
}
}
/**
* Wraps HTMLElement in a Reflect.construct constructor, so that transpiled
* classes can `_this = superClass.call(this)` during their construction.
*
* This is only used when Custom Elements v1 is already available _and_ we're
* using transpiled classes (which use ES5 construction idioms).
*
* @param {!Window} win
* @suppress {globalThis}
*/
function wrapHTMLElement(win) {
const {HTMLElement, Reflect} = win;
/** @return {!Element} */
function HTMLElementWrapper() {
const ctor = /** @type {function(...?):?|undefined} */ (this.constructor);
// Reflect.construct allows us to construct a new HTMLElement without using
// `new` (which will always fail because native HTMLElement is a restricted
// constructor).
return Reflect.construct(HTMLElement, [], ctor);
}
subClass(HTMLElement, HTMLElementWrapper);
// Expose the wrapped HTMLElement constructor for everyone to extend from.
win.HTMLElementOrig = win.HTMLElement;
win.HTMLElement = HTMLElementWrapper;
}
/**
* Setups up prototype inheritance
*
* @param {!SUPER} superClass
* @param {!SUB} subClass
* @template SUPER
* @template SUB
*/
function subClass(superClass, subClass) {
// Object.getOwnPropertyDescriptor(superClass.prototype, 'constructor')
// {value: ƒ, writable: true, enumerable: false, configurable: true}
subClass.prototype = Object.create(superClass.prototype, {
constructor: {
// enumerable: false,
configurable: true,
writable: true,
value: subClass,
},
});
setPrototypeOf(subClass, superClass);
}
/**
* Tests whether setting '__proto__' will change the prototype chain of an
* object. Only needed for old IE.
* @return {boolean}
*/
function supportsUnderProto() {
const proto = {'test': true};
const obj = {};
obj.__proto__ = proto;
return !!obj['test'];
}
/**
* Sets the prototype chain of an object, with various fallbacks to support
* old IE.
* @param {!Object} obj
* @param {!Object} prototype
*/
function setPrototypeOf(obj, prototype) {
if (mode.isEsm() || Object.setPrototypeOf) {
// Every decent browser.
Object.setPrototypeOf(obj, prototype);
} else if (supportsUnderProto()) {
// IE11
obj.__proto__ = prototype;
} else {
// IE10 man. :sigh:
copyProperties(obj, prototype);
}
}
/**
* Copies the property descriptors from prototype to obj. This is only
* necessary for old IE, which can't properly set the prototype of an already
* created object.
* @param {!Object} obj
* @param {!Object} prototype
* @visibleForTesting
*/
export function copyProperties(obj, prototype) {
let current = prototype;
while (current !== null) {
if (Object.isPrototypeOf.call(current, obj)) {
break;
}
for (const prop of Object.getOwnPropertyNames(current)) {
if (Object.hasOwnProperty.call(obj, prop)) {
continue;
}
const desc = /** @type {!ObjectPropertyDescriptor<Object>} */ (
Object.getOwnPropertyDescriptor(current, prop)
);
Object.defineProperty(obj, prop, desc);
}
current = Object.getPrototypeOf(current);
}
}
/**
* Polyfills Custom Elements v1 API. This has 5 modes:
*
* 1. Custom elements v1 already supported, using native classes
* 2. Custom elements v1 already supported, using transpiled classes
* 3. Custom elements v1 not supported, using native classes
* 4. Custom elements v1 not supported, using transpiled classes
* 5. No sample class constructor provided
*
* In mode 1, nothing is done. In mode 2, a minimal polyfill is used to support
* extending the HTMLElement base class. In mode 3, 4, and 5 a full polyfill is
* done.
*
* @param {!Window} win
* @param {!Function=} ctor
*/
export function install(win, ctor) {
// Don't install in no-DOM environments e.g. worker.
const shouldInstall = win.document;
const hasCE = hasCustomElements(win);
if (!shouldInstall || (hasCE && isPatched(win))) {
return;
}
let install = true;
let installWrapper = false;
if (ctor && hasCE) {
// If ctor is constructable without new, it's a function. That means it was
// compiled down, and we need to do the minimal polyfill because all you
// cannot extend HTMLElement without native classes.
try {
const {Reflect} = win;
// "Construct" ctor using ES5 idioms
// I'm not sure why, but Closure will complain at the
// `Function.call.call()` below unless we cast to a Function instance
// here.
const instance = /** @type {!Function} */ (Object.create(ctor.prototype));
// This will throw an error unless we're in a transpiled environemnt.
// Native classes must be called as `new Ctor`, not `Ctor.call(instance)`.
// We use `Function.call.call` because Closure is too smart for regular
// `Ctor.call`.
Function.call.call(ctor, instance);
// If that didn't throw, we're transpiled.
// Let's find out if we can wrap HTMLElement and avoid a full patch.
installWrapper = !!Reflect?.construct;
} catch (e) {
// The ctor threw when we constructed it via ES5, so it's a real class.
// We're ok to not install the polyfill.
install = false;
}
}
if (installWrapper) {
wrapHTMLElement(win);
} else if (install) {
polyfill(win);
}
}