browser/metro/base/content/bindings/grid.xml

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     1 <?xml version="1.0"?>
     2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
     3    - License, v. 2.0. If a copy of the MPL was not distributed with this
     4    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
     6 <bindings
     7     xmlns="http://www.mozilla.org/xbl"
     8     xmlns:xbl="http://www.mozilla.org/xbl"
     9     xmlns:html="http://www.w3.org/1999/xhtml"
    10     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    12   <binding id="richgrid"
    13            extends="chrome://global/content/bindings/general.xml#basecontrol">
    15     <content>
    16       <html:div id="grid-div" anonid="grid" class="richgrid-grid" xbl:inherits="compact">
    17         <children/>
    18       </html:div>
    19     </content>
    21     <implementation implements="nsIDOMXULSelectControlElement">
    22       <property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/>
    24       <property name="isBound" readonly="true" onget="return !!this._grid"/>
    25       <property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/>
    27       <field name="controller">null</field>
    29       <!-- collection of child items excluding empty tiles -->
    30       <property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/>
    31       <property name="itemCount" readonly="true" onget="return this.items.length;"/>
    33       <method name="isItem">
    34         <parameter name="anItem"/>
    35         <body>
    36           <![CDATA[
    37             // only non-empty child nodes are considered items
    38             return anItem && anItem.hasAttribute("value") &&
    39                    anItem.parentNode == this;
    40           ]]>
    41         </body>
    42       </method>
    44       <!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
    46       <method name="clearSelection">
    47         <body>
    48           <![CDATA[
    49             // 'selection' and 'selected' are confusingly overloaded here
    50             // as richgrid is adopting multi-select behavior, but select/selected are already being
    51             // used to describe triggering the default action of a tile
    52             if (this._selectedItem){
    53               this._selectedItem.removeAttribute("selected");
    54               this._selectedItem = null;
    55             }
    57             for (let childItem of this.selectedItems) {
    58               childItem.removeAttribute("selected");
    59             }
    60           ]]>
    61         </body>
    62       </method>
    64       <method name="toggleItemSelection">
    65         <parameter name="anItem"/>
    66         <body>
    67           <![CDATA[
    68             if (!this.isItem(anItem))
    69               return;
    71             let wasSelected = anItem.selected;
    72             if ("single" == this.getAttribute("seltype")) {
    73               this.clearSelection();
    74             }
    75             this._selectedItem = wasSelected ? null : anItem;
    76             if (wasSelected)
    77               anItem.removeAttribute("selected");
    78             else
    79               anItem.setAttribute("selected", true);
    80             this._fireEvent("selectionchange");
    81           ]]>
    82         </body>
    83       </method>
    85       <method name="selectItem">
    86         <parameter name="anItem"/>
    87         <body>
    88           <![CDATA[
    89             if (!this.isItem(anItem))
    90               return;
    91             let wasSelected = anItem.selected,
    92                 isSingleMode = ("single" == this.getAttribute("seltype"));
    93             if (isSingleMode) {
    94               this.clearSelection();
    95             }
    96             this._selectedItem = anItem;
    97             if (wasSelected) {
    98               return;
    99             }
   100             anItem.setAttribute("selected", true);
   101             if (isSingleMode) {
   102               this._fireEvent("select");
   103             } else {
   104               this._fireEvent("selectionchange");
   105             }
   106           ]]>
   107         </body>
   108       </method>
   110       <method name="selectNone">
   111         <body>
   112           <![CDATA[
   113             let selectedCount = this.selectedItems.length;
   114             this.clearSelection();
   115             if (selectedCount && "single" != this.getAttribute("seltype")) {
   116               this._fireEvent("selectionchange");
   117             }
   118           ]]>
   119         </body>
   120       </method>
   122       <method name="handleItemClick">
   123         <parameter name="aItem"/>
   124         <parameter name="aEvent"/>
   125         <body>
   126           <![CDATA[
   127             if (!(this.isBound && this.isItem(aItem)))
   128               return;
   130             if ("single" == this.getAttribute("seltype")) {
   131               // we'll republish this as a selectionchange event on the grid
   132               aEvent.stopPropagation();
   133               this.selectItem(aItem);
   134             }
   136             if (this.controller && this.controller.handleItemClick)
   137               this.controller.handleItemClick(aItem, aEvent);
   138           ]]>
   139         </body>
   140       </method>
   142       <method name="handleItemContextMenu">
   143         <parameter name="aItem"/>
   144         <parameter name="aEvent"/>
   145         <body>
   146           <![CDATA[
   147             if (!this.isBound || this.noContext || !this.isItem(aItem))
   148               return;
   149             // we'll republish this as a selectionchange event on the grid
   150             aEvent.stopPropagation();
   151             this.toggleItemSelection(aItem);
   152           ]]>
   153         </body>
   154       </method>
   156       <property name="contextSetName" readonly="true"
   157                 onget="return this.getAttribute('set-name');"/>
   159       <property name="contextActions">
   160         <getter>
   161           <![CDATA[
   162             // return the subset of verbs that apply to all selected tiles
   163             let tileNodes = this.selectedItems;
   164             if (!tileNodes.length) {
   165               return new Set();
   166             }
   168             // given one or more sets of values,
   169             // return a set with only those values present in each
   170             let initialItem = tileNodes[0];
   172             let verbSet = new Set(initialItem.contextActions);
   173             for (let i=1; i<tileNodes.length; i++){
   174               let set = tileNodes[i].contextActions;
   175               for (let item of verbSet) {
   176                 if (!set.has(item)){
   177                   verbSet.delete(item);
   178                 }
   179               }
   180             }
   181             // add the clear-selection button if more than one tiles are selected
   182             if (tileNodes.length > 1) {
   183               verbSet.add('clear');
   184             }
   185             // returns Set
   186             return verbSet;
   187           ]]>
   188         </getter>
   189       </property>
   191     <!-- nsIDOMXULSelectControlElement -->
   193       <field name="_selectedItem">null</field>
   194       <property name="selectedItem" onget="return this._selectedItem;">
   195         <setter>
   196           <![CDATA[
   197             this.selectItem(val);
   198           ]]>
   199         </setter>
   200       </property>
   202       <!-- partial implementation of multiple selection interface -->
   203       <property name="selectedItems">
   204         <getter>
   205           <![CDATA[
   206             return this.querySelectorAll("richgriditem[value][selected]");
   207           ]]>
   208         </getter>
   209       </property>
   211       <property name="selectedIndex">
   212         <getter>
   213           <![CDATA[
   214             return this.getIndexOfItem(this._selectedItem);
   215           ]]>
   216         </getter>
   217         <setter>
   218           <![CDATA[
   219             if (val >= 0) {
   220               let selected = this.getItemAtIndex(val);
   221               this.selectItem(selected);
   222             } else {
   223               this.selectNone();
   224             }
   225           ]]>
   226         </setter>
   227       </property>
   229       <method name="appendItem">
   230         <parameter name="aLabel"/>
   231         <parameter name="aValue"/>
   232         <parameter name="aSkipArrange"/>
   233         <body>
   234           <![CDATA[
   235             let item = this.nextSlot();
   236             item.setAttribute("value", aValue);
   237             item.setAttribute("label", aLabel);
   239             if (!aSkipArrange)
   240               this.arrangeItems();
   241             return item;
   242           ]]>
   243         </body>
   244       </method>
   246       <method name="_slotValues">
   247         <body><![CDATA[
   248           return Array.map(this.children, (cnode) => cnode.getAttribute("value"));
   249         ]]></body>
   250       </method>
   252       <property name="minSlots" readonly="true"
   253                 onget="return this.getAttribute('minSlots') || 3;"/>
   255       <method name="clearAll">
   256         <parameter name="aSkipArrange"/>
   257         <body>
   258           <![CDATA[
   259             const ELEMENT_NODE_TYPE = Components.interfaces.nsIDOMNode.ELEMENT_NODE;
   260             let slotCount = this.minSlots;
   261             let childIndex = 0;
   262             let child = this.firstChild;
   263             while (child) {
   264               // remove excess elements and non-element nodes
   265               if (child.nodeType !== ELEMENT_NODE_TYPE || childIndex+1 > slotCount) {
   266                 let orphanNode = child;
   267                 child = orphanNode.nextSibling;
   268                 this.removeChild(orphanNode);
   269                 continue;
   270               }
   271               if (child.hasAttribute("value")) {
   272                 this._releaseSlot(child);
   273               }
   274               child = child.nextSibling;
   275               childIndex++;
   276             }
   277             // create our quota of item slots
   278             for (let count = this.childElementCount; count < slotCount; count++) {
   279               this.appendChild( this._createItemElement() );
   280             }
   282             if (!aSkipArrange)
   283               this.arrangeItems();
   284           ]]>
   285         </body>
   286       </method>
   288       <method name="_slotAt">
   289         <parameter name="anIndex"/>
   290         <body>
   291           <![CDATA[
   292             // backfill with new slots as necessary
   293             let count = Math.max(1+anIndex, this.minSlots) - this.childElementCount;
   294             for (; count > 0; count--) {
   295               this.appendChild( this._createItemElement() );
   296             }
   297             return this.children[anIndex];
   298           ]]>
   299         </body>
   300       </method>
   302       <method name="nextSlot">
   303         <body>
   304           <![CDATA[
   305             if (!this.itemCount) {
   306               return this._slotAt(0);
   307             }
   308             let lastItem = this.items[this.itemCount-1];
   309             let nextIndex = 1 + Array.indexOf(this.children, lastItem);
   310             return this._slotAt(nextIndex);
   311           ]]>
   312         </body>
   313       </method>
   315       <method name="_releaseSlot">
   316         <parameter name="anItem"/>
   317         <body>
   318           <![CDATA[
   319             // Flush out data and state attributes so we can recycle this slot/element
   320             let exclude = { value: 1, tiletype: 1 };
   321             let attrNames = [attr.name for (attr of anItem.attributes)];
   322             for (let attrName of attrNames) {
   323               if (!(attrName in exclude))
   324                 anItem.removeAttribute(attrName);
   325             }
   326             // clear out inline styles
   327             anItem.removeAttribute("style");
   328             // finally clear the value, which should apply the richgrid-empty-item binding
   329             anItem.removeAttribute("value");
   330           ]]>
   331         </body>
   332       </method>
   334       <method name="insertItemAt">
   335         <parameter name="anIndex"/>
   336         <parameter name="aLabel"/>
   337         <parameter name="aValue"/>
   338         <parameter name="aSkipArrange"/>
   339         <body>
   340           <![CDATA[
   341             anIndex = Math.min(this.itemCount, anIndex);
   342             let insertedItem;
   343             let existing = this.getItemAtIndex(anIndex);
   344             if (existing) {
   345               // use an empty slot if we have one, otherwise insert it
   346               let childIndex = Array.indexOf(this.children, existing);
   347               if (childIndex > 0 && !this.children[childIndex-1].hasAttribute("value")) {
   348                 insertedItem = this.children[childIndex-1];
   349               } else {
   350                 insertedItem = this.insertBefore(this._createItemElement(),existing);
   351               }
   352             }
   353             if (!insertedItem) {
   354               insertedItem = this._slotAt(anIndex);
   355             }
   356             insertedItem.setAttribute("value", aValue);
   357             insertedItem.setAttribute("label", aLabel);
   358             if (!aSkipArrange)
   359               this.arrangeItems();
   360             return insertedItem;
   361           ]]>
   362         </body>
   363       </method>
   365       <method name="removeItemAt">
   366         <parameter name="anIndex"/>
   367         <parameter name="aSkipArrange"/>
   368         <body>
   369           <![CDATA[
   370             let item = this.getItemAtIndex(anIndex);
   371             if (!item)
   372               return null;
   373             return this.removeItem(item, aSkipArrange);
   374           ]]>
   375         </body>
   376       </method>
   378       <method name="removeItem">
   379         <parameter name="aItem"/>
   380         <parameter name="aSkipArrange"/>
   381         <body>
   382           <![CDATA[
   383             if (!this.isItem(aItem))
   384               return null;
   386             let removal = this.removeChild(aItem);
   387             // replace the slot if necessary
   388             if (this.childElementCount < this.minSlots) {
   389               this.nextSlot();
   390             }
   392             if (removal && !aSkipArrange)
   393                 this.arrangeItems();
   395             // note that after removal the node is unbound
   396             // so none of the richgriditem binding methods & properties are available
   397             return removal;
   398           ]]>
   399         </body>
   400       </method>
   402       <method name="getIndexOfItem">
   403         <parameter name="anItem"/>
   404         <body>
   405           <![CDATA[
   406             if (!this.isItem(anItem))
   407               return -1;
   409             return Array.indexOf(this.items, anItem);
   410           ]]>
   411         </body>
   412       </method>
   414       <method name="getItemAtIndex">
   415         <parameter name="anIndex"/>
   416         <body>
   417           <![CDATA[
   418             if (!this._isIndexInBounds(anIndex))
   419               return null;
   420             return this.items.item(anIndex);
   421           ]]>
   422         </body>
   423       </method>
   425       <method name="getItemsByUrl">
   426         <parameter name="aUrl"/>
   427         <body>
   428           <![CDATA[
   429             return this.querySelectorAll('richgriditem[value="'+aUrl+'"]');
   430           ]]>
   431         </body>
   432       </method>
   434     <!-- Interface for offsetting selection and checking bounds -->
   436       <property name="isSelectionAtStart" readonly="true"
   437                 onget="return this.selectedIndex == 0;"/>
   439       <property name="isSelectionAtEnd" readonly="true"
   440                 onget="return this.selectedIndex == (this.itemCount - 1);"/>
   442       <property name="isSelectionInStartRow" readonly="true">
   443         <getter>
   444           <![CDATA[
   445             return this.selectedIndex < this.columnCount;
   446           ]]>
   447         </getter>
   448       </property>
   450       <property name="isSelectionInEndRow" readonly="true">
   451         <getter>
   452           <![CDATA[
   453             let lowerBound = (this.rowCount - 1) * this.columnCount;
   454             let higherBound = this.rowCount * this.columnCount;
   456             return this.selectedIndex >= lowerBound &&
   457                    this.selectedIndex < higherBound;
   458           ]]>
   459         </getter>
   460       </property>
   462       <method name="offsetSelection">
   463         <parameter name="aOffset"/>
   464         <body>
   465           <![CDATA[
   466             let newIndex = this.selectedIndex + aOffset;
   467             if (this._isIndexInBounds(newIndex))
   468               this.selectedIndex = newIndex;
   469           ]]>
   470         </body>
   471       </method>
   473       <method name="offsetSelectionByRow">
   474         <parameter name="aRowOffset"/>
   475         <body>
   476           <![CDATA[
   477             let newIndex = this.selectedIndex + (this.columnCount * aRowOffset);
   478             if (this._isIndexInBounds(newIndex))
   479               this.selectedIndex -= this.columnCount;
   480           ]]>
   481         </body>
   482       </method>
   484       <!-- Interface for grid layout management -->
   486       <field name="_rowCount">0</field>
   487       <property name="rowCount" readonly="true" onget="return this._rowCount;"/>
   488       <field name="_columnCount">0</field>
   489       <property name="columnCount" readonly="true" onget="return this._columnCount;"/>
   490       <property name="_containerSize">
   491         <getter><![CDATA[
   492             // return the rect that represents our bounding box
   493             let containerNode = this.hasAttribute("flex") ? this : this.parentNode;
   494             let rect = containerNode.getBoundingClientRect();
   495             // return falsy if the container has no height
   496             return rect.height ? {
   497               width: rect.width,
   498               height: rect.height
   499             } : null;
   500         ]]></getter>
   501       </property>
   503       <property name="_itemSize">
   504         <getter><![CDATA[
   505             // return the dimensions that represent an item in the grid
   507             // grab tile/item dimensions
   508             this._tileSizes = this._getTileSizes();
   510             let type = this.getAttribute("tiletype") || "default";
   511             let dims = this._tileSizes && this._tileSizes[type];
   512             if (!dims) {
   513               throw new Error("Missing tile sizes for '" + type + "' type");
   514             }
   515             return dims;
   516         ]]></getter>
   517       </property>
   519       <!-- do conditions allow layout/arrange of the grid? -->
   520       <property name="_canLayout" readonly="true">
   521         <getter>
   522           <![CDATA[
   523             if (!(this._grid && this._grid.style)) {
   524                return false;
   525             }
   527             let gridItemSize = this._itemSize;
   529             // If we don't have valid item dimensions we can't arrange yet
   530             if (!(gridItemSize && gridItemSize.height)) {
   531               return false;
   532             }
   534             let container = this._containerSize;
   535             // If we don't have valid container dimensions we can't arrange yet
   536             if (!(container && container.height)) {
   537               return false;
   538             }
   539             return true;
   540           ]]>
   541         </getter>
   542       </property>
   544       <field name="_scheduledArrangeItemsTimerId">null</field>
   545       <field name="_scheduledArrangeItemsTries">0</field>
   546       <field name="_maxArrangeItemsRetries">5</field>
   548       <method name="_scheduleArrangeItems">
   549         <parameter name="aTime"/>
   550         <body>
   551           <![CDATA[
   552               // cap the number of times we reschedule calling arrangeItems
   553               if (
   554                   !this._scheduledArrangeItemsTimerId &&
   555                   this._maxArrangeItemsRetries > this._scheduledArrangeItemsTries
   556               ) {
   557                 this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), aTime || 0);
   558                 // track how many times we've attempted arrangeItems
   559                 this._scheduledArrangeItemsTries++;
   560               }
   561           ]]>
   562         </body>
   563       </method>
   565       <method name="arrangeItems">
   566         <body>
   567           <![CDATA[
   568             if (this.hasAttribute("deferlayout")) {
   569               return;
   570             }
   571             if (!this._canLayout) {
   572               // try again later
   573               this._scheduleArrangeItems();
   574               return;
   575             }
   577             let itemDims = this._itemSize;
   578             let containerDims = this._containerSize;
   579             let slotsCount = this.childElementCount;
   581             // reset the flags
   582             if (this._scheduledArrangeItemsTimerId) {
   583               clearTimeout(this._scheduledArrangeItemsTimerId);
   584               delete this._scheduledArrangeItemsTimerId;
   585             }
   586             this._scheduledArrangeItemsTries = 0;
   588             // clear explicit width and columns before calculating from avail. height again
   589             let gridStyle = this._grid.style;
   590             gridStyle.removeProperty("min-width");
   591             gridStyle.removeProperty("-moz-column-count");
   593             if (this.hasAttribute("vertical")) {
   594               this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1;
   595               this._rowCount = Math.floor(slotsCount / this._columnCount);
   596             } else {
   597               // rows attribute is fixed number of rows
   598               let maxRows = Math.floor(containerDims.height / itemDims.height);
   599               this._rowCount = this.getAttribute("rows") ?
   600                                   // fit indicated rows when possible
   601                                   Math.min(maxRows, this.getAttribute("rows")) :
   602                                   // at least 1 row
   603                                   Math.min(maxRows, slotsCount) || 1;
   605               // columns attribute is min number of cols
   606               this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1;
   607               if (this.getAttribute("columns")) {
   608                 this._columnCount = Math.max(this._columnCount, this.getAttribute("columns"));
   609               }
   610             }
   612             // width is typically auto, cap max columns by truncating items collection
   613             // or, setting max-width style property with overflow hidden
   614             if (this._columnCount) {
   615               gridStyle.MozColumnCount = this._columnCount;
   616             }
   617             this._fireEvent("arranged");
   618           ]]>
   619         </body>
   620       </method>
   621       <method name="arrangeItemsNow">
   622         <body>
   623           <![CDATA[
   624             this.removeAttribute("deferlayout");
   625             // cancel any scheduled arrangeItems and reset flags
   626             if (this._scheduledArrangeItemsTimerId) {
   627               clearTimeout(this._scheduledArrangeItemsTimerId);
   628               delete this._scheduledArrangeItemsTimerId;
   629             }
   630             this._scheduledArrangeItemsTries = 0;
   631             // pass over any params
   632             return this.arrangeItems.apply(this, arguments);
   633           ]]>
   634         </body>
   635       </method>
   637       <!-- Inteface to suppress selection events -->
   638       <property name="suppressOnSelect"
   639                   onget="return this.getAttribute('suppressonselect') == 'true';"
   640                   onset="this.setAttribute('suppressonselect', val);"/>
   641       <property name="noContext"
   642                   onget="return this.hasAttribute('nocontext');"
   643                   onset="if (val) this.setAttribute('nocontext', true); else this.removeAttribute('nocontext');"/>
   644       <property name="crossSlideBoundary"
   645           onget="return this.hasAttribute('crossslideboundary')? this.getAttribute('crossslideboundary') : Infinity;"/>
   647     <!-- Internal methods -->
   648       <field name="_xslideHandler"/>
   649       <constructor>
   650         <![CDATA[
   651           // create our quota of item slots
   652           for (let count = this.childElementCount, slotCount = this.minSlots;
   653               count < slotCount; count++) {
   654             this.appendChild( this._createItemElement() );
   655           }
   656           if (this.controller && this.controller.gridBoundCallback != undefined)
   657             this.controller.gridBoundCallback();
   658           // XXX This event was never actually implemented (bug 223411).
   659           let event = document.createEvent("Events");
   660           event.initEvent("contentgenerated", true, true);
   661           this.dispatchEvent(event);
   662         ]]>
   663       </constructor>
   665       <destructor>
   666         <![CDATA[
   667           this.disableCrossSlide();
   668         ]]>
   669       </destructor>
   670       <method name="enableCrossSlide">
   671         <body>
   672           <![CDATA[
   673             // set up cross-slide gesture handling for multiple-selection grids
   674             if (!this._xslideHandler &&
   675                 "undefined" !== typeof CrossSlide && !this.noContext) {
   676               this._xslideHandler = new CrossSlide.Handler(this, {
   677                     REARRANGESTART: this.crossSlideBoundary
   678               });
   679             }
   680           ]]>
   681         </body>
   682       </method>
   684       <method name="disableCrossSlide">
   685         <body>
   686           <![CDATA[
   687             if (this._xslideHandler) {
   688               this.removeEventListener("touchstart", this._xslideHandler);
   689               this.removeEventListener("touchmove", this._xslideHandler);
   690               this.removeEventListener("touchend", this._xslideHandler);
   691               this._xslideHandler = null;
   692             }
   693           ]]>
   694         </body>
   695       </method>
   697       <property name="tileWidth" readonly="true" onget="return this._itemSize.width"/>
   698       <property name="tileHeight" readonly="true" onget="return this._itemSize.height"/>
   699       <field name="_tileStyleSheetName">"tiles.css"</field>
   700       <method name="_getTileSizes">
   701         <body>
   702           <![CDATA[
   703             // Tile sizes are constants, this avoids the need to measure a rendered item before grid layout
   704             // The defines.inc used by the theme CSS is the single source of truth for these values
   705             // This method locates and parses out (just) those dimensions from the stylesheet
   707             let typeSizes = this.ownerDocument.defaultView._richgridTileSizes;
   708             if (typeSizes && typeSizes["default"]) {
   709               return typeSizes;
   710             }
   712             // cache sizes on the global window object, for reuse between bound nodes
   713             typeSizes = this.ownerDocument.defaultView._richgridTileSizes = {};
   715             let sheets = this.ownerDocument.styleSheets;
   716             // The (first matching) rules that will give us tile type => width/height values
   717             // The keys in this object are string-matched against the selectorText
   718             // of rules in our stylesheet. Quoted values in a selector will always use " not '
   719             let typeSelectors = {
   720               'richgriditem' : "default",
   721               'richgriditem[tiletype="thumbnail"]': "thumbnail",
   722               'richgriditem[search]': "search",
   723               'richgriditem[compact]': "compact"
   724             };
   725             let rules, sheet;
   726             for (let i=0; (sheet=sheets[i]); i++) {
   727               if (sheet.href && sheet.href.endsWith( this._tileStyleSheetName )) {
   728                 rules = sheet.cssRules;
   729                 break;
   730               }
   731             }
   732             if (rules) {
   733               // walk the stylesheet rules until we've matched all our selectors
   734               for (let i=0, rule;(rule=rules[i]); i++) {
   735                 let type = rule.selectorText && typeSelectors[rule.selectorText];
   736                 if (type) {
   737                   let sizes = typeSizes[type] = {};
   738                   typeSelectors[type] = null;
   739                   delete typeSelectors[type];
   740                   // we assume px unit for tile dimension values
   741                   sizes.width =  parseInt(rule.style.getPropertyValue("width"));
   742                   sizes.height = parseInt(rule.style.getPropertyValue("height"));
   743                 }
   744                 if (!Object.keys(typeSelectors).length)
   745                   break;
   746               }
   747             } else {
   748               throw new Error("Failed to find stylesheet to parse out richgriditem dimensions\n");
   749             }
   750             return typeSizes;
   751           ]]>
   752         </body>
   753       </method>
   755       <method name="_isIndexInBounds">
   756         <parameter name="anIndex"/>
   757         <body>
   758           <![CDATA[
   759             return anIndex >= 0 && anIndex < this.itemCount;
   760           ]]>
   761         </body>
   762       </method>
   764       <method name="_createItemElement">
   765         <parameter name="aLabel"/>
   766         <parameter name="aValue"/>
   767         <body>
   768           <![CDATA[
   769             let item = this.ownerDocument.createElement("richgriditem");
   770             if (aValue) {
   771               item.setAttribute("value", aValue);
   772             }
   773             if (aLabel) {
   774               item.setAttribute("label", aLabel);
   775             }
   776             if (this.hasAttribute("tiletype")) {
   777               item.setAttribute("tiletype", this.getAttribute("tiletype"));
   778             }
   779             return item;
   780           ]]>
   781         </body>
   782       </method>
   784       <method name="_fireEvent">
   785         <parameter name="aType"/>
   786         <body>
   787           <![CDATA[
   788             switch (aType) {
   789               case "select" :
   790               case "selectionchange" :
   791                 if (this.suppressOnSelect)
   792                   return;
   793                 break;
   794               case "arranged" :
   795                 break;
   796             }
   798             let event = document.createEvent("Events");
   799             event.initEvent(aType, true, true);
   800             this.dispatchEvent(event);
   801           ]]>
   802         </body>
   803       </method>
   805       <method name="bendItem">
   806         <parameter name="aItem"/>
   807         <parameter name="aEvent"/>
   808         <body><![CDATA[
   809           // apply the transform to the contentBox element of the item
   810           let bendNode = this.isItem(aItem) ? aItem._contentBox : null;
   811           if (!bendNode || aItem.hasAttribute("bending"))
   812             return;
   814           let event = aEvent;
   815           let rect = bendNode.getBoundingClientRect();
   816           let angle;
   817           let x = (event.clientX - rect.left) / rect.width;
   818           let y = (event.clientY - rect.top) / rect.height;
   819           let perspective = '450px';
   820           // scaling factors for the angle of deflection,
   821           //   based on the aspect-ratio of the tile
   822           let aspectRatio = rect.width/rect.height;
   823           let deflectX = 10 * Math.ceil(1/aspectRatio);
   824           let deflectY = 10 * Math.ceil(aspectRatio);
   826           if (Math.abs(x - .5) < .1 && Math.abs(y - .5) < .1) {
   827             bendNode.style.transform = "perspective("+perspective+") translateZ(-10px)";
   828           }
   829           else if (x > y) {
   830             if (1 - y > x) {
   831               angle = Math.ceil((.5 - y) * deflectY);
   832               bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)";
   833               bendNode.style.transformOrigin = "center bottom";
   834             } else {
   835               angle = Math.ceil((x - .5) * deflectX);
   836               bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)";
   837               bendNode.style.transformOrigin = "left center";
   838             }
   839           } else {
   840             if (1 - y < x) {
   841               angle = -Math.ceil((y - .5) * deflectY);
   842               bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)";
   843               bendNode.style.transformOrigin = "center top";
   844             } else {
   845               angle = -Math.ceil((.5 - x) * deflectX);
   846               bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)";
   847               bendNode.style.transformOrigin = "right center";
   848             }
   849           }
   850           // mark when bend effect is applied
   851           aItem.setAttribute("bending", true);
   852         ]]></body>
   853       </method>
   855       <method name="unbendItem">
   856         <parameter name="aItem"/>
   857         <body><![CDATA[
   858           // clear the 'bend' transform on the contentBox element of the item
   859           let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox;
   860           if (bendNode && aItem.hasAttribute("bending")) {
   861             bendNode.style.removeProperty('transform');
   862             bendNode.style.removeProperty('transformOrigin');
   863             aItem.removeAttribute("bending");
   864           }
   865         ]]></body>
   866       </method>
   867     </implementation>
   868     <handlers>
   869       <!--  item bend effect handlers -->
   870       <handler event="mousedown" button="0" phase="capturing" action="this.bendItem(event.target, event)"/>
   871       <handler event="touchstart" action="this.bendItem(event.target, event.touches[0])"/>
   872       <handler event="mouseup" button="0" action="this.unbendItem(event.target)"/>
   873       <handler event="mouseout" button="0" action="this.unbendItem(event.target)"/>
   874       <handler event="touchend" action="this.unbendItem(event.target)"/>
   875       <handler event="touchcancel" action="this.unbendItem(event.target)"/>
   876       <!--  /item bend effect handler -->
   878       <handler event="context-action">
   879         <![CDATA[
   880           // context-action is an event fired by the appbar typically
   881           // which directs us to do something to the selected tiles
   882           switch (event.action) {
   883             case "clear":
   884               this.selectNone();
   885               break;
   886             default:
   887               if (this.controller && this.controller.doActionOnSelectedTiles) {
   888                 this.controller.doActionOnSelectedTiles(event.action, event);
   889               }
   890           }
   891         ]]>
   892       </handler>
   893       <handler event="MozCrossSliding">
   894         <![CDATA[
   895           // MozCrossSliding is swipe gesture across a tile
   896           // The tile should follow the drag to reinforce the gesture
   897           // (with inertia/speedbump behavior)
   898           let state = event.crossSlidingState;
   899           let thresholds = this._xslideHandler.thresholds;
   900           let transformValue;
   901           switch (state) {
   902             case "cancelled":
   903               this.unbendItem(event.target);
   904               event.target.removeAttribute('crosssliding');
   905               // hopefully nothing else is transform-ing the tile
   906               event.target.style.removeProperty('transform');
   907               break;
   908             case "dragging":
   909             case "selecting":
   910               // remove bend/depress effect when a cross-slide begins
   911               this.unbendItem(event.target);
   913               event.target.setAttribute("crosssliding", true);
   914               // just track the mouse in the initial phases of the drag gesture
   915               transformValue = (event.direction=='x') ?
   916                                       'translateX('+event.delta+'px)' :
   917                                       'translateY('+event.delta+'px)';
   918               event.target.style.transform = transformValue;
   919               break;
   920             case "selectSpeedBumping":
   921             case "speedBumping":
   922               event.target.setAttribute('crosssliding', true);
   923               // in speed-bump phase, we add inertia to the drag
   924               let offset = CrossSlide.speedbump(
   925                 event.delta,
   926                 thresholds.SPEEDBUMPSTART,
   927                 thresholds.SPEEDBUMPEND
   928               );
   929               transformValue = (event.direction=='x') ?
   930                                       'translateX('+offset+'px)' :
   931                                       'translateY('+offset+'px)';
   932               event.target.style.transform = transformValue;
   933               break;
   934             // "rearranging" case not used or implemented here
   935             case "completed":
   936               event.target.removeAttribute('crosssliding');
   937               event.target.style.removeProperty('transform');
   938               break;
   939           }
   940         ]]>
   941       </handler>
   942       <handler event="MozCrossSlideSelect">
   943         <![CDATA[
   944           if (this.noContext)
   945             return;
   946           this.toggleItemSelection(event.target);
   947         ]]>
   948       </handler>
   949     </handlers>
   950   </binding>
   952   <binding id="richgrid-item">
   953     <content>
   954       <html:div anonid="anon-tile" class="tile-content" xbl:inherits="customImage">
   955         <html:div class="tile-start-container" xbl:inherits="customImage">
   956           <html:div class="tile-icon-box" anonid="anon-tile-icon-box"><xul:image anonid="anon-tile-icon" xbl:inherits="src=iconURI"/></html:div>
   957         </html:div>
   958         <html:div anonid="anon-tile-label" class="tile-desc" xbl:inherits="xbl:text=label"/>
   959       </html:div>
   960     </content>
   962     <implementation>
   963       <property name="isBound" readonly="true" onget="return !!this._icon"/>
   964       <constructor>
   965         <![CDATA[
   966             this.refresh();
   967         ]]>
   968       </constructor>
   969       <property name="_contentBox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-content');"/>
   970       <property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-desc');"/>
   971       <property name="_top" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-start-container');"/>
   972       <property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon');"/>
   973       <property name="_iconBox" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon-box');"/>
   974       <property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-label');"/>
   975       <property name="iconSrc"
   976                 onset="this._icon.src = val; this.setAttribute('iconURI', val);"
   977                 onget="return this._icon.src;" />
   979       <property name="selected"
   980                 onget="return this.hasAttribute('selected');"
   981                 onset="if (val) this.setAttribute('selected', val); else this.removeAttribute('selected');" />
   982       <property name="url"
   983                 onget="return this.getAttribute('value')"
   984                 onset="this.setAttribute('value', val);"/>
   985       <property name="label"
   986                 onget="return this._label.getAttribute('value')"
   987                 onset="this.setAttribute('label', val); this._label.setAttribute('value', val);"/>
   988       <property name="pinned"
   989                 onget="return this.hasAttribute('pinned')"
   990                 onset="if (val) { this.setAttribute('pinned', val) } else this.removeAttribute('pinned');"/>
   992       <method name="refresh">
   993         <body>
   994           <![CDATA[
   995             // Prevent an exception in case binding is not done yet.
   996             if (!this.isBound)
   997               return;
   999             // Seed the binding properties from bound-node attribute values
  1000             // Usage: node.refresh()
  1001             //        - reinitializes all binding properties from their associated attributes
  1003             this.iconSrc = this.getAttribute('iconURI');
  1004             this.color = this.getAttribute("customColor");
  1005             this.label = this.getAttribute('label');
  1006             // url getter just looks directly at attribute
  1007             // selected getter just looks directly at attribute
  1008             // pinned getter just looks directly at attribute
  1009             // value getter just looks directly at attribute
  1010             this._contextActions = null;
  1011             this.refreshBackgroundImage();
  1012           ]]>
  1013         </body>
  1014       </method>
  1016       <property name="control">
  1017         <getter><![CDATA[
  1018           let parent = this.parentNode;
  1019           while (parent && parent != this.ownerDocument.documentElement) {
  1020             if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
  1021               return parent;
  1022             parent = parent.parentNode;
  1024           return null;
  1025         ]]></getter>
  1026       </property>
  1028       <property name="color" onget="return this.getAttribute('customColor');">
  1029         <setter><![CDATA[
  1030           if (val) {
  1031             this.setAttribute("customColor", val);
  1032             this._contentBox.style.backgroundColor = val;
  1034             // overridden in tiles.css for non-thumbnail types
  1035             this._label.style.backgroundColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.8)');
  1037             // Small icons get a border+background-color treatment.
  1038             // See tiles.css for large icon overrides
  1039             this._iconBox.style.borderColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.6)');
  1040             this._iconBox.style.backgroundColor = this.hasAttribute("tintColor") ?
  1041                                                     this.getAttribute("tintColor") : "#fff";
  1042           } else {
  1043             this.removeAttribute("customColor");
  1044             this._contentBox.style.removeProperty("background-color");
  1045             this._label.style.removeProperty("background-color");
  1046             this._iconBox.style.removeProperty("border-color");
  1047             this._iconBox.style.removeProperty("background-color");
  1049         ]]></setter>
  1050       </property>
  1052       <property name="backgroundImage" onget="return this.getAttribute('customImage');">
  1053         <setter><![CDATA[
  1054           if (val) {
  1055             this.setAttribute("customImage", val);
  1056             this._top.style.backgroundImage = val;
  1057           } else {
  1058             this.removeAttribute("customImage");
  1059             this._top.style.removeProperty("background-image");
  1061         ]]></setter>
  1062       </property>
  1064       <method name="refreshBackgroundImage">
  1065         <body><![CDATA[
  1066           if (!this.isBound)
  1067             return;
  1068           if (this.backgroundImage) {
  1069             this._top.style.removeProperty("background-image");
  1070             this._top.style.setProperty("background-image", this.backgroundImage);
  1072         ]]></body>
  1073       </method>
  1075       <field name="_contextActions">null</field>
  1076       <property name="contextActions">
  1077         <getter>
  1078           <![CDATA[
  1079             if (!this._contextActions) {
  1080               this._contextActions = new Set();
  1081               let actionSet = this._contextActions;
  1082               let actions = this.getAttribute("data-contextactions");
  1083               if (actions) {
  1084                 actions.split(/[,\s]+/).forEach(function(verb){
  1085                   actionSet.add(verb);
  1086                 });
  1089             return this._contextActions;
  1090           ]]>
  1091         </getter>
  1092       </property>
  1093     </implementation>
  1095     <handlers>
  1096       <handler event="click" button="0">
  1097         <![CDATA[
  1098           // left-click/touch handler
  1099           this.control.handleItemClick(this, event);
  1100           // Stop this from bubbling, when the richgrid container
  1101           // receives click events, we blur the nav bar.
  1102           event.stopPropagation();
  1103         ]]>
  1104       </handler>
  1106       <handler event="contextmenu">
  1107         <![CDATA[
  1108           // fires for right-click, long-click and (keyboard) contextmenu input
  1109           // toggle the selected state of tiles in a grid
  1110           let gridParent = this.control;
  1111           if (!this.isBound || !gridParent)
  1112             return;
  1113           gridParent.handleItemContextMenu(this, event);
  1114         ]]>
  1115       </handler>
  1116     </handlers>
  1117   </binding>
  1119   <binding id="richgrid-empty-item">
  1120     <content>
  1121       <html:div anonid="anon-tile" class="tile-content"></html:div>
  1122     </content>
  1123   </binding>
  1125 </bindings>

mercurial