Source: lists/views/layouts/_MultiItemRendererMixin.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 mixin provides the capability to a widget to iterate over the data that is returned by making
 * a request to the Alfresco Repository for a list of nodes to be displayed in a Document Library. This
 * should be mixed into any widget that will process a JSON model of widgets because it extends the
 * [createWidget]{@link module:alfresco/core/Core#createWidget} function to pass the details of the item
 * that is currently being rendered to all of its processed widgets. It also overrides the default 
 * [allWidgetsProcessed]{@link module:alfresco/core/Core#allWidgetsProcessed} function to make a call
 * to render the next item in the [currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
 * attribute (if applicable).
 * 
 * @module alfresco/lists/views/layouts/_MultiItemRendererMixin
 * @extends module:aikau/core/ChildProcessing
 * @mixes module:alfresco/documentlibrary/_AlfDocumentListTopicMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "aikau/core/ChildProcessing",
        "alfresco/core/ObjectTypeUtils",
        "alfresco/core/JsNode",
        "alfresco/documentlibrary/_AlfDocumentListTopicMixin",
        "alfresco/lists/views/RenderAppendixSentinel",
        "dojo/dom-class",
        "dojo/_base/array",
        "dojo/_base/lang",
        "dojo/dom-style",
        "dojo/on",
        "dojo/_base/event"], 
        function(declare, ChildProcessing, ObjectTypeUtils, JsNode, _AlfDocumentListTopicMixin, 
                 RenderAppendixSentinel, domClass, array, lang, domStyle, on, event) {
   
   return declare([ChildProcessing, _AlfDocumentListTopicMixin], {

      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/_MultiItemRendererMixin.css"}]
       */
      cssRequirements: [{cssFile:"./css/_MultiItemRendererMixin.css"}],
      
      /**
       * This should be set to the data to iterate over. This should be an object that contains
       * an "items" attribute.
       * 
       * @instance
       * @type {Object[]}
       * @default
       */
      currentData: null,
      
      /**
       * The index of the item in [currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
       * items array that is currently being rendered
       * 
       * @instance
       * @type {number}
       * @default
       */
      currentIndex: null,
      
      /**
       * The current item being rendered
       * 
       * @instance
       * @type {Object} 
       * @default
       */
      currentItem: null,
      
      /**
       * Indicates whether or not focused items should have a highlight style applied to them.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      focusHighlighting: false,

      /**
       * This is the property that should be used to compare unique keys when comparing items. This will default
       * to "nodeRef" if not set.
       * 
       * @instance
       * @type {string}
       * @default
       */
      itemKey: "nodeRef",

      /**
       * This is the widget that acts as the root of the view. By default this will
       * be instantiated as a [Table]{@link module:alfresco/documentlibrary/views/layoutTable}.
       * 
       * @instance
       * @type {Object} 
       * @default
       */
      rootViewWidget: null,
      
      /**
       * @instance
       * @type {object[]}
       * @default
       */
      rootWidgetSubscriptions: null,
      
      /**
       * Records all the widgets that are processed for each item. This differs from the 
       * [_processedWidgets]{@link module:alfresco/core/CoreWidgetProcessing#_processedWidgets}
       * attribute because that captures the widgets processed for the last item (i.e. the 
       * data is replaced on each item iteration)
       *
       * @instance
       * @type {array}
       * @default
       */
      _renderedItemWidgets: null,

      /**
       * A setter for [currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
       * @instance
       * @param {Object} data The data to set
       */
      setData: function alfresco_lists_views_layout___MultiItemRendererMixin__setData(data) {
         this.currentData = data;
      },

      /**
       * An advanced setter for[currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
       * It intelligently merges the new data to the old data
       *
       * @param {object} newData data to add to the existing data
       */
      augmentData: function alfresco_lists_views_layout___MultiItemRendererMixin__augmentData(newData) {
         if (!this.currentData)
         {
            // We don't need to worry about combining data if there isn't any already.
            this.alfLog("debug", "augmentData called but this.currentData empty, so using setData instead.");
            this.setData(newData);
         }
         else
         {
            // It is also necessary to keep the overall number found count up-to-date (with eventual consistency)
            // on searching, this could actually change after the initial data is set.
            // TODO: This will also need updating for regular document lists...
            this.currentData.numberFound = newData.numberFound;

            // Merge Items arrays.
            if (lang.isArray(this.currentData.items) && lang.isArray(newData.items))
            {
               // Store the old length so we know where to start rendering from.
               this.currentData.previousItemCount = this.currentData.items.length;
               this.currentData.items = this.currentData.items.concat(newData.items);
            }
         }
      },

      /**
       * Reset the current Data object.
       * 
       * @instance
       */
      clearData: function alfresco_lists_views_layout___MultiItemRendererMixin__clearData() {
         this.alfLog("info", "Clearing currentData.");
         this.currentData = null;
      },

      /**
       * Return the current data object.
       *
       * @instance
       * @returns {Object[]}
       */
      getData: function alfresco_lists_views_layout___MultiItemRendererMixin__getData() {
         return this.currentData;
      },

      /**
       * This function should be called to iterate over new data.
       * It sets the currentData object and resets the index back to zero. When [processWidgets]{@link module:alfresco/core/Core#processWidgets}
       * function is called it will being set [currentItem]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentItem} 
       * as the first element in the items attribute belonging to [currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
       * 
       * @instance
       */
      renderData: function alfresco_lists_views_layout___MultiItemRendererMixin__renderData() {
         /*jshint eqnull:true*/
         
         this._renderedItemWidgets = [];

         // Ensure that an array is created to hold the root widget subscriptions...
         if (!this.rootWidgetSubscriptions)
         {
            this.rootWidgetSubscriptions = [];
         }
         // Iterate over any previously created subscriptions for last data set and remove them...
         // It is important that this it carried out to clean up the previous data set. By default
         // the only subscriptions that will be present are those for selecting items, but extending
         // classes could have added additional subscriptions. If the subscriptions aren't cleaned 
         // up properly then destroyed widgets will try to be actioned.
         array.forEach(this.rootWidgetSubscriptions, function(handle) {
            if (typeof handle.remove === "function")
            {
               handle.remove();
            }
         });
         
         if (this.currentData && this.currentData.items)
         {
            this.alfLog("log", "Rendering data", this.currentData.items);
            // Set current Index to previousItemCount (so rendering starts at new items)
            this.currentIndex = this.currentData.previousItemCount || 0;
            this.currentItem = this.currentData.items[this.currentIndex];
            
            // Add in the index...
            if (this.currentItem && this.currentItem.index == null)
            {
               this.currentItem.index = this.currentIndex;
            }

            var itemsToRender = (this.currentIndex)? this.currentData.items.slice(this.currentIndex): this.currentData.items;
            array.forEach(itemsToRender, lang.hitch(this, this.renderNextItem));
            this.allItemsRendered();
         }
         else
         {
            this.alfLog("warn", "No data to render!");
         }
      },
      
      /**
       * Calls [processWidgets]{@link module:alfresco/core/Core#processWidgets} to instantiate the widgets
       * defined in the JSON model for [currentItem]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentItem}
       * @instance
       */
      renderNextItem: function alfresco_lists_views_layout___MultiItemRendererMixin__renderNextItem() {
         var itemToRender = this.currentData.items[this.currentIndex];
         if (itemToRender === RenderAppendixSentinel && this.widgetsForAppendix)
         {
            // The current item is a marker to render an "appendix". This is a non-data entry into the list
            // of items to be rendered, the original use case is for some kind of "Add" style control that
            // can be used to create a new entry...
            this.processWidgets(this.widgetsForAppendix, this.containerNode, "RENDER_APPENDIX_SENTINEL");
         }
         else
         {
            // Process the widgets defined using the current item as the data to go into those widgets...
            this.alfLog("log", "Rendering item", itemToRender);
            
            // Mark the current item with an attribute indicating that it is the last item.
            // This is done for the benefit of renderers that need to know if they are the last item.
            this.currentData.items[this.currentIndex].isLastItem = (this.currentItem.index === this.currentData.items.length -1);

            // Set a width if provided...
            if (this.width)
            {
               domStyle.set(this.domNode, "width", this.width);
            }
            
            if (this.containerNode)
            {
               // It is necessary to clone the widget definition to prevent them being modified for future iterations...
               // var clonedWidgets = lang.clone(this.widgets);
               // Intentionally switched from lang.clone to native JSON approach to cloning for performance...
               var clonedWidgets = JSON.parse(JSON.stringify(this.widgets));
               this.processWidgets(clonedWidgets, this.containerNode);
            }
            else
            {
               this.alfLog("warn", "There is no 'containerNode' for adding an item to");
            }
         }
      },
      
      /**
       * Overrides the default implementation to start the rendering of the next item.
       * 
       * @instance
       * @param {Object[]}
       * @param {string} processWidgetsId An optional ID that might have been provided to map the results of multiple calls to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
       */
      allWidgetsProcessed: function alfresco_lists_views_layout___MultiItemRendererMixin__allWidgetsProcessed(widgets, processWidgetsId) {
         /*jshint eqnull:true*/
         if (!processWidgetsId || processWidgetsId === "RENDER_APPENDIX_SENTINEL")
         {
            // Push the processed widgets for the last item into the array of rendered widgets...
            if (!this._renderedItemWidgets)
            {
               this._renderedItemWidgets = [];
            }

            if (!processWidgetsId)
            {
               this._renderedItemWidgets.push(widgets);
            }
            
            // Increment the current index and check to see if there are more items to render...
            // Only the root widget(s) will have the currentData object set so we don't start rendering the next item
            // on nested widgets...
            this.currentIndex++;
            if (this.currentData && 
                this.currentData.items &&
                this.currentData.items.length != null)
            {
               array.forEach(widgets, lang.hitch(this, this.rootWidgetProcessing));
               if (this.currentIndex < this.currentData.items.length)
               {
                  // Render the next item...
                  this.currentItem = this.currentData.items[this.currentIndex];

                  // Add in the index...
                  if (this.currentItem.index == null)
                  {
                     this.currentItem.index = this.currentIndex;
                  }
               }
            }
            else
            {
               // TODO: We need to make sure that we're able to stop rendering if another request arrives before we've completed
            }
         }
         else
         {
            this.inherited(arguments);
         }
      },
      
      /**
       * @instance
       */
      allItemsRendered: function alfresco_lists_views_layout___MultiItemRendererMixin__allItemsRendered() {
         // No action by default.
      },
      
      /**
       * Adds the "alfresco-lists-views-layout-_MultiItemRendererMixin__item" class to the root DOM node
       * of the widget and additionally subscribes to item selection publications so that additional CSS classes
       * can be added when an item is selected (so that they can be visually indicate selection).
       * 
       * @instance
       * @param {object} widget The widget to add the styling to
       * @param {number} index The index of the widget
       */
      rootWidgetProcessing: function alfresco_lists_views_layout___MultiItemRendererMixin__rootWidgetProcessing(widget, /*jshint unused:false*/ index) {
         domClass.add(widget.domNode, "alfresco-lists-views-layout-_MultiItemRendererMixin__item");
         if (widget.focusHighlighting === true)
         {
            domClass.add(widget.domNode, "alfresco-lists-views-layout-_MultiItemRendererMixin__item--focusHighlighting");
         }
         if (!this.rootWidgetSubscriptions)
         {
            this.rootWidgetSubscriptions = [];
         }

         if (this.supportsItemSelection === true)
         {
            this.rootWidgetSubscriptions.push(this.alfSubscribe(this.documentSelectedTopic, lang.hitch(this, "onItemSelection", widget)));
            this.rootWidgetSubscriptions.push(this.alfSubscribe(this.documentDeselectedTopic, lang.hitch(this, "onItemDeselection", widget)));
         }
      },
      
      /**
       * Adds the "selected" CSS class to the root widget if it has been selected.
       * 
       * @instance
       * @param {object} payload The details of the selected item
       */
      onItemSelection: function alfresco_lists_views_layout___MultiItemRendererMixin__onItemSelection(widget, payload) {
         if (this.compareItems(widget.currentItem, payload.value))
         {
            domClass.add(widget.domNode, "selected");
         }
      },

      /**
       * Removes the "selected" CSS class to the root widget if it has been de-selected.
       * 
       * @instance
       * @param {object} payload The details of the selected item
       */
      onItemDeselection: function alfresco_lists_views_layout___MultiItemRendererMixin__onItemDeselection(widget, payload) {
         if (this.compareItems(widget.currentItem, payload.value))
         {
            domClass.remove(widget.domNode, "selected");
         }
      },

      /**
       * Compares the nodeRef attribute of both item arguments. This has been abstracted to a separate function to
       * allow simpler overriding when comparing items. This function is called by the
       * [onItemSelection]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#onItemSelection}
       * function to determine whether the item currently selected is the item represented by the current widget. 
       * 
       * @instance
       * @param {object} item1 The first item to compare
       * @param {object} item2 The second item to compare
       * @returns {boolean} true if the items are the same and false otherwise.
       */
      compareItems: function alfresco_lists_views_layout___MultiItemRendererMixin__compareItems(item1, item2) {
         var key1 = lang.getObject(this.itemKey, null, item1);
         var key2 = lang.getObject(this.itemKey, null, item2);
         return (key1 && (key1 === key2));
      },
      
      /**
       * Overrides the default implementation of create widget to update the widget configuration with the
       * current item being rendered. This ensures that each widget has access to all the data about that
       * item.
       * 
       * @instance 
       * @param {Object} config The configuration to pass as an instantiation argument to the widget
       * @param {element} domNode The DOM node to attach the widget to
       * @param {function} callback A function to call once the widget has been instantiated
       * @param {Array} callbackArgs An array of arguments to pass to the callback function
       */
      createWidget: function alfresco_lists_views_layout___MultiItemRendererMixin__createWidget(config, domNode, callback, callbackScope, index, processWidgetsId) {
         if (!processWidgetsId)
         {
            // Only create a widget if there is data to create it with
            if (!config)
            {
               config = {
                  config: {}
               };
            }
            else if (!config.config)
            {
               config.config = {};
            }
            if (this.currentItem)
            {
               // This checks if the "jsNode" attribute has been created, and if not will make an attempt
               // to create it. This is in place purely for handling node based items, but shouldn't
               // break anything else...
               if (typeof this.currentItem.jsNode === "undefined" && this.currentItem.node)
               {
                  this.currentItem.jsNode = new JsNode(this.currentItem.node);
               }
               config.config.currentItem = this.currentItem;

               // Pass on any metadata...
               if (this.currentData && this.currentData.metadata)
               {
                  config.config.currentMetadata = this.currentData.metadata;
               }
               return this.inherited(arguments);
            }
         }
         else
         {
            return this.inherited(arguments);
         }
      },

      /**
       * This function has been added to that mixing modules can ensure that they request focus from
       * their respective container. This ensures that focus is given to the correct item and is not
       * just given to the first child in the container when focus returns to it.
       * 
       * @instance
       * @param {object} evt The click event that gave focus.
       */
      onFocusClick: function alfresco_lists_views_layout___MultiItemRendererMixin__onFocusClick(evt) {
         if (!evt.preventFocusTheft)
         {
            on.emit(this.domNode, "onItemFocused", {
               bubbles: true,
               cancelable: true,
               item: this
            });
         }
         event.stop(evt);
      }
   });
});