Source: navigation/PathTree.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * This extends the [standard tree]{@link module:alfresco/navigation/Tree} to handle path filtering updates
 * to ensure that the tree expands all nodes on the path. 
 * 
 * @module alfresco/navigation/PathTree
 * @extends module:alfresco/navigation/Tree
 * @mixes module:alfresco/documentlibrary/_AlfDocumentListTopicMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/navigation/Tree",
        "alfresco/documentlibrary/_AlfDocumentListTopicMixin",
        "alfresco/core/topics",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/dom-class",
        "dojo/query",
        "dojo/NodeList-dom"], 
        function(declare, Tree, _AlfDocumentListTopicMixin, topics, lang, array, domClass, query) {
   
   return declare([Tree, _AlfDocumentListTopicMixin], {
      
      /**
       * This determines whether or not to subcribe to the 
       * [hash change topic]{@link module:alfresco/documentlibrary/_AlfDocumentListTopicMixin#hashChangeTopic}
       * or the [path change topic]{@link module:alfresco/documentlibrary/_AlfDocumentListTopicMixin#pathChangeTopic}.
       * Although the path tree doesn't make changes to the browser URL hash, it can be driven from them to that
       * it expands tree nodes to reflect the path attribute set as a hash parameter.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useHash: true,

      /**
       * Extends the inherited function to subscribe to the [hashChangeTopic]{@link module:alfresco/documentlibrary/_AlfDocumentListTopicMixin#hashChangeTopic}
       * topic passing the [onFilterChange function]{@link module:alfresco/navigation/PathTree#onFilterChange} as the callback handler.
       * 
       * @instance
       * @listens module:alfresco/core/topics#CONTENT_DELETED
       */
      postMixInProperties: function alfresco_navigation_PathTree__postMixInProperties() {
         this.inherited(arguments);
         if (this.useHash === true)
         {
            this.alfSubscribe(this.hashChangeTopic, lang.hitch(this, this.onFilterChange));
         }
         else
         {
            this.alfSubscribe(this.pathChangeTopic, lang.hitch(this, this.onFilterChange));
         }

         this.alfSubscribe(topics.CONTENT_CREATED, lang.hitch(this, this.onContentCreated));
         this.alfSubscribe(topics.CONTENT_DELETED, lang.hitch(this, this.onContentDeleted));
      },

      /**
       * Handles the refreshing of a particular node in the tree. In order to perform the refresh
       * it is necessary to remove any cached data from the model relating to the children of the node
       * and to make sure any previously loaded data is removed from the node itself. The tree node
       * is collapsed and expanded to render the latest data. This function is called from the
       * [onContentCreated]{@link module:alfresco/navigaion/PathTree#onContentCreated} and
       * [onContentDeleted]{@link module:alfresco/navigaion/PathTree#onContentDeleted} functions to 
       * refresh the tree when folders are created and deleted.
       * 
       * @instance
       * @param  {object} treeNode The node widget to refresh in the tree.
       * @since 1.0.48
       */
      refreshTreeNode: function alfresco_navigation_PathTree__refreshTreeNode(treeNode) {
         var id = treeNode.getIdentity(); // This should be a nodeRef - we need to remove this from the cache...
         
         // Remove the parentId from the cached data of the model for the tree. This means that in the getChildren
         // function of the tree that there won't be data available to re-use...
         delete this.tree.model.childrenCache[id]; 

         // It is necessary to collapse the node before deleting the _loadDeferred data from it otherwise
         // an exception will occur in the _expandNode function 
         this.tree._collapseNode(treeNode);

         // Delete the previously loaded data from the node. This means that in the _expandNode function
         // of the tree that it will force the tree to reload.
         delete treeNode._loadDeferred;

         // Finally expand the node, because the cache and loading promise have been deleted then a
         // request will be made to fetch the current state.
         this.tree._expandNode(treeNode);
      },

      /**
       * This function attempts to [refresh the node]{@link module:alfresco/navigaion/PathTree#refreshTreeNode} 
       * that content has just been created in.
       * 
       * @instance
       * @param  {object} payload A payload containing the nodeRef of the created object and the
       * nodeRef of the node it was created as a child of.
       * @since 1.0.48
       */
      onContentCreated: function alfresco_navigation_PathTree__onContentCreated(payload) {
         if (payload.parentNodeRef)
         {
            var parentTreeNode = this.tree._itemNodesMap[payload.parentNodeRef];
            if (parentTreeNode && parentTreeNode.length)
            {
               parentTreeNode = parentTreeNode[0];
               this.refreshTreeNode(parentTreeNode);
            }
            else
            {
               // For the root node, the supplied parentNoderef will be undefined - therefore
               // we need to use the ROOT id (which is known)...
               parentTreeNode = this.tree._itemNodesMap[this.id + "_ROOT"];
               if (parentTreeNode && parentTreeNode.length)
               {
                  parentTreeNode = parentTreeNode[0];
                  this.refreshTreeNode(parentTreeNode);
               }
            }
         }
         else
         {
            this.alfLog("warn", "A publication was made indicating that content was created, but no 'parentNodeRef' was provided in the payload", payload, this);
         }
      },

      /**
       * This function attempts to [refresh the node]{@link module:alfresco/navigaion/PathTree#refreshTreeNode} 
       * that content has just been deleted from.
       * 
       * @instance
       * @param  {object} payload A payload containing an array of nodeRefs for the deleted nodes.
       * @since 1.0.48
       */
      onContentDeleted: function alfresco_navigation_PathTree__onContentDeleted(payload) {
         if (payload.nodeRefs && payload.nodeRefs.length)
         {
            var nodeRef = payload.nodeRefs[0];
            var treeNode = this.tree._itemNodesMap[nodeRef];
            if (treeNode && treeNode.length)
            {
               // If the parent node has not been expanded then the deleted item will not have been rendered
               // in the tree, therefore we need to be sure that it is in the tree before attempting to refresh.
               treeNode = treeNode[0];
               var parentTreeNode = treeNode.getParent();
               this.refreshTreeNode(parentTreeNode);
            }
         }
         else
         {
            this.alfLog("warn", "A publication was made indicating that content was deleted, but no 'nodeRefs' were provided in the payload", payload, this);
         }
      },
      
      /**
       * When the filter is updated to represent a path then this callback function will ensure
       * that the tree expands all of the relevant nodes to display that path. 
       * 
       * @instance
       * @param {object} payload 
       */
      onFilterChange: function alfresco_navigation_PathTree__onFilterChange(payload) {
         if (payload && payload.path !== null && payload.path !== undefined)
         {
            this.alfLog("log", "Filter updated", payload);

            // See AKU-1118... add a forward slash prefix if missing...
            var path = payload.path;
            if (path[0] !== "/")
            {
               path = "/" + path;
            }

            var pathElements = path.split("/");
            if (this.tree !== null && this.tree !== undefined && pathElements.length > 0)
            {
               var rootNode = this.tree.getChildren()[0];
               pathElements.shift();
               this.expandPathElement(rootNode, pathElements);
            }
         }
      },
      
      /**
       * Expands the child of the target node that matches the next element on the supplied
       * path. It then recursively calls it self until all tree nodes representing the current
       * path have been expanded.
       * 
       * @instance
       * @param {object} node The tree node to check the children of
       * @param {string[]} pathElements The array of remaining path elements to process (the first element will be searched for)
       */
      expandPathElement: function alfresco_navigation_PathTree__expandPathElement(node, pathElements) {
         this.alfLog("log", "Expanding path nodes: ", node, pathElements);
         if (node !== null && !node.isExpanded)
         {
            // It's almost certain that the node won't be expanded when first requested, and this is likely
            // to be because the data load is deferred (e.g. awaiting the results of the XHR request to get
            // it's children). In this event it is necessary to wait for the results to be available before
            // continuing to expand nodes on the path...
            this.alfLog("log", "Node load deferred", node._loadDeferred);
            node._loadDeferred.then(lang.hitch(this, "expandPathElement", node, pathElements));
         }
         else if (node !== null &&
                  pathElements !== null &&
                  pathElements.length > 0)
         {
            // If the node is expanded and there are more path elements to process then we can
            // immediately find the target node for the next path element and expand it...
            var childNodes = node.getChildren(),
                pathElement = pathElements.shift(),
                filteredNodes = array.filter(childNodes, function(item) {
               // NOTE: Compare the value, not the name as the name can be switched (e.g. documentLibrary 
               //       becomes Document Library, see "updateChild" function in TreeStore).
               return item.item.value === pathElement;
            });
            if (filteredNodes.length === 1)
            {
               // There should only ever be one result, but it's important to check...
               // at least an invalid path (e.g. to a deleted or moved folder) will not
               // be processed. There should never be more than 1 result!
               var targetChildNode = filteredNodes[0];
               query(".dijitTreeRowSelected", this.tree.domNode).removeClass("dijitTreeRowSelected");
               domClass.add(targetChildNode.rowNode, "dijitTreeRowSelected");
               this.tree._expandNode(targetChildNode);
               this.expandPathElement(targetChildNode, pathElements);
            }
            else
            {
               // Select the last expanded node
               query(".dijitTreeRowSelected", this.tree.domNode).removeClass("dijitTreeRowSelected");
               domClass.add(node.rowNode, "dijitTreeRowSelected");
            }
         }
      }
   });
});