Source: dnd/DroppedNestingItemWrapper.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 is an extension of the basic [DroppedItemWrapper]{@link module:alfresco/dnd/DroppedItemWrapper}
 * and should be used when the item dropped can contain one or more nested 
 * [DragAndDropTargets]{@link module:alfresco/dnd/DragAndDropTarget}.
 * 
 * @module alfresco/dnd/DroppedNestingItemWrapper
 * @extends module:alfresco/dnd/DroppedItemWrapper
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/dnd/DroppedItemWrapper",
        "alfresco/dnd/Constants",
        "alfresco/core/ObjectTypeUtils",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/on",
        "dojo/Deferred",
        "dijit/registry",
        "jquery"], 
        function(declare, DroppedItemWrapper, Constants, ObjectTypeUtils, lang, array, on, Deferred, registry, $) {
   
   return declare([DroppedItemWrapper], {
      
      /**
       * Extends the [inherited function]{@link module:alfresco/dnd/DroppedItemWrapper#postCreate}
       * to listen to events generated when an item is dropped into a nested target.
       * 
       * @instance
       */
      postCreate: function alfresco_dnd_DroppedNestingItemWrapper__postCreate() {
         // Provide any additional configuration that the nested item might use...
         var promise = new Deferred();
         promise.then(lang.hitch(this, this.onNestedConfig));
         this.alfPublish(Constants.requestWidgetsForNestedConfigTopic, {
            value: this.value,
            promise: promise
         });

         this.inherited(arguments);
         on(this.domNode, Constants.updateItemsEvent, lang.hitch(this, this.onNestedTargetUpdated));
         on(this.domNode, Constants.reorderItemsEvent, lang.hitch(this, this.onNestedTargetReordered));
      },

      /**
       * Updates the display items with the nested widget configuration model. The widgets processed are expected
       * to be [DragAndDropNestedTarget]{@link module:alfresco/dnd/DragAndDropNestedTarget} widgets that will be
       * able to pass this additional configuration onto the items that are dropped into them.
       *
       * @instance
       * @param {array} widgets The widgets processed.
       */
      allWidgetsProcessed: function alfresco_dnd_DroppedNestingItemWrapper__allWidgetsProcessed(widgets) {
         this.inherited(arguments);
         array.forEach(widgets, function(widget) {
            widget.widgetsForNestedConfig = this._widgetsForNestedConfig;

            // Ensure that nested items are rendered if values are available for them...
            if (widget.targetProperty)
            {
               var value = lang.getObject(widget.targetProperty, false, this.value);
               if (value && typeof widget.setValue === "function")
               {
                  widget.setValue(value);
               }
            }
         }, this);
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/dnd/DroppedItemWrapper#onItemDelete} to to iterate
       * over all the nested items an individually delete them to ensure that any items that are configured for
       * single use only are ultimately returned to the [DragAndDropItems]{@link module:alfresco/dnd/DragAndDropItems}
       * widget from which they were originally sourced.
       *
       * @instance
       * @param {object} evt The deletion event
       */
      onItemDelete: function alfresco_dnd_DroppedNestingItemWrapper__onItemDelete(/* jshint unused:false */ evt) {
         // Need to get the index of the node BEFORE we delete it (otherwise it'll lost when we remove the node,
         // and the index is the best way of knowing what to delete from the underlying data model)...
         var deleteIndex = $(this.domNode).index();

         // Now remove the item...
         array.forEach(this._renderedWidgets, function(widget) {
            if (widget.previewTarget && typeof widget.previewTarget.getAllNodes === "function")
            {
               var nodes = widget.previewTarget.getAllNodes.call(widget.previewTarget);
               array.forEach(nodes, function(node) {
                  var w = registry.byNode(node);
                  if (w && typeof w.onItemDelete === "function") {
                     w.onItemDelete.call(w);
                  }
               });
            }
         });

         // We're deliberately NOT going to call the inherited function here, instead we'll ALMOST (but not quite)
         // duplicate it, because we want to include the deleteIndex we retrieved earlier...
         on.emit(this.domNode, Constants.deleteItemEvent, {
            bubbles: true,
            cancelable: true,
            targetWidget: this,
            deleteIndex: deleteIndex
         });
         this.alfPublish(Constants.itemDeletedTopic, {
            item: this.getValue()
         });
      },

      /**
       * Handles items being dropped into nested targets.
       * 
       * @instance
       * @param {object} evt The drop event.
       */
      onNestedTargetUpdated: function alfresco_dnd_DroppedNestingItemWrapper__onNestedTargetUpdated(evt) {
         if (evt.targetWidget === this)
         {
            // We've managed to catch our own emitted event, allow bubbling to continue...
         }
         else
         {
            if (typeof evt.stopPropagation === "function")
            {
               evt.stopPropagation();
            }
            if (typeof evt.preventDefault === "function")
            {
               evt.preventDefault();
            }

            // Attempt to mix the updated target value into the wrapped value...
            if (evt.targetWidget)
            {
               if (evt.targetWidget.targetProperty && typeof evt.targetWidget.getValue === "function")
               {
                  var listToReorder = lang.getObject(evt.targetWidget.targetProperty, false, this.value);
                  if (!ObjectTypeUtils.isArray(listToReorder))
                  {
                     // The target value will usually be an array, but just in case - just set the value as given...
                     lang.setObject(evt.targetWidget.targetProperty, evt.targetWidget.getValue(), this.value);
                     this.notifyParentOfChange();
                  }
                  else if (!isNaN(evt.index))
                  {
                     // Set the supplied index...
                     listToReorder[evt.index] = evt.targetWidget.getValue();
                     this.notifyParentOfChange();
                  }
                  else if (!isNaN(evt.deleteIndex))
                  {
                     // Delete an item...
                     listToReorder.splice(evt.deleteIndex, 1);
                     this.notifyParentOfChange();
                  }
                  else
                  {
                     // If all else fails, then we're just going to try and work out the new value based on the current
                     // value of the target widgets preview target. This is expected to be the case when items are dragged
                     // around in a target...
                     var value = [];
                     array.forEach(evt.targetWidget.previewTarget.getAllNodes(), function(node) {
                        var widget = registry.getEnclosingWidget(node);
                        if (widget && typeof widget.getValue === "function")
                        {
                           value.push(widget.getValue());
                        }
                     }, this);
                     lang.setObject(evt.targetWidget.targetProperty, value, this.value);
                     this.notifyParentOfChange();
                  }
               }
               else
               {
                  this.alfLog("warn", "The 'targetWidget' has no 'targetProperty' or no 'getValue' function", evt.targetWidget, this);
               }
            }
            else
            {
               this.alfLog("warn", "The update event contains no 'targetWidget' attribute", evt, this);
            }
         }
      },

      /**
       * Handles items in a target being reordered
       * 
       * @instance
       * @param {object} evt The drop event.
       */
      onNestedTargetReordered: function alfresco_dnd_DroppedNestingItemWrapper__onNestedTargetReordered(evt) {
         if (evt.targetWidget === this)
         {
            // We've managed to catch our own emitted event, allow bubbling to continue...
         }
         else
         {
            if (typeof evt.stopPropagation === "function")
            {
               evt.stopPropagation();
            }
            if (typeof evt.preventDefault === "function")
            {
               evt.preventDefault();
            }

            // Attempt to mix the updated target value into the wrapped value...
            if (evt.targetWidget && evt.targetWidget.targetProperty)
            {
               var listToReorder = lang.getObject(evt.targetWidget.targetProperty, false, this.value);
               if (!ObjectTypeUtils.isArray(listToReorder))
               {
                  this.alfLog("warn", "Can't reorder because the targetProperty does not exist or is not an array", evt.targetWidget.targetProperty, this.value, this);
               }
               else if (isNaN(evt.oldIndex) || isNaN(evt.newIndex))
               {
                  this.alfLog("warn", "Can't reorder because either the oldIndex or the newIndex (or both) are missing or are not numbers", evt, this);
               }
               else
               {
                  // Do the re-ordering...
                  var tmp1 = listToReorder[evt.oldIndex];
                  var tmp2 = listToReorder[evt.newIndex];
                  listToReorder[evt.oldIndex] = tmp2;
                  listToReorder[evt.newIndex] = tmp1;

                  // ...then emit an event to capture the new value...
                  this.notifyParentOfChange();
               }
            }
            else
            {
               this.alfLog("warn", "The update event contains no 'targetWidget' attribute", evt, this);
            }
         }
      },

      /**
       * This function can be called to update wrapping widgets that the current item has changed.
       *
       * @instance
       */
      notifyParentOfChange: function alfresco_dnd_DroppedNestingItemWrapper__notifyParentOfChange() {
         on.emit(this.domNode, Constants.updateItemsEvent, {
            bubbles: true,
            cancelable: true,
            targetWidget: this,
            index: $(this.domNode).index()
         });
      },

      /**
       * Retrieves additional configuration data from a [DndModellingService]{@link module:alfresco/services/DragAndDropModellingService}
       * that can then be passed onto the child widget.
       * 
       * @instance
       * @param {object} targetWidget The target widget to update with the additional configuration
       * @param {promise} resolvedPromise A resolved promise that is expected to contain a widgets array
       */
      onNestedConfig: function alfresco_dnd_DroppedNestingItemWrapper__onNestedConfig(resolvedPromise) {
         if (resolvedPromise.widgets)
         {
            this._widgetsForNestedConfig = resolvedPromise.widgets;
         }
         else
         {
            this.alfLog("warn", "Wigets were not provided in the resolved promise", resolvedPromise, this);
         }
      },

      /**
       * Extends the inherited function to add in any nested configuration widgets.
       * 
       * @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_DroppedNestingItemWrapper__onEditConfig(item, resolvedPromise) {
         if (resolvedPromise.widgets && this.widgetsForNestedConfig)
         {
            resolvedPromise.widgets = this.widgetsForNestedConfig.concat(resolvedPromise.widgets);
         }
         this.inherited(arguments);
      }
   });
});