config.js (22137B)
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"; 5 6 var Cm = Components.manager; 7 8 const VKB_ENTER_KEY = 13; // User press of VKB enter key 9 const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment 10 const PREFS_BUFFER_MAX = 30; // 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 14 15 var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( 16 Ci.nsIClipboardHelper 17 ); 18 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 = { 27 _prefsShield: null, 28 29 _newPrefsDialog: null, 30 _newPrefItem: null, 31 _prefNameInputElt: null, 32 _prefTypeSelectElt: null, 33 34 _booleanValue: null, 35 _booleanToggle: null, 36 _stringValue: null, 37 _intValue: null, 38 39 _positiveButton: null, 40 41 get type() { 42 return this._prefTypeSelectElt.value; 43 }, 44 45 set type(aType) { 46 this._prefTypeSelectElt.value = aType; 47 switch (this._prefTypeSelectElt.value) { 48 case "boolean": 49 this._prefTypeSelectElt.selectedIndex = 0; 50 break; 51 case "string": 52 this._prefTypeSelectElt.selectedIndex = 1; 53 break; 54 case "int": 55 this._prefTypeSelectElt.selectedIndex = 2; 56 break; 57 } 58 59 this._newPrefItem.setAttribute("typestyle", aType); 60 }, 61 62 // Init the NewPrefDialog 63 init: function AC_init() { 64 this._prefsShield = document.getElementById("prefs-shield"); 65 66 this._newPrefsDialog = document.getElementById("new-pref-container"); 67 this._newPrefItem = document.getElementById("new-pref-item"); 68 this._prefNameInputElt = document.getElementById("new-pref-name"); 69 this._prefTypeSelectElt = document.getElementById("new-pref-type"); 70 71 this._booleanValue = document.getElementById("new-pref-value-boolean"); 72 this._stringValue = document.getElementById("new-pref-value-string"); 73 this._intValue = document.getElementById("new-pref-value-int"); 74 75 this._positiveButton = document.getElementById("positive-button"); 76 }, 77 78 // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status 79 // As new pref name is initially displayed, re-focused, or modifed during user input 80 _updatePositiveButton: function AC_updatePositiveButton(aPrefName) { 81 document.l10n.setAttributes( 82 this._positiveButton, 83 "config-new-pref-create-button" 84 ); 85 this._positiveButton.setAttribute("disabled", true); 86 if (aPrefName == "") { 87 return; 88 } 89 90 // If item already in list, it's being changed, else added 91 const item = AboutConfig._list.filter(i => { 92 return i.name == aPrefName; 93 }); 94 if (item.length) { 95 document.l10n.setAttributes( 96 this._positiveButton, 97 "config-new-pref-change-button" 98 ); 99 } else { 100 this._positiveButton.removeAttribute("disabled"); 101 } 102 }, 103 104 // When we want to cancel/hide an existing, or show a new pref dialog 105 toggleShowHide: function AC_toggleShowHide() { 106 if (this._newPrefsDialog.classList.contains("show")) { 107 this.hide(); 108 } else { 109 this._show(); 110 } 111 }, 112 113 // When we want to show the new pref dialog / shield the prefs list 114 _show: function AC_show() { 115 this._newPrefsDialog.classList.add("show"); 116 this._prefsShield.setAttribute("shown", true); 117 118 // Initial default field values 119 this._prefNameInputElt.value = ""; 120 this._updatePositiveButton(this._prefNameInputElt.value); 121 122 this.type = "boolean"; 123 this._booleanValue.value = "false"; 124 this._stringValue.value = ""; 125 this._intValue.value = ""; 126 127 this._prefNameInputElt.focus(); 128 129 window.addEventListener("keypress", this.handleKeypress); 130 }, 131 132 // When we want to cancel/hide the new pref dialog / un-shield the prefs list 133 hide: function AC_hide() { 134 this._newPrefsDialog.classList.remove("show"); 135 this._prefsShield.removeAttribute("shown"); 136 137 window.removeEventListener("keypress", this.handleKeypress); 138 }, 139 140 // Watch user key input so we can provide Enter key action, commit input values 141 handleKeypress: function AC_handleKeypress(aEvent) { 142 // Close our VKB on new pref enter key press 143 if (aEvent.keyCode == VKB_ENTER_KEY) { 144 aEvent.target.blur(); 145 } 146 }, 147 148 // New prefs create dialog only allows creating a non-existing preference, doesn't allow for 149 // Changing an existing one on-the-fly, tap existing/displayed line item pref for that 150 create: function AC_create() { 151 if (this._positiveButton.getAttribute("disabled") == "true") { 152 return; 153 } 154 155 switch (this.type) { 156 case "boolean": 157 Services.prefs.setBoolPref( 158 this._prefNameInputElt.value, 159 !!(this._booleanValue.value == "true") 160 ); 161 break; 162 case "string": 163 Services.prefs.setCharPref( 164 this._prefNameInputElt.value, 165 this._stringValue.value 166 ); 167 break; 168 case "int": 169 Services.prefs.setIntPref( 170 this._prefNameInputElt.value, 171 this._intValue.value 172 ); 173 break; 174 } 175 176 // Ensure pref adds flushed to disk immediately 177 Services.prefs.savePrefFile(null); 178 179 this.hide(); 180 }, 181 182 // Display proper positive button text/state on new prefs name input focus 183 focusName: function AC_focusName(aEvent) { 184 this._updatePositiveButton(aEvent.target.value); 185 }, 186 187 // Display proper positive button text/state as user changes new prefs name 188 updateName: function AC_updateName(aEvent) { 189 this._updatePositiveButton(aEvent.target.value); 190 }, 191 192 // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an 193 // Actual Services.prefs.*etBoolPref() 194 toggleBoolValue: function AC_toggleBoolValue() { 195 this._booleanValue.value = 196 this._booleanValue.value == "true" ? "false" : "true"; 197 }, 198 }; 199 200 /* ============================== AboutConfig ============================== 201 * 202 * Main AboutConfig object and methods 203 * 204 * Implements User Interfaces for maintenance of a list of Preference settings 205 * 206 */ 207 var AboutConfig = { 208 contextMenuLINode: null, 209 filterInput: null, 210 _filterPrevInput: null, 211 _filterChangeTimer: null, 212 _prefsContainer: null, 213 _loadingContainer: null, 214 _list: null, 215 216 // Init the main AboutConfig dialog 217 init: function AC_init() { 218 this.filterInput = document.getElementById("filter-input"); 219 this._prefsContainer = document.getElementById("prefs-container"); 220 this._loadingContainer = document.getElementById("loading-container"); 221 222 const list = Services.prefs.getChildList(""); 223 this._list = list.sort().map(function AC_getMapPref(aPref) { 224 return new Pref(aPref); 225 }, this); 226 227 // Support filtering about:config via a ?filter=<string> param 228 const match = /[?&]filter=([^&]+)/i.exec(window.location.href); 229 if (match) { 230 this.filterInput.value = decodeURIComponent(match[1]); 231 } 232 233 // Display the current prefs list (retains searchFilter value) 234 this.bufferFilterInput(); 235 236 // Setup the prefs observers 237 Services.prefs.addObserver("", this); 238 }, 239 240 // Uninit the main AboutConfig dialog 241 uninit: function AC_uninit() { 242 // Remove the prefs observer 243 Services.prefs.removeObserver("", this); 244 }, 245 246 // Buffer down rapid changes in filterInput value from keyboard 247 bufferFilterInput: function AC_bufferFilterInput() { 248 if (this._filterChangeTimer) { 249 clearTimeout(this._filterChangeTimer); 250 } 251 252 this._filterChangeTimer = setTimeout(() => { 253 this._filterChangeTimer = null; 254 // Display updated prefs list when filterInput value settles 255 this._displayNewList(); 256 }, FILTER_CHANGE_TRIGGER); 257 }, 258 259 // Update displayed list when filterInput value changes 260 _displayNewList: function AC_displayNewList() { 261 // This survives the search filter value past a page refresh 262 this.filterInput.setAttribute("value", this.filterInput.value); 263 264 // Don't start new filter search if same as last 265 if (this.filterInput.value == this._filterPrevInput) { 266 return; 267 } 268 this._filterPrevInput = this.filterInput.value; 269 270 // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on 271 this.selected = ""; 272 this._clearPrefsContainer(); 273 this._addMorePrefsToContainer(); 274 window.onscroll = this.onScroll.bind(this); 275 276 // Pause for screen to settle, then ensure at top 277 setTimeout(() => { 278 window.scrollTo(0, 0); 279 }, INITIAL_PAGE_DELAY); 280 }, 281 282 // Clear the displayed preferences list 283 _clearPrefsContainer: function AC_clearPrefsContainer() { 284 // Quick clear the prefsContainer list 285 const empty = this._prefsContainer.cloneNode(false); 286 this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer); 287 this._prefsContainer = empty; 288 289 // Quick clear the prefs li.HTML list 290 this._list.forEach(function (item) { 291 delete item.li; 292 }); 293 }, 294 295 // Get a small manageable block of prefs items, and add them to the displayed list 296 _addMorePrefsToContainer: function AC_addMorePrefsToContainer() { 297 // Create filter regex 298 const filterExp = this.filterInput.value 299 ? new RegExp(this.filterInput.value, "i") 300 : null; 301 302 // Get a new block for the display list 303 const prefsBuffer = []; 304 for ( 305 let i = 0; 306 i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; 307 i++ 308 ) { 309 if (!this._list[i].li && this._list[i].test(filterExp)) { 310 prefsBuffer.push(this._list[i]); 311 } 312 } 313 314 // Add the new block to the displayed list 315 for (let i = 0; i < prefsBuffer.length; i++) { 316 this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode()); 317 } 318 319 // Determine if anything left to add later by scrolling 320 let anotherPrefsBufferRemains = false; 321 for (let i = 0; i < this._list.length; i++) { 322 if (!this._list[i].li && this._list[i].test(filterExp)) { 323 anotherPrefsBufferRemains = true; 324 break; 325 } 326 } 327 328 if (anotherPrefsBufferRemains) { 329 // If still more could be displayed, show the throbber 330 this._loadingContainer.style.display = "block"; 331 } else { 332 // If no more could be displayed, hide the throbber, and stop noticing scroll events 333 this._loadingContainer.style.display = "none"; 334 window.onscroll = null; 335 } 336 }, 337 338 // If scrolling at the bottom, maybe add some more entries 339 onScroll: function AC_onScroll() { 340 if ( 341 this._prefsContainer.scrollHeight - 342 (window.pageYOffset + window.innerHeight) < 343 PAGE_SCROLL_TRIGGER 344 ) { 345 if (!this._filterChangeTimer) { 346 this._addMorePrefsToContainer(); 347 } 348 } 349 }, 350 351 // Return currently selected list item node 352 get selected() { 353 return document.querySelector(".pref-item.selected"); 354 }, 355 356 // Set list item node as selected 357 set selected(aSelection) { 358 const currentSelection = this.selected; 359 if (aSelection == currentSelection) { 360 return; 361 } 362 363 // Clear any previous selection 364 if (currentSelection) { 365 currentSelection.classList.remove("selected"); 366 currentSelection.removeEventListener("keypress", this.handleKeypress); 367 } 368 369 // Set any current selection 370 if (aSelection) { 371 aSelection.classList.add("selected"); 372 aSelection.addEventListener("keypress", this.handleKeypress); 373 } 374 }, 375 376 // Watch user key input so we can provide Enter key action, commit input values 377 handleKeypress: function AC_handleKeypress(aEvent) { 378 if (aEvent.keyCode == VKB_ENTER_KEY) { 379 aEvent.target.blur(); 380 } 381 }, 382 383 // Return the target list item node of an action event 384 getLINodeForEvent: function AC_getLINodeForEvent(aEvent) { 385 let node = aEvent.target; 386 while (node && node.nodeName != "li") { 387 node = node.parentNode; 388 } 389 390 return node; 391 }, 392 393 // Return a pref of a list item node 394 _getPrefForNode: function AC_getPrefForNode(aNode) { 395 const pref = aNode.getAttribute("name"); 396 397 return new Pref(pref); 398 }, 399 400 // When list item name or value are tapped 401 selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) { 402 const node = this.getLINodeForEvent(aEvent); 403 404 // If not already selected, just do so 405 if (this.selected != node) { 406 this.selected = node; 407 return; 408 } 409 410 // If already selected, and value is boolean, toggle it 411 const pref = this._getPrefForNode(node); 412 if (pref.type != Services.prefs.PREF_BOOL) { 413 return; 414 } 415 416 this.toggleBoolPref(aEvent); 417 }, 418 419 // When finalizing list input values due to blur 420 setIntOrStringPref: function AC_setIntOrStringPref(aEvent) { 421 const node = this.getLINodeForEvent(aEvent); 422 423 // Skip if locked 424 const pref = this._getPrefForNode(node); 425 if (pref.locked) { 426 return; 427 } 428 429 // Boolean inputs blur to remove focus from "button" 430 if (pref.type == Services.prefs.PREF_BOOL) { 431 return; 432 } 433 434 // String and Int inputs change / commit on blur 435 pref.value = aEvent.target.value; 436 }, 437 438 // When we reset a pref to it's default value (note resetting a user created pref will delete it) 439 resetDefaultPref: function AC_resetDefaultPref(aEvent) { 440 const node = this.getLINodeForEvent(aEvent); 441 442 // If not already selected, do so 443 if (this.selected != node) { 444 this.selected = node; 445 } 446 447 // Reset will handle any locked condition 448 const pref = this._getPrefForNode(node); 449 pref.reset(); 450 451 // Ensure pref reset flushed to disk immediately 452 Services.prefs.savePrefFile(null); 453 }, 454 455 // When we want to toggle a bool pref 456 toggleBoolPref: function AC_toggleBoolPref(aEvent) { 457 const node = this.getLINodeForEvent(aEvent); 458 459 // Skip if locked, or not boolean 460 const pref = this._getPrefForNode(node); 461 if (pref.locked) { 462 return; 463 } 464 465 // Toggle, and blur to remove field focus 466 pref.value = !pref.value; 467 aEvent.target.blur(); 468 }, 469 470 // When Int inputs have their Up or Down arrows toggled 471 incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) { 472 const node = this.getLINodeForEvent(aEvent); 473 474 // Skip if locked 475 const pref = this._getPrefForNode(node); 476 if (pref.locked) { 477 return; 478 } 479 480 pref.value += aInt; 481 }, 482 483 // Observe preference changes 484 observe: function AC_observe(aSubject, aTopic, aPrefName) { 485 const pref = new Pref(aPrefName); 486 487 // Ignore uninteresting changes, and avoid "private" preferences 488 if (aTopic != "nsPref:changed") { 489 return; 490 } 491 492 // If pref type invalid, refresh display as user reset/removed an item from the list 493 if (pref.type == Services.prefs.PREF_INVALID) { 494 document.location.reload(); 495 return; 496 } 497 498 // If pref onscreen, update in place. 499 const item = document.querySelector( 500 '.pref-item[name="' + CSS.escape(pref.name) + '"]' 501 ); 502 if (item) { 503 item.setAttribute("value", pref.value); 504 const input = item.querySelector("input"); 505 input.setAttribute("value", pref.value); 506 input.value = pref.value; 507 508 pref.default 509 ? item.querySelector(".reset").setAttribute("disabled", "true") 510 : item.querySelector(".reset").removeAttribute("disabled"); 511 return; 512 } 513 514 // If pref not already in list, refresh display as it's being added 515 const anyWhere = this._list.filter(i => { 516 return i.name == pref.name; 517 }); 518 if (!anyWhere.length) { 519 document.location.reload(); 520 } 521 }, 522 523 // Quick context menu helpers for about:config 524 clipboardCopy: function AC_clipboardCopy(aField) { 525 const pref = this._getPrefForNode(this.contextMenuLINode); 526 if (aField == "name") { 527 gClipboardHelper.copyString(pref.name); 528 } else { 529 gClipboardHelper.copyString(pref.value); 530 } 531 }, 532 }; 533 534 /* ============================== Pref ============================== 535 * 536 * Individual Preference object / methods 537 * 538 * Defines a Pref object, a document list item tied to Preferences Services 539 * And the methods by which they interact. 540 * 541 */ 542 function Pref(aName) { 543 this.name = aName; 544 } 545 546 Pref.prototype = { 547 get type() { 548 return Services.prefs.getPrefType(this.name); 549 }, 550 551 get value() { 552 switch (this.type) { 553 case Services.prefs.PREF_BOOL: 554 return Services.prefs.getBoolPref(this.name); 555 case Services.prefs.PREF_INT: 556 return Services.prefs.getIntPref(this.name); 557 case Services.prefs.PREF_STRING: 558 default: 559 return Services.prefs.getCharPref(this.name); 560 } 561 }, 562 set value(aPrefValue) { 563 switch (this.type) { 564 case Services.prefs.PREF_BOOL: 565 Services.prefs.setBoolPref(this.name, aPrefValue); 566 break; 567 case Services.prefs.PREF_INT: 568 Services.prefs.setIntPref(this.name, aPrefValue); 569 break; 570 case Services.prefs.PREF_STRING: 571 default: 572 Services.prefs.setCharPref(this.name, aPrefValue); 573 } 574 575 // Ensure pref change flushed to disk immediately 576 Services.prefs.savePrefFile(null); 577 }, 578 579 get default() { 580 return !Services.prefs.prefHasUserValue(this.name); 581 }, 582 583 get locked() { 584 return Services.prefs.prefIsLocked(this.name); 585 }, 586 587 reset: function AC_reset() { 588 Services.prefs.clearUserPref(this.name); 589 }, 590 591 test: function AC_test(aValue) { 592 return aValue ? aValue.test(this.name) : true; 593 }, 594 595 // Get existing or create new LI node for the pref 596 getOrCreateNewLINode: function AC_getOrCreateNewLINode() { 597 if (!this.li) { 598 this.li = document.createElement("li"); 599 600 this.li.className = "pref-item"; 601 this.li.setAttribute("name", this.name); 602 603 // Click callback to ensure list item selected even on no-action tap events 604 this.li.addEventListener("click", function (aEvent) { 605 AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent); 606 }); 607 608 // Contextmenu callback to identify selected list item 609 this.li.addEventListener("contextmenu", function (aEvent) { 610 AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent); 611 }); 612 613 this.li.setAttribute("contextmenu", "prefs-context-menu"); 614 615 const prefName = document.createElement("div"); 616 prefName.className = "pref-name"; 617 prefName.addEventListener("click", function (event) { 618 AboutConfig.selectOrToggleBoolPref(event); 619 }); 620 prefName.textContent = this.name; 621 622 this.li.appendChild(prefName); 623 624 const prefItemLine = document.createElement("div"); 625 prefItemLine.className = "pref-item-line"; 626 627 const prefValue = document.createElement("input"); 628 prefValue.className = "pref-value"; 629 prefValue.addEventListener("blur", function (event) { 630 AboutConfig.setIntOrStringPref(event); 631 }); 632 prefValue.addEventListener("click", function (event) { 633 AboutConfig.selectOrToggleBoolPref(event); 634 }); 635 prefValue.value = ""; 636 prefItemLine.appendChild(prefValue); 637 638 const resetButton = document.createElement("div"); 639 resetButton.className = "pref-button reset"; 640 resetButton.addEventListener("click", function (event) { 641 AboutConfig.resetDefaultPref(event); 642 }); 643 resetButton.setAttribute("data-l10n-id", "config-pref-reset-button"); 644 prefItemLine.appendChild(resetButton); 645 646 const toggleButton = document.createElement("div"); 647 toggleButton.className = "pref-button toggle"; 648 toggleButton.addEventListener("click", function (event) { 649 AboutConfig.toggleBoolPref(event); 650 }); 651 toggleButton.setAttribute("data-l10n-id", "config-pref-toggle-button"); 652 prefItemLine.appendChild(toggleButton); 653 654 const upButton = document.createElement("div"); 655 upButton.className = "pref-button up"; 656 upButton.addEventListener("click", function (event) { 657 AboutConfig.incrOrDecrIntPref(event, 1); 658 }); 659 prefItemLine.appendChild(upButton); 660 661 const downButton = document.createElement("div"); 662 downButton.className = "pref-button down"; 663 downButton.addEventListener("click", function (event) { 664 AboutConfig.incrOrDecrIntPref(event, -1); 665 }); 666 prefItemLine.appendChild(downButton); 667 668 this.li.appendChild(prefItemLine); 669 670 // Delay providing the list item values, until the LI is returned and added to the document 671 setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY); 672 } 673 674 return this.li; 675 }, 676 677 // Initialize list item object values 678 _valueSetup: function AC_valueSetup() { 679 this.li.setAttribute("type", this.type); 680 this.li.setAttribute("value", this.value); 681 682 const valDiv = this.li.querySelector(".pref-value"); 683 valDiv.value = this.value; 684 685 switch (this.type) { 686 case Services.prefs.PREF_BOOL: 687 valDiv.setAttribute("type", "button"); 688 this.li.querySelector(".up").setAttribute("disabled", true); 689 this.li.querySelector(".down").setAttribute("disabled", true); 690 break; 691 case Services.prefs.PREF_STRING: 692 valDiv.setAttribute("type", "text"); 693 this.li.querySelector(".up").setAttribute("disabled", true); 694 this.li.querySelector(".down").setAttribute("disabled", true); 695 this.li.querySelector(".toggle").setAttribute("disabled", true); 696 break; 697 case Services.prefs.PREF_INT: 698 valDiv.setAttribute("type", "number"); 699 this.li.querySelector(".toggle").setAttribute("disabled", true); 700 break; 701 } 702 703 this.li.setAttribute("default", this.default); 704 if (this.default) { 705 this.li.querySelector(".reset").setAttribute("disabled", true); 706 } 707 708 if (this.locked) { 709 valDiv.setAttribute("disabled", this.locked); 710 this.li.querySelector(".pref-name").setAttribute("locked", true); 711 } 712 }, 713 };