HomeDev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Drag-and-drop

Describes how drag-and-drop works in the Optimizely Content Management System (CMS) user interface.

Drag-and-drop (dnd) in Optimizely Content Management System (CMS) uses the "dnd" system in Dojo with some additions. See the Dojo Drag'n'Drop Redux tutorial.

📘

Note

To use the additional functionality in CMS your drag source or target class should implement the CMS shell classes instead of the Dojo base classes (epi/shell/dnd/Source or epi/shell/dnd/Target).

Create sources and targets

The following example is a fully functioning component that both implements a drag source and a drag target.

define("epi/samples/dnd/DndSamples", 
      [
        "dojo/_base/declare",
        "dojo/dom-construct",
        "dojo/when",
        "dijit/_Widget",
        "dijit/_TemplatedMixin",
    
        // Optimizely dnd classes
        "epi/shell/dnd/Source",
        "epi/shell/dnd/Target"
      ],
    
      function (declare, domConstruct, when, _Widget, _TemplatedMixin, Source, Target) 
        {
          return declare([_Widget, _TemplatedMixin], 
            {
              templateString: '\
                <div>\
                  <h2>Source</h2>\
                  <ul dojoAttachPoint="source"></ul>\
                  <h2>Target</h2>\
                  <ul dojoAttachPoint="target"><li>Test</li></ul>\
                </div>',
              //Seems to be a bug when we have an empty list so we just create a dummy list item for the example.
    
              postCreate: function () 
                {
                  this.inherited(arguments);
                  this._setupSource();
                  this._setupTarget();
                },
    
              _setupSource: function () 
                {
                  var source = new Source(this.source, 
                    {
                      creator    : this._createDragItem.bind(this),
                      copyOnly   : true,
                      selfAccept : false,
                      selfCopy   : false
                    });
    
              source.insertNodes(false, 
                [
                  { url: "http://www.fakecompany.com", name: "Fake Company", text: "Company web site", type: ["link"] },
                  { url: "http://www.episerver.com", name: "EPi", text: "EPiServer", type: ["link"] },
                  { url: "http://www.dn.se", name: "DN", text: "Daily news", type: ["link"]}
                ]);
            },
    
            _setupTarget: function () 
              {
                var target = new Target(this.target, 
                  {
                    accept           : ["link", "someothertype"],
                    //Set createItemOnDrop if you're only interested to receive the data, and not create a new node.
                    createItemOnDrop : true
                  });
                  this.connect(target, "onDropData", "_onDropData");
              },
    
            _createDragItem: function (item, hint) 
              {
                var node;
    
                if (hint == "avatar") 
                  {
                    //This node is used when dragging the item.
                    node = domConstruct.create("div", { innerHTML: item.text });
                  } 
                else 
                  {
                    node = domConstruct.create("li", { innerHTML: item.text });
                  }
                return 
                  {
                    "node" : node,
                    "type" : item.type,
                    "data" : item
                  };
              },
    
            _onDropData: function (dndData, source, nodes, copy) 
              {
                //Drop item is an array with dragged items. This example just handles the first item.
                var dropItem = dndData ? (dndData.length ? dndData[0] : dndData) : null;
    
                if (dropItem) 
                  {
                    //The data property might be a deffered so we need to call dojo/when just in case.
                    when(dropItem.data, function (value) 
                      {
                        //Do something with the data, here we just log it to the console.
                        console.log(value);
                      }.bind(this));
                  }
              }
           });
         }
          );

Data types

Because JavaScript is a loosely typed language, you should treat data types as a contract or interface. CMS uses a number of known types for drag and drop, which are described in the following table:

Type nameData format
"epi.cms.pagereference"
"epi.cms.blockreference"
"epi.cms.folderreference"
"epi.cms.contentreference"
A string representation of a content reference, such as 3_122. There are converters that convert specific types (such as page reference) to content references. This means that a drag target that accepts content references also accepts page references but not vice versa.
"fileurl"A URL to any file, such as /myvpp/myfolder/mydocument.pdf.
"imageurl"A URL to an image, such as /myvpp/myfolder/myimage.jpg.
"link"An object with two properties:
- url – the URL to the resource
- text – the inner text for the link
"epi.cms.content.lighturi" (there also are specific versions like "epi.cms.page.lighturi")An object with two properties:
- name – The name of the page
- uri – The string representation of the content uri. This might or might not include version information. For instance epi.cms.contentdata:///8.
"epi.cms.content.light" (there also are specific versions like "epi.cms.page.light")An object with two properties:

- name – The name of the page
- contentLink – The string representation of the content reference without any version information, such as 123.

Type converters

You might want to support drag and drop to or from something for which you do not control the source code. You also might want to convert the dragged data to another format. Because you might specify types as an array, both for the source and the target, specify all types that the source delivers and the target accepts.

  • For a target, you might have to convert the data on acceptance.
  • For the source, this works fine as long as you can create the object and still support several types.

Example:

A dnd source delivers image URLs. It may specify the type as ["imageurl", "fileurl"] because these types expect the object to the a string with the URL to the actual content. This content can be dragged to targets that accept an "imageurl" or a "fileurl". However, if you want to drag this content to a target that accepts a link, the source cannot create an object that can be both an "imageurl" or "fileurl" and a link at the same time, because the link expects a complex object with two properties: url and text.

To handle these kinds of scenarios, CMS added object type converters. These are simple classes that implement a convert method with sourceDataType, targetDataType and data as parameters. You can use a singleton converter registry to register a converter, in the initialization method of your module class, for combinations of conversions:

define([
        ...
        "epi-cms/conversion/ContentLightUriConverter",
        "epi/shell/conversion/ObjectConverterRegistry"
    ], function (
         ...
         ContentLightUriConverter,
         ObjectConverterRegistry
       ) {
         ...
         var converter = new ContentLightUriConverter();
         ObjectConverterRegistry.registerConverter("link", "epi.cms.content.light", converter);
    });

The following example shows how to convert between content dnd formats. In a standard installation, this is used to drag pages from the page tree to a page selector to select a page reference or a TinyMCE editor to create a link to the page.

define([
        "dojo/_base/declare",
        "dojo/when",
        "epi/dependency",
        "epi/UriParser",
        "epi/cms/core/ContentReference"
    ], function (declare, when, dependency, UriParser, ContentReference) {
    
            return declare([], {
                // summary:
                //    Converts data between light weight content types containing an URI as a specifyer and other types.
                // 
                // tags:
                //    public
    
                _pageDataStore: null,
    
                constructor: function(params) {
                    // summary:
                    //    The constructor function
                    // 
                    // params: Object
                    //    The parameters that define the object.
                    // tags:
                    //    public
    
                    this._pageDataStore = this._pageDataStore || dependency.resolve("epi.storeregistry").get("epi.cms.contentdata");
                },
    
                registerConverter: function(/*Object*/registry) {
                    // summary:
                    //    Registers the type conversions that this class supports to the given registry.
                    // 
                    // registry: Object
                    //    The converter registry to add mappings to.
                    registry.registerConverter("epi.cms.page.lighturi", "link", this);
                    registry.registerConverter("epi.cms.page.lighturi", "epi.cms.pagereference", this);
                    registry.registerConverter("epi.cms.block.lighturi", "epi.cms.blockreference", this);
                    registry.registerConverter("epi.cms.folder.lighturi", "epi.cms.folderreference", this);
                    registry.registerConverter("epi.cms.content.lighturi", "epi.cms.contentreference", this);
                    registry.registerConverter("epi.cms.content.lighturi", "epi.cms.content.light", this);
                    registry.registerConverter("epi.cms.page.lighturi", "epi.cms.content.light", this);
                    registry.registerConverter("epi.cms.block.lighturi", "epi.cms.content.light", this);
                    registry.registerConverter("epi.cms.folder.lighturi", "epi.cms.content.light", this);
                },
    
                convert: function(/*String*/sourceDataType, /*String*/targetDataType, /*Object*/data) {
                    // summary:
                    //    Converts data between two types.
                    // 
                    // sourceDataType: String
                    //    The source data type.
                    // 
                    // targetDataType: String
                    //    The target data type.
                    // 
                    // converter: Object
                    //    The class that performs the actual conversion.
                    // tags:
                    //    public
                    if (!data) {
                        return null;
                    } else if (targetDataType === "epi.cms.content.lighturi") {
                        return data;
                    }
                    var uri = new UriParser(data.uri);
                    var link = uri.getId();
                    var versionAgnosticId = new ContentReference(link).createVersionUnspecificReference().toString();
    
                    if (targetDataType === "epi.cms.content.light"
                        || targetDataType === "epi.cms.page.light"
                        || targetDataType === "epi.cms.block.light"
                        || targetDataType === "epi.cms.folder.light") {
                        return { contentLink: versionAgnosticId, name: data.name };
                    } else if (targetDataType === "epi.cms.contentreference"
                        || targetDataType === "epi.cms.pagereference"
                        || targetDataType === "epi.cms.blockreference"
                        || targetDataType === "epi.cms.folderreference") {
                        return versionAgnosticId;
                    }
                    if (targetDataType === "link") {
                        //should only be available for pages right now but allow anything for future types as files etc.
                        return when(this._pageDataStore.get(versionAgnosticId), function(page) {
                            return { url: page.properties.pageLinkURL, text: page.name };
                        });
                    }
                }
            });
        });