1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/content/widgets/richlistbox.xml Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,588 @@ 1.4 +<?xml version="1.0"?> 1.5 + 1.6 +<!-- This Source Code Form is subject to the terms of the Mozilla Public 1.7 + - License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> 1.9 + 1.10 +<bindings id="richlistboxBindings" 1.11 + xmlns="http://www.mozilla.org/xbl" 1.12 + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 1.13 + xmlns:xbl="http://www.mozilla.org/xbl"> 1.14 + 1.15 + <binding id="richlistbox" 1.16 + extends="chrome://global/content/bindings/listbox.xml#listbox-base"> 1.17 + <resources> 1.18 + <stylesheet src="chrome://global/skin/richlistbox.css"/> 1.19 + </resources> 1.20 + 1.21 + <content> 1.22 + <children includes="listheader"/> 1.23 + <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box" 1.24 + flex="1" style="overflow: auto;" xbl:inherits="dir,pack"> 1.25 + <children/> 1.26 + </xul:scrollbox> 1.27 + </content> 1.28 + 1.29 + <implementation> 1.30 + <field name="_scrollbox"> 1.31 + document.getAnonymousElementByAttribute(this, "anonid", "main-box"); 1.32 + </field> 1.33 + <field name="scrollBoxObject"> 1.34 + this._scrollbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject); 1.35 + </field> 1.36 + <constructor> 1.37 + <![CDATA[ 1.38 + // add a template build listener 1.39 + if (this.builder) 1.40 + this.builder.addListener(this._builderListener); 1.41 + else 1.42 + this._refreshSelection(); 1.43 + ]]> 1.44 + </constructor> 1.45 + 1.46 + <destructor> 1.47 + <![CDATA[ 1.48 + // remove the template build listener 1.49 + if (this.builder) 1.50 + this.builder.removeListener(this._builderListener); 1.51 + ]]> 1.52 + </destructor> 1.53 + 1.54 + <!-- Overriding baselistbox --> 1.55 + <method name="_fireOnSelect"> 1.56 + <body> 1.57 + <![CDATA[ 1.58 + // make sure not to modify last-selected when suppressing select events 1.59 + // (otherwise we'll lose the selection when a template gets rebuilt) 1.60 + if (this._suppressOnSelect || this.suppressOnSelect) 1.61 + return; 1.62 + 1.63 + // remember the current item and all selected items with IDs 1.64 + var state = this.currentItem ? this.currentItem.id : ""; 1.65 + if (this.selType == "multiple" && this.selectedCount) { 1.66 + let getId = function getId(aItem) { return aItem.id; } 1.67 + state += " " + this.selectedItems.filter(getId).map(getId).join(" "); 1.68 + } 1.69 + if (state) 1.70 + this.setAttribute("last-selected", state); 1.71 + else 1.72 + this.removeAttribute("last-selected"); 1.73 + 1.74 + // preserve the index just in case no IDs are available 1.75 + if (this.currentIndex > -1) 1.76 + this._currentIndex = this.currentIndex + 1; 1.77 + 1.78 + var event = document.createEvent("Events"); 1.79 + event.initEvent("select", true, true); 1.80 + this.dispatchEvent(event); 1.81 + 1.82 + // always call this (allows a commandupdater without controller) 1.83 + document.commandDispatcher.updateCommands("richlistbox-select"); 1.84 + ]]> 1.85 + </body> 1.86 + </method> 1.87 + 1.88 + <!-- We override base-listbox here because those methods don't take dir 1.89 + into account on listbox (which doesn't support dir yet) --> 1.90 + <method name="getNextItem"> 1.91 + <parameter name="aStartItem"/> 1.92 + <parameter name="aDelta"/> 1.93 + <body> 1.94 + <![CDATA[ 1.95 + var prop = this.dir == "reverse" && this._mayReverse ? 1.96 + "previousSibling" : 1.97 + "nextSibling"; 1.98 + while (aStartItem) { 1.99 + aStartItem = aStartItem[prop]; 1.100 + if (aStartItem && aStartItem instanceof 1.101 + Components.interfaces.nsIDOMXULSelectControlItemElement && 1.102 + (!this._userSelecting || this._canUserSelect(aStartItem))) { 1.103 + --aDelta; 1.104 + if (aDelta == 0) 1.105 + return aStartItem; 1.106 + } 1.107 + } 1.108 + return null; 1.109 + ]]></body> 1.110 + </method> 1.111 + 1.112 + <method name="getPreviousItem"> 1.113 + <parameter name="aStartItem"/> 1.114 + <parameter name="aDelta"/> 1.115 + <body> 1.116 + <![CDATA[ 1.117 + var prop = this.dir == "reverse" && this._mayReverse ? 1.118 + "nextSibling" : 1.119 + "previousSibling"; 1.120 + while (aStartItem) { 1.121 + aStartItem = aStartItem[prop]; 1.122 + if (aStartItem && aStartItem instanceof 1.123 + Components.interfaces.nsIDOMXULSelectControlItemElement && 1.124 + (!this._userSelecting || this._canUserSelect(aStartItem))) { 1.125 + --aDelta; 1.126 + if (aDelta == 0) 1.127 + return aStartItem; 1.128 + } 1.129 + } 1.130 + return null; 1.131 + ]]> 1.132 + </body> 1.133 + </method> 1.134 + 1.135 + <method name="appendItem"> 1.136 + <parameter name="aLabel"/> 1.137 + <parameter name="aValue"/> 1.138 + <body> 1.139 + return this.insertItemAt(-1, aLabel, aValue); 1.140 + </body> 1.141 + </method> 1.142 + 1.143 + <method name="insertItemAt"> 1.144 + <parameter name="aIndex"/> 1.145 + <parameter name="aLabel"/> 1.146 + <parameter name="aValue"/> 1.147 + <body> 1.148 + const XULNS = 1.149 + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.150 + 1.151 + var item = 1.152 + this.ownerDocument.createElementNS(XULNS, "richlistitem"); 1.153 + item.setAttribute("value", aValue); 1.154 + 1.155 + var label = this.ownerDocument.createElementNS(XULNS, "label"); 1.156 + label.setAttribute("value", aLabel); 1.157 + label.setAttribute("flex", "1"); 1.158 + label.setAttribute("crop", "end"); 1.159 + item.appendChild(label); 1.160 + 1.161 + var before = this.getItemAtIndex(aIndex); 1.162 + if (!before) 1.163 + this.appendChild(item); 1.164 + else 1.165 + this.insertBefore(item, before); 1.166 + 1.167 + return item; 1.168 + </body> 1.169 + </method> 1.170 + 1.171 + <property name="itemCount" readonly="true" 1.172 + onget="return this.children.length"/> 1.173 + 1.174 + <method name="getIndexOfItem"> 1.175 + <parameter name="aItem"/> 1.176 + <body> 1.177 + <![CDATA[ 1.178 + // don't search the children, if we're looking for none of them 1.179 + if (aItem == null) 1.180 + return -1; 1.181 + 1.182 + return this.children.indexOf(aItem); 1.183 + ]]> 1.184 + </body> 1.185 + </method> 1.186 + 1.187 + <method name="getItemAtIndex"> 1.188 + <parameter name="aIndex"/> 1.189 + <body> 1.190 + return this.children[aIndex] || null; 1.191 + </body> 1.192 + </method> 1.193 + 1.194 + <method name="ensureIndexIsVisible"> 1.195 + <parameter name="aIndex"/> 1.196 + <body> 1.197 + <![CDATA[ 1.198 + // work around missing implementation in scrollBoxObject 1.199 + return this.ensureElementIsVisible(this.getItemAtIndex(aIndex)); 1.200 + ]]> 1.201 + </body> 1.202 + </method> 1.203 + 1.204 + <method name="ensureElementIsVisible"> 1.205 + <parameter name="aElement"/> 1.206 + <body> 1.207 + <![CDATA[ 1.208 + if (!aElement) 1.209 + return; 1.210 + var targetRect = aElement.getBoundingClientRect(); 1.211 + var scrollRect = this._scrollbox.getBoundingClientRect(); 1.212 + var offset = targetRect.top - scrollRect.top; 1.213 + if (offset >= 0) { 1.214 + // scrollRect.bottom wouldn't take a horizontal scroll bar into account 1.215 + let scrollRectBottom = scrollRect.top + this._scrollbox.clientHeight; 1.216 + offset = targetRect.bottom - scrollRectBottom; 1.217 + if (offset <= 0) 1.218 + return; 1.219 + } 1.220 + this._scrollbox.scrollTop += offset; 1.221 + ]]> 1.222 + </body> 1.223 + </method> 1.224 + 1.225 + <method name="scrollToIndex"> 1.226 + <parameter name="aIndex"/> 1.227 + <body> 1.228 + <![CDATA[ 1.229 + var item = this.getItemAtIndex(aIndex); 1.230 + if (item) 1.231 + this.scrollBoxObject.scrollToElement(item); 1.232 + ]]> 1.233 + </body> 1.234 + </method> 1.235 + 1.236 + <method name="getNumberOfVisibleRows"> 1.237 + <!-- returns the number of currently visible rows --> 1.238 + <!-- don't rely on this function, if the items' height can vary! --> 1.239 + <body> 1.240 + <![CDATA[ 1.241 + var children = this.children; 1.242 + 1.243 + for (var top = 0; top < children.length && !this._isItemVisible(children[top]); top++); 1.244 + for (var ix = top; ix < children.length && this._isItemVisible(children[ix]); ix++); 1.245 + 1.246 + return ix - top; 1.247 + ]]> 1.248 + </body> 1.249 + </method> 1.250 + 1.251 + <method name="getIndexOfFirstVisibleRow"> 1.252 + <body> 1.253 + <![CDATA[ 1.254 + var children = this.children; 1.255 + 1.256 + for (var ix = 0; ix < children.length; ix++) 1.257 + if (this._isItemVisible(children[ix])) 1.258 + return ix; 1.259 + 1.260 + return -1; 1.261 + ]]> 1.262 + </body> 1.263 + </method> 1.264 + 1.265 + <method name="getRowCount"> 1.266 + <body> 1.267 + <![CDATA[ 1.268 + return this.children.length; 1.269 + ]]> 1.270 + </body> 1.271 + </method> 1.272 + 1.273 + <method name="scrollOnePage"> 1.274 + <parameter name="aDirection"/> <!-- Must be -1 or 1 --> 1.275 + <body> 1.276 + <![CDATA[ 1.277 + var children = this.children; 1.278 + 1.279 + if (children.length == 0) 1.280 + return 0; 1.281 + 1.282 + // If nothing is selected, we just select the first element 1.283 + // at the extreme we're moving away from 1.284 + if (!this.currentItem) 1.285 + return aDirection == -1 ? children.length : 0; 1.286 + 1.287 + // If the current item is visible, scroll by one page so that 1.288 + // the new current item is at approximately the same position as 1.289 + // the existing current item. 1.290 + if (this._isItemVisible(this.currentItem)) 1.291 + this.scrollBoxObject.scrollBy(0, this.scrollBoxObject.height * aDirection); 1.292 + 1.293 + // Figure out, how many items fully fit into the view port 1.294 + // (including the currently selected one), and determine 1.295 + // the index of the first one lying (partially) outside 1.296 + var height = this.scrollBoxObject.height; 1.297 + var startBorder = this.currentItem.boxObject.y; 1.298 + if (aDirection == -1) 1.299 + startBorder += this.currentItem.boxObject.height; 1.300 + 1.301 + var index = this.currentIndex; 1.302 + for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) { 1.303 + var boxObject = children[ix].boxObject; 1.304 + if (boxObject.height == 0) 1.305 + continue; // hidden children have a y of 0 1.306 + var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0); 1.307 + if ((endBorder - startBorder) * aDirection > height) 1.308 + break; // we've reached the desired distance 1.309 + index = ix; 1.310 + } 1.311 + 1.312 + return index != this.currentIndex ? index - this.currentIndex : aDirection; 1.313 + ]]> 1.314 + </body> 1.315 + </method> 1.316 + 1.317 + <!-- richlistbox specific --> 1.318 + <property name="children" readonly="true"> 1.319 + <getter> 1.320 + <![CDATA[ 1.321 + var childNodes = []; 1.322 + var isReverse = this.dir == "reverse" && this._mayReverse; 1.323 + var child = isReverse ? this.lastChild : this.firstChild; 1.324 + var prop = isReverse ? "previousSibling" : "nextSibling"; 1.325 + while (child) { 1.326 + if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement) 1.327 + childNodes.push(child); 1.328 + child = child[prop]; 1.329 + } 1.330 + return childNodes; 1.331 + ]]> 1.332 + </getter> 1.333 + </property> 1.334 + 1.335 + <field name="_builderListener" readonly="true"> 1.336 + <![CDATA[ 1.337 + ({ 1.338 + mOuter: this, 1.339 + item: null, 1.340 + willRebuild: function(builder) { }, 1.341 + didRebuild: function(builder) { 1.342 + this.mOuter._refreshSelection(); 1.343 + } 1.344 + }); 1.345 + ]]> 1.346 + </field> 1.347 + 1.348 + <method name="_refreshSelection"> 1.349 + <body> 1.350 + <![CDATA[ 1.351 + // when this method is called, we know that either the currentItem 1.352 + // and selectedItems we have are null (ctor) or a reference to an 1.353 + // element no longer in the DOM (template). 1.354 + 1.355 + // first look for the last-selected attribute 1.356 + var state = this.getAttribute("last-selected"); 1.357 + if (state) { 1.358 + var ids = state.split(" "); 1.359 + 1.360 + var suppressSelect = this._suppressOnSelect; 1.361 + this._suppressOnSelect = true; 1.362 + this.clearSelection(); 1.363 + for (var i = 1; i < ids.length; i++) { 1.364 + var selectedItem = document.getElementById(ids[i]); 1.365 + if (selectedItem) 1.366 + this.addItemToSelection(selectedItem); 1.367 + } 1.368 + 1.369 + var currentItem = document.getElementById(ids[0]); 1.370 + if (!currentItem && this._currentIndex) 1.371 + currentItem = this.getItemAtIndex(Math.min( 1.372 + this._currentIndex - 1, this.getRowCount())); 1.373 + if (currentItem) { 1.374 + this.currentItem = currentItem; 1.375 + if (this.selType != "multiple" && this.selectedCount == 0) 1.376 + this.selectedItem = currentItem; 1.377 + 1.378 + if (this.scrollBoxObject.height) { 1.379 + this.ensureElementIsVisible(currentItem); 1.380 + } 1.381 + else { 1.382 + // XXX hack around a bug in ensureElementIsVisible as it will 1.383 + // scroll beyond the last element, bug 493645. 1.384 + var previousElement = this.dir == "reverse" ? currentItem.nextSibling : 1.385 + currentItem.previousSibling; 1.386 + this.ensureElementIsVisible(previousElement); 1.387 + } 1.388 + } 1.389 + this._suppressOnSelect = suppressSelect; 1.390 + // XXX actually it's just a refresh, but at least 1.391 + // the Extensions manager expects this: 1.392 + this._fireOnSelect(); 1.393 + return; 1.394 + } 1.395 + 1.396 + // try to restore the selected items according to their IDs 1.397 + // (applies after a template rebuild, if last-selected was not set) 1.398 + if (this.selectedItems) { 1.399 + for (i = this.selectedCount - 1; i >= 0; i--) { 1.400 + if (this.selectedItems[i] && this.selectedItems[i].id) 1.401 + this.selectedItems[i] = document.getElementById(this.selectedItems[i].id); 1.402 + else 1.403 + this.selectedItems[i] = null; 1.404 + if (!this.selectedItems[i]) 1.405 + this.selectedItems.splice(i, 1); 1.406 + } 1.407 + } 1.408 + if (this.currentItem && this.currentItem.id) 1.409 + this.currentItem = document.getElementById(this.currentItem.id); 1.410 + else 1.411 + this.currentItem = null; 1.412 + 1.413 + // if we have no previously current item or if the above check fails to 1.414 + // find the previous nodes (which causes it to clear selection) 1.415 + if (!this.currentItem && this.selectedCount == 0) { 1.416 + this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0; 1.417 + 1.418 + // cf. listbox constructor: 1.419 + // select items according to their attributes 1.420 + var children = this.children; 1.421 + for (var i = 0; i < children.length; ++i) { 1.422 + if (children[i].getAttribute("selected") == "true") 1.423 + this.selectedItems.push(children[i]); 1.424 + } 1.425 + } 1.426 + 1.427 + if (this.selType != "multiple" && this.selectedCount == 0) 1.428 + this.selectedItem = this.currentItem; 1.429 + ]]> 1.430 + </body> 1.431 + </method> 1.432 + 1.433 + <method name="_isItemVisible"> 1.434 + <parameter name="aItem"/> 1.435 + <body> 1.436 + <![CDATA[ 1.437 + if (!aItem) 1.438 + return false; 1.439 + 1.440 + var y = {}; 1.441 + this.scrollBoxObject.getPosition({}, y); 1.442 + y.value += this.scrollBoxObject.y; 1.443 + 1.444 + // Partially visible items are also considered visible 1.445 + return (aItem.boxObject.y + aItem.boxObject.height > y.value) && 1.446 + (aItem.boxObject.y < y.value + this.scrollBoxObject.height); 1.447 + ]]> 1.448 + </body> 1.449 + </method> 1.450 + 1.451 + <field name="_currentIndex">null</field> 1.452 + 1.453 + <!-- For backwards-compatibility and for convenience. 1.454 + Use getIndexOfItem instead. --> 1.455 + <method name="getIndexOf"> 1.456 + <parameter name="aElement"/> 1.457 + <body> 1.458 + <![CDATA[ 1.459 + return this.getIndexOfItem(aElement); 1.460 + ]]> 1.461 + </body> 1.462 + </method> 1.463 + 1.464 + <!-- For backwards-compatibility and for convenience. 1.465 + Use ensureElementIsVisible instead --> 1.466 + <method name="ensureSelectedElementIsVisible"> 1.467 + <body> 1.468 + <![CDATA[ 1.469 + return this.ensureElementIsVisible(this.selectedItem); 1.470 + ]]> 1.471 + </body> 1.472 + </method> 1.473 + 1.474 + <!-- For backwards-compatibility and for convenience. 1.475 + Use moveByOffset instead. --> 1.476 + <method name="goUp"> 1.477 + <body> 1.478 + <![CDATA[ 1.479 + var index = this.currentIndex; 1.480 + this.moveByOffset(-1, true, false); 1.481 + return index != this.currentIndex; 1.482 + ]]> 1.483 + </body> 1.484 + </method> 1.485 + <method name="goDown"> 1.486 + <body> 1.487 + <![CDATA[ 1.488 + var index = this.currentIndex; 1.489 + this.moveByOffset(1, true, false); 1.490 + return index != this.currentIndex; 1.491 + ]]> 1.492 + </body> 1.493 + </method> 1.494 + 1.495 + <!-- deprecated (is implied by currentItem and selectItem) --> 1.496 + <method name="fireActiveItemEvent"><body/></method> 1.497 + </implementation> 1.498 + 1.499 + <handlers> 1.500 + <handler event="click"> 1.501 + <![CDATA[ 1.502 + // clicking into nothing should unselect 1.503 + if (event.originalTarget == this._scrollbox) { 1.504 + this.clearSelection(); 1.505 + this.currentItem = null; 1.506 + } 1.507 + ]]> 1.508 + </handler> 1.509 + 1.510 + <handler event="MozSwipeGesture"> 1.511 + <![CDATA[ 1.512 + // Only handle swipe gestures up and down 1.513 + switch (event.direction) { 1.514 + case event.DIRECTION_DOWN: 1.515 + this._scrollbox.scrollTop = this._scrollbox.scrollHeight; 1.516 + break; 1.517 + case event.DIRECTION_UP: 1.518 + this._scrollbox.scrollTop = 0; 1.519 + break; 1.520 + } 1.521 + ]]> 1.522 + </handler> 1.523 + </handlers> 1.524 + </binding> 1.525 + 1.526 + <binding id="richlistitem" 1.527 + extends="chrome://global/content/bindings/listbox.xml#listitem"> 1.528 + <content> 1.529 + <children/> 1.530 + </content> 1.531 + 1.532 + <resources> 1.533 + <stylesheet src="chrome://global/skin/richlistbox.css"/> 1.534 + </resources> 1.535 + 1.536 + <implementation> 1.537 + <destructor> 1.538 + <![CDATA[ 1.539 + var control = this.control; 1.540 + if (!control) 1.541 + return; 1.542 + // When we are destructed and we are current or selected, unselect ourselves 1.543 + // so that richlistbox's selection doesn't point to something not in the DOM. 1.544 + // We don't want to reset last-selected, so we set _suppressOnSelect. 1.545 + if (this.selected) { 1.546 + var suppressSelect = control._suppressOnSelect; 1.547 + control._suppressOnSelect = true; 1.548 + control.removeItemFromSelection(this); 1.549 + control._suppressOnSelect = suppressSelect; 1.550 + } 1.551 + if (this.current) 1.552 + control.currentItem = null; 1.553 + ]]> 1.554 + </destructor> 1.555 + 1.556 + <property name="label" readonly="true"> 1.557 + <!-- Setter purposely not implemented; the getter returns a 1.558 + concatentation of label text to expose via accessibility APIs --> 1.559 + <getter> 1.560 + <![CDATA[ 1.561 + const XULNS = 1.562 + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.563 + return Array.map(this.getElementsByTagNameNS(XULNS, "label"), 1.564 + function (label) label.value) 1.565 + .join(" "); 1.566 + ]]> 1.567 + </getter> 1.568 + </property> 1.569 + 1.570 + <property name="searchLabel"> 1.571 + <getter> 1.572 + <![CDATA[ 1.573 + return this.hasAttribute("searchlabel") ? 1.574 + this.getAttribute("searchlabel") : this.label; 1.575 + ]]> 1.576 + </getter> 1.577 + <setter> 1.578 + <![CDATA[ 1.579 + if (val !== null) 1.580 + this.setAttribute("searchlabel", val); 1.581 + else 1.582 + // fall back to the label property (default value) 1.583 + this.removeAttribute("searchlabel"); 1.584 + return val; 1.585 + ]]> 1.586 + </setter> 1.587 + </property> 1.588 + </implementation> 1.589 + </binding> 1.590 +</bindings> 1.591 +