michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["SideMenuWidget"]; michael@0: michael@0: /** michael@0: * A simple side menu, with the ability of grouping menu items. michael@0: * michael@0: * Note: this widget should be used in tandem with the WidgetMethods in michael@0: * ViewHelpers.jsm. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The element associated with the widget. michael@0: * @param Object aOptions michael@0: * - showArrows: specifies if items should display horizontal arrows. michael@0: * - showItemCheckboxes: specifies if items should display checkboxes. michael@0: * - showGroupCheckboxes: specifies if groups should display checkboxes. michael@0: */ michael@0: this.SideMenuWidget = function SideMenuWidget(aNode, aOptions={}) { michael@0: this.document = aNode.ownerDocument; michael@0: this.window = this.document.defaultView; michael@0: this._parent = aNode; michael@0: michael@0: let { showArrows, showItemCheckboxes, showGroupCheckboxes } = aOptions; michael@0: this._showArrows = showArrows || false; michael@0: this._showItemCheckboxes = showItemCheckboxes || false; michael@0: this._showGroupCheckboxes = showGroupCheckboxes || false; michael@0: michael@0: // Create an internal scrollbox container. michael@0: this._list = this.document.createElement("scrollbox"); michael@0: this._list.className = "side-menu-widget-container theme-sidebar"; michael@0: this._list.setAttribute("flex", "1"); michael@0: this._list.setAttribute("orient", "vertical"); michael@0: this._list.setAttribute("with-arrows", this._showArrows); michael@0: this._list.setAttribute("with-item-checkboxes", this._showItemCheckboxes); michael@0: this._list.setAttribute("with-group-checkboxes", this._showGroupCheckboxes); michael@0: this._list.setAttribute("tabindex", "0"); michael@0: this._list.addEventListener("keypress", e => this.emit("keyPress", e), false); michael@0: this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false); michael@0: this._parent.appendChild(this._list); michael@0: michael@0: // Menu items can optionally be grouped. michael@0: this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings. michael@0: this._orderedGroupElementsArray = []; michael@0: this._orderedMenuElementsArray = []; michael@0: this._itemsByElement = new Map(); michael@0: michael@0: // This widget emits events that can be handled in a MenuContainer. michael@0: EventEmitter.decorate(this); michael@0: michael@0: // Delegate some of the associated node's methods to satisfy the interface michael@0: // required by MenuContainer instances. michael@0: ViewHelpers.delegateWidgetAttributeMethods(this, aNode); michael@0: ViewHelpers.delegateWidgetEventMethods(this, aNode); michael@0: }; michael@0: michael@0: SideMenuWidget.prototype = { michael@0: /** michael@0: * Specifies if groups in this container should be sorted. michael@0: */ michael@0: sortedGroups: true, michael@0: michael@0: /** michael@0: * The comparator used to sort groups. michael@0: */ michael@0: groupSortPredicate: function(a, b) a.localeCompare(b), michael@0: michael@0: /** michael@0: * Specifies that the container viewport should be "stuck" to the michael@0: * bottom. That is, the container is automatically scrolled down to michael@0: * keep appended items visible, but only when the scroll position is michael@0: * already at the bottom. michael@0: */ michael@0: autoscrollWithAppendedItems: false, michael@0: michael@0: /** michael@0: * Inserts an item in this container at the specified index, optionally michael@0: * grouping by name. michael@0: * michael@0: * @param number aIndex michael@0: * The position in the container intended for this item. michael@0: * @param nsIDOMNode aContents michael@0: * The node displayed in the container. michael@0: * @param object aAttachment [optional] michael@0: * Some attached primitive/object. Custom options supported: michael@0: * - group: a string specifying the group to place this item into michael@0: * - checkboxState: the checked state of the checkbox, if shown michael@0: * - checkboxTooltip: the tooltip text for the checkbox, if shown michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: insertItemAt: function(aIndex, aContents, aAttachment={}) { michael@0: // Maintaining scroll position at the bottom when a new item is inserted michael@0: // depends on several factors (the order of testing is important to avoid michael@0: // needlessly expensive operations that may cause reflows): michael@0: let maintainScrollAtBottom = michael@0: // 1. The behavior should be enabled, michael@0: this.autoscrollWithAppendedItems && michael@0: // 2. There shouldn't currently be any selected item in the list. michael@0: !this._selectedItem && michael@0: // 3. The new item should be appended at the end of the list. michael@0: (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) && michael@0: // 4. The list should already be scrolled at the bottom. michael@0: (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight); michael@0: michael@0: let group = this._getMenuGroupForName(aAttachment.group); michael@0: let item = this._getMenuItemForGroup(group, aContents, aAttachment); michael@0: let element = item.insertSelfAt(aIndex); michael@0: michael@0: if (maintainScrollAtBottom) { michael@0: this._list.scrollTop = this._list.scrollHeight; michael@0: } michael@0: michael@0: return element; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the child node in this container situated at the specified index. michael@0: * michael@0: * @param number aIndex michael@0: * The position in the container intended for this item. michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: getItemAtIndex: function(aIndex) { michael@0: return this._orderedMenuElementsArray[aIndex]; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the specified child node from this container. michael@0: * michael@0: * @param nsIDOMNode aChild michael@0: * The element associated with the displayed item. michael@0: */ michael@0: removeChild: function(aChild) { michael@0: this._getNodeForContents(aChild).remove(); michael@0: michael@0: this._orderedMenuElementsArray.splice( michael@0: this._orderedMenuElementsArray.indexOf(aChild), 1); michael@0: michael@0: this._itemsByElement.delete(aChild); michael@0: michael@0: if (this._selectedItem == aChild) { michael@0: this._selectedItem = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes all of the child nodes from this container. michael@0: */ michael@0: removeAllItems: function() { michael@0: let parent = this._parent; michael@0: let list = this._list; michael@0: michael@0: while (list.hasChildNodes()) { michael@0: list.firstChild.remove(); michael@0: } michael@0: michael@0: this._selectedItem = null; michael@0: michael@0: this._groupsByName.clear(); michael@0: this._orderedGroupElementsArray.length = 0; michael@0: this._orderedMenuElementsArray.length = 0; michael@0: this._itemsByElement.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the currently selected child node in this container. michael@0: * @return nsIDOMNode michael@0: */ michael@0: get selectedItem() { michael@0: return this._selectedItem; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the currently selected child node in this container. michael@0: * @param nsIDOMNode aChild michael@0: */ michael@0: set selectedItem(aChild) { michael@0: let menuArray = this._orderedMenuElementsArray; michael@0: michael@0: if (!aChild) { michael@0: this._selectedItem = null; michael@0: } michael@0: for (let node of menuArray) { michael@0: if (node == aChild) { michael@0: this._getNodeForContents(node).classList.add("selected"); michael@0: this._selectedItem = node; michael@0: } else { michael@0: this._getNodeForContents(node).classList.remove("selected"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures the specified element is visible. michael@0: * michael@0: * @param nsIDOMNode aElement michael@0: * The element to make visible. michael@0: */ michael@0: ensureElementIsVisible: function(aElement) { michael@0: if (!aElement) { michael@0: return; michael@0: } michael@0: michael@0: // Ensure the element is visible but not scrolled horizontally. michael@0: let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject); michael@0: boxObject.ensureElementIsVisible(aElement); michael@0: boxObject.scrollBy(-this._list.clientWidth, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Shows all the groups, even the ones with no visible children. michael@0: */ michael@0: showEmptyGroups: function() { michael@0: for (let group of this._orderedGroupElementsArray) { michael@0: group.hidden = false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Hides all the groups which have no visible children. michael@0: */ michael@0: hideEmptyGroups: function() { michael@0: let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])"; michael@0: michael@0: for (let group of this._orderedGroupElementsArray) { michael@0: group.hidden = group.querySelectorAll(visibleChildNodes).length == 0; michael@0: } michael@0: for (let menuItem of this._orderedMenuElementsArray) { michael@0: menuItem.parentNode.hidden = menuItem.hidden; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds a new attribute or changes an existing attribute on this container. michael@0: * michael@0: * @param string aName michael@0: * The name of the attribute. michael@0: * @param string aValue michael@0: * The desired attribute value. michael@0: */ michael@0: setAttribute: function(aName, aValue) { michael@0: this._parent.setAttribute(aName, aValue); michael@0: michael@0: if (aName == "emptyText") { michael@0: this._textWhenEmpty = aValue; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes an attribute on this container. michael@0: * michael@0: * @param string aName michael@0: * The name of the attribute. michael@0: */ michael@0: removeAttribute: function(aName) { michael@0: this._parent.removeAttribute(aName); michael@0: michael@0: if (aName == "emptyText") { michael@0: this._removeEmptyText(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Set the checkbox state for the item associated with the given node. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The dom node for an item we want to check. michael@0: * @param boolean aCheckState michael@0: * True to check, false to uncheck. michael@0: */ michael@0: checkItem: function(aNode, aCheckState) { michael@0: const widgetItem = this._itemsByElement.get(aNode); michael@0: if (!widgetItem) { michael@0: throw new Error("No item for " + aNode); michael@0: } michael@0: widgetItem.check(aCheckState); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the text displayed in this container when empty. michael@0: * @param string aValue michael@0: */ michael@0: set _textWhenEmpty(aValue) { michael@0: if (this._emptyTextNode) { michael@0: this._emptyTextNode.setAttribute("value", aValue); michael@0: } michael@0: this._emptyTextValue = aValue; michael@0: this._showEmptyText(); michael@0: }, michael@0: michael@0: /** michael@0: * Creates and appends a label signaling that this container is empty. michael@0: */ michael@0: _showEmptyText: function() { michael@0: if (this._emptyTextNode || !this._emptyTextValue) { michael@0: return; michael@0: } michael@0: let label = this.document.createElement("label"); michael@0: label.className = "plain side-menu-widget-empty-text"; michael@0: label.setAttribute("value", this._emptyTextValue); michael@0: michael@0: this._parent.insertBefore(label, this._list); michael@0: this._emptyTextNode = label; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the label representing a notice in this container. michael@0: */ michael@0: _removeEmptyText: function() { michael@0: if (!this._emptyTextNode) { michael@0: return; michael@0: } michael@0: michael@0: this._parent.removeChild(this._emptyTextNode); michael@0: this._emptyTextNode = null; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a container representing a group for menu items. If the container michael@0: * is not available yet, it is immediately created. michael@0: * michael@0: * @param string aName michael@0: * The required group name. michael@0: * @return SideMenuGroup michael@0: * The newly created group. michael@0: */ michael@0: _getMenuGroupForName: function(aName) { michael@0: let cachedGroup = this._groupsByName.get(aName); michael@0: if (cachedGroup) { michael@0: return cachedGroup; michael@0: } michael@0: michael@0: let group = new SideMenuGroup(this, aName, { michael@0: showCheckbox: this._showGroupCheckboxes michael@0: }); michael@0: michael@0: this._groupsByName.set(aName, group); michael@0: group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf(this.groupSortPredicate) : -1); michael@0: michael@0: return group; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a menu item to be displayed inside a group. michael@0: * @see SideMenuWidget.prototype._getMenuGroupForName michael@0: * michael@0: * @param SideMenuGroup aGroup michael@0: * The group to contain the menu item. michael@0: * @param nsIDOMNode aContents michael@0: * The node displayed in the container. michael@0: * @param object aAttachment [optional] michael@0: * Some attached primitive/object. michael@0: */ michael@0: _getMenuItemForGroup: function(aGroup, aContents, aAttachment) { michael@0: return new SideMenuItem(aGroup, aContents, aAttachment, { michael@0: showArrow: this._showArrows, michael@0: showCheckbox: this._showItemCheckboxes michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the .side-menu-widget-item node corresponding to a SideMenuItem. michael@0: * To optimize the markup, some redundant elemenst are skipped when creating michael@0: * these child items, in which case we need to be careful on which nodes michael@0: * .selected class names are added, or which nodes are removed. michael@0: * michael@0: * @param nsIDOMNode aChild michael@0: * An element which is the target node of a SideMenuItem. michael@0: * @return nsIDOMNode michael@0: * The wrapper node if there is one, or the same child otherwise. michael@0: */ michael@0: _getNodeForContents: function(aChild) { michael@0: if (aChild.hasAttribute("merged-item-contents")) { michael@0: return aChild; michael@0: } else { michael@0: return aChild.parentNode; michael@0: } michael@0: }, michael@0: michael@0: window: null, michael@0: document: null, michael@0: _showArrows: false, michael@0: _showItemCheckboxes: false, michael@0: _showGroupCheckboxes: false, michael@0: _parent: null, michael@0: _list: null, michael@0: _selectedItem: null, michael@0: _groupsByName: null, michael@0: _orderedGroupElementsArray: null, michael@0: _orderedMenuElementsArray: null, michael@0: _itemsByElement: null, michael@0: _emptyTextNode: null, michael@0: _emptyTextValue: "" michael@0: }; michael@0: michael@0: /** michael@0: * A SideMenuGroup constructor for the BreadcrumbsWidget. michael@0: * Represents a group which should contain SideMenuItems. michael@0: * michael@0: * @param SideMenuWidget aWidget michael@0: * The widget to contain this menu item. michael@0: * @param string aName michael@0: * The string displayed in the container. michael@0: * @param object aOptions [optional] michael@0: * An object containing the following properties: michael@0: * - showCheckbox: specifies if a checkbox should be displayed. michael@0: */ michael@0: function SideMenuGroup(aWidget, aName, aOptions={}) { michael@0: this.document = aWidget.document; michael@0: this.window = aWidget.window; michael@0: this.ownerView = aWidget; michael@0: this.identifier = aName; michael@0: michael@0: // Create an internal title and list container. michael@0: if (aName) { michael@0: let target = this._target = this.document.createElement("vbox"); michael@0: target.className = "side-menu-widget-group"; michael@0: target.setAttribute("name", aName); michael@0: michael@0: let list = this._list = this.document.createElement("vbox"); michael@0: list.className = "side-menu-widget-group-list"; michael@0: michael@0: let title = this._title = this.document.createElement("hbox"); michael@0: title.className = "side-menu-widget-group-title"; michael@0: michael@0: let name = this._name = this.document.createElement("label"); michael@0: name.className = "plain name"; michael@0: name.setAttribute("value", aName); michael@0: name.setAttribute("crop", "end"); michael@0: name.setAttribute("flex", "1"); michael@0: michael@0: // Show a checkbox before the content. michael@0: if (aOptions.showCheckbox) { michael@0: let checkbox = this._checkbox = makeCheckbox(title, { description: aName }); michael@0: checkbox.className = "side-menu-widget-group-checkbox"; michael@0: } michael@0: michael@0: title.appendChild(name); michael@0: target.appendChild(title); michael@0: target.appendChild(list); michael@0: } michael@0: // Skip a few redundant nodes when no title is shown. michael@0: else { michael@0: let target = this._target = this._list = this.document.createElement("vbox"); michael@0: target.className = "side-menu-widget-group side-menu-widget-group-list"; michael@0: target.setAttribute("merged-group-contents", ""); michael@0: } michael@0: } michael@0: michael@0: SideMenuGroup.prototype = { michael@0: get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray, michael@0: get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray, michael@0: get _itemsByElement() { return this.ownerView._itemsByElement; }, michael@0: michael@0: /** michael@0: * Inserts this group in the parent container at the specified index. michael@0: * michael@0: * @param number aIndex michael@0: * The position in the container intended for this group. michael@0: */ michael@0: insertSelfAt: function(aIndex) { michael@0: let ownerList = this.ownerView._list; michael@0: let groupsArray = this._orderedGroupElementsArray; michael@0: michael@0: if (aIndex >= 0) { michael@0: ownerList.insertBefore(this._target, groupsArray[aIndex]); michael@0: groupsArray.splice(aIndex, 0, this._target); michael@0: } else { michael@0: ownerList.appendChild(this._target); michael@0: groupsArray.push(this._target); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Finds the expected index of this group based on its name. michael@0: * michael@0: * @return number michael@0: * The expected index. michael@0: */ michael@0: findExpectedIndexForSelf: function(sortPredicate) { michael@0: let identifier = this.identifier; michael@0: let groupsArray = this._orderedGroupElementsArray; michael@0: michael@0: for (let group of groupsArray) { michael@0: let name = group.getAttribute("name"); michael@0: if (sortPredicate(name, identifier) > 0 && // Insertion sort at its best :) michael@0: !name.contains(identifier)) { // Least significant group should be last. michael@0: return groupsArray.indexOf(group); michael@0: } michael@0: } michael@0: return -1; michael@0: }, michael@0: michael@0: window: null, michael@0: document: null, michael@0: ownerView: null, michael@0: identifier: "", michael@0: _target: null, michael@0: _checkbox: null, michael@0: _title: null, michael@0: _name: null, michael@0: _list: null michael@0: }; michael@0: michael@0: /** michael@0: * A SideMenuItem constructor for the BreadcrumbsWidget. michael@0: * michael@0: * @param SideMenuGroup aGroup michael@0: * The group to contain this menu item. michael@0: * @param nsIDOMNode aContents michael@0: * The node displayed in the container. michael@0: * @param object aAttachment [optional] michael@0: * The attachment object. michael@0: * @param object aOptions [optional] michael@0: * An object containing the following properties: michael@0: * - showArrow: specifies if a horizontal arrow should be displayed. michael@0: * - showCheckbox: specifies if a checkbox should be displayed. michael@0: */ michael@0: function SideMenuItem(aGroup, aContents, aAttachment={}, aOptions={}) { michael@0: this.document = aGroup.document; michael@0: this.window = aGroup.window; michael@0: this.ownerView = aGroup; michael@0: michael@0: if (aOptions.showArrow || aOptions.showCheckbox) { michael@0: let container = this._container = this.document.createElement("hbox"); michael@0: container.className = "side-menu-widget-item"; michael@0: michael@0: let target = this._target = this.document.createElement("vbox"); michael@0: target.className = "side-menu-widget-item-contents"; michael@0: michael@0: // Show a checkbox before the content. michael@0: if (aOptions.showCheckbox) { michael@0: let checkbox = this._checkbox = makeCheckbox(container, aAttachment); michael@0: checkbox.className = "side-menu-widget-item-checkbox"; michael@0: } michael@0: michael@0: container.appendChild(target); michael@0: michael@0: // Show a horizontal arrow towards the content. michael@0: if (aOptions.showArrow) { michael@0: let arrow = this._arrow = this.document.createElement("hbox"); michael@0: arrow.className = "side-menu-widget-item-arrow"; michael@0: container.appendChild(arrow); michael@0: } michael@0: } michael@0: // Skip a few redundant nodes when no horizontal arrow or checkbox is shown. michael@0: else { michael@0: let target = this._target = this._container = this.document.createElement("hbox"); michael@0: target.className = "side-menu-widget-item side-menu-widget-item-contents"; michael@0: target.setAttribute("merged-item-contents", ""); michael@0: } michael@0: michael@0: this._target.setAttribute("flex", "1"); michael@0: this.contents = aContents; michael@0: } michael@0: michael@0: SideMenuItem.prototype = { michael@0: get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray, michael@0: get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray, michael@0: get _itemsByElement() { return this.ownerView._itemsByElement; }, michael@0: michael@0: /** michael@0: * Inserts this item in the parent group at the specified index. michael@0: * michael@0: * @param number aIndex michael@0: * The position in the container intended for this item. michael@0: * @return nsIDOMNode michael@0: * The element associated with the displayed item. michael@0: */ michael@0: insertSelfAt: function(aIndex) { michael@0: let ownerList = this.ownerView._list; michael@0: let menuArray = this._orderedMenuElementsArray; michael@0: michael@0: if (aIndex >= 0) { michael@0: ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]); michael@0: menuArray.splice(aIndex, 0, this._target); michael@0: } else { michael@0: ownerList.appendChild(this._container); michael@0: menuArray.push(this._target); michael@0: } michael@0: this._itemsByElement.set(this._target, this); michael@0: michael@0: return this._target; michael@0: }, michael@0: michael@0: /** michael@0: * Check or uncheck the checkbox associated with this item. michael@0: * michael@0: * @param boolean aCheckState michael@0: * True to check, false to uncheck. michael@0: */ michael@0: check: function(aCheckState) { michael@0: if (!this._checkbox) { michael@0: throw new Error("Cannot check items that do not have checkboxes."); michael@0: } michael@0: // Don't set or remove the "checked" attribute, assign the property instead. michael@0: // Otherwise, the "CheckboxStateChange" event will not be fired. XUL!! michael@0: this._checkbox.checked = !!aCheckState; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the contents displayed in this item's view. michael@0: * michael@0: * @param string | nsIDOMNode aContents michael@0: * The string or node displayed in the container. michael@0: */ michael@0: set contents(aContents) { michael@0: // If there are already some contents displayed, replace them. michael@0: if (this._target.hasChildNodes()) { michael@0: this._target.replaceChild(aContents, this._target.firstChild); michael@0: return; michael@0: } michael@0: // These are the first contents ever displayed. michael@0: this._target.appendChild(aContents); michael@0: }, michael@0: michael@0: window: null, michael@0: document: null, michael@0: ownerView: null, michael@0: _target: null, michael@0: _container: null, michael@0: _checkbox: null, michael@0: _arrow: null michael@0: }; michael@0: michael@0: /** michael@0: * Creates a checkbox to a specified parent node. Emits a "check" event michael@0: * whenever the checkbox is checked or unchecked by the user. michael@0: * michael@0: * @param nsIDOMNode aParentNode michael@0: * The parent node to contain this checkbox. michael@0: * @param object aOptions michael@0: * An object containing some or all of the following properties: michael@0: * - description: defaults to "item" if unspecified michael@0: * - checkboxState: true for checked, false for unchecked michael@0: * - checkboxTooltip: the tooltip text of the checkbox michael@0: */ michael@0: function makeCheckbox(aParentNode, aOptions) { michael@0: let checkbox = aParentNode.ownerDocument.createElement("checkbox"); michael@0: checkbox.setAttribute("tooltiptext", aOptions.checkboxTooltip); michael@0: michael@0: if (aOptions.checkboxState) { michael@0: checkbox.setAttribute("checked", true); michael@0: } else { michael@0: checkbox.removeAttribute("checked"); michael@0: } michael@0: michael@0: // Stop the toggling of the checkbox from selecting the list item. michael@0: checkbox.addEventListener("mousedown", e => { michael@0: e.stopPropagation(); michael@0: }, false); michael@0: michael@0: // Emit an event from the checkbox when it is toggled. Don't listen for the michael@0: // "command" event! It won't fire for programmatic changes. XUL!! michael@0: checkbox.addEventListener("CheckboxStateChange", e => { michael@0: ViewHelpers.dispatchEvent(checkbox, "check", { michael@0: description: aOptions.description || "item", michael@0: checked: checkbox.checked michael@0: }); michael@0: }, false); michael@0: michael@0: aParentNode.appendChild(checkbox); michael@0: return checkbox; michael@0: }