Source: navigation/Tree.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/>.
 */

/**
 * @module alfresco/navigation/Tree
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/renderers/_PublishPayloadMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/documentlibrary/_AlfDocumentListTopicMixin
 * @mixes module:alfresco/services/_NavigationServiceTopicMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/Tree.html",
        "alfresco/renderers/_PublishPayloadMixin",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/core/topics",
        "service/constants/Default",
        "alfresco/documentlibrary/_AlfDocumentListTopicMixin",
        "alfresco/services/_NavigationServiceTopicMixin",
        "dojo/dom-construct",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/aspect",
        "dojo/dom-class",
        "alfresco/navigation/TreeStore",
        "dijit/tree/ObjectStoreModel",
        "dijit/Tree"], 
        function(declare, _Widget, _Templated, template, _PublishPayloadMixin, AlfCore, CoreWidgetProcessing, topics, AlfConstants, _AlfDocumentListTopicMixin, 
                 _NavigationServiceTopicMixin, domConstruct, lang, array, aspect, domClass, TreeStore, ObjectStoreModel, Tree) {
   
   // Extend the standard Dijit tree to support better identification of nodes (primarily for the purpose of unit testing)...
   var AikauTree = declare([Tree], {
      _createTreeNode: function alfresco_navigation_Tree_AikauTree___createTreeNode(){
         var id = lang.getObject("item.id", false, arguments[0]);
         if (id)
         {
            declare.safeMixin(arguments[0], {
               id: this.id + "_" + id.replace("://", "_").replace("/", "_") // Clean up NodeRef IDs
            });
         }
         var treeNode = this.inherited(arguments);

         // When a tree node is added it is possible that it can be disabled if it meets 
         // configured disablement rules. This has been implemented to support the use case
         // required for Cloud Sync where existing sync folders cannot be reused.
         if (this.tree.treeNodeDisablementConfig && this.tree.treeNodeDisablementConfig.rules)
         {
            var item = arguments[0];
            var disabled = array.some(this.tree.treeNodeDisablementConfig.rules, lang.hitch(this, this.processDisablementRule, item));
            if (disabled) 
            {
               domClass.add(treeNode.domNode, "alfresco-navigation-Tree__node--disabled");
            }
         }

         return treeNode;
      },

      /**
       * Extended to ignore clicks on disabled nodes.
       * 
       * @param  {object}  nodeWidget The tree node widget clicked
       * @param  {object}  e          The click event
       * @param  {boolean} doOpen     Whether or not to open
       * @param  {string}  func       A click callback function.
       * @since 1.0.94
       */
      __click: function alfresco_navigation_Tree_AikauTree____click(/*jshint unused:false*/ nodeWidget, e, doOpen, func) {
         if (nodeWidget && nodeWidget.domNode && domClass.contains(nodeWidget.domNode, "alfresco-navigation-Tree__node--disabled"))
         {
            e.stopPropagation();
            e.preventDefault();
         }
         else
         {
            this.inherited(arguments);
         }
      },

      /**
       * Processes any configured disablement rules for tree nodes. Please note that at present only
       * the "contains" rule is supported. If other rules are required then please raise an issue
       * for them to be implemented.
       *
       * @instance
       * @param  {object} node The node item to evaluate the data of
       * @param  {object} rule The rule configuration to process
       * @return {boolean} Whether or not the tree node should be disabled
       * @since 1.0.94
       */
      processDisablementRule: function alfresco_navigation_Tree_AikauTree__processDisablementRule(node, rule) {
         var disabled = false;
         if (rule.property && rule.contains)
         {
            var targetArray = lang.getObject(rule.property, false, node);
            if (targetArray)
            {
               disabled = array.some(targetArray, function(target) {
                  return array.some(rule.contains, function(containsItem) {
                     return containsItem === target;
                  });
               });
            }
         }
         return disabled;
      }
   });


   return declare([_Widget, _Templated, _PublishPayloadMixin, AlfCore, CoreWidgetProcessing, _AlfDocumentListTopicMixin, _NavigationServiceTopicMixin], {
      
      /**
       * An array of the i18n files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/Tree.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/Tree.properties"}],
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/Tree.css"}]
       */
      cssRequirements: [{cssFile:"./css/Tree.css"}],
      
      /**
       * Additional space separated CSS classes to be applied to the root DOM node of the widget.
       * 
       * @instance
       * @type {string}
       * @default
       */
      customCssClasses: "",
      
      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {string}
       */
      templateString: template,
      
      /**
       * @instance
       * @type {string}
       * @default
       */
      siteId: null,
       
      /**
       * This should be set when the current context is a site, typically this will be set to "documentlibrary"
       * 
       * @instance
       * @type {string}
       * @default
       */
      containerId: null,
      
      /**
       * This should be set if "siteId" is not set.
       * 
       * @instance
       * @type {string}
       * @default
       */
      rootNode: null,
      
      /**
       * @instance
       * @type {string}
       * @default
       */
      rootLabel: "documentlibrary.root.label",
      
      /**
       * This is the value that should be given to the root node. This node is not actually requested
       * but is simply generated on instantiation. The value assigned to it is important because it will
       * be included as the filter data when it is clicked.
       * 
       * @instance
       * @type {string}
       * @default
       */
      rootValue: "",
      
      /**
       * @instance
       * @type {string}
       * @default
       */
      filterPrefsName: "docListTreePref",

       /**
       * This is an array of Regular Expressions that should match pathes to show. At least one of the 
       * Regular Expressions in the array needs to pass true for the tree node to be displayed. To show
       * all paths this should be left as null (the default value).
       *
       * @instance
       * @type {array}
       * @default
       */
      filterPaths: null,

      /**
       * Indicates whether or not the root node of the tree should be displayed or not.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      showRoot: true,
      
      /**
       * This is a topic that can be published to request child data.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.39
       * @event
       */
      childRequestPublishTopic: null,

      /**
       * This is the payload that will be published to request child data when a
       * [publishTopic]{@link module:alfresco/navigation/Tree#childRequestPublishTopic} has been configured.
       * 
       * @instance
       * @type {object}
       * @default
       * @since 1.0.39
       */
      childRequestPublishPayload: null,

      /**
       * Indicates whether or not requests to get child data will be published globally using the 
       * [publishTopic]{@link module:alfresco/navigation/Tree#childRequestPublishTopic} that has been configured.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.39
       */
      childRequestPublishGlobal: true,

      /**
       * Configuration rules that determine when a tree node should be disabled.
       * 
       * @instance
       * @type {object}
       * @default
       * @since 1.0.94
       */
      treeNodeDisablementConfig: null,

      /**
       * @instance
       * @return {string} The root of the URL to use when requesting child nodes.
       */
      getTargetUrl: function alfresco_navigation_Tree__getTargetUrl() {
         var url = null;
         if (this.siteId && this.containerId)
         {
            url = AlfConstants.PROXY_URI + "slingshot/doclib/treenode/site/" + this.siteId + "/documentlibrary";
         }
         else if (this.rootNode)
         {
            url = AlfConstants.PROXY_URI + "slingshot/doclib/treenode/node/alfresco/company/home";
         }
         else if (!this.childRequestPublishTopic)
         {
            this.alfLog("error", "Cannot create a tree without 'siteId' and 'containerId' or 'rootNode' attributes", this);
         }
         return url;
      },
      
      /**
       * Gets an object containing the query parameters to include when requesting child nodes. The object returned
       * will differ slightly depending upon whether the tree is configured to represent a site or a general node. 
       * A general node query will include a library root parameter (this will typically map to either company home,
       * user home or shared files but can be any valid node). 
       * 
       * @instance
       * @return {object} The query object to include when requesting child nodes.
       */
      getTargetQueryObject: function alfresco_navigation_Tree__getTargetQueryObject() {
         var query = {
            perms: "false",
            children: "false",
            max: "-1"
         };
         if (this.siteId && this.containerId)
         {
            // No changes to the default query
         }
         else if (this.rootNode)
         {
            query.max = "500";
            query.libraryRoot = this.rootNode;
         }
         return query;
      },
      
      /**
       * Creates the a dijit/Tree widget.
       * 
       * @instance
       */
      postCreate: function alfresco_navigation_Tree__postCreate() {

         this.showRoot = this.showRoot !== null ? this.showRoot : true;

         // Create a new tree store using the siteId as part of the URL
         this.treeStore = this.createWidget({
            name: "alfresco/navigation/TreeStore",
            config: {
               publishTopic: this.childRequestPublishTopic,
               publishPayload: this.childRequestPublishPayload,
               publishGlobal: this.childRequestPublishGlobal,
               target: this.getTargetUrl(),
               targetQueryObject: this.getTargetQueryObject(),
               filterPaths: this.filterPaths
            }
         });
         
         // Create the object store...
         // It is important that we set a root object. This should reflect correct location and label of the
         // document library location...
         this.treeModel = new ObjectStoreModel({
            root: {
               id: this.id + "_ROOT",
               name: this.message(this.rootLabel),
               value: this.rootValue,
               path: this.rootValue + "/"
            },
            store: this.treeStore,
            query: {}
         });
         
         // Create the tree and add it to our widget...
         this.tree = new AikauTree({
            id: this.id + "_TREE",
            model: this.treeModel,
            showRoot: this.showRoot,
            onClick: lang.hitch(this, this.onClick),
            onOpen: lang.hitch(this, this.onNodeExpand),
            onClose: lang.hitch(this, this.onNodeCollapse),
            treeNodeDisablementConfig: this.treeNodeDisablementConfig
         });
         this.tree.placeAt(this.domNode);
         this.tree.startup();

         // This section of code creates an aspect around the dndController created for the tree
         // to prevent it from selecting nodes when the selected node is disabled...
         // It works by only calling the original function when a non-disabled node has been selected
         if (this.tree.dndController)
         {
            aspect.around(this.tree.dndController, "setSelection", function(originalFunction) {
               return function(newSelection) {
                  if (newSelection && 
                      newSelection.length && 
                      newSelection[0].domNode &&
                      domClass.contains(newSelection[0].domNode, "alfresco-navigation-Tree__node--disabled"))
                  {
                     return null;
                  }
                  else
                  {
                     return originalFunction.apply(this, arguments);
                  }
               };
            });
         }
      },
      
      /**
       * This is the topic that is published when a node on the tree is clicked. The data applied
       * to the filter is the value of the node clicked. By default it is expected that the
       * tree represents a path so is set to the [path changed topic]{@link module:alfresco/core/topics#PATH_CHANGED}.
       * 
       * @instance
       * @type {string}
       * @default [topics.PATH_CHANGED]{@link module:alfresco/core/topics#PATH_CHANGED}
       */
      publishTopic: topics.PATH_CHANGED,

      /**
       * By default the payload type will the current item. This will automatically be set
       * to be the tree node clicked.
       *
       * @instance
       * @type {string}
       * @default
       */
      publishPayloadType: "CURRENT_ITEM",

      /**
       * @instance
       * @param {object} item The store item represented by the clicked node
       * @param {object} node The node on the tree that was clicked
       * @param {object} evt The click event
       */
      onClick: function alfresco_navigation_Tree__onClick(item, node, evt) {
         if (node && node.domNode && domClass.contains(node.domNode, "alfresco-navigation-Tree__node--disabled"))
         {
            // Ignore clicks on nodes where the tree node is disabled...
            this.alfLog("log", "Ignoring click on disabled tree node", item, node, evt);
         }
         else
         {
            this.alfLog("log", "Tree Node clicked", item, node, evt);

            // Assign the clicked item as the currentItem value so that it can be used in payload generation...
            this.currentItem = item;
            if (!this.currentItem.nodeRef)
            {
               this.currentItem.nodeRef = this.rootNode;
            }

            this.publishPayload = lang.clone(this.publishPayload);
            var generatedPayload = this.getGeneratedPayload(true);
            this.alfPublish(this.publishTopic, generatedPayload, this.publishGlobal);
         }
      },
      
      /**
       * @instance
       * @param {object} item The store item represented by the opened node
       * @param {object} node The node on the tree that was opened
       */
      onNodeExpand: function alfresco_navigation_Tree__onNodeExpand(item, node) {
         if (node !== null && node._loadDeferred !== null && node._loadDeferred !== undefined)
         {
            this.alfLog("log", "Wait for node expand before rezize", node._loadDeferred);
            node._loadDeferred.then(lang.hitch(this, this.requestSidebarResize));
         }
      },
      
      /**
       * @instance
       * @param {object} item The store item represented by the collapsed node
       * @param {object} node The node on the tree that was collapsed
       */
      onNodeCollapse: function alfresco_navigation_Tree__onNodeExpand(item, node) {
         if (node !== null && node._collapseDeferred !== null && node._collapseDeferred !== undefined)
         {
            this.alfLog("log", "Wait for node collapse before rezize", node._collapseDeferred);
            node._loadDeferred.then(lang.hitch(this, this.requestSidebarResize));
         }
      },
      
      /**
       * Publishes a request to resize the side bar (if the tree is not in a side bar then this will
       * have no effect).
       * 
       * @instance
       */
      requestSidebarResize: function alfresco_navigation_Tree__requestSidebarResize() {
         this.alfPublish("ALF_RESIZE_SIDEBAR", {});
      }
   });
});