////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package mx.collections
{
import flash.events.EventDispatcher;
import flash.utils.Dictionary;
import flash.xml.XMLNode;
import mx.collections.errors.ItemPendingError;
import mx.core.EventPriority;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.utils.IXMLNotifiable;
import mx.utils.UIDUtil;
import mx.utils.XMLNotifier;
use namespace mx_internal;
/**
* The HierarchicalCollectionView class provides a hierarchical view of a standard collection.
*
* @mxml
*
* The <mx.HierarchicalCollectionView>
inherits all the tag attributes of its superclass,
* and defines the following tag attributes:
* <mx:HierarchicalCollectionView * Properties * showRoot="true|false" * source="No default" * /> ** * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public class HierarchicalCollectionView extends EventDispatcher implements IHierarchicalCollectionView, IXMLNotifiable { include "../core/Version.as"; //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @param hierarchicalData The data structure containing the hierarchical data. * * @param argOpenNodes The Object that defines a node to appear as open. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function HierarchicalCollectionView( hierarchicalData:IHierarchicalData = null, argOpenNodes:Object = null) { super(); if (hierarchicalData) initializeCollection(hierarchicalData.getRoot(), hierarchicalData, argOpenNodes); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private */ private var hierarchicalData:IHierarchicalData; /** * @private */ private var cursor:HierarchicalCollectionViewCursor; /** * @private * The total number of nodes we know about. */ private var currentLength:int; /** * @private * Top level XML node if there is one */ private var parentNode:XML; /** * @private * Mapping of nodes to children. Used by getChildren. */ private var childrenMap:Dictionary; /** * @private */ private var childrenMapCache:Dictionary = new Dictionary(true); /** * @private */ mx_internal var treeData:ICollectionView; /** * @private * Mapping of UID to parents. Must be maintained as things get removed/added * This map is created as objects are visited */ mx_internal var parentMap:Object; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // hasRoot //---------------------------------- private var _hasRoot:Boolean; /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function get hasRoot():Boolean { return _hasRoot; } //---------------------------------- // openNodes //---------------------------------- /** * @private * Storage for the openNodes property. */ private var _openNodes:Object; /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function get openNodes():Object { return _openNodes; } /** * @private */ public function set openNodes(value:Object):void { // openNodes cant be null if (value) { _openNodes = {}; for each (var item:* in value) { _openNodes[UIDUtil.getUID(item)] = item; } } else _openNodes = {}; if (hierarchicalData) { //calc initial length currentLength = calculateLength(); // need to refresh the collection after setting openNodes var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE); event.kind = CollectionEventKind.REFRESH; dispatchEvent(event); } } //---------------------------------- // showRoot //---------------------------------- private var _showRoot:Boolean = true; [Bindable] [Inspectable(category="Data", enumeration="true,false", defaultValue="true")] /** * @inheritDoc * * @default true * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function get showRoot():Boolean { return _showRoot; } /** * @private */ public function set showRoot(value:Boolean):void { if (_showRoot != value) { _showRoot = value; if (hierarchicalData) { initializeCollection(hierarchicalData.getRoot(), hierarchicalData, openNodes); //setting showRoot resets the collection var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE); event.kind = CollectionEventKind.RESET; dispatchEvent(event); } } } //---------------------------------- // source //---------------------------------- /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function get source():IHierarchicalData { return hierarchicalData; } /** * @private */ public function set source(value:IHierarchicalData):void { initializeCollection(value.getRoot(), value, openNodes); } //---------------------------------- // filter //---------------------------------- /** * @private * Storage for the filterFunction property. */ private var _filterFunction:Function; [Bindable("filterFunctionChanged")] [Inspectable(category="General")] /** * @private */ public function get filterFunction():Function { return _filterFunction; } /** * @private */ public function set filterFunction(value:Function):void { _filterFunction = value; } //---------------------------------- // length //---------------------------------- /** * The length of the currently parsed collection. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function get length():int { return currentLength; } //---------------------------------- // sort //---------------------------------- /** * @private * Storage for the sort property. */ private var _sort:ISort; [Bindable("sortChanged")] [Inspectable(category="General")] /** * @private */ public function get sort():ISort { return _sort; } /** * @private */ public function set sort(value:ISort):void { _sort = value; } //-------------------------------------------------------------------------- // // ICollectionView Methods // //-------------------------------------------------------------------------- /** * Returns a new instance of a view iterator over the items in this view. * * @return IViewCursor instance. * * @see mx.utils.IViewCursor * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function createCursor():IViewCursor { return new HierarchicalCollectionViewCursor( this, treeData, hierarchicalData); } /** * Checks the collection for the data item using standard equality test. * * @param item The Object that defines the node to look for. * * @param
true
if the collection contains the item,
*
* @return true
if the data item is in the collection,
* and false
if not.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function contains(item:Object):Boolean
{
var cursor:IViewCursor = createCursor();
while (!cursor.afterLast)
{
if (cursor.current == item)
return true;
try
{
cursor.moveNext();
}
catch (e:ItemPendingError)
{
// item is pending.
// we are not sure if the item is present or not,
// so return false
return false;
}
}
return false;
}
/**
* @private
*/
public function disableAutoUpdate():void
{
// propogate to all the child collections
treeData.disableAutoUpdate();
for (var p:Object in childrenMap)
ICollectionView(childrenMap[p]).disableAutoUpdate();
}
/**
* @private
*/
public function enableAutoUpdate():void
{
// propogate to all the child collections
treeData.enableAutoUpdate();
for (var p:Object in childrenMap)
ICollectionView(childrenMap[p]).enableAutoUpdate();
}
/**
* @private
*/
public function itemUpdated(item:Object, property:Object = null,
oldValue:Object = null,
newValue:Object = null):void
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.UPDATE;
var objEvent:PropertyChangeEvent =
new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
objEvent.source = item;
objEvent.property = property;
objEvent.oldValue = oldValue;
objEvent.newValue = newValue;
event.items.push(objEvent);
dispatchEvent(event);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function refresh():Boolean
{
return internalRefresh(true);
}
//--------------------------------------------------------------------------
//
// IHierarchicalCollectionView Methods
//
//--------------------------------------------------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getNodeDepth(node:Object):int
{
var depth:int = 1;
var parent:Object = getParentItem(node);
while(parent != null)
{
parent = getParentItem(parent);
depth++;
}
depth = (hasRoot && !showRoot) ? (depth - 1) : depth;
// depth cant be less then 1
return (depth < 1) ? 1 : depth;
}
/**
* Returns the parent of a node.
* The parent of a top-level node is null
.
*
* @param node The Object that defines the node.
*
* @return The parent node containing the node,
* null
for a top-level node,
* and undefined
if the parent cannot be determined.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getParentItem(node:Object):*
{
var uid:String = UIDUtil.getUID(node);
if (parentMap.hasOwnProperty(uid))
return parentMap[uid];
return undefined;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getChildren(node:Object):ICollectionView
{
// Using uid because XML cant be referenced correctly using Dictionary
var uid:String = UIDUtil.getUID(node);
var children:* = hierarchicalData.getChildren(node);
var childrenCollection:ICollectionView = childrenMapCache[uid];
if (children is XMLList && childrenCollection)
{
//We don't want to send a RESET type of collectionChange event in this case.
XMLListCollection(childrenCollection).mx_internal::dispatchResetEvent = false;
XMLListCollection(childrenCollection).source = children;
// refresh the collection to apply the sort/filter
childrenCollection.refresh();
}
// check the cache and return from it.
// useful in sorting/filtering.
if(childrenCollection)
{
// node might have changed, so update the childrenMap
childrenMap[node] = childrenCollection;
return childrenCollection;
}
// if there is no children, return null
if (!children)
return null;
//then wrap children in ICollectionView if necessary
if (children is ICollectionView)
{
childrenCollection = ICollectionView(children);
}
else if (children is Array)
{
childrenCollection = new ArrayCollection(children);
}
else if (children is XMLList)
{
childrenCollection = new XMLListCollection(children);
}
else
{
var childArray:Array = new Array(children);
if (childArray != null)
{
childrenCollection = new ArrayCollection(childArray);
}
}
childrenMapCache[uid] = childrenCollection;
var oldChildren:ICollectionView = childrenMap[node];
if (oldChildren != childrenCollection)
{
if (oldChildren != null)
{
oldChildren.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
nestedCollectionChangeHandler);
}
childrenCollection.addEventListener(CollectionEvent.COLLECTION_CHANGE,
nestedCollectionChangeHandler, false, 0, true);
childrenMap[node] = childrenCollection;
}
return childrenCollection;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function openNode(node:Object):void
{
var uid:String = UIDUtil.getUID(node);
// check if the node is already opened
if (_openNodes[uid] != null)
return;
// add the node to the openNodes object and update the length
_openNodes[uid] = node;
// apply the sort/filter to the child collection of the opened node.
var childrenCollection:ICollectionView = getChildren(node);
// return if there are no children
if (!childrenCollection)
return;
if (sortCanBeApplied(childrenCollection) && !(childrenCollection.sort == null && sort == null))
{
childrenCollection.sort = this.sort;
}
if (!(childrenCollection.filterFunction == null && filterFunction == null))
{
childrenCollection.filterFunction = this.filterFunction;
}
childrenCollection.refresh();
updateParentMapAndLength(childrenCollection, node);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function closeNode(node:Object):void
{
var childrenCollection:ICollectionView = childrenMap[node];
// removes the node from the openNodes object and update the length
delete _openNodes[UIDUtil.getUID(node)];
if (childrenCollection)
{
var cursor:IViewCursor = childrenCollection.createCursor();
while (!cursor.afterLast)
{
var uid:String = UIDUtil.getUID(cursor.current);
delete parentMap[uid];
try
{
cursor.moveNext();
}
catch (e:ItemPendingError)
{
break;
}
}
}
// update the length
updateLength();
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addChild(parent:Object, newChild:Object):Boolean
{
if (parent == null)
return addChildAt(parent, newChild, treeData.length);
else
return addChildAt(parent, newChild, getChildren(parent).length);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeChild(parent:Object, child:Object):Boolean
{
var cursor:IViewCursor;
var index:int = 0;
if (parent == null)
{
cursor = treeData.createCursor();
}
else
{
var children:ICollectionView = getChildren(parent);
cursor = children.createCursor();
}
while (!cursor.afterLast)
{
if (cursor.current == child)
{
cursor.remove();
return true;
}
try
{
cursor.moveNext();
}
catch (e:ItemPendingError)
{
// Items are pending - so return false
return false;
}
}
return false;
}
/**
* Add a child node to a node at the specified index.
* This implementation does the following:
*
* parent
is null or undefined,
* inserts the child
at the
* specified index
in the collection specified
* by source
.
* parent
has a children
* field or property, the method adds the child
* to it at the index
location.
* In this case, the source
is not required.
* parent
does not have a children
* field or property, the method adds the children
* to the parent
. The method then adds the
* child
to the parent at the
* index
location.
* In this case, the source
is not required.
* index
value is greater than the collection
* length or number of children in the parent, adds the object as
* the last child.
* true
if the child is added successfully.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addChildAt(parent:Object, newChild:Object, index:int):Boolean
{
var cursor:IViewCursor;
if (!parent)
{
cursor = treeData.createCursor();
}
else
{
if (!hierarchicalData.canHaveChildren(parent))
return false;
var children:ICollectionView = getChildren(parent);
cursor = children.createCursor();
}
try
{
cursor.seek(CursorBookmark.FIRST, index);
}
catch (e:ItemPendingError)
{
// Item Pending
return false;
}
cursor.insert(newChild);
return true;
}
/**
* Removes the child node from a node at the specified index.
*
* @param parent The Object that defines the parent node.
*
* @param index The 0-based index of the child node to remove relative to the parent.
*
* @return true
if the child is removed successfully.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeChildAt(parent:Object, index:int):Boolean
{
var cursor:IViewCursor;
if (!parent)
{
cursor = treeData.createCursor();
}
else
{
var children:ICollectionView = getChildren(parent);
cursor = children.createCursor();
}
try
{
cursor.seek(CursorBookmark.FIRST, index);
}
catch (e:ItemPendingError)
{
// Item Pending
return false;
}
if (cursor.beforeFirst || cursor.afterLast)
return false;
cursor.remove();
return true;
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*
* returns the collection view of the given object
*/
private function getCollection(value:Object):ICollectionView
{
// handle strings and xml
if (typeof(value)=="string")
value = new XML(value);
else if (value is XMLNode)
value = new XML(XMLNode(value).toString());
else if (value is XMLList)
value = new XMLListCollection(value as XMLList);
if (value is XML)
{
var xl:XMLList = new XMLList();
xl += value;
return new XMLListCollection(xl);
}
//if already a collection dont make new one
else if (value is ICollectionView)
{
return ICollectionView(value);
}
else if (value is Array)
{
return new ArrayCollection(value as Array);
}
//all other types get wrapped in an ArrayCollection
else if (value is Object)
{
// convert to an array containing this one item
var tmp:Array = [];
tmp.push(value);
return new ArrayCollection(tmp);
}
else
{
return new ArrayCollection();
}
}
/**
* @private
*
* Initialize the collection. set its various properties and
* update its length.
*/
private function initializeCollection(model:Object,
hierarchicalData:IHierarchicalData,
argOpenNodes:Object = null):void
{
parentMap = {};
childrenMap = new Dictionary(true);
childrenMapCache = new Dictionary(true);
if (treeData)
treeData.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false);
if (this.hierarchicalData)
this.hierarchicalData.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false);
treeData = getCollection(model);
if (treeData)
_hasRoot = treeData.length == 1;
var tmpCollection:Object = model;
// are we swallowing the root?
if (hierarchicalData && !showRoot && hasRoot)
{
var obj:Object = treeData.createCursor().current;
if (hierarchicalData.hasChildren(obj))
{
// then get rootItem children
tmpCollection = hierarchicalData.getChildren(obj);
treeData = getCollection(tmpCollection);
}
}
// listen for add/remove events from developer as weak reference
treeData.addEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false,
EventPriority.DEFAULT_HANDLER,
true);
this.hierarchicalData = hierarchicalData;
// listen for reset/refresh events
this.hierarchicalData.addEventListener(
CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false,
EventPriority.DEFAULT_HANDLER,
true);
// openNodes cant be null
if (argOpenNodes)
_openNodes = argOpenNodes;
else
_openNodes = {};
//calc initial length
currentLength = calculateLength();
}
/**
* @private
*
* Update the parent map and adjust the length.
*/
private function updateParentMapAndLength(collection:ICollectionView, node:Object):void
{
var cursor:IViewCursor = collection.createCursor();
currentLength += collection.length;
while (!cursor.afterLast)
{
var item:Object = cursor.current;
var uid:String = UIDUtil.getUID(item);
parentMap[uid] = node;
// check that the node is opened or not.
// If it is open, then update the length with the node's children.
if (_openNodes[uid] != null)
{
var childrenCollection:ICollectionView = getChildren(item);
if (childrenCollection)
updateParentMapAndLength(childrenCollection, item);
}
try
{
cursor.moveNext();
}
catch (e:ItemPendingError)
{
break;
}
}
}
/**
* @private
* Calculate the total length of the collection, but only count nodes
* that we can reach.
*/
public function calculateLength(node:Object = null, parent:Object = null):int
{
var length:int = 0;
var childNodes:ICollectionView;
var modelOffset:int = 0;
var firstNode:Boolean = true;
if (node == null)
{
// special case counting the whole thing
// watch for page faults
var modelCursor:IViewCursor = treeData.createCursor();
if (modelCursor.beforeFirst)
{
// indicates that an IPE occured on the first item
return treeData.length;
}
while (!modelCursor.afterLast)
{
node = modelCursor.current;
if (node is XML)
{
if (firstNode)
{
firstNode = false;
var parNode:* = node.parent();
if (parNode != null)
{
startTrackUpdates(parNode);
childrenMap[parNode] = treeData;
parentNode = parNode;
}
}
startTrackUpdates(node);
}
if (node === null)
length += 1;
else
length += calculateLength(node, null) + 1;
modelOffset++;
try
{
modelCursor.moveNext();
}
catch (e:ItemPendingError)
{
// just stop where we are, no sense paging
// the whole thing just to get length. make a rough
// guess assuming that all un-paged nodes are closed
length += treeData.length - modelOffset;
return length;
}
}
}
else
{
var uid:String = UIDUtil.getUID(node);
parentMap[uid] = parent;
if (node != null &&
openNodes[uid] &&
hierarchicalData.canHaveChildren(node) &&
hierarchicalData.hasChildren(node))
{
childNodes = getChildren(node);
if (childNodes != null)
{
var childCursor:IViewCursor = childNodes.createCursor();
try
{
childCursor.seek(CursorBookmark.FIRST);
while (!childCursor.afterLast)
{
if (node is XML)
startTrackUpdates(childCursor.current);
length += calculateLength(childCursor.current, node) + 1;
modelOffset++;
try
{
childCursor.moveNext();
}
catch (e:ItemPendingError)
{
// just stop where we are, no sense paging
// the whole thing just to get length. make a rough
// guess assuming that all un-paged nodes are closed
length += childNodes.length - modelOffset;
return length;
}
}
}
catch (e:ItemPendingError)
{
// assume that the child collection has one item
length += 1;
}
}
}
}
return length;
}
/**
* @private
*/
private function internalRefresh(dispatch:Boolean):Boolean
{
var obj:Object;
var coll:ICollectionView
var needUpdate:Boolean = false;
// apply filter function to all the collections including the child collections
if (!(treeData.filterFunction == null && filterFunction == null))
{
treeData.filterFunction = filterFunction;
treeData.refresh();
needUpdate = true;
}
for each(obj in openNodes)
{
coll = getChildren(obj);
if (coll && !(coll.filterFunction == null && filterFunction == null))
{
coll.filterFunction = filterFunction;
coll.refresh();
needUpdate = true
}
}
// if filter is applied to any collection, only then update the length
if (needUpdate)
updateLength(); // length will change after filtering, so update it.
// apply sort to all the collections including the child collections
if (sortCanBeApplied(treeData) && !(treeData.sort == null && sort == null))
{
treeData.sort = sort;
treeData.refresh();
dispatch = true;
}
// recursive sort for every field
for each(obj in openNodes)
{
coll = getChildren(obj);
if (coll && sortCanBeApplied(coll) && !(coll.sort == null && sort == null))
{
coll.sort = sort;
coll.refresh();
dispatch = true;
}
}
// No concept of a sort level, so commenting the code
/* for(var i:int = 0;i