summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/experimental.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/experimental.js')
-rw-r--r--browser/components/preferences/experimental.js163
1 files changed, 163 insertions, 0 deletions
diff --git a/browser/components/preferences/experimental.js b/browser/components/preferences/experimental.js
new file mode 100644
index 0000000000..266aeabc4f
--- /dev/null
+++ b/browser/components/preferences/experimental.js
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from preferences.js */
+
+var gExperimentalPane = {
+ inited: false,
+ _template: null,
+ _featureGatesContainer: null,
+ _boundRestartObserver: null,
+ _observedPrefs: [],
+ _shouldPromptForRestart: true,
+
+ _featureGatePrefTypeToPrefServiceType(featureGatePrefType) {
+ if (featureGatePrefType != "boolean") {
+ throw new Error("Only boolean FeatureGates are supported");
+ }
+ return "bool";
+ },
+
+ async _observeRestart(aSubject, aTopic, aData) {
+ if (!this._shouldPromptForRestart) {
+ return;
+ }
+ let prefValue = Services.prefs.getBoolPref(aData);
+ let buttonIndex = await confirmRestartPrompt(prefValue, 1, true, false);
+ if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ return;
+ }
+ this._shouldPromptForRestart = false;
+ Services.prefs.setBoolPref(aData, !prefValue);
+ this._shouldPromptForRestart = true;
+ },
+
+ addPrefObserver(name, fn) {
+ this._observedPrefs.push({ name, fn });
+ Services.prefs.addObserver(name, fn);
+ },
+
+ removePrefObservers() {
+ for (let { name, fn } of this._observedPrefs) {
+ Services.prefs.removeObserver(name, fn);
+ }
+ this._observedPrefs = [];
+ },
+
+ // Reset the features to their default values
+ async resetAllFeatures() {
+ let features = await gExperimentalPane.getFeatures();
+ for (let feature of features) {
+ Services.prefs.setBoolPref(feature.preference, feature.defaultValue);
+ }
+ },
+
+ async getFeatures() {
+ let searchParams = new URLSearchParams(document.documentURIObject.query);
+ let definitionsUrl = searchParams.get("definitionsUrl");
+ let features = await FeatureGate.all(definitionsUrl);
+ return features.filter(f => f.isPublic);
+ },
+
+ async _sortFeatures(features) {
+ // Sort the features alphabetically by their title
+ let titles = await document.l10n.formatMessages(
+ features.map(f => {
+ return { id: f.title };
+ })
+ );
+ titles = titles.map((title, index) => [title.attributes[0].value, index]);
+ titles.sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()));
+ // Get the features in order of sorted titles.
+ return titles.map(([, index]) => features[index]);
+ },
+
+ async init() {
+ if (this.inited) {
+ return;
+ }
+ this.inited = true;
+
+ let features = await this.getFeatures();
+ let shouldHide = !features.length;
+ document.getElementById("category-experimental").hidden = shouldHide;
+ // Cache the visibility so we can show it quicker in subsequent loads.
+ Services.prefs.setBoolPref(
+ "browser.preferences.experimental.hidden",
+ shouldHide
+ );
+ if (shouldHide) {
+ // Remove the 'experimental' category if there are no available features
+ document.getElementById("firefoxExperimentalCategory").remove();
+ if (
+ document.getElementById("categories").selectedItem?.id ==
+ "category-experimental"
+ ) {
+ // Leave the 'experimental' category if there are no available features
+ gotoPref("general");
+ return;
+ }
+ }
+
+ features = await this._sortFeatures(features);
+
+ setEventListener(
+ "experimentalCategory-reset",
+ "command",
+ gExperimentalPane.resetAllFeatures
+ );
+
+ window.addEventListener("unload", () => this.removePrefObservers());
+ this._template = document.getElementById("template-featureGate");
+ this._featureGatesContainer = document.getElementById(
+ "pane-experimental-featureGates"
+ );
+ this._boundRestartObserver = this._observeRestart.bind(this);
+ let frag = document.createDocumentFragment();
+ for (let feature of features) {
+ if (Preferences.get(feature.preference)) {
+ console.error(
+ "Preference control already exists for experimental feature '" +
+ feature.id +
+ "' with preference '" +
+ feature.preference +
+ "'"
+ );
+ continue;
+ }
+ if (feature.restartRequired) {
+ this.addPrefObserver(feature.preference, this._boundRestartObserver);
+ }
+ let template = this._template.content.cloneNode(true);
+ let description = template.querySelector(".featureGateDescription");
+ description.id = feature.id + "-description";
+ let descriptionLinks = feature.descriptionLinks || {};
+ for (let [key, value] of Object.entries(descriptionLinks)) {
+ let link = document.createElement("a");
+ link.setAttribute("data-l10n-name", key);
+ link.setAttribute("href", value);
+ link.setAttribute("target", "_blank");
+ description.append(link);
+ }
+ document.l10n.setAttributes(description, feature.description);
+ let checkbox = template.querySelector(".featureGateCheckbox");
+ checkbox.setAttribute("preference", feature.preference);
+ checkbox.id = feature.id;
+ checkbox.setAttribute("aria-describedby", description.id);
+ document.l10n.setAttributes(checkbox, feature.title);
+ frag.appendChild(template);
+ let preference = Preferences.add({
+ id: feature.preference,
+ type: gExperimentalPane._featureGatePrefTypeToPrefServiceType(
+ feature.type
+ ),
+ });
+ preference.setElementValue(checkbox);
+ }
+ this._featureGatesContainer.appendChild(frag);
+ },
+};