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