diff --git ui/transformations_ui.css ui/transformations_ui.css
index 7b1ab32..c05a229 100644
--- ui/transformations_ui.css
+++ ui/transformations_ui.css
@@ -1,6 +1,6 @@
 /* $Id$ */
 
-.transformations-operation-blocks {
+#transformations-operation-blocks {
   margin-top: 1em;
 }
 
@@ -123,3 +123,13 @@ input[disabled], .transformations-operation-block-connections-body input[disable
   text-align: right;
   float: right;
 }
+
+#transformations-grippie {
+  position: absolute;
+  bottom: 0px;
+  width: 100%;
+  background: #eeeeee;
+  border-top: 1px solid darkgrey;
+  text-align: center;
+  cursor: s-resize;
+}
diff --git ui/transformations_ui.dragndrop.js ui/transformations_ui.dragndrop.js
new file mode 100644
index 0000000..b233420
--- /dev/null
+++ ui/transformations_ui.dragndrop.js
@@ -0,0 +1,152 @@
+// $Id$
+
+// Operation element constructor.
+Drupal.transformationsUiOperationElement = function(id, type, key, operation_id) {
+  this.id = id;
+  this.type = type;
+  this.key = key;
+  this.operation = operation_id;
+};
+
+// Grippie object constructor.
+Drupal.transformationsUiGrippie = function() {
+  this.object = '';
+  this.height = 0;
+}
+
+// Global JS variables. TODO: OUCH!!
+var elements = new Array();
+var grippie = new Drupal.transformationsUiGrippie();
+
+Drupal.behaviors.transformationsUiDragAndDrop = function() {
+  var operations = Drupal.settings.transformationsUiOperations;
+
+  // Save all elements in a global array and replace the submit buttons
+  // with divs and links.
+  for (var operationId in operations) {
+    for (var elementId in operations[operationId]) {
+      var element = operations[operationId][elementId];
+      elements[elementId] = new Drupal.transformationsUiOperationElement(
+        elementId, element.keyType, element.key, operationId
+      );
+      var input = '#' + elementId + ' > input';
+      if (element.keyType == 'input' && operationId != '4') {
+        $(input).replaceWith('<span style="' + $(input).attr('style')
+            + '" ><a href="?q=admin/build/transformations/'
+            + Drupal.settings.transformationsUiPipelinePersistenceId
+            + '/edit/input/' + operationId + '/' + element.key + '" >'
+            + $(input).attr('value') + '</a></span>');
+      }
+      else {
+        var parent = $(input).parent();
+        var div = $(input).replaceWith('<span style="' + $(input).attr('style')
+            + '" >' + $(input).attr('value') + '</span>');
+
+        // Disconnect two slots by clicking on the output. (Reloads the page.)
+        parent.children('span:first').click(function() {
+          var elementId = $(this).parent().attr('id');
+          var operationId = elements[elementId].operation;
+          var key = elements[elementId].key;
+          var type = elements[elementId].type;
+
+          var arguments = Drupal.settings.transformationsPipeID + '/'
+            + operationId + '/' + key + '/' + type;
+          $.get('?q=build/transformations/ajax/disconnect/' + arguments, null, function(data) {
+            window.location.reload();
+          });
+        });
+      }
+    }
+  }
+
+  // This allows the user to resize the operation block area. It works like
+  // Drupal's original grippie, but grippie is for textareas only.
+  $('#transformations-operation-blocks').resizable({
+    maxWidth : $('#transformations-operation-blocks').width(),
+    minWidth : $('#transformations-operation-blocks').width(),
+    minHeight : $('#transformations-operation-blocks').height(),
+    handles : { s : $('#transformations-grippie') },
+  });
+  Drupal.transformationsUi_initDragDrop();
+}
+
+
+/**
+ * Initialize all elements that can be dragged and dropped.
+ */
+Drupal.transformationsUi_initDragDrop = function() {
+  // Set the position for each operation.
+  $('#transformations-operation-blocks .transformations-operation-block').each(function() {
+    var div_operation = this.id;
+    var arguments = Drupal.settings.transformationsPipeID + '/' + this.id;
+
+    $.get('?q=build/transformations/ajax/get_position/' + arguments, null, function(data) {
+      var result = Drupal.parseJson(data);
+      $('#' + div_operation).css('top', result.top + 'px');
+      $('#' + div_operation).css('left', result.left + 'px');
+
+      // Look for the lowest position and store the corresponding operation id
+      // in the grippie object.
+      var top = $('#' + div_operation).position().top + $('#' + div_operation).height();
+      if (top > grippie.height) {
+        grippie.height = top + 20;
+        grippie.object = div_operation;
+        $('#transformations-operation-blocks').resizable('option', 'minHeight', grippie.height);
+        $('#transformations-operation-blocks').css('height', grippie.height + 'px');
+      }
+    });
+  });
+
+  // Initialize all draggable operation blocks.
+  $('.transformations-operation-block').draggable({
+    stack: {
+      group: '#transformations-operation-blocks .transformations-operation-block',
+      min: 1,
+    },
+    containment: 'parent',
+    handle: '.transformations-operation-block-header-cell',
+    stop: function(event, ui) {
+      var arguments = Drupal.settings.transformationsPipeID + '/'
+        + $(this).attr('id') + '/' + $(this).css('z-index') + '/'
+        + ui.position.top  + '/' + ui.position.left;
+      $.get('?q=build/transformations/ajax/save_position/' + arguments);
+
+      // If an operation block was moved, it might be required to have the
+      // minHeight set in the operation block area, otherwise the user will
+      // be able to drag an operation block out of it.
+      // The minHeight will also be set if the lowest object was moved.
+      var top = $(this).height() + $(this).position().top;
+      if (top > grippie.height) {
+        grippie.height = top + 20;
+        grippie.object = $(this).attr('id');
+        $('#transformations-operation-blocks').resizable('option', 'minHeight', grippie.height);
+      }
+    },
+  });
+
+  // Initialize all draggable outputs.
+  $('.transformations-operation-output').draggable({
+    cursor: 'move',
+    helper: 'clone',
+    opacity: 0.5,
+  });
+
+  // Initialize all droppable inputs.
+  $('.transformations-operation-input').droppable({
+    hoverClass: 'transformations-input-hover',
+    drop: function(event, ui) {
+      var idInput = $(this).attr('id');
+      var idOutput = $(ui.draggable).attr('id');
+      var input = elements[idInput];
+      var output = elements[idOutput];
+      if (input != null && output != null) {
+        arguments = Drupal.settings.transformationsPipeID + '/'
+          + output.operation + '/' + output.key + '/'
+          + input.operation + '/' + input.key;
+        $.get('?q=build/transformations/ajax/connect/' + arguments, null, function(data) {
+          window.location.reload();
+        });
+      }
+    },
+  });
+};
diff --git ui/transformations_ui.js ui/transformations_ui.js
index a120e48..fb17cc6 100644
--- ui/transformations_ui.js
+++ ui/transformations_ui.js
@@ -3,7 +3,7 @@
 /**
  * Auto-attach for connection button highlighting behavior.
  */
-Drupal.behaviors.transformationsUiConnections = function(context) {
+Drupal.behaviors.transformationsUiConnectionHighlighting = function(context) {
   var connections = Drupal.settings.transformationsUiConnections;
 
   $('.transformations-operation-input input[type="submit"], .transformations-operation-output input[type="submit"]', context).mouseover(function() {
diff --git ui/transformations_ui.module ui/transformations_ui.module
index e8b683b..d92dfe0 100644
--- ui/transformations_ui.module
+++ ui/transformations_ui.module
@@ -108,6 +108,32 @@ function transformations_ui_menu() {
     'type' => MENU_CALLBACK,
   ) + $base;
 
+  // Ajax callbacks.
+  $items['build/transformations/ajax/connect'] = array(
+    'title' => 'transformations_ajax_connect',
+    'page callback' => 'transformations_ui_pipeline_ajax_connect',
+    'type' => MENU_CALLBACK,
+    'file' => 'transformations_ui.pipeline.ajax.inc',
+  ) + $base;
+  $items['build/transformations/ajax/disconnect'] = array(
+    'title' => 'transformations_ajax_disconnect',
+    'page callback' => 'transformations_ui_pipeline_ajax_disconnect',
+    'type' => MENU_CALLBACK,
+    'file' => 'transformations_ui.pipeline.ajax.inc',
+  ) + $base;
+  $items['build/transformations/ajax/position-save'] = array(
+    'title' => 'transformations_ajax_position_save',
+    'page callback' => 'transformations_ui_ajax_save_position',
+    'type' => MENU_CALLBACK,
+    'file' => 'transformations_ui.pipeline.ajax.inc',
+  ) + $base;
+  $items['build/transformations/ajax/position-get'] = array(
+    'title' => 'transformations_ajax_position_get',
+    'page callback' => 'transformations_ui_ajax_get_position',
+    'type' => MENU_CALLBACK,
+    'file' => 'transformations_ui.pipeline.ajax.inc',
+  ) + $base;
+
   return $items;
 }
 
diff --git ui/transformations_ui.pipeline.ajax.inc ui/transformations_ui.pipeline.ajax.inc
new file mode 100644
index 0000000..e86d011
--- /dev/null
+++ ui/transformations_ui.pipeline.ajax.inc
@@ -0,0 +1,115 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Transformations UI -
+ * An interface for managing transformation pipelines.
+ */
+
+
+/**
+ * Connect a pipeline with parameters from javascript and persist it in the
+ * object cache.
+ */
+function transformations_ui_pipeline_ajax_connect($pipeline_id, $output_opID, $output_key, $input_opID, $input_key) {
+  $pipeline = transformations_ui_persisted_pipeline_load($pipeline_id);
+  if (empty($pipeline)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  if (isset($output_opID) && isset($output_key)) {
+    $source = array(
+      'entity' => $output_opID,
+      'key' => $output_key,
+    );
+  }
+  else {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  if (isset($input_opID) && isset($input_key)) {
+    $target = array(
+      'entity' => $input_opID,
+      'key' => $input_key,
+    );
+  }
+  else {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  $pipeline->connect($source['entity'], $source['key'], $target['entity'], $target['key']);
+  transformations_ui_pipeline_persist($pipeline);
+
+  print drupal_to_js(TRUE);
+}
+
+/**
+ * Disconnect a connection source or target.
+ */
+function transformations_ui_pipeline_ajax_disconnect($pipeline_id, $operationId, $key, $keyType) {
+  $pipeline = transformations_ui_persisted_pipeline_load($pipeline_id);
+  if (empty($pipeline)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  if (isset($operationId) && isset($key) && isset($keyType)) {
+    if ($keyType == 'input') {
+      $pipeline->disconnectTarget($operationId, $key);
+    }
+    elseif ($keyType == 'output') {
+      $pipeline->disconnectSource($operationId, $key);
+    }
+    transformations_ui_pipeline_persist($pipeline);
+    print drupal_to_js(TRUE);
+    return;
+  }
+  else {
+    print drupal_to_js(FALSE);
+    return;
+  }
+}
+
+/**
+ * Save the position of an operation block.
+ */
+function transformations_ui_ajax_save_position($pipeline_id, $operation_id, $z_index, $top, $left) {
+  $pipeline = transformations_ui_persisted_pipeline_load($pipeline_id);
+  if (empty($pipeline)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+  if (empty($top) || empty($left) || empty($operation_id)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  $pipeline->setProperty('transformations_ui:position:' . $operation_id, array(
+    'z-index' => $z_index,
+    'top' => $top,
+    'left' => $left,
+  ));
+  transformations_ui_pipeline_persist($pipeline);
+  print drupal_to_js(TRUE);
+}
+
+/**
+ * Get the position of an operation.
+ */
+function transformations_ui_ajax_get_position($pipeline_id, $operation_id) {
+  $pipeline = transformations_ui_persisted_pipeline_load($pipeline_id);
+  if (empty($pipeline)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  if (empty($operation_id)) {
+    print drupal_to_js(FALSE);
+    return;
+  }
+
+  print drupal_to_js($pipeline->property('transformations_ui:position:' . $operation_id));
+}
diff --git ui/transformations_ui.pipeline.edit.inc ui/transformations_ui.pipeline.edit.inc
index f080887..fa7472b 100644
--- ui/transformations_ui.pipeline.edit.inc
+++ ui/transformations_ui.pipeline.edit.inc
@@ -121,11 +121,13 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
   }
   $blocks[TfPipeline::Output]['input']['keys'][] = TfPipeline::NewOutboundConnection;
 
+  $jsOperations = array();
+
   // Big pile of confusing code that generates the blocks and connection buttons.
   foreach ($blocks as $operationId => $block) {
     $operationElements[$operationId] = array(
       '#value' => '',
-      '#prefix' => '<div class="transformations-operation-block">'
+      '#prefix' => '<div id="' . $operationId . '" class="transformations-operation-block">'
         . '<div class="transformations-operation-block-table">',
       '#suffix' => '</div></div>',
     );
@@ -234,6 +236,7 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
       '#suffix' => '</div>',
     );
 
+    $jsOperations[$operationId] = array();
     // Construct input/output connection elements, as form buttons.
     foreach (array('input', 'output') as $keyType) {
       $otherKeyType = ($keyType == 'input') ? 'output' : 'input';
@@ -368,6 +371,9 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
           $description = '';
         }
 
+        $jsElement = array('key' => $key, 'keyType' => $keyType);
+        $jsOperations[$operationId][$elementId] = $jsElement;
+
         // The connector button for the input/output slot.
         $element = array(
           '#type' => 'submit',
@@ -379,7 +385,7 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
           '#disabled' => $isEditingOperation &&
             (!$isConnectingTo[$otherKeyType] || $_GET['edit-op'] == $operationId),
           '#attributes' => array(),
-          '#prefix' => '<div class="transformations-operation-' . $keyType . '">',
+          '#prefix' => '<div id="' . $elementId . '" class="transformations-operation-' . $keyType . '">',
           '#suffix' => '</div>',
           '#submit' => array('transformations_ui_pipeline_operation_connect'),
         );
@@ -447,10 +453,21 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
     } // end of foreach (array('input', 'output'))
   }
 
+
   // Add JavaScript and connection info enabling it to highlight opposite keys.
   drupal_add_js(drupal_get_path('module', 'transformations_ui') . '/transformations_ui.js');
   drupal_add_js(array('transformationsUiConnections' => $connectionElements), 'setting');
 
+  if (module_exists('jquery_ui')) {
+    jquery_ui_add(array('ui.resizable', 'ui.resizable'));
+    jquery_ui_add(array('ui.draggable', 'ui.droppable'));
+
+    drupal_add_js(array('transformationsUiOperations' => $jsOperations), 'setting');
+    drupal_add_js(array('transformationsUiPipelinePersistenceId' => $form['#pipeline_id']), 'setting');
+
+    drupal_add_js(drupal_get_path('module', 'transformations_ui') . '/transformations_ui.dragndrop.js');
+  }
+
   if (empty($operationElements)) {
     $form['operations'] = array(
       '#value' => t('No operations have yet been defined for this pipeline.'),
@@ -459,8 +476,8 @@ function transformations_ui_pipeline_edit(&$form_state, TfPipeline $pipeline) {
   else {
     $form['operations'] = array(
       '#value' => '',
-      '#prefix' => '<div class="clear-block transformations-operation-blocks">',
-      '#suffix' => '</div>',
+      '#prefix' => '<div id="transformations-operation-blocks" class="clear-block">',
+      '#suffix' => '<div class="ui-resizable-handle" id="transformations-grippie">v</div></div>',
     );
     $form['operations'] += $operationElements;
   }
