//////////////////////////////////////////////////////////////////////////////// // // 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: * * * * @param node The Object that defines the parent node. * * @param newChild The Object that defines the child node. * * @param index The 0-based index of where to insert the child node. * * @param source The entire collection that this node is a part of. * * @return 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