browser/devtools/shared/autocomplete-popup.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 "use strict";
michael@0 7
michael@0 8 const {Cc, Ci, Cu} = require("chrome");
michael@0 9 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 10
michael@0 11 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
michael@0 12 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
michael@0 13
michael@0 14 /**
michael@0 15 * Autocomplete popup UI implementation.
michael@0 16 *
michael@0 17 * @constructor
michael@0 18 * @param nsIDOMDocument aDocument
michael@0 19 * The document you want the popup attached to.
michael@0 20 * @param Object aOptions
michael@0 21 * An object consiting any of the following options:
michael@0 22 * - panelId {String} The id for the popup panel.
michael@0 23 * - listBoxId {String} The id for the richlistbox inside the panel.
michael@0 24 * - position {String} The position for the popup panel.
michael@0 25 * - theme {String} String related to the theme of the popup.
michael@0 26 * - autoSelect {Boolean} Boolean to allow the first entry of the popup
michael@0 27 * panel to be automatically selected when the popup shows.
michael@0 28 * - direction {String} The direction of the text in the panel. rtl or ltr
michael@0 29 * - onSelect {String} The select event handler for the richlistbox
michael@0 30 * - onClick {String} The click event handler for the richlistbox.
michael@0 31 * - onKeypress {String} The keypress event handler for the richlistitems.
michael@0 32 */
michael@0 33 function AutocompletePopup(aDocument, aOptions = {})
michael@0 34 {
michael@0 35 this._document = aDocument;
michael@0 36
michael@0 37 this.autoSelect = aOptions.autoSelect || false;
michael@0 38 this.position = aOptions.position || "after_start";
michael@0 39 this.direction = aOptions.direction || "ltr";
michael@0 40
michael@0 41 this.onSelect = aOptions.onSelect;
michael@0 42 this.onClick = aOptions.onClick;
michael@0 43 this.onKeypress = aOptions.onKeypress;
michael@0 44
michael@0 45 let id = aOptions.panelId || "devtools_autoCompletePopup";
michael@0 46 let theme = aOptions.theme || "dark";
michael@0 47 // If theme is auto, use the devtools.theme pref
michael@0 48 if (theme == "auto") {
michael@0 49 theme = Services.prefs.getCharPref("devtools.theme");
michael@0 50 this.autoThemeEnabled = true;
michael@0 51 // Setup theme change listener.
michael@0 52 this._handleThemeChange = this._handleThemeChange.bind(this);
michael@0 53 gDevTools.on("pref-changed", this._handleThemeChange);
michael@0 54 }
michael@0 55 // Reuse the existing popup elements.
michael@0 56 this._panel = this._document.getElementById(id);
michael@0 57 if (!this._panel) {
michael@0 58 this._panel = this._document.createElementNS(XUL_NS, "panel");
michael@0 59 this._panel.setAttribute("id", id);
michael@0 60 this._panel.className = "devtools-autocomplete-popup devtools-monospace "
michael@0 61 + theme + "-theme";
michael@0 62
michael@0 63 this._panel.setAttribute("noautofocus", "true");
michael@0 64 this._panel.setAttribute("level", "top");
michael@0 65 if (!aOptions.onKeypress) {
michael@0 66 this._panel.setAttribute("ignorekeys", "true");
michael@0 67 }
michael@0 68
michael@0 69 let mainPopupSet = this._document.getElementById("mainPopupSet");
michael@0 70 if (mainPopupSet) {
michael@0 71 mainPopupSet.appendChild(this._panel);
michael@0 72 }
michael@0 73 else {
michael@0 74 this._document.documentElement.appendChild(this._panel);
michael@0 75 }
michael@0 76 }
michael@0 77 else {
michael@0 78 this._list = this._panel.firstChild;
michael@0 79 }
michael@0 80
michael@0 81 if (!this._list) {
michael@0 82 this._list = this._document.createElementNS(XUL_NS, "richlistbox");
michael@0 83 this._panel.appendChild(this._list);
michael@0 84
michael@0 85 // Open and hide the panel, so we initialize the API of the richlistbox.
michael@0 86 this._panel.openPopup(null, this.position, 0, 0);
michael@0 87 this._panel.hidePopup();
michael@0 88 }
michael@0 89
michael@0 90 this._list.setAttribute("flex", "1");
michael@0 91 this._list.setAttribute("seltype", "single");
michael@0 92
michael@0 93 if (aOptions.listBoxId) {
michael@0 94 this._list.setAttribute("id", aOptions.listBoxId);
michael@0 95 }
michael@0 96 this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
michael@0 97
michael@0 98 if (this.onSelect) {
michael@0 99 this._list.addEventListener("select", this.onSelect, false);
michael@0 100 }
michael@0 101
michael@0 102 if (this.onClick) {
michael@0 103 this._list.addEventListener("click", this.onClick, false);
michael@0 104 }
michael@0 105
michael@0 106 if (this.onKeypress) {
michael@0 107 this._list.addEventListener("keypress", this.onKeypress, false);
michael@0 108 }
michael@0 109 }
michael@0 110 exports.AutocompletePopup = AutocompletePopup;
michael@0 111
michael@0 112 AutocompletePopup.prototype = {
michael@0 113 _document: null,
michael@0 114 _panel: null,
michael@0 115 _list: null,
michael@0 116 __scrollbarWidth: null,
michael@0 117
michael@0 118 // Event handlers.
michael@0 119 onSelect: null,
michael@0 120 onClick: null,
michael@0 121 onKeypress: null,
michael@0 122
michael@0 123 /**
michael@0 124 * Open the autocomplete popup panel.
michael@0 125 *
michael@0 126 * @param nsIDOMNode aAnchor
michael@0 127 * Optional node to anchor the panel to.
michael@0 128 * @param Number aXOffset
michael@0 129 * Horizontal offset in pixels from the left of the node to the left
michael@0 130 * of the popup.
michael@0 131 * @param Number aYOffset
michael@0 132 * Vertical offset in pixels from the top of the node to the starting
michael@0 133 * of the popup.
michael@0 134 */
michael@0 135 openPopup: function AP_openPopup(aAnchor, aXOffset = 0, aYOffset = 0)
michael@0 136 {
michael@0 137 this.__maxLabelLength = -1;
michael@0 138 this._updateSize();
michael@0 139 this._panel.openPopup(aAnchor, this.position, aXOffset, aYOffset);
michael@0 140
michael@0 141 if (this.autoSelect) {
michael@0 142 this.selectFirstItem();
michael@0 143 }
michael@0 144 },
michael@0 145
michael@0 146 /**
michael@0 147 * Hide the autocomplete popup panel.
michael@0 148 */
michael@0 149 hidePopup: function AP_hidePopup()
michael@0 150 {
michael@0 151 this._panel.hidePopup();
michael@0 152 },
michael@0 153
michael@0 154 /**
michael@0 155 * Check if the autocomplete popup is open.
michael@0 156 */
michael@0 157 get isOpen() {
michael@0 158 return this._panel.state == "open" || this._panel.state == "showing";
michael@0 159 },
michael@0 160
michael@0 161 /**
michael@0 162 * Destroy the object instance. Please note that the panel DOM elements remain
michael@0 163 * in the DOM, because they might still be in use by other instances of the
michael@0 164 * same code. It is the responsability of the client code to perform DOM
michael@0 165 * cleanup.
michael@0 166 */
michael@0 167 destroy: function AP_destroy()
michael@0 168 {
michael@0 169 if (this.isOpen) {
michael@0 170 this.hidePopup();
michael@0 171 }
michael@0 172 this.clearItems();
michael@0 173
michael@0 174 if (this.onSelect) {
michael@0 175 this._list.removeEventListener("select", this.onSelect, false);
michael@0 176 }
michael@0 177
michael@0 178 if (this.onClick) {
michael@0 179 this._list.removeEventListener("click", this.onClick, false);
michael@0 180 }
michael@0 181
michael@0 182 if (this.onKeypress) {
michael@0 183 this._list.removeEventListener("keypress", this.onKeypress, false);
michael@0 184 }
michael@0 185
michael@0 186 if (this.autoThemeEnabled) {
michael@0 187 gDevTools.off("pref-changed", this._handleThemeChange);
michael@0 188 }
michael@0 189
michael@0 190 this._document = null;
michael@0 191 this._list = null;
michael@0 192 this._panel = null;
michael@0 193 },
michael@0 194
michael@0 195 /**
michael@0 196 * Get the autocomplete items array.
michael@0 197 *
michael@0 198 * @param Number aIndex The index of the item what is wanted.
michael@0 199 *
michael@0 200 * @return The autocomplete item at index aIndex.
michael@0 201 */
michael@0 202 getItemAtIndex: function AP_getItemAtIndex(aIndex)
michael@0 203 {
michael@0 204 return this._list.getItemAtIndex(aIndex)._autocompleteItem;
michael@0 205 },
michael@0 206
michael@0 207 /**
michael@0 208 * Get the autocomplete items array.
michael@0 209 *
michael@0 210 * @return array
michael@0 211 * The array of autocomplete items.
michael@0 212 */
michael@0 213 getItems: function AP_getItems()
michael@0 214 {
michael@0 215 let items = [];
michael@0 216
michael@0 217 Array.forEach(this._list.childNodes, function(aItem) {
michael@0 218 items.push(aItem._autocompleteItem);
michael@0 219 });
michael@0 220
michael@0 221 return items;
michael@0 222 },
michael@0 223
michael@0 224 /**
michael@0 225 * Set the autocomplete items list, in one go.
michael@0 226 *
michael@0 227 * @param array aItems
michael@0 228 * The list of items you want displayed in the popup list.
michael@0 229 */
michael@0 230 setItems: function AP_setItems(aItems)
michael@0 231 {
michael@0 232 this.clearItems();
michael@0 233 aItems.forEach(this.appendItem, this);
michael@0 234
michael@0 235 // Make sure that the new content is properly fitted by the XUL richlistbox.
michael@0 236 if (this.isOpen) {
michael@0 237 if (this.autoSelect) {
michael@0 238 this.selectFirstItem();
michael@0 239 }
michael@0 240 this._updateSize();
michael@0 241 }
michael@0 242 },
michael@0 243
michael@0 244 /**
michael@0 245 * Selects the first item of the richlistbox. Note that first item here is the
michael@0 246 * item closes to the input element, which means that 0th index if position is
michael@0 247 * below, and last index if position is above.
michael@0 248 */
michael@0 249 selectFirstItem: function AP_selectFirstItem()
michael@0 250 {
michael@0 251 if (this.position.contains("before")) {
michael@0 252 this.selectedIndex = this.itemCount - 1;
michael@0 253 }
michael@0 254 else {
michael@0 255 this.selectedIndex = 0;
michael@0 256 }
michael@0 257 this._list.ensureIndexIsVisible(this._list.selectedIndex);
michael@0 258 },
michael@0 259
michael@0 260 __maxLabelLength: -1,
michael@0 261
michael@0 262 get _maxLabelLength() {
michael@0 263 if (this.__maxLabelLength != -1) {
michael@0 264 return this.__maxLabelLength;
michael@0 265 }
michael@0 266
michael@0 267 let max = 0;
michael@0 268 for (let i = 0; i < this._list.childNodes.length; i++) {
michael@0 269 let item = this._list.childNodes[i]._autocompleteItem;
michael@0 270 let str = item.label;
michael@0 271 if (item.count) {
michael@0 272 str += (item.count + "");
michael@0 273 }
michael@0 274 max = Math.max(str.length, max);
michael@0 275 }
michael@0 276
michael@0 277 this.__maxLabelLength = max;
michael@0 278 return this.__maxLabelLength;
michael@0 279 },
michael@0 280
michael@0 281 /**
michael@0 282 * Update the panel size to fit the content.
michael@0 283 *
michael@0 284 * @private
michael@0 285 */
michael@0 286 _updateSize: function AP__updateSize()
michael@0 287 {
michael@0 288 if (!this._panel) {
michael@0 289 return;
michael@0 290 }
michael@0 291
michael@0 292 this._list.style.width = (this._maxLabelLength + 3) +"ch";
michael@0 293 this._list.ensureIndexIsVisible(this._list.selectedIndex);
michael@0 294 },
michael@0 295
michael@0 296 /**
michael@0 297 * Clear all the items from the autocomplete list.
michael@0 298 */
michael@0 299 clearItems: function AP_clearItems()
michael@0 300 {
michael@0 301 // Reset the selectedIndex to -1 before clearing the list
michael@0 302 this.selectedIndex = -1;
michael@0 303
michael@0 304 while (this._list.hasChildNodes()) {
michael@0 305 this._list.removeChild(this._list.firstChild);
michael@0 306 }
michael@0 307
michael@0 308 this.__maxLabelLength = -1;
michael@0 309
michael@0 310 // Reset the panel and list dimensions. New dimensions are calculated when
michael@0 311 // a new set of items is added to the autocomplete popup.
michael@0 312 this._list.width = "";
michael@0 313 this._list.style.width = "";
michael@0 314 this._list.height = "";
michael@0 315 this._panel.width = "";
michael@0 316 this._panel.height = "";
michael@0 317 this._panel.top = "";
michael@0 318 this._panel.left = "";
michael@0 319 },
michael@0 320
michael@0 321 /**
michael@0 322 * Getter for the index of the selected item.
michael@0 323 *
michael@0 324 * @type number
michael@0 325 */
michael@0 326 get selectedIndex() {
michael@0 327 return this._list.selectedIndex;
michael@0 328 },
michael@0 329
michael@0 330 /**
michael@0 331 * Setter for the selected index.
michael@0 332 *
michael@0 333 * @param number aIndex
michael@0 334 * The number (index) of the item you want to select in the list.
michael@0 335 */
michael@0 336 set selectedIndex(aIndex) {
michael@0 337 this._list.selectedIndex = aIndex;
michael@0 338 if (this.isOpen && this._list.ensureIndexIsVisible) {
michael@0 339 this._list.ensureIndexIsVisible(this._list.selectedIndex);
michael@0 340 }
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * Getter for the selected item.
michael@0 345 * @type object
michael@0 346 */
michael@0 347 get selectedItem() {
michael@0 348 return this._list.selectedItem ?
michael@0 349 this._list.selectedItem._autocompleteItem : null;
michael@0 350 },
michael@0 351
michael@0 352 /**
michael@0 353 * Setter for the selected item.
michael@0 354 *
michael@0 355 * @param object aItem
michael@0 356 * The object you want selected in the list.
michael@0 357 */
michael@0 358 set selectedItem(aItem) {
michael@0 359 this._list.selectedItem = this._findListItem(aItem);
michael@0 360 if (this.isOpen) {
michael@0 361 this._list.ensureIndexIsVisible(this._list.selectedIndex);
michael@0 362 }
michael@0 363 },
michael@0 364
michael@0 365 /**
michael@0 366 * Append an item into the autocomplete list.
michael@0 367 *
michael@0 368 * @param object aItem
michael@0 369 * The item you want appended to the list.
michael@0 370 * The item object can have the following properties:
michael@0 371 * - label {String} Property which is used as the displayed value.
michael@0 372 * - preLabel {String} [Optional] The String that will be displayed
michael@0 373 * before the label indicating that this is the already
michael@0 374 * present text in the input box, and label is the text
michael@0 375 * that will be auto completed. When this property is
michael@0 376 * present, |preLabel.length| starting characters will be
michael@0 377 * removed from label.
michael@0 378 * - count {Number} [Optional] The number to represent the count of
michael@0 379 * autocompleted label.
michael@0 380 */
michael@0 381 appendItem: function AP_appendItem(aItem)
michael@0 382 {
michael@0 383 let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
michael@0 384 if (this.direction) {
michael@0 385 listItem.setAttribute("dir", this.direction);
michael@0 386 }
michael@0 387 let label = this._document.createElementNS(XUL_NS, "label");
michael@0 388 label.setAttribute("value", aItem.label);
michael@0 389 label.setAttribute("class", "autocomplete-value");
michael@0 390 if (aItem.preLabel) {
michael@0 391 let preDesc = this._document.createElementNS(XUL_NS, "label");
michael@0 392 preDesc.setAttribute("value", aItem.preLabel);
michael@0 393 preDesc.setAttribute("class", "initial-value");
michael@0 394 listItem.appendChild(preDesc);
michael@0 395 label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
michael@0 396 }
michael@0 397 listItem.appendChild(label);
michael@0 398 if (aItem.count && aItem.count > 1) {
michael@0 399 let countDesc = this._document.createElementNS(XUL_NS, "label");
michael@0 400 countDesc.setAttribute("value", aItem.count);
michael@0 401 countDesc.setAttribute("flex", "1");
michael@0 402 countDesc.setAttribute("class", "autocomplete-count");
michael@0 403 listItem.appendChild(countDesc);
michael@0 404 }
michael@0 405 listItem._autocompleteItem = aItem;
michael@0 406
michael@0 407 this._list.appendChild(listItem);
michael@0 408 },
michael@0 409
michael@0 410 /**
michael@0 411 * Find the richlistitem element that belongs to an item.
michael@0 412 *
michael@0 413 * @private
michael@0 414 *
michael@0 415 * @param object aItem
michael@0 416 * The object you want found in the list.
michael@0 417 *
michael@0 418 * @return nsIDOMNode|null
michael@0 419 * The nsIDOMNode that belongs to the given item object. This node is
michael@0 420 * the richlistitem element.
michael@0 421 */
michael@0 422 _findListItem: function AP__findListItem(aItem)
michael@0 423 {
michael@0 424 for (let i = 0; i < this._list.childNodes.length; i++) {
michael@0 425 let child = this._list.childNodes[i];
michael@0 426 if (child._autocompleteItem == aItem) {
michael@0 427 return child;
michael@0 428 }
michael@0 429 }
michael@0 430 return null;
michael@0 431 },
michael@0 432
michael@0 433 /**
michael@0 434 * Remove an item from the popup list.
michael@0 435 *
michael@0 436 * @param object aItem
michael@0 437 * The item you want removed.
michael@0 438 */
michael@0 439 removeItem: function AP_removeItem(aItem)
michael@0 440 {
michael@0 441 let item = this._findListItem(aItem);
michael@0 442 if (!item) {
michael@0 443 throw new Error("Item not found!");
michael@0 444 }
michael@0 445 this._list.removeChild(item);
michael@0 446 },
michael@0 447
michael@0 448 /**
michael@0 449 * Getter for the number of items in the popup.
michael@0 450 * @type number
michael@0 451 */
michael@0 452 get itemCount() {
michael@0 453 return this._list.childNodes.length;
michael@0 454 },
michael@0 455
michael@0 456 /**
michael@0 457 * Getter for the height of each item in the list.
michael@0 458 *
michael@0 459 * @private
michael@0 460 *
michael@0 461 * @type number
michael@0 462 */
michael@0 463 get _itemHeight() {
michael@0 464 return this._list.selectedItem.clientHeight;
michael@0 465 },
michael@0 466
michael@0 467 /**
michael@0 468 * Select the next item in the list.
michael@0 469 *
michael@0 470 * @return object
michael@0 471 * The newly selected item object.
michael@0 472 */
michael@0 473 selectNextItem: function AP_selectNextItem()
michael@0 474 {
michael@0 475 if (this.selectedIndex < (this.itemCount - 1)) {
michael@0 476 this.selectedIndex++;
michael@0 477 }
michael@0 478 else {
michael@0 479 this.selectedIndex = 0;
michael@0 480 }
michael@0 481
michael@0 482 return this.selectedItem;
michael@0 483 },
michael@0 484
michael@0 485 /**
michael@0 486 * Select the previous item in the list.
michael@0 487 *
michael@0 488 * @return object
michael@0 489 * The newly-selected item object.
michael@0 490 */
michael@0 491 selectPreviousItem: function AP_selectPreviousItem()
michael@0 492 {
michael@0 493 if (this.selectedIndex > 0) {
michael@0 494 this.selectedIndex--;
michael@0 495 }
michael@0 496 else {
michael@0 497 this.selectedIndex = this.itemCount - 1;
michael@0 498 }
michael@0 499
michael@0 500 return this.selectedItem;
michael@0 501 },
michael@0 502
michael@0 503 /**
michael@0 504 * Select the top-most item in the next page of items or
michael@0 505 * the last item in the list.
michael@0 506 *
michael@0 507 * @return object
michael@0 508 * The newly-selected item object.
michael@0 509 */
michael@0 510 selectNextPageItem: function AP_selectNextPageItem()
michael@0 511 {
michael@0 512 let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
michael@0 513 let nextPageIndex = this.selectedIndex + itemsPerPane + 1;
michael@0 514 this.selectedIndex = nextPageIndex > this.itemCount - 1 ?
michael@0 515 this.itemCount - 1 : nextPageIndex;
michael@0 516
michael@0 517 return this.selectedItem;
michael@0 518 },
michael@0 519
michael@0 520 /**
michael@0 521 * Select the bottom-most item in the previous page of items,
michael@0 522 * or the first item in the list.
michael@0 523 *
michael@0 524 * @return object
michael@0 525 * The newly-selected item object.
michael@0 526 */
michael@0 527 selectPreviousPageItem: function AP_selectPreviousPageItem()
michael@0 528 {
michael@0 529 let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
michael@0 530 let prevPageIndex = this.selectedIndex - itemsPerPane - 1;
michael@0 531 this.selectedIndex = prevPageIndex < 0 ? 0 : prevPageIndex;
michael@0 532
michael@0 533 return this.selectedItem;
michael@0 534 },
michael@0 535
michael@0 536 /**
michael@0 537 * Focuses the richlistbox.
michael@0 538 */
michael@0 539 focus: function AP_focus()
michael@0 540 {
michael@0 541 this._list.focus();
michael@0 542 },
michael@0 543
michael@0 544 /**
michael@0 545 * Manages theme switching for the popup based on the devtools.theme pref.
michael@0 546 *
michael@0 547 * @private
michael@0 548 *
michael@0 549 * @param String aEvent
michael@0 550 * The name of the event. In this case, "pref-changed".
michael@0 551 * @param Object aData
michael@0 552 * An object passed by the emitter of the event. In this case, the
michael@0 553 * object consists of three properties:
michael@0 554 * - pref {String} The name of the preference that was modified.
michael@0 555 * - newValue {Object} The new value of the preference.
michael@0 556 * - oldValue {Object} The old value of the preference.
michael@0 557 */
michael@0 558 _handleThemeChange: function AP__handleThemeChange(aEvent, aData)
michael@0 559 {
michael@0 560 if (aData.pref == "devtools.theme") {
michael@0 561 this._panel.classList.toggle(aData.oldValue + "-theme", false);
michael@0 562 this._panel.classList.toggle(aData.newValue + "-theme", true);
michael@0 563 this._list.classList.toggle(aData.oldValue + "-theme", false);
michael@0 564 this._list.classList.toggle(aData.newValue + "-theme", true);
michael@0 565 }
michael@0 566 },
michael@0 567 };

mercurial