Bug 1852754: part 9) Add tests for dynamically loading <link rel="prefetch"> elements...
[gecko.git] / toolkit / actors / UAWidgetsChild.sys.mjs
blob6f4244ffe9dda28e01efaa775f313f0e0b5d0d51
1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 export class UAWidgetsChild extends JSWindowActorChild {
7   constructor() {
8     super();
10     this.widgets = new WeakMap();
11     this.prefsCache = new Map();
12     this.observedPrefs = [];
14     // Bug 1570744 - JSWindowActorChild's cannot be used as nsIObserver's
15     // directly, so we create a new function here instead to act as our
16     // nsIObserver, which forwards the notification to the observe method.
17     this.observerFunction = (subject, topic, data) => {
18       this.observe(subject, topic, data);
19     };
20   }
22   didDestroy() {
23     for (let pref in this.observedPrefs) {
24       Services.prefs.removeObserver(pref, this.observerFunction);
25     }
26   }
28   unwrap(obj) {
29     return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
30   }
32   handleEvent(aEvent) {
33     switch (aEvent.type) {
34       case "UAWidgetSetupOrChange":
35         this.setupOrNotifyWidget(aEvent.target);
36         break;
37       case "UAWidgetTeardown":
38         this.teardownWidget(aEvent.target);
39         break;
40     }
41   }
43   setupOrNotifyWidget(aElement) {
44     if (!this.widgets.has(aElement)) {
45       this.setupWidget(aElement);
46       return;
47     }
49     let { widget } = this.widgets.get(aElement);
51     if (typeof widget.onchange == "function") {
52       if (
53         this.unwrap(aElement.openOrClosedShadowRoot) !=
54         this.unwrap(widget.shadowRoot)
55       ) {
56         console.error(
57           "Getting a UAWidgetSetupOrChange event without the ShadowRoot. " +
58             "Torn down already?"
59         );
60         return;
61       }
62       try {
63         widget.onchange();
64       } catch (ex) {
65         console.error(ex);
66       }
67     }
68   }
70   setupWidget(aElement) {
71     let uri;
72     let widgetName;
73     // Use prefKeys to optionally send a list of preferences to forward to
74     // the UAWidget. The UAWidget will receive those preferences as key-value
75     // pairs as the second argument to its constructor. Updates to those prefs
76     // can be observed by implementing an optional onPrefChange method for the
77     // UAWidget that receives the changed pref name as the first argument, and
78     // the updated value as the second.
79     let prefKeys = [];
80     switch (aElement.localName) {
81       case "video":
82       case "audio":
83         uri = "chrome://global/content/elements/videocontrols.js";
84         widgetName = "VideoControlsWidget";
85         prefKeys = [
86           "media.videocontrols.picture-in-picture.enabled",
87           "media.videocontrols.picture-in-picture.video-toggle.enabled",
88           "media.videocontrols.picture-in-picture.video-toggle.always-show",
89           "media.videocontrols.picture-in-picture.video-toggle.min-video-secs",
90           "media.videocontrols.picture-in-picture.video-toggle.position",
91           "media.videocontrols.picture-in-picture.video-toggle.has-used",
92           "media.videocontrols.keyboard-tab-to-all-controls",
93           "media.videocontrols.picture-in-picture.respect-disablePictureInPicture",
94         ];
95         break;
96       case "input":
97         uri = "chrome://global/content/elements/datetimebox.js";
98         widgetName = "DateTimeBoxWidget";
99         break;
100       case "marquee":
101         uri = "chrome://global/content/elements/marquee.js";
102         widgetName = "MarqueeWidget";
103         break;
104       case "img":
105         uri = "chrome://global/content/elements/textrecognition.js";
106         widgetName = "TextRecognitionWidget";
107     }
109     if (!uri || !widgetName) {
110       console.error(
111         "Getting a UAWidgetSetupOrChange event on undefined element."
112       );
113       return;
114     }
116     let shadowRoot = aElement.openOrClosedShadowRoot;
117     if (!shadowRoot) {
118       console.error(
119         "Getting a UAWidgetSetupOrChange event without the Shadow Root. " +
120           "Torn down already?"
121       );
122       return;
123     }
125     let isSystemPrincipal = aElement.nodePrincipal.isSystemPrincipal;
126     let sandbox = isSystemPrincipal
127       ? Object.create(null)
128       : Cu.getUAWidgetScope(aElement.nodePrincipal);
130     if (!sandbox[widgetName]) {
131       Services.scriptloader.loadSubScript(uri, sandbox);
132     }
134     let prefs = Cu.cloneInto(
135       this.getPrefsForUAWidget(widgetName, prefKeys),
136       sandbox
137     );
139     let widget = new sandbox[widgetName](shadowRoot, prefs);
140     if (!isSystemPrincipal) {
141       widget = widget.wrappedJSObject;
142     }
143     if (this.unwrap(widget.shadowRoot) != this.unwrap(shadowRoot)) {
144       console.error("Widgets should expose their shadow root.");
145     }
146     this.widgets.set(aElement, { widget, widgetName });
147     try {
148       widget.onsetup();
149     } catch (ex) {
150       console.error(ex);
151     }
152   }
154   teardownWidget(aElement) {
155     if (!this.widgets.has(aElement)) {
156       return;
157     }
158     let { widget } = this.widgets.get(aElement);
159     if (typeof widget.teardown == "function") {
160       try {
161         widget.teardown();
162       } catch (ex) {
163         console.error(ex);
164       }
165     }
166     this.widgets.delete(aElement);
167   }
169   getPrefsForUAWidget(aWidgetName, aPrefKeys) {
170     let result = this.prefsCache.get(aWidgetName);
171     if (result) {
172       return result;
173     }
175     result = {};
176     for (let key of aPrefKeys) {
177       result[key] = this.getPref(key);
178       this.observePref(key);
179     }
181     this.prefsCache.set(aWidgetName, result);
182     return result;
183   }
185   observePref(prefKey) {
186     Services.prefs.addObserver(prefKey, this.observerFunction);
187     this.observedPrefs.push(prefKey);
188   }
190   getPref(prefKey) {
191     switch (Services.prefs.getPrefType(prefKey)) {
192       case Ci.nsIPrefBranch.PREF_BOOL: {
193         return Services.prefs.getBoolPref(prefKey);
194       }
195       case Ci.nsIPrefBranch.PREF_INT: {
196         return Services.prefs.getIntPref(prefKey);
197       }
198       case Ci.nsIPrefBranch.PREF_STRING: {
199         return Services.prefs.getStringPref(prefKey);
200       }
201     }
203     return undefined;
204   }
206   observe(subject, topic, data) {
207     if (topic == "nsPref:changed") {
208       for (let [widgetName, prefCache] of this.prefsCache) {
209         if (prefCache.hasOwnProperty(data)) {
210           let newValue = this.getPref(data);
211           prefCache[data] = newValue;
213           this.notifyWidgetsOnPrefChange(widgetName, data, newValue);
214         }
215       }
216     }
217   }
219   notifyWidgetsOnPrefChange(nameOfWidgetToNotify, prefKey, newValue) {
220     let elements = ChromeUtils.nondeterministicGetWeakMapKeys(this.widgets);
221     for (let element of elements) {
222       if (!Cu.isDeadWrapper(element) && element.isConnected) {
223         let { widgetName, widget } = this.widgets.get(element);
224         if (widgetName == nameOfWidgetToNotify) {
225           if (typeof widget.onPrefChange == "function") {
226             try {
227               widget.onPrefChange(prefKey, newValue);
228             } catch (ex) {
229               console.error(ex);
230             }
231           }
232         }
233       }
234     }
235   }