azure-devops-ui
Version:
React components for building web UI in Azure DevOps
463 lines (461 loc) • 23.3 kB
JavaScript
import { __spreadArray } from "tslib";
import { ObservableArray } from '../Core/Observable';
/**
* A TreeItemProvider is designed to store and manage the state of a tree.
* As items are added/removed, expanded/collapsed, the providers "value" will
* represent the current set of ordered items within the tree.
*/
var TreeItemProvider = /** @class */ (function () {
function TreeItemProvider(rootItems) {
if (rootItems === void 0) { rootItems = []; }
// Track a map for each ITreeItem being tracked in the tree to its internal ITreeItemEx.
this.itemMap = new Map();
var treeItems = [];
// Make a copy of the initial root items
this.rootItems = __spreadArray([], rootItems, true);
// Add the root item and its entire sub-tree to the item map and update the items passed to the underlying tree.
this.addItems(rootItems, undefined, treeItems);
// Create the underlying observable we use to manage the underlying table items.
this.tableItems = new ObservableArray(treeItems);
}
Object.defineProperty(TreeItemProvider.prototype, "length", {
get: function () {
return this.tableItems.length;
},
enumerable: false,
configurable: true
});
Object.defineProperty(TreeItemProvider.prototype, "roots", {
get: function () {
return this.rootItems;
},
enumerable: false,
configurable: true
});
Object.defineProperty(TreeItemProvider.prototype, "value", {
get: function () {
return this.tableItems.value;
},
enumerable: false,
configurable: true
});
TreeItemProvider.prototype.subscribe = function (observer, action) {
return this.tableItems.subscribe(observer, action);
};
TreeItemProvider.prototype.unsubscribe = function (observer, action) {
return this.tableItems.unsubscribe(observer, action);
};
/**
* add can be used to add a single item to the tree at a given location. If the
* specified parentItem or insertAfter are not in the tree or the insertAfter is not
* a child of parentItem, the addition will be ignored.
*
* @param item The item to add to the tree.
* @param parentItem The direct parent of the item being added.
* If this is a rootItem leave the parentItem undefined.
* @param insertAfter The sibling item this item should be inserted after.
* If this is the first item within the parent leave insertAfter undefined.
*/
TreeItemProvider.prototype.add = function (item, parentItem, insertAfter) {
return this.splice(parentItem, undefined, [
{
insertAfter: insertAfter,
items: [item]
}
]);
};
/**
* clear can be used to reset the provider. This is an optimized way to
* remove all items from the tree.
*/
TreeItemProvider.prototype.clear = function () {
this.tableItems.splice(0, this.tableItems.length);
this.rootItems.splice(0, this.rootItems.length);
};
/**
* Collapse the node.
*
* @param treeItem The item whose state should be collapsed. If the treeItem is
* not in the tree or if the item is already expanded the method will no-op.
*/
TreeItemProvider.prototype.collapse = function (treeItem) {
treeItem.expanded && this.toggle(treeItem);
};
/**
* Expand the node. Optionally expand all parent nodes.
*
* @param treeItem The item whose state should be expanded. If the treeItem is
* not in the tree the method will no-op.
* @param expandParents If all parent nodes should be expanded. @default false
*/
TreeItemProvider.prototype.expand = function (treeItem, expandParents) {
var treeItemEx = this.itemMap.get(treeItem);
if (!treeItemEx) {
return;
}
var expandNodes = [];
do {
if (!treeItemEx.underlyingItem.expanded) {
expandNodes.push(treeItemEx);
}
treeItemEx = treeItemEx.parentItem;
} while (expandParents && treeItemEx);
for (var index = expandNodes.length - 1; index >= 0; index--) {
this.toggle(expandNodes[index].underlyingItem);
}
};
/**
* Remove the specified item from the tree. If the item doesnt exist the
* remove will be ignored.
*
* @param treeItem The item that should be removed from the tree.
* @param parentItem The parentItem of the item being removed. If the caller
* doesnt have the parentItem readily available it will be computed, but this
* can be expensive. The caller should supply it if they can.
*/
TreeItemProvider.prototype.remove = function (treeItem, parentItem) {
if (!parentItem) {
var itemIndex = void 0;
if ((itemIndex = this.indexOf(treeItem)) === -1) {
return;
}
var removeItem = this.tableItems.value[itemIndex];
parentItem = removeItem.parentItem && removeItem.parentItem.underlyingItem;
}
return this.splice(parentItem, [treeItem]);
};
/**
* spliceItems is used to update the direct children of a given item
* in the tree. If the parentItem is supplied and not in the tree the
* changes will be ignored.
*
* @NOTE: Adds are processed before removes, this allows the existing
* elements to be used as an insertAfter for placement. An item can't
* be in both the add and remove set. This will cause duplication.
*
* @param parentItem The item whos children are being updated. If undefined
* is supplied the updates will modify the root.
* @param itemsToRemove The set of items that should removed from the parent.
* @param itemsToAdd The set of items that should be added to the parent.
*/
TreeItemProvider.prototype.splice = function (parentItem, itemsToRemove, itemsToAdd) {
var _a;
var parentItemEx;
var notifyChange = false;
var parentIndex = -1;
var childItems = this.rootItems;
if (parentItem) {
parentItemEx = this.itemMap.get(parentItem);
if (!parentItemEx) {
return;
}
// Find the location in the table items we need splice changes.
parentIndex = this.indexOf(parentItem);
childItems = parentItemEx.underlyingItem.childItems || [];
}
// Before making any changes make a copy of the removed array, this allows
// the caller to supply the current childItems to clear them.
if (itemsToRemove) {
itemsToRemove = itemsToRemove.slice(0);
}
// We will process the adds before the removes.
if (itemsToAdd) {
for (var index = 0; index < itemsToAdd.length; index++) {
var tableItems = !parentItem || (parentIndex >= 0 && parentItem.expanded) ? [] : undefined;
var itemToAdd = itemsToAdd[index];
var insertIndex = parentIndex;
var childIndex = 0;
// If we are inserting after a specific item find that item. If it
// isn't found, ignore the add.
if (itemToAdd.insertAfter) {
// Ensure the insertAfter is a child of the parentItem.
if ((childIndex = childItems.indexOf(itemToAdd.insertAfter)) === -1) {
continue;
}
// Find the location in the underlying tableItems to insert.
var childInsertIndex = this.indexOf(itemToAdd.insertAfter, parentIndex + 1);
if (childInsertIndex !== -1) {
// Determine the number of elements in table under the insertAfter, we need to insert
// after these elements to ensure the inserted element is a sibling of insertAfter.
insertIndex = childInsertIndex + this.getTableChildCount(itemToAdd.insertAfter);
}
}
else {
// if we add to the front, childIndex needs to be -1 so when we do the +1 later, it will be 0
childIndex--;
}
// Add the items to the overall item map and compute the item being sent to the table.
this.addItems(itemToAdd.items, parentItemEx, tableItems);
// Update the childItems and if this is the first child update the treeItem.
// We will notify if we are adding the first child since the state of the item is changing.
childItems.splice.apply(childItems, __spreadArray([childIndex + 1, 0], itemToAdd.items, false));
if (parentItemEx && !parentItemEx.underlyingItem.childItems) {
parentItemEx.underlyingItem.childItems = childItems;
notifyChange = true;
}
// Add the new items to the set of table items.
if (tableItems) {
(_a = this.tableItems).splice.apply(_a, __spreadArray([insertIndex + 1, 0], tableItems, false));
notifyChange = true;
}
}
}
// Now process the removes.
if (itemsToRemove) {
for (var index = 0; index < itemsToRemove.length; index++) {
var itemToRemove = itemsToRemove[index];
var tableItems = !parentItem || parentItem.expanded ? [] : undefined;
var childIndex = void 0;
// Ensure the itemToRemove is a child of the parentItem.
if ((childIndex = childItems.indexOf(itemToRemove)) === -1) {
continue;
}
// Remove the items from the underlying map and compute the set of items in the table.
this.removeItem(itemToRemove, tableItems);
// Remove the item from this childItem list. Notify if we removed the last child
// since this is changing the state of the item.
childItems.splice(childIndex, 1);
if (childItems.length === 0 && parentItem) {
delete parentItem.childItems;
notifyChange = true;
}
var removeIndex = this.indexOf(itemToRemove, parentIndex + 1);
if (removeIndex === -1) {
continue;
}
// Remove the items from the underlying observable.
if (tableItems) {
this.tableItems.splice(removeIndex, tableItems.length);
notifyChange = true;
}
}
}
if (notifyChange && parentItemEx && parentIndex !== -1) {
this.tableItems.change(parentIndex, parentItemEx);
}
};
/**
* spliceBatch is used to update the direct children of a given item
* in the tree in batches. It has the same logic as splice, but it
* processes updates sequentially.
* @param batch The array of updates to be applied to the tree sequentially
* one by one.
*/
TreeItemProvider.prototype.spliceBatch = function (batch) {
var _a;
var orderedBatch = [];
// Everything we will do with a copy of this.tableItems will not trigger any notifications of subscribers
// because creating new ObservableArray doesn't copy subscribers.
var copyOfTableItems = new ObservableArray(this.tableItems.value);
for (var _i = 0, batch_1 = batch; _i < batch_1.length; _i++) {
var el = batch_1[_i];
var parentItemEx = void 0;
var notifyChange = false;
var parentIndex = -1;
var childItems = this.rootItems;
if (el.parentItem) {
parentItemEx = this.itemMap.get(el.parentItem);
if (!parentItemEx) {
return;
}
// Find the location in the table items we need splice changes.
parentIndex = this.indexOf(el.parentItem, undefined, copyOfTableItems);
childItems = parentItemEx.underlyingItem.childItems || [];
}
// Before making any changes make a copy of the removed array, this allows
// the caller to supply the current childItems to clear them.
if (el.itemsToRemove) {
el.itemsToRemove = el.itemsToRemove.slice(0);
}
// We will process the adds before the removes.
if (el.itemsToAdd) {
for (var index = 0; index < el.itemsToAdd.length; index++) {
var tableItems = !el.parentItem || (parentIndex >= 0 && el.parentItem.expanded) ? [] : undefined;
var itemToAdd = el.itemsToAdd[index];
var insertIndex = parentIndex;
var childIndex = 0;
// If we are inserting after a specific item find that item. If it
// isn't found, ignore the add.
if (itemToAdd.insertAfter) {
// Ensure the insertAfter is a child of the parentItem.
if ((childIndex = childItems.indexOf(itemToAdd.insertAfter)) === -1) {
continue;
}
// Find the location in the underlying tableItems to insert.
var childInsertIndex = this.indexOf(itemToAdd.insertAfter, parentIndex + 1, copyOfTableItems);
if (childInsertIndex !== -1) {
// Determine the number of elements in table under the insertAfter, we need to insert
// after these elements to ensure the inserted element is a sibling of insertAfter.
insertIndex = childInsertIndex + this.getTableChildCount(itemToAdd.insertAfter);
}
}
else {
// if we add to the front, childIndex needs to be -1 so when we do the +1 later, it will be 0
childIndex--;
}
// Add the items to the overall item map and compute the item being sent to the table.
this.addItems(itemToAdd.items, parentItemEx, tableItems);
// Update the childItems and if this is the first child update the treeItem.
// We will notify if we are adding the first child since the state of the item is changing.
childItems.splice.apply(childItems, __spreadArray([childIndex + 1, 0], itemToAdd.items, false));
if (parentItemEx && !parentItemEx.underlyingItem.childItems) {
parentItemEx.underlyingItem.childItems = childItems;
notifyChange = true;
}
// Add the new items to the set of table items.
if (tableItems) {
var itemsToInsert = el.ignoreDuplicatesOnAdding
? tableItems.filter(function (tableItem) { return !copyOfTableItems.value.some(function (x) { return x.underlyingItem == tableItem.underlyingItem; }); })
: tableItems;
copyOfTableItems.splice.apply(copyOfTableItems, __spreadArray([insertIndex + 1, 0], itemsToInsert, false));
notifyChange = true;
}
}
}
// Now process the removes.
if (el.itemsToRemove) {
for (var index = 0; index < el.itemsToRemove.length; index++) {
var itemToRemove = el.itemsToRemove[index];
var tableItems = !el.parentItem || el.parentItem.expanded ? [] : undefined;
var childIndex = void 0;
// Ensure the itemToRemove is a child of the parentItem.
if ((childIndex = childItems.indexOf(itemToRemove)) === -1) {
continue;
}
// Remove the items from the underlying map and compute the set of items in the table.
this.removeItem(itemToRemove, tableItems);
// Remove the item from this childItem list. Notify if we removed the last child
// since this is changing the state of the item.
childItems.splice(childIndex, 1);
if (childItems.length === 0 && el.parentItem) {
delete el.parentItem.childItems;
notifyChange = true;
}
var removeIndex = this.indexOf(itemToRemove, parentIndex + 1, copyOfTableItems);
if (removeIndex === -1) {
continue;
}
// Remove the items from the underlying observable.
if (tableItems) {
copyOfTableItems.splice(removeIndex, tableItems.length);
notifyChange = true;
}
}
}
if (notifyChange && parentItemEx && parentIndex !== -1) {
orderedBatch.push({ start: parentIndex, items: [parentItemEx] });
}
}
// Update tableItems at a heat.
(_a = this.tableItems).splice.apply(_a, __spreadArray([0, this.tableItems.length], copyOfTableItems.value, false));
if (orderedBatch.length) {
this.tableItems.changeOrderedBatch(orderedBatch);
}
};
/**
* toggleItem is used to toggle the expand/collapse state of a given tree item.
*
* @param treeItem The item whose state should be toggled. If the treeItem is
* not in the tree the method will no-op.
*/
TreeItemProvider.prototype.toggle = function (treeItem) {
var _a;
var itemIndex = this.indexOf(treeItem);
// Toggle the expanded state of the treeItem.
treeItem.expanded = !treeItem.expanded;
if (itemIndex >= 0) {
if (treeItem.childItems) {
var tableItems = [];
// Get the set of children being added to the underlying array.
for (var index = 0; index < treeItem.childItems.length; index++) {
this.getTableItems(treeItem.childItems[index], tableItems);
}
// We need to update the underlying observables items.
if (treeItem.expanded) {
(_a = this.tableItems).splice.apply(_a, __spreadArray([itemIndex + 1, 0], tableItems, false));
}
else {
this.tableItems.splice(itemIndex + 1, tableItems.length);
}
}
}
};
TreeItemProvider.prototype.addItems = function (treeItems, parentItem, tableItems) {
for (var index = 0; index < treeItems.length; index++) {
var treeItem = treeItems[index];
var treeItemEx = { depth: parentItem ? parentItem.depth + 1 : 0, parentItem: parentItem, underlyingItem: treeItem };
// Add this treeItem and computed treeItemEx to the map.
this.itemMap.set(treeItem, treeItemEx);
// If the caller requested the set of items that should be given to the table.
if (tableItems) {
tableItems.push(treeItemEx);
}
// Go through all the children and add them to the map, and if it is expanded
// we will forward the current tableItems array.
if (treeItem.childItems) {
this.addItems(treeItem.childItems, treeItemEx, treeItem.expanded ? tableItems : undefined);
}
}
};
TreeItemProvider.prototype.getTableItems = function (treeItem, tableItems) {
var treeItemEx = this.itemMap.get(treeItem);
if (treeItemEx) {
tableItems.push(treeItemEx);
if (treeItem.childItems && treeItem.expanded) {
for (var _i = 0, _a = treeItem.childItems; _i < _a.length; _i++) {
var childItem = _a[_i];
this.getTableItems(childItem, tableItems);
}
}
}
};
TreeItemProvider.prototype.getTableChildCount = function (treeItem) {
var count = 0;
if (treeItem.childItems && treeItem.expanded) {
for (var index = 0; index < treeItem.childItems.length; index++) {
count += this.getTableChildCount(treeItem.childItems[index]) + 1;
}
}
return count;
};
/**
* indexOfItem is used to find the index of a source treeItem. The underlying
* observable indexOf will find instances of ITreeItemEx. This returns the
* index from the set of items passed to the table.
*
* @param treeItem The item to find in the tree.
* @param fromIndex The index to start the search.
* @param tableItems TreeProvider to search at. this.tableItems will be used as default if not provided.
* @returns If the item is found an index >= 0 is returned, if it is not found -1 is returned.
*/
TreeItemProvider.prototype.indexOf = function (treeItem, fromIndex, tableItems) {
if (fromIndex === void 0) { fromIndex = 0; }
if (tableItems === undefined) {
tableItems = this.tableItems;
}
// @TODO: Can we come up with a faster method than this.
for (var index = fromIndex; index < tableItems.length; index++) {
if (treeItem === tableItems.value[index].underlyingItem) {
return index;
}
}
return -1;
};
TreeItemProvider.prototype.removeItem = function (treeItem, tableItems) {
// Add this treeItem and computed treeItemEx to the map.
this.itemMap.delete(treeItem);
// If the caller requested the set of items that should be given to the table.
if (tableItems) {
tableItems.push(treeItem);
}
// Go through all the children and add them to the map, and if it is expanded
// we will forward the current tableItems array.
if (treeItem.childItems) {
for (var childIndex = 0; childIndex < treeItem.childItems.length; childIndex++) {
this.removeItem(treeItem.childItems[childIndex], treeItem.expanded ? tableItems : undefined);
}
}
};
return TreeItemProvider;
}());
export { TreeItemProvider };