/* 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/. */
"use strict";
var Cm = Components.manager;
const VKB_ENTER_KEY = 13;
// User press of VKB enter key
const INITIAL_PAGE_DELAY = 500;
// Initial pause on program start for scroll alignment
const PREFS_BUFFER_MAX = 30;
// Max prefs buffer size for getPrefsBuffer()
const PAGE_SCROLL_TRIGGER = 200;
// Triggers additional getPrefsBuffer() on user scroll-to-bottom
const FILTER_CHANGE_TRIGGER = 200;
// Delay between responses to filterInput changes
const INNERHTML_VALUE_DELAY = 100;
// Delay before providing prefs innerHTML value
var gClipboardHelper = Cc[
"@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
/* ============================== NewPrefDialog ==============================
*
* New Preference Dialog Object and methods
*
* Implements User Interfaces for creation of a single(new) Preference setting
*
*/
var NewPrefDialog = {
_prefsShield:
null,
_newPrefsDialog:
null,
_newPrefItem:
null,
_prefNameInputElt:
null,
_prefTypeSelectElt:
null,
_booleanValue:
null,
_booleanToggle:
null,
_stringValue:
null,
_intValue:
null,
_positiveButton:
null,
get type() {
return this._prefTypeSelectElt.value;
},
set type(aType) {
this._prefTypeSelectElt.value = aType;
switch (
this._prefTypeSelectElt.value) {
case "boolean":
this._prefTypeSelectElt.selectedIndex = 0;
break;
case "string":
this._prefTypeSelectElt.selectedIndex = 1;
break;
case "int":
this._prefTypeSelectElt.selectedIndex = 2;
break;
}
this._newPrefItem.setAttribute(
"typestyle", aType);
},
// Init the NewPrefDialog
init:
function AC_init() {
this._prefsShield = document.getElementById(
"prefs-shield");
this._newPrefsDialog = document.getElementById(
"new-pref-container");
this._newPrefItem = document.getElementById(
"new-pref-item");
this._prefNameInputElt = document.getElementById(
"new-pref-name");
this._prefTypeSelectElt = document.getElementById(
"new-pref-type");
this._booleanValue = document.getElementById(
"new-pref-value-boolean");
this._stringValue = document.getElementById(
"new-pref-value-string");
this._intValue = document.getElementById(
"new-pref-value-int");
this._positiveButton = document.getElementById(
"positive-button");
},
// Called to update positive button to display text ("Create"/"Change), and enabled/disabled status
// As new pref name is initially displayed, re-focused, or modifed during user input
_updatePositiveButton:
function AC_updatePositiveButton(aPrefName) {
document.l10n.setAttributes(
this._positiveButton,
"config-new-pref-create-button"
);
this._positiveButton.setAttribute(
"disabled",
true);
if (aPrefName ==
"") {
return;
}
// If item already in list, it's being changed, else added
const item = AboutConfig._list.filter(i => {
return i.name == aPrefName;
});
if (item.length) {
document.l10n.setAttributes(
this._positiveButton,
"config-new-pref-change-button"
);
}
else {
this._positiveButton.removeAttribute(
"disabled");
}
},
// When we want to cancel/hide an existing, or show a new pref dialog
toggleShowHide:
function AC_toggleShowHide() {
if (
this._newPrefsDialog.classList.contains(
"show")) {
this.hide();
}
else {
this._show();
}
},
// When we want to show the new pref dialog / shield the prefs list
_show:
function AC_show() {
this._newPrefsDialog.classList.add(
"show");
this._prefsShield.setAttribute(
"shown",
true);
// Initial default field values
this._prefNameInputElt.value =
"";
this._updatePositiveButton(
this._prefNameInputElt.value);
this.type =
"boolean";
this._booleanValue.value =
"false";
this._stringValue.value =
"";
this._intValue.value =
"";
this._prefNameInputElt.focus();
window.addEventListener(
"keypress",
this.handleKeypress);
},
// When we want to cancel/hide the new pref dialog / un-shield the prefs list
hide:
function AC_hide() {
this._newPrefsDialog.classList.remove(
"show");
this._prefsShield.removeAttribute(
"shown");
window.removeEventListener(
"keypress",
this.handleKeypress);
},
// Watch user key input so we can provide Enter key action, commit input values
handleKeypress:
function AC_handleKeypress(aEvent) {
// Close our VKB on new pref enter key press
if (aEvent.keyCode == VKB_ENTER_KEY) {
aEvent.target.blur();
}
},
// New prefs create dialog only allows creating a non-existing preference, doesn't allow for
// Changing an existing one on-the-fly, tap existing/displayed line item pref for that
create:
function AC_create() {
if (
this._positiveButton.getAttribute(
"disabled") ==
"true") {
return;
}
switch (
this.type) {
case "boolean":
Services.prefs.setBoolPref(
this._prefNameInputElt.value,
!!(
this._booleanValue.value ==
"true")
);
break;
case "string":
Services.prefs.setCharPref(
this._prefNameInputElt.value,
this._stringValue.value
);
break;
case "int":
Services.prefs.setIntPref(
this._prefNameInputElt.value,
this._intValue.value
);
break;
}
// Ensure pref adds flushed to disk immediately
Services.prefs.savePrefFile(
null);
this.hide();
},
// Display proper positive button text/state on new prefs name input focus
focusName:
function AC_focusName(aEvent) {
this._updatePositiveButton(aEvent.target.value);
},
// Display proper positive button text/state as user changes new prefs name
updateName:
function AC_updateName(aEvent) {
this._updatePositiveButton(aEvent.target.value);
},
// In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an
// Actual Services.prefs.*etBoolPref()
toggleBoolValue:
function AC_toggleBoolValue() {
this._booleanValue.value =
this._booleanValue.value ==
"true" ?
"false" :
"true";
},
};
/* ============================== AboutConfig ==============================
*
* Main AboutConfig object and methods
*
* Implements User Interfaces for maintenance of a list of Preference settings
*
*/
var AboutConfig = {
contextMenuLINode:
null,
filterInput:
null,
_filterPrevInput:
null,
_filterChangeTimer:
null,
_prefsContainer:
null,
_loadingContainer:
null,
_list:
null,
// Init the main AboutConfig dialog
init:
function AC_init() {
this.filterInput = document.getElementById(
"filter-input");
this._prefsContainer = document.getElementById(
"prefs-container");
this._loadingContainer = document.getElementById(
"loading-container");
const list = Services.prefs.getChildList(
"");
this._list = list.sort().map(
function AC_getMapPref(aPref) {
return new Pref(aPref);
},
this);
// Support filtering about:config via a ?filter=<string> param
const match = /[?&]filter=([^&]+)/i.exec(window.location.href);
if (match) {
this.filterInput.value = decodeURIComponent(match[1]);
}
// Display the current prefs list (retains searchFilter value)
this.bufferFilterInput();
// Setup the prefs observers
Services.prefs.addObserver(
"",
this);
},
// Uninit the main AboutConfig dialog
uninit:
function AC_uninit() {
// Remove the prefs observer
Services.prefs.removeObserver(
"",
this);
},
// Clear the filterInput value, to display the entire list
clearFilterInput:
function AC_clearFilterInput() {
this.filterInput.value =
"";
this.bufferFilterInput();
},
// Buffer down rapid changes in filterInput value from keyboard
bufferFilterInput:
function AC_bufferFilterInput() {
if (
this._filterChangeTimer) {
clearTimeout(
this._filterChangeTimer);
}
this._filterChangeTimer = setTimeout(() => {
this._filterChangeTimer =
null;
// Display updated prefs list when filterInput value settles
this._displayNewList();
}, FILTER_CHANGE_TRIGGER);
},
// Update displayed list when filterInput value changes
_displayNewList:
function AC_displayNewList() {
// This survives the search filter value past a page refresh
this.filterInput.setAttribute(
"value",
this.filterInput.value);
// Don't start new filter search if same as last
if (
this.filterInput.value ==
this._filterPrevInput) {
return;
}
this._filterPrevInput =
this.filterInput.value;
// Clear list item selection / context menu, prefs list, get first buffer, set scrolling on
this.selected =
"";
this._clearPrefsContainer();
this._addMorePrefsToContainer();
window.onscroll =
this.onScroll.bind(
this);
// Pause for screen to settle, then ensure at top
setTimeout(() => {
window.scrollTo(0, 0);
}, INITIAL_PAGE_DELAY);
},
// Clear the displayed preferences list
_clearPrefsContainer:
function AC_clearPrefsContainer() {
// Quick clear the prefsContainer list
const empty =
this._prefsContainer.cloneNode(
false);
this._prefsContainer.parentNode.replaceChild(empty,
this._prefsContainer);
this._prefsContainer = empty;
// Quick clear the prefs li.HTML list
this._list.forEach(
function (item) {
delete item.li;
});
},
// Get a small manageable block of prefs items, and add them to the displayed list
_addMorePrefsToContainer:
function AC_addMorePrefsToContainer() {
// Create filter regex
const filterExp =
this.filterInput.value
?
new RegExp(
this.filterInput.value,
"i")
:
null;
// Get a new block for the display list
const prefsBuffer = [];
for (
let i = 0;
i <
this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX;
i++
) {
if (!
this._list[i].li &&
this._list[i].test(filterExp)) {
prefsBuffer.push(
this._list[i]);
}
}
// Add the new block to the displayed list
for (let i = 0; i < prefsBuffer.length; i++) {
this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode());
}
// Determine if anything left to add later by scrolling
let anotherPrefsBufferRemains =
false;
for (let i = 0; i <
this._list.length; i++) {
if (!
this._list[i].li &&
this._list[i].test(filterExp)) {
anotherPrefsBufferRemains =
true;
break;
}
}
if (anotherPrefsBufferRemains) {
// If still more could be displayed, show the throbber
this._loadingContainer.style.display =
"block";
}
else {
// If no more could be displayed, hide the throbber, and stop noticing scroll events
this._loadingContainer.style.display =
"none";
window.onscroll =
null;
}
},
// If scrolling at the bottom, maybe add some more entries
onScroll:
function AC_onScroll() {
if (
this._prefsContainer.scrollHeight -
(window.pageYOffset + window.innerHeight) <
PAGE_SCROLL_TRIGGER
) {
if (!
this._filterChangeTimer) {
this._addMorePrefsToContainer();
}
}
},
// Return currently selected list item node
get selected() {
return document.querySelector(
".pref-item.selected");
},
// Set list item node as selected
set selected(aSelection) {
const currentSelection =
this.selected;
if (aSelection == currentSelection) {
return;
}
// Clear any previous selection
if (currentSelection) {
currentSelection.classList.remove(
"selected");
currentSelection.removeEventListener(
"keypress",
this.handleKeypress);
}
// Set any current selection
if (aSelection) {
aSelection.classList.add(
"selected");
aSelection.addEventListener(
"keypress",
this.handleKeypress);
}
},
// Watch user key input so we can provide Enter key action, commit input values
handleKeypress:
function AC_handleKeypress(aEvent) {
if (aEvent.keyCode == VKB_ENTER_KEY) {
aEvent.target.blur();
}
},
// Return the target list item node of an action event
getLINodeForEvent:
function AC_getLINodeForEvent(aEvent) {
let node = aEvent.target;
while (node && node.nodeName !=
"li") {
node = node.parentNode;
}
return node;
},
// Return a pref of a list item node
_getPrefForNode:
function AC_getPrefForNode(aNode) {
const pref = aNode.getAttribute(
"name");
return new Pref(pref);
},
// When list item name or value are tapped
selectOrToggleBoolPref:
function AC_selectOrToggleBoolPref(aEvent) {
const node =
this.getLINodeForEvent(aEvent);
// If not already selected, just do so
if (
this.selected != node) {
this.selected = node;
return;
}
// If already selected, and value is boolean, toggle it
const pref =
this._getPrefForNode(node);
if (pref.type != Services.prefs.PREF_BOOL) {
return;
}
this.toggleBoolPref(aEvent);
},
// When finalizing list input values due to blur
setIntOrStringPref:
function AC_setIntOrStringPref(aEvent) {
const node =
this.getLINodeForEvent(aEvent);
// Skip if locked
const pref =
this._getPrefForNode(node);
if (pref.locked) {
return;
}
// Boolean inputs blur to remove focus from "button"
if (pref.type == Services.prefs.PREF_BOOL) {
return;
}
// String and Int inputs change / commit on blur
pref.value = aEvent.target.value;
},
// When we reset a pref to it's default value (note resetting a user created pref will delete it)
resetDefaultPref:
function AC_resetDefaultPref(aEvent) {
const node =
this.getLINodeForEvent(aEvent);
// If not already selected, do so
if (
this.selected != node) {
this.selected = node;
}
// Reset will handle any locked condition
const pref =
this._getPrefForNode(node);
pref.reset();
// Ensure pref reset flushed to disk immediately
Services.prefs.savePrefFile(
null);
},
// When we want to toggle a bool pref
toggleBoolPref:
function AC_toggleBoolPref(aEvent) {
const node =
this.getLINodeForEvent(aEvent);
// Skip if locked, or not boolean
const pref =
this._getPrefForNode(node);
if (pref.locked) {
return;
}
// Toggle, and blur to remove field focus
pref.value = !pref.value;
aEvent.target.blur();
},
// When Int inputs have their Up or Down arrows toggled
incrOrDecrIntPref:
function AC_incrOrDecrIntPref(aEvent, aInt) {
const node =
this.getLINodeForEvent(aEvent);
// Skip if locked
const pref =
this._getPrefForNode(node);
if (pref.locked) {
return;
}
pref.value += aInt;
},
// Observe preference changes
observe:
function AC_observe(aSubject, aTopic, aPrefName) {
const pref =
new Pref(aPrefName);
// Ignore uninteresting changes, and avoid "private" preferences
if (aTopic !=
"nsPref:changed") {
return;
}
// If pref type invalid, refresh display as user reset/removed an item from the list
if (pref.type == Services.prefs.PREF_INVALID) {
document.location.reload();
return;
}
// If pref onscreen, update in place.
const item = document.querySelector(
'.pref-item[name="' + CSS.escape(pref.name) +
'"]'
);
if (item) {
item.setAttribute(
"value", pref.value);
const input = item.querySelector(
"input");
input.setAttribute(
"value", pref.value);
input.value = pref.value;
pref.
default
? item.querySelector(
".reset").setAttribute(
"disabled",
"true")
: item.querySelector(
".reset").removeAttribute(
"disabled");
return;
}
// If pref not already in list, refresh display as it's being added
const anyWhere =
this._list.filter(i => {
return i.name == pref.name;
});
if (!anyWhere.length) {
document.location.reload();
}
},
// Quick context menu helpers for about:config
clipboardCopy:
function AC_clipboardCopy(aField) {
const pref =
this._getPrefForNode(
this.contextMenuLINode);
if (aField ==
"name") {
gClipboardHelper.copyString(pref.name);
}
else {
gClipboardHelper.copyString(pref.value);
}
},
};
/* ============================== Pref ==============================
*
* Individual Preference object / methods
*
* Defines a Pref object, a document list item tied to Preferences Services
* And the methods by which they interact.
*
*/
function Pref(aName) {
this.name = aName;
}
Pref.prototype = {
get type() {
return Services.prefs.getPrefType(
this.name);
},
get value() {
switch (
this.type) {
case Services.prefs.PREF_BOOL:
return Services.prefs.getBoolPref(
this.name);
case Services.prefs.PREF_INT:
return Services.prefs.getIntPref(
this.name);
case Services.prefs.PREF_STRING:
default:
return Services.prefs.getCharPref(
this.name);
}
},
set value(aPrefValue) {
switch (
this.type) {
case Services.prefs.PREF_BOOL:
Services.prefs.setBoolPref(
this.name, aPrefValue);
break;
case Services.prefs.PREF_INT:
Services.prefs.setIntPref(
this.name, aPrefValue);
break;
case Services.prefs.PREF_STRING:
default:
Services.prefs.setCharPref(
this.name, aPrefValue);
}
// Ensure pref change flushed to disk immediately
Services.prefs.savePrefFile(
null);
},
get
default() {
return !Services.prefs.prefHasUserValue(
this.name);
},
get locked() {
return Services.prefs.prefIsLocked(
this.name);
},
reset:
function AC_reset() {
Services.prefs.clearUserPref(
this.name);
},
test:
function AC_test(aValue) {
return aValue ? aValue.test(
this.name) :
true;
},
// Get existing or create new LI node for the pref
getOrCreateNewLINode:
function AC_getOrCreateNewLINode() {
if (!
this.li) {
this.li = document.createElement(
"li");
this.li.className =
"pref-item";
this.li.setAttribute(
"name",
this.name);
// Click callback to ensure list item selected even on no-action tap events
this.li.addEventListener(
"click",
function (aEvent) {
AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent);
});
// Contextmenu callback to identify selected list item
this.li.addEventListener(
"contextmenu",
function (aEvent) {
AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent);
});
this.li.setAttribute(
"contextmenu",
"prefs-context-menu");
const prefName = document.createElement(
"div");
prefName.className =
"pref-name";
prefName.addEventListener(
"click",
function (event) {
AboutConfig.selectOrToggleBoolPref(event);
});
prefName.textContent =
this.name;
this.li.appendChild(prefName);
const prefItemLine = document.createElement(
"div");
prefItemLine.className =
"pref-item-line";
const prefValue = document.createElement(
"input");
prefValue.className =
"pref-value";
prefValue.addEventListener(
"blur",
function (event) {
AboutConfig.setIntOrStringPref(event);
});
prefValue.addEventListener(
"click",
function (event) {
AboutConfig.selectOrToggleBoolPref(event);
});
prefValue.value =
"";
prefItemLine.appendChild(prefValue);
const resetButton = document.createElement(
"div");
resetButton.className =
"pref-button reset";
resetButton.addEventListener(
"click",
function (event) {
AboutConfig.resetDefaultPref(event);
});
resetButton.setAttribute(
"data-l10n-id",
"config-pref-reset-button");
prefItemLine.appendChild(resetButton);
const toggleButton = document.createElement(
"div");
toggleButton.className =
"pref-button toggle";
toggleButton.addEventListener(
"click",
function (event) {
AboutConfig.toggleBoolPref(event);
});
toggleButton.setAttribute(
"data-l10n-id",
"config-pref-toggle-button");
prefItemLine.appendChild(toggleButton);
const upButton = document.createElement(
"div");
upButton.className =
"pref-button up";
upButton.addEventListener(
"click",
function (event) {
AboutConfig.incrOrDecrIntPref(event, 1);
});
prefItemLine.appendChild(upButton);
const downButton = document.createElement(
"div");
downButton.className =
"pref-button down";
downButton.addEventListener(
"click",
function (event) {
AboutConfig.incrOrDecrIntPref(event, -1);
});
prefItemLine.appendChild(downButton);
this.li.appendChild(prefItemLine);
// Delay providing the list item values, until the LI is returned and added to the document
setTimeout(
this._valueSetup.bind(
this), INNERHTML_VALUE_DELAY);
}
return this.li;
},
// Initialize list item object values
_valueSetup:
function AC_valueSetup() {
this.li.setAttribute(
"type",
this.type);
this.li.setAttribute(
"value",
this.value);
const valDiv =
this.li.querySelector(
".pref-value");
valDiv.value =
this.value;
switch (
this.type) {
case Services.prefs.PREF_BOOL:
valDiv.setAttribute(
"type",
"button");
this.li.querySelector(
".up").setAttribute(
"disabled",
true);
this.li.querySelector(
".down").setAttribute(
"disabled",
true);
break;
case Services.prefs.PREF_STRING:
valDiv.setAttribute(
"type",
"text");
this.li.querySelector(
".up").setAttribute(
"disabled",
true);
this.li.querySelector(
".down").setAttribute(
"disabled",
true);
this.li.querySelector(
".toggle").setAttribute(
"disabled",
true);
break;
case Services.prefs.PREF_INT:
valDiv.setAttribute(
"type",
"number");
this.li.querySelector(
".toggle").setAttribute(
"disabled",
true);
break;
}
this.li.setAttribute(
"default",
this.
default);
if (
this.
default) {
this.li.querySelector(
".reset").setAttribute(
"disabled",
true);
}
if (
this.locked) {
valDiv.setAttribute(
"disabled",
this.locked);
this.li.querySelector(
".pref-name").setAttribute(
"locked",
true);
}
},
};