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