Source: dnd/DroppedItemWrapper.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 widget is used to wrap dropped items in a [DragAndDropTarget]{@link module:alfresco/dnd/DragAndDropTarget}.
 * It provides a drag handle (for re-ordering or dragging to other targets) as well as edit and
 * delete buttons. It is also reponsible for managing the state of the item that it represents as well
 * as rendering the a representation of that item.
 * 
 * @module alfresco/dnd/DroppedItemWrapper
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/CoreWidgetProcessing
 * @mixes module:alfresco/renderers/_PublishPayloadMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/DroppedItemWrapper.html",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/renderers/_PublishPayloadMixin",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/on",
        "alfresco/dnd/Constants",
        "dojo/Deferred",
        "dojo/dom-construct",
        "dojo/dom-style",
        "jquery"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, CoreWidgetProcessing, _PublishPayloadMixin,
                 lang, array, on, Constants, Deferred, domConstruct, domStyle, $) {
   
   return declare([_WidgetBase, _TemplatedMixin, AlfCore, CoreWidgetProcessing, _PublishPayloadMixin], {
      
      /**
       * The array of file(s) containing internationalised strings.
       *
       * @instance
       * @type {object}
       * @default [{i18nFile: "./i18n/DroppedItemWrapper.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/DroppedItemWrapper.properties"}],

      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {Array}
       */
      cssRequirements: [{cssFile:"./css/DroppedItemWrapper.css"}],
      
      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {string}
       */
      templateString: template,
      
      /**
       * This is the value that the wrapper represents.
       *
       * @instance
       * @type {object}
       * @default
       */
      value: null,

      /**
       * The name of the file to use for the move up image. This is expected to be in the css/images relative path
       * of the widget.
       *
       * @instance
       * @type {string}
       * @default
       */
      upImg: "move-up.png",
      
      /**
       * The name of the file to use for the move down image. This is expected to be in the css/images relative path
       * of the widget.
       *
       * @instance
       * @type {string}
       * @default
       */
      downImg: "move-down.png",
      
      /**
       * The name of the file to use for the edit image. This is expected to be in the css/images relative path
       * of the widget.
       *
       * @instance
       * @type {string}
       * @default
       */
      editImg: "edit-16.png",
      
      /**
       * The name of the file to use for the delete image. This is expected to be in the css/images relative path
       * of the widget.
       *
       * @instance
       * @type {string}
       * @default
       */
      deleteImg: "trashcan-16.png",

      /**
       * The message to display as alt text for the move up image. This will also be displayed as a title on the
       * image.
       *
       * @instance
       * @type {string}
       * @default
       */
      upAltText: "droppedItemWrapper.up.alt.text",
      
      /**
       * The message to display as alt text for the move down image. This will also be displayed as a title on the
       * image.
       *
       * @instance
       * @type {string}
       * @default
       */
      downAltText: "droppedItemWrapper.down.alt.text",
      
      /**
       * The message to display as alt text for the delete image. This will also be displayed as a title on the
       * image.
       *
       * @instance
       * @type {string}
       * @default
       */
      deleteAltText: "droppedItemWrapper.delete.alt.text",
      
      /**
       * The message to display as alt text for the edit image. This will also be displayed as a title on the
       * image.
       *
       * @instance
       * @type {string}
       * @default
       */
      editAltText: "droppedItemWrapper.edit.alt.text",

      /**
       * This controls whether or not the edit button is displayed. By default it is not and this should only be 
       * configured to be true when using a [modelling service]{@link module:alfresco/dnd/DragAndDropTarget#useModellingService}
       * that is configured to provide edit configuration for the item.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      showEditButton: false,

      /**
       * Sets up images and translations for alt text and titles.
       * 
       * @instance
       */
      postMixInProperties: function alfresco_dnd_DroppedItemWrapper__postMixInProperties() {
         this.upAltText = this.encodeHTML(this.message(this.upAltText));
         this.downAltText = this.encodeHTML(this.message(this.downAltText));
         this.deleteAltText = this.encodeHTML(this.message(this.deleteAltText));
         this.editAltText = this.encodeHTML(this.message(this.editAltText));
         this.upImageSrc = require.toUrl("alfresco/dnd/css/images/" + this.upImg);
         this.downImageSrc = require.toUrl("alfresco/dnd/css/images/" + this.downImg);
         this.editImageSrc = require.toUrl("alfresco/dnd/css/images/" + this.editImg);
         this.deleteImageSrc = require.toUrl("alfresco/dnd/css/images/" + this.deleteImg);
      },

      /**
       * @instance
       */
      postCreate: function alfresco_dnd_DroppedItemWrapper__postCreate() {
         if (this.showEditButton === false)
         {
            domStyle.set(this.editNode, "display", "none");
         }

         if (this.label)
         {
            this.labelNode.innerHTML = this.encodeHTML(this.message(this.label));

            // Save the label as part of the overall value so that it is not lost when an enclosing
            // widget is edited. See AKU-318
            if (this.value)
            {
               this.value.label = this.label;
               this.value.type = this.type;
            }
         }
         if (this.widgets !== null && this.widgets !== undefined)
         {
            this.processWidgets(this.widgets, this.controlNode);
         }
      },

      /**
       * This is an extension point for handling the completion of calls to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
       *
       * @instance
       * @param {Array} widgets An array of all the widgets that have been processed
       */
      allWidgetsProcessed: function alfresco_dnd_DroppedItemWrapper__allWidgetsProcessed(widgets) {
         array.forEach(widgets, lang.hitch(this, this.setWidgetValue));
         this._renderedWidgets = widgets;
      },

      /**
       * Sets the value of the supplied widget with the current value.
       *
       * @instance
       * @param {object} widget The widget to set the value of.
       */
      setWidgetValue: function alfresco_dnd_DroppedItemWrapper__setWidgetValue(widget) {
         if (widget && typeof widget.setValue === "function")
         {
            widget.setValue(this.value);
         }
      },

      /**
       * Returns the current value of the item.
       *
       * @instance
       * @returns {object} The value represented by the dropped item.
       */
      getValue: function alfresco_dnd_DroppedItemWrapper__getValue() {
         return this.value;
      },

      /**
       * Emits a custom a event to indicate that the widget should be deleted.
       * TODO: Should this prompt the user with a confirmation dialog?
       * 
       * @instance
       * @param {object} evt The click event that triggers the delete.
       */
      onItemDelete: function alfresco_dnd_DroppedItemWrapper__onItemDelete(/* jshint unused:false */ evt) {
         on.emit(this.domNode, Constants.deleteItemEvent, {
            bubbles: true,
            cancelable: true,
            targetWidget: this
         });
         this.alfPublish(Constants.itemDeletedTopic, {
            item: this.getValue()
         });
      },

      /**
       * Publishes a request to get the configuration model for the current item. This request is expected to 
       * be handled by a [DndModellingService]{@link module:alfresco/services/DragAndDropModellingService}.
       * 
       * @instance
       * @param {object} evt The click event that triggers the delete.
       */
      onItemEdit: function alfresco_dnd_DroppedItemWrapper__onItemEdit(/* jshint unused:false */ evt) {
         var promise = new Deferred();
         promise.then(lang.hitch(this, this.onEditConfig, this.value));
         this.alfPublish(Constants.requestWidgetsForConfigTopic, {
            value: this.value,
            promise: promise
         });
      },

      /**
       * Handles the data provided by a [DndModellingService]{@link module:alfresco/services/DragAndDropModellingService}
       * requested when the user edits the current item. This will request the creation of a new dialog to display 
       * the form widgets expected to have been provided in the resolved promise.
       * 
       * @param {object} item The current item being edited.
       * @param {promise} resolvedPromise A resolved promise that is expected to contain a widgets array
       */
      onEditConfig: function alfresco_dnd_DroppedItemWrapper__onEditConfig(item, resolvedPromise) {
         if (resolvedPromise.widgets)
         {
            var subscriptionTopic = this.generateUuid();
            var subscriptionHandle = this.alfSubscribe(subscriptionTopic, lang.hitch(this, this.onEditSave));

            var clonedItem = lang.clone(item);
            var payloadMixin = {
               subscriptionHandle: subscriptionHandle
            };
            $.extend(true, payloadMixin, clonedItem);

            // Set the current item for processing purposes...
            this.currentItem = {
               item: clonedItem,
               subscriptionTopic: subscriptionTopic,
               payloadMixin: payloadMixin,
               widgets:resolvedPromise.widgets
            };

            // Generate a payload and publish it...
            var publishPayload = this.generatePayload(this.editPublishPayload, this.currentItem, null, this.editPublishPayloadType, this.editPublishPayloadItemMixin, this.editPublishPayloadModifiers);
            this.currentItem = {};
            this.alfPublish(this.editPublishTopic, publishPayload, this.editPublishGlobal, this.editPublishToParent);
         }
         else
         {
            this.alfLog("warn", "Wigets were not provided in the resolved promise", item, resolvedPromise, this);
         }
      },

      /**
       * This is the topic that is published when a request is made to edit the value of the item
       * represented by this wrapper. The default behaviour is to request that a dialog be displayed.
       *
       * @instance
       * @type {string}
       * @default
       */
      editPublishTopic: "ALF_CREATE_FORM_DIALOG_REQUEST",

      /**
       * Indicates whether the edit publication should be published globally.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      editPublishGlobal: true,

      /**
       * Indicates whether edit publications should be published on the parent scope.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      editPublishToParent: false,

      /**
       * This is the payload that will be published when a request is made to edit the value of the 
       * item represented by this wrapper. If required the 
       *
       * @instance
       * @type {object}
       */
      editPublishPayload: {
         dialogId: "ALF_DROPPED_ITEM_CONFIGURATION_DIALOG",
         dialogTitle: "{item.name}",
         formSubmissionTopic: "{subscriptionTopic}",
         formSubmissionPayloadMixin: "{payloadMixin}",
         contentWidth: "600px",
         contentHeight: "700px",
         fixedWidth: true,
         formValue: "{item}",
         widgets: "{widgets}"
      },

      /**
       * This is the type of payload defined by the 
       * [editPublishPayload]{@link module:alfresco/dnd/DroppedItemWrapper#editPublishPayload}. By 
       * default this is set to "PROCESS" indicating that the payload contains tokens to be substituted.
       *
       * @instance
       * @type {string}
       * @default
       */
      editPublishPayloadType: "PROCESS",

      /**
       * This indicates whether ot not the payload should have the "currentItem" attribute mixed into it.
       * By default this is set to false.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      editPublishPayloadItemMixin: false,

      /**
       * This defines any processor functions that should be used to process the 
       * [editPublishPayload]{@link module:alfresco/dnd/DroppedItemWrapper#editPublishPayload}. These will
       * only be applied if the [editPublishPayloadType]{@link module:alfresco/dnd/DroppedItemWrapper#editPublishPayloadType}
       * is configured to be "PROCESS" (which is the default behaviour).
       *
       * @instance
       * @type {string[]}
       * @default ["processCurrentItemTokens"]
       */
      editPublishPayloadModifiers: ["processCurrentItemTokens"],

      /**
       * Handles submission of the dialog requested when the user edits the value of the widget.
       * This will mix the published value into the current value (rather than overwriting it)
       * and then destroy and previously rendered widgets and re-create them using the updated value.
       *
       * @instance
       * @param {object} payload The updated value for the item.
       */
      onEditSave: function alfresco_dnd_DroppedItemWrapper__onEditSave(payload) {
         if (payload.subscriptionHandle)
         {
            this.alfUnsubscribe(payload.subscriptionHandle);
            delete payload.subscriptionHandle;
         }
         this.alfCleanFrameworkAttributes(payload, true);

         // We should consider whether we actually want to mixin in the updated value
         // or just override it completely. It depends on whether or not we want to be
         // able to remove data.
         $.extend(true, this.value, payload);
         // this.value = lang.clone(payload);
         on.emit(this.domNode, Constants.itemSavedEvent, {
            bubbles: true,
            cancelable: true,
            targetWidget: this
         });

         if (this._renderedWidgets)
         {
            array.forEach(this._renderedWidgets, function(widget) {
               if (typeof widget.destroy === "function")
               {
                  widget.destroy();
               }
            });
            domConstruct.empty(this.controlNode);
            this.postCreate();
         }
      },

      /**
       * Moves the item up one place.
       * 
       * @instance
       */
      onItemUp: function alfresco_dnd_DroppedItemWrapper__onItemUp() {
         var currentIndex = $(this.domNode).index();
         if (currentIndex !== 0)
         {
            var previousItem = $(this.domNode.parentNode).children().eq(currentIndex -1);
            $(this.domNode).after($(previousItem));

            on.emit(this.domNode, Constants.reorderItemsEvent, {
               bubbles: true,
               cancelable: true,
               targetWidget: this,
               oldIndex: currentIndex,
               newIndex: currentIndex - 1 
            });
         }
      },

      /**
       * Moves the item down one place.
       * 
       * @instance
       */
      onItemDown: function alfresco_dnd_DroppedItemWrapper__onItemDown() {
         var currentIndex = $(this.domNode).index();
         var itemCount = $(this.domNode.parentNode).children().length;
         if (currentIndex !== itemCount - 1)
         {
            var nextItem = $(this.domNode.parentNode).children().eq(currentIndex + 1);
            $(this.domNode).before($(nextItem));

            // NOTE: Potential alternative animation, not sure about it though...
            // $(this.domNode).slideUp(300, function () {
            //    $(this).insertAfter(nextItem).slideDown(300);
            // });

            on.emit(this.domNode, Constants.reorderItemsEvent, {
               bubbles: true,
               cancelable: true,
               targetWidget: this,
               oldIndex: currentIndex,
               newIndex: currentIndex + 1 
            });
         }
      }
   });
});