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

/**
 * Extends the [inline edit property renderer]{@link module:alfresco/renderers/InlineEditProperty} to provide
 * the ability to edit and save tags.
 * 
 * @module alfresco/renderers/Tags
 * @extends module:alfresco/renderers/InlineEditProperty
 * @mixes external:dojo/_OnDijitClickMixin
 * @mixes module:alfresco/core/CoreXhr
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/renderers/InlineEditProperty", 
        "alfresco/core/ObjectTypeUtils",
        "alfresco/core/topics",
        "dojo/_base/array",
        "dojo/_base/lang",
        "alfresco/forms/controls/ComboBox",
        "alfresco/renderers/ReadOnlyTag",
        "alfresco/renderers/EditTag",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dijit/registry",
        "dojo/on",
        "dojo/keys",
        "dojo/_base/event"], 
        function(declare, InlineEditProperty, ObjectTypeUtils, topics, array, lang, ComboBox, ReadOnlyTag,
            EditTag, domConstruct, domClass, registry, on, keys, event) {

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

      /**
       * Override [default configuration]{@link module:alfresco/renderers/Property#warnIfNotAvailable} to 
       * display a message when no tags have been configured.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      warnIfNotAvailable: true,

      /**
       * Override [default configuration]{@link module:alfresco/renderers/Property#warnIfNotAvailableMessage} to 
       * to set a tag specific warning message.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      warnIfNotAvailableMessage: "no.tags.message",

      /**
       * Overrides the [inherited function]{@link module:alfresco/renderers/Property#getRenderedProperty} to convert the tags
       * value into visual tokens.
       * 
       * @instance
       */
      getRenderedProperty: function alfresco_renderers_Tags__getRenderedProperty(value) {
         // Reset the tags...
         var renderedValue = null;
         array.forEach(this.currentTags, lang.hitch(this, this.destroyTag));
         this.currentTags = [];
         if (!ObjectTypeUtils.isArray(value))
         {
            this.alfLog("warn", "Expected an array value for tags", this, value);
         }
         else
         {
            array.forEach(value, lang.hitch(this, this.createReadOnlyTag, "name", "nodeRef"));
            renderedValue = ""; // By setting an empty string as a rendered value the inherited postMixInProperties function knows that a value is set.
         }
         return renderedValue; // Always return the empty string
      },
  
      /**
       * By default this simply calls the destroy function of the tag widget (if it has one)
       * 
       * @instance
       * @param {object} tagWidget The tag to be destroyed.
       */
      destroyTag: function alfresco_renderers_Tags__destroyTag(tagWidget) {
         if (typeof tagWidget.destroy === "function")
         {
            tagWidget.destroy();
         }
      },
      
      /**
       * Called from [getRenderedProperty]{@link module:alfresco/renderers/Tags#getRenderedProperty} to render an individual
       * read only tag.
       * 
       * @instance
       * @param {string} nameAttribute The attribute to use from the tagData object as the tag name
       * @param {string} valueAttribute The attribute to use from the tagData object as the tag value
       * @param {object} tagData The tag data to render
       * @param {number} index The index of the tag in the overall array
       */
      createReadOnlyTag: function alfresco_renderers_Tags__createReadOnlyTag(nameAttribute, valueAttribute, tagData, /*jshint unused:false*/ index) {
         if (!tagData || !tagData[nameAttribute])
         {
            this.alfLog("warn", "No '" + nameAttribute + "' attribute for tag", this, tagData);
         }
         else
         {
            // Create a ReadOnlyTag widget and place it in the rendered value section.
            var tagWidget = new ReadOnlyTag({
               tagName: tagData[nameAttribute],
               tagValue: tagData[valueAttribute]
            });
            
            // Keep track of the initial set of tags as this will be updated as new tags are added or tags
            // are removed...
            this.currentTags.push(tagWidget);
         }
      },
      
      /**
       * Extends the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#postCreate} to add a custom
       * event handler for "ALF_REMOVE_TAG" events that are fired from descendant DOM nodes and to iterate over the
       * [read only tags]{@link module:alfresco/renderers/ReadOnlyTag} created by calls to the [createReadOnlyTag]{@link module:alfresco/renderers/Tags#createReadOnlyTag}
       * function and calls [placeTag]{@link module:alfresco/renderers/Tags#placeTag} on each of them to add them
       * to the widget.
       * 
       * @instance
       */
      postCreate: function alfresco_renderers_Tags__postCreate() {
         this.inherited(arguments);

         if (this.currentTags && this.currentTags.length > 0)
         {
            domConstruct.empty(this.renderedValueNode);
            array.forEach(this.currentTags, lang.hitch(this, this.placeReadOnlyTag));
         }

         // Make a backup of the initial tags so that they can be restored on a cancel action...
         this.initialTags = lang.clone(this.currentTags);
      },
      
      /**
       * @instance
       * @param {object} tagWidget The tag widget to place.
       * @param {number} index The index of the tag
       */
      placeReadOnlyTag: function alfresco_renderers_Tags__placeReadOnlyTag(tagWidget, /*jshint unused:false*/ index) {
         tagWidget.placeAt(this.renderedValueNode);
      },
      
      /**
       * Overrides the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#getPrimaryFormWidget}
       * to return a [select form control]{@link module:alfresco/forms/controls/Select}.
       *
       * @instance
       * @returns {object} The widget for editing.
       */
      getPrimaryFormWidget: function alfresco_renderers_Tags__getPrimaryFormWidget() {
         return {
            id: this.id + "_EDIT_TAGS",
            name: "alfresco/forms/controls/ComboBox", 
            config: {
               label: "",
               name: this.postParam,
               additionalCssClasses: "hiddenlabel",
               optionsConfig: {
                  queryAttribute: "name",
                  publishTopic: topics.RETRIEVE_CURRENT_TAGS,
                  publishPayload: {
                     resultsProperty: "response.data.items"
                  }
               }
            }
         };
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#onEditClick} to 
       * create the edit tag instances.
       * 
       * @instance
       */
      onEditClick: function alfresco_renderers_Tags__onEditClick() {
         this.inherited(arguments);
         if (this.editTagsNode)
         {
            var oldEditTags = registry.findWidgets(this.editTagsNode);
            array.forEach(oldEditTags, lang.hitch(this, this.destroyTag));
            domConstruct.destroy(this.editTagsNode);
         }
         this.editTagsNode = domConstruct.create("span", {}, this.editNode, "first");
         on(this.editTagsNode, "ALF_REMOVE_TAG", lang.hitch(this, this.onRemoveEditTag));
         array.forEach(this.currentTags, lang.hitch(this, this.createEditTag, "tagName", "tagValue"));
      },
      
      /**
       * Called from [onEditClick]{@link module:alfresco/renderers/Tags#onEditClick} to render an individual
       * edit tag.
       * 
       * @instance
       * @param {string} nameAttribute The attribute to use from the tagData object as the tag name
       * @param {string} valueAttribute The attribute to use from the tagData object as the tag value
       * @param {object} tagData The read-only tag widget to create a corresponding edit tag for.
       * @param {number} index The index of the tag in the overall array
       */
      createEditTag: function alfresco_renderers_Tags__createEditTag(nameAttribute, valueAttribute, tagData, /*jshint unused:false*/ index) {
         var tagWidget = new EditTag({
            tagName: tagData[nameAttribute],
            tagValue: tagData[valueAttribute]
         });
         tagWidget.placeAt(this.editTagsNode);
         this.currentTags.push(tagWidget);
      },
      
      /**
       * Handles "ALF_REMOVE_TAG" events that are emitted from a DOM node descendant. The event target
       * should map to a previously created edit tag that can then be destroyed.
       * 
       * @instance
       */
      onRemoveEditTag: function alfresco_renderers_Tags__onRemoveEditTag(evt) {
         var tagWidget = registry.byNode(evt.target);
         if (tagWidget)
         {
            // Filter out the deleted tag from the list of current tags, this is done to ensure
            // that we persist the correct tags on save and redraw the correct tags on readonly mode...
            var tagName = tagWidget.tagName;
            this.currentTags = array.filter(this.currentTags, function(currTag) {
               return currTag.tagName !== tagName;
            });
            tagWidget.destroy();
         }
      },
      
      /**
       * This function is connected via the widget template. It occurs whenever a key is pressed whilst
       * focus is on the input field for updating the property value. All keypress events other than the
       * enter and escape key are ignored. Enter will save the data, escape will cancel editing
       * 
       * @instance
       * @param {object} e The key press event
       */
      onValueEntryKeyPress: function alfresco_renderers_Tags__onValueEntryKeyPress(e) {
         if(e.charOrCode === keys.ESCAPE || e.keyCode === keys.ESCAPE)
         {
            event.stop(e);
            this.onCancel();
         }
         else if(e.charOrCode === keys.ENTER || e.keyCode === keys.ENTER)
         {
            event.stop(e);
            var formValue = this.getFormWidget().getValue();
            var tagName = lang.getObject(this.postParam, false, formValue);
            if (tagName !== "")
            {
               var o = {};
               lang.setObject(this.postParam, "", o);
               this.getFormWidget().setValue(o);
               this.createTag(tagName, false);
            }
            else
            {
               this.onSave();
            }
         }
      },
      
      /**
       * Creates a tag at the remote store (the same location from which available tags are retrieved). This
       * function is called regardless of whether or not an existing tag was created. It is expected that
       * the REST API will be able to handle duplicated (e.g. not recreate a duplicate but just return the
       * details of the existing tag).
       * 
       * @instance
       * @param {string} tagName The name of the tag to create.
       * @param {boolean} saveTagsAfterCreate Indicates whether or not to save all tags on successful creation.
       * @return {object} The created tag details
       */
      createTag: function alfresco_renderers_Tags__createTag(tagName, /*jshint unused:false*/ saveTagsAfterCreate) {

         var tagUsed = array.some(this.currentTags, function(currentTag, index) {
            return currentTag.tagName === tagName;
         });

         if (tagUsed === true)
         {
            this.alfLog("log", "Tag already used, no need to add again", this, tagName);
            // TODO: Should we clear the ComboBox? Should we display a message?
         }
         else
         {
            var responseTopic = this.generateUuid();
            var payload = {
               tagName: tagName,
               alfResponseTopic: responseTopic
            };
            this._createTagHandle = this.alfSubscribe(responseTopic + "_SUCCESS", lang.hitch(this, this.onTagCreated), true);
            this.alfPublish(topics.CREATE_TAG, payload, true);
         }
      },
      
      /**
       * @instance
       * @param {object} payload The payload of the successful tag creation request.
       */
      onTagCreated: function alfresco_renderers_Tags__onTagCreated(payload) {
         this.alfUnsubscribeSaveHandles([this._createTagHandle]);
         this.createEditTag("name", "nodeRef", payload.response);
         if (payload.originalRequestConfig && payload.originalRequestConfig.saveTagsAfterCreate === true)
         {
            this.onSave();
         }
      },
      
      /**
       * This extends the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#onSave} to check whether
       * or not there is anything selected in the ComboBox. If so it uses the data to create the edit tag rather than
       * saving. This ensures that all of the tags are captured and saved.
       * 
       * @instance
       */
      onSave: function alfresco_renderers_Tags__onSave() {
         var formValue = this.getFormWidget().getValue();
         var tagName = lang.getObject(this.postParam, false, formValue);
         if (tagName !== "")
         {
            var o = {};
            lang.setObject(this.postParam, "", o);
            this.getFormWidget().setValue(o);
            this.createTag(tagName, true);
         }
         else
         {
            this.inherited(arguments);
         }
      },
      
      /**
       * Overrides the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#updateSaveData} to 
       * set the save payload with the tag data.
       *
       * @instance
       * @param {object} payload The save payload to update.
       */
      updateSaveData: function alfresco_renderers_Tags__getSaveData(payload) {
         var editTags = registry.findWidgets(this.editTagsNode);
         var tags = "";
         for (var i=0; i<editTags.length; i++) 
         {
            tags = tags + "," + editTags[i].tagValue;
         }
         // Trim the first comma...
         if (tags.length > 0)
         {
            tags = tags.substring(1);
         }
         payload[this.postParam] = tags;
      },

      /**
       * Overrides the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#onSaveSuccess} 
       * to render the newly saved tags.
       *
       * @instance
       * @param {object} payload The success payload
       * @fires module:alfresco/core/topics#DOCUMENT_TAGGED
       */
      onSaveSuccess: function alfresco_renderers_Tags__onSaveSuccess(/*jshint unused:false*/ payload) {
         this.alfUnsubscribeSaveHandles([this._saveSuccessHandle, this._saveFailureHandle]);

         this.alfLog("log", "Property '" + this.propertyToRender + "' successfully updated for node: ", this.currentItem);

         // Remove all the old read-only tags...
         var oldReadOnlyTags = registry.findWidgets(this.renderedValueNode);
         array.forEach(oldReadOnlyTags, lang.hitch(this, this.destroyTag));
         domConstruct.empty(this.renderedValueNode);

         // Build the list of new tags from those in the edit view...
         this.currentTags = [];
         var editTags = registry.findWidgets(this.editTagsNode);
         array.forEach(editTags, lang.hitch(this, this.createReadOnlyTag, "tagName", "tagValue"));
         array.forEach(this.currentTags, lang.hitch(this, this.placeReadOnlyTag));

         // Update the initial tags to reflect the updated state...
         this.initialTags = lang.clone(this.currentTags);

         // Switch back into the "view" mode...
         domClass.remove(this.renderedValueNode, "hidden faded");
         domClass.add(this.editNode, "hidden");
         this.renderedValueNode.focus();

         // Fire document tagged event
         this.alfPublish(topics.DOCUMENT_TAGGED);
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/renderers/InlineEditProperty#onCancel} to clear
       * the edit tags.
       *
       * @instance
       */
      onCancel: function alfresco_renderers_Tags__onCancel() {
         this.inherited(arguments);
         this.currentTags = lang.clone(this.initialTags);
      }
   });
});