Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
// This file defines these globals on the window object.
6
// Define them here so that ESLint can find them:
7
/* globals MozXULElement, MozElements */
8
9
"use strict";
10
11
// This is loaded into chrome windows with the subscript loader. Wrap in
12
// a block to prevent accidentally leaking globals onto `window`.
13
(() => {
14
// Handle customElements.js being loaded as a script in addition to the subscriptLoader
15
// from MainProcessSingleton, to handle pages that can open both before and after
16
// MainProcessSingleton starts. See Bug 1501845.
17
if (window.MozXULElement) {
18
return;
19
}
20
21
const MozElements = {};
22
window.MozElements = MozElements;
23
24
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
25
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
26
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
27
const instrumentClasses = env.get("MOZ_INSTRUMENT_CUSTOM_ELEMENTS");
28
const instrumentedClasses = instrumentClasses ? new Set() : null;
29
const instrumentedBaseClasses = instrumentClasses ? new WeakSet() : null;
30
31
// If requested, wrap the normal customElements.define to give us a chance
32
// to modify the class so we can instrument function calls in local development:
33
if (instrumentClasses) {
34
let define = window.customElements.define;
35
window.customElements.define = function(name, c, opts) {
36
instrumentCustomElementClass(c);
37
return define.call(this, name, c, opts);
38
};
39
window.addEventListener("load", () => {
40
MozElements.printInstrumentation(true);
41
}, { once: true, capture: true });
42
}
43
44
MozElements.printInstrumentation = function(collapsed) {
45
let summaries = [];
46
let totalCalls = 0;
47
let totalTime = 0;
48
for (let c of instrumentedClasses) {
49
// Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter
50
let includeClass = instrumentClasses == 1 ||
51
instrumentClasses.split(",").some(n => c.name.toLowerCase().includes(n.toLowerCase()));
52
let summary = c.__instrumentation_summary;
53
if (includeClass && summary) {
54
summaries.push(summary);
55
totalCalls += summary.totalCalls;
56
totalTime += summary.totalTime;
57
}
58
}
59
if (summaries.length) {
60
let groupName = `Instrumentation data for custom elements in ${document.documentURI}`;
61
console[collapsed ? "groupCollapsed" : "group"](groupName);
62
console.log(`Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed(2)}`);
63
for (let summary of summaries) {
64
console.log(`${summary.name} (# instances: ${summary.instances})`);
65
if (Object.keys(summary.data).length > 1) {
66
console.table(summary.data);
67
}
68
}
69
console.groupEnd(groupName);
70
}
71
};
72
73
function instrumentCustomElementClass(c) {
74
// Climb up prototype chain to see if we inherit from a MozElement.
75
// Keep track of classes to instrument, for example:
76
// MozMenuCaption->MozMenuBase->BaseText->BaseControl->MozXULElement
77
let inheritsFromBase = instrumentedBaseClasses.has(c);
78
let classesToInstrument = [c];
79
let proto = Object.getPrototypeOf(c);
80
while (proto) {
81
classesToInstrument.push(proto);
82
if (instrumentedBaseClasses.has(proto)) {
83
inheritsFromBase = true;
84
break;
85
}
86
proto = Object.getPrototypeOf(proto);
87
}
88
89
if (inheritsFromBase) {
90
for (let c of classesToInstrument.reverse()) {
91
instrumentIndividualClass(c);
92
}
93
}
94
}
95
96
function instrumentIndividualClass(c) {
97
if (instrumentedClasses.has((c))) {
98
return;
99
}
100
101
instrumentedClasses.add((c));
102
let data = { instances: 0 };
103
104
function wrapFunction(name, fn) {
105
return function() {
106
if (!data[name]) {
107
data[name] = {time: 0, calls: 0};
108
}
109
data[name].calls++;
110
let n = performance.now();
111
let r = fn.apply(this, arguments);
112
data[name].time += performance.now() - n;
113
return r;
114
};
115
}
116
function wrapPropertyDescriptor(obj, name) {
117
if (name == "constructor") {
118
return;
119
}
120
let prop = Object.getOwnPropertyDescriptor(obj, name);
121
if (prop.get) {
122
prop.get = wrapFunction(`<get> ${name}`, prop.get);
123
}
124
if (prop.set) {
125
prop.set = wrapFunction(`<set> ${name}`, prop.set);
126
}
127
if (prop.writable && prop.value && prop.value.apply) {
128
prop.value = wrapFunction(name, prop.value);
129
}
130
Object.defineProperty(obj, name, prop);
131
}
132
133
// Handle static properties
134
for (let name of Object.getOwnPropertyNames((c))) {
135
wrapPropertyDescriptor(c, name);
136
}
137
138
// Handle instance properties
139
for (let name of Object.getOwnPropertyNames(c.prototype)) {
140
wrapPropertyDescriptor(c.prototype, name);
141
}
142
143
c.__instrumentation_data = data;
144
Object.defineProperty(c, "__instrumentation_summary", {
145
enumerable: false,
146
configurable: false,
147
get() {
148
if (data.instances == 0) {
149
return null;
150
}
151
152
let clonedData = JSON.parse(JSON.stringify(data));
153
delete clonedData.instances;
154
let totalCalls = 0;
155
let totalTime = 0;
156
for (let d in clonedData) {
157
let {time, calls} = clonedData[d];
158
time = parseFloat(time.toFixed(2));
159
totalCalls += calls;
160
totalTime += time;
161
clonedData[d]["time (ms)"] = time;
162
delete clonedData[d].time;
163
clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4));
164
}
165
166
let timePerCall = parseFloat((totalTime / totalCalls).toFixed(4));
167
totalTime = parseFloat(totalTime.toFixed(2));
168
169
// Add a spaced-out final row with summed up totals
170
clonedData["\ntotals"] = { "time (ms)": `\n${totalTime}`, calls: `\n${totalCalls}`, timePerCall: `\n${timePerCall}` };
171
return {
172
instances: data.instances,
173
data: clonedData,
174
name: c.name,
175
totalCalls,
176
totalTime,
177
};
178
},
179
});
180
}
181
182
// The listener of DOMContentLoaded must be set on window, rather than
183
// document, because the window can go away before the event is fired.
184
// In that case, we don't want to initialize anything, otherwise we
185
// may be leaking things because they will never be destroyed after.
186
let gIsDOMContentLoaded = false;
187
const gElementsPendingConnection = new Set();
188
window.addEventListener("DOMContentLoaded", () => {
189
gIsDOMContentLoaded = true;
190
for (let element of gElementsPendingConnection) {
191
try {
192
if (element.isConnected) {
193
element.isRunningDelayedConnectedCallback = true;
194
element.connectedCallback();
195
}
196
} catch (ex) { console.error(ex); }
197
element.isRunningDelayedConnectedCallback = false;
198
}
199
gElementsPendingConnection.clear();
200
}, { once: true, capture: true });
201
202
const gXULDOMParser = new DOMParser();
203
gXULDOMParser.forceEnableXULXBL();
204
205
MozElements.MozElementMixin = Base => {
206
let MozElementBase = class extends Base {
207
constructor() {
208
super();
209
210
if (instrumentClasses) {
211
let proto = this.constructor;
212
while (proto && proto != Base) {
213
proto.__instrumentation_data.instances++;
214
proto = Object.getPrototypeOf(proto);
215
}
216
}
217
}
218
/*
219
* A declarative way to wire up attribute inheritance and automatically generate
220
* the `observedAttributes` getter. For example, if you returned:
221
* {
222
* ".foo": "bar,baz=bat"
223
* }
224
*
225
* Then the base class will automatically return ["bar", "bat"] from `observedAttributes`,
226
* and set up an `attributeChangedCallback` to pass those attributes down onto an element
227
* matching the ".foo" selector.
228
*
229
* See the `inheritAttribute` function for more details on the attribute string format.
230
*
231
* @return {Object<string selector, string attributes>}
232
*/
233
static get inheritedAttributes() {
234
return null;
235
}
236
237
static get flippedInheritedAttributes() {
238
// Have to be careful here, if a subclass overrides inheritedAttributes
239
// and its parent class is instantiated first, then reading
240
// this._flippedInheritedAttributes on the child class will return the
241
// computed value from the parent. We store it separately on each class
242
// to ensure everything works correctly when inheritedAttributes is
243
// overridden.
244
if (!this.hasOwnProperty("_flippedInheritedAttributes")) {
245
let {inheritedAttributes} = this;
246
if (!inheritedAttributes) {
247
this._flippedInheritedAttributes = null;
248
} else {
249
this._flippedInheritedAttributes = {};
250
for (let selector in inheritedAttributes) {
251
let attrRules = inheritedAttributes[selector].split(",");
252
for (let attrRule of attrRules) {
253
let attrName = attrRule;
254
let attrNewName = attrRule;
255
let split = attrName.split("=");
256
if (split.length == 2) {
257
attrName = split[1];
258
attrNewName = split[0];
259
}
260
261
if (!this._flippedInheritedAttributes[attrName]) {
262
this._flippedInheritedAttributes[attrName] = [];
263
}
264
this._flippedInheritedAttributes[attrName].push([selector, attrNewName]);
265
}
266
}
267
}
268
}
269
270
return this._flippedInheritedAttributes;
271
}
272
/*
273
* Generate this array based on `inheritedAttributes`, if any. A class is free to override
274
* this if it needs to do something more complex or wants to opt out of this behavior.
275
*/
276
static get observedAttributes() {
277
return Object.keys(this.flippedInheritedAttributes || {});
278
}
279
280
/*
281
* Provide default lifecycle callback for attribute changes that will inherit attributes
282
* based on the static `inheritedAttributes` Object. This can be overridden by callers.
283
*/
284
attributeChangedCallback(name, oldValue, newValue) {
285
if (oldValue === newValue || !this.initializedAttributeInheritance) {
286
return;
287
}
288
289
let list = this.constructor.flippedInheritedAttributes[name];
290
if (list) {
291
this.inheritAttribute(list, name);
292
}
293
}
294
295
/*
296
* After setting content, calling this will cache the elements from selectors in the
297
* static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`,
298
* so in the simple case, this is the only function you need to call.
299
*
300
* This should be called any time the children that are inheriting attributes changes. For instance,
301
* it's common in a connectedCallback to do something like:
302
*
303
* this.textContent = "";
304
* this.append(MozXULElement.parseXULToFragment(`<label />`))
305
* this.initializeAttributeInheritance();
306
*
307
*/
308
initializeAttributeInheritance() {
309
let {flippedInheritedAttributes} = this.constructor;
310
if (!flippedInheritedAttributes) {
311
return;
312
}
313
314
// Clear out any existing cached elements:
315
this._inheritedElements = null;
316
317
this.initializedAttributeInheritance = true;
318
for (let attr in flippedInheritedAttributes) {
319
if (this.hasAttribute(attr)) {
320
this.inheritAttribute(flippedInheritedAttributes[attr], attr);
321
}
322
}
323
}
324
325
/*
326
* Implements attribute value inheritance by child elements.
327
*
328
* @param {array} list
329
* An array of (to-element-selector, to-attr) pairs.
330
* @param {string} attr
331
* An attribute to propagate.
332
*/
333
inheritAttribute(list, attr) {
334
if (!this._inheritedElements) {
335
this._inheritedElements = {};
336
}
337
338
let hasAttr = this.hasAttribute(attr);
339
let attrValue = this.getAttribute(attr);
340
341
for (let [selector, newAttr] of list) {
342
if (!(selector in this._inheritedElements)) {
343
let parent = this.shadowRoot || this;
344
this._inheritedElements[selector] = parent.querySelector(selector);
345
}
346
let el = this._inheritedElements[selector];
347
if (el) {
348
if (newAttr == "text") {
349
el.textContent = hasAttr ? attrValue : "";
350
} else if (hasAttr) {
351
el.setAttribute(newAttr, attrValue);
352
} else {
353
el.removeAttribute(newAttr);
354
}
355
}
356
}
357
}
358
359
/**
360
* Sometimes an element may not want to run connectedCallback logic during
361
* parse. This could be because we don't want to initialize the element before
362
* the element's contents have been fully parsed, or for performance reasons.
363
* If you'd like to opt-in to this, then add this to the beginning of your
364
* `connectedCallback` and `disconnectedCallback`:
365
*
366
* if (this.delayConnectedCallback()) { return }
367
*
368
* And this at the beginning of your `attributeChangedCallback`
369
*
370
* if (!this.isConnectedAndReady) { return; }
371
*/
372
delayConnectedCallback() {
373
if (gIsDOMContentLoaded) {
374
return false;
375
}
376
gElementsPendingConnection.add(this);
377
return true;
378
}
379
380
get isConnectedAndReady() {
381
return gIsDOMContentLoaded && this.isConnected;
382
}
383
384
/**
385
* Allows eager deterministic construction of XUL elements with XBL attached, by
386
* parsing an element tree and returning a DOM fragment to be inserted in the
387
* document before any of the inner elements is referenced by JavaScript.
388
*
389
* This process is required instead of calling the createElement method directly
390
* because bindings get attached when:
391
*
392
* 1. the node gets a layout frame constructed, or
393
* 2. the node gets its JavaScript reflector created, if it's in the document,
394
*
395
* whichever happens first. The createElement method would return a JavaScript
396
* reflector, but the element wouldn't be in the document, so the node wouldn't
397
* get XBL attached. After that point, even if the node is inserted into a
398
* document, it won't get XBL attached until either the frame is constructed or
399
* the reflector is garbage collected and the element is touched again.
400
*
401
* @param {string} str
402
* String with the XML representation of XUL elements.
403
* @param {string[]} [entities]
404
* An array of DTD URLs containing entity definitions.
405
*
406
* @return {DocumentFragment} `DocumentFragment` instance containing
407
* the corresponding element tree, including element nodes
408
* but excluding any text node.
409
*/
410
static parseXULToFragment(str, entities = []) {
411
let doc = gXULDOMParser.parseFromString(`
412
${entities.length ? `<!DOCTYPE bindings [
413
${entities.reduce((preamble, url, index) => {
414
return preamble + `<!ENTITY % _dtd-${index} SYSTEM "${url}">
415
%_dtd-${index};
416
`;
417
}, "")}
418
]>` : ""}
420
xmlns:html="http://www.w3.org/1999/xhtml">
421
${str}
422
</box>
423
`, "application/xml");
424
// The XUL/XBL parser is set to ignore all-whitespace nodes, whereas (X)HTML
425
// does not do this. Most XUL code assumes that the whitespace has been
426
// stripped out, so we simply remove all text nodes after using the parser.
427
let nodeIterator = doc.createNodeIterator(doc, NodeFilter.SHOW_TEXT);
428
let currentNode = nodeIterator.nextNode();
429
while (currentNode) {
430
// Remove whitespace-only nodes. Regex is taken from:
432
if (!(/[^\t\n\r ]/.test(currentNode.textContent))) {
433
currentNode.remove();
434
}
435
436
currentNode = nodeIterator.nextNode();
437
}
438
// We use a range here so that we don't access the inner DOM elements from
439
// JavaScript before they are imported and inserted into a document.
440
let range = doc.createRange();
441
range.selectNodeContents(doc.querySelector("box"));
442
return range.extractContents();
443
}
444
445
/**
446
* Insert a localization link to an FTL file. This is used so that
447
* a Custom Element can wait to inject the link until it's connected,
448
* and so that consuming documents don't require the correct <link>
449
* present in the markup.
450
*
451
* @param path
452
* The path to the FTL file
453
*/
454
static insertFTLIfNeeded(path) {
455
let container = document.head || document.querySelector("linkset");
456
if (!container) {
457
if (document.contentType == "application/vnd.mozilla.xul+xml") {
458
container = document.createXULElement("linkset");
459
document.documentElement.appendChild(container);
460
} else if (document.documentURI == AppConstants.BROWSER_CHROME_URL) {
461
// Special case for browser.xhtml. Here `document.head` is null, so
462
// just insert the link at the end of the window.
463
container = document.documentElement;
464
} else {
465
throw new Error("Attempt to inject localization link before document.head is available");
466
}
467
}
468
469
for (let link of container.querySelectorAll("link")) {
470
if (link.getAttribute("href") == path) {
471
return;
472
}
473
}
474
475
let link = document.createElementNS("http://www.w3.org/1999/xhtml",
476
"link");
477
link.setAttribute("rel", "localization");
478
link.setAttribute("href", path);
479
480
container.appendChild(link);
481
}
482
483
/**
484
* Indicate that a class defining a XUL element implements one or more
485
* XPCOM interfaces by adding a getCustomInterface implementation to it,
486
* as well as an implementation of QueryInterface.
487
*
488
* The supplied class should implement the properties and methods of
489
* all of the interfaces that are specified.
490
*
491
* @param cls
492
* The class that implements the interface.
493
* @param names
494
* Array of interface names.
495
*/
496
static implementCustomInterface(cls, ifaces) {
497
if (cls.prototype.customInterfaces) {
498
ifaces.push(...cls.prototype.customInterfaces);
499
}
500
cls.prototype.customInterfaces = ifaces;
501
502
cls.prototype.QueryInterface = ChromeUtils.generateQI(ifaces);
503
cls.prototype.getCustomInterfaceCallback = function getCustomInterfaceCallback(ifaceToCheck) {
504
if (cls.prototype.customInterfaces.some(iface => iface.equals(ifaceToCheck))) {
505
return getInterfaceProxy(this);
506
}
507
return null;
508
};
509
}
510
};
511
512
// Rename the class so we can distinguish between MozXULElement and MozXULPopupElement, for example.
513
Object.defineProperty(MozElementBase, "name", {value: `Moz${Base.name}`});
514
if (instrumentedBaseClasses) {
515
instrumentedBaseClasses.add(MozElementBase);
516
}
517
return MozElementBase;
518
};
519
520
const MozXULElement = MozElements.MozElementMixin(XULElement);
521
522
/**
523
* Given an object, add a proxy that reflects interface implementations
524
* onto the object itself.
525
*/
526
function getInterfaceProxy(obj) {
527
/* globals MozQueryInterface */
528
if (!obj._customInterfaceProxy) {
529
obj._customInterfaceProxy = new Proxy(obj, {
530
get(target, prop, receiver) {
531
let propOrMethod = target[prop];
532
if (typeof propOrMethod == "function") {
533
if (propOrMethod instanceof MozQueryInterface) {
534
return Reflect.get(target, prop, receiver);
535
}
536
return function(...args) {
537
return propOrMethod.apply(target, args);
538
};
539
}
540
return propOrMethod;
541
},
542
});
543
}
544
545
return obj._customInterfaceProxy;
546
}
547
548
MozElements.BaseControlMixin = Base => {
549
class BaseControl extends Base {
550
get disabled() {
551
return this.getAttribute("disabled") == "true";
552
}
553
554
set disabled(val) {
555
if (val) {
556
this.setAttribute("disabled", "true");
557
} else {
558
this.removeAttribute("disabled");
559
}
560
}
561
562
get tabIndex() {
563
return parseInt(this.getAttribute("tabindex")) || 0;
564
}
565
566
set tabIndex(val) {
567
if (val) {
568
this.setAttribute("tabindex", val);
569
} else {
570
this.removeAttribute("tabindex");
571
}
572
}
573
}
574
575
MozXULElement.implementCustomInterface(BaseControl,
576
[Ci.nsIDOMXULControlElement]);
577
return BaseControl;
578
};
579
MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement);
580
581
const BaseTextMixin = Base => class BaseText extends MozElements.BaseControlMixin(Base) {
582
set label(val) {
583
this.setAttribute("label", val);
584
return val;
585
}
586
587
get label() {
588
return this.getAttribute("label");
589
}
590
591
set crop(val) {
592
this.setAttribute("crop", val);
593
return val;
594
}
595
596
get crop() {
597
return this.getAttribute("crop");
598
}
599
600
set image(val) {
601
this.setAttribute("image", val);
602
return val;
603
}
604
605
get image() {
606
return this.getAttribute("image");
607
}
608
609
set command(val) {
610
this.setAttribute("command", val);
611
return val;
612
}
613
614
get command() {
615
return this.getAttribute("command");
616
}
617
618
set accessKey(val) {
619
// Always store on the control
620
this.setAttribute("accesskey", val);
621
// If there is a label, change the accesskey on the labelElement
622
// if it's also set there
623
if (this.labelElement) {
624
this.labelElement.accessKey = val;
625
}
626
return val;
627
}
628
629
get accessKey() {
630
return this.labelElement ? this.labelElement.accessKey : this.getAttribute("accesskey");
631
}
632
};
633
MozElements.BaseTextMixin = BaseTextMixin;
634
MozElements.BaseText = BaseTextMixin(MozXULElement);
635
636
// Attach the base class to the window so other scripts can use it:
637
window.MozXULElement = MozXULElement;
638
639
customElements.setElementCreationCallback("browser", () => {
640
Services.scriptloader.loadSubScript("chrome://global/content/elements/browser-custom-element.js", window);
641
});
642
643
// For now, don't load any elements in the extension dummy document.
644
// We will want to load <browser> when that's migrated (bug 1441935).
645
const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
646
if (!isDummyDocument) {
647
for (let script of [
666
]) {
667
Services.scriptloader.loadSubScript(script, window);
668
}
669
670
for (let [tag, script] of [
675
["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
677
]) {
678
customElements.setElementCreationCallback(tag, () => {
679
Services.scriptloader.loadSubScript(script, window);
680
});
681
}
682
}
683
})();