|
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 EventEmitter = require("devtools/toolkit/event-emitter"); |
|
9 const { Cu, Ci } = require("chrome"); |
|
10 const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {}); |
|
11 |
|
12 /** |
|
13 * A list menu widget that attempts to be very fast. |
|
14 * |
|
15 * Note: this widget should be used in tandem with the WidgetMethods in |
|
16 * ViewHelpers.jsm. |
|
17 * |
|
18 * @param nsIDOMNode aNode |
|
19 * The element associated with the widget. |
|
20 */ |
|
21 const FastListWidget = module.exports = function FastListWidget(aNode) { |
|
22 this.document = aNode.ownerDocument; |
|
23 this.window = this.document.defaultView; |
|
24 this._parent = aNode; |
|
25 this._fragment = this.document.createDocumentFragment(); |
|
26 |
|
27 // This is a prototype element that each item added to the list clones. |
|
28 this._templateElement = this.document.createElement("hbox"); |
|
29 |
|
30 // Create an internal scrollbox container. |
|
31 this._list = this.document.createElement("scrollbox"); |
|
32 this._list.className = "fast-list-widget-container theme-body"; |
|
33 this._list.setAttribute("flex", "1"); |
|
34 this._list.setAttribute("orient", "vertical"); |
|
35 this._list.setAttribute("tabindex", "0"); |
|
36 this._list.addEventListener("keypress", e => this.emit("keyPress", e), false); |
|
37 this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false); |
|
38 this._parent.appendChild(this._list); |
|
39 |
|
40 this._orderedMenuElementsArray = []; |
|
41 this._itemsByElement = new Map(); |
|
42 |
|
43 // This widget emits events that can be handled in a MenuContainer. |
|
44 EventEmitter.decorate(this); |
|
45 |
|
46 // Delegate some of the associated node's methods to satisfy the interface |
|
47 // required by MenuContainer instances. |
|
48 ViewHelpers.delegateWidgetAttributeMethods(this, aNode); |
|
49 ViewHelpers.delegateWidgetEventMethods(this, aNode); |
|
50 } |
|
51 |
|
52 FastListWidget.prototype = { |
|
53 /** |
|
54 * Inserts an item in this container at the specified index, optionally |
|
55 * grouping by name. |
|
56 * |
|
57 * @param number aIndex |
|
58 * The position in the container intended for this item. |
|
59 * @param nsIDOMNode aContents |
|
60 * The node to be displayed in the container. |
|
61 * @param Object aAttachment [optional] |
|
62 * Extra data for the user. |
|
63 * @return nsIDOMNode |
|
64 * The element associated with the displayed item. |
|
65 */ |
|
66 insertItemAt: function(aIndex, aContents, aAttachment={}) { |
|
67 let element = this._templateElement.cloneNode(); |
|
68 element.appendChild(aContents); |
|
69 |
|
70 if (aIndex >= 0) { |
|
71 throw new Error("FastListWidget only supports appending items."); |
|
72 } |
|
73 |
|
74 this._fragment.appendChild(element); |
|
75 this._orderedMenuElementsArray.push(element); |
|
76 this._itemsByElement.set(element, this); |
|
77 |
|
78 return element; |
|
79 }, |
|
80 |
|
81 /** |
|
82 * This is a non-standard widget implementation method. When appending items, |
|
83 * they are queued in a document fragment. This method appends the document |
|
84 * fragment to the dom. |
|
85 */ |
|
86 flush: function() { |
|
87 this._list.appendChild(this._fragment); |
|
88 }, |
|
89 |
|
90 /** |
|
91 * Removes all of the child nodes from this container. |
|
92 */ |
|
93 removeAllItems: function() { |
|
94 let parent = this._parent; |
|
95 let list = this._list; |
|
96 |
|
97 while (list.hasChildNodes()) { |
|
98 list.firstChild.remove(); |
|
99 } |
|
100 |
|
101 this._selectedItem = null; |
|
102 |
|
103 this._orderedMenuElementsArray.length = 0; |
|
104 this._itemsByElement.clear(); |
|
105 }, |
|
106 |
|
107 /** |
|
108 * Remove the given item. |
|
109 */ |
|
110 removeChild: function(child) { |
|
111 throw new Error("Not yet implemented"); |
|
112 }, |
|
113 |
|
114 /** |
|
115 * Gets the currently selected child node in this container. |
|
116 * @return nsIDOMNode |
|
117 */ |
|
118 get selectedItem() { |
|
119 return this._selectedItem; |
|
120 }, |
|
121 |
|
122 /** |
|
123 * Sets the currently selected child node in this container. |
|
124 * @param nsIDOMNode child |
|
125 */ |
|
126 set selectedItem(child) { |
|
127 let menuArray = this._orderedMenuElementsArray; |
|
128 |
|
129 if (!child) { |
|
130 this._selectedItem = null; |
|
131 } |
|
132 for (let node of menuArray) { |
|
133 if (node == child) { |
|
134 node.classList.add("selected"); |
|
135 this._selectedItem = node; |
|
136 } else { |
|
137 node.classList.remove("selected"); |
|
138 } |
|
139 } |
|
140 |
|
141 this.ensureElementIsVisible(this.selectedItem); |
|
142 }, |
|
143 |
|
144 /** |
|
145 * Returns the child node in this container situated at the specified index. |
|
146 * |
|
147 * @param number index |
|
148 * The position in the container intended for this item. |
|
149 * @return nsIDOMNode |
|
150 * The element associated with the displayed item. |
|
151 */ |
|
152 getItemAtIndex: function(index) { |
|
153 return this._orderedMenuElementsArray[index]; |
|
154 }, |
|
155 |
|
156 /** |
|
157 * Adds a new attribute or changes an existing attribute on this container. |
|
158 * |
|
159 * @param string name |
|
160 * The name of the attribute. |
|
161 * @param string value |
|
162 * The desired attribute value. |
|
163 */ |
|
164 setAttribute: function(name, value) { |
|
165 this._parent.setAttribute(name, value); |
|
166 |
|
167 if (name == "emptyText") { |
|
168 this._textWhenEmpty = value; |
|
169 } |
|
170 }, |
|
171 |
|
172 /** |
|
173 * Removes an attribute on this container. |
|
174 * |
|
175 * @param string name |
|
176 * The name of the attribute. |
|
177 */ |
|
178 removeAttribute: function(name) { |
|
179 this._parent.removeAttribute(name); |
|
180 |
|
181 if (name == "emptyText") { |
|
182 this._removeEmptyText(); |
|
183 } |
|
184 }, |
|
185 |
|
186 /** |
|
187 * Ensures the specified element is visible. |
|
188 * |
|
189 * @param nsIDOMNode element |
|
190 * The element to make visible. |
|
191 */ |
|
192 ensureElementIsVisible: function(element) { |
|
193 if (!element) { |
|
194 return; |
|
195 } |
|
196 |
|
197 // Ensure the element is visible but not scrolled horizontally. |
|
198 let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject); |
|
199 boxObject.ensureElementIsVisible(element); |
|
200 boxObject.scrollBy(-this._list.clientWidth, 0); |
|
201 }, |
|
202 |
|
203 /** |
|
204 * Sets the text displayed in this container when empty. |
|
205 * @param string aValue |
|
206 */ |
|
207 set _textWhenEmpty(aValue) { |
|
208 if (this._emptyTextNode) { |
|
209 this._emptyTextNode.setAttribute("value", aValue); |
|
210 } |
|
211 this._emptyTextValue = aValue; |
|
212 this._showEmptyText(); |
|
213 }, |
|
214 |
|
215 /** |
|
216 * Creates and appends a label signaling that this container is empty. |
|
217 */ |
|
218 _showEmptyText: function() { |
|
219 if (this._emptyTextNode || !this._emptyTextValue) { |
|
220 return; |
|
221 } |
|
222 let label = this.document.createElement("label"); |
|
223 label.className = "plain fast-list-widget-empty-text"; |
|
224 label.setAttribute("value", this._emptyTextValue); |
|
225 |
|
226 this._parent.insertBefore(label, this._list); |
|
227 this._emptyTextNode = label; |
|
228 }, |
|
229 |
|
230 /** |
|
231 * Removes the label signaling that this container is empty. |
|
232 */ |
|
233 _removeEmptyText: function() { |
|
234 if (!this._emptyTextNode) { |
|
235 return; |
|
236 } |
|
237 this._parent.removeChild(this._emptyTextNode); |
|
238 this._emptyTextNode = null; |
|
239 }, |
|
240 |
|
241 window: null, |
|
242 document: null, |
|
243 _parent: null, |
|
244 _list: null, |
|
245 _selectedItem: null, |
|
246 _orderedMenuElementsArray: null, |
|
247 _itemsByElement: null, |
|
248 _emptyTextNode: null, |
|
249 _emptyTextValue: "" |
|
250 }; |