Source: renderers/Indicators.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 creates a set of icons that indicate the status of the item. Indicators are defined in the Alfresco Share
 * XML configuration to be rendered in the Document Library. When using this renderer in the context of Alfresco Share
 * then the [legacyMode]{@link module:alfresco/renderers/Indicators#legacyMode} should be configured to be true in 
 * order to support 3rd party extensions to the Share configuration. By default the widget will attempt to map the
 * indicator "icon" attribute to an image path either in Share (with [legacyMode]{@link module:alfresco/renderers/Indicators#legacyMode})
 * enabled) or within the Aikau - however, it is also possible to configure an
 * [iconMapping]{@link module:alfresco/renderers/Indicators#iconMapping} to map indicator icons to custom image files.
 *
 * @example <caption>Standalone Aikau Client configuration (images expected to be found in "alfresco/renderers/css/images/indicators/"):</caption>
 * {
 *   name: "alfresco/renderers/Indicators",
 * }
 *
 * @example <caption>Alfresco Share configuration (images expected to be found in"components/documentlibrary/indicators/"):</caption>
 * {
 *   name: "alfresco/renderers/Indicators",
 *   config: {
 *     legacyMode: true
 *   }
 * }
 *
 * @example <caption>Custom mapping for EXIF indicator image:</caption>
 * {
 *   name: "alfresco/renderers/Indicators",
 *   config: {
 *     iconMapping: {
 *       "exif-16.png": "custom/icon/folder/my-exif-icon.png"
 *     }
 *   }
 * }
 *
 * @module alfresco/renderers/Indicators
 * @extends module:aikau/core/BaseWidget
 * @mixes module:alfresco/renderers/_PublishPayloadMixin
 * @author Dave Draper
 * @author Martin Doyle
 */
define(["dojo/_base/declare", 
        "aikau/core/BaseWidget",
        "alfresco/renderers/_PublishPayloadMixin", 
        "service/constants/Default",
        "dojo/_base/array", 
        "dojo/_base/lang", 
        "dojo/dom-construct", 
        "dojo/on"], 
        function(declare, BaseWidget, _PublishPayloadMixin, AlfConstants, array, lang, domConstruct, on) {

   return declare([BaseWidget, _PublishPayloadMixin], {

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

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

      /**
       * An object that can map each icon attribute to a custom source file for the indicator image. All mappings
       * will be appended to the "resource URL" (e.g. in Alfresc Share this prefix would be "share/res/") which give
       * maximum opportunity for finding images anywhere within the application.
       * 
       * @instance
       * @type {object}
       * @default
       * @since 1.0.55
       */
      iconMapping: null,

      /**
       * This is a white list of the icons available in Aikau. If any indicator is configured without
       * [iconMapping]{@link module:alfresco/renderers/Indicators#iconMapping} and is rendering a
       * [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem} that does not map
       * one of these values then [legacyMode]{@link module:alfresco/renderers/Indicators#legacyMode}
       * will automatically be enabled.
       * 
       * @instance
       * @type {string[]}
       * @since 1.0.71
       * @default
       */
      iconWhiteList: [
         "active-workflows-16.png",
         "cloud-indirect-sync-16.png",
         "cloud-indirect-sync-failed-16.png",
         "cloud-synced-16.png",
         "cloud-sync-failed-16.png",
         "editing-16.png",
         "exif-16.png",
         "geographic-16.png",
         "locked-16.png",
         "lock-owner-16.png",
         "rules-16.png",
         "simple-workflow-16.png",
         "transferred-node-16.png"
      ],

      /**
       * This indicates whether or not to use the root path of "/res/components/documentlibrary/indicators/" for icon
       * images. This would be the path expected when the widget is being used within the Alfresco Share application
       * and all icons would need to be found at this location to satify customization requirements.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.55
       */
      legacyMode: false,

      /**
       * This should be set to the name of the property to render (e.g. "cm:name"). The property is expected
       * to be in the properties map for the item being rendered.
       *
       * @instance
       * @type {string}
       * @default
       */
      propertyToRender: null,

      /**
       * Indicates whether or not the configured actions for the indicators supported. So if this is configured
       * to be false then no indicator will be clickable regardless of whether or not an action has been
       * provided for it.
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.72
       */
      supportActions: true,

      /**
       * This is an auto-populated instance property that holds a cleaned-up array of indicators for the current item.
       *
       * @instance
       * @protected
       * @type {object[]}
       */
      _currentIndicators: null,

      /**
       * Overrides [the inherited function]{@link module:aikau/core/BaseWidget#createWidgetDom}
       * to construct the DOM for the widget using native browser capabilities.
       *
       * @instance
       * @since 1.0.101
       */
      createWidgetDom: function alfresco_renderers_Indicators__createWidgetDom() {
         this.containerNode = this.domNode = document.createElement("div");
         this.domNode.classList.add("alfresco-renderers-Indicators");
      },

      /**
       * Set up the attributes to be used when rendering the template.
       *
       * @instance
       */
      postMixInProperties: function alfresco_renderers_Indicators__postMixInProperties() {
         var property = this.currentItem && this.propertyToRender && this.currentItem.node.properties[this.propertyToRender];
         this.renderedValue = (property && this.getRenderedProperty(property)) || "";
         this._currentIndicators = this.normaliseIndicators(this.currentItem);
      },

      /**
       *
       * @instance
       */
      postCreate: function alfresco_renderers_Indicators__postCreate() {
         array.forEach(this._currentIndicators, this.addIndicator, this);
      },

      /**
       * Normalise and return the indicators for the current item as an array. This means
       * things such as sorting according to index and respecting the overrides properties
       * on the indicators.
       *
       * @instance
       * @protected
       * @param {object=} currentItem The current item, on which the indicators can be found
       * @returns  {object[]} The indicators.
       */
      normaliseIndicators: function alfresco_renderers_Indicators__normaliseIndicators(currentItem) {
         /*jshint loopfunc:true*/

         // Setup variables
         var indicators = (currentItem && currentItem.indicators) || [];

         // Setup variables
         var source = {}, // Starting container
            canBeOverridden = {}, // Used to find edges
            topologicallySorted = [], // Container for sorted items
            actuallyOverridden = {}, // Used to override after the sort
            normalised = []; // The output

         // Populate the source and canBeOverridden maps
         array.forEach(indicators, function(item) {
            source[item.id] = item;
            array.forEach((item.overrides || []), function(overriddenItemId) {
               canBeOverridden[overriddenItemId] = item.id;
            });
         });

         // Find the edges
         array.forEach(Object.keys(source), function(id) {
            if (!canBeOverridden.hasOwnProperty(id)) {
               topologicallySorted.push(source[id]);
               delete source[id];
            }
         });

         // Run through the sorted array, finding overridden children
         while (Object.keys(source).length) {
            array.forEach(topologicallySorted, function(sortedItem) {
               array.forEach((sortedItem.overrides || []), function(overriddenId) {
                  if (source[overriddenId]) {
                     topologicallySorted.push(source[overriddenId]);
                     delete source[overriddenId];
                  }
               });
            });
         }

         // Now we have the toposort, override in order
         array.forEach(topologicallySorted, function(nextSortedItem) {
            if (!actuallyOverridden.hasOwnProperty(nextSortedItem.id)) {
               array.forEach((nextSortedItem.overrides || []), function(overriddenId) {
                  actuallyOverridden[overriddenId] = true;
               });
               normalised.push(nextSortedItem);
            }
         });

         // Sort by index
         normalised.sort(function(a, b) {
            return a.index - b.index;
         });

         // Pass back the new collection
         return normalised;
      },

      /**
       *
       * @instance
       * @param {object} indicator The indicator configuration to add
       * @param {integer} index The index of the indicator
       */
      addIndicator: function alfresco_renderers_Indicators__addIndicator(indicator, index) {
         /*jshint unused:false*/

         // Make an attempt to convert any label parameters that are provided with the indicator for
         // use with the supplied label. This is a meeting of the old and new document library 
         // implementations which is why it is necessary to remove the leading and trailing curly
         // brackets on dot notation properties of the current item.
         var messageArgs = {};
         if (indicator.labelParams) {
            for (var i = 0; i < indicator.labelParams.length; i++) {
               var param = indicator.labelParams[i].replace(/{/g, "").replace(/}/g, "");
               if (lang.exists(param, this.currentItem)) {
                  param = lang.getObject(param, false, this.currentItem);
               }
               messageArgs[i.toString()] = param;
            }
         }
         var label = this.message(indicator.label, messageArgs),
            classes = ["indicator"];
         if (this.supportActions && indicator.action) {
            classes.push("has-action");
         }

         // Attempt to map the icon to a custom image...
         var src;
         if (this.iconMapping && this.iconMapping[indicator.icon])
         {
            src = AlfConstants.URL_RESCONTEXT + this.iconMapping[indicator.icon];
         }

         if (!src)
         {
            // If mapping is unsuccessful then use either the legcacy path or the "pure" Aikau path to set the 
            // icon image source...
            if (this.legacyMode || !this.iconInWhiteList(indicator.icon))
            {
               src = AlfConstants.URL_RESCONTEXT + "components/documentlibrary/indicators/" + indicator.icon;
            }
            else
            {
               src = require.toUrl("alfresco/renderers/css/images/indicators/" + indicator.icon);
            }
         }
         
         var img = domConstruct.create("img", {
            "src": src,
            "title": label,
            "alt": indicator.id,
            "class": classes.join(" ")
         }, this.containerNode);
         if (this.supportActions && indicator.action) {
            on(img, "click", lang.hitch(this, this.onActionClick, indicator));
         }
      },

      /**
       * Checks the [whitelist]{@link module:alfresco/renderers/Indicators#iconWhiteList}
       * for the presence of the supplied icon.
       * 
       * @instance
       * @param  {string} icon The icon to check the whitelist for
       * @return {boolean} True if the icon is found and false otherwise
       * @since 1.0.71
       */
      iconInWhiteList: function alfresco_renderers_Indicators__iconInWhiteList(icon) {
         return array.some(this.iconWhiteList, function(currIcon) {
            return currIcon === icon;
         });
      },

      /**
       *
       * @instance
       * @param {object} indicator The indicator to generate the action for
       */
      onActionClick: function alfresco_renderers_Indicators__onActionClick(indicator) {
         var publishTopic = indicator.publishTopic || "ALF_SINGLE_DOCUMENT_ACTION_REQUEST";
         var publishPayload;
         var publishGlobal = true;
         var parentScope = false;

         if (indicator.publishTopic) {
            // If a custom indicator is being provided (indicated by the definition of a custom
            // publishTopic attribute then the assumption is that the payload and scope should
            // also be provided by the indicator configuration)
            publishPayload = this.generatePayload(indicator.publishPayload,
               this.currentItem,
               null,
               indicator.publishPayloadType,
               indicator.publishPayloadItemMixin,
               indicator.publishPayloadModifiers);

            if (indicator.publishGlobal === false) {
               publishGlobal = false;
            }
            if (indicator.parentScope === true) {
               parentScope = true;
            }
         } else {
            // If a publishTopic isn't defined on the indicator then the assumption is that
            // this is a standard legacy action defined in the Alfresco Share configuration 
            // and is expected to be handled by the ActionService (or alternative service)
            publishPayload = {
               action: indicator.action,
               document: this.currentItem
            };
         }
         this.alfPublish(publishTopic, publishPayload, publishGlobal, parentScope);
      }
   });
});