Source: lists/views/AlfListView.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/>.
 */

/**
 * An abstract view for the Alfresco Share document list. It can be used in JSON page models if
 * configured with a widgets definition. Otherwise it can be extended to define specific views
 *
 * @module alfresco/lists/views/AlfListView
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes external:dojo/_KeyNavContainer
 * @mixes module:alfresco/lists/views/layouts/_MultiItemRendererMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase",
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/AlfListView.html",
        "alfresco/lists/views/layouts/_MultiItemRendererMixin",
        "alfresco/documentlibrary/_AlfDndDocumentUploadMixin",
        "aikau/lists/views/ListRenderer",
        "alfresco/lists/views/RenderAppendixSentinel",
        "alfresco/core/Core",
        "alfresco/core/JsNode",
        "alfresco/core/WidgetsCreator",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/dom-style",
        "dojo/query"],
        function(declare, _WidgetBase, _TemplatedMixin, template, _MultiItemRendererMixin, _AlfDndDocumentUploadMixin, ListRenderer,
                 RenderAppendixSentinel, AlfCore, JsNode, WidgetsCreator, lang, array, domConstruct, domClass, domStyle, query) {

   return declare([_WidgetBase, _TemplatedMixin, _MultiItemRendererMixin, AlfCore, _AlfDndDocumentUploadMixin], {

      /**
       * An array of the i18n files to use with this widget.
       *
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/AlfListView.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/AlfListView.properties"}],

      /**
       * An array of the CSS files to use with this widget.
       *
       * @instance cssRequirements {Array}
       * @type {object[]}
       * @default [{cssFile:"./css/AlfListView.css"}]
       */
      cssRequirements: [{cssFile:"./css/AlfListView.css"}],

      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {String}
       */
      templateString: template,

      /**
       * This is the topic that will be subscribed to when [subscribeToDocRequests]
       * {@link module:alfresco/lists/views/AlfListView#subscribeToDocRequests} is
       * configured to be true.
       *
       * @instance
       * @type {string}
       * @default
       */
      documentSubscriptionTopic: "ALF_RETRIEVE_DOCUMENTS_REQUEST_SUCCESS",

      /**
       * This is the property of each item in the list that uniquely identifies that item. This
       * should be configured correctly in order for items to be
       * [brought into view]{@link module alfresco/lists/views/ListRenderer#bringItemIntoView} as required.
       *
       * @instance
       * @type {string}
       * @default
       */
      itemKey: "nodeRef",

      /**
       * This is the property that is used to lookup documents in the subscribed topic.
       *
       * @instance
       * @type {string}
       * @default
       */
      itemsProperty: "response.items",

      /**
       * Minimum height for this view. Not used by default. Can be either a Number (treated as pixels) or
       * a String (treated as CSS units).
       *
       * @instance
       * @type {Number|String}
       * @default
       * @since 1.0.67
       */
      minHeight: null,

      /**
       * This can be set to be a custom message that is displayed when there are no items to
       * be displayed in the current view. This will not be used if
       * [widgetsForNoDataDisplay]{@link module alfresco/lists/views/ListRenderer#widgetsForNoDataDisplay}
       * is configured.
       *
       * @instance
       * @type {string}
       * @default
       */
      noItemsMessage: null,

      /**
       * This is the default CSS selector query to use to check whether any data has actually been rendered.
       * It's used from within the [renderView]{@link module:alfresco/lists/views/AlfListView#renderView}
       * function to check that data is actually rendered (because even though renderable items might exist it's possible
       * for them to be filtered out so that they're not displayed). This needs to be overriden by views that don't
       * render a DOM that matches the query.
       *
       * @instance
       * @type {string}
       * @default
       */
      renderFilterSelectorQuery: "tr",

      /**
       * Should the widget subscribe to events triggered by the documents request?
       * This should be set to true in the widget config for standalone/isolated usage.
       *
       * @instance
       * @type Boolean
       * @default
       */
      subscribeToDocRequests: false,

      /**
       * Overrides the
       * [inherited default configuration]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#suppressDndUploading}
       * to suppress the drag-and-drop upload highlighting.
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.39
       */
      suppressDndUploading: true,

      /**
       * This will be automatically set when the view is used in an [AlfHashList]{@link module:alfresco/lists/AlfHashList}.
       * It indicates whether or not the list is being driven by data set on the browser URL hash and it can be useful
       * for views to have access to this information.
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.56
       */
      useHash: null,

      /**
       * Whether the list that's creating this view has infinite scroll turned on
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.32
       */
      useInfiniteScroll: false,

      /**
       * The configuration for view selection menu items. This needs to be either configured or defined in an
       * extending module. If this isn't specified then the view will not be selectable in the document list.
       *
       * @instance
       * @type {Object}
       * @default {}
       */
      viewSelectionConfig: {
         label: "Abstract",
         value: "Abstract"
      },

      /**
       * The widgets to be processed to generate each item in the rendered view.
       *
       * @instance
       * @type {object[]}
       * @default
       */
      widgets: null,

      /**
       * An optional widget model to be rendered as an appendix to the actual data. If this is
       * defined then it will never be possible for the
       * [widgetsForNoDataDisplay]{@link module:alfresco/lists/views/AlfListView#widgetsForNoDataDisplay}
       * model to be rendered because it results in a special
       * [marker]{@link module:alfresco/lists/views/RenderAppendixSentinel}
       * being added to the data set to be rendered.
       *
       * @instance
       * @type {object[]}
       * @default
       * @since 1.0.44
       */
      widgetsForAppendix: null,

      /**
       * An optional JSON model defining the widgets to display when no data is available to display.
       *
       * @instance
       * @type {array}
       * @default
       */
      widgetsForNoDataDisplay: null,

      /**
       * This can be called to focus on a specific item in the view. The itemKey provided must match
       * the value of the [itemKey property]{@link module:alfresco/lists/views/AlfListView#itemKey}
       * of an item in the rendered data.
       *
       * @instance
       * @param {string} itemKey The key of the item to focus on.
       * @since 1.0.77
       */
      focusOnItem: function alfresco_lists_views_AlfListView__focusOnItem(itemKey) {
         if (this.docListRenderer && this.docListRenderer._renderedItemWidgets)
         {
            array.some(this.docListRenderer._renderedItemWidgets, function(widgets) {
               return array.some(widgets, function(widget) {
                  var found = false;
                  if (widget &&
                      widget.currentItem &&
                      (widget.currentItem[this.itemKey] || widget.currentItem[this.itemKey] === 0) &&
                      widget.currentItem[this.itemKey].toString() === itemKey)
                  {
                     if (widget.domNode)
                     {
                        widget.domNode.click();
                     }
                     found = true;
                  }
                  return found;
               }, this);
            }, this);
         }
      },

      /**
       * Implements the widget life-cycle method to add drag-and-drop upload capabilities to the root DOM node.
       * This allows files to be dragged and dropped from the operating system directly into the browser
       * and uploaded to the location represented by the document list.
       *
       * @instance
       */
      postCreate: function alfresco_lists_views_AlfListView__postCreate() {
         this.inherited(arguments);

         // Set up a subscription to handle requests for the view name.
         // This supports other widgets displaying the name (rather than value) of views and
         // also acts as a "role call" of views to check that an expected view is available
         this.alfSubscribe("ALF_VIEW_NAME_REQUEST", lang.hitch(this, this.onViewNameRequest));

         // Add in any additional CSS classes...
         domClass.add(this.domNode, (this.additionalCssClasses ? this.additionalCssClasses : ""));

         // Allow custom messages to be displayed when no items are available for display...
         if (!this.noItemsMessage)
         {
            this.noItemsMessage = this.message("doclistview.no.data.message");
         }
         else
         {
            this.noItemsMessage = this.message(this.noItemsMessage);
         }

         // Call DND upload mixin functions to add support for uploading behaviour...
         this.subscribeToCurrentNodeChanges(this.domNode);
         this.addUploadDragAndDrop(this.domNode);

         if (this.subscribeToDocRequests)
         {
            this.alfSubscribe(this.documentSubscriptionTopic, lang.hitch(this, this.onDocumentsLoaded));
         }
         if (this.currentData)
         {
            // Render the initial data - make sure any previous data is cleared (not that there should be any!)
            this.renderView(false);
         }
         this._renderOptionalElements();

         // Apply any min-height value
         if (typeof this.minHeight === "number") {
            this.minHeight += "px";
         }
         this.minHeight && domStyle.set(this.domNode, "min-height", this.minHeight);
      },

      /**
       * If the supplied payload contains a view value that matches this view then this will resolve
       * the promise that should be included.
       *
       * @instance
       * @param {object} payload A payload containing a view value and a promise to resolve.
       */
      onViewNameRequest: function alfresco_lists_views_AlfListView__onViewNameRequest(payload) {
         if (payload &&
             payload.value === this.getViewName() &&
             payload.promise &&
             typeof payload.promise.resolve === "function")
         {
            payload.promise.resolve({
               value: this.getViewName(),
               label: this.viewSelectionConfig.label
            });
         }
      },

      /**
       * @instance
       * @param {object} payload The details of the documents that have been provided.
       */
      onDocumentsLoaded: function alfresco_lists_views_AlfListView__onDocumentsLoaded(payload) {
         var items = lang.getObject(this.itemsProperty, false, payload);
         if (items)
         {
            array.forEach(items, lang.hitch(this, this.processItem));
            this.setData({
               items: items
            });
            this.renderView(false);
         }
         else
         {
            this.alfLog("warn", "Payload contained no 'response.items' attribute", payload, this);
         }
      },

      /**
       * Attempts to process an item provided to the
       * [onDocumentsLoaded]{@link module:alfresco/lists/views/AlfListView#onDocumentsLoaded}
       * function. By default this attempts to process node data as the default behaviour is to assume this
       * is an Alfresco node.
       *
       * @instance
       * @param {object} item The item to process
       * @param {number} index The index of the item
       */
      processItem: function alfresco_lists_views_AlfListView__processItem(item, /*jshint unused:false*/ index) {
         try
         {
            if (item.node)
            {
               item.jsNode = new JsNode(item.node);
            }
         }
         catch (e)
         {
            this.alfLog("warn", "Could not process item as Alfresco node", item, e);
         }
      },

      /**
       * This should be overridden to give each view a name. If it's not overridden then the view will just get given
       * a name of the index that it was registered with. It will still be possible to select the view but it will cause
       * issues with preferences.
       *
       * @instance
       * @returns {string} "Abstract"
       */
      getViewName: function alfresco_lists_views_AlfListView__getViewName() {
         return this.viewSelectionConfig.value;
      },

      /**
       * This should be overridden to provide configuration for view selection. As a minimum, a localised label MUST be provided
       * as the "label" attribute. Other attributes that could be provided would be "iconClass". This configuration will typically
       * be used to construct a menu item. By default this just returns the
       * [viewSelectionConfig]{@link module:alfresco/lists/views/AlfListView#viewSelectionConfig}
       *
       * @instance
       * @returns {Object} The configuration for selecting the view.
       */
      getViewSelectionConfig: function alfresco_lists_views_AlfListView__getViewSelectionConfig() {
         return this.viewSelectionConfig;
      },


      /**
       * This function should be overridden to publish the details of any additional controls that are needed to control
       * view of the data that it provides. An example of a control would be the thumbnail size slider for the gallery
       * control.
       *
       * @instance getAdditionalControls
       * @returns {Object[]}
       */
      getAdditionalControls: function alfresco_lists_views_AlfListView__getAdditionalControls() {
         // For the abstract view there are no additional controls.
         return [];
      },

      /**
       * Extends the inherited function to also update the docListRenderer if it exists with the data.
       *
       * @instance
       * @param {object} newData The additional data to add.
       */
      augmentData: function alfresco_lists_views_AlfListView__augmentData(/*jshint unused:false*/ newData) {
         this.inherited(arguments);
         if (this.docListRenderer)
         {
            this.docListRenderer.currentData = this.currentData;
         }
      },

      /**
       * Calls the [renderData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#renderData}
       * function if the [currentData]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentData}
       * attribute has been set to an object with an "items" attribute that is an array of objects.
       *
       * @instance
       * @param {boolean} preserveCurrentData This should be set to true when you don't want to clear the old data, the
       * most common example of this is when infinite scroll is being used.
       */
      renderView: function alfresco_lists_views_AlfListView__renderView(preserveCurrentData) {
         var promisedView = new Promise(lang.hitch(this, function(resolve, /*jshint unused:false*/ reject) {
            // jshint maxcomplexity:false
            if (this.currentData && this.currentData.items)
            {
               if (this.widgetsForAppendix && this.currentData.items)
               {
                  var containsSentinel = this.currentData.items.some(function(item) {
                     return item === RenderAppendixSentinel;
                  });
                  !containsSentinel && this.currentData.items.push(RenderAppendixSentinel);
               }

               if (this.currentData.items.length > 0)
               {
                  try
                  {
                     if (this.messageNode)
                     {
                        domConstruct.destroy(this.messageNode);
                     }

                     // If we don't want to preserve the current data (e.g. if infinite scroll isn't being used)
                     // then we should destroy the previous renderer...
                     if ((preserveCurrentData === false || preserveCurrentData === undefined) && this.docListRenderer)
                     {
                        this.destroyRenderer();
                     }

                     // If the renderer is null we need to create one (this typically wouldn't be expected to happen)
                     // when rendering additional infinite scroll data...
                     if (!this.docListRenderer)
                     {
                        this.docListRenderer = this.createListRenderer();
                        this.docListRenderer.placeAt(this.tableNode, "last");
                     }

                     // Ensure that the renderer has has the same itemKey value as configured on the view. This is
                     // so that comparisons can be made for selection and items can be brought into view as necessary
                     this.docListRenderer.itemKey = this.itemKey;

                     // Finally, render the current data (when using infinite scroll the data should have been augmented)
                     var promisedData = this.docListRenderer.renderData();
                     if (promisedData)
                     {
                        promisedData.then(
                           lang.hitch(this, function(renderedItems) {
                              if (renderedItems.length)
                              {
                                 resolve(renderedItems);
                              }
                              else
                              {
                                 this.renderNoDataDisplay();
                                 resolve();
                              }
                           }),
                           lang.hitch(this, function(reason) {
                              this.alfLog("error", "The following error occurred rendering the data:", reason, this);
                                 this.renderErrorDisplay();
                                 resolve();
                              })
                        );
                     }
                     else if (query(this.renderFilterSelectorQuery, this.tableNode).length === 0)
                     {
                        this.renderNoDataDisplay();
                        resolve();
                     }
                     else
                     {
                        // TODO: Better error handling when the renderer doesn't return a promise
                        this.alfLog("warn", "The view renderer does not return a promise when rendering data", this);
                        resolve();
                     }
                  }
                  catch(e)
                  {
                     // TODO: This should return a promise itself...
                     this.alfLog("error", "The following error occurred rendering the data", e, this);
                     this.renderErrorDisplay();
                     resolve();
                  }
               }
               else
               {
                  // TODO: This should return a promise itself...
                  this.renderNoDataDisplay();
                  resolve();
               }
            }
            else
            {
               // TODO: This should return a promise itself...
               this.renderNoDataDisplay();
               resolve();
            }
         }));
         return promisedView;
      },

      /**
       * Creates a new [ListRenderer]{@link module:alfresco/lists/views/ListRenderer}
       * which is used to render the actual items in the view. This function can be overridden by extending views
       * (such as the [Film Strip View]{@link module:alfresco/documentlibrary/views/AlfFilmStripView}) to create
       * alternative widgets applicable to that view.
       *
       * @instance
       * @returns {object} A new [ListRenderer]{@link module:alfresco/lists/views/ListRenderer}
       */
      createListRenderer: function alfresco_lists_views_AlfListView__createListRenderer() {
         var dlr = new ListRenderer({
            id: this.id + "_ITEMS",
            widgets: this.widgets,
            currentData: this.currentData,
            pubSubScope: this.pubSubScope,
            parentPubSubScope: this.parentPubSubScope,
            widgetsForAppendix: this.widgetsForAppendix
         });
         return dlr;
      },

      /**
       * Removes the previously rendered view.
       *
       * @instance
       */
      clearOldView: function alfresco_lists_views_AlfListView__clearOldView() {
         this.destroyRenderer();
         if (this.messageNode)
         {
            domConstruct.destroy(this.messageNode);
         }
         // Remove all table body elements. (preserves headers)
         query("tbody", this.tableNode).forEach(domConstruct.destroy);
         this.clearData();
      },

      /**
       * This should be called when the renderers need to be removed.
       *
       * @instance
       * @extendable
       * @since 1.0.32
       */
      destroyRenderer: function alfresco_lists_views_AlfListView__destroyRenderer() {
         if (this.docListRenderer)
         {
            this.docListRenderer.destroy();
            // TODO: Concerned about this - it needs further investigation as to why anything is being left behind!
            this.docListRenderer = null;
         }
      },

      /**
       * Called from [renderView]{@link module:alfresco/lists/views/AlfListView#renderView} for
       * every widget created for the last view. It is important that widgets are properly destroyed to ensure that
       * they do not respond to topics that they have subscribed to (e.g. selection events such as selecting all
       * documents).
       *
       * @instance
       * @param {object} widget The widget to destroy
       * @param {number} index The index of the widget
       */
      destroyWidget: function alfresco_lists_views_AlfListView__destroyWidget(widget) {
         if (typeof widget.destroyRecursive === "function")
         {
            widget.destroyRecursive();
         }
      },

      /**
       * Called after the view has been shown (note that [renderView]{@link module:alfresco/lists/views/AlfListView#renderView}
       * does not mean that the view has been displayed, just that it has been rendered.
       * @instance
       */
      onViewShown: function alfresco_lists_views_AlfListView__onViewShown() {
         this.alfPublish("ALF_WIDGET_PROCESSING_COMPLETE", {}, true);
      },

      /**
       * This method is called when there is no data to be shown. By default this just shows a standard localized
       * message to say that there is no data.
       *
       * @instance
       */
      renderNoDataDisplay: function alfresco_lists_views_AlfListView__renderNoDataDisplay() {
         this.clearOldView();

         // Only generate message node if there's a label or widgets to go in it.
         if (this.noItemsMessage || this.widgetsForNoDataDisplay)
         {
            this.messageNode = domConstruct.create("div", {
               className: "alfresco-lists-views-AlfListView__no-data",
               innerHTML: this.noItemsMessage
            }, this.domNode);
         }

         // If specific widgets have been defined to display when there are no results then replace
         // the default message with them...
         if (this.widgetsForNoDataDisplay)
         {
            domConstruct.empty(this.messageNode);
            this.processWidgets(this.widgetsForNoDataDisplay, this.messageNode, "WIDGETS_FOR_NO_DATA_DISPLAY");
         }
      },

      /**
       * This method is called when there is an error occurred rendering the view
       *
       * @instance
       */
      renderErrorDisplay: function alfresco_lists_views_AlfListView__renderErrorDisplay() {
         this.clearOldView();
         this.messageNode = domConstruct.create("div", {
            className: "alfresco-lists-views-AlfListView__render-error",
            innerHTML: this.message("doclistview.rendering.error.message")
         }, this.domNode);
      },

      /**
       * Runs _renderHeader() and _renderCaption() in the correct order to construct the elements appropriately
       *
       * @instance
       */
      _renderOptionalElements: function alfresco_lists_views_AlfListView___renderOptionalElements() {
         this._renderHeader();
         this._renderCaption();
      },

      /**
       * Optionally builds the header contents from a nested set of widgets in attribute widgetsForHeader
       *
       * @instance
       */
      _renderHeader: function alfresco_lists_views_AlfListView___renderHeader() {
         this.currentItem = {};
         if (this.widgetsForHeader)
         {
            var thead = domConstruct.create("thead", null, this.tableNode, "first");
            var clonedWidgets = lang.clone(this.widgetsForHeader);
            this.processObject(["processInstanceTokens"], clonedWidgets);
            this.processWidgets(clonedWidgets, thead);
         }
         this.currentItem = null;
      },

      /**
       * Optionally add a caption to the generated table
       *
       * @instance
       */
      _renderCaption: function alfresco_lists_views_AlfListView___renderCaption() {
         if(this.a11yCaption && this.tableNode)
         {
            // Create a caption node
            var caption = domConstruct.create("caption", {
               innerHTML: this.encodeHTML(this.a11yCaption)
            }, this.tableNode, "first");

            // Apply a class to the caption
            if(this.a11yCaptionClass)
            {
               domClass.add(caption, this.a11yCaptionClass);
            }
         }
      }
   });
});