!import
1 <?xml version="1.0"?>
2
3 <!-- ***** BEGIN LICENSE BLOCK *****
4 - Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 -
6 - The contents of this file are subject to the Mozilla Public License Version
7 - 1.1 (the "License"); you may not use this file except in compliance with
8 - the License. You may obtain a copy of the License at
9 - http://www.mozilla.org/MPL/
10 -
11 - Software distributed under the License is distributed on an "AS IS" basis,
12 - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 - for the specific language governing rights and limitations under the
14 - License.
15 -
16 - The Original Code is Richlistbox code.
17 -
18 - The Initial Developer of the Original Code is
19 - IBM Corporation.
20 - Portions created by the Initial Developer are Copyright (C) 2005
21 - IBM Corporation. All Rights Reserved.
22 -
23 - Contributor(s):
24 - Doron Rosenberg <doronr@us.ibm.com> (Original Author)
25 - Simon Bünzli <zeniko@gmail.com>
26 -
27 - Alternatively, the contents of this file may be used under the terms of
28 - either the GNU General Public License Version 2 or later (the "GPL"), or
29 - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
30 - in which case the provisions of the GPL or the LGPL are applicable instead
31 - of those above. If you wish to allow use of your version of this file only
32 - under the terms of either the GPL or the LGPL, and not to allow others to
33 - use your version of this file under the terms of the MPL, indicate your
34 - decision by deleting the provisions above and replace them with the notice
35 - and other provisions required by the GPL or the LGPL. If you do not delete
36 - the provisions above, a recipient may use your version of this file under
37 - the terms of any one of the MPL, the GPL or the LGPL.
38 -
39 - ***** END LICENSE BLOCK ***** -->
40
41 <bindings id="richlistboxBindings"
42 xmlns="http://www.mozilla.org/xbl"
43 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
44 xmlns:xbl="http://www.mozilla.org/xbl">
45
46 <binding id="richlistbox"
47 extends="chrome://global/content/bindings/listbox.xml#listbox-base">
48 <resources>
49 <stylesheet src="chrome://global/skin/richlistbox.css"/>
50 </resources>
51
52 <content>
53 <children includes="listheader"/>
54 <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box"
55 flex="1" style="overflow: auto;">
56 <children/>
57 </xul:scrollbox>
58 </content>
59
60 <implementation>
field_scrollBoxObject
61 <field name="scrollBoxObject">null</field>
constructor
62 <constructor>
63 <![CDATA[
64 var x = document.getAnonymousElementByAttribute(this, "anonid", "main-box");
65 this.scrollBoxObject = x.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
66
67 // add a template build listener
68 if (this.builder)
69 this.builder.addListener(this._builderListener);
70 else
71 this._refreshSelection();
72 ]]>
73 </constructor>
74
destructor
75 <destructor>
76 <![CDATA[
77 // remove the template build listener
78 if (this.builder)
79 this.builder.removeListener(this._builderListener);
80 ]]>
81 </destructor>
82
83 <!-- Overriding baselistbox -->
84 <method name="_fireOnSelect">
_fireOnSelect
85 <body>
86 <![CDATA[
87 // make sure not to modify last-selected when suppressing select events
88 // (otherwise we'll lose the selection when a template gets rebuilt)
89 if (this._suppressOnSelect || this.suppressOnSelect)
90 return;
91
92 // remember the current item and all selected items with IDs
93 var state = this.currentItem ? this.currentItem.id : "";
94 if (this.selType == "multiple" && this.selectedCount) {
getId
95 function getId(aItem) { return aItem.id; }
96 state += " " + this.selectedItems.filter(getId).map(getId).join(" ");
97 }
98 if (state)
99 this.setAttribute("last-selected", state);
100 else
101 this.removeAttribute("last-selected");
102
103 // preserve the index just in case no IDs are available
104 if (this.currentIndex > -1)
105 this._currentIndex = this.currentIndex + 1;
106
107 var event = document.createEvent("Events");
108 event.initEvent("select", true, true);
109 this.dispatchEvent(event);
110
111 // always call this (allows a commandupdater without controller)
112 document.commandDispatcher.updateCommands("richlistbox-select");
113 ]]>
114 </body>
115 </method>
116
117 <method name="appendItem">
118 <parameter name="aLabel"/>
119 <parameter name="aValue"/>
appendItem
120 <body>
121 return this.insertItemAt(-1, aLabel, aValue);
122 </body>
123 </method>
124
125 <method name="insertItemAt">
126 <parameter name="aIndex"/>
127 <parameter name="aLabel"/>
128 <parameter name="aValue"/>
insertItemAt
129 <body>
130 const XULNS =
131 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
132
133 var item =
134 this.ownerDocument.createElementNS(XULNS, "richlistitem");
135 item.setAttribute("value", aValue);
136
137 var label = this.ownerDocument.createElementNS(XULNS, "label");
138 label.setAttribute("value", aLabel);
139 label.setAttribute("flex", "1");
140 label.setAttribute("crop", "end");
141 item.appendChild(label);
142
143 var before = this.getItemAtIndex(aIndex);
144 if (!before)
145 this.appendChild(item);
146 else
147 this.insertBefore(item, before);
148
149 return item;
150 </body>
151 </method>
152
get_itemCount
153 <property name="itemCount" readonly="true"
154 onget="return this.children.length"/>
155
156 <method name="getIndexOfItem">
157 <parameter name="aItem"/>
getIndexOfItem
158 <body>
159 <![CDATA[
160 // don't search the children, if we're looking for none of them
161 if (aItem == null)
162 return -1;
163
164 return this.children.indexOf(aItem);
165 ]]>
166 </body>
167 </method>
168
169 <method name="getItemAtIndex">
170 <parameter name="aIndex"/>
getItemAtIndex
171 <body>
172 return this.children[aIndex] || null;
173 </body>
174 </method>
175
176 <method name="ensureIndexIsVisible">
177 <parameter name="aIndex"/>
ensureIndexIsVisible
178 <body>
179 <![CDATA[
180 // work around missing implementation in scrollBoxObject
181 return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
182 ]]>
183 </body>
184 </method>
185
186 <method name="ensureElementIsVisible">
187 <parameter name="aElement"/>
ensureElementIsVisible
188 <body>
189 <![CDATA[
190 if (aElement)
191 this.scrollBoxObject.ensureElementIsVisible(aElement);
192 ]]>
193 </body>
194 </method>
195
196 <method name="scrollToIndex">
197 <parameter name="aIndex"/>
scrollToIndex
198 <body>
199 <![CDATA[
200 var item = this.getItemAtIndex(aIndex);
201 if (item)
202 this.scrollBoxObject.scrollToElement(item);
203 ]]>
204 </body>
205 </method>
206
207 <method name="getNumberOfVisibleRows">
208 <!-- returns the number of currently visible rows -->
209 <!-- don't rely on this function, if the items' height can vary! -->
getNumberOfVisibleRows
210 <body>
211 <![CDATA[
212 var children = this.children;
213
214 for (var top = 0; top < children.length && !this._isItemVisible(children[top]); top++);
215 for (var ix = top; ix < children.length && this._isItemVisible(children[ix]); ix++);
216
217 return ix - top;
218 ]]>
219 </body>
220 </method>
221
222 <method name="getIndexOfFirstVisibleRow">
getIndexOfFirstVisibleRow
223 <body>
224 <![CDATA[
225 var children = this.children;
226
227 for (var ix = 0; ix < children.length; ix++)
228 if (this._isItemVisible(children[ix]))
229 return ix;
230
231 return -1;
232 ]]>
233 </body>
234 </method>
235
236 <method name="getRowCount">
getRowCount
237 <body>
238 <![CDATA[
239 return this.children.length;
240 ]]>
241 </body>
242 </method>
243
244 <method name="scrollOnePage">
245 <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
scrollOnePage
246 <body>
247 <![CDATA[
248 var children = this.children;
249
250 if (children.length == 0)
251 return 0;
252
253 // If nothing is selected, we just select the first element
254 // at the extreme we're moving away from
255 if (!this.currentItem)
256 return aDirection == -1 ? children.length : 0;
257
258 // If the current item is visible, scroll by one page so that
259 // the new current item is at approximately the same position as
260 // the existing current item.
261 if (this._isItemVisible(this.currentItem))
262 this.scrollBoxObject.scrollBy(0, this.scrollBoxObject.height * aDirection);
263
264 // Figure out, how many items fully fit into the view port
265 // (including the currently selected one), and determine
266 // the index of the first one lying (partially) outside
267 var height = this.scrollBoxObject.height;
268 var startBorder = this.currentItem.boxObject.y;
269 if (aDirection == -1)
270 startBorder += this.currentItem.boxObject.height;
271
272 var index = this.currentIndex;
273 for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
274 var boxObject = children[ix].boxObject;
275 if (boxObject.height == 0)
276 continue; // hidden children have a y of 0
277 var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
278 if ((endBorder - startBorder) * aDirection > height)
279 break; // we've reached the desired distance
280 index = ix;
281 }
282
283 return index != this.currentIndex ? index - this.currentIndex : aDirection;
284 ]]>
285 </body>
286 </method>
287
288 <!-- richlistbox specific -->
289 <property name="children" readonly="true">
get_children
290 <getter>
291 <![CDATA[
292 var childNodes = [];
293 for (var child = this.firstChild; child; child = child.nextSibling) {
294 if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement)
295 childNodes.push(child);
296 }
297 return childNodes;
298 ]]>
299 </getter>
300 </property>
301
field__builderListener
302 <field name="_builderListener" readonly="true">
303 <![CDATA[
304 ({
305 mOuter: this,
306 item: null,
willRebuild
307 willRebuild: function(builder) { },
didRebuild
308 didRebuild: function(builder) {
309 this.mOuter._refreshSelection();
310 }
311 });
312 ]]>
313 </field>
314
315 <method name="_refreshSelection">
_refreshSelection
316 <body>
317 <![CDATA[
318 // when this method is called, we know that either the currentItem
319 // and selectedItems we have are null (ctor) or a reference to an
320 // element no longer in the DOM (template).
321
322 // first look for the last-selected attribute
323 var state = this.getAttribute("last-selected");
324 if (state) {
325 var ids = state.split(" ");
326
327 var suppressSelect = this._suppressOnSelect;
328 this._suppressOnSelect = true;
329 this.clearSelection();
330 for (var i = 1; i < ids.length; i++) {
331 var selectedItem = document.getElementById(ids[i]);
332 if (selectedItem)
333 this.addItemToSelection(selectedItem);
334 }
335
336 var currentItem = document.getElementById(ids[0]);
337 if (!currentItem && this._currentIndex)
338 currentItem = this.getItemAtIndex(Math.min(
339 this._currentIndex - 1, this.getRowCount()));
340 if (currentItem) {
341 this.currentItem = currentItem;
342 if (this.selType != "multiple" && this.selectedCount == 0)
343 this.selectedItem = currentItem;
344
345 if (this.scrollBoxObject.height)
346 this.ensureElementIsVisible(currentItem);
347 else // XXX hack around a bug in ensureElementIsVisible
348 this.ensureElementIsVisible(currentItem.previousSibling);
349 }
350 this._suppressOnSelect = suppressSelect;
351 // XXX actually it's just a refresh, but at least
352 // the Extensions manager expects this:
353 this._fireOnSelect();
354 return;
355 }
356
357 // try to restore the selected items according to their IDs
358 // (applies after a template rebuild, if last-selected was not set)
359 if (this.selectedItems) {
360 for (i = this.selectedCount - 1; i >= 0; i--) {
361 if (this.selectedItems[i] && this.selectedItems[i].id)
362 this.selectedItems[i] = document.getElementById(this.selectedItems[i].id);
363 else
364 this.selectedItems[i] = null;
365 if (!this.selectedItems[i])
366 this.selectedItems.splice(i, 1);
367 }
368 }
369 if (this.currentItem && this.currentItem.id)
370 this.currentItem = document.getElementById(this.currentItem.id);
371 else
372 this.currentItem = null;
373
374 // if we have no previously current item or if the above check fails to
375 // find the previous nodes (which causes it to clear selection)
376 if (!this.currentItem && this.selectedCount == 0) {
377 this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
378
379 // cf. listbox constructor:
380 // select items according to their attributes
381 var children = this.children;
382 for (var i = 0; i < children.length; ++i) {
383 if (children[i].getAttribute("selected") == "true")
384 this.selectedItems.push(children[i]);
385 }
386 }
387
388 if (this.selType != "multiple" && this.selectedCount == 0)
389 this.selectedItem = this.currentItem;
390 ]]>
391 </body>
392 </method>
393
394 <method name="_isItemVisible">
395 <parameter name="aItem"/>
_isItemVisible
396 <body>
397 <![CDATA[
398 if (!aItem)
399 return false;
400
401 var y = {};
402 this.scrollBoxObject.getPosition({}, y);
403 y.value += this.scrollBoxObject.y;
404
405 // Partially visible items are also considered visible
406 return (aItem.boxObject.y + aItem.boxObject.height > y.value) &&
407 (aItem.boxObject.y < y.value + this.scrollBoxObject.height);
408 ]]>
409 </body>
410 </method>
411
field__currentIndex
412 <field name="_currentIndex">null</field>
413
414 <!-- For backwards-compatibility and for convenience.
415 Use getIndexOfItem instead. -->
416 <method name="getIndexOf">
417 <parameter name="aElement"/>
getIndexOf
418 <body>
419 <![CDATA[
420 return this.getIndexOfItem(aElement);
421 ]]>
422 </body>
423 </method>
424
425 <!-- For backwards-compatibility and for convenience.
426 Use ensureElementIsVisible instead -->
427 <method name="ensureSelectedElementIsVisible">
ensureSelectedElementIsVisible
428 <body>
429 <![CDATA[
430 return this.ensureElementIsVisible(this.selectedItem);
431 ]]>
432 </body>
433 </method>
434
435 <!-- For backwards-compatibility and for convenience.
436 Use moveByOffset instead. -->
437 <method name="goUp">
goUp
438 <body>
439 <![CDATA[
440 var index = this.currentIndex;
441 this.moveByOffset(-1, true, false);
442 return index != this.currentIndex;
443 ]]>
444 </body>
445 </method>
446 <method name="goDown">
goDown
447 <body>
448 <![CDATA[
449 var index = this.currentIndex;
450 this.moveByOffset(1, true, false);
451 return index != this.currentIndex;
452 ]]>
453 </body>
454 </method>
455
456 <!-- deprecated (is implied by currentItem and selectItem) -->
fireActiveItemEvent
457 <method name="fireActiveItemEvent"><body/></method>
458 </implementation>
459
460 <handlers>
onclick
461 <handler event="click">
462 <![CDATA[
463 // clicking into nothing should unselect
464 if (event.originalTarget.getAttribute("anonid") == "main-box") {
465 this.clearSelection();
466 this.currentItem = null;
467 }
468 ]]>
469 </handler>
470 </handlers>
471 </binding>
472
473 <binding id="richlistitem"
474 extends="chrome://global/content/bindings/listbox.xml#listitem">
475 <content>
476 <children/>
477 </content>
478
479 <resources>
480 <stylesheet src="chrome://global/skin/richlistbox.css"/>
481 </resources>
482
483 <implementation>
destructor
484 <destructor>
485 <![CDATA[
486 var control = this.control;
487 if (!control)
488 return;
489 // When we are destructed and we are current or selected, unselect ourselves
490 // so that richlistbox's selection doesn't point to something not in the DOM.
491 // We don't want to reset last-selected, so we set _suppressOnSelect.
492 if (this.selected) {
493 var suppressSelect = control._suppressOnSelect;
494 control._suppressOnSelect = true;
495 control.removeItemFromSelection(this);
496 control._suppressOnSelect = suppressSelect;
497 }
498 if (this.current)
499 control.currentItem = null;
500 ]]>
501 </destructor>
502
503 <property name="label" readonly="true">
504 <!-- Setter purposely not implemented; the getter returns a
505 concatentation of label text to expose via accessibility APIs -->
get_label
506 <getter>
507 <![CDATA[
508 const XULNS =
509 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
510
511 var labelText = "";
512 var startEl = document.getAnonymousNodes(this);
513 if (!startEl.length)
514 startEl = [this];
515 var labels = startEl[0].getElementsByTagNameNS(XULNS, "label");
516 for (var count = 0; count < labels.length; count++)
517 labelText += labels[count].value + " ";
518 return labelText;
519 ]]>
520 </getter>
521 </property>
522
523 <property name="searchLabel">
get_searchLabel
524 <getter>
525 <![CDATA[
526 return this.hasAttribute("searchlabel") ?
527 this.getAttribute("searchlabel") : this.label;
528 ]]>
529 </getter>
set_searchLabel
530 <setter>
531 <![CDATA[
532 if (val !== null)
533 this.setAttribute("searchlabel", val);
534 else
535 // fall back to the label property (default value)
536 this.removeAttribute("searchlabel");
537 return val;
538 ]]>
539 </setter>
540 </property>
541 </implementation>
542 </binding>
543 </bindings>