Mypal/devtools/client/shared/options-view.js

187 lines
6.0 KiB
JavaScript

"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const Services = require("Services");
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
const OPTIONS_SHOWN_EVENT = "options-shown";
const OPTIONS_HIDDEN_EVENT = "options-hidden";
const PREF_CHANGE_EVENT = "pref-changed";
/**
* OptionsView constructor. Takes several options, all required:
* - branchName: The name of the prefs branch, like "devtools.debugger."
* - menupopup: The XUL `menupopup` item that contains the pref buttons.
*
* Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as
* the second argument. Fires events on opening/closing the XUL panel
* (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT) as the second argument in the
* listener, used for tests mostly.
*/
const OptionsView = function (options = {}) {
this.branchName = options.branchName;
this.menupopup = options.menupopup;
this.window = this.menupopup.ownerDocument.defaultView;
let { document } = this.window;
this.$ = document.querySelector.bind(document);
this.$$ = (selector, parent = document) => parent.querySelectorAll(selector);
// Get the corresponding button that opens the popup by looking
// for an element with a `popup` attribute matching the menu's ID
this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`);
this.prefObserver = new PrefObserver(this.branchName);
EventEmitter.decorate(this);
};
exports.OptionsView = OptionsView;
OptionsView.prototype = {
/**
* Binds the events and observers for the OptionsView.
*/
initialize: function () {
let { MutationObserver } = this.window;
this._onPrefChange = this._onPrefChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._onPopupShown = this._onPopupShown.bind(this);
this._onPopupHidden = this._onPopupHidden.bind(this);
// We use a mutation observer instead of a click handler
// because the click handler is fired before the XUL menuitem updates its
// checked status, which cascades incorrectly with the Preference observer.
this.mutationObserver = new MutationObserver(this._onOptionChange);
let observerConfig = { attributes: true, attributeFilter: ["checked"]};
// Sets observers and default options for all options
for (let $el of this.$$("menuitem", this.menupopup)) {
let prefName = $el.getAttribute("data-pref");
if (this.prefObserver.get(prefName)) {
$el.setAttribute("checked", "true");
} else {
$el.removeAttribute("checked");
}
this.mutationObserver.observe($el, observerConfig);
}
// Listen to any preference change in the specified branch
this.prefObserver.register();
this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange);
// Bind to menupopup's open and close event
this.menupopup.addEventListener("popupshown", this._onPopupShown);
this.menupopup.addEventListener("popuphidden", this._onPopupHidden);
},
/**
* Removes event handlers for all of the option buttons and
* preference observer.
*/
destroy: function () {
this.mutationObserver.disconnect();
this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
this.menupopup.removeEventListener("popupshown", this._onPopupShown);
this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
},
/**
* Returns the value for the specified `prefName`
*/
getPref: function (prefName) {
return this.prefObserver.get(prefName);
},
/**
* Called when a preference is changed (either via clicking an option
* button or by changing it in about:config). Updates the checked status
* of the corresponding button.
*/
_onPrefChange: function (_, prefName) {
let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
let value = this.prefObserver.get(prefName);
// If options panel does not contain a menuitem for the
// pref, emit an event and do nothing.
if (!$el) {
this.emit(PREF_CHANGE_EVENT, prefName);
return;
}
if (value) {
$el.setAttribute("checked", value);
} else {
$el.removeAttribute("checked");
}
this.emit(PREF_CHANGE_EVENT, prefName);
},
/**
* Mutation handler for handling a change on an options button.
* Sets the preference accordingly.
*/
_onOptionChange: function (mutations) {
let { target } = mutations[0];
let prefName = target.getAttribute("data-pref");
let value = target.getAttribute("checked") === "true";
this.prefObserver.set(prefName, value);
},
/**
* Fired when the `menupopup` is opened, bound via XUL.
* Fires an event used in tests.
*/
_onPopupShown: function () {
this.button.setAttribute("open", true);
this.emit(OPTIONS_SHOWN_EVENT);
},
/**
* Fired when the `menupopup` is closed, bound via XUL.
* Fires an event used in tests.
*/
_onPopupHidden: function () {
this.button.removeAttribute("open");
this.emit(OPTIONS_HIDDEN_EVENT);
}
};
/**
* Constructor for PrefObserver. Small helper for observing changes
* on a preference branch. Takes a `branchName`, like "devtools.debugger."
*
* Fires an event of PREF_CHANGE_EVENT with the preference name that changed
* as the second argument in the listener.
*/
const PrefObserver = function (branchName) {
this.branchName = branchName;
this.branch = Services.prefs.getBranch(branchName);
EventEmitter.decorate(this);
};
PrefObserver.prototype = {
/**
* Returns `prefName`'s value. Does not require the branch name.
*/
get: function (prefName) {
let fullName = this.branchName + prefName;
return Preferences.get(fullName);
},
/**
* Sets `prefName`'s `value`. Does not require the branch name.
*/
set: function (prefName, value) {
let fullName = this.branchName + prefName;
Preferences.set(fullName, value);
},
register: function () {
this.branch.addObserver("", this, false);
},
unregister: function () {
this.branch.removeObserver("", this);
},
observe: function (subject, topic, prefName) {
this.emit(PREF_CHANGE_EVENT, prefName);
}
};