1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,676 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +"use strict"; 1.10 + 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 + 1.14 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); 1.15 +Cu.import("resource://gre/modules/devtools/event-emitter.js"); 1.16 + 1.17 +this.EXPORTED_SYMBOLS = ["SideMenuWidget"]; 1.18 + 1.19 +/** 1.20 + * A simple side menu, with the ability of grouping menu items. 1.21 + * 1.22 + * Note: this widget should be used in tandem with the WidgetMethods in 1.23 + * ViewHelpers.jsm. 1.24 + * 1.25 + * @param nsIDOMNode aNode 1.26 + * The element associated with the widget. 1.27 + * @param Object aOptions 1.28 + * - showArrows: specifies if items should display horizontal arrows. 1.29 + * - showItemCheckboxes: specifies if items should display checkboxes. 1.30 + * - showGroupCheckboxes: specifies if groups should display checkboxes. 1.31 + */ 1.32 +this.SideMenuWidget = function SideMenuWidget(aNode, aOptions={}) { 1.33 + this.document = aNode.ownerDocument; 1.34 + this.window = this.document.defaultView; 1.35 + this._parent = aNode; 1.36 + 1.37 + let { showArrows, showItemCheckboxes, showGroupCheckboxes } = aOptions; 1.38 + this._showArrows = showArrows || false; 1.39 + this._showItemCheckboxes = showItemCheckboxes || false; 1.40 + this._showGroupCheckboxes = showGroupCheckboxes || false; 1.41 + 1.42 + // Create an internal scrollbox container. 1.43 + this._list = this.document.createElement("scrollbox"); 1.44 + this._list.className = "side-menu-widget-container theme-sidebar"; 1.45 + this._list.setAttribute("flex", "1"); 1.46 + this._list.setAttribute("orient", "vertical"); 1.47 + this._list.setAttribute("with-arrows", this._showArrows); 1.48 + this._list.setAttribute("with-item-checkboxes", this._showItemCheckboxes); 1.49 + this._list.setAttribute("with-group-checkboxes", this._showGroupCheckboxes); 1.50 + this._list.setAttribute("tabindex", "0"); 1.51 + this._list.addEventListener("keypress", e => this.emit("keyPress", e), false); 1.52 + this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false); 1.53 + this._parent.appendChild(this._list); 1.54 + 1.55 + // Menu items can optionally be grouped. 1.56 + this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings. 1.57 + this._orderedGroupElementsArray = []; 1.58 + this._orderedMenuElementsArray = []; 1.59 + this._itemsByElement = new Map(); 1.60 + 1.61 + // This widget emits events that can be handled in a MenuContainer. 1.62 + EventEmitter.decorate(this); 1.63 + 1.64 + // Delegate some of the associated node's methods to satisfy the interface 1.65 + // required by MenuContainer instances. 1.66 + ViewHelpers.delegateWidgetAttributeMethods(this, aNode); 1.67 + ViewHelpers.delegateWidgetEventMethods(this, aNode); 1.68 +}; 1.69 + 1.70 +SideMenuWidget.prototype = { 1.71 + /** 1.72 + * Specifies if groups in this container should be sorted. 1.73 + */ 1.74 + sortedGroups: true, 1.75 + 1.76 + /** 1.77 + * The comparator used to sort groups. 1.78 + */ 1.79 + groupSortPredicate: function(a, b) a.localeCompare(b), 1.80 + 1.81 + /** 1.82 + * Specifies that the container viewport should be "stuck" to the 1.83 + * bottom. That is, the container is automatically scrolled down to 1.84 + * keep appended items visible, but only when the scroll position is 1.85 + * already at the bottom. 1.86 + */ 1.87 + autoscrollWithAppendedItems: false, 1.88 + 1.89 + /** 1.90 + * Inserts an item in this container at the specified index, optionally 1.91 + * grouping by name. 1.92 + * 1.93 + * @param number aIndex 1.94 + * The position in the container intended for this item. 1.95 + * @param nsIDOMNode aContents 1.96 + * The node displayed in the container. 1.97 + * @param object aAttachment [optional] 1.98 + * Some attached primitive/object. Custom options supported: 1.99 + * - group: a string specifying the group to place this item into 1.100 + * - checkboxState: the checked state of the checkbox, if shown 1.101 + * - checkboxTooltip: the tooltip text for the checkbox, if shown 1.102 + * @return nsIDOMNode 1.103 + * The element associated with the displayed item. 1.104 + */ 1.105 + insertItemAt: function(aIndex, aContents, aAttachment={}) { 1.106 + // Maintaining scroll position at the bottom when a new item is inserted 1.107 + // depends on several factors (the order of testing is important to avoid 1.108 + // needlessly expensive operations that may cause reflows): 1.109 + let maintainScrollAtBottom = 1.110 + // 1. The behavior should be enabled, 1.111 + this.autoscrollWithAppendedItems && 1.112 + // 2. There shouldn't currently be any selected item in the list. 1.113 + !this._selectedItem && 1.114 + // 3. The new item should be appended at the end of the list. 1.115 + (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) && 1.116 + // 4. The list should already be scrolled at the bottom. 1.117 + (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight); 1.118 + 1.119 + let group = this._getMenuGroupForName(aAttachment.group); 1.120 + let item = this._getMenuItemForGroup(group, aContents, aAttachment); 1.121 + let element = item.insertSelfAt(aIndex); 1.122 + 1.123 + if (maintainScrollAtBottom) { 1.124 + this._list.scrollTop = this._list.scrollHeight; 1.125 + } 1.126 + 1.127 + return element; 1.128 + }, 1.129 + 1.130 + /** 1.131 + * Returns the child node in this container situated at the specified index. 1.132 + * 1.133 + * @param number aIndex 1.134 + * The position in the container intended for this item. 1.135 + * @return nsIDOMNode 1.136 + * The element associated with the displayed item. 1.137 + */ 1.138 + getItemAtIndex: function(aIndex) { 1.139 + return this._orderedMenuElementsArray[aIndex]; 1.140 + }, 1.141 + 1.142 + /** 1.143 + * Removes the specified child node from this container. 1.144 + * 1.145 + * @param nsIDOMNode aChild 1.146 + * The element associated with the displayed item. 1.147 + */ 1.148 + removeChild: function(aChild) { 1.149 + this._getNodeForContents(aChild).remove(); 1.150 + 1.151 + this._orderedMenuElementsArray.splice( 1.152 + this._orderedMenuElementsArray.indexOf(aChild), 1); 1.153 + 1.154 + this._itemsByElement.delete(aChild); 1.155 + 1.156 + if (this._selectedItem == aChild) { 1.157 + this._selectedItem = null; 1.158 + } 1.159 + }, 1.160 + 1.161 + /** 1.162 + * Removes all of the child nodes from this container. 1.163 + */ 1.164 + removeAllItems: function() { 1.165 + let parent = this._parent; 1.166 + let list = this._list; 1.167 + 1.168 + while (list.hasChildNodes()) { 1.169 + list.firstChild.remove(); 1.170 + } 1.171 + 1.172 + this._selectedItem = null; 1.173 + 1.174 + this._groupsByName.clear(); 1.175 + this._orderedGroupElementsArray.length = 0; 1.176 + this._orderedMenuElementsArray.length = 0; 1.177 + this._itemsByElement.clear(); 1.178 + }, 1.179 + 1.180 + /** 1.181 + * Gets the currently selected child node in this container. 1.182 + * @return nsIDOMNode 1.183 + */ 1.184 + get selectedItem() { 1.185 + return this._selectedItem; 1.186 + }, 1.187 + 1.188 + /** 1.189 + * Sets the currently selected child node in this container. 1.190 + * @param nsIDOMNode aChild 1.191 + */ 1.192 + set selectedItem(aChild) { 1.193 + let menuArray = this._orderedMenuElementsArray; 1.194 + 1.195 + if (!aChild) { 1.196 + this._selectedItem = null; 1.197 + } 1.198 + for (let node of menuArray) { 1.199 + if (node == aChild) { 1.200 + this._getNodeForContents(node).classList.add("selected"); 1.201 + this._selectedItem = node; 1.202 + } else { 1.203 + this._getNodeForContents(node).classList.remove("selected"); 1.204 + } 1.205 + } 1.206 + }, 1.207 + 1.208 + /** 1.209 + * Ensures the specified element is visible. 1.210 + * 1.211 + * @param nsIDOMNode aElement 1.212 + * The element to make visible. 1.213 + */ 1.214 + ensureElementIsVisible: function(aElement) { 1.215 + if (!aElement) { 1.216 + return; 1.217 + } 1.218 + 1.219 + // Ensure the element is visible but not scrolled horizontally. 1.220 + let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject); 1.221 + boxObject.ensureElementIsVisible(aElement); 1.222 + boxObject.scrollBy(-this._list.clientWidth, 0); 1.223 + }, 1.224 + 1.225 + /** 1.226 + * Shows all the groups, even the ones with no visible children. 1.227 + */ 1.228 + showEmptyGroups: function() { 1.229 + for (let group of this._orderedGroupElementsArray) { 1.230 + group.hidden = false; 1.231 + } 1.232 + }, 1.233 + 1.234 + /** 1.235 + * Hides all the groups which have no visible children. 1.236 + */ 1.237 + hideEmptyGroups: function() { 1.238 + let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])"; 1.239 + 1.240 + for (let group of this._orderedGroupElementsArray) { 1.241 + group.hidden = group.querySelectorAll(visibleChildNodes).length == 0; 1.242 + } 1.243 + for (let menuItem of this._orderedMenuElementsArray) { 1.244 + menuItem.parentNode.hidden = menuItem.hidden; 1.245 + } 1.246 + }, 1.247 + 1.248 + /** 1.249 + * Adds a new attribute or changes an existing attribute on this container. 1.250 + * 1.251 + * @param string aName 1.252 + * The name of the attribute. 1.253 + * @param string aValue 1.254 + * The desired attribute value. 1.255 + */ 1.256 + setAttribute: function(aName, aValue) { 1.257 + this._parent.setAttribute(aName, aValue); 1.258 + 1.259 + if (aName == "emptyText") { 1.260 + this._textWhenEmpty = aValue; 1.261 + } 1.262 + }, 1.263 + 1.264 + /** 1.265 + * Removes an attribute on this container. 1.266 + * 1.267 + * @param string aName 1.268 + * The name of the attribute. 1.269 + */ 1.270 + removeAttribute: function(aName) { 1.271 + this._parent.removeAttribute(aName); 1.272 + 1.273 + if (aName == "emptyText") { 1.274 + this._removeEmptyText(); 1.275 + } 1.276 + }, 1.277 + 1.278 + /** 1.279 + * Set the checkbox state for the item associated with the given node. 1.280 + * 1.281 + * @param nsIDOMNode aNode 1.282 + * The dom node for an item we want to check. 1.283 + * @param boolean aCheckState 1.284 + * True to check, false to uncheck. 1.285 + */ 1.286 + checkItem: function(aNode, aCheckState) { 1.287 + const widgetItem = this._itemsByElement.get(aNode); 1.288 + if (!widgetItem) { 1.289 + throw new Error("No item for " + aNode); 1.290 + } 1.291 + widgetItem.check(aCheckState); 1.292 + }, 1.293 + 1.294 + /** 1.295 + * Sets the text displayed in this container when empty. 1.296 + * @param string aValue 1.297 + */ 1.298 + set _textWhenEmpty(aValue) { 1.299 + if (this._emptyTextNode) { 1.300 + this._emptyTextNode.setAttribute("value", aValue); 1.301 + } 1.302 + this._emptyTextValue = aValue; 1.303 + this._showEmptyText(); 1.304 + }, 1.305 + 1.306 + /** 1.307 + * Creates and appends a label signaling that this container is empty. 1.308 + */ 1.309 + _showEmptyText: function() { 1.310 + if (this._emptyTextNode || !this._emptyTextValue) { 1.311 + return; 1.312 + } 1.313 + let label = this.document.createElement("label"); 1.314 + label.className = "plain side-menu-widget-empty-text"; 1.315 + label.setAttribute("value", this._emptyTextValue); 1.316 + 1.317 + this._parent.insertBefore(label, this._list); 1.318 + this._emptyTextNode = label; 1.319 + }, 1.320 + 1.321 + /** 1.322 + * Removes the label representing a notice in this container. 1.323 + */ 1.324 + _removeEmptyText: function() { 1.325 + if (!this._emptyTextNode) { 1.326 + return; 1.327 + } 1.328 + 1.329 + this._parent.removeChild(this._emptyTextNode); 1.330 + this._emptyTextNode = null; 1.331 + }, 1.332 + 1.333 + /** 1.334 + * Gets a container representing a group for menu items. If the container 1.335 + * is not available yet, it is immediately created. 1.336 + * 1.337 + * @param string aName 1.338 + * The required group name. 1.339 + * @return SideMenuGroup 1.340 + * The newly created group. 1.341 + */ 1.342 + _getMenuGroupForName: function(aName) { 1.343 + let cachedGroup = this._groupsByName.get(aName); 1.344 + if (cachedGroup) { 1.345 + return cachedGroup; 1.346 + } 1.347 + 1.348 + let group = new SideMenuGroup(this, aName, { 1.349 + showCheckbox: this._showGroupCheckboxes 1.350 + }); 1.351 + 1.352 + this._groupsByName.set(aName, group); 1.353 + group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf(this.groupSortPredicate) : -1); 1.354 + 1.355 + return group; 1.356 + }, 1.357 + 1.358 + /** 1.359 + * Gets a menu item to be displayed inside a group. 1.360 + * @see SideMenuWidget.prototype._getMenuGroupForName 1.361 + * 1.362 + * @param SideMenuGroup aGroup 1.363 + * The group to contain the menu item. 1.364 + * @param nsIDOMNode aContents 1.365 + * The node displayed in the container. 1.366 + * @param object aAttachment [optional] 1.367 + * Some attached primitive/object. 1.368 + */ 1.369 + _getMenuItemForGroup: function(aGroup, aContents, aAttachment) { 1.370 + return new SideMenuItem(aGroup, aContents, aAttachment, { 1.371 + showArrow: this._showArrows, 1.372 + showCheckbox: this._showItemCheckboxes 1.373 + }); 1.374 + }, 1.375 + 1.376 + /** 1.377 + * Returns the .side-menu-widget-item node corresponding to a SideMenuItem. 1.378 + * To optimize the markup, some redundant elemenst are skipped when creating 1.379 + * these child items, in which case we need to be careful on which nodes 1.380 + * .selected class names are added, or which nodes are removed. 1.381 + * 1.382 + * @param nsIDOMNode aChild 1.383 + * An element which is the target node of a SideMenuItem. 1.384 + * @return nsIDOMNode 1.385 + * The wrapper node if there is one, or the same child otherwise. 1.386 + */ 1.387 + _getNodeForContents: function(aChild) { 1.388 + if (aChild.hasAttribute("merged-item-contents")) { 1.389 + return aChild; 1.390 + } else { 1.391 + return aChild.parentNode; 1.392 + } 1.393 + }, 1.394 + 1.395 + window: null, 1.396 + document: null, 1.397 + _showArrows: false, 1.398 + _showItemCheckboxes: false, 1.399 + _showGroupCheckboxes: false, 1.400 + _parent: null, 1.401 + _list: null, 1.402 + _selectedItem: null, 1.403 + _groupsByName: null, 1.404 + _orderedGroupElementsArray: null, 1.405 + _orderedMenuElementsArray: null, 1.406 + _itemsByElement: null, 1.407 + _emptyTextNode: null, 1.408 + _emptyTextValue: "" 1.409 +}; 1.410 + 1.411 +/** 1.412 + * A SideMenuGroup constructor for the BreadcrumbsWidget. 1.413 + * Represents a group which should contain SideMenuItems. 1.414 + * 1.415 + * @param SideMenuWidget aWidget 1.416 + * The widget to contain this menu item. 1.417 + * @param string aName 1.418 + * The string displayed in the container. 1.419 + * @param object aOptions [optional] 1.420 + * An object containing the following properties: 1.421 + * - showCheckbox: specifies if a checkbox should be displayed. 1.422 + */ 1.423 +function SideMenuGroup(aWidget, aName, aOptions={}) { 1.424 + this.document = aWidget.document; 1.425 + this.window = aWidget.window; 1.426 + this.ownerView = aWidget; 1.427 + this.identifier = aName; 1.428 + 1.429 + // Create an internal title and list container. 1.430 + if (aName) { 1.431 + let target = this._target = this.document.createElement("vbox"); 1.432 + target.className = "side-menu-widget-group"; 1.433 + target.setAttribute("name", aName); 1.434 + 1.435 + let list = this._list = this.document.createElement("vbox"); 1.436 + list.className = "side-menu-widget-group-list"; 1.437 + 1.438 + let title = this._title = this.document.createElement("hbox"); 1.439 + title.className = "side-menu-widget-group-title"; 1.440 + 1.441 + let name = this._name = this.document.createElement("label"); 1.442 + name.className = "plain name"; 1.443 + name.setAttribute("value", aName); 1.444 + name.setAttribute("crop", "end"); 1.445 + name.setAttribute("flex", "1"); 1.446 + 1.447 + // Show a checkbox before the content. 1.448 + if (aOptions.showCheckbox) { 1.449 + let checkbox = this._checkbox = makeCheckbox(title, { description: aName }); 1.450 + checkbox.className = "side-menu-widget-group-checkbox"; 1.451 + } 1.452 + 1.453 + title.appendChild(name); 1.454 + target.appendChild(title); 1.455 + target.appendChild(list); 1.456 + } 1.457 + // Skip a few redundant nodes when no title is shown. 1.458 + else { 1.459 + let target = this._target = this._list = this.document.createElement("vbox"); 1.460 + target.className = "side-menu-widget-group side-menu-widget-group-list"; 1.461 + target.setAttribute("merged-group-contents", ""); 1.462 + } 1.463 +} 1.464 + 1.465 +SideMenuGroup.prototype = { 1.466 + get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray, 1.467 + get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray, 1.468 + get _itemsByElement() { return this.ownerView._itemsByElement; }, 1.469 + 1.470 + /** 1.471 + * Inserts this group in the parent container at the specified index. 1.472 + * 1.473 + * @param number aIndex 1.474 + * The position in the container intended for this group. 1.475 + */ 1.476 + insertSelfAt: function(aIndex) { 1.477 + let ownerList = this.ownerView._list; 1.478 + let groupsArray = this._orderedGroupElementsArray; 1.479 + 1.480 + if (aIndex >= 0) { 1.481 + ownerList.insertBefore(this._target, groupsArray[aIndex]); 1.482 + groupsArray.splice(aIndex, 0, this._target); 1.483 + } else { 1.484 + ownerList.appendChild(this._target); 1.485 + groupsArray.push(this._target); 1.486 + } 1.487 + }, 1.488 + 1.489 + /** 1.490 + * Finds the expected index of this group based on its name. 1.491 + * 1.492 + * @return number 1.493 + * The expected index. 1.494 + */ 1.495 + findExpectedIndexForSelf: function(sortPredicate) { 1.496 + let identifier = this.identifier; 1.497 + let groupsArray = this._orderedGroupElementsArray; 1.498 + 1.499 + for (let group of groupsArray) { 1.500 + let name = group.getAttribute("name"); 1.501 + if (sortPredicate(name, identifier) > 0 && // Insertion sort at its best :) 1.502 + !name.contains(identifier)) { // Least significant group should be last. 1.503 + return groupsArray.indexOf(group); 1.504 + } 1.505 + } 1.506 + return -1; 1.507 + }, 1.508 + 1.509 + window: null, 1.510 + document: null, 1.511 + ownerView: null, 1.512 + identifier: "", 1.513 + _target: null, 1.514 + _checkbox: null, 1.515 + _title: null, 1.516 + _name: null, 1.517 + _list: null 1.518 +}; 1.519 + 1.520 +/** 1.521 + * A SideMenuItem constructor for the BreadcrumbsWidget. 1.522 + * 1.523 + * @param SideMenuGroup aGroup 1.524 + * The group to contain this menu item. 1.525 + * @param nsIDOMNode aContents 1.526 + * The node displayed in the container. 1.527 + * @param object aAttachment [optional] 1.528 + * The attachment object. 1.529 + * @param object aOptions [optional] 1.530 + * An object containing the following properties: 1.531 + * - showArrow: specifies if a horizontal arrow should be displayed. 1.532 + * - showCheckbox: specifies if a checkbox should be displayed. 1.533 + */ 1.534 +function SideMenuItem(aGroup, aContents, aAttachment={}, aOptions={}) { 1.535 + this.document = aGroup.document; 1.536 + this.window = aGroup.window; 1.537 + this.ownerView = aGroup; 1.538 + 1.539 + if (aOptions.showArrow || aOptions.showCheckbox) { 1.540 + let container = this._container = this.document.createElement("hbox"); 1.541 + container.className = "side-menu-widget-item"; 1.542 + 1.543 + let target = this._target = this.document.createElement("vbox"); 1.544 + target.className = "side-menu-widget-item-contents"; 1.545 + 1.546 + // Show a checkbox before the content. 1.547 + if (aOptions.showCheckbox) { 1.548 + let checkbox = this._checkbox = makeCheckbox(container, aAttachment); 1.549 + checkbox.className = "side-menu-widget-item-checkbox"; 1.550 + } 1.551 + 1.552 + container.appendChild(target); 1.553 + 1.554 + // Show a horizontal arrow towards the content. 1.555 + if (aOptions.showArrow) { 1.556 + let arrow = this._arrow = this.document.createElement("hbox"); 1.557 + arrow.className = "side-menu-widget-item-arrow"; 1.558 + container.appendChild(arrow); 1.559 + } 1.560 + } 1.561 + // Skip a few redundant nodes when no horizontal arrow or checkbox is shown. 1.562 + else { 1.563 + let target = this._target = this._container = this.document.createElement("hbox"); 1.564 + target.className = "side-menu-widget-item side-menu-widget-item-contents"; 1.565 + target.setAttribute("merged-item-contents", ""); 1.566 + } 1.567 + 1.568 + this._target.setAttribute("flex", "1"); 1.569 + this.contents = aContents; 1.570 +} 1.571 + 1.572 +SideMenuItem.prototype = { 1.573 + get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray, 1.574 + get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray, 1.575 + get _itemsByElement() { return this.ownerView._itemsByElement; }, 1.576 + 1.577 + /** 1.578 + * Inserts this item in the parent group at the specified index. 1.579 + * 1.580 + * @param number aIndex 1.581 + * The position in the container intended for this item. 1.582 + * @return nsIDOMNode 1.583 + * The element associated with the displayed item. 1.584 + */ 1.585 + insertSelfAt: function(aIndex) { 1.586 + let ownerList = this.ownerView._list; 1.587 + let menuArray = this._orderedMenuElementsArray; 1.588 + 1.589 + if (aIndex >= 0) { 1.590 + ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]); 1.591 + menuArray.splice(aIndex, 0, this._target); 1.592 + } else { 1.593 + ownerList.appendChild(this._container); 1.594 + menuArray.push(this._target); 1.595 + } 1.596 + this._itemsByElement.set(this._target, this); 1.597 + 1.598 + return this._target; 1.599 + }, 1.600 + 1.601 + /** 1.602 + * Check or uncheck the checkbox associated with this item. 1.603 + * 1.604 + * @param boolean aCheckState 1.605 + * True to check, false to uncheck. 1.606 + */ 1.607 + check: function(aCheckState) { 1.608 + if (!this._checkbox) { 1.609 + throw new Error("Cannot check items that do not have checkboxes."); 1.610 + } 1.611 + // Don't set or remove the "checked" attribute, assign the property instead. 1.612 + // Otherwise, the "CheckboxStateChange" event will not be fired. XUL!! 1.613 + this._checkbox.checked = !!aCheckState; 1.614 + }, 1.615 + 1.616 + /** 1.617 + * Sets the contents displayed in this item's view. 1.618 + * 1.619 + * @param string | nsIDOMNode aContents 1.620 + * The string or node displayed in the container. 1.621 + */ 1.622 + set contents(aContents) { 1.623 + // If there are already some contents displayed, replace them. 1.624 + if (this._target.hasChildNodes()) { 1.625 + this._target.replaceChild(aContents, this._target.firstChild); 1.626 + return; 1.627 + } 1.628 + // These are the first contents ever displayed. 1.629 + this._target.appendChild(aContents); 1.630 + }, 1.631 + 1.632 + window: null, 1.633 + document: null, 1.634 + ownerView: null, 1.635 + _target: null, 1.636 + _container: null, 1.637 + _checkbox: null, 1.638 + _arrow: null 1.639 +}; 1.640 + 1.641 +/** 1.642 + * Creates a checkbox to a specified parent node. Emits a "check" event 1.643 + * whenever the checkbox is checked or unchecked by the user. 1.644 + * 1.645 + * @param nsIDOMNode aParentNode 1.646 + * The parent node to contain this checkbox. 1.647 + * @param object aOptions 1.648 + * An object containing some or all of the following properties: 1.649 + * - description: defaults to "item" if unspecified 1.650 + * - checkboxState: true for checked, false for unchecked 1.651 + * - checkboxTooltip: the tooltip text of the checkbox 1.652 + */ 1.653 +function makeCheckbox(aParentNode, aOptions) { 1.654 + let checkbox = aParentNode.ownerDocument.createElement("checkbox"); 1.655 + checkbox.setAttribute("tooltiptext", aOptions.checkboxTooltip); 1.656 + 1.657 + if (aOptions.checkboxState) { 1.658 + checkbox.setAttribute("checked", true); 1.659 + } else { 1.660 + checkbox.removeAttribute("checked"); 1.661 + } 1.662 + 1.663 + // Stop the toggling of the checkbox from selecting the list item. 1.664 + checkbox.addEventListener("mousedown", e => { 1.665 + e.stopPropagation(); 1.666 + }, false); 1.667 + 1.668 + // Emit an event from the checkbox when it is toggled. Don't listen for the 1.669 + // "command" event! It won't fire for programmatic changes. XUL!! 1.670 + checkbox.addEventListener("CheckboxStateChange", e => { 1.671 + ViewHelpers.dispatchEvent(checkbox, "check", { 1.672 + description: aOptions.description || "item", 1.673 + checked: checkbox.checked 1.674 + }); 1.675 + }, false); 1.676 + 1.677 + aParentNode.appendChild(checkbox); 1.678 + return checkbox; 1.679 +}