Source: dnd/DragAndDropTarget.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/>.
 */

/**
 * <p>This widget represents a drop target for items dragged from a [DragAndDropItems]{@link module:alfresco/dnd/DragAndDropItems} widget. When an item is dropped onto this
 * widget it will render that item using the configured [widgetsForWrappingDroppedItems]{@link module:alfresco/dnd/DragAndDropTarget#widgetsForWrappingDroppedItems} and
 * [widgetsForDroppedItems]{@link module:alfresco/dnd/DragAndDropTarget#widgetsForDroppedItems} 
 * unless it has been configured to [use a modelling service]{@link module:alfresco/dnd/DragAndDropTarget#useModellingService}
 * (in which case the [service]{@link module:alfresco/services/DragAndDropModellingService} will provide the widget models).</p>
 * 
 * @module alfresco/dnd/DragAndDropTarget
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/core/ObjectProcessingMixin",
        "dojo/text!./templates/DragAndDropTarget.html",
        "alfresco/core/Core",
        "alfresco/core/PubQueue",
        "alfresco/core/topics",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dijit/registry",
        "dojo/dnd/Source",
        "dojo/dnd/Target",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/aspect",
        "dojo/on",
        "alfresco/dnd/Constants",
        "dojo/Deferred",
        "dojo/_base/event",
        "dojo/keys",
        "dojo/dnd/Manager"], 
        function(declare, _Widget, _Templated, CoreWidgetProcessing, ObjectProcessingMixin, template, AlfCore, PubQueue, topics,
                 lang, array, registry, Source, Target, domConstruct, domClass, aspect, on, Constants, Deferred, Event, keys, DndManager) {
   
   return declare([_Widget, _Templated, CoreWidgetProcessing, ObjectProcessingMixin, AlfCore], {
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {Array}
       */
      cssRequirements: [{cssFile:"./css/DragAndDropTarget.css"}],
      
      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {String}
       */
      templateString: template,
      
      /**
       * @instance
       * @type {boolean}
       * @default
       */
      horizontal: false,
      
      /**
       * The target for dropping widgets onto.
       * 
       * @instance
       * @type {object}
       * @default
       */
      previewTarget: null,
      
      /**
       * A list of the initial items to add to the drop zone when it is first created.
       * 
       * @instance
       * @type {object[]}
       * @default
       */
      initialItems: null,
      
      /**
       * The types that this drop zone will accept. By default this is set to null but if not specified
       * in the configuration this will be initialised to ["widget"].
       *
       * @instance
       * @type {string[]}
       * @default
       */
      acceptTypes: null,

      /**
       * Indicates whether or not dragging should be done with handles.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      withHandles: true,

      /**
       * An optional label to provide for the target.
       *
       * @instance
       * @type {string}
       * @default
       */
      label: null,

      /**
       * @instance
       */
      postCreate: function alfresco_dnd_DragAndDropTarget__postCreate() {
         if (this.label)
         {
            domConstruct.create("div", {
               "class": "label",
               innerHTML: this.encodeHTML(this.message(this.label))
            }, this.domNode, "first");
         }

         if (this.acceptTypes === null)
         {
            this.acceptTypes = ["widget"];
         }
         
         this.previewTarget = new Source(this.previewNode, { 
            accept: lang.clone(this.acceptTypes),
            creator: lang.hitch(this, this.creator),
            withHandles: this.withHandles,
            horizontal: this.horizontal
         });

         // Capture wrappers being selected...
         aspect.after(this.previewTarget, "onMouseDown", lang.hitch(this, this.onWidgetSelected), true);
         
         // Capture widgets being dropped...
         aspect.after(this.previewTarget, "insertNodes", lang.hitch(this, this.onItemsUpdated), true);
         aspect.after(this.previewTarget, "deleteSelectedNodes", lang.hitch(this, this.onItemsUpdated), true);


         if (this.previewTarget)
         {
            aspect.after(this.previewTarget, "onOutEvent", lang.hitch(this, this.onItemDraggedOut), true);
            aspect.after(this.previewTarget, "onOverEvent", lang.hitch(this, this.onItemDraggedOver), true);
         }
         
         // Listen for widgets requesting to be deleted...
         on(this.previewNode, Constants.deleteItemEvent, lang.hitch(this, this.deleteItem));
         on(this.previewNode, Constants.nestedDragOutEvent, lang.hitch(this, this.onNestedDragOut));
         on(this.previewNode, Constants.nestedDragOverEvent, lang.hitch(this, this.onNestedDragOver));

         var _this = this;
         this.watch("value", function(name, oldValue, newValue) {
            _this.setValue(newValue);
         });
      },
      
      /**
       * The widget model used to wrap each dropped item.
       * 
       * @instance
       * @type {array}
       */
      widgetsForWrappingDroppedItems: [
         {
            name: "alfresco/dnd/DroppedItemWrapper",
            config: {
               label: "{label}",
               value: "{value}",
               type: "{type}",
               widgets: "{widgets}"
            }
         }
      ],

      /**
       * This is the default widget model to use for each dropped item. It can be overridden if required
       * and will not be used if the data in the dropped item contains a "widgets" attribute.
       *
       * @instance
       * @type {array}
       */
      widgetsForDroppedItems: [
         {
            name: "alfresco/dnd/DroppedItem"
         }
      ],

      /**
       * Indicates whether or not to use a modelling service to render the dropped items.
       * This will result in publications being made to request the widgets to use for each
       * dropped item based on the value of the dropped item.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useModellingService: false,

      /**
       * This function is called from the [creator]{@link module:alfresco/dnd/DragAndDropTarget#creator} function
       * when [useModellingService]{@link module:alfresco/dnd/DragAndDropTarget#useModellingService} is set to true.
       * It publishes a request for a widget model for the value of the dropped item that is expected to 
       * be serviced by a modelling service (such as the [DndModellingService]{@link module:alfresco/services/DragAndDropModellingService}).
       * 
       * @instance
       * @param {object} item The dropped item
       */
      createViaService: function alfresco_dnd_DragAndDropTarget__createViaService(item, node) {
         var promise = new Deferred();
         promise.then(lang.hitch(this, this.createDroppedItemsWidgets, item, node));
         this.alfPublish(Constants.requestWidgetsForDisplayTopic, {
            type: item.type,
            value: item.value,
            promise: promise
         });
      },

      /**
       * This function is the callback for the promise published by the [createViaService]
       * {@link module:alfresco/dnd/DragAndDropTarget#createViaService} function. It will
       * render the model provided to wrap and represent the dropped item.
       *
       * @instance
       * @param {object} item The dropped item configuration
       * @param {element} node The dropped element
       * @param {object} resolvedPromise A resolved promise containing the widget models to render
       */
      createDroppedItemsWidgets: function alfresco_dnd_DragAndDropTarget__createDroppedItemsWidgets(item, node, resolvedPromise) {
         if (resolvedPromise.widgets)
         {
            var widgetModel = lang.clone(resolvedPromise.widgets);

            // Set the value to be processed...
            this.currentItem = {};
            this.currentItem.label = item.label || item.value.label; // If no label is provided on the item, check the value (See AKU-318)
            this.currentItem.value = item.value;
            this.currentItem.type = item.type;
            this.processObject(["processCurrentItemTokens"], widgetModel);
            
            // Create the widgets...
            this.processWidgets(widgetModel, node);
         }
         else
         {
            this.alfLog("error", "Resolved promise did not contain a widget model", resolvedPromise, this);
         }
         
      },

      /**
       * This handles the creation of the widget in the preview panel.
       * 
       * @instance
       */
      creator: function alfresco_dnd_DragAndDropTarget__creator(item, /*jshint unused:false*/ hint) {
         var node = domConstruct.create("div");
         var clonedItem = lang.clone(item);
         if (clonedItem.value !== null && clonedItem.value !== undefined)
         {
            if (this.useModellingService === true)
            {
               this.createViaService(clonedItem, node);
            }
            else
            {
               var widgetModel = lang.clone(this.widgetsForWrappingDroppedItems);
            
               // Not sure this is the ideal solution, maybe currentItem shouldn't be abused like this?
               this.currentItem = {};

               // Set the value to be processed...
               this.currentItem.value = clonedItem.value;
               this.currentItem.label = clonedItem.label || clonedItem.value.label;
               this.currentItem.type = clonedItem.type;

               // Check to see if a specific widget model has been requested for rendering the dropped item...
               // TODO Not sure that we actually care about this yet? If at all... shouldn't everything be part of the value?
               //      This might give flexibility though...
               if (clonedItem.widgets !== null && clonedItem.widgets !== undefined)
               {
                  this.currentItem.widgets = clonedItem.widgets;
               }
               else
               {
                  this.currentItem.widgets = lang.clone(this.widgetsForDroppedItems);
               }

               // TODO: processInstanceTokens needs an update to substitute entire objects for strings
               //       e.g. the value should be an object and not a string
               this.processObject(["processCurrentItemTokens"], widgetModel);
               
               // Create the widgets...
               this.processWidgets(widgetModel, node);
            }
         }

         // NOTE that the node returned is the firstChild of the element we created. This is because the widget is created
         // as a child of the node we passed
         return {node: node.firstChild, data: clonedItem, type: clonedItem.type};
      },
      
      /**
       * An optional property that the target represents. This is expected to be used when a target is 
       * nested within the display of another dropped item.
       *
       * @instance
       * @type {string}
       * @default
       */
      targetProperty: null,

      /**
       * Iterates over all the dropped nodes, finds the widget associated with each node and then calls
       * that widgets getValue function (if it has one). The resulting values are all pushed into an
       * array that is then returned.
       * 
       * @instance
       * @returns {array} The array of values represented by the dropped items.
       */
      getValue: function alfresco_dnd_DragAndDropTarget__getValue() {
         var value = [];
         var nodes = this.previewTarget.getAllNodes();
         array.forEach(nodes, function(node) {
            // Get the widgets for the node...
            var widget = registry.byNode(node);
            if (widget && typeof widget.getValue === "function")
            {
               value.push(widget.getValue());
            }
         }, this);
         return value;
      },

      /**
       * This will render new items for each value in the element array provided.
       * 
       * @instance
       * @param {object} value The value to set.
       */
      setValue: function alfresco_dnd_DragAndDropTarget__setValue(value) {
         if (value !== undefined && value !== null && value !== "")
         {
            // See AKU-938 which resulted in ensuring that publications are always published
            // in the correct order. Prior to this change it was possible for publications made
            // to the modelling service to "jump the queue" so that they were processed synchronously.
            // However, since correcting publication order behaviour it is essential that we make
            // sure that the publication queue is fully emptied otherwise we need to wait for 
            // the page to finish loading (as this indicates that the publication queue has been emptied)
            if (!this.useModellingService || PubQueue.getSingleton()._unreleasedEmptied)
            {
               this.createDndNodesForValue(value);
            }
            else
            {
               // Set up a subcription for the page readiness...
               var handle = this.alfSubscribe(topics.PAGE_WIDGETS_READY, lang.hitch(this, function() {
                  this.alfUnsubscribe(handle);
                  this.createDndNodesForValue(value);
               }));
            }
         }
      },

      /**
       * Create the drag-and-drop nodes as required for the supplied value.
       * 
       * @instance
       * @param {object} value The value to set.
       * @since 1.0.65
       */
      createDndNodesForValue: function alfresco_dnd_DragAndDropTarget__createDndNodesForValue(value) {
         array.forEach(value, function(item) {
            var data = {
               type: lang.clone(item.type),
               value: item
            };
            var createdItem = this.creator(data);
            this.previewTarget.insertNodes(true, [createdItem.data]);
            this.alfPublish(Constants.itemAddedTopic, createdItem.data);
         }, this);

         this.previewTarget.selectNone();
      },
      
      /**
       * Handles requests to delete a previously dropped item.
       * 
       * @instance
       * @param {object} evt The event.
       */
      deleteItem: function alfresco_dnd_DragAndDropTarget__deleteItem(evt) {
         this.alfLog("log", "Delete widget request detected", evt);
         if (evt.target && 
             evt.target.id &&
             this.previewTarget.getItem(evt.target.id) &&
             evt.targetWidget) 
         {
            evt.targetWidget.destroyRecursive(false);
            // TODO: This is destroying the wrong widget - the wrapper, not the DroppedItem
            this.previewTarget.delItem(evt.target.id);
            
            // If the last item has just been deleted the add the dashed border back...
            if (this.previewTarget.getAllNodes().length === 0)
            {
               domClass.remove(this.previewNode, "containsItems");
            }
            // Emit the event to alert wrapping widgets to changes...
            on.emit(this.domNode, Constants.updateItemsEvent, {
               bubbles: true,
               cancelable: true,
               targetWidget: this,
               deleteIndex: evt.deleteIndex
            });
         }
      },

      /**
       * Although this function's name suggests it handles an nodes selection, there is no guarantee
       * that a node has actually been selected. This is simply attached to the mouseDown event.
       * 
       * @instance
       * @param {object} evt The selection event
       */
      onWidgetSelected: function alfresco_dnd_DragAndDropTarget__onWidgetSelected(/*jshint unused:false*/evt) {
         var selectedNodes = this.previewTarget.getSelectedNodes();
         if (selectedNodes.length > 0 && selectedNodes[0] !== null)
         {
            var selectedItem = this.previewTarget.getItem(selectedNodes[0].id);
         }
      },

      /**
       * This function is called after a new item is dropped onto the target or when items are deleted from it
       *
       * @instance
       */
      onItemsUpdated: function alfresco_dnd_DragAndDropTarget__onItemsUpdated() {
         domClass.remove(this.previewNode, "alfresco-dnd-DragAndDropTarget--over");
         on.emit(this.domNode, Constants.updateItemsEvent, {
            bubbles: true,
            cancelable: true,
            targetWidget: this
         });

         // NOTE: This is needed to ensure that form controls are rendered correctly
         //       after being dropped onto the page...
         this.alfPublish("ALF_WIDGET_PROCESSING_COMPLETE", {}, true);
      },

      /**
       * Handles key presses when the drop target has focus. If the key pressed is the ENTER key
       * then a request will be published to request an item to insert. This is expected to be
       * the currently selected item in a [DragAndDropItems]{@link module:alfresco/dnd/DragAndDropItems}
       * widget.
       * 
       * @instance
       * @param {object} evt The keypress event
       */
      onKeyPress: function alfresco_dnd_DragAndDropTarget__onKeyPress(evt) {
         if (evt.charOrCode === keys.ENTER)
         {
            evt && Event.stop(evt);
            var promise = new Deferred();
            promise.then(lang.hitch(this, this.addItem));
            this.alfPublish(Constants.requestItemToAddTopic, {
               promise: promise
            });
         }
      },

      /**
       * Inserts a new item provided by a resolved promise.
       *
       * @instance
       * @param {promise} resolvedPromise A resolved promise that is expected to contain an item to insert
       */
      addItem: function alfresco_dnd_DragAndDropTarget__addItem(resolvedPromise) {
         if (resolvedPromise.item)
         {
            var createdItem = this.creator(resolvedPromise.item);
            this.previewTarget.insertNodes(true, [createdItem.data]);
            if (typeof resolvedPromise.addCallback === "function")
            {
               resolvedPromise.addCallback.call(resolvedPromise.addCallbackScope || this, resolvedPromise.item);
            }
            this.onItemsUpdated();
         }
      },

      /**
       * Called whenever an item is dragged out of the current drop target. This is emits an event
       * to indicating that this has occurred to enabled nested drop targets to be supported.
       * 
       * @instance
       */
      onItemDraggedOver: function alfresco_dnd_DragAndDropTarget__onItemDraggedOver() {
         domClass.add(this.previewNode, "alfresco-dnd-DragAndDropTarget--over");
      },

      /**
       * Called whenever an item is dragged out of the current drop target. This is emits an event
       * to indicating that this has occurred to enabled nested drop targets to be supported.
       * 
       * @instance
       */
      onItemDraggedOut: function alfresco_dnd_DragAndDropTarget__onItemDraggedOut() {
         domClass.remove(this.previewNode, "alfresco-dnd-DragAndDropTarget--over");
      },

      /**
       * This function is called when a [DragAndDropNestedTarget]{@link module:alfresco/dnd/DragAndDropNestedTarget}
       * has an item dragged out of it. This function will then call the Dojo dojo.dnd.Manager singleton to let it know
       * that the item is currently over the current target. This is required because the Dojo drag and drop framework
       * that is used does not support nested targets.
       *
       * @instance 
       * @param {object} evt The drag out event
       */
      onNestedDragOut: function alfresco_dnd_DragAndDropTarget__onNestedDragOut(evt) {
         domClass.add(this.previewNode, "alfresco-dnd-DragAndDropTarget--over");
         evt && Event.stop(evt);
         var m = DndManager.manager();
         m.overSource(this.previewTarget);
      },

      /**
       * This function is called when a [DragAndDropNestedTarget]{@link module:alfresco/dnd/DragAndDropNestedTarget}
       * has an item dragged over it. This then updates the CSS classes to indicate that the item is no longer
       * the current target (because the nested target is the current target).
       *
       * @instance 
       * @param {object} evt The drag out event
       */
      onNestedDragOver: function alfresco_dnd_DragAndDropTarget__onNestedDragOver(evt) {
         // jshint unused:false
         domClass.remove(this.previewNode, "alfresco-dnd-DragAndDropTarget--over");
      }
   });
});