Source: renderers/Property.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/>.
 */

/**
 * Renders the value of a property found in the configured [currentItem]{@link module:alfresco/renderers/Property#currentItem}
 * attribute. The property that will be rendered is determined by the [propertyToRender]{@link module:alfresco/renderers/Property#propertyToRender}
 * attribute which should be defined in "dot-notation" format (e.g. "node.properties.cm:title"). This widget accepts a number
 * of different configuration options that control how the property is ultimately displayed.
 *
 * @module alfresco/renderers/Property
 * @extends module:aikau/core/BaseWidget
 * @mixes module:alfresco/renderers/_JsNodeMixin
 * @mixes module:alfresco/renderers/_ItemLinkMixin
 * @mixes module:alfresco/core/ValueDisplayMapMixin
 * @mixes module:alfresco/core/UrlUtilsMixin
 * @mixes module:alfresco/core/TemporalUtils
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "aikau/core/BaseWidget",
        "alfresco/renderers/_JsNodeMixin", 
        "alfresco/core/ValueDisplayMapMixin", 
        "alfresco/core/ObjectTypeUtils", 
        "alfresco/core/UrlUtilsMixin", 
        "alfresco/core/TemporalUtils", 
        "dojo/_base/lang", 
        "dojo/dom-class", 
        "dojo/dom-style", 
        "dijit/Tooltip", 
        "dojo/on"], 
        function(declare, BaseWidget, _JsNodeMixin, ValueDisplayMapMixin, ObjectTypeUtils, UrlUtilsMixin, 
                 TemporalUtils, lang, domClass, domStyle, Tooltip, on) {

   return declare([BaseWidget, _JsNodeMixin, ValueDisplayMapMixin, TemporalUtils, UrlUtilsMixin], {

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

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

      /**
       * This is the object that the property to be rendered will be retrieved from.
       *
       * @instance
       * @type {object}
       * @default
       */
      currentItem: null,

      /**
       * Indicates whether or not the property should be de-emphasized. This will result in a lighter colour
       * being used.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      deemphasized: false,

      /**
       * This is the property within the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#highlightProperty}
       * that should be used to identify the content with in the 
       * [renderedValue]{@link module:alfresco/renderers/Property#renderedValue} to highlight.
       * 
       * @instance
       * @type {string}
       * @default 
       * @since 1.0.87
       */
      highlightProperty: null,

      /**
       * The prefix string that indicates the start of text to highlight.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.92
       */
      highlightPrefix: null,

      /**
       * The postfix string that indicates the start of text to highlight.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.92
       */
      highlightPostfix: null,
      
      /**
       * This is a specific value to highlight. It will be not be used if
       * [highlightProperty]{@link module:alfresco/renderers/Property#highlightProperty} is configured.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.87
       */
      highlightValue: null,

      /**
       * The label for the property. Won't be shown if left as null.
       *
       * @instance
       * @type {string}
       * @default
       */
      label: null,

      /**
       * Specifies a maximum width for the content of the renderer, and will overflow with an ellipsis if necessary
       *
       * @instance
       * @type {string}
       * @default
       */
      maxWidth: null,

      /**
       * Indicates that this should only be displayed when the item (note: NOT the renderer) is
       * hovered over.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      onlyShowOnHover: 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,

      /**
       * This will be set with the rendered value.
       *
       * @instance
       * @type {string}
       * @default
       */
      renderedValue: null,

      /**
       * This can be set to apply a CSS class to the rendered property value
       *
       * @instance
       * @type {string}
       * @default
       */
      renderedValueClass: "alfresco-renderers-Property",

      /**
       * A label to go before the rendered value
       * 
       * @instance
       * @type {string}
       * @default
       */
      renderedValuePrefix: "",

      /**
       * A label to go after the rendered value
       * 
       * @instance
       * @type {string}
       * @default
       */
      renderedValueSuffix: "",

      /**
       * Indicates whether or not the property will be rendered on a new line.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      renderOnNewLine: false,

      /**
       * This indicates whether or not the requestes property can be found for the current item
       *
       * @instance
       * @type {boolean}
       * @default
       */
      renderPropertyNotFound: true,

      /**
       * This can be either small, medium or large.
       * 
       * @instance
       * @type {string}
       * @default
       */
      renderSize: "medium",

      /**
       * A title attribute to apply to the property to set an HTML title attribute on the outer
       * element of the widget. The value will be displayed in a tooltip when hovered over.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.102
       */
      title: null,

      /**
       * Indicates whether or not string values should be trimmed.
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.99
       */
      trimValue: false,

      /**
       * Indicates whether or not a message should be displayed in place of the 
       * [propertyToRender]{@link module:alfresco/renderers/Property#propertyToRender} when it is not available.
       * This defaults to false but if configured to be true then the 
       * [warnIfNotAvailableMessage]{@link module:alfresco/renderers/Property#warnIfNotAvailableMessage}
       * will be displayed.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      warnIfNotAvailable: false,

      /**
       * This is the message that will be displayed in place of the property value if that value does not
       * exist. The message will only be displayed if 
       * [warnIfNotAvailable]{@link module:alfresco/renderers/Property#warnIfNotAvailable} is not configured
       * to be true. If this is left as the default value then an attempt will be made to set an appropriate
       * message but will ultimately fallback to referencing the configured 
       * [propertyToRender]{@link module:alfresco/renderers/Property#propertyToRender} value.
       *
       * @instance
       * @type {string}
       * @default
       */
      warnIfNotAvailableMessage: null,

      /**
       * Indicates whether or not a warning should (or is) being displayed. This is for internal use only
       * and should not be configured or programmatically set.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.35
       */
      warningDisplayed: false,

      /**
       * The positions where a tooltip can appear over a truncated value
       *
       * @static
       * @instance
       * @protected
       * @type {string[]}
       */
      _tooltipPositions: ["below-centered", "above-centered"],

      /**
       * 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.100
       */
      createWidgetDom: function alfresco_renderers_Property__createWidgetDom() {
         this.renderedValueNode = this.domNode = document.createElement("span");
         this.renderedValueClassArray.forEach(function(className) {
            this.domNode.classList.add(className);
         }, this);
         this.domNode.setAttribute("tabindex", "0");

         this.innerSpan = document.createElement("span");
         this.innerSpan.classList.add("inner");

         var labelSpan = document.createElement("span");
         labelSpan.classList.add("label");
         labelSpan.textContent = this.label;

         var valueSpan = document.createElement("span");
         valueSpan.classList.add("value");
         valueSpan.innerHTML = this.renderedValue;

         this.innerSpan.appendChild(labelSpan);
         this.innerSpan.appendChild(valueSpan);
         this.domNode.appendChild(this.innerSpan);

         // See AKU-1166
         if (this.title)
         {
            this.domNode.setAttribute("title", this.title);
         }
      },

      /**
       * Updates CSS classes based on the current state of the renderer. Currently this only
       * addressed warning message states.
       * 
       * @instance
       * @since 1.0.35
       */
      updateCssClasses: function alfresco_renderers_Property__updateCssClasses() {
         if (this.renderedValueNode)
         {
            if(this.warningDisplayed)
            {
               domClass.add(this.renderedValueNode, "faded");
            }
            else
            {
               domClass.remove(this.renderedValueNode, "faded");
            }
         }
      },

      /**
       * Generates the rendering of the property value. This will include any 
       * [prefix]{@link module:alfresco/renderers/Property#renderedValuePrefix} and/or 
       * [suffix]{@link module:alfresco/renderers/Property#renderedValueSuffix} as well
       * as any configured
       * [warning]{@link module:alfresco/renderers/Property#warnIfNotAvailableMessage}
       * to be displayed when the property does not exist.
       * 
       * @instance
       * @param {string} value The value to be rendered
       * @return {string} Returns the rendered value
       * @since 1.0.35
       */
      generateRendering: function alfresco_renderers_Property__generateRendering(value) {
         var valueRendering;

         // If the renderedValue is not set then display a warning message if requested...
         if (!ObjectTypeUtils.isValueSet(value, false)) 
         {
            if (this.warnIfNotAvailable)
            {
               var warningMessage = this.getNotAvailableMessage();
               valueRendering = this.renderedValuePrefix + warningMessage + this.renderedValueSuffix;
               this.warningDisplayed = true;
            }
            else
            {
               valueRendering = value;
            }
         }
         else
         {
            valueRendering = this.renderedValuePrefix + value + this.renderedValueSuffix;
            this.warningDisplayed = false;
         }

         return valueRendering;
      },

      /**
       * Get the message to display if the property isn't available. Note that
       * this does not check the [warnIfNotAvailable property]{@link module:alfresco/renderers/Property#warnIfNotAvailable} because it could then not be easily used by subclasses.
       *
       * @instance
       * @returns {string} The warning message
       * @since 1.0.57
       */
      getNotAvailableMessage: function alfresco_renderers_Property__getNotAvailableMessage() {
         // Get appropriate message
         // Check message based on propertyToRender otherwise default to sensible alternative
         var warningKey = this.warnIfNotAvailableMessage;
         var warningMessage = "";
         if (!warningKey) {
            warningKey = "no." + this.propertyToRender + ".message";
            warningMessage = this.message(warningKey);
            if (warningMessage === warningKey) {
               warningMessage = this.message("no.property.message", {
                  0: this.propertyToRender
               });
            }
         } else {
            warningMessage = this.message(warningKey);
         }
         return warningMessage;
      },

      /**
       * Set up the attributes to be used when rendering the template.
       *
       * @instance
       */
      postMixInProperties: function alfresco_renderers_Property__postMixInProperties() {
         if (this.label) 
         {
            this.label = this.message(this.label) + ": ";
         } 
         else 
         {
            this.label = "";
         }

         var highlight = this.highlightValue;
         if (this.highlightProperty)
         {
            highlight = lang.getObject(this.highlightProperty, false, this.currentItem);
         }

         if (ObjectTypeUtils.isString(this.propertyToRender) &&
             ObjectTypeUtils.isObject(this.currentItem) &&
             lang.exists(this.propertyToRender, this.currentItem)) 
         {
            this.renderPropertyNotFound = false;
            this.originalRenderedValue = this.getRenderedProperty(lang.getObject(this.propertyToRender, false, this.currentItem),
                                                                  highlight);
            this.renderedValue = this.mapValueToDisplayValue(this.originalRenderedValue);
         } 
         else 
         {
            this.alfLog("log", "Property does not exist:", this);
         }

         this.renderedValue = this.generateRendering(this.renderedValue);
         this.updateRenderedValueClass();
      },

      /**
       * Updates the [renderedValueClass]{@link module:alfresco/renderers/Property#renderedValueClass}
       * attribute based on the current configuration. This is abstracted to a function so that it can
       * be easily called by extending renderers.
       *
       * @instance
       */
      updateRenderedValueClass: function alfresco_renderers_Property__updateRenderedValueClass() {
         // Need to set both renderedValueClassArray (for widgets without a template) and
         // renderedValueClass (for those that do)...
         var renderedValueClass = this.renderedValueClass || "";
         
         // See AKU-1152 - handle spaces in arrays...
         this.renderedValueClassArray = renderedValueClass.replace(/\s+/g, " ").trim().split(" ");
         this.renderedValueClassArray = this.renderedValueClassArray.concat(["alfresco-renderers-Property", this.renderSize]);
         this.renderedValueClass = this.renderedValueClass + " " + this.renderSize;
         if (this.renderOnNewLine === true) 
         {
            this.renderedValueClass = this.renderedValueClass + " block";
            this.renderedValueClassArray.push("block");
         }
         if (this.deemphasized === true) 
         {
            this.renderedValueClass = this.renderedValueClass + " deemphasized";
            this.renderedValueClassArray.push("deemphasize");
         }
      },

      /**
       * Determines whether or not the property should only be displayed when the item is hovered over.
       *
       * @instance
       */
      postCreate: function alfresco_renderers_Property__postCreate() {
         this.updateCssClasses();
         if (this.maxWidth) 
         {
            domClass.add(this.domNode, "has-max-width");
            domStyle.set(this.domNode, {
               maxWidth: this.maxWidth
            });
         }
         if (this.onlyShowOnHover === true) 
         {
            domClass.add(this.domNode, "hover-only");
         }
      },

      /**
       * Called on widget startup
       *
       * @instance
       */
      startup: function alfresco_renderers_Property__startup() {
         this.inherited(arguments);
         if (this.maxWidth && this.domNode.scrollWidth > this.domNode.clientWidth) {
            var target = this.domNode,
               tooltipPositions = this._tooltipPositions;
            this.own(on(target, "focus,mouseover", function() {
               var content = lang.trim(target.textContent || target.innerText || "");
               Tooltip.show(content, target, tooltipPositions);
            }));
            this.own(on(target, "blur,mouseout", function() {
               Tooltip.hide(target);
            }));
         }
      },

      /**
       * Renders a date property.
       *
       * @instance
       */
      renderDate: function alfresco_renderers_Property__renderDate(date, format) {
         return this.formatDate(this.fromISO8601(date), format);
      },

      /**
       * Updates the supplied value to include HTML mark elements around all instances of the
       * supplied highlight value.
       * 
       * @instance
       * @param {string} value The value to update with highlight marks
       * @param {string} highlight The text to highlight
       * @return {string} The highlighted value
       * @since 1.0.87
       */
      addHighlightMarks: function alfresco_renderers_Property__addHighlightMarks(value, highlight) {
         var re = new RegExp(highlight, "gi");
         return value.replace(re, "<mark>$&</mark>");
      },

      /**
       * @instance
       * @param {string} property The name of the property to render
       */
      getRenderedProperty: function alfresco_renderers_Property__getRenderedProperty(property, highlight) {
         /*jshint maxcomplexity:false*/
         var value = "";
         if (property === null || typeof property === "undefined") 
         {
            // No action required if a property isn't supplied
         } 
         else if (ObjectTypeUtils.isString(property)) 
         {
            value = this.encodeHTML(property);
         } 
         else if (ObjectTypeUtils.isArray(property)) 
         {
            value = property.length;
         } 
         else if (ObjectTypeUtils.isBoolean(property)) 
         {
            value = property;
         } 
         else if (ObjectTypeUtils.isNumber(property)) 
         {
            value = property;
         } 
         else if (ObjectTypeUtils.isObject(property)) 
         {
            // TODO: This should probably be moved out into a Node specific sub-class
            if (property.hasOwnProperty("iso8601")) 
            {
               value = this.renderDate(property.iso8601);
            } 
            else if (property.hasOwnProperty("userName") && property.hasOwnProperty("displayName")) 
            {
               value = this.userProfileLink(property.userName, property.displayName);
            } 
            else if (property.hasOwnProperty("displayName")) 
            {
               value = this.encodeHTML(property.displayName || "");
            } 
            else if (property.hasOwnProperty("title")) 
            {
               value = this.encodeHTML(property.title || "");
            } 
            else if (property.hasOwnProperty("name")) 
            {
               value = this.encodeHTML(property.name || "");
            }
         }

         if (value && highlight)
         {
            value = this.addHighlightMarks(value, highlight);
         }
         else if (value && this.highlightPrefix && this.highlightPostfix)
         {
            value = value.replace(new RegExp(this.highlightPrefix, "g"), "<mark>").replace(new RegExp(this.highlightPostfix, "g"), "</mark>");
         }

         if (value && this.trimValue && typeof value.trim === "function")
         {
            value = value.replace(/ /g, " ").trim();
         }

         return value;
      }
   });
});