1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/bindings/grid.xml Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1125 @@ 1.4 +<?xml version="1.0"?> 1.5 +<!-- This Source Code Form is subject to the terms of the Mozilla Public 1.6 + - License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> 1.8 + 1.9 +<bindings 1.10 + xmlns="http://www.mozilla.org/xbl" 1.11 + xmlns:xbl="http://www.mozilla.org/xbl" 1.12 + xmlns:html="http://www.w3.org/1999/xhtml" 1.13 + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 1.14 + 1.15 + <binding id="richgrid" 1.16 + extends="chrome://global/content/bindings/general.xml#basecontrol"> 1.17 + 1.18 + <content> 1.19 + <html:div id="grid-div" anonid="grid" class="richgrid-grid" xbl:inherits="compact"> 1.20 + <children/> 1.21 + </html:div> 1.22 + </content> 1.23 + 1.24 + <implementation implements="nsIDOMXULSelectControlElement"> 1.25 + <property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/> 1.26 + 1.27 + <property name="isBound" readonly="true" onget="return !!this._grid"/> 1.28 + <property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/> 1.29 + 1.30 + <field name="controller">null</field> 1.31 + 1.32 + <!-- collection of child items excluding empty tiles --> 1.33 + <property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/> 1.34 + <property name="itemCount" readonly="true" onget="return this.items.length;"/> 1.35 + 1.36 + <method name="isItem"> 1.37 + <parameter name="anItem"/> 1.38 + <body> 1.39 + <![CDATA[ 1.40 + // only non-empty child nodes are considered items 1.41 + return anItem && anItem.hasAttribute("value") && 1.42 + anItem.parentNode == this; 1.43 + ]]> 1.44 + </body> 1.45 + </method> 1.46 + 1.47 + <!-- nsIDOMXULMultiSelectControlElement (not fully implemented) --> 1.48 + 1.49 + <method name="clearSelection"> 1.50 + <body> 1.51 + <![CDATA[ 1.52 + // 'selection' and 'selected' are confusingly overloaded here 1.53 + // as richgrid is adopting multi-select behavior, but select/selected are already being 1.54 + // used to describe triggering the default action of a tile 1.55 + if (this._selectedItem){ 1.56 + this._selectedItem.removeAttribute("selected"); 1.57 + this._selectedItem = null; 1.58 + } 1.59 + 1.60 + for (let childItem of this.selectedItems) { 1.61 + childItem.removeAttribute("selected"); 1.62 + } 1.63 + ]]> 1.64 + </body> 1.65 + </method> 1.66 + 1.67 + <method name="toggleItemSelection"> 1.68 + <parameter name="anItem"/> 1.69 + <body> 1.70 + <![CDATA[ 1.71 + if (!this.isItem(anItem)) 1.72 + return; 1.73 + 1.74 + let wasSelected = anItem.selected; 1.75 + if ("single" == this.getAttribute("seltype")) { 1.76 + this.clearSelection(); 1.77 + } 1.78 + this._selectedItem = wasSelected ? null : anItem; 1.79 + if (wasSelected) 1.80 + anItem.removeAttribute("selected"); 1.81 + else 1.82 + anItem.setAttribute("selected", true); 1.83 + this._fireEvent("selectionchange"); 1.84 + ]]> 1.85 + </body> 1.86 + </method> 1.87 + 1.88 + <method name="selectItem"> 1.89 + <parameter name="anItem"/> 1.90 + <body> 1.91 + <![CDATA[ 1.92 + if (!this.isItem(anItem)) 1.93 + return; 1.94 + let wasSelected = anItem.selected, 1.95 + isSingleMode = ("single" == this.getAttribute("seltype")); 1.96 + if (isSingleMode) { 1.97 + this.clearSelection(); 1.98 + } 1.99 + this._selectedItem = anItem; 1.100 + if (wasSelected) { 1.101 + return; 1.102 + } 1.103 + anItem.setAttribute("selected", true); 1.104 + if (isSingleMode) { 1.105 + this._fireEvent("select"); 1.106 + } else { 1.107 + this._fireEvent("selectionchange"); 1.108 + } 1.109 + ]]> 1.110 + </body> 1.111 + </method> 1.112 + 1.113 + <method name="selectNone"> 1.114 + <body> 1.115 + <![CDATA[ 1.116 + let selectedCount = this.selectedItems.length; 1.117 + this.clearSelection(); 1.118 + if (selectedCount && "single" != this.getAttribute("seltype")) { 1.119 + this._fireEvent("selectionchange"); 1.120 + } 1.121 + ]]> 1.122 + </body> 1.123 + </method> 1.124 + 1.125 + <method name="handleItemClick"> 1.126 + <parameter name="aItem"/> 1.127 + <parameter name="aEvent"/> 1.128 + <body> 1.129 + <![CDATA[ 1.130 + if (!(this.isBound && this.isItem(aItem))) 1.131 + return; 1.132 + 1.133 + if ("single" == this.getAttribute("seltype")) { 1.134 + // we'll republish this as a selectionchange event on the grid 1.135 + aEvent.stopPropagation(); 1.136 + this.selectItem(aItem); 1.137 + } 1.138 + 1.139 + if (this.controller && this.controller.handleItemClick) 1.140 + this.controller.handleItemClick(aItem, aEvent); 1.141 + ]]> 1.142 + </body> 1.143 + </method> 1.144 + 1.145 + <method name="handleItemContextMenu"> 1.146 + <parameter name="aItem"/> 1.147 + <parameter name="aEvent"/> 1.148 + <body> 1.149 + <![CDATA[ 1.150 + if (!this.isBound || this.noContext || !this.isItem(aItem)) 1.151 + return; 1.152 + // we'll republish this as a selectionchange event on the grid 1.153 + aEvent.stopPropagation(); 1.154 + this.toggleItemSelection(aItem); 1.155 + ]]> 1.156 + </body> 1.157 + </method> 1.158 + 1.159 + <property name="contextSetName" readonly="true" 1.160 + onget="return this.getAttribute('set-name');"/> 1.161 + 1.162 + <property name="contextActions"> 1.163 + <getter> 1.164 + <![CDATA[ 1.165 + // return the subset of verbs that apply to all selected tiles 1.166 + let tileNodes = this.selectedItems; 1.167 + if (!tileNodes.length) { 1.168 + return new Set(); 1.169 + } 1.170 + 1.171 + // given one or more sets of values, 1.172 + // return a set with only those values present in each 1.173 + let initialItem = tileNodes[0]; 1.174 + 1.175 + let verbSet = new Set(initialItem.contextActions); 1.176 + for (let i=1; i<tileNodes.length; i++){ 1.177 + let set = tileNodes[i].contextActions; 1.178 + for (let item of verbSet) { 1.179 + if (!set.has(item)){ 1.180 + verbSet.delete(item); 1.181 + } 1.182 + } 1.183 + } 1.184 + // add the clear-selection button if more than one tiles are selected 1.185 + if (tileNodes.length > 1) { 1.186 + verbSet.add('clear'); 1.187 + } 1.188 + // returns Set 1.189 + return verbSet; 1.190 + ]]> 1.191 + </getter> 1.192 + </property> 1.193 + 1.194 + <!-- nsIDOMXULSelectControlElement --> 1.195 + 1.196 + <field name="_selectedItem">null</field> 1.197 + <property name="selectedItem" onget="return this._selectedItem;"> 1.198 + <setter> 1.199 + <![CDATA[ 1.200 + this.selectItem(val); 1.201 + ]]> 1.202 + </setter> 1.203 + </property> 1.204 + 1.205 + <!-- partial implementation of multiple selection interface --> 1.206 + <property name="selectedItems"> 1.207 + <getter> 1.208 + <![CDATA[ 1.209 + return this.querySelectorAll("richgriditem[value][selected]"); 1.210 + ]]> 1.211 + </getter> 1.212 + </property> 1.213 + 1.214 + <property name="selectedIndex"> 1.215 + <getter> 1.216 + <![CDATA[ 1.217 + return this.getIndexOfItem(this._selectedItem); 1.218 + ]]> 1.219 + </getter> 1.220 + <setter> 1.221 + <![CDATA[ 1.222 + if (val >= 0) { 1.223 + let selected = this.getItemAtIndex(val); 1.224 + this.selectItem(selected); 1.225 + } else { 1.226 + this.selectNone(); 1.227 + } 1.228 + ]]> 1.229 + </setter> 1.230 + </property> 1.231 + 1.232 + <method name="appendItem"> 1.233 + <parameter name="aLabel"/> 1.234 + <parameter name="aValue"/> 1.235 + <parameter name="aSkipArrange"/> 1.236 + <body> 1.237 + <![CDATA[ 1.238 + let item = this.nextSlot(); 1.239 + item.setAttribute("value", aValue); 1.240 + item.setAttribute("label", aLabel); 1.241 + 1.242 + if (!aSkipArrange) 1.243 + this.arrangeItems(); 1.244 + return item; 1.245 + ]]> 1.246 + </body> 1.247 + </method> 1.248 + 1.249 + <method name="_slotValues"> 1.250 + <body><![CDATA[ 1.251 + return Array.map(this.children, (cnode) => cnode.getAttribute("value")); 1.252 + ]]></body> 1.253 + </method> 1.254 + 1.255 + <property name="minSlots" readonly="true" 1.256 + onget="return this.getAttribute('minSlots') || 3;"/> 1.257 + 1.258 + <method name="clearAll"> 1.259 + <parameter name="aSkipArrange"/> 1.260 + <body> 1.261 + <![CDATA[ 1.262 + const ELEMENT_NODE_TYPE = Components.interfaces.nsIDOMNode.ELEMENT_NODE; 1.263 + let slotCount = this.minSlots; 1.264 + let childIndex = 0; 1.265 + let child = this.firstChild; 1.266 + while (child) { 1.267 + // remove excess elements and non-element nodes 1.268 + if (child.nodeType !== ELEMENT_NODE_TYPE || childIndex+1 > slotCount) { 1.269 + let orphanNode = child; 1.270 + child = orphanNode.nextSibling; 1.271 + this.removeChild(orphanNode); 1.272 + continue; 1.273 + } 1.274 + if (child.hasAttribute("value")) { 1.275 + this._releaseSlot(child); 1.276 + } 1.277 + child = child.nextSibling; 1.278 + childIndex++; 1.279 + } 1.280 + // create our quota of item slots 1.281 + for (let count = this.childElementCount; count < slotCount; count++) { 1.282 + this.appendChild( this._createItemElement() ); 1.283 + } 1.284 + 1.285 + if (!aSkipArrange) 1.286 + this.arrangeItems(); 1.287 + ]]> 1.288 + </body> 1.289 + </method> 1.290 + 1.291 + <method name="_slotAt"> 1.292 + <parameter name="anIndex"/> 1.293 + <body> 1.294 + <![CDATA[ 1.295 + // backfill with new slots as necessary 1.296 + let count = Math.max(1+anIndex, this.minSlots) - this.childElementCount; 1.297 + for (; count > 0; count--) { 1.298 + this.appendChild( this._createItemElement() ); 1.299 + } 1.300 + return this.children[anIndex]; 1.301 + ]]> 1.302 + </body> 1.303 + </method> 1.304 + 1.305 + <method name="nextSlot"> 1.306 + <body> 1.307 + <![CDATA[ 1.308 + if (!this.itemCount) { 1.309 + return this._slotAt(0); 1.310 + } 1.311 + let lastItem = this.items[this.itemCount-1]; 1.312 + let nextIndex = 1 + Array.indexOf(this.children, lastItem); 1.313 + return this._slotAt(nextIndex); 1.314 + ]]> 1.315 + </body> 1.316 + </method> 1.317 + 1.318 + <method name="_releaseSlot"> 1.319 + <parameter name="anItem"/> 1.320 + <body> 1.321 + <![CDATA[ 1.322 + // Flush out data and state attributes so we can recycle this slot/element 1.323 + let exclude = { value: 1, tiletype: 1 }; 1.324 + let attrNames = [attr.name for (attr of anItem.attributes)]; 1.325 + for (let attrName of attrNames) { 1.326 + if (!(attrName in exclude)) 1.327 + anItem.removeAttribute(attrName); 1.328 + } 1.329 + // clear out inline styles 1.330 + anItem.removeAttribute("style"); 1.331 + // finally clear the value, which should apply the richgrid-empty-item binding 1.332 + anItem.removeAttribute("value"); 1.333 + ]]> 1.334 + </body> 1.335 + </method> 1.336 + 1.337 + <method name="insertItemAt"> 1.338 + <parameter name="anIndex"/> 1.339 + <parameter name="aLabel"/> 1.340 + <parameter name="aValue"/> 1.341 + <parameter name="aSkipArrange"/> 1.342 + <body> 1.343 + <![CDATA[ 1.344 + anIndex = Math.min(this.itemCount, anIndex); 1.345 + let insertedItem; 1.346 + let existing = this.getItemAtIndex(anIndex); 1.347 + if (existing) { 1.348 + // use an empty slot if we have one, otherwise insert it 1.349 + let childIndex = Array.indexOf(this.children, existing); 1.350 + if (childIndex > 0 && !this.children[childIndex-1].hasAttribute("value")) { 1.351 + insertedItem = this.children[childIndex-1]; 1.352 + } else { 1.353 + insertedItem = this.insertBefore(this._createItemElement(),existing); 1.354 + } 1.355 + } 1.356 + if (!insertedItem) { 1.357 + insertedItem = this._slotAt(anIndex); 1.358 + } 1.359 + insertedItem.setAttribute("value", aValue); 1.360 + insertedItem.setAttribute("label", aLabel); 1.361 + if (!aSkipArrange) 1.362 + this.arrangeItems(); 1.363 + return insertedItem; 1.364 + ]]> 1.365 + </body> 1.366 + </method> 1.367 + 1.368 + <method name="removeItemAt"> 1.369 + <parameter name="anIndex"/> 1.370 + <parameter name="aSkipArrange"/> 1.371 + <body> 1.372 + <![CDATA[ 1.373 + let item = this.getItemAtIndex(anIndex); 1.374 + if (!item) 1.375 + return null; 1.376 + return this.removeItem(item, aSkipArrange); 1.377 + ]]> 1.378 + </body> 1.379 + </method> 1.380 + 1.381 + <method name="removeItem"> 1.382 + <parameter name="aItem"/> 1.383 + <parameter name="aSkipArrange"/> 1.384 + <body> 1.385 + <![CDATA[ 1.386 + if (!this.isItem(aItem)) 1.387 + return null; 1.388 + 1.389 + let removal = this.removeChild(aItem); 1.390 + // replace the slot if necessary 1.391 + if (this.childElementCount < this.minSlots) { 1.392 + this.nextSlot(); 1.393 + } 1.394 + 1.395 + if (removal && !aSkipArrange) 1.396 + this.arrangeItems(); 1.397 + 1.398 + // note that after removal the node is unbound 1.399 + // so none of the richgriditem binding methods & properties are available 1.400 + return removal; 1.401 + ]]> 1.402 + </body> 1.403 + </method> 1.404 + 1.405 + <method name="getIndexOfItem"> 1.406 + <parameter name="anItem"/> 1.407 + <body> 1.408 + <![CDATA[ 1.409 + if (!this.isItem(anItem)) 1.410 + return -1; 1.411 + 1.412 + return Array.indexOf(this.items, anItem); 1.413 + ]]> 1.414 + </body> 1.415 + </method> 1.416 + 1.417 + <method name="getItemAtIndex"> 1.418 + <parameter name="anIndex"/> 1.419 + <body> 1.420 + <![CDATA[ 1.421 + if (!this._isIndexInBounds(anIndex)) 1.422 + return null; 1.423 + return this.items.item(anIndex); 1.424 + ]]> 1.425 + </body> 1.426 + </method> 1.427 + 1.428 + <method name="getItemsByUrl"> 1.429 + <parameter name="aUrl"/> 1.430 + <body> 1.431 + <![CDATA[ 1.432 + return this.querySelectorAll('richgriditem[value="'+aUrl+'"]'); 1.433 + ]]> 1.434 + </body> 1.435 + </method> 1.436 + 1.437 + <!-- Interface for offsetting selection and checking bounds --> 1.438 + 1.439 + <property name="isSelectionAtStart" readonly="true" 1.440 + onget="return this.selectedIndex == 0;"/> 1.441 + 1.442 + <property name="isSelectionAtEnd" readonly="true" 1.443 + onget="return this.selectedIndex == (this.itemCount - 1);"/> 1.444 + 1.445 + <property name="isSelectionInStartRow" readonly="true"> 1.446 + <getter> 1.447 + <![CDATA[ 1.448 + return this.selectedIndex < this.columnCount; 1.449 + ]]> 1.450 + </getter> 1.451 + </property> 1.452 + 1.453 + <property name="isSelectionInEndRow" readonly="true"> 1.454 + <getter> 1.455 + <![CDATA[ 1.456 + let lowerBound = (this.rowCount - 1) * this.columnCount; 1.457 + let higherBound = this.rowCount * this.columnCount; 1.458 + 1.459 + return this.selectedIndex >= lowerBound && 1.460 + this.selectedIndex < higherBound; 1.461 + ]]> 1.462 + </getter> 1.463 + </property> 1.464 + 1.465 + <method name="offsetSelection"> 1.466 + <parameter name="aOffset"/> 1.467 + <body> 1.468 + <![CDATA[ 1.469 + let newIndex = this.selectedIndex + aOffset; 1.470 + if (this._isIndexInBounds(newIndex)) 1.471 + this.selectedIndex = newIndex; 1.472 + ]]> 1.473 + </body> 1.474 + </method> 1.475 + 1.476 + <method name="offsetSelectionByRow"> 1.477 + <parameter name="aRowOffset"/> 1.478 + <body> 1.479 + <![CDATA[ 1.480 + let newIndex = this.selectedIndex + (this.columnCount * aRowOffset); 1.481 + if (this._isIndexInBounds(newIndex)) 1.482 + this.selectedIndex -= this.columnCount; 1.483 + ]]> 1.484 + </body> 1.485 + </method> 1.486 + 1.487 + <!-- Interface for grid layout management --> 1.488 + 1.489 + <field name="_rowCount">0</field> 1.490 + <property name="rowCount" readonly="true" onget="return this._rowCount;"/> 1.491 + <field name="_columnCount">0</field> 1.492 + <property name="columnCount" readonly="true" onget="return this._columnCount;"/> 1.493 + <property name="_containerSize"> 1.494 + <getter><![CDATA[ 1.495 + // return the rect that represents our bounding box 1.496 + let containerNode = this.hasAttribute("flex") ? this : this.parentNode; 1.497 + let rect = containerNode.getBoundingClientRect(); 1.498 + // return falsy if the container has no height 1.499 + return rect.height ? { 1.500 + width: rect.width, 1.501 + height: rect.height 1.502 + } : null; 1.503 + ]]></getter> 1.504 + </property> 1.505 + 1.506 + <property name="_itemSize"> 1.507 + <getter><![CDATA[ 1.508 + // return the dimensions that represent an item in the grid 1.509 + 1.510 + // grab tile/item dimensions 1.511 + this._tileSizes = this._getTileSizes(); 1.512 + 1.513 + let type = this.getAttribute("tiletype") || "default"; 1.514 + let dims = this._tileSizes && this._tileSizes[type]; 1.515 + if (!dims) { 1.516 + throw new Error("Missing tile sizes for '" + type + "' type"); 1.517 + } 1.518 + return dims; 1.519 + ]]></getter> 1.520 + </property> 1.521 + 1.522 + <!-- do conditions allow layout/arrange of the grid? --> 1.523 + <property name="_canLayout" readonly="true"> 1.524 + <getter> 1.525 + <![CDATA[ 1.526 + if (!(this._grid && this._grid.style)) { 1.527 + return false; 1.528 + } 1.529 + 1.530 + let gridItemSize = this._itemSize; 1.531 + 1.532 + // If we don't have valid item dimensions we can't arrange yet 1.533 + if (!(gridItemSize && gridItemSize.height)) { 1.534 + return false; 1.535 + } 1.536 + 1.537 + let container = this._containerSize; 1.538 + // If we don't have valid container dimensions we can't arrange yet 1.539 + if (!(container && container.height)) { 1.540 + return false; 1.541 + } 1.542 + return true; 1.543 + ]]> 1.544 + </getter> 1.545 + </property> 1.546 + 1.547 + <field name="_scheduledArrangeItemsTimerId">null</field> 1.548 + <field name="_scheduledArrangeItemsTries">0</field> 1.549 + <field name="_maxArrangeItemsRetries">5</field> 1.550 + 1.551 + <method name="_scheduleArrangeItems"> 1.552 + <parameter name="aTime"/> 1.553 + <body> 1.554 + <![CDATA[ 1.555 + // cap the number of times we reschedule calling arrangeItems 1.556 + if ( 1.557 + !this._scheduledArrangeItemsTimerId && 1.558 + this._maxArrangeItemsRetries > this._scheduledArrangeItemsTries 1.559 + ) { 1.560 + this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), aTime || 0); 1.561 + // track how many times we've attempted arrangeItems 1.562 + this._scheduledArrangeItemsTries++; 1.563 + } 1.564 + ]]> 1.565 + </body> 1.566 + </method> 1.567 + 1.568 + <method name="arrangeItems"> 1.569 + <body> 1.570 + <![CDATA[ 1.571 + if (this.hasAttribute("deferlayout")) { 1.572 + return; 1.573 + } 1.574 + if (!this._canLayout) { 1.575 + // try again later 1.576 + this._scheduleArrangeItems(); 1.577 + return; 1.578 + } 1.579 + 1.580 + let itemDims = this._itemSize; 1.581 + let containerDims = this._containerSize; 1.582 + let slotsCount = this.childElementCount; 1.583 + 1.584 + // reset the flags 1.585 + if (this._scheduledArrangeItemsTimerId) { 1.586 + clearTimeout(this._scheduledArrangeItemsTimerId); 1.587 + delete this._scheduledArrangeItemsTimerId; 1.588 + } 1.589 + this._scheduledArrangeItemsTries = 0; 1.590 + 1.591 + // clear explicit width and columns before calculating from avail. height again 1.592 + let gridStyle = this._grid.style; 1.593 + gridStyle.removeProperty("min-width"); 1.594 + gridStyle.removeProperty("-moz-column-count"); 1.595 + 1.596 + if (this.hasAttribute("vertical")) { 1.597 + this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1; 1.598 + this._rowCount = Math.floor(slotsCount / this._columnCount); 1.599 + } else { 1.600 + // rows attribute is fixed number of rows 1.601 + let maxRows = Math.floor(containerDims.height / itemDims.height); 1.602 + this._rowCount = this.getAttribute("rows") ? 1.603 + // fit indicated rows when possible 1.604 + Math.min(maxRows, this.getAttribute("rows")) : 1.605 + // at least 1 row 1.606 + Math.min(maxRows, slotsCount) || 1; 1.607 + 1.608 + // columns attribute is min number of cols 1.609 + this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1; 1.610 + if (this.getAttribute("columns")) { 1.611 + this._columnCount = Math.max(this._columnCount, this.getAttribute("columns")); 1.612 + } 1.613 + } 1.614 + 1.615 + // width is typically auto, cap max columns by truncating items collection 1.616 + // or, setting max-width style property with overflow hidden 1.617 + if (this._columnCount) { 1.618 + gridStyle.MozColumnCount = this._columnCount; 1.619 + } 1.620 + this._fireEvent("arranged"); 1.621 + ]]> 1.622 + </body> 1.623 + </method> 1.624 + <method name="arrangeItemsNow"> 1.625 + <body> 1.626 + <![CDATA[ 1.627 + this.removeAttribute("deferlayout"); 1.628 + // cancel any scheduled arrangeItems and reset flags 1.629 + if (this._scheduledArrangeItemsTimerId) { 1.630 + clearTimeout(this._scheduledArrangeItemsTimerId); 1.631 + delete this._scheduledArrangeItemsTimerId; 1.632 + } 1.633 + this._scheduledArrangeItemsTries = 0; 1.634 + // pass over any params 1.635 + return this.arrangeItems.apply(this, arguments); 1.636 + ]]> 1.637 + </body> 1.638 + </method> 1.639 + 1.640 + <!-- Inteface to suppress selection events --> 1.641 + <property name="suppressOnSelect" 1.642 + onget="return this.getAttribute('suppressonselect') == 'true';" 1.643 + onset="this.setAttribute('suppressonselect', val);"/> 1.644 + <property name="noContext" 1.645 + onget="return this.hasAttribute('nocontext');" 1.646 + onset="if (val) this.setAttribute('nocontext', true); else this.removeAttribute('nocontext');"/> 1.647 + <property name="crossSlideBoundary" 1.648 + onget="return this.hasAttribute('crossslideboundary')? this.getAttribute('crossslideboundary') : Infinity;"/> 1.649 + 1.650 + <!-- Internal methods --> 1.651 + <field name="_xslideHandler"/> 1.652 + <constructor> 1.653 + <![CDATA[ 1.654 + // create our quota of item slots 1.655 + for (let count = this.childElementCount, slotCount = this.minSlots; 1.656 + count < slotCount; count++) { 1.657 + this.appendChild( this._createItemElement() ); 1.658 + } 1.659 + if (this.controller && this.controller.gridBoundCallback != undefined) 1.660 + this.controller.gridBoundCallback(); 1.661 + // XXX This event was never actually implemented (bug 223411). 1.662 + let event = document.createEvent("Events"); 1.663 + event.initEvent("contentgenerated", true, true); 1.664 + this.dispatchEvent(event); 1.665 + ]]> 1.666 + </constructor> 1.667 + 1.668 + <destructor> 1.669 + <![CDATA[ 1.670 + this.disableCrossSlide(); 1.671 + ]]> 1.672 + </destructor> 1.673 + <method name="enableCrossSlide"> 1.674 + <body> 1.675 + <![CDATA[ 1.676 + // set up cross-slide gesture handling for multiple-selection grids 1.677 + if (!this._xslideHandler && 1.678 + "undefined" !== typeof CrossSlide && !this.noContext) { 1.679 + this._xslideHandler = new CrossSlide.Handler(this, { 1.680 + REARRANGESTART: this.crossSlideBoundary 1.681 + }); 1.682 + } 1.683 + ]]> 1.684 + </body> 1.685 + </method> 1.686 + 1.687 + <method name="disableCrossSlide"> 1.688 + <body> 1.689 + <![CDATA[ 1.690 + if (this._xslideHandler) { 1.691 + this.removeEventListener("touchstart", this._xslideHandler); 1.692 + this.removeEventListener("touchmove", this._xslideHandler); 1.693 + this.removeEventListener("touchend", this._xslideHandler); 1.694 + this._xslideHandler = null; 1.695 + } 1.696 + ]]> 1.697 + </body> 1.698 + </method> 1.699 + 1.700 + <property name="tileWidth" readonly="true" onget="return this._itemSize.width"/> 1.701 + <property name="tileHeight" readonly="true" onget="return this._itemSize.height"/> 1.702 + <field name="_tileStyleSheetName">"tiles.css"</field> 1.703 + <method name="_getTileSizes"> 1.704 + <body> 1.705 + <![CDATA[ 1.706 + // Tile sizes are constants, this avoids the need to measure a rendered item before grid layout 1.707 + // The defines.inc used by the theme CSS is the single source of truth for these values 1.708 + // This method locates and parses out (just) those dimensions from the stylesheet 1.709 + 1.710 + let typeSizes = this.ownerDocument.defaultView._richgridTileSizes; 1.711 + if (typeSizes && typeSizes["default"]) { 1.712 + return typeSizes; 1.713 + } 1.714 + 1.715 + // cache sizes on the global window object, for reuse between bound nodes 1.716 + typeSizes = this.ownerDocument.defaultView._richgridTileSizes = {}; 1.717 + 1.718 + let sheets = this.ownerDocument.styleSheets; 1.719 + // The (first matching) rules that will give us tile type => width/height values 1.720 + // The keys in this object are string-matched against the selectorText 1.721 + // of rules in our stylesheet. Quoted values in a selector will always use " not ' 1.722 + let typeSelectors = { 1.723 + 'richgriditem' : "default", 1.724 + 'richgriditem[tiletype="thumbnail"]': "thumbnail", 1.725 + 'richgriditem[search]': "search", 1.726 + 'richgriditem[compact]': "compact" 1.727 + }; 1.728 + let rules, sheet; 1.729 + for (let i=0; (sheet=sheets[i]); i++) { 1.730 + if (sheet.href && sheet.href.endsWith( this._tileStyleSheetName )) { 1.731 + rules = sheet.cssRules; 1.732 + break; 1.733 + } 1.734 + } 1.735 + if (rules) { 1.736 + // walk the stylesheet rules until we've matched all our selectors 1.737 + for (let i=0, rule;(rule=rules[i]); i++) { 1.738 + let type = rule.selectorText && typeSelectors[rule.selectorText]; 1.739 + if (type) { 1.740 + let sizes = typeSizes[type] = {}; 1.741 + typeSelectors[type] = null; 1.742 + delete typeSelectors[type]; 1.743 + // we assume px unit for tile dimension values 1.744 + sizes.width = parseInt(rule.style.getPropertyValue("width")); 1.745 + sizes.height = parseInt(rule.style.getPropertyValue("height")); 1.746 + } 1.747 + if (!Object.keys(typeSelectors).length) 1.748 + break; 1.749 + } 1.750 + } else { 1.751 + throw new Error("Failed to find stylesheet to parse out richgriditem dimensions\n"); 1.752 + } 1.753 + return typeSizes; 1.754 + ]]> 1.755 + </body> 1.756 + </method> 1.757 + 1.758 + <method name="_isIndexInBounds"> 1.759 + <parameter name="anIndex"/> 1.760 + <body> 1.761 + <![CDATA[ 1.762 + return anIndex >= 0 && anIndex < this.itemCount; 1.763 + ]]> 1.764 + </body> 1.765 + </method> 1.766 + 1.767 + <method name="_createItemElement"> 1.768 + <parameter name="aLabel"/> 1.769 + <parameter name="aValue"/> 1.770 + <body> 1.771 + <![CDATA[ 1.772 + let item = this.ownerDocument.createElement("richgriditem"); 1.773 + if (aValue) { 1.774 + item.setAttribute("value", aValue); 1.775 + } 1.776 + if (aLabel) { 1.777 + item.setAttribute("label", aLabel); 1.778 + } 1.779 + if (this.hasAttribute("tiletype")) { 1.780 + item.setAttribute("tiletype", this.getAttribute("tiletype")); 1.781 + } 1.782 + return item; 1.783 + ]]> 1.784 + </body> 1.785 + </method> 1.786 + 1.787 + <method name="_fireEvent"> 1.788 + <parameter name="aType"/> 1.789 + <body> 1.790 + <![CDATA[ 1.791 + switch (aType) { 1.792 + case "select" : 1.793 + case "selectionchange" : 1.794 + if (this.suppressOnSelect) 1.795 + return; 1.796 + break; 1.797 + case "arranged" : 1.798 + break; 1.799 + } 1.800 + 1.801 + let event = document.createEvent("Events"); 1.802 + event.initEvent(aType, true, true); 1.803 + this.dispatchEvent(event); 1.804 + ]]> 1.805 + </body> 1.806 + </method> 1.807 + 1.808 + <method name="bendItem"> 1.809 + <parameter name="aItem"/> 1.810 + <parameter name="aEvent"/> 1.811 + <body><![CDATA[ 1.812 + // apply the transform to the contentBox element of the item 1.813 + let bendNode = this.isItem(aItem) ? aItem._contentBox : null; 1.814 + if (!bendNode || aItem.hasAttribute("bending")) 1.815 + return; 1.816 + 1.817 + let event = aEvent; 1.818 + let rect = bendNode.getBoundingClientRect(); 1.819 + let angle; 1.820 + let x = (event.clientX - rect.left) / rect.width; 1.821 + let y = (event.clientY - rect.top) / rect.height; 1.822 + let perspective = '450px'; 1.823 + // scaling factors for the angle of deflection, 1.824 + // based on the aspect-ratio of the tile 1.825 + let aspectRatio = rect.width/rect.height; 1.826 + let deflectX = 10 * Math.ceil(1/aspectRatio); 1.827 + let deflectY = 10 * Math.ceil(aspectRatio); 1.828 + 1.829 + if (Math.abs(x - .5) < .1 && Math.abs(y - .5) < .1) { 1.830 + bendNode.style.transform = "perspective("+perspective+") translateZ(-10px)"; 1.831 + } 1.832 + else if (x > y) { 1.833 + if (1 - y > x) { 1.834 + angle = Math.ceil((.5 - y) * deflectY); 1.835 + bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)"; 1.836 + bendNode.style.transformOrigin = "center bottom"; 1.837 + } else { 1.838 + angle = Math.ceil((x - .5) * deflectX); 1.839 + bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)"; 1.840 + bendNode.style.transformOrigin = "left center"; 1.841 + } 1.842 + } else { 1.843 + if (1 - y < x) { 1.844 + angle = -Math.ceil((y - .5) * deflectY); 1.845 + bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)"; 1.846 + bendNode.style.transformOrigin = "center top"; 1.847 + } else { 1.848 + angle = -Math.ceil((.5 - x) * deflectX); 1.849 + bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)"; 1.850 + bendNode.style.transformOrigin = "right center"; 1.851 + } 1.852 + } 1.853 + // mark when bend effect is applied 1.854 + aItem.setAttribute("bending", true); 1.855 + ]]></body> 1.856 + </method> 1.857 + 1.858 + <method name="unbendItem"> 1.859 + <parameter name="aItem"/> 1.860 + <body><![CDATA[ 1.861 + // clear the 'bend' transform on the contentBox element of the item 1.862 + let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox; 1.863 + if (bendNode && aItem.hasAttribute("bending")) { 1.864 + bendNode.style.removeProperty('transform'); 1.865 + bendNode.style.removeProperty('transformOrigin'); 1.866 + aItem.removeAttribute("bending"); 1.867 + } 1.868 + ]]></body> 1.869 + </method> 1.870 + </implementation> 1.871 + <handlers> 1.872 + <!-- item bend effect handlers --> 1.873 + <handler event="mousedown" button="0" phase="capturing" action="this.bendItem(event.target, event)"/> 1.874 + <handler event="touchstart" action="this.bendItem(event.target, event.touches[0])"/> 1.875 + <handler event="mouseup" button="0" action="this.unbendItem(event.target)"/> 1.876 + <handler event="mouseout" button="0" action="this.unbendItem(event.target)"/> 1.877 + <handler event="touchend" action="this.unbendItem(event.target)"/> 1.878 + <handler event="touchcancel" action="this.unbendItem(event.target)"/> 1.879 + <!-- /item bend effect handler --> 1.880 + 1.881 + <handler event="context-action"> 1.882 + <![CDATA[ 1.883 + // context-action is an event fired by the appbar typically 1.884 + // which directs us to do something to the selected tiles 1.885 + switch (event.action) { 1.886 + case "clear": 1.887 + this.selectNone(); 1.888 + break; 1.889 + default: 1.890 + if (this.controller && this.controller.doActionOnSelectedTiles) { 1.891 + this.controller.doActionOnSelectedTiles(event.action, event); 1.892 + } 1.893 + } 1.894 + ]]> 1.895 + </handler> 1.896 + <handler event="MozCrossSliding"> 1.897 + <![CDATA[ 1.898 + // MozCrossSliding is swipe gesture across a tile 1.899 + // The tile should follow the drag to reinforce the gesture 1.900 + // (with inertia/speedbump behavior) 1.901 + let state = event.crossSlidingState; 1.902 + let thresholds = this._xslideHandler.thresholds; 1.903 + let transformValue; 1.904 + switch (state) { 1.905 + case "cancelled": 1.906 + this.unbendItem(event.target); 1.907 + event.target.removeAttribute('crosssliding'); 1.908 + // hopefully nothing else is transform-ing the tile 1.909 + event.target.style.removeProperty('transform'); 1.910 + break; 1.911 + case "dragging": 1.912 + case "selecting": 1.913 + // remove bend/depress effect when a cross-slide begins 1.914 + this.unbendItem(event.target); 1.915 + 1.916 + event.target.setAttribute("crosssliding", true); 1.917 + // just track the mouse in the initial phases of the drag gesture 1.918 + transformValue = (event.direction=='x') ? 1.919 + 'translateX('+event.delta+'px)' : 1.920 + 'translateY('+event.delta+'px)'; 1.921 + event.target.style.transform = transformValue; 1.922 + break; 1.923 + case "selectSpeedBumping": 1.924 + case "speedBumping": 1.925 + event.target.setAttribute('crosssliding', true); 1.926 + // in speed-bump phase, we add inertia to the drag 1.927 + let offset = CrossSlide.speedbump( 1.928 + event.delta, 1.929 + thresholds.SPEEDBUMPSTART, 1.930 + thresholds.SPEEDBUMPEND 1.931 + ); 1.932 + transformValue = (event.direction=='x') ? 1.933 + 'translateX('+offset+'px)' : 1.934 + 'translateY('+offset+'px)'; 1.935 + event.target.style.transform = transformValue; 1.936 + break; 1.937 + // "rearranging" case not used or implemented here 1.938 + case "completed": 1.939 + event.target.removeAttribute('crosssliding'); 1.940 + event.target.style.removeProperty('transform'); 1.941 + break; 1.942 + } 1.943 + ]]> 1.944 + </handler> 1.945 + <handler event="MozCrossSlideSelect"> 1.946 + <![CDATA[ 1.947 + if (this.noContext) 1.948 + return; 1.949 + this.toggleItemSelection(event.target); 1.950 + ]]> 1.951 + </handler> 1.952 + </handlers> 1.953 + </binding> 1.954 + 1.955 + <binding id="richgrid-item"> 1.956 + <content> 1.957 + <html:div anonid="anon-tile" class="tile-content" xbl:inherits="customImage"> 1.958 + <html:div class="tile-start-container" xbl:inherits="customImage"> 1.959 + <html:div class="tile-icon-box" anonid="anon-tile-icon-box"><xul:image anonid="anon-tile-icon" xbl:inherits="src=iconURI"/></html:div> 1.960 + </html:div> 1.961 + <html:div anonid="anon-tile-label" class="tile-desc" xbl:inherits="xbl:text=label"/> 1.962 + </html:div> 1.963 + </content> 1.964 + 1.965 + <implementation> 1.966 + <property name="isBound" readonly="true" onget="return !!this._icon"/> 1.967 + <constructor> 1.968 + <![CDATA[ 1.969 + this.refresh(); 1.970 + ]]> 1.971 + </constructor> 1.972 + <property name="_contentBox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-content');"/> 1.973 + <property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-desc');"/> 1.974 + <property name="_top" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-start-container');"/> 1.975 + <property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon');"/> 1.976 + <property name="_iconBox" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon-box');"/> 1.977 + <property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-label');"/> 1.978 + <property name="iconSrc" 1.979 + onset="this._icon.src = val; this.setAttribute('iconURI', val);" 1.980 + onget="return this._icon.src;" /> 1.981 + 1.982 + <property name="selected" 1.983 + onget="return this.hasAttribute('selected');" 1.984 + onset="if (val) this.setAttribute('selected', val); else this.removeAttribute('selected');" /> 1.985 + <property name="url" 1.986 + onget="return this.getAttribute('value')" 1.987 + onset="this.setAttribute('value', val);"/> 1.988 + <property name="label" 1.989 + onget="return this._label.getAttribute('value')" 1.990 + onset="this.setAttribute('label', val); this._label.setAttribute('value', val);"/> 1.991 + <property name="pinned" 1.992 + onget="return this.hasAttribute('pinned')" 1.993 + onset="if (val) { this.setAttribute('pinned', val) } else this.removeAttribute('pinned');"/> 1.994 + 1.995 + <method name="refresh"> 1.996 + <body> 1.997 + <![CDATA[ 1.998 + // Prevent an exception in case binding is not done yet. 1.999 + if (!this.isBound) 1.1000 + return; 1.1001 + 1.1002 + // Seed the binding properties from bound-node attribute values 1.1003 + // Usage: node.refresh() 1.1004 + // - reinitializes all binding properties from their associated attributes 1.1005 + 1.1006 + this.iconSrc = this.getAttribute('iconURI'); 1.1007 + this.color = this.getAttribute("customColor"); 1.1008 + this.label = this.getAttribute('label'); 1.1009 + // url getter just looks directly at attribute 1.1010 + // selected getter just looks directly at attribute 1.1011 + // pinned getter just looks directly at attribute 1.1012 + // value getter just looks directly at attribute 1.1013 + this._contextActions = null; 1.1014 + this.refreshBackgroundImage(); 1.1015 + ]]> 1.1016 + </body> 1.1017 + </method> 1.1018 + 1.1019 + <property name="control"> 1.1020 + <getter><![CDATA[ 1.1021 + let parent = this.parentNode; 1.1022 + while (parent && parent != this.ownerDocument.documentElement) { 1.1023 + if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement) 1.1024 + return parent; 1.1025 + parent = parent.parentNode; 1.1026 + } 1.1027 + return null; 1.1028 + ]]></getter> 1.1029 + </property> 1.1030 + 1.1031 + <property name="color" onget="return this.getAttribute('customColor');"> 1.1032 + <setter><![CDATA[ 1.1033 + if (val) { 1.1034 + this.setAttribute("customColor", val); 1.1035 + this._contentBox.style.backgroundColor = val; 1.1036 + 1.1037 + // overridden in tiles.css for non-thumbnail types 1.1038 + this._label.style.backgroundColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.8)'); 1.1039 + 1.1040 + // Small icons get a border+background-color treatment. 1.1041 + // See tiles.css for large icon overrides 1.1042 + this._iconBox.style.borderColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.6)'); 1.1043 + this._iconBox.style.backgroundColor = this.hasAttribute("tintColor") ? 1.1044 + this.getAttribute("tintColor") : "#fff"; 1.1045 + } else { 1.1046 + this.removeAttribute("customColor"); 1.1047 + this._contentBox.style.removeProperty("background-color"); 1.1048 + this._label.style.removeProperty("background-color"); 1.1049 + this._iconBox.style.removeProperty("border-color"); 1.1050 + this._iconBox.style.removeProperty("background-color"); 1.1051 + } 1.1052 + ]]></setter> 1.1053 + </property> 1.1054 + 1.1055 + <property name="backgroundImage" onget="return this.getAttribute('customImage');"> 1.1056 + <setter><![CDATA[ 1.1057 + if (val) { 1.1058 + this.setAttribute("customImage", val); 1.1059 + this._top.style.backgroundImage = val; 1.1060 + } else { 1.1061 + this.removeAttribute("customImage"); 1.1062 + this._top.style.removeProperty("background-image"); 1.1063 + } 1.1064 + ]]></setter> 1.1065 + </property> 1.1066 + 1.1067 + <method name="refreshBackgroundImage"> 1.1068 + <body><![CDATA[ 1.1069 + if (!this.isBound) 1.1070 + return; 1.1071 + if (this.backgroundImage) { 1.1072 + this._top.style.removeProperty("background-image"); 1.1073 + this._top.style.setProperty("background-image", this.backgroundImage); 1.1074 + } 1.1075 + ]]></body> 1.1076 + </method> 1.1077 + 1.1078 + <field name="_contextActions">null</field> 1.1079 + <property name="contextActions"> 1.1080 + <getter> 1.1081 + <![CDATA[ 1.1082 + if (!this._contextActions) { 1.1083 + this._contextActions = new Set(); 1.1084 + let actionSet = this._contextActions; 1.1085 + let actions = this.getAttribute("data-contextactions"); 1.1086 + if (actions) { 1.1087 + actions.split(/[,\s]+/).forEach(function(verb){ 1.1088 + actionSet.add(verb); 1.1089 + }); 1.1090 + } 1.1091 + } 1.1092 + return this._contextActions; 1.1093 + ]]> 1.1094 + </getter> 1.1095 + </property> 1.1096 + </implementation> 1.1097 + 1.1098 + <handlers> 1.1099 + <handler event="click" button="0"> 1.1100 + <![CDATA[ 1.1101 + // left-click/touch handler 1.1102 + this.control.handleItemClick(this, event); 1.1103 + // Stop this from bubbling, when the richgrid container 1.1104 + // receives click events, we blur the nav bar. 1.1105 + event.stopPropagation(); 1.1106 + ]]> 1.1107 + </handler> 1.1108 + 1.1109 + <handler event="contextmenu"> 1.1110 + <![CDATA[ 1.1111 + // fires for right-click, long-click and (keyboard) contextmenu input 1.1112 + // toggle the selected state of tiles in a grid 1.1113 + let gridParent = this.control; 1.1114 + if (!this.isBound || !gridParent) 1.1115 + return; 1.1116 + gridParent.handleItemContextMenu(this, event); 1.1117 + ]]> 1.1118 + </handler> 1.1119 + </handlers> 1.1120 + </binding> 1.1121 + 1.1122 + <binding id="richgrid-empty-item"> 1.1123 + <content> 1.1124 + <html:div anonid="anon-tile" class="tile-content"></html:div> 1.1125 + </content> 1.1126 + </binding> 1.1127 + 1.1128 +</bindings>