Dactyloidae/mailnews/base/content/jsTreeView.js

235 lines
7.9 KiB
JavaScript

/* 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/. */
/**
* This file contains a prototype object designed to make the implementation of
* nsITreeViews in javascript simpler. This object requires that consumers
* override the _rebuild function. This function must set the _rowMap object to
* an array of objects fitting the following interface:
*
* readonly attribute string id - a unique identifier for the row/object
* readonly attribute integer level - the hierarchy level of the row
* attribute boolean open - whether or not this item's children are exposed
* string getText(aColName) - return the text to display for this row in the
* specified column
* string getProperties() - return the css-selectors
* attribute array children - return an array of child-objects also meeting this
* interface
*/
function PROTO_TREE_VIEW() {
this._tree = null;
this._rowMap = [];
this._persistOpenMap = [];
}
PROTO_TREE_VIEW.prototype = {
get rowCount() {
return this._rowMap.length;
},
/**
* CSS files will cue off of these. Note that we reach into the rowMap's
* items so that custom data-displays can define their own properties
*/
getCellProperties: function jstv_getCellProperties(aRow, aCol) {
return this._rowMap[aRow].getProperties(aCol);
},
/**
* The actual text to display in the tree
*/
getCellText: function jstv_getCellText(aRow, aCol) {
return this._rowMap[aRow].getText(aCol.id);
},
/**
* The jstv items take care of assigning this when building children lists
*/
getLevel: function jstv_getLevel(aIndex) {
return this._rowMap[aIndex].level;
},
/**
* This is easy since the jstv items assigned the _parent property when making
* the child lists
*/
getParentIndex: function jstv_getParentIndex(aIndex) {
return this._rowMap.indexOf(this._rowMap[aIndex]._parent);
},
/**
* This is duplicative for our normal jstv views, but custom data-displays may
* want to do something special here
*/
getRowProperties: function jstv_getRowProperties(aRow) {
return this._rowMap[aRow].getProperties();
},
/**
* If an item in our list has the same level and parent as us, it's a sibling
*/
hasNextSibling: function jstv_hasNextSibling(aIndex, aNextIndex) {
let targetLevel = this._rowMap[aIndex].level;
for (let i = aNextIndex + 1; i < this._rowMap.length; i++) {
if (this._rowMap[i].level == targetLevel)
return true;
if (this._rowMap[i].level < targetLevel)
return false;
}
return false;
},
/**
* If we have a child-list with at least one element, we are a container.
*/
isContainer: function jstv_isContainer(aIndex) {
return this._rowMap[aIndex].children.length > 0;
},
isContainerEmpty: function jstv_isContainerEmpty(aIndex) {
// If the container has no children, the container is empty.
return !this._rowMap[aIndex].children.length;
},
/**
* Just look at the jstv item here
*/
isContainerOpen: function jstv_isContainerOpen(aIndex) {
return this._rowMap[aIndex].open;
},
isEditable: function jstv_isEditable(aRow, aCol) {
// We don't support editing rows in the tree yet.
return false;
},
isSeparator: function jstv_isSeparator(aIndex) {
// There are no separators in our trees
return false;
},
isSorted: function jstv_isSorted() {
// We do our own customized sorting
return false;
},
setTree: function jstv_setTree(aTree) {
this._tree = aTree;
},
recursivelyAddToMap: function jstv_recursivelyAddToMap(aChild, aNewIndex) {
// When we add sub-children, we're going to need to increase our index
// for the next add item at our own level.
let currentCount = this._rowMap.length;
if (aChild.children.length && aChild.open) {
for (let [i, child] in Iterator(this._rowMap[aNewIndex].children)) {
let index = aNewIndex + i + 1;
this._rowMap.splice(index, 0, child);
aNewIndex += this.recursivelyAddToMap(child, index);
}
}
return this._rowMap.length - currentCount;
},
/**
* Opens or closes a container with children. The logic here is a bit hairy, so
* be very careful about changing anything.
*/
toggleOpenState: function jstv_toggleOpenState(aIndex) {
// Ok, this is a bit tricky.
this._rowMap[aIndex]._open = !this._rowMap[aIndex].open;
if (!this._rowMap[aIndex].open) {
// We're closing the current container. Remove the children
// Note that we can't simply splice out children.length, because some of
// them might have children too. Find out how many items we're actually
// going to splice
let level = this._rowMap[aIndex].level;
let row = aIndex + 1;
while (row < this._rowMap.length && this._rowMap[row].level > level) {
row++;
}
let count = row - aIndex - 1;
this._rowMap.splice(aIndex + 1, count);
// Remove us from the persist map
let index = this._persistOpenMap.indexOf(this._rowMap[aIndex].id);
if (index != -1)
this._persistOpenMap.splice(index, 1);
// Notify the tree of changes
if (this._tree) {
this._tree.rowCountChanged(aIndex + 1, -count);
}
} else {
// We're opening the container. Add the children to our map
// Note that these children may have been open when we were last closed,
// and if they are, we also have to add those grandchildren to the map
let oldCount = this._rowMap.length;
this.recursivelyAddToMap(this._rowMap[aIndex], aIndex);
// Add this container to the persist map
let id = this._rowMap[aIndex].id;
if (this._persistOpenMap.indexOf(id) == -1)
this._persistOpenMap.push(id);
// Notify the tree of changes
if (this._tree)
this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount);
}
// Invalidate the toggled row, so that the open/closed marker changes
if (this._tree)
this._tree.invalidateRow(aIndex);
},
// We don't implement any of these at the moment
canDrop: function jstv_canDrop(aIndex, aOrientation) {},
drop: function jstv_drop(aRow, aOrientation) {},
performAction: function jstv_performAction(aAction) {},
performActionOnCell: function jstv_performActionOnCell(aAction, aRow, aCol) {},
performActionOnRow: function jstv_performActionOnRow(aAction, aRow) {},
selectionChanged: function jstv_selectionChanged() {},
setCellText: function jstv_setCellText(aRow, aCol, aValue) {},
setCellValue: function jstv_setCellValue(aRow, aCol, aValue) {},
getCellValue: function jstv_getCellValue(aRow, aCol) {},
getColumnProperties: function jstv_getColumnProperties(aCol) { return ""; },
getImageSrc: function jstv_getImageSrc(aRow, aCol) {},
getProgressMode: function jstv_getProgressMode(aRow, aCol) {},
cycleCell: function jstv_cycleCell(aRow, aCol) {},
cycleHeader: function jstv_cycleHeader(aCol) {},
_tree: null,
/**
* An array of jstv items, where each item corresponds to a row in the tree
*/
_rowMap: null,
/**
* This is a javascript map of which containers we had open, so that we can
* persist their state over-time. It is designed to be used as a JSON object.
*/
_persistOpenMap: null,
_restoreOpenStates: function jstv__restoreOpenStates() {
// Note that as we iterate through here, .length may grow
for (let i = 0; i < this._rowMap.length; i++) {
if (this._persistOpenMap.indexOf(this._rowMap[i].id) != -1)
this.toggleOpenState(i);
}
},
QueryInterface: function QueryInterface(aIID) {
if (aIID.equals(Components.interfaces.nsITreeView) ||
aIID.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
}
};