browser/devtools/shared/widgets/SideMenuWidget.jsm

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6 "use strict";
michael@0 7
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cu = Components.utils;
michael@0 10
michael@0 11 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 12 Cu.import("resource://gre/modules/devtools/event-emitter.js");
michael@0 13
michael@0 14 this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
michael@0 15
michael@0 16 /**
michael@0 17 * A simple side menu, with the ability of grouping menu items.
michael@0 18 *
michael@0 19 * Note: this widget should be used in tandem with the WidgetMethods in
michael@0 20 * ViewHelpers.jsm.
michael@0 21 *
michael@0 22 * @param nsIDOMNode aNode
michael@0 23 * The element associated with the widget.
michael@0 24 * @param Object aOptions
michael@0 25 * - showArrows: specifies if items should display horizontal arrows.
michael@0 26 * - showItemCheckboxes: specifies if items should display checkboxes.
michael@0 27 * - showGroupCheckboxes: specifies if groups should display checkboxes.
michael@0 28 */
michael@0 29 this.SideMenuWidget = function SideMenuWidget(aNode, aOptions={}) {
michael@0 30 this.document = aNode.ownerDocument;
michael@0 31 this.window = this.document.defaultView;
michael@0 32 this._parent = aNode;
michael@0 33
michael@0 34 let { showArrows, showItemCheckboxes, showGroupCheckboxes } = aOptions;
michael@0 35 this._showArrows = showArrows || false;
michael@0 36 this._showItemCheckboxes = showItemCheckboxes || false;
michael@0 37 this._showGroupCheckboxes = showGroupCheckboxes || false;
michael@0 38
michael@0 39 // Create an internal scrollbox container.
michael@0 40 this._list = this.document.createElement("scrollbox");
michael@0 41 this._list.className = "side-menu-widget-container theme-sidebar";
michael@0 42 this._list.setAttribute("flex", "1");
michael@0 43 this._list.setAttribute("orient", "vertical");
michael@0 44 this._list.setAttribute("with-arrows", this._showArrows);
michael@0 45 this._list.setAttribute("with-item-checkboxes", this._showItemCheckboxes);
michael@0 46 this._list.setAttribute("with-group-checkboxes", this._showGroupCheckboxes);
michael@0 47 this._list.setAttribute("tabindex", "0");
michael@0 48 this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
michael@0 49 this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
michael@0 50 this._parent.appendChild(this._list);
michael@0 51
michael@0 52 // Menu items can optionally be grouped.
michael@0 53 this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
michael@0 54 this._orderedGroupElementsArray = [];
michael@0 55 this._orderedMenuElementsArray = [];
michael@0 56 this._itemsByElement = new Map();
michael@0 57
michael@0 58 // This widget emits events that can be handled in a MenuContainer.
michael@0 59 EventEmitter.decorate(this);
michael@0 60
michael@0 61 // Delegate some of the associated node's methods to satisfy the interface
michael@0 62 // required by MenuContainer instances.
michael@0 63 ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
michael@0 64 ViewHelpers.delegateWidgetEventMethods(this, aNode);
michael@0 65 };
michael@0 66
michael@0 67 SideMenuWidget.prototype = {
michael@0 68 /**
michael@0 69 * Specifies if groups in this container should be sorted.
michael@0 70 */
michael@0 71 sortedGroups: true,
michael@0 72
michael@0 73 /**
michael@0 74 * The comparator used to sort groups.
michael@0 75 */
michael@0 76 groupSortPredicate: function(a, b) a.localeCompare(b),
michael@0 77
michael@0 78 /**
michael@0 79 * Specifies that the container viewport should be "stuck" to the
michael@0 80 * bottom. That is, the container is automatically scrolled down to
michael@0 81 * keep appended items visible, but only when the scroll position is
michael@0 82 * already at the bottom.
michael@0 83 */
michael@0 84 autoscrollWithAppendedItems: false,
michael@0 85
michael@0 86 /**
michael@0 87 * Inserts an item in this container at the specified index, optionally
michael@0 88 * grouping by name.
michael@0 89 *
michael@0 90 * @param number aIndex
michael@0 91 * The position in the container intended for this item.
michael@0 92 * @param nsIDOMNode aContents
michael@0 93 * The node displayed in the container.
michael@0 94 * @param object aAttachment [optional]
michael@0 95 * Some attached primitive/object. Custom options supported:
michael@0 96 * - group: a string specifying the group to place this item into
michael@0 97 * - checkboxState: the checked state of the checkbox, if shown
michael@0 98 * - checkboxTooltip: the tooltip text for the checkbox, if shown
michael@0 99 * @return nsIDOMNode
michael@0 100 * The element associated with the displayed item.
michael@0 101 */
michael@0 102 insertItemAt: function(aIndex, aContents, aAttachment={}) {
michael@0 103 // Maintaining scroll position at the bottom when a new item is inserted
michael@0 104 // depends on several factors (the order of testing is important to avoid
michael@0 105 // needlessly expensive operations that may cause reflows):
michael@0 106 let maintainScrollAtBottom =
michael@0 107 // 1. The behavior should be enabled,
michael@0 108 this.autoscrollWithAppendedItems &&
michael@0 109 // 2. There shouldn't currently be any selected item in the list.
michael@0 110 !this._selectedItem &&
michael@0 111 // 3. The new item should be appended at the end of the list.
michael@0 112 (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) &&
michael@0 113 // 4. The list should already be scrolled at the bottom.
michael@0 114 (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight);
michael@0 115
michael@0 116 let group = this._getMenuGroupForName(aAttachment.group);
michael@0 117 let item = this._getMenuItemForGroup(group, aContents, aAttachment);
michael@0 118 let element = item.insertSelfAt(aIndex);
michael@0 119
michael@0 120 if (maintainScrollAtBottom) {
michael@0 121 this._list.scrollTop = this._list.scrollHeight;
michael@0 122 }
michael@0 123
michael@0 124 return element;
michael@0 125 },
michael@0 126
michael@0 127 /**
michael@0 128 * Returns the child node in this container situated at the specified index.
michael@0 129 *
michael@0 130 * @param number aIndex
michael@0 131 * The position in the container intended for this item.
michael@0 132 * @return nsIDOMNode
michael@0 133 * The element associated with the displayed item.
michael@0 134 */
michael@0 135 getItemAtIndex: function(aIndex) {
michael@0 136 return this._orderedMenuElementsArray[aIndex];
michael@0 137 },
michael@0 138
michael@0 139 /**
michael@0 140 * Removes the specified child node from this container.
michael@0 141 *
michael@0 142 * @param nsIDOMNode aChild
michael@0 143 * The element associated with the displayed item.
michael@0 144 */
michael@0 145 removeChild: function(aChild) {
michael@0 146 this._getNodeForContents(aChild).remove();
michael@0 147
michael@0 148 this._orderedMenuElementsArray.splice(
michael@0 149 this._orderedMenuElementsArray.indexOf(aChild), 1);
michael@0 150
michael@0 151 this._itemsByElement.delete(aChild);
michael@0 152
michael@0 153 if (this._selectedItem == aChild) {
michael@0 154 this._selectedItem = null;
michael@0 155 }
michael@0 156 },
michael@0 157
michael@0 158 /**
michael@0 159 * Removes all of the child nodes from this container.
michael@0 160 */
michael@0 161 removeAllItems: function() {
michael@0 162 let parent = this._parent;
michael@0 163 let list = this._list;
michael@0 164
michael@0 165 while (list.hasChildNodes()) {
michael@0 166 list.firstChild.remove();
michael@0 167 }
michael@0 168
michael@0 169 this._selectedItem = null;
michael@0 170
michael@0 171 this._groupsByName.clear();
michael@0 172 this._orderedGroupElementsArray.length = 0;
michael@0 173 this._orderedMenuElementsArray.length = 0;
michael@0 174 this._itemsByElement.clear();
michael@0 175 },
michael@0 176
michael@0 177 /**
michael@0 178 * Gets the currently selected child node in this container.
michael@0 179 * @return nsIDOMNode
michael@0 180 */
michael@0 181 get selectedItem() {
michael@0 182 return this._selectedItem;
michael@0 183 },
michael@0 184
michael@0 185 /**
michael@0 186 * Sets the currently selected child node in this container.
michael@0 187 * @param nsIDOMNode aChild
michael@0 188 */
michael@0 189 set selectedItem(aChild) {
michael@0 190 let menuArray = this._orderedMenuElementsArray;
michael@0 191
michael@0 192 if (!aChild) {
michael@0 193 this._selectedItem = null;
michael@0 194 }
michael@0 195 for (let node of menuArray) {
michael@0 196 if (node == aChild) {
michael@0 197 this._getNodeForContents(node).classList.add("selected");
michael@0 198 this._selectedItem = node;
michael@0 199 } else {
michael@0 200 this._getNodeForContents(node).classList.remove("selected");
michael@0 201 }
michael@0 202 }
michael@0 203 },
michael@0 204
michael@0 205 /**
michael@0 206 * Ensures the specified element is visible.
michael@0 207 *
michael@0 208 * @param nsIDOMNode aElement
michael@0 209 * The element to make visible.
michael@0 210 */
michael@0 211 ensureElementIsVisible: function(aElement) {
michael@0 212 if (!aElement) {
michael@0 213 return;
michael@0 214 }
michael@0 215
michael@0 216 // Ensure the element is visible but not scrolled horizontally.
michael@0 217 let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
michael@0 218 boxObject.ensureElementIsVisible(aElement);
michael@0 219 boxObject.scrollBy(-this._list.clientWidth, 0);
michael@0 220 },
michael@0 221
michael@0 222 /**
michael@0 223 * Shows all the groups, even the ones with no visible children.
michael@0 224 */
michael@0 225 showEmptyGroups: function() {
michael@0 226 for (let group of this._orderedGroupElementsArray) {
michael@0 227 group.hidden = false;
michael@0 228 }
michael@0 229 },
michael@0 230
michael@0 231 /**
michael@0 232 * Hides all the groups which have no visible children.
michael@0 233 */
michael@0 234 hideEmptyGroups: function() {
michael@0 235 let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])";
michael@0 236
michael@0 237 for (let group of this._orderedGroupElementsArray) {
michael@0 238 group.hidden = group.querySelectorAll(visibleChildNodes).length == 0;
michael@0 239 }
michael@0 240 for (let menuItem of this._orderedMenuElementsArray) {
michael@0 241 menuItem.parentNode.hidden = menuItem.hidden;
michael@0 242 }
michael@0 243 },
michael@0 244
michael@0 245 /**
michael@0 246 * Adds a new attribute or changes an existing attribute on this container.
michael@0 247 *
michael@0 248 * @param string aName
michael@0 249 * The name of the attribute.
michael@0 250 * @param string aValue
michael@0 251 * The desired attribute value.
michael@0 252 */
michael@0 253 setAttribute: function(aName, aValue) {
michael@0 254 this._parent.setAttribute(aName, aValue);
michael@0 255
michael@0 256 if (aName == "emptyText") {
michael@0 257 this._textWhenEmpty = aValue;
michael@0 258 }
michael@0 259 },
michael@0 260
michael@0 261 /**
michael@0 262 * Removes an attribute on this container.
michael@0 263 *
michael@0 264 * @param string aName
michael@0 265 * The name of the attribute.
michael@0 266 */
michael@0 267 removeAttribute: function(aName) {
michael@0 268 this._parent.removeAttribute(aName);
michael@0 269
michael@0 270 if (aName == "emptyText") {
michael@0 271 this._removeEmptyText();
michael@0 272 }
michael@0 273 },
michael@0 274
michael@0 275 /**
michael@0 276 * Set the checkbox state for the item associated with the given node.
michael@0 277 *
michael@0 278 * @param nsIDOMNode aNode
michael@0 279 * The dom node for an item we want to check.
michael@0 280 * @param boolean aCheckState
michael@0 281 * True to check, false to uncheck.
michael@0 282 */
michael@0 283 checkItem: function(aNode, aCheckState) {
michael@0 284 const widgetItem = this._itemsByElement.get(aNode);
michael@0 285 if (!widgetItem) {
michael@0 286 throw new Error("No item for " + aNode);
michael@0 287 }
michael@0 288 widgetItem.check(aCheckState);
michael@0 289 },
michael@0 290
michael@0 291 /**
michael@0 292 * Sets the text displayed in this container when empty.
michael@0 293 * @param string aValue
michael@0 294 */
michael@0 295 set _textWhenEmpty(aValue) {
michael@0 296 if (this._emptyTextNode) {
michael@0 297 this._emptyTextNode.setAttribute("value", aValue);
michael@0 298 }
michael@0 299 this._emptyTextValue = aValue;
michael@0 300 this._showEmptyText();
michael@0 301 },
michael@0 302
michael@0 303 /**
michael@0 304 * Creates and appends a label signaling that this container is empty.
michael@0 305 */
michael@0 306 _showEmptyText: function() {
michael@0 307 if (this._emptyTextNode || !this._emptyTextValue) {
michael@0 308 return;
michael@0 309 }
michael@0 310 let label = this.document.createElement("label");
michael@0 311 label.className = "plain side-menu-widget-empty-text";
michael@0 312 label.setAttribute("value", this._emptyTextValue);
michael@0 313
michael@0 314 this._parent.insertBefore(label, this._list);
michael@0 315 this._emptyTextNode = label;
michael@0 316 },
michael@0 317
michael@0 318 /**
michael@0 319 * Removes the label representing a notice in this container.
michael@0 320 */
michael@0 321 _removeEmptyText: function() {
michael@0 322 if (!this._emptyTextNode) {
michael@0 323 return;
michael@0 324 }
michael@0 325
michael@0 326 this._parent.removeChild(this._emptyTextNode);
michael@0 327 this._emptyTextNode = null;
michael@0 328 },
michael@0 329
michael@0 330 /**
michael@0 331 * Gets a container representing a group for menu items. If the container
michael@0 332 * is not available yet, it is immediately created.
michael@0 333 *
michael@0 334 * @param string aName
michael@0 335 * The required group name.
michael@0 336 * @return SideMenuGroup
michael@0 337 * The newly created group.
michael@0 338 */
michael@0 339 _getMenuGroupForName: function(aName) {
michael@0 340 let cachedGroup = this._groupsByName.get(aName);
michael@0 341 if (cachedGroup) {
michael@0 342 return cachedGroup;
michael@0 343 }
michael@0 344
michael@0 345 let group = new SideMenuGroup(this, aName, {
michael@0 346 showCheckbox: this._showGroupCheckboxes
michael@0 347 });
michael@0 348
michael@0 349 this._groupsByName.set(aName, group);
michael@0 350 group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf(this.groupSortPredicate) : -1);
michael@0 351
michael@0 352 return group;
michael@0 353 },
michael@0 354
michael@0 355 /**
michael@0 356 * Gets a menu item to be displayed inside a group.
michael@0 357 * @see SideMenuWidget.prototype._getMenuGroupForName
michael@0 358 *
michael@0 359 * @param SideMenuGroup aGroup
michael@0 360 * The group to contain the menu item.
michael@0 361 * @param nsIDOMNode aContents
michael@0 362 * The node displayed in the container.
michael@0 363 * @param object aAttachment [optional]
michael@0 364 * Some attached primitive/object.
michael@0 365 */
michael@0 366 _getMenuItemForGroup: function(aGroup, aContents, aAttachment) {
michael@0 367 return new SideMenuItem(aGroup, aContents, aAttachment, {
michael@0 368 showArrow: this._showArrows,
michael@0 369 showCheckbox: this._showItemCheckboxes
michael@0 370 });
michael@0 371 },
michael@0 372
michael@0 373 /**
michael@0 374 * Returns the .side-menu-widget-item node corresponding to a SideMenuItem.
michael@0 375 * To optimize the markup, some redundant elemenst are skipped when creating
michael@0 376 * these child items, in which case we need to be careful on which nodes
michael@0 377 * .selected class names are added, or which nodes are removed.
michael@0 378 *
michael@0 379 * @param nsIDOMNode aChild
michael@0 380 * An element which is the target node of a SideMenuItem.
michael@0 381 * @return nsIDOMNode
michael@0 382 * The wrapper node if there is one, or the same child otherwise.
michael@0 383 */
michael@0 384 _getNodeForContents: function(aChild) {
michael@0 385 if (aChild.hasAttribute("merged-item-contents")) {
michael@0 386 return aChild;
michael@0 387 } else {
michael@0 388 return aChild.parentNode;
michael@0 389 }
michael@0 390 },
michael@0 391
michael@0 392 window: null,
michael@0 393 document: null,
michael@0 394 _showArrows: false,
michael@0 395 _showItemCheckboxes: false,
michael@0 396 _showGroupCheckboxes: false,
michael@0 397 _parent: null,
michael@0 398 _list: null,
michael@0 399 _selectedItem: null,
michael@0 400 _groupsByName: null,
michael@0 401 _orderedGroupElementsArray: null,
michael@0 402 _orderedMenuElementsArray: null,
michael@0 403 _itemsByElement: null,
michael@0 404 _emptyTextNode: null,
michael@0 405 _emptyTextValue: ""
michael@0 406 };
michael@0 407
michael@0 408 /**
michael@0 409 * A SideMenuGroup constructor for the BreadcrumbsWidget.
michael@0 410 * Represents a group which should contain SideMenuItems.
michael@0 411 *
michael@0 412 * @param SideMenuWidget aWidget
michael@0 413 * The widget to contain this menu item.
michael@0 414 * @param string aName
michael@0 415 * The string displayed in the container.
michael@0 416 * @param object aOptions [optional]
michael@0 417 * An object containing the following properties:
michael@0 418 * - showCheckbox: specifies if a checkbox should be displayed.
michael@0 419 */
michael@0 420 function SideMenuGroup(aWidget, aName, aOptions={}) {
michael@0 421 this.document = aWidget.document;
michael@0 422 this.window = aWidget.window;
michael@0 423 this.ownerView = aWidget;
michael@0 424 this.identifier = aName;
michael@0 425
michael@0 426 // Create an internal title and list container.
michael@0 427 if (aName) {
michael@0 428 let target = this._target = this.document.createElement("vbox");
michael@0 429 target.className = "side-menu-widget-group";
michael@0 430 target.setAttribute("name", aName);
michael@0 431
michael@0 432 let list = this._list = this.document.createElement("vbox");
michael@0 433 list.className = "side-menu-widget-group-list";
michael@0 434
michael@0 435 let title = this._title = this.document.createElement("hbox");
michael@0 436 title.className = "side-menu-widget-group-title";
michael@0 437
michael@0 438 let name = this._name = this.document.createElement("label");
michael@0 439 name.className = "plain name";
michael@0 440 name.setAttribute("value", aName);
michael@0 441 name.setAttribute("crop", "end");
michael@0 442 name.setAttribute("flex", "1");
michael@0 443
michael@0 444 // Show a checkbox before the content.
michael@0 445 if (aOptions.showCheckbox) {
michael@0 446 let checkbox = this._checkbox = makeCheckbox(title, { description: aName });
michael@0 447 checkbox.className = "side-menu-widget-group-checkbox";
michael@0 448 }
michael@0 449
michael@0 450 title.appendChild(name);
michael@0 451 target.appendChild(title);
michael@0 452 target.appendChild(list);
michael@0 453 }
michael@0 454 // Skip a few redundant nodes when no title is shown.
michael@0 455 else {
michael@0 456 let target = this._target = this._list = this.document.createElement("vbox");
michael@0 457 target.className = "side-menu-widget-group side-menu-widget-group-list";
michael@0 458 target.setAttribute("merged-group-contents", "");
michael@0 459 }
michael@0 460 }
michael@0 461
michael@0 462 SideMenuGroup.prototype = {
michael@0 463 get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
michael@0 464 get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
michael@0 465 get _itemsByElement() { return this.ownerView._itemsByElement; },
michael@0 466
michael@0 467 /**
michael@0 468 * Inserts this group in the parent container at the specified index.
michael@0 469 *
michael@0 470 * @param number aIndex
michael@0 471 * The position in the container intended for this group.
michael@0 472 */
michael@0 473 insertSelfAt: function(aIndex) {
michael@0 474 let ownerList = this.ownerView._list;
michael@0 475 let groupsArray = this._orderedGroupElementsArray;
michael@0 476
michael@0 477 if (aIndex >= 0) {
michael@0 478 ownerList.insertBefore(this._target, groupsArray[aIndex]);
michael@0 479 groupsArray.splice(aIndex, 0, this._target);
michael@0 480 } else {
michael@0 481 ownerList.appendChild(this._target);
michael@0 482 groupsArray.push(this._target);
michael@0 483 }
michael@0 484 },
michael@0 485
michael@0 486 /**
michael@0 487 * Finds the expected index of this group based on its name.
michael@0 488 *
michael@0 489 * @return number
michael@0 490 * The expected index.
michael@0 491 */
michael@0 492 findExpectedIndexForSelf: function(sortPredicate) {
michael@0 493 let identifier = this.identifier;
michael@0 494 let groupsArray = this._orderedGroupElementsArray;
michael@0 495
michael@0 496 for (let group of groupsArray) {
michael@0 497 let name = group.getAttribute("name");
michael@0 498 if (sortPredicate(name, identifier) > 0 && // Insertion sort at its best :)
michael@0 499 !name.contains(identifier)) { // Least significant group should be last.
michael@0 500 return groupsArray.indexOf(group);
michael@0 501 }
michael@0 502 }
michael@0 503 return -1;
michael@0 504 },
michael@0 505
michael@0 506 window: null,
michael@0 507 document: null,
michael@0 508 ownerView: null,
michael@0 509 identifier: "",
michael@0 510 _target: null,
michael@0 511 _checkbox: null,
michael@0 512 _title: null,
michael@0 513 _name: null,
michael@0 514 _list: null
michael@0 515 };
michael@0 516
michael@0 517 /**
michael@0 518 * A SideMenuItem constructor for the BreadcrumbsWidget.
michael@0 519 *
michael@0 520 * @param SideMenuGroup aGroup
michael@0 521 * The group to contain this menu item.
michael@0 522 * @param nsIDOMNode aContents
michael@0 523 * The node displayed in the container.
michael@0 524 * @param object aAttachment [optional]
michael@0 525 * The attachment object.
michael@0 526 * @param object aOptions [optional]
michael@0 527 * An object containing the following properties:
michael@0 528 * - showArrow: specifies if a horizontal arrow should be displayed.
michael@0 529 * - showCheckbox: specifies if a checkbox should be displayed.
michael@0 530 */
michael@0 531 function SideMenuItem(aGroup, aContents, aAttachment={}, aOptions={}) {
michael@0 532 this.document = aGroup.document;
michael@0 533 this.window = aGroup.window;
michael@0 534 this.ownerView = aGroup;
michael@0 535
michael@0 536 if (aOptions.showArrow || aOptions.showCheckbox) {
michael@0 537 let container = this._container = this.document.createElement("hbox");
michael@0 538 container.className = "side-menu-widget-item";
michael@0 539
michael@0 540 let target = this._target = this.document.createElement("vbox");
michael@0 541 target.className = "side-menu-widget-item-contents";
michael@0 542
michael@0 543 // Show a checkbox before the content.
michael@0 544 if (aOptions.showCheckbox) {
michael@0 545 let checkbox = this._checkbox = makeCheckbox(container, aAttachment);
michael@0 546 checkbox.className = "side-menu-widget-item-checkbox";
michael@0 547 }
michael@0 548
michael@0 549 container.appendChild(target);
michael@0 550
michael@0 551 // Show a horizontal arrow towards the content.
michael@0 552 if (aOptions.showArrow) {
michael@0 553 let arrow = this._arrow = this.document.createElement("hbox");
michael@0 554 arrow.className = "side-menu-widget-item-arrow";
michael@0 555 container.appendChild(arrow);
michael@0 556 }
michael@0 557 }
michael@0 558 // Skip a few redundant nodes when no horizontal arrow or checkbox is shown.
michael@0 559 else {
michael@0 560 let target = this._target = this._container = this.document.createElement("hbox");
michael@0 561 target.className = "side-menu-widget-item side-menu-widget-item-contents";
michael@0 562 target.setAttribute("merged-item-contents", "");
michael@0 563 }
michael@0 564
michael@0 565 this._target.setAttribute("flex", "1");
michael@0 566 this.contents = aContents;
michael@0 567 }
michael@0 568
michael@0 569 SideMenuItem.prototype = {
michael@0 570 get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
michael@0 571 get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
michael@0 572 get _itemsByElement() { return this.ownerView._itemsByElement; },
michael@0 573
michael@0 574 /**
michael@0 575 * Inserts this item in the parent group at the specified index.
michael@0 576 *
michael@0 577 * @param number aIndex
michael@0 578 * The position in the container intended for this item.
michael@0 579 * @return nsIDOMNode
michael@0 580 * The element associated with the displayed item.
michael@0 581 */
michael@0 582 insertSelfAt: function(aIndex) {
michael@0 583 let ownerList = this.ownerView._list;
michael@0 584 let menuArray = this._orderedMenuElementsArray;
michael@0 585
michael@0 586 if (aIndex >= 0) {
michael@0 587 ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
michael@0 588 menuArray.splice(aIndex, 0, this._target);
michael@0 589 } else {
michael@0 590 ownerList.appendChild(this._container);
michael@0 591 menuArray.push(this._target);
michael@0 592 }
michael@0 593 this._itemsByElement.set(this._target, this);
michael@0 594
michael@0 595 return this._target;
michael@0 596 },
michael@0 597
michael@0 598 /**
michael@0 599 * Check or uncheck the checkbox associated with this item.
michael@0 600 *
michael@0 601 * @param boolean aCheckState
michael@0 602 * True to check, false to uncheck.
michael@0 603 */
michael@0 604 check: function(aCheckState) {
michael@0 605 if (!this._checkbox) {
michael@0 606 throw new Error("Cannot check items that do not have checkboxes.");
michael@0 607 }
michael@0 608 // Don't set or remove the "checked" attribute, assign the property instead.
michael@0 609 // Otherwise, the "CheckboxStateChange" event will not be fired. XUL!!
michael@0 610 this._checkbox.checked = !!aCheckState;
michael@0 611 },
michael@0 612
michael@0 613 /**
michael@0 614 * Sets the contents displayed in this item's view.
michael@0 615 *
michael@0 616 * @param string | nsIDOMNode aContents
michael@0 617 * The string or node displayed in the container.
michael@0 618 */
michael@0 619 set contents(aContents) {
michael@0 620 // If there are already some contents displayed, replace them.
michael@0 621 if (this._target.hasChildNodes()) {
michael@0 622 this._target.replaceChild(aContents, this._target.firstChild);
michael@0 623 return;
michael@0 624 }
michael@0 625 // These are the first contents ever displayed.
michael@0 626 this._target.appendChild(aContents);
michael@0 627 },
michael@0 628
michael@0 629 window: null,
michael@0 630 document: null,
michael@0 631 ownerView: null,
michael@0 632 _target: null,
michael@0 633 _container: null,
michael@0 634 _checkbox: null,
michael@0 635 _arrow: null
michael@0 636 };
michael@0 637
michael@0 638 /**
michael@0 639 * Creates a checkbox to a specified parent node. Emits a "check" event
michael@0 640 * whenever the checkbox is checked or unchecked by the user.
michael@0 641 *
michael@0 642 * @param nsIDOMNode aParentNode
michael@0 643 * The parent node to contain this checkbox.
michael@0 644 * @param object aOptions
michael@0 645 * An object containing some or all of the following properties:
michael@0 646 * - description: defaults to "item" if unspecified
michael@0 647 * - checkboxState: true for checked, false for unchecked
michael@0 648 * - checkboxTooltip: the tooltip text of the checkbox
michael@0 649 */
michael@0 650 function makeCheckbox(aParentNode, aOptions) {
michael@0 651 let checkbox = aParentNode.ownerDocument.createElement("checkbox");
michael@0 652 checkbox.setAttribute("tooltiptext", aOptions.checkboxTooltip);
michael@0 653
michael@0 654 if (aOptions.checkboxState) {
michael@0 655 checkbox.setAttribute("checked", true);
michael@0 656 } else {
michael@0 657 checkbox.removeAttribute("checked");
michael@0 658 }
michael@0 659
michael@0 660 // Stop the toggling of the checkbox from selecting the list item.
michael@0 661 checkbox.addEventListener("mousedown", e => {
michael@0 662 e.stopPropagation();
michael@0 663 }, false);
michael@0 664
michael@0 665 // Emit an event from the checkbox when it is toggled. Don't listen for the
michael@0 666 // "command" event! It won't fire for programmatic changes. XUL!!
michael@0 667 checkbox.addEventListener("CheckboxStateChange", e => {
michael@0 668 ViewHelpers.dispatchEvent(checkbox, "check", {
michael@0 669 description: aOptions.description || "item",
michael@0 670 checked: checkbox.checked
michael@0 671 });
michael@0 672 }, false);
michael@0 673
michael@0 674 aParentNode.appendChild(checkbox);
michael@0 675 return checkbox;
michael@0 676 }

mercurial