Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 const {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components;
7 Cu.import("resource://gre/modules/Services.jsm");
9 const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment
10 const PREFS_BUFFER_MAX = 100; // Max prefs buffer size for getPrefsBuffer()
11 const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom
12 const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes
13 const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value
15 let gStringBundle = Services.strings.createBundle("chrome://browser/locale/config.properties");
16 let gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
19 /* ============================== NewPrefDialog ==============================
20 *
21 * New Preference Dialog Object and methods
22 *
23 * Implements User Interfaces for creation of a single(new) Preference setting
24 *
25 */
26 var NewPrefDialog = {
28 _prefsShield: null,
30 _newPrefsDialog: null,
31 _newPrefItem: null,
32 _prefNameInputElt: null,
33 _prefTypeSelectElt: null,
35 _booleanValue: null,
36 _booleanToggle: null,
37 _stringValue: null,
38 _intValue: null,
40 _positiveButton: null,
42 get type() {
43 return this._prefTypeSelectElt.value;
44 },
46 set type(aType) {
47 this._prefTypeSelectElt.value = aType;
48 switch(this._prefTypeSelectElt.value) {
49 case "boolean":
50 this._prefTypeSelectElt.selectedIndex = 0;
51 break;
52 case "string":
53 this._prefTypeSelectElt.selectedIndex = 1;
54 break;
55 case "int":
56 this._prefTypeSelectElt.selectedIndex = 2;
57 break;
58 }
60 this._newPrefItem.setAttribute("typestyle", aType);
61 },
63 // Init the NewPrefDialog
64 init: function AC_init() {
65 this._prefsShield = document.getElementById("prefs-shield");
67 this._newPrefsDialog = document.getElementById("new-pref-container");
68 this._newPrefItem = document.getElementById("new-pref-item");
69 this._prefNameInputElt = document.getElementById("new-pref-name");
70 this._prefTypeSelectElt = document.getElementById("new-pref-type");
72 this._booleanValue = document.getElementById("new-pref-value-boolean");
73 this._stringValue = document.getElementById("new-pref-value-string");
74 this._intValue = document.getElementById("new-pref-value-int");
76 this._positiveButton = document.getElementById("positive-button");
77 },
79 // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status
80 // As new pref name is initially displayed, re-focused, or modifed during user input
81 _updatePositiveButton: function AC_updatePositiveButton(aPrefName) {
82 this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.createButton");
83 this._positiveButton.setAttribute("disabled", true);
84 if (aPrefName == "") {
85 return;
86 }
88 // If item already in list, it's being changed, else added
89 let item = document.querySelector(".pref-item[name=" + aPrefName.quote() + "]");
90 if (item) {
91 this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.changeButton");
92 } else {
93 this._positiveButton.removeAttribute("disabled");
94 }
95 },
97 // When we want to cancel/hide an existing, or show a new pref dialog
98 toggleShowHide: function AC_toggleShowHide() {
99 if (this._newPrefsDialog.classList.contains("show")) {
100 this.hide();
101 } else {
102 this._show();
103 }
104 },
106 // When we want to show the new pref dialog / shield the prefs list
107 _show: function AC_show() {
108 this._newPrefsDialog.classList.add("show");
109 this._prefsShield.setAttribute("shown", true);
111 // Initial default field values
112 this._prefNameInputElt.value = "";
113 this._updatePositiveButton(this._prefNameInputElt.value);
115 this.type = "boolean";
116 this._booleanValue.value = "false";
117 this._stringValue.value = "";
118 this._intValue.value = "";
120 this._prefNameInputElt.focus();
122 window.addEventListener("keypress", this.handleKeypress, false);
123 },
125 // When we want to cancel/hide the new pref dialog / un-shield the prefs list
126 hide: function AC_hide() {
127 this._newPrefsDialog.classList.remove("show");
128 this._prefsShield.removeAttribute("shown");
130 window.removeEventListener("keypress", this.handleKeypress, false);
131 },
133 // Watch user key input so we can provide Enter key action, commit input values
134 handleKeypress: function AC_handleKeypress(aEvent) {
135 // Close our VKB on new pref enter key press
136 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
137 aEvent.target.blur();
138 },
140 // New prefs create dialog only allows creating a non-existing preference, doesn't allow for
141 // Changing an existing one on-the-fly, tap existing/displayed line item pref for that
142 create: function AC_create(aEvent) {
143 if (this._positiveButton.getAttribute("disabled") == "true") {
144 return;
145 }
147 switch(this.type) {
148 case "boolean":
149 Services.prefs.setBoolPref(this._prefNameInputElt.value, (this._booleanValue.value == "true") ? true : false);
150 break;
151 case "string":
152 Services.prefs.setCharPref(this._prefNameInputElt.value, this._stringValue.value);
153 break;
154 case "int":
155 Services.prefs.setIntPref(this._prefNameInputElt.value, this._intValue.value);
156 break;
157 }
159 this.hide();
160 },
162 // Display proper positive button text/state on new prefs name input focus
163 focusName: function AC_focusName(aEvent) {
164 this._updatePositiveButton(aEvent.target.value);
165 },
167 // Display proper positive button text/state as user changes new prefs name
168 updateName: function AC_updateName(aEvent) {
169 this._updatePositiveButton(aEvent.target.value);
170 },
172 // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an
173 // Actual Services.prefs.*etBoolPref()
174 toggleBoolValue: function AC_toggleBoolValue() {
175 this._booleanValue.value = (this._booleanValue.value == "true" ? "false" : "true");
176 }
177 }
180 /* ============================== AboutConfig ==============================
181 *
182 * Main AboutConfig object and methods
183 *
184 * Implements User Interfaces for maintenance of a list of Preference settings
185 *
186 */
187 var AboutConfig = {
189 filterInput: null,
190 _filterPrevInput: null,
191 _filterChangeTimer: null,
192 _prefsContainer: null,
193 _loadingContainer: null,
194 _list: null,
196 // Init the main AboutConfig dialog
197 init: function AC_init() {
198 this.filterInput = document.getElementById("filter-input");
199 this._prefsContainer = document.getElementById("prefs-container");
200 this._loadingContainer = document.getElementById("loading-container");
202 let list = Services.prefs.getChildList("");
203 this._list = list.sort().map( function AC_getMapPref(aPref) {
204 return new Pref(aPref);
205 }, this);
207 // Display the current prefs list (retains searchFilter value)
208 this.bufferFilterInput();
210 // Setup the prefs observers
211 Services.prefs.addObserver("", this, false);
212 },
214 // Uninit the main AboutConfig dialog
215 uninit: function AC_uninit() {
216 // Remove the prefs observer
217 Services.prefs.removeObserver("", this);
218 },
220 // Clear the filterInput value, to display the entire list
221 clearFilterInput: function AC_clearFilterInput() {
222 this.filterInput.value = "";
223 this.bufferFilterInput();
224 },
226 // Buffer down rapid changes in filterInput value from keyboard
227 bufferFilterInput: function AC_bufferFilterInput() {
228 if (this._filterChangeTimer) {
229 clearTimeout(this._filterChangeTimer);
230 }
232 this._filterChangeTimer = setTimeout((function() {
233 this._filterChangeTimer = null;
234 // Display updated prefs list when filterInput value settles
235 this._displayNewList();
236 }).bind(this), FILTER_CHANGE_TRIGGER);
237 },
239 // Update displayed list when filterInput value changes
240 _displayNewList: function AC_displayNewList() {
241 // This survives the search filter value past a page refresh
242 this.filterInput.setAttribute("value", this.filterInput.value);
244 // Don't start new filter search if same as last
245 if (this.filterInput.value == this._filterPrevInput) {
246 return;
247 }
248 this._filterPrevInput = this.filterInput.value;
250 // Clear list item selection and prefs list, get first buffer, set scrolling on
251 this.selected = "";
252 this._clearPrefsContainer();
253 this._addMorePrefsToContainer();
254 window.onscroll = this.onScroll.bind(this);
256 // Pause for screen to settle, then ensure at top
257 setTimeout((function() {
258 window.scrollTo(0, 0);
259 }).bind(this), INITIAL_PAGE_DELAY);
260 },
262 // Clear the displayed preferences list
263 _clearPrefsContainer: function AC_clearPrefsContainer() {
264 // Quick clear the prefsContainer list
265 let empty = this._prefsContainer.cloneNode(false);
266 this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer);
267 this._prefsContainer = empty;
269 // Quick clear the prefs li.HTML list
270 this._list.forEach(function(item) {
271 delete item.li;
272 });
273 },
275 // Get a small manageable block of prefs items, and add them to the displayed list
276 _addMorePrefsToContainer: function AC_addMorePrefsToContainer() {
277 // Create filter regex
278 let filterExp = this.filterInput.value ?
279 new RegExp(this.filterInput.value, "i") : null;
281 // Get a new block for the display list
282 let prefsBuffer = [];
283 for (let i = 0; i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; i++) {
284 if (!this._list[i].li && this._list[i].test(filterExp)) {
285 prefsBuffer.push(this._list[i]);
286 }
287 }
289 // Add the new block to the displayed list
290 for (let i = 0; i < prefsBuffer.length; i++) {
291 this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode());
292 }
294 // Determine if anything left to add later by scrolling
295 let anotherPrefsBufferRemains = false;
296 for (let i = 0; i < this._list.length; i++) {
297 if (!this._list[i].li && this._list[i].test(filterExp)) {
298 anotherPrefsBufferRemains = true;
299 break;
300 }
301 }
303 if (anotherPrefsBufferRemains) {
304 // If still more could be displayed, show the throbber
305 this._loadingContainer.style.display = "block";
306 } else {
307 // If no more could be displayed, hide the throbber, and stop noticing scroll events
308 this._loadingContainer.style.display = "none";
309 window.onscroll = null;
310 }
311 },
313 // If scrolling at the bottom, maybe add some more entries
314 onScroll: function AC_onScroll(aEvent) {
315 if (this._prefsContainer.scrollHeight - (window.pageYOffset + window.innerHeight) < PAGE_SCROLL_TRIGGER) {
316 if (!this._filterChangeTimer) {
317 this._addMorePrefsToContainer();
318 }
319 }
320 },
322 // Return currently selected list item node
323 get selected() {
324 return document.querySelector(".pref-item.selected");
325 },
327 // Set list item node as selected
328 set selected(aSelection) {
329 let currentSelection = this.selected;
330 if (aSelection == currentSelection) {
331 return;
332 }
334 // Clear any previous selection
335 if (currentSelection) {
336 currentSelection.classList.remove("selected");
337 currentSelection.removeEventListener("keypress", this.handleKeypress, false);
338 }
340 // Set any current selection
341 if (aSelection) {
342 aSelection.classList.add("selected");
343 aSelection.addEventListener("keypress", this.handleKeypress, false);
344 }
345 },
347 // Watch user key input so we can provide Enter key action, commit input values
348 handleKeypress: function AC_handleKeypress(aEvent) {
349 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
350 aEvent.target.blur();
351 },
353 // Return the target list item node of an action event
354 getLINodeForEvent: function AC_getLINodeForEvent(aEvent) {
355 let node = aEvent.target;
356 while (node && node.nodeName != "li") {
357 node = node.parentNode;
358 }
360 return node;
361 },
363 // Return a pref of a list item node
364 _getPrefForNode: function AC_getPrefForNode(aNode) {
365 let pref = aNode.getAttribute("name");
367 return new Pref(pref);
368 },
370 // When list item name or value are tapped
371 selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) {
372 let node = this.getLINodeForEvent(aEvent);
374 // If not already selected, just do so
375 if (this.selected != node) {
376 this.selected = node;
377 return;
378 }
380 // If already selected, and value is boolean, toggle it
381 let pref = this._getPrefForNode(node);
382 if (pref.type != Services.prefs.PREF_BOOL) {
383 return;
384 }
386 this.toggleBoolPref(aEvent);
387 },
389 // When finalizing list input values due to blur
390 setIntOrStringPref: function AC_setIntOrStringPref(aEvent) {
391 let node = this.getLINodeForEvent(aEvent);
393 // Skip if locked
394 let pref = this._getPrefForNode(node);
395 if (pref.locked) {
396 return;
397 }
399 // Boolean inputs blur to remove focus from "button"
400 if (pref.type == Services.prefs.PREF_BOOL) {
401 return;
402 }
404 // String and Int inputs change / commit on blur
405 pref.value = aEvent.target.value;
406 },
408 // When we reset a pref to it's default value (note resetting a user created pref will delete it)
409 resetDefaultPref: function AC_resetDefaultPref(aEvent) {
410 let node = this.getLINodeForEvent(aEvent);
412 // If not already selected, do so
413 if (this.selected != node) {
414 this.selected = node;
415 }
417 // Reset will handle any locked condition
418 let pref = this._getPrefForNode(node);
419 pref.reset();
420 },
422 // When we want to toggle a bool pref
423 toggleBoolPref: function AC_toggleBoolPref(aEvent) {
424 let node = this.getLINodeForEvent(aEvent);
426 // Skip if locked, or not boolean
427 let pref = this._getPrefForNode(node);
428 if (pref.locked) {
429 return;
430 }
432 // Toggle, and blur to remove field focus
433 pref.value = !pref.value;
434 aEvent.target.blur();
435 },
437 // When Int inputs have their Up or Down arrows toggled
438 incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) {
439 let node = this.getLINodeForEvent(aEvent);
441 // Skip if locked
442 let pref = this._getPrefForNode(node);
443 if (pref.locked) {
444 return;
445 }
447 pref.value += aInt;
448 },
450 // Observe preference changes
451 observe: function AC_observe(aSubject, aTopic, aPrefName) {
452 let pref = new Pref(aPrefName);
454 // Ignore uninteresting changes, and avoid "private" preferences
455 if (aTopic != "nsPref:changed") {
456 return;
457 }
459 // If pref type invalid, refresh display as user reset/removed an item from the list
460 if (pref.type == Services.prefs.PREF_INVALID) {
461 document.location.reload();
462 return;
463 }
465 // If pref not already in list, refresh display as it's being added
466 let item = document.querySelector(".pref-item[name=" + pref.name.quote() + "]");
467 if (!item) {
468 document.location.reload();
469 return;
470 }
472 // Else we're modifying a pref
473 item.setAttribute("value", pref.value);
474 let input = item.querySelector("input");
475 input.setAttribute("value", pref.value);
476 input.value = pref.value;
478 pref.default ?
479 item.querySelector(".reset").setAttribute("disabled", "true") :
480 item.querySelector(".reset").removeAttribute("disabled");
481 }
482 }
485 /* ============================== Pref ==============================
486 *
487 * Individual Preference object / methods
488 *
489 * Defines a Pref object, a document list item tied to Preferences Services
490 * And the methods by which they interact.
491 *
492 */
493 function Pref(aName) {
494 this.name = aName;
495 }
497 Pref.prototype = {
498 get type() {
499 return Services.prefs.getPrefType(this.name);
500 },
502 get value() {
503 switch (this.type) {
504 case Services.prefs.PREF_BOOL:
505 return Services.prefs.getBoolPref(this.name);
506 case Services.prefs.PREF_INT:
507 return Services.prefs.getIntPref(this.name);
508 case Services.prefs.PREF_STRING:
509 default:
510 return Services.prefs.getCharPref(this.name);
511 }
513 },
515 set value(aPrefValue) {
516 switch (this.type) {
517 case Services.prefs.PREF_BOOL:
518 Services.prefs.setBoolPref(this.name, aPrefValue);
519 break;
520 case Services.prefs.PREF_INT:
521 Services.prefs.setIntPref(this.name, aPrefValue);
522 break;
523 case Services.prefs.PREF_STRING:
524 default:
525 Services.prefs.setCharPref(this.name, aPrefValue);
526 }
527 },
529 get default() {
530 return !Services.prefs.prefHasUserValue(this.name);
531 },
533 get locked() {
534 return Services.prefs.prefIsLocked(this.name);
535 },
537 reset: function AC_reset() {
538 Services.prefs.clearUserPref(this.name);
539 },
541 test: function AC_test(aValue) {
542 return aValue ? aValue.test(this.name) : true;
543 },
545 // Get existing or create new LI node for the pref
546 getOrCreateNewLINode: function AC_getOrCreateNewLINode() {
547 if (!this.li) {
548 this.li = document.createElement("li");
550 this.li.className = "pref-item";
551 this.li.setAttribute("name", this.name);
553 // Click callback to ensure list item selected even on no-action tap events
554 this.li.addEventListener("click",
555 function(aEvent) {
556 AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent);
557 },
558 false
559 );
561 // Create list item outline, bind to object actions
562 this.li.innerHTML =
563 "<div class='pref-name' " +
564 "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
565 this.name +
566 "</div>" +
567 "<div class='pref-item-line'>" +
568 "<input class='pref-value' value='' " +
569 "onblur='AboutConfig.setIntOrStringPref(event);' " +
570 "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
571 "</input>" +
572 "<div class='pref-button reset' " +
573 "onclick='AboutConfig.resetDefaultPref(event);'>" +
574 gStringBundle.GetStringFromName("pref.resetButton") +
575 "</div>" +
576 "<div class='pref-button toggle' " +
577 "onclick='AboutConfig.toggleBoolPref(event);'>" +
578 gStringBundle.GetStringFromName("pref.toggleButton") +
579 "</div>" +
580 "<div class='pref-button up' " +
581 "onclick='AboutConfig.incrOrDecrIntPref(event, 1);'>" +
582 "</div>" +
583 "<div class='pref-button down' " +
584 "onclick='AboutConfig.incrOrDecrIntPref(event, -1);'>" +
585 "</div>" +
586 "</div>";
588 // Delay providing the list item values, until the LI is returned and added to the document
589 setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
590 }
592 return this.li;
593 },
595 // Initialize list item object values
596 _valueSetup: function AC_valueSetup() {
598 this.li.setAttribute("type", this.type);
599 this.li.setAttribute("value", this.value);
601 let valDiv = this.li.querySelector(".pref-value");
602 valDiv.value = this.value;
604 switch(this.type) {
605 case Services.prefs.PREF_BOOL:
606 valDiv.setAttribute("type", "button");
607 this.li.querySelector(".up").setAttribute("disabled", true);
608 this.li.querySelector(".down").setAttribute("disabled", true);
609 break;
610 case Services.prefs.PREF_STRING:
611 valDiv.setAttribute("type", "text");
612 this.li.querySelector(".up").setAttribute("disabled", true);
613 this.li.querySelector(".down").setAttribute("disabled", true);
614 this.li.querySelector(".toggle").setAttribute("disabled", true);
615 break;
616 case Services.prefs.PREF_INT:
617 valDiv.setAttribute("type", "number");
618 this.li.querySelector(".toggle").setAttribute("disabled", true);
619 break;
620 }
622 this.li.setAttribute("default", this.default);
623 if (this.default) {
624 this.li.querySelector(".reset").setAttribute("disabled", true);
625 }
627 if (this.locked) {
628 valDiv.setAttribute("disabled", this.locked);
629 this.li.querySelector(".pref-name").setAttribute("locked", true);
630 }
631 }
632 }