openmct/platform/commonUI/general/src/TreeNodeController.js
Victor Woeltjen 7080ca585e [Layout] Avoid infinite digest exception
Auto-expand tree nodes on a timeout, instead of immediately.
This is necessary in cases where auto-expansion several
layers deep occurs; if done immediately, each expansion
will trigger a new digest cycle, which at a certain depth
will appear to Angular as unstable.

Done in the context of WTD-535.
2014-12-05 13:31:57 -08:00

165 lines
6.8 KiB
JavaScript

/*global define,Promise*/
/**
* Module defining TreeNodeController. Created by vwoeltje on 11/10/14.
*/
define(
[],
function () {
"use strict";
/**
* The TreeNodeController supports the tree node representation;
* a tree node has a label for the current object as well as a
* subtree which shows (and is not loaded until) the node is
* expanded.
*
* This controller tracks the following, so that the tree node
* template may update its state accordingly:
*
* * Whether or not the tree node has ever been expanded (this
* is used to lazily load, exactly once, the subtree)
* * Whether or not the node is currently the domain object
* of navigation (this gets highlighted differently to
* provide the user with visual feedback.)
*
* Additionally, this controller will automatically trigger
* node expansion when this tree node's _subtree_ will contain
* the navigated object (recursively, this becomes an
* expand-to-show-navigated-object behavior.)
* @constructor
*/
function TreeNodeController($scope, $timeout) {
var selectedObject = ($scope.ngModel || {}).selectedObject,
isSelected = false,
hasBeenExpanded = false;
// Look up the id for a domain object. A convenience
// for mapping; additionally does some undefined-checking.
function getId(obj) {
return obj && obj.getId && obj.getId();
}
// Check if two domain objects have the same ID
function idsEqual(objA, objB) {
return getId(objA) === getId(objB);
}
// Get the parent of a domain object, as reported by
// its context capability. This is used to distinguish
// two different instances of a domain object that have
// been reached in different ways.
function parentOf(domainObject) {
var context = domainObject &&
domainObject.getCapability("context");
return context && context.getParent();
}
// Verify that id paths are equivalent, staring at
// index, ending at the end of the node path.
function checkPath(nodePath, navPath, index) {
index = index || 0;
// The paths overlap if we have made it past the
// end of the node's path; otherwise, check the
// id at the current index for equality and perform
// a recursive step for subsequent ids in the paths,
// until we exceed path length or hit a mismatch.
return (index >= nodePath.length) ||
(idsEqual(navPath[index], nodePath[index]) &&
checkPath(nodePath, navPath, index + 1));
}
// Check if the navigated object is in the subtree of this
// node's domain object, by comparing the paths reported
// by their context capability.
function isOnSelectionPath(nodeObject, navObject) {
var nodeContext = nodeObject &&
nodeObject.getCapability('context'),
navContext = navObject &&
navObject.getCapability('context'),
nodePath,
navPath;
if (nodeContext && navContext) {
nodePath = nodeContext.getPath().map(getId);
navPath = navContext.getPath().map(getId);
return (navPath.length > nodePath.length) &&
checkPath(nodePath, navPath);
}
return false; // No context to judge by
}
// Track that a node has been expanded, either by the
// user or automatically to show a selection.
function trackExpansion() {
if (!hasBeenExpanded) {
// Run on a timeout; if a lot of expansion needs to
// occur (e.g. if the selection is several nodes deep) we
// want this to be spread across multiple digest cycles.
$timeout(function () { hasBeenExpanded = true; }, 0);
}
}
// Consider the currently-navigated object and update
// parameters which support display.
function checkSelection() {
var nodeObject = $scope.domainObject;
// Check if we are the navigated object. Check the parent
// as well to make sure we are the same instance of the
// navigated object.
isSelected =
idsEqual(nodeObject, selectedObject) &&
idsEqual(parentOf(nodeObject), parentOf(selectedObject));
// Expand if necessary (if the navigated object will
// be in this node's subtree)
if (isOnSelectionPath(nodeObject, selectedObject) &&
$scope.toggle !== undefined) {
$scope.toggle.setState(true);
trackExpansion();
}
}
// Callback for the navigation service; track the currently
// navigated object and update display parameters as needed.
function setSelection(object) {
selectedObject = object;
checkSelection();
}
// Listen for changes which will effect display parameters
$scope.$watch("ngModel.selectedObject", setSelection);
$scope.$watch("domainObject", checkSelection);
return {
/**
* This method should be called when a node is expanded
* to record that this has occurred, to support one-time
* lazy loading of the node's subtree.
*/
trackExpansion: trackExpansion,
/**
* Check if this not has ever been expanded.
* @returns true if it has been expanded
*/
hasBeenExpanded: function () {
return hasBeenExpanded;
},
/**
* Check whether or not the domain object represented by
* this tree node should be highlighted.
* An object will be highlighted if it matches
* ngModel.selectedObject
* @returns true if this should be highlighted
*/
isSelected: function () {
return isSelected;
}
};
}
return TreeNodeController;
}
);