Introduction

The Drag and Drop (dnd) module is responsible for the Scald UX. It is a generic module to create the drag and drop experience between a library and target fields. Library module must have a specified interface (see below), contains an atom list, a list of quick atom add buttons and provides search capabilities in the atoms list.

The DnD Library (scald_dnd_library) module is a built-in library to use with the DnD module. This library is mostly a view on the atoms with exposed filters to make the search. Thus it is customisable as a standard view in Views UI.

Drag and Drop module

Each dropped atom contain 2 parts:

- The "editor" part: is the rendered atom itself. The default library (scald_dnd_library) uses sdl_editor_representation context to render it. This part should generally not be modified, it could be in HTML format, or the token-like SAS (Scald Atom Shorthand) format and it is updated automatically.

- The "legend" part: usually is the atom title and atom author. You can modify, or even delete, text in this part. A provider can omit this part by setting
$atom->omit_legend = TRUE.

PHP part

Permission

The modules add on permission which is 'Administer DnD'.

Theme

We have one hook_theme implementation that will fire the dnd_library_wrapper function.
the will return the following code :

'<div id="'. $variables['settings']['library_id'] .'" class="dnd-library-wrapper"></div>'

The $variables['settings']['library_id'] allows the administrator to choose which library will be rendered via javascript in the administration section of the module.

Used hooks

We use the hook_element_info_alter() to enable the processing of drag and drop functionalities to a textarea, if it has been enabled in the field options of the entities ( content type, taxonomies etc... ).
we will then launch the dnd_process_textarea below :

function dnd_process_textarea($element, $form_state) {
  // the field is a drag and drop enabled field
  if (!empty($element['#dnd-enabled'])) {
    // add the library : We mostly add defaut JS libraries that will be used by the library modules.
    // we also add ctools modal stuff to create the popup used to create new atoms
    dnd_add_library();

    $settings = array(
      'library_id' => $element['#id'] . DND_ID_SUFFIX,
      // The url of the defaut library
      'url' => dnd_get_library(),
    );

    // We take a string or an object or an array
    if (is_object($element['#dnd-settings'])) {
      $settings = (array) $element['#dnd-settings'] + $settings;
    }
    else if (is_array($element['#dnd-settings'])) {
      $settings = $element['#dnd-settings'] + $settings;
    }

    // add the necessary javascript
    // Those are the settings of the used library, that we be used to render it
    drupal_add_js(array(
      'dndDropAreas' => array($element['#id'] => $settings),
    ), 'setting');
  }

  return $element;
}

hook_wysiwyg_plugin() :
For the moment we plug over the Wysiwyg module that calls the rich text editor for us. We mostly support Tinymce and Ckeditor.
@TODO : We created a custom JS plugin for tinymce

  switch ($editor) {
    case 'tinymce':
      if ($version > 3) {
        $plugins['forcecontainer'] = array(
          'title' => t('Force Container Plugin'),
          'description' => t('A custom plugin to forces a selection up to the outer container of a given element.'),
          'extensions' => array('forcecontainer' => t('Force Container')),
          'path' => drupal_get_path('module', 'dnd') .'/js/tinymce/forcecontainer/editor_plugin_src.js',
          'load' => TRUE,
          'options' => array(
            'forcecontainer_class' => 'dnd-drop-wrapper',
            'forcecontainer_trigger_dnd' => TRUE,
          ),
        );
      }
      break;
  }

hook_library() :
We use this standard hook to add necessary JS, jquery dependencies and CSS for the library theming.

Created Hooks

hook_dnd_libraries_info is created and invoked in dnd_get_libraries() to get libraries defined by some external modules. This is done to get the list of libs and extract the one used as the administrator choosed.

Javascript part

The main part of the javascript is located on the file dnd-library.js.
This builds on the DnD jQuery plugin written to provide drag and drop media handling to Rich Text Editors to consume, display, and attach behavior to a "media library" provided via JSON and implemented for Drupal running the Wysiwyg plugin.

First we initialize a namespace called Drupal.dnd. We put some constant for the beginning and two functions :

  html2sas: function(text) {
    text = text.replace(/<!-- (scald=(\d+):([a-z_]+)) -->[\r\n\s\S]*<!-- END scald=\2 -->/g, '[$1]');
    return text;
  },

  // Convert SAS to HTML.
  // Simple here, the two forms are present in the JSON array of atoms. 
  sas2html: function(text) {
    for (var i in Drupal.dnd.Atoms) {
      atom = Drupal.dnd.Atoms[i];
      if (text.indexOf(atom.sas) > -1) {
        text = text.replace(atom.sas, atom.editor);
      }
    }
    return text;
  }

Then we declare the default theming function of an atom :

Drupal.theme.prototype.scaldEmbed = function(atom) {
  var output = '<div class="dnd-atom-wrapper"><div class="dnd-drop-wrapper">' + atom.editor + '</div>';
  if (atom.meta.legend) {
    output += '<div class="dnd-legend-wrapper">' + atom.meta.legend + '</div>';
  }
  output += '</div>';

  // Trick: if not the image might come out and go into the current hovered
  // paragraph.
  output = '<p>&nbsp;</p>' + output;

  return output;
}

Next we create a new behaviour where the library will be rendered : Drupal.behaviors.dndLibrary.

we define an ajax command that refers to scald.pages.inc in scald core, see scald_atom_add_page. This is done to refresh the library when a new atom is added :

  Drupal.ajax.prototype.commands.dnd_refresh = Drupal.dnd.refreshLibraries;

We get the JSON array and pass it to the rendering function :

 $.getJSON(wrapper.library_url, function(data) {
   Drupal.behaviors.dndLibrary.renderLibrary.call(wrapper, data, $editor);
 });

In renderLibrary we basically add the necessary HTML and put the atoms inside, then we add the drag and drop bindings for HTML5 ( dragstart, dragend ) :

  .bind('dragstart', function(e) {
    // get the atom transferred
    var dt = e.originalEvent.dataTransfer, id = e.target.id, $this = $(this);
    var $img;
    if ($this.is('img')) {
      $img = $this;
    }
    else {
      $this.find('img');
    }
    var id = $.url.setUrl($img.attr('src')).param('dnd_id');
    dt.dropEffect = 'copy';
    //if we are not in a RTE
    dt.setData('Text', Drupal.dnd.Atoms[id].sas);
    //RTE representation, we call the default theming function for the atom
    dt.setData('text/html', Drupal.theme('scaldEmbed', Drupal.dnd.Atoms[id]));

Library

The library is generated by a view with custom plugins as we implement a hook_views_plugins() :

  • style (Scald library) : Used to format atoms to be used in the library
  • handler : scald_plugin_style_library
  • theme : sdl_library
  • display (Dnd library) : scald_plugin_style_library
  • handler : scald_plugin_display_library

The main point is the display plugin. This will render the library in JSON and directly output it so the view won't go thru all the usual rendering stack.

// First we start with the header of the library
    $header = '<div class="summary">';
    $header .= '<div class="toggle"></div><div class="title">'. t('search') .'</div>';
    if (!empty($summary['sort'])) {
      $header .= '<div class="sort">'. $summary['sort'] .'</div>';
    }
    if (!isset($summary['criteria'])) {
      // @todo remove this workaround. Normally is it always set.
      $summary['criteria'] = array();
    }
    $header .= theme('item_list', array('items' => $summary['criteria']));
    $header .= '</div>';
    $library['library'] .= $header;
// End of the header

// We put all the views result into the library, after theming it thru dnd_library
    foreach ($this->view->result as $result) {
      $sid = $result->sid;
      scald_dnd_library_add_item($library, $sid);
    }
// We add the code with the help of ctools to generate the buttons to get the iframe enabling
// the possibility to add atoms directly from the library
    $atom_types = scald_types();
    $buttons = array('type' => 'ul', 'title' => NULL, 'attributes' => array());
    ctools_include('ajax');
    ctools_include('modal');
// Go thru all the providers entity bundle 
    foreach ($atom_types as $type) {
      if (user_access('create atom of ' . $type->type . ' type')) {
        $buttons['items'][] = array(
          'data' => ctools_modal_text_button(t($type->type), 'atoms/nojs/add/' . $type->type, '', 'ctools-modal-custom-style'),
          'class' => array('add-' . strtolower($type->type)),
        );
      }
    }
    $library['menu'] = '<div class="scald-menu"><div class="add-buttons">'. theme_item_list($buttons) .'</div></div>';
    $library['library'] = '<div class="scald-library">' . $library['library'] . '</div>';
    $library['anchor'] = '<div class="scald-anchor"></div>';
// output the rendered view in JSON
    drupal_json_output($library);
...