Source: creation/DropZone.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/>.
 */

/**
 * @module alfresco/creation/DropZone
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/DropZone.html",
        "alfresco/core/Core",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dijit/registry",
        "dojo/dnd/Source",
        "dojo/dnd/Target",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/aspect",
        "alfresco/creation/DropZoneWrapper",
        "dojo/on"], 
        function(declare, _Widget, _Templated, template, AlfCore, lang, array, registry, Source, Target, domConstruct, domClass, aspect, DropZoneWrapper, on) {
   
   return declare([_Widget, _Templated, AlfCore], {
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {Array}
       */
      cssRequirements: [{cssFile:"./css/DropZone.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,

      /**
       * @instance
       */
      postCreate: function alfresco_creation_DropZone__postCreate() {
         /*jshint eqnull:true*/
         if (this.acceptTypes == null)
         {
            this.acceptTypes = ["widget"];
         }
         
         this.previewTarget = new Source(this.previewNode, { 
            accept: this.acceptTypes,
            creator: lang.hitch(this, "creator"),
            withHandles: true,
            horizontal: this.horizontal
         });

         // Create a new UUID to pass on to the widgets that are dropped into this instance
         // this is done so that this instance can subscribe to requests from it's direct dropped items
         this.childPubSubScope = this.generateUuid();
         
         // Capture wrappers being selected...
         aspect.after(this.previewTarget, "onMouseDown", lang.hitch(this, "onWidgetSelected"), true);
         
         // Capture widgets being dropped...
         aspect.after(this.previewTarget, "onDrop", lang.hitch(this, "refreshChildren"), true);
         
         // When additional nodes are created as a result of dropping them into the preview target it
         // will be necessary to publish the details of the available fields. This is done for the benefit
         // of any controls that are dependent upon that information.
         aspect.after(this.previewTarget, "insertNodes", lang.hitch(this, "publishAvailableFields"), true);
         
         // Subscribe to events containing data to re-render and existing item...
         this.alfSubscribe("ALF_UPDATE_RENDERED_WIDGET", lang.hitch(this, "updateItem"));
         
         // Subscribe to requests to publish the details of the fields that are available...
         this.alfSubscribe(this.childPubSubScope + "ALF_REQUEST_AVAILABLE_FORM_FIELDS", lang.hitch(this, "publishAvailableFields"), true);
         
         // Listen for widgets requesting to be deleted...
         on(this.previewNode, "onWidgetDelete", lang.hitch(this, "deleteItem"));
         
         // Add in any items that are included as instantiation arguments...
         // These would be included when a DropZone is created as the display
         // widget of a widget with children. The children will be the items to add.
         if (this.initialItems != null)
         {
            // var items = [];
            // array.forEach(this.initialItems, function (item, index) {
            //    items.push(this.alfGetData(item));
            // }, this);
            this.previewTarget.insertNodes(false, this.initialItems, false, null);
            // this.previewTarget.insertNodes(false, items, false, null);
         }

         if (this.value != null && this.value !== "")
         {
            array.forEach(this.value, function(widget) {
               var data = {
                  name: widget.name,
                  module: widget.module,
                  defaultConfig: widget.defaultConfig,
                  widgetsForDisplay: widget.widgetsForDisplay,
                  widgetsForConfig: widget.widgetsForConfig
               };
               var dndData = this.creator(data);
               this.previewTarget.insertNodes(true, [dndData.data]);
            }, this);
         }
      },
      
      /**
       * @instance
       * @returns {object[]} The currently available fields.
       */
      getAvailableFields: function alfresco_creation_DropZone__getAvailableFields() {
         /*jshint eqnull:true*/
         var currentFields = [];
         for (var key in this.previewTarget.map)
         {
            if (this.previewTarget.map.hasOwnProperty(key))
            {
               var currentField = this.previewTarget.map[key];
               
               // Get the field name to use as the label (this can change) and the id (which should remain
               // static once created) to populate an individual option.
               var fieldName = lang.getObject("updatedConfig.defaultConfig.name", false, currentField.data);
               if (fieldName == null)
               {
                  fieldName = lang.getObject("defaultConfig.name", false, currentField.data);
               }
               var fieldId = lang.getObject("updatedConfig.defaultConfig.fieldId", false, currentField.data);
               if (fieldId == null)
               {
                  fieldId = lang.getObject("defaultConfig.fieldId", false, currentField.data);
               }
               currentFields.push({
                  label: fieldName,
                  value: fieldId
               });
            }
         }
         return currentFields;
      },
      
      /**
       * Publishes an array of the names of all of the currently configured fields.
       * 
       * @instance
       */
      publishAvailableFields: function alfresco_creation_DropZone__publishAvailableFields() {
         var payload = {};
         payload.options = this.getAvailableFields();
         this.alfLog("log", "Publishing available fields:", payload, this);
         this.alfPublish(this.childPubSubScope + "ALF_FORM_FIELDS_UPDATE", payload, true);
      },
      
      /**
       * @instance
       * @param {object} evt The event.
       */
      deleteItem: function alfresco_creation_DropZone__deleteItem(evt) {
         /*jshint eqnull:true*/
         this.alfLog("log", "Delete widget request detected", evt);
         
         if (evt.target != null && 
             evt.target.id != null &&
             this.previewTarget.getItem(evt.target.id) != null &&
             evt.widgetToDelete != null) 
         {
            var target = this.previewTarget.getItem(evt.target.id);
            var fieldId = lang.getObject("data.defaultConfig.fieldId", false, target),
                parentId = lang.getObject("data.parentId", false, target);
            if (fieldId != null && parentId != null)
            {
               // Remove all references in parent...
               this.removeReferencesFromParent(parentId, fieldId);

               // Delete from data model - setting to null is good enough...
               this.alfSetData(fieldId, null);
            }

            evt.widgetToDelete.destroyRecursive(false);
            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");
            }

            // Request that the any config display is cleared.
            // TODO: Currently this doesn't specify the item, we might not want to delete if the deleted item isn't currently selected.
            // Could check to see if it is the selected item? Or include item details in payload.
            this.alfPublish("ALF_CLEAR_CONFIGURE_WIDGET", {});
            
            // Emit the event to alert wrapping widgets to changes...
            this.refreshChildren();
            
            this.publishAvailableFields();
         }
      },
      
      /**
       * Handles updates to the configuration for a currently rendered item.
       * 
       * @instance
       */
      updateItem: function alfresco_creation_DropZone__updateItem(payload) {
         /*jshint eqnull:true,maxstatements:false,maxcomplexity:false*/
         // Check that the updated item belongs to this DropZone instance (this is done to prevent
         // multiple DropZone instances trying to modify the wrong objects)...
         if (payload.node != null)
         {
            var myNode = array.some(this.previewTarget.getAllNodes(), function(node) {
               return payload.node.id === node.id;
            });
            if (myNode === true)
            {
               this.alfLog("log", "Updating item", payload);

               // TODO: Potentially fragile...
               var fieldId = payload.originalConfig.defaultConfig.fieldId;

               // Get the configuration for the widget that has been updated...
               var config = this.alfGetData(fieldId);
               if (config != null)
               {
                  // Get the keys to use to define the object (typically these are just "name" and "config")...
                  // However, they can be different, e.g. when defining publication data (i.e. "publishTopic" and "publishPayload")...
                  var itemNameKey = (config.itemNameKey != null) ? config.itemNameKey : "name",
                      itemConfigKey = (config.itemConfigKey != null) ? config.itemConfigKey : "config";

                  // Create a new widget definition object (overwriting the previous data if necessary)...
                  // Use the custom keys as necessary...
                  config.widgetConfig = {};
                  config.widgetConfig[itemNameKey] = config.module;
                  config.widgetConfig[itemConfigKey] = {};

                  // We need to keep a separate record of the updated config...
                  config.updatedConfig = {
                     defaultConfig: {},
                     additionalConfig: {}
                  };

                  // Set the main config...
                  var v;
                  for (var key in payload.updatedConfig.defaultConfig)
                  {
                     if (payload.updatedConfig.defaultConfig.hasOwnProperty(key))
                     {
                        v = payload.updatedConfig.defaultConfig[key];
                        config.widgetConfig[itemConfigKey][key] = v;
                        lang.setObject(key, v, config.updatedConfig.defaultConfig);
                     }
                  }

                  // Set additional config...
                  for (key in payload.updatedConfig.additionalConfig)
                  {
                     if (payload.updatedConfig.additionalConfig.hasOwnProperty(key))
                     {
                        v = payload.updatedConfig.additionalConfig[key];
                        config.widgetConfig[key] = v;
                        lang.setObject(key, v, config.updatedConfig.additionalConfig);
                     }
                  }

                  array.forEach(config.widgetsForConfig, lang.hitch(this, this.updateWidgetConfig, payload.updatedConfig));
                  // for (var i=0; i<config.widgetsForConfig.length; i++)
                  // {
                  //    // clonedConfig.widgetsForConfig[i].config.value = payload.updatedConfig[clonedConfig.widgetsForConfig[i].config.name];
                  //    if (config.widgetsForConfig[i].config.name)
                  //    {
                  //       this.updateWidgetConfig(payload.updatedConfig, config.widgetsForConfig[i], 0);
                  //       // config.widgetsForConfig[i].config.value = lang.getObject(config.widgetsForConfig[i].config.name, false, payload.updatedConfig);
                  //    }
                  // }
               }
               
               // Remove any existing widgets associated with the currently selected node,
               // however preserve the DOM so that it can be used as a reference for adding
               // the replacement widget (it will be removed afterwards)...
               array.forEach(this.previewTarget.getSelectedNodes(), function(node) {
                  var widget = registry.byNode(node);
                  if (widget)
                  {
                     widget.destroyRecursive(true);
                  }
               }, this);
               
               try
               {
                  // Create the updated object...
                  this.previewTarget.insertNodes(false, [config], true, payload.node);
               }
               catch(e)
               {
                  this.alfLog("log", "Error", e);
               }
               finally
               {
                  this.alfLog("log", "Finally");
               }
               
               // Remove the previous nodes...
               this.previewTarget.deleteSelectedNodes();

               // Emit the event to alert wrapping widgets to changes...
               this.refreshChildren();
               
               // Publish the details of the latest fields...
               this.publishAvailableFields();
            }
         }
      },
      
      /**
       * 
       * @instance
       * @param {string} parentId The ID of the parent to remove the child from
       * @param {string} childIdToRemove The ID of the child to remove
       */
      removeReferencesFromParent: function alfresco_creation_DropZone__removeReferencesFromParent(parentId, childIdToRemove) {
         /*jshint eqnull:true*/
         if (parentId != null)
         {
            // The current item already has a parent - this means that we're doing a move...
            // Therefore we need to remove this as a child of the old parent...
            var oldParentConfig = this.alfGetData(parentId);
            if (oldParentConfig != null)
            {
               // Filter out the current child...
               // TODO: This should ONLY be done on drop - not drag !!
               oldParentConfig.children = array.filter(oldParentConfig.children, function(childId) {
                  return childId !== childIdToRemove;
               }, this);
            }
            else
            {
               this.alfLog("warn", "Expected to find parentId in the data model", parentId, this);
            }
         }
      },

      /**
       * This handles the creation of the widget in the preview panel.
       * 
       * @instance
       */
      creator: function alfresco_creation_DropZone__creator(item, hint) {
         /*jshint maxcomplexity:false,maxstatements:false,eqnull:true*/
         this.alfLog("log", "Creating", item, hint);
         
         var node = domConstruct.create("div");
         if (item.module != null && item.module !== "")
         {
            // Clone the supplied item... there are several potential possibilities for this
            // creator being called. Either an avatrar is required (should actually be handled
            // separately) a new field is being created or an existing field is being moved.
            // It's important that we create a fieldId if not defined (e.g. when creating a new
            // field) but preserve any existing values.
            var clonedItem = lang.clone(item); 
            var config = (clonedItem.defaultConfig != null) ? clonedItem.defaultConfig : {};
            if (config.fieldId === undefined)
            {
               config.fieldId = this.generateUuid();
            }
            
            // Preview the widget within the wrapper if requested or if no widgetsForDisplay
            // configuration has been provided...
            var widgets = null;
            var savedConfig = this.alfGetData(config.fieldId);
            if (savedConfig != null)
            {
               widgets = savedConfig.widgetsForDisplay;
            }
            else
            {
               // Initialise the widget config...
               // Create the initial widget config (this will be used if the widget is not changed from the defaults)...
               var itemNameKey = (clonedItem.itemNameKey != null) ? clonedItem.itemNameKey : "name",
                   itemConfigKey = (clonedItem.itemConfigKey != null) ? clonedItem.itemConfigKey : "config";
               clonedItem.widgetConfig = {};
               clonedItem.widgetConfig[itemNameKey] = clonedItem.module;
               clonedItem.widgetConfig[itemConfigKey] = {};
               for (var key in clonedItem.defaultConfig)
               {
                  if (clonedItem.defaultConfig.hasOwnProperty(key))
                  {
                     var v = clonedItem.defaultConfig[key];
                     clonedItem.widgetConfig[itemConfigKey][key] = v;
                  }
               }

               // Initialise widgets for display...
               if (clonedItem.previewWidget === true || 
                   clonedItem.widgetsForDisplay == null ||
                   clonedItem.widgetsForDisplay.length === 0)
               {
                  widgets = [
                     {
                        name: clonedItem.module,
                        config: config
                     }
                  ];
               }
               else
               {
                  widgets = clonedItem.widgetsForDisplay;
               }
            }

            // Add in any additional configuration...
            if (this.widgetsForNestedConfig != null)
            {
               // Check to see if the item passed in has the "originalConfigWidgets" attribute set...
               // If it doesn't then this is a create triggered by dragging from the palette. If it has
               // the attribute set then this call has been triggered by an update...
               if (clonedItem.originalConfigWidgets === undefined)
               {
                  clonedItem.originalConfigWidgets = lang.clone(clonedItem.widgetsForConfig);
               }
               
               var clonedWidgetsForNestedConfig = lang.clone(this.widgetsForNestedConfig);
               if (savedConfig != null && savedConfig.updatedConfig != null)
               {
                  // Update the normal config values with the latest saved data...
                  array.forEach(clonedItem.originalConfigWidgets, function(widget) {
                     if (widget.config.name)
                     {
                        this.updateWidgetConfig(savedConfig.updatedConfig, widget, 0);
                        // var updatedValue = lang.getObject(widget.config.name, false, savedConfig.updatedConfig);
                        // if (updatedValue != null)
                        // {
                        //    widget.config.value = updatedValue;
                        // }
                     }
                  }, this);

                  // Update the additional config controls with the latest saved data...
                  array.forEach(clonedWidgetsForNestedConfig, lang.hitch(this, this.updateWidgetConfig, savedConfig.updatedConfig.additionalConfig));
                  // array.forEach(clonedWidgetsForNestedConfig, function(widget, i) {
                  //    if (widget.config.name)
                  //    {
                  //       var updatedValue = lang.getObject(widget.config.name, false, savedConfig.updatedConfig.additionalConfig);
                  //       if (updatedValue != null)
                  //       {
                  //          widget.config.value = updatedValue;
                  //       }
                  //    }
                  // }, this);
               }
               else
               {
                  // Make sure that each of the additional widgets is set with an up-to-date value...
                  clonedItem.widgetsForConfig = clonedItem.originalConfigWidgets.concat(clonedWidgetsForNestedConfig);
                  array.forEach(clonedItem.widgetsForConfig, lang.hitch(this, this.updateWidgetConfig, clonedItem));
                  // array.forEach(clonedItem.widgetsForConfig, function(widget, i) {
                  //    if (widget && widget.config && widget.config.name)
                  //    {
                  //       var updatedValue = lang.getObject(widget.config.name, false, clonedItem);
                  //       if (updatedValue != null)
                  //       {
                  //          widget.config.value = updatedValue;
                  //       }
                  //    }
                  // }, this);
               }
            }

            // Update the pubSubScope so that they can request available fields on the correct pubSubScope...
            array.forEach(clonedItem.widgetsForConfig, function(widget) {
               lang.setObject("config.pubSubScope", this.childPubSubScope, widget);
            }, this);

            // Update the configuration to set the ID of the parent...
            // The ID is either that of the parent DropZoneWrapper or the actual ID if there is no DropZoneWrapper
            // There won't be a DropZoneWrapper if this is the root DropZone (e.g. one created by a DropZoneFormControl)...
            var myUuid = this.getMyUuid();

            // Remove any references to the current widget from it's previous parent...
            this.removeReferencesFromParent(clonedItem.parentId, config.fieldId);

            // // Set a reference to the parent (if this is a move event then we're just updating the parent)...
            clonedItem.parentId = myUuid;
            // TODO: Re-subscribe to events such as available fields?
            
            // Get MY configuration from the data model...
            var myConfig = this.alfGetData(myUuid);
            if (myConfig == null)
            {
               // It's possible that configuration won't exist for root DropZones...
               this.alfLog("warn", "No entry in the data model for DropZone!", this);
            }
            else
            {
               // Make sure that there is configuration set for dropped child nodes...
               if (myConfig.children == null)
               {
                  myConfig.children = [];
               }
               myConfig.children.push(config.fieldId);
            }

            // Store the field id against the updated config...
            // This needs to be saved so it can be retrieved when inserting nodes again...
            this.alfSetData(config.fieldId, clonedItem);
            
            var widgetWrapper = new DropZoneWrapper({
               fieldId: config.fieldId,
               parentPubSubScope: this.childPubSubScope,
               pubSubScope: this.pubSubScope,
               widgets: widgets,
               moduleName: clonedItem.name
            }, node);
         }
         else
         {
            this.alfLog("log", "The requested item to create was missing a 'module' attribute", item, this);
         }
         
         // Add a class to indicate that items are present (removes the dashed border)...
         domClass.add(this.previewNode, "containsItems");
         return {node: widgetWrapper.domNode, data: clonedItem, type: ["widget"]};
      },
      
      /**
       *
       *
       * @instance
       * @param {object} configToUpdateFrom The configuration to update the widget config from
       * @param {object} widget The widget to update
       * @param {number} i The index of the widget
       */
      updateWidgetConfig: function alfresco_creation_DropZone__updateValues(configToUpdateFrom, widget, /*jshint unused:false*/ i) {
         /*jshint eqnull:true*/
         if (widget.config.name)
         {
            var updatedValue = lang.getObject(widget.config.name, false, configToUpdateFrom);
            if (updatedValue != null)
            {
               widget.config.value = updatedValue;
            }
         }
         else if (widget.config.widgets)
         {
            array.forEach(widget.config.widgets, lang.hitch(this, this.updateWidgetConfig, configToUpdateFrom));
         }
      },

      /**
       * 
       *
       * @instance
       * @return {string} The UUID of the widget associated with this DropZone.
       */
      getMyUuid: function alfresco_creation_DropZone__getMyUuid() {
         /*jshint eqnull:true*/
         // Update the configuration to set the ID of the parent...
         // The ID is either that of the parent DropZoneWrapper or the actual ID if there is no DropZoneWrapper
         // There won't be a DropZoneWrapper if this is the root DropZone (e.g. one created by a DropZoneFormControl)...
         var myUuid = lang.getObject("_dropZoneWrapperId", false, this);
         if (myUuid == null)
         {
            myUuid = this.id;
         }
         return myUuid;
      },

      /**
       * 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} e The selection event
       */
      onWidgetSelected: function alfresco_creation_DropZone__onWidgetSelected(/*jshint unused:false*/ e) {
         /*jshint eqnull:true*/
         var selectedNodes = this.previewTarget.getSelectedNodes();
         if (selectedNodes.length > 0 && selectedNodes[0] != null)
         {
            var selectedItem = this.previewTarget.getItem(selectedNodes[0].id);
            this.alfLog("log", "Widget selected", selectedItem);
            var payload = {
               pubSubScope: this.childPubSubScope,
               selectedNode: selectedNodes[0],
               selectedItem: selectedItem.data
            };
            this.alfPublish("ALF_CONFIGURE_WIDGET", payload);
         }
      },
      
      /**
       * This function is called as an after aspect of the onDrop function of the DND target. It is used
       * to capture widgets being added to or removed from the DropZone.
       * 
       * @instance
       */
      refreshChildren: function alfresco_creation_DropZone__refreshChildren() {
         /*jshint eqnull:true*/
         this.alfLog("log", "Widgets updated");
         
         var myUuid = this.getMyUuid();
         var myConfig = this.alfGetData(myUuid);

         // It's necessary to ensure that the configuration accurately reflects the order of the
         // widgets in the DropZone. To do this we need to build a map of the id of each dropped
         // item to it's index in the DropZone
         var sourceOrderMap = {};
         var nodes = this.previewTarget.getAllNodes();
         array.forEach(nodes, function(node, i) {
            var item = this.previewTarget.getItem(node.id);
            var id = lang.getObject("data.updatedConfig.defaultConfig.fieldId", false, item);
            if (id == null)
            {
               id = lang.getObject("data.defaultConfig.fieldId", false, item);
            }
            sourceOrderMap[id] = i;
         }, this);

         // We now need to build the actual data represented by the items in the DropZone...
         // We're going to get the data from the data store and then ensure that it's order
         // matches the current order of the nodes representing each data item...
         var items = [];
         var updatedChildren = [];
         array.forEach(myConfig.children, function (item) {
            var itemData = this.alfGetData(item);
            var id = lang.getObject("updatedConfig.defaultConfig.fieldId", false, itemData);
            if (id == null)
            {
               id = lang.getObject("defaultConfig.fieldId", false, itemData);
            }
            var targetIndex = sourceOrderMap[id];
            items[targetIndex] = itemData;
            updatedChildren[targetIndex] = item;
         }, this);
         myConfig.children = updatedChildren;

         // Add the display widgets...
         myConfig.widgetsForDisplay = [
            {
               name: "alfresco/creation/DropZone",
               config: {
                  attributeKey: (this.attributeKey != null) ? this.attributeKey : "widgets",
                  horizontal: this.horizontal,
                  widgetsForNestedConfig: this.widgetsForNestedConfig,
                  initialItems: items
               }
            }
         ];

         // Emit an event that is intended to bubble up to an outer DropZoneControl...
         on.emit(this.domNode, "onWidgetUpdate", {
            bubbles: true,
            cancelable: true
         });
      }
   });
});