gecko-dev/browser/metro/base/content/bindings/grid.xml

1109 lines
40 KiB
XML

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<bindings
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="richgrid"
extends="chrome://global/content/bindings/general.xml#basecontrol">
<content>
<html:div id="grid-div" anonid="grid" class="richgrid-grid" xbl:inherits="compact">
<children/>
</html:div>
</content>
<implementation implements="nsIDOMXULSelectControlElement">
<property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/>
<property name="isBound" readonly="true" onget="return !!this._grid"/>
<property name="isArranging" readonly="true" onget="return !!this._scheduledArrangeItemsTimerId"/>
<field name="controller">null</field>
<!-- collection of child items excluding empty tiles -->
<property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/>
<property name="itemCount" readonly="true" onget="return this.items.length;"/>
<!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
<method name="clearSelection">
<body>
<![CDATA[
// 'selection' and 'selected' are confusingly overloaded here
// as richgrid is adopting multi-select behavior, but select/selected are already being
// used to describe triggering the default action of a tile
if (this._selectedItem){
this._selectedItem.removeAttribute("selected");
this._selectedItem = null;
}
for (let childItem of this.selectedItems) {
childItem.removeAttribute("selected");
}
]]>
</body>
</method>
<method name="toggleItemSelection">
<parameter name="anItem"/>
<body>
<![CDATA[
let wasSelected = anItem.selected;
if ("single" == this.getAttribute("seltype")) {
this.clearSelection();
}
this._selectedItem = wasSelected ? null : anItem;
if (wasSelected)
anItem.removeAttribute("selected");
else
anItem.setAttribute("selected", true);
this._fireEvent("selectionchange");
]]>
</body>
</method>
<method name="selectItem">
<parameter name="anItem"/>
<body>
<![CDATA[
let wasSelected = anItem.selected,
isSingleMode = ("single" == this.getAttribute("seltype"));
if (isSingleMode) {
this.clearSelection();
}
this._selectedItem = anItem;
if (wasSelected) {
return;
}
anItem.setAttribute("selected", true);
if (isSingleMode) {
this._fireEvent("select");
} else {
this._fireEvent("selectionchange");
}
]]>
</body>
</method>
<method name="selectNone">
<body>
<![CDATA[
let selectedCount = this.selectedItems.length;
this.clearSelection();
if (selectedCount && "single" != this.getAttribute("seltype")) {
this._fireEvent("selectionchange");
}
]]>
</body>
</method>
<method name="handleItemClick">
<parameter name="aItem"/>
<parameter name="aEvent"/>
<body>
<![CDATA[
if (!this.isBound)
return;
if ("single" == this.getAttribute("seltype")) {
// we'll republish this as a selectionchange event on the grid
aEvent.stopPropagation();
this.selectItem(aItem);
}
if (this.controller && this.controller.handleItemClick)
this.controller.handleItemClick(aItem, aEvent);
]]>
</body>
</method>
<method name="handleItemContextMenu">
<parameter name="aItem"/>
<parameter name="aEvent"/>
<body>
<![CDATA[
if (!this.isBound || this.noContext)
return;
// we'll republish this as a selectionchange event on the grid
aEvent.stopPropagation();
this.toggleItemSelection(aItem);
]]>
</body>
</method>
<property name="contextSetName" readonly="true"
onget="return this.getAttribute('set-name');"/>
<property name="contextActions">
<getter>
<![CDATA[
// return the subset of verbs that apply to all selected tiles
let tileNodes = this.selectedItems;
if (!tileNodes.length) {
return new Set();
}
// given one or more sets of values,
// return a set with only those values present in each
let initialItem = tileNodes[0];
let verbSet = new Set(initialItem.contextActions);
for (let i=1; i<tileNodes.length; i++){
let set = tileNodes[i].contextActions;
for (let item of verbSet) {
if (!set.has(item)){
verbSet.delete(item);
}
}
}
// add the clear-selection button if more than one tiles are selected
if (tileNodes.length > 1) {
verbSet.add('clear');
}
// returns Set
return verbSet;
]]>
</getter>
</property>
<!-- nsIDOMXULSelectControlElement -->
<field name="_selectedItem">null</field>
<property name="selectedItem" onget="return this._selectedItem;">
<setter>
<![CDATA[
this.selectItem(val);
]]>
</setter>
</property>
<!-- partial implementation of multiple selection interface -->
<property name="selectedItems">
<getter>
<![CDATA[
return this.querySelectorAll("richgriditem[value][selected]");
]]>
</getter>
</property>
<property name="selectedIndex">
<getter>
<![CDATA[
return this.getIndexOfItem(this._selectedItem);
]]>
</getter>
<setter>
<![CDATA[
if (val >= 0) {
let selected = this.getItemAtIndex(val);
this.selectItem(selected);
} else {
this.selectNone();
}
]]>
</setter>
</property>
<method name="appendItem">
<parameter name="aLabel"/>
<parameter name="aValue"/>
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
let item = this.nextSlot();
item.setAttribute("value", aValue);
item.setAttribute("label", aLabel);
if (!aSkipArrange)
this.arrangeItems();
return item;
]]>
</body>
</method>
<method name="_slotValues">
<body><![CDATA[
return Array.map(this.children, (cnode) => cnode.getAttribute("value"));
]]></body>
</method>
<property name="minSlots" readonly="true"
onget="return this.getAttribute('minSlots') || 3;"/>
<method name="clearAll">
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
const ELEMENT_NODE_TYPE = Components.interfaces.nsIDOMNode.ELEMENT_NODE;
let slotCount = this.minSlots;
let childIndex = 0;
let child = this.firstChild;
while (child) {
// remove excess elements and non-element nodes
if (child.nodeType !== ELEMENT_NODE_TYPE || childIndex+1 > slotCount) {
let orphanNode = child;
child = orphanNode.nextSibling;
this.removeChild(orphanNode);
continue;
}
if (child.hasAttribute("value")) {
this._releaseSlot(child);
}
child = child.nextSibling;
childIndex++;
}
// create our quota of item slots
for (let count = this.childElementCount; count < slotCount; count++) {
this.appendChild( this._createItemElement() );
}
if (!aSkipArrange)
this.arrangeItems();
]]>
</body>
</method>
<method name="_slotAt">
<parameter name="anIndex"/>
<body>
<![CDATA[
// backfill with new slots as necessary
let count = Math.max(1+anIndex, this.minSlots) - this.childElementCount;
for (; count > 0; count--) {
this.appendChild( this._createItemElement() );
}
return this.children[anIndex];
]]>
</body>
</method>
<method name="nextSlot">
<body>
<![CDATA[
if (!this.itemCount) {
return this._slotAt(0);
}
let lastItem = this.items[this.itemCount-1];
let nextIndex = 1 + Array.indexOf(this.children, lastItem);
return this._slotAt(nextIndex);
]]>
</body>
</method>
<method name="_releaseSlot">
<parameter name="anItem"/>
<body>
<![CDATA[
// Flush out data and state attributes so we can recycle this slot/element
let exclude = { value: 1, tiletype: 1 };
let attrNames = [attr.name for (attr of anItem.attributes)];
for (let attrName of attrNames) {
if (!(attrName in exclude))
anItem.removeAttribute(attrName);
}
// clear out inline styles
anItem.removeAttribute("style");
// finally clear the value, which should apply the richgrid-empty-item binding
anItem.removeAttribute("value");
]]>
</body>
</method>
<method name="insertItemAt">
<parameter name="anIndex"/>
<parameter name="aLabel"/>
<parameter name="aValue"/>
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
anIndex = Math.min(this.itemCount, anIndex);
let insertedItem;
let existing = this.getItemAtIndex(anIndex);
if (existing) {
// use an empty slot if we have one, otherwise insert it
let childIndex = Array.indexOf(this.children, existing);
if (childIndex > 0 && !this.children[childIndex-1].hasAttribute("value")) {
insertedItem = this.children[childIndex-1];
} else {
insertedItem = this.insertBefore(this._createItemElement(),existing);
}
}
if (!insertedItem) {
insertedItem = this._slotAt(anIndex);
}
insertedItem.setAttribute("value", aValue);
insertedItem.setAttribute("label", aLabel);
if (!aSkipArrange)
this.arrangeItems();
return insertedItem;
]]>
</body>
</method>
<method name="removeItemAt">
<parameter name="anIndex"/>
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
let item = this.getItemAtIndex(anIndex);
if (!item)
return null;
return this.removeItem(item, aSkipArrange);
]]>
</body>
</method>
<method name="removeItem">
<parameter name="aItem"/>
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
if (!aItem || Array.indexOf(this.items, aItem) < 0)
return null;
let removal = this.removeChild(aItem);
// replace the slot if necessary
if (this.childElementCount < this.minSlots) {
this.nextSlot();
}
if (removal && !aSkipArrange)
this.arrangeItems();
// note that after removal the node is unbound
// so none of the richgriditem binding methods & properties are available
return removal;
]]>
</body>
</method>
<method name="getIndexOfItem">
<parameter name="anItem"/>
<body>
<![CDATA[
if (!anItem)
return -1;
return Array.indexOf(this.items, anItem);
]]>
</body>
</method>
<method name="getItemAtIndex">
<parameter name="anIndex"/>
<body>
<![CDATA[
if (!this._isIndexInBounds(anIndex))
return null;
return this.items.item(anIndex);
]]>
</body>
</method>
<method name="getItemsByUrl">
<parameter name="aUrl"/>
<body>
<![CDATA[
return this.querySelectorAll('richgriditem[value="'+aUrl+'"]');
]]>
</body>
</method>
<!-- Interface for offsetting selection and checking bounds -->
<property name="isSelectionAtStart" readonly="true"
onget="return this.selectedIndex == 0;"/>
<property name="isSelectionAtEnd" readonly="true"
onget="return this.selectedIndex == (this.itemCount - 1);"/>
<property name="isSelectionInStartRow" readonly="true">
<getter>
<![CDATA[
return this.selectedIndex < this.columnCount;
]]>
</getter>
</property>
<property name="isSelectionInEndRow" readonly="true">
<getter>
<![CDATA[
let lowerBound = (this.rowCount - 1) * this.columnCount;
let higherBound = this.rowCount * this.columnCount;
return this.selectedIndex >= lowerBound &&
this.selectedIndex < higherBound;
]]>
</getter>
</property>
<method name="offsetSelection">
<parameter name="aOffset"/>
<body>
<![CDATA[
let newIndex = this.selectedIndex + aOffset;
if (this._isIndexInBounds(newIndex))
this.selectedIndex = newIndex;
]]>
</body>
</method>
<method name="offsetSelectionByRow">
<parameter name="aRowOffset"/>
<body>
<![CDATA[
let newIndex = this.selectedIndex + (this.columnCount * aRowOffset);
if (this._isIndexInBounds(newIndex))
this.selectedIndex -= this.columnCount;
]]>
</body>
</method>
<!-- Interface for grid layout management -->
<field name="_rowCount">0</field>
<property name="rowCount" readonly="true" onget="return this._rowCount;"/>
<field name="_columnCount">0</field>
<property name="columnCount" readonly="true" onget="return this._columnCount;"/>
<property name="_containerSize">
<getter><![CDATA[
// return the rect that represents our bounding box
let containerNode = this.hasAttribute("flex") ? this : this.parentNode;
let rect = containerNode.getBoundingClientRect();
// return falsy if the container has no height
return rect.height ? {
width: rect.width,
height: rect.height
} : null;
]]></getter>
</property>
<property name="_itemSize">
<getter><![CDATA[
// return the dimensions that represent an item in the grid
// grab tile/item dimensions
this._tileSizes = this._getTileSizes();
let type = this.getAttribute("tiletype") || "default";
let dims = this._tileSizes && this._tileSizes[type];
if (!dims) {
throw new Error("Missing tile sizes for '" + type + "' type");
}
return dims;
]]></getter>
</property>
<!-- do conditions allow layout/arrange of the grid? -->
<property name="_canLayout" readonly="true">
<getter>
<![CDATA[
if (!(this._grid && this._grid.style)) {
return false;
}
let gridItemSize = this._itemSize;
// If we don't have valid item dimensions we can't arrange yet
if (!(gridItemSize && gridItemSize.height)) {
return false;
}
let container = this._containerSize;
// If we don't have valid container dimensions we can't arrange yet
if (!(container && container.height)) {
return false;
}
return true;
]]>
</getter>
</property>
<field name="_scheduledArrangeItemsTimerId">null</field>
<field name="_scheduledArrangeItemsTries">0</field>
<field name="_maxArrangeItemsRetries">5</field>
<method name="_scheduleArrangeItems">
<parameter name="aTime"/>
<body>
<![CDATA[
// cap the number of times we reschedule calling arrangeItems
if (
!this._scheduledArrangeItemsTimerId &&
this._maxArrangeItemsRetries > this._scheduledArrangeItemsTries
) {
this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), aTime || 0);
// track how many times we've attempted arrangeItems
this._scheduledArrangeItemsTries++;
}
]]>
</body>
</method>
<method name="arrangeItems">
<body>
<![CDATA[
if (this.hasAttribute("deferlayout")) {
return;
}
if (!this._canLayout) {
// try again later
this._scheduleArrangeItems();
return;
}
let itemDims = this._itemSize;
let containerDims = this._containerSize;
let slotsCount = this.childElementCount;
// reset the flags
if (this._scheduledArrangeItemsTimerId) {
clearTimeout(this._scheduledArrangeItemsTimerId);
delete this._scheduledArrangeItemsTimerId;
}
this._scheduledArrangeItemsTries = 0;
// clear explicit width and columns before calculating from avail. height again
let gridStyle = this._grid.style;
gridStyle.removeProperty("min-width");
gridStyle.removeProperty("-moz-column-count");
if (this.hasAttribute("vertical")) {
this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1;
this._rowCount = Math.floor(slotsCount / this._columnCount);
} else {
// rows attribute is fixed number of rows
let maxRows = Math.floor(containerDims.height / itemDims.height);
this._rowCount = this.getAttribute("rows") ?
// fit indicated rows when possible
Math.min(maxRows, this.getAttribute("rows")) :
// at least 1 row
Math.min(maxRows, slotsCount) || 1;
// columns attribute is min number of cols
this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1;
if (this.getAttribute("columns")) {
this._columnCount = Math.max(this._columnCount, this.getAttribute("columns"));
}
}
// width is typically auto, cap max columns by truncating items collection
// or, setting max-width style property with overflow hidden
if (this._columnCount) {
gridStyle.MozColumnCount = this._columnCount;
}
this._fireEvent("arranged");
]]>
</body>
</method>
<method name="arrangeItemsNow">
<body>
<![CDATA[
this.removeAttribute("deferlayout");
// cancel any scheduled arrangeItems and reset flags
if (this._scheduledArrangeItemsTimerId) {
clearTimeout(this._scheduledArrangeItemsTimerId);
delete this._scheduledArrangeItemsTimerId;
}
this._scheduledArrangeItemsTries = 0;
// pass over any params
return this.arrangeItems.apply(this, arguments);
]]>
</body>
</method>
<!-- Inteface to suppress selection events -->
<property name="suppressOnSelect"
onget="return this.getAttribute('suppressonselect') == 'true';"
onset="this.setAttribute('suppressonselect', val);"/>
<property name="noContext"
onget="return this.hasAttribute('nocontext');"
onset="if (val) this.setAttribute('nocontext', true); else this.removeAttribute('nocontext');"/>
<property name="crossSlideBoundary"
onget="return this.hasAttribute('crossslideboundary')? this.getAttribute('crossslideboundary') : Infinity;"/>
<!-- Internal methods -->
<field name="_xslideHandler"/>
<constructor>
<![CDATA[
// create our quota of item slots
for (let count = this.childElementCount, slotCount = this.minSlots;
count < slotCount; count++) {
this.appendChild( this._createItemElement() );
}
if (this.controller && this.controller.gridBoundCallback != undefined)
this.controller.gridBoundCallback();
// XXX This event was never actually implemented (bug 223411).
let event = document.createEvent("Events");
event.initEvent("contentgenerated", true, true);
this.dispatchEvent(event);
]]>
</constructor>
<destructor>
<![CDATA[
this.disableCrossSlide();
]]>
</destructor>
<method name="enableCrossSlide">
<body>
<![CDATA[
// set up cross-slide gesture handling for multiple-selection grids
if (!this._xslideHandler &&
"undefined" !== typeof CrossSlide && !this.noContext) {
this._xslideHandler = new CrossSlide.Handler(this, {
REARRANGESTART: this.crossSlideBoundary
});
}
]]>
</body>
</method>
<method name="disableCrossSlide">
<body>
<![CDATA[
if (this._xslideHandler) {
this.removeEventListener("touchstart", this._xslideHandler);
this.removeEventListener("touchmove", this._xslideHandler);
this.removeEventListener("touchend", this._xslideHandler);
this._xslideHandler = null;
}
]]>
</body>
</method>
<property name="tileWidth" readonly="true" onget="return this._itemSize.width"/>
<property name="tileHeight" readonly="true" onget="return this._itemSize.height"/>
<field name="_tileStyleSheetName">"tiles.css"</field>
<method name="_getTileSizes">
<body>
<![CDATA[
// Tile sizes are constants, this avoids the need to measure a rendered item before grid layout
// The defines.inc used by the theme CSS is the single source of truth for these values
// This method locates and parses out (just) those dimensions from the stylesheet
let typeSizes = this.ownerDocument.defaultView._richgridTileSizes;
if (typeSizes && typeSizes["default"]) {
return typeSizes;
}
// cache sizes on the global window object, for reuse between bound nodes
typeSizes = this.ownerDocument.defaultView._richgridTileSizes = {};
let sheets = this.ownerDocument.styleSheets;
// The (first matching) rules that will give us tile type => width/height values
// The keys in this object are string-matched against the selectorText
// of rules in our stylesheet. Quoted values in a selector will always use " not '
let typeSelectors = {
'richgriditem' : "default",
'richgriditem[tiletype="thumbnail"]': "thumbnail",
'richgriditem[search]': "search",
'richgriditem[compact]': "compact"
};
let rules, sheet;
for (let i=0; (sheet=sheets[i]); i++) {
if (sheet.href && sheet.href.endsWith( this._tileStyleSheetName )) {
rules = sheet.cssRules;
break;
}
}
if (rules) {
// walk the stylesheet rules until we've matched all our selectors
for (let i=0, rule;(rule=rules[i]); i++) {
let type = rule.selectorText && typeSelectors[rule.selectorText];
if (type) {
let sizes = typeSizes[type] = {};
typeSelectors[type] = null;
delete typeSelectors[type];
// we assume px unit for tile dimension values
sizes.width = parseInt(rule.style.getPropertyValue("width"));
sizes.height = parseInt(rule.style.getPropertyValue("height"));
}
if (!Object.keys(typeSelectors).length)
break;
}
} else {
throw new Error("Failed to find stylesheet to parse out richgriditem dimensions\n");
}
return typeSizes;
]]>
</body>
</method>
<method name="_isIndexInBounds">
<parameter name="anIndex"/>
<body>
<![CDATA[
return anIndex >= 0 && anIndex < this.itemCount;
]]>
</body>
</method>
<method name="_createItemElement">
<parameter name="aLabel"/>
<parameter name="aValue"/>
<body>
<![CDATA[
let item = this.ownerDocument.createElement("richgriditem");
if (aValue) {
item.setAttribute("value", aValue);
}
if (aLabel) {
item.setAttribute("label", aLabel);
}
if (this.hasAttribute("tiletype")) {
item.setAttribute("tiletype", this.getAttribute("tiletype"));
}
return item;
]]>
</body>
</method>
<method name="_fireEvent">
<parameter name="aType"/>
<body>
<![CDATA[
switch (aType) {
case "select" :
case "selectionchange" :
if (this.suppressOnSelect)
return;
break;
case "arranged" :
break;
}
let event = document.createEvent("Events");
event.initEvent(aType, true, true);
this.dispatchEvent(event);
]]>
</body>
</method>
<method name="bendItem">
<parameter name="aItem"/>
<parameter name="aEvent"/>
<body><![CDATA[
// apply the transform to the contentBox element of the item
let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox;
if (!bendNode)
return;
let event = aEvent;
let rect = bendNode.getBoundingClientRect();
let angle;
let x = (event.clientX - rect.left) / rect.width;
let y = (event.clientY - rect.top) / rect.height;
let perspective = '450px';
// scaling factors for the angle of deflection,
// based on the aspect-ratio of the tile
let aspectRatio = rect.width/rect.height;
let deflectX = 10 * Math.ceil(1/aspectRatio);
let deflectY = 10 * Math.ceil(aspectRatio);
if (Math.abs(x - .5) < .1 && Math.abs(y - .5) < .1) {
bendNode.style.transform = "perspective("+perspective+") translateZ(-10px)";
}
else if (x > y) {
if (1 - y > x) {
angle = Math.ceil((.5 - y) * deflectY);
bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)";
bendNode.style.transformOrigin = "center bottom";
} else {
angle = Math.ceil((x - .5) * deflectX);
bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)";
bendNode.style.transformOrigin = "left center";
}
} else {
if (1 - y < x) {
angle = -Math.ceil((y - .5) * deflectY);
bendNode.style.transform = "perspective("+perspective+") rotateX(" + angle + "deg)";
bendNode.style.transformOrigin = "center top";
} else {
angle = -Math.ceil((.5 - x) * deflectX);
bendNode.style.transform = "perspective("+perspective+") rotateY(" + angle + "deg)";
bendNode.style.transformOrigin = "right center";
}
}
// mark when bend effect is applied
aItem.setAttribute("bending", true);
]]></body>
</method>
<method name="unbendItem">
<parameter name="aItem"/>
<body><![CDATA[
// clear the 'bend' transform on the contentBox element of the item
let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox;
if (bendNode && aItem.hasAttribute("bending")) {
bendNode.style.removeProperty('transform');
bendNode.style.removeProperty('transformOrigin');
aItem.removeAttribute("bending");
}
]]></body>
</method>
</implementation>
<handlers>
<!-- item bend effect handlers -->
<handler event="mousedown" button="0" phase="capturing" action="this.bendItem(event.target, event)"/>
<handler event="touchstart" action="this.bendItem(event.target, event.touches[0])"/>
<handler event="mouseup" button="0" action="this.unbendItem(event.target)"/>
<handler event="mouseout" button="0" action="this.unbendItem(event.target)"/>
<handler event="touchend" action="this.unbendItem(event.target)"/>
<!-- /item bend effect handler -->
<handler event="context-action">
<![CDATA[
// context-action is an event fired by the appbar typically
// which directs us to do something to the selected tiles
switch (event.action) {
case "clear":
this.selectNone();
break;
default:
if (this.controller && this.controller.doActionOnSelectedTiles) {
this.controller.doActionOnSelectedTiles(event.action, event);
}
}
]]>
</handler>
<handler event="MozCrossSliding">
<![CDATA[
// MozCrossSliding is swipe gesture across a tile
// The tile should follow the drag to reinforce the gesture
// (with inertia/speedbump behavior)
let state = event.crossSlidingState;
let thresholds = this._xslideHandler.thresholds;
let transformValue;
switch (state) {
case "cancelled":
this.unbendItem(event.target);
event.target.removeAttribute('crosssliding');
// hopefully nothing else is transform-ing the tile
event.target.style.removeProperty('transform');
break;
case "dragging":
case "selecting":
// remove bend/depress effect when a cross-slide begins
this.unbendItem(event.target);
event.target.setAttribute("crosssliding", true);
// just track the mouse in the initial phases of the drag gesture
transformValue = (event.direction=='x') ?
'translateX('+event.delta+'px)' :
'translateY('+event.delta+'px)';
event.target.style.transform = transformValue;
break;
case "selectSpeedBumping":
case "speedBumping":
event.target.setAttribute('crosssliding', true);
// in speed-bump phase, we add inertia to the drag
let offset = CrossSlide.speedbump(
event.delta,
thresholds.SPEEDBUMPSTART,
thresholds.SPEEDBUMPEND
);
transformValue = (event.direction=='x') ?
'translateX('+offset+'px)' :
'translateY('+offset+'px)';
event.target.style.transform = transformValue;
break;
// "rearranging" case not used or implemented here
case "completed":
event.target.removeAttribute('crosssliding');
event.target.style.removeProperty('transform');
break;
}
]]>
</handler>
<handler event="MozCrossSlideSelect">
<![CDATA[
if (this.noContext)
return;
this.toggleItemSelection(event.target);
]]>
</handler>
</handlers>
</binding>
<binding id="richgrid-item">
<content>
<html:div anonid="anon-tile" class="tile-content" xbl:inherits="customImage">
<html:div class="tile-start-container" xbl:inherits="customImage">
<html:div class="tile-icon-box" anonid="anon-tile-icon-box"><xul:image anonid="anon-tile-icon" xbl:inherits="src=iconURI"/></html:div>
</html:div>
<html:div anonid="anon-tile-label" class="tile-desc" xbl:inherits="xbl:text=label"/>
</html:div>
</content>
<implementation>
<property name="isBound" readonly="true" onget="return !!this._icon"/>
<constructor>
<![CDATA[
this.refresh();
]]>
</constructor>
<property name="_contentBox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-content');"/>
<property name="_textbox" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-desc');"/>
<property name="_top" onget="return document.getAnonymousElementByAttribute(this, 'class', 'tile-start-container');"/>
<property name="_icon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon');"/>
<property name="_iconBox" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-icon-box');"/>
<property name="_label" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-tile-label');"/>
<property name="iconSrc"
onset="this._icon.src = val; this.setAttribute('iconURI', val);"
onget="return this._icon.src;" />
<property name="selected"
onget="return this.hasAttribute('selected');"
onset="if (val) this.setAttribute('selected', val); else this.removeAttribute('selected');" />
<property name="url"
onget="return this.getAttribute('value')"
onset="this.setAttribute('value', val);"/>
<property name="label"
onget="return this._label.getAttribute('value')"
onset="this.setAttribute('label', val); this._label.setAttribute('value', val);"/>
<property name="pinned"
onget="return this.hasAttribute('pinned')"
onset="if (val) { this.setAttribute('pinned', val) } else this.removeAttribute('pinned');"/>
<method name="refresh">
<body>
<![CDATA[
// Prevent an exception in case binding is not done yet.
if (!this.isBound)
return;
// Seed the binding properties from bound-node attribute values
// Usage: node.refresh()
// - reinitializes all binding properties from their associated attributes
this.iconSrc = this.getAttribute('iconURI');
this.color = this.getAttribute("customColor");
this.label = this.getAttribute('label');
// url getter just looks directly at attribute
// selected getter just looks directly at attribute
// pinned getter just looks directly at attribute
// value getter just looks directly at attribute
this._contextActions = null;
this.refreshBackgroundImage();
]]>
</body>
</method>
<property name="control">
<getter><![CDATA[
let parent = this.parentNode;
while (parent && parent != this.ownerDocument.documentElement) {
if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
return parent;
parent = parent.parentNode;
}
return null;
]]></getter>
</property>
<property name="color" onget="return this.getAttribute('customColor');">
<setter><![CDATA[
if (val) {
this.setAttribute("customColor", val);
this._contentBox.style.backgroundColor = val;
// overridden in tiles.css for non-thumbnail types
this._label.style.backgroundColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.8)');
// Small icons get a border+background-color treatment.
// See tiles.css for large icon overrides
this._iconBox.style.borderColor = val.replace(/rgb\(([^\)]+)\)/, 'rgba($1, 0.6)');
this._iconBox.style.backgroundColor = this.hasAttribute("tintColor") ?
this.getAttribute("tintColor") : "#fff";
} else {
this.removeAttribute("customColor");
this._contentBox.style.removeProperty("background-color");
this._label.style.removeProperty("background-color");
this._iconBox.style.removeProperty("border-color");
this._iconBox.style.removeProperty("background-color");
}
]]></setter>
</property>
<property name="backgroundImage" onget="return this.getAttribute('customImage');">
<setter><![CDATA[
if (val) {
this.setAttribute("customImage", val);
this._top.style.backgroundImage = val;
} else {
this.removeAttribute("customImage");
this._top.style.removeProperty("background-image");
}
]]></setter>
</property>
<method name="refreshBackgroundImage">
<body><![CDATA[
if (!this.isBound)
return;
if (this.backgroundImage) {
this._top.style.removeProperty("background-image");
this._top.style.setProperty("background-image", this.backgroundImage);
}
]]></body>
</method>
<field name="_contextActions">null</field>
<property name="contextActions">
<getter>
<![CDATA[
if (!this._contextActions) {
this._contextActions = new Set();
let actionSet = this._contextActions;
let actions = this.getAttribute("data-contextactions");
if (actions) {
actions.split(/[,\s]+/).forEach(function(verb){
actionSet.add(verb);
});
}
}
return this._contextActions;
]]>
</getter>
</property>
</implementation>
<handlers>
<handler event="click" button="0">
<![CDATA[
// left-click/touch handler
this.control.handleItemClick(this, event);
// Stop this from bubbling, when the richgrid container
// receives click events, we blur the nav bar.
event.stopPropagation();
]]>
</handler>
<handler event="contextmenu">
<![CDATA[
// fires for right-click, long-click and (keyboard) contextmenu input
// toggle the selected state of tiles in a grid
let gridParent = this.control;
if (!this.isBound || !gridParent)
return;
gridParent.handleItemContextMenu(this, event);
]]>
</handler>
</handlers>
</binding>
<binding id="richgrid-empty-item">
<content>
<html:div anonid="anon-tile" class="tile-content"></html:div>
</content>
</binding>
</bindings>