mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 20:47:44 +00:00
1109 lines
40 KiB
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>
|