mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 12:37:37 +00:00
569 lines
18 KiB
XML
569 lines
18 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">
|
|
<children/>
|
|
</html:div>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMXULSelectControlElement">
|
|
<property name="_grid" readonly="true" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'grid');"/>
|
|
<field name="controller">null</field>
|
|
|
|
<!-- 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.selected = false;
|
|
this._selectedItem = null;
|
|
}
|
|
|
|
for (let childItem of this.selectedItems) {
|
|
childItem.selected = false;
|
|
}
|
|
// reset context actions
|
|
this._contextActions = null;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="toggleItemSelection">
|
|
<parameter name="anItem"/>
|
|
<body>
|
|
<![CDATA[
|
|
anItem.selected = !anItem.selected;
|
|
this._fireOnSelectionChange();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="selectItem">
|
|
<parameter name="anItem"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.clearSelection();
|
|
this._selectedItem = anItem;
|
|
this._selectedItem.selected = true;
|
|
|
|
this._fireOnSelect();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleItemClick">
|
|
<parameter name="aItem"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.controller)
|
|
this.controller.handleItemClick(aItem);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleItemContextMenu">
|
|
<parameter name="aItem"/>
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
// we'll republish this as a selectionchange event on the grid
|
|
aEvent.stopPropagation();
|
|
this.toggleItemSelection(aItem);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<field name="_contextActions">null</field>
|
|
<property name="contextActions">
|
|
<getter>
|
|
<![CDATA[
|
|
// return the subset of verbs that apply to all selected tiles
|
|
// use cached list, it'll get cleared out when selection changes
|
|
if (this._contextActions) {
|
|
return this._contextActions;
|
|
}
|
|
let tileNodes = this.selectedItems;
|
|
if (!tileNodes.length) {
|
|
return new Set();
|
|
}
|
|
|
|
function cloneSet(aSet) {
|
|
let clone = new Set();
|
|
if (aSet) {
|
|
for (let item of aSet) {
|
|
clone.add(item);
|
|
}
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
// given one or more sets of values,
|
|
// return a set with only those values present in each
|
|
let initialItem = tileNodes[0];
|
|
|
|
let verbSet = cloneSet(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._contextActions = verbSet;
|
|
// returns Set
|
|
return this._contextActions;
|
|
]]>
|
|
</getter>
|
|
</property>
|
|
|
|
<!-- nsIDOMXULSelectControlElement -->
|
|
|
|
<property name="itemCount" readonly="true" onget="return this.children.length;"/>
|
|
|
|
<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[selected='true']");
|
|
]]>
|
|
</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.clearSelection();
|
|
}
|
|
]]>
|
|
</setter>
|
|
</property>
|
|
|
|
<method name="appendItem">
|
|
<parameter name="aLabel"/>
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
let addition = this._createItemElement(aLabel, aValue);
|
|
this.appendChild(addition);
|
|
this.arrangeItems();
|
|
return addition;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="clearAll">
|
|
<body>
|
|
<![CDATA[
|
|
while (this.firstChild) {
|
|
this.removeChild(this.firstChild);
|
|
}
|
|
this._grid.style.width = "0px";
|
|
this._contextActions = null;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="insertItemAt">
|
|
<parameter name="anIndex"/>
|
|
<parameter name="aLabel"/>
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
let existing = this.getItemAtIndex(anIndex);
|
|
let addition = this._createItemElement(aLabel, aValue);
|
|
if (existing) {
|
|
this.insertBefore(addition, existing);
|
|
} else {
|
|
this.appendChild(addition);
|
|
}
|
|
this.arrangeItems();
|
|
return addition;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="removeItemAt">
|
|
<parameter name="anIndex"/>
|
|
<body>
|
|
<![CDATA[
|
|
let removal = this.getItemAtIndex(anIndex);
|
|
this.removeChild(removal);
|
|
this.arrangeItems();
|
|
return removal;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getIndexOfItem">
|
|
<parameter name="anItem"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!anItem)
|
|
return -1;
|
|
|
|
return Array.prototype.indexOf.call(this.children, anItem);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getItemAtIndex">
|
|
<parameter name="anIndex"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!this._isIndexInBounds(anIndex))
|
|
return null;
|
|
return this.children.item(anIndex);
|
|
]]>
|
|
</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;"/>
|
|
|
|
<field name="_scheduledArrangeItemsTries">0</field>
|
|
|
|
<!-- define a height where we consider an item not yet rendered
|
|
10 is the height of the empty item (padding/border etc. only) -->
|
|
<field name="_itemHeightRenderThreshold">10</field>
|
|
|
|
<method name="arrangeItems">
|
|
<parameter name="aNumRows"/>
|
|
<parameter name="aNumColumns"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.itemCount <= 0)
|
|
return;
|
|
let item = this.getItemAtIndex(0);
|
|
if (item == null)
|
|
return;
|
|
let gridItemRect = item.getBoundingClientRect();
|
|
|
|
// cap the number of times we reschedule calling arrangeItems
|
|
let maxRetries = 5;
|
|
|
|
// delay as necessary until the item has a proper height
|
|
if (gridItemRect.height <= this._itemHeightRenderThreshold) {
|
|
if (this._scheduledArrangeItemsTimerId) {
|
|
// retry of arrangeItems already scheduled
|
|
return;
|
|
}
|
|
|
|
// track how many times we've attempted arrangeItems
|
|
this._scheduledArrangeItemsTries++;
|
|
|
|
if (maxRetries > this._scheduledArrangeItemsTries) {
|
|
// schedule re-try of arrangeItems at the next tick
|
|
this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// items ready to arrange (or retries max exceeded)
|
|
// reset the flags
|
|
if (this._scheduledArrangeItemsTimerId) {
|
|
clearTimeout(this._scheduledArrangeItemsTimerId);
|
|
delete this._scheduledArrangeItemsTimerId;
|
|
}
|
|
if (this._scheduledArrangeItemsTries) {
|
|
this._scheduledArrangeItemsTries = 0;
|
|
}
|
|
|
|
// Autocomplete is a binding within a binding, so we have to step
|
|
// up an additional parentNode.
|
|
let container = null;
|
|
if (this.parentNode.id == "results-vbox" ||
|
|
this.parentNode.id == "searches-vbox")
|
|
container = this.parentNode.parentNode.getBoundingClientRect();
|
|
else
|
|
container = this.parentNode.getBoundingClientRect();
|
|
|
|
// If we don't have valid dimensions we can't arrange yet
|
|
if (!container.height || !gridItemRect.height)
|
|
return;
|
|
|
|
// We favor overflowing horizontally, not vertically
|
|
let maxRowCount = Math.floor(container.height / gridItemRect.height) - 1;
|
|
|
|
if (aNumRows) {
|
|
this._rowCount = aNumRows;
|
|
} else {
|
|
this._rowCount = Math.min(this.itemCount, maxRowCount);
|
|
}
|
|
if (aNumColumns) {
|
|
this._columnCount = aNumColumns;
|
|
} else {
|
|
this._columnCount = Math.ceil(this.itemCount / this._rowCount);
|
|
}
|
|
|
|
this._grid.style.width = (this._columnCount * gridItemRect.width) + "px";
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<!-- Inteface to suppress selection events -->
|
|
|
|
<field name="_suppressOnSelect"/>
|
|
<property name="suppressOnSelect"
|
|
onget="return this.getAttribute('suppressonselect') == 'true';"
|
|
onset="this.setAttribute('suppressonselect', val);"/>
|
|
|
|
<!-- Internal methods -->
|
|
|
|
<constructor>
|
|
<![CDATA[
|
|
if (this.controller && this.controller.gridBoundCallback != undefined)
|
|
this.controller.gridBoundCallback();
|
|
|
|
// XXX This event was never actually implemented (bug 223411).
|
|
var event = document.createEvent("Events");
|
|
event.initEvent("contentgenerated", true, true);
|
|
this.dispatchEvent(event);
|
|
]]>
|
|
</constructor>
|
|
|
|
<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");
|
|
item.control = this;
|
|
item.setAttribute("label", aLabel);
|
|
if (aValue)
|
|
item.setAttribute("value", aValue);
|
|
|
|
// copy over the richgrid's data-contextactions as each child is created
|
|
if (this.hasAttribute("data-contextactions")) {
|
|
item.setAttribute("data-contextactions", this.getAttribute("data-contextactions"));
|
|
}
|
|
return item;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_fireOnSelect">
|
|
<body>
|
|
<![CDATA[
|
|
if (this.suppressOnSelect || this._suppressOnSelect)
|
|
return;
|
|
|
|
var event = document.createEvent("Events");
|
|
event.initEvent("select", true, true);
|
|
this.dispatchEvent(event);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
<method name="_fireOnSelectionChange">
|
|
<body>
|
|
<![CDATA[
|
|
// flush out selection-related cached properties so they get recalc'd next time
|
|
this._contextActions = null;
|
|
|
|
// fire an event?
|
|
if (this.suppressOnSelect || this._suppressOnSelect)
|
|
return;
|
|
|
|
var event = document.createEvent("Events");
|
|
event.initEvent("selectionchange", true, true);
|
|
this.dispatchEvent(event);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
</implementation>
|
|
</binding>
|
|
|
|
<binding id="richgrid-item">
|
|
<content>
|
|
<xul:vbox anonid="anon-richgrid-item" class="richgrid-item-content">
|
|
<xul:hbox class="richgrid-icon-container">
|
|
<xul:box class="richgrid-icon-box"><xul:image xbl:inherits="src=iconURI"/></xul:box>
|
|
<xul:box flex="1" />
|
|
</xul:hbox>
|
|
<xul:description xbl:inherits="value=label" crop="end"/>
|
|
</xul:vbox>
|
|
</content>
|
|
|
|
<implementation>
|
|
<property name="_box" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'anon-richgrid-item');"/>
|
|
<property name="color" onset="this._color = val; this.setColor();" onget="return this._color;"/>
|
|
<property name="selected"
|
|
onget="return this.getAttribute('selected') == 'true';"
|
|
onset="this.setAttribute('selected', val);"/>
|
|
|
|
<constructor>
|
|
<![CDATA[
|
|
// Bindings don't get bound until the item is displayed,
|
|
// so we have to reset the background color when we get
|
|
// created.
|
|
this.setColor();
|
|
]]>
|
|
</constructor>
|
|
|
|
<property name="control">
|
|
<getter><![CDATA[
|
|
var parent = this.parentNode;
|
|
while (parent) {
|
|
if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
|
|
return parent;
|
|
parent = parent.parentNode;
|
|
}
|
|
return null;
|
|
]]></getter>
|
|
</property>
|
|
|
|
<method name="setColor">
|
|
<body>
|
|
<![CDATA[
|
|
if (this.color != undefined) {
|
|
this._box.parentNode.setAttribute("customColorPresent", "true");
|
|
this._box.style.backgroundColor = this.color;
|
|
} else {
|
|
this._box.parentNode.removeAttribute("customColorPresent");
|
|
}
|
|
]]>
|
|
</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);
|
|
]]>
|
|
</handler>
|
|
<handler event="contextmenu">
|
|
<![CDATA[
|
|
// fires for right-click, long-click and (keyboard) contextmenu input
|
|
// TODO: handle cross-slide event when it becomes available,
|
|
// .. using contextmenu is a stop-gap measure to allow us to
|
|
// toggle the selected state of tiles in a grid
|
|
this.control.handleItemContextMenu(this, event);
|
|
]]>
|
|
</handler>
|
|
</handlers>
|
|
</binding>
|
|
</bindings>
|