changeset 31:767ebf925654

Added forcecontainer tinymce plugin, lots and lots and lots of refactorizing.
author David Eads <eads@chicagotech.org>
date Thu, 19 Mar 2009 15:58:36 -0500
parents 2d49adbd8992
children ee520ba7d98b
files dnd.module js/dnd-library.js js/tinymce/atomic/editor_plugin.js js/tinymce/atomic/editor_plugin_src.js js/tinymce/forcecontainer/editor_plugin_src.js modules/dnd_test/dnd-library-footer.tpl.php modules/dnd_test/dnd_test.module
diffstat 7 files changed, 270 insertions(+), 639 deletions(-) [+]
line wrap: on
line diff
--- a/dnd.module	Tue Mar 17 21:29:00 2009 -0500
+++ b/dnd.module	Thu Mar 19 15:58:36 2009 -0500
@@ -30,7 +30,7 @@
  */
 function dnd_theme() {
   return array(
-    'dnd_library' => array('arguments' => array('library' => NULL, 'library_id' => NULL), 'template' => 'dnd-library'),
+    'dnd_library_wrapper' => array('arguments' => array('settings' => NULL, 'element' => NULL)),
   );
 }
 
@@ -109,21 +109,13 @@
       'dndEnabledLibraries' => array($element['#id'] => $settings),
     ), 'setting');
 
-    // Store editor representations in Drupal setting
-    drupal_add_js(array(
-      'dndEditorRepresentations' => $library['editor_representations'], 
-      'dndLibraryPreviews' => $library['library_previews'], 
-    ), 'setting');
-
-    // Note that we brute force the wrapper
+    // Generate 
     $element['#suffix'] = '<div class="dnd-library-wrapper" id="'. $settings['library_id'] .'"></div>'. $element['#suffix'];
 
   }
   return $element;
 }
 
-function template_preprocess_dnd_library($library) {}
-
 /**
  * Implementation of hook_wywiwyg_plugin().
  */
@@ -132,22 +124,28 @@
   switch ($editor) {
     case 'tinymce':
       if ($version > 3) {
-        $plugins['atomic'] = array(
-          'type' => 'external',
-          'title' => t('Atomic selection plugin'),
-          'description' => t('This plugin forces a selection up to the outer container of a given element.  It is available via the third party repository maintained by MoxieCode on SourceForge.'),
-          'extensions' => array('atomic' => t('Atomic Selection')),
-          'path' => drupal_get_path('module', 'dnd') .'/js/tinymce/atomic/editor_plugin.js', 
-          'url' => 'http://sourceforge.net/tracker/index.php?func=detail&aid=2519211&group_id=103281&atid=738747',
+        $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,
-        );
-        $plugins['noneditable'] = array(
-          'path' => drupal_get_path('module', 'wysiwyg') .'/tinymce/jscripts/tiny_mce/plugins/noneditable/editor_plugin.js',
-          'extensions' => array('noneditable' => t('Noneditable')),
-          'load' => TRUE,
+          'options' => array(
+            'forcecontainer_class' => 'dnd-drop-wrapper',
+            'forcecontainer_trigger_dnd' => TRUE,
+          ),
         );
       }
       break;
   }
   return $plugins;
 }
+
+
+/**
+ * Theme the markup that will surround a library loaded via JSON.
+ */
+function theme_dnd_library_wrapper($settings, $element = NULL) {
+  return '<div id="'. $settings['library_id'] .'" class="dnd-library-wrapper"></div>';
+}
+
--- a/js/dnd-library.js	Tue Mar 17 21:29:00 2009 -0500
+++ b/js/dnd-library.js	Thu Mar 19 15:58:36 2009 -0500
@@ -17,7 +17,7 @@
 (function($) {
   // Custom selectors
   $.extend($.expr[":"], {
-    'empty' : function(a, i, m) {
+    'dnd_empty' : function(a, i, m) {
       return !$(a).filter(function(i) {
         return !$(this).is('br');
       }).length && !$.trim(a.textContent || a.innerText||$(a).text() || "");
@@ -38,6 +38,7 @@
 
     // Set up some initial settings for BeautyTips
     var settings = Drupal.settings.dndEnabledLibraries[$editor.get(0).id] = $.extend({
+      'btSettings' : {
         'trigger': 'none',
         'width': 375,
         'spikeLength': 7,
@@ -46,6 +47,23 @@
         'strokeWidth': 1,
         'fill': '#ffd',
         'strokeStyle': '#555'
+      },
+      'libraryHoverIntentSettings' : {
+        'interval': 500,
+        'timeout' : 0,
+        'over': function() {
+          var $this = $(this);
+          this.btOn();
+          // Remove the preview once dragging of any image has commenced
+          $('img', $this).bind('drag', function(e) {
+            $this.btOff();
+          });
+          $('img', $this).bind('click', function(e) {
+            $this.btOff();
+          });
+        }, 
+        'out': function() { this.btOff(); }
+      }
     }, Drupal.settings.dndEnabledLibraries[$editor.get(0).id]);
 
     // Bind Drag and Drop plugin invocation to events emanating from Wysiwyg
@@ -56,7 +74,7 @@
     Drupal.settings.dndEditorRepresentations = {};
     Drupal.settings.dndLibraryPreviews = {};
 
-    // Populate
+    // Initialize the library
     $.getJSON(Drupal.settings.basePath + settings.url, function(data) {
       Drupal.behaviors.dndLibrary.renderLibrary.call($this.get(0), data, $editor);
     });
@@ -84,45 +102,44 @@
 
   // Add preview behavior to editor items (thanks, BeautyTips!)
   $('.editor-item', $this).each(function () {
-    $(this).bt(Drupal.settings.dndLibraryPreviews[this.id], settings.bt_settings);
-    var hover_opts = $.extend({
-      'interval': 500,
-      'timeout' : 0,
-      'over': function() {
-        var $this = $(this);
-        this.btOn();
-        // Remove the preview once dragging of any image has commenced
-        $('img', $this).bind('drag', function(e) {
-          $this.btOff();
-        });
-        $('img', $this).bind('click', function(e) {
-          $this.btOff();
-        });
-      }, 
-      'out': function() { this.btOff(); }
-    }, settings.libraryHoverIntentOpts);
-    $(this).hoverIntent(hover_opts);
+    $(this).bt(Drupal.settings.dndLibraryPreviews[this.id], settings.btSettings);
+    $(this).hoverIntent(settings.libraryHoverIntentSettings);
   });
 
   // Preload images in editor representations
   var cached = $.data($(editor), 'dnd_preload') || {};
-  /*for (editor_id in Drupal.settings.dndEditorRepresentations) {
+  for (editor_id in Drupal.settings.dndEditorRepresentations) {
     if (!cached[editor_id]) {
       $representation = $(Drupal.settings.dndEditorRepresentations[editor_id].body);
       if ($representation.is('img') && $representation.get(0).src) { 
         $representation.attr('src', $representation.get(0).src);
       } else {
         $('img', $representation).each(function() {
-          this.attr('src', this.src);
+          $(this).attr('src', this.src);
         });
       }
     }
   }
-  $.data($(editor), 'dnd_preload', cached);*/
+  $.data($(editor), 'dnd_preload', cached);
 
+  $('.pager a', $this).click(function() {
+    $.getJSON(this.href, function(data) {
+      Drupal.behaviors.dndLibrary.renderLibrary.call($this.get(0), data, $(editor));
+    });
+    return false;
+  });
+  $('.view-filters input[type=submit]', $this).click(function() {
+    $(this).ajaxSubmit({
+      'url' : Drupal.settings.basePath + settings.url,
+      'dataType' : 'json',
+      'success' : function(data) {
+        Drupal.behaviors.dndLibrary.renderLibrary.call($this.get(0), data, $(editor));
+      }
+    });
+    return false;
+  });
 }
 
-
 // Dynamically compose a callback based on the editor name
 Drupal.behaviors.dndLibrary.attach_library = function(e, data) {
   var settings = $.extend({idSelector: Drupal.behaviors.dndLibrary.idSelector}, Drupal.settings.dndEnabledLibraries[data.field]);
@@ -133,12 +150,7 @@
 }
 
 // Do garbage collection on detach
-Drupal.behaviors.dndLibrary.detach_library = function(e, data) {
-  for (t in $(document).data('dnd_timers')) {
-    clearInterval(t);
-  }
-  $(document).removeData('dnd_timers');
-}
+Drupal.behaviors.dndLibrary.detach_library = function(e, data) {}
 
 // Basic textareas
 Drupal.behaviors.dndLibrary.attach_none = function(data, settings) {
@@ -150,7 +162,7 @@
       // Update element count
       Drupal.behaviors.dndLibrary.countElements.call(target, representation_id);
 
-      var snippet = '<p class="dnd-dropped-wrapper">' + Drupal.settings.dndEditorRepresentations[representation_id].body + '</p>';
+      var snippet = '<p class="dnd-drop-wrapper">' + Drupal.settings.dndEditorRepresentations[representation_id].body + '</p>';
       $target.replaceSelection(snippet, true);
     }
   }, settings);
@@ -190,13 +202,24 @@
 
   settings = $.extend({
     targets: $('#'+ data.field +'-wrapper iframe'),
+    processTargets: function(targets) {
+      return targets.each(function() {
+        var target = this
+        // Decrement counter on delete
+        $(target).bind('dnd_delete', function(e, data) {
+          Drupal.behaviors.dndLibrary.countElements(target, $(data.node).attr('dnd_id'), true); 
+        });
+        $('head', $(this).contents()).append('<style type="text/css">img { display: none; } img.dnd-dropped {display: block; }</style>');
+        return this;
+      });
+    },
     processIframeDrop: function(drop, id_selector) {
       var representation_id = id_selector.call(this, drop);
       var representation = Drupal.settings.dndEditorRepresentations[representation_id].body;
       var target = this, $target = $(target), $drop = $(drop), block;
 
       // Update element count
-      //Drupal.behaviors.dndLibrary.countElements.call(target, representation_id);
+      Drupal.behaviors.dndLibrary.countElements(target, representation_id);
 
       // Search through block level parents
       $drop.parents().each(function() {
@@ -211,7 +234,7 @@
       $drop.remove();
 
       // Create an element to insert
-      var insert = dom.create('p', {'class' : 'dnd-dropped-wrapper', 'id' : 'dnd-inserted'}, representation);
+      var insert = dom.create('p', {'class' : 'dnd-drop-wrapper', 'id' : 'dnd-inserted'}, representation);
 
       // The no-parent case
       if ($(block).is('body')) {
@@ -223,17 +246,17 @@
         $block = $('#target-block', $target.contents());
 
         // @TODO is finding the parent broken in safari??
-        $block.after('<p class="dnd-dropped-wrapper" id="dnd-inserted">' + representation + '</p>');
+        $block.after('<p class="dnd-drop-wrapper" id="dnd-inserted">' + representation + '</p>');
 
         // The active target block should be empty
-        if ($('#target-block:empty', $target.contents()).length > 0) {
+        if ($('#target-block:dnd_empty', $target.contents()).length > 0) {
           $('#target-block', $target.contents()).remove();
         } else if (old_id) {
           block.id = old_id;
         } else {
           $block.removeAttr('id');
         }
-      }
+      } 
 
       var $inserted = $('#dnd-inserted', $target.contents());
       var inserted = $inserted.get(0);
@@ -243,12 +266,8 @@
 
       // If the previous element is also an editor representation, we need to
       // put a dummy paragraph between the elements to prevent editor errors.
-      if (previous && $(previous).hasClass('dnd-dropped-wrapper')) {
-        $inserted.before('<p><span id="__spacer">_</span></p>');
-        c = dom.get('__spacer');
-        s.select(c);
-        ed.execCommand('Delete', false, null);
-        dom.remove(c);
+      if (previous ) {
+        $inserted.before('<p></p>');
       }
 
       // Look ahead in the DOM
@@ -257,22 +276,28 @@
       // If the next item exists and isn't an editor representation, drop the
       // caret at the beginning of the element, otherwise make a new paragraph
       // to advance the caret to.
-      if (next && !$(next).hasClass('dnd-dropped-wrapper')) {
+      if (next && !$(next).hasClass('dnd-drop-wrapper')) {
         $(next).prepend('<span id="__caret">_</span>');
       }
-      else {
+      else if (!$(next).hasClass('dnd-drop-wrapper')) {
         var after = dom.create('p', {}, '<span id="__caret">_</span>');
         dom.insertAfter(after, 'dnd-inserted');
       }
-
-      // Clear the ID for the next drop
-      $inserted.removeAttr('id');
-
+ 
       // Force selection to reset the caret
       var c = dom.get('__caret');
-      s.select(c);
-      ed.execCommand('Delete', false, null);
-      dom.remove(c);
+      if (c) {
+        s.select(c);
+        ed.execCommand('Delete', false, null);
+        dom.remove(c);
+      }
+
+      // Unset id for next drop and add special dnd attribute for counting
+      // purposes
+      $inserted
+        .removeAttr('id')
+        .attr('dnd_id', representation_id);
+
     }
   }, settings);
 
@@ -281,18 +306,16 @@
 
 
 // Keep a counter of times a representation ID has been used
-Drupal.behaviors.dndLibrary.countElements = function(representation_id) {
-  // We need to track element usage betwen all editors, so we look for the
-  // parent form item
-  $target = $(this).parents('.form-item');
-  var counter = $target.data('representation_counter');      
+Drupal.behaviors.dndLibrary.countElements = function(target, representation_id, decrement) {
+  var counter = $(target).data('dnd_representation_counter');      
   if (!counter) {
     counter = {}
     counter[representation_id] = 1;
   } else if (counter && !counter[representation_id]) {
     counter[representation_id] = 1;
   } else {
-    counter[representation_id] = counter[representation_id] + 1;
+    counter[representation_id] = counter[representation_id] + ((decrement) ? -1 : 1);
   }
-  $target.data('representation_counter', counter);
+  $(target).data('dnd_representation_counter', counter);
+  return counter[representation_id];
 }
--- a/js/tinymce/atomic/editor_plugin.js	Tue Mar 17 21:29:00 2009 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-(function(){var Event=tinymce.dom.Event;var direction=0;var hasClicked=false;var atStart=null;var atEnd=null;tinymce.create('tinymce.plugins.AtomicPlugin',{init:function(ed,url){var t=this,atomicClass;t.editor=ed;atomicClass=ed.getParam("atomic_atomic_class","mceAtomic");ed.onNodeChange.addToTop(function(ed,cm,n){var sc,ec,wasEnd=false;sc=ed.dom.getParent(ed.selection.getStart(),function(n){return ed.dom.hasClass(n,atomicClass);});ec=ed.dom.getParent(ed.selection.getEnd(),function(n){return ed.dom.hasClass(n,atomicClass);});if(atEnd){wasEnd=true;}atStart=null;atEnd=null;if(sc||ec){var s=ed.selection.getSel();var r=ed.selection.getRng();var select=s.type?(s.type!="None"):(s.anchorNode!=s.focusNode||s.anchorOffset!=s.focusOffset);var move=false;if(!select){if(direction==-1){if(r.setStart){var lc=t._leftNeighbour(ed,sc);if(lc){var offset=lc.nodeType==3?lc.length:1;r.setStart(lc,offset);r.setEnd(lc,offset);}else{r.setStart(sc,0);r.setEnd(sc,0);}atStart=sc;move=true;}else{var r2=ed.getBody().createTextRange();r2.moveToElementText(ec);r2.collapse(false);if(!r2.isEqual(r)){r.moveToElementText(sc);r.collapse();atStart=sc;move=true;}else{var tempEl=ed.dom.create("span",null,"&shy;");ec.insertAdjacentElement("afterEnd",tempEl);r.moveToElementText(tempEl);r.collapse(false);ed.selection.setRng(r);ed.dom.remove(tempEl);atEnd=ec;}}}else if(direction==1){if(r.setStartAfter){r.setStartAfter(ec);r.setEndAfter(ec);move=true;}else{if(wasEnd){r.move('character');ed.selection.setRng(r);ec=null;}else{var tempEl=ed.dom.create("span",null,"&shy;");ec.insertAdjacentElement("afterEnd",tempEl);r.moveToElementText(tempEl);r.collapse(false);ed.selection.setRng(r);ed.dom.remove(tempEl);}}atEnd=ec;}else if(hasClicked){if(r.setStart){r.setStart(sc,0);r.setEnd(ec,ec.childNodes.length);}else{r.moveToElementText(sc);}move=true;}else if(wasEnd&&!r.setStart){var tempEl=ed.dom.create("span",null,"&shy;");ec.insertAdjacentElement("afterEnd",tempEl);r.moveToElementText(tempEl);r.collapse(false);ed.selection.setRng(r);ed.dom.remove(tempEl);atEnd=ec;}}else{var reverse=false;if(s.anchorNode&&s.anchorNode==r.endContainer&&s.anchorOffset==r.endOffset){reverse=true;}var moveStart=false;var moveEnd=false;var rsc,rec;if(sc){if(s.anchorNode){if(sc!=r.startContainer||(r.startOffset!=0&&r.startOffset!=sc.childNodes.length)){moveStart=true;}}else{rsc=ed.getBody().createTextRange();rsc.moveToElementText(sc);moveStart=(r.compareEndPoints("StartToStart",rsc)!=0);}}if(ec){if(s.anchorNode){if(ec!=r.endContainer||(r.endOffset!=0&&r.endOffset!=ec.childNodes.length)){moveEnd=true;}}else{rec=ed.getBody().createTextRange();rec.moveToElementText(ec);moveEnd=(r.compareEndPoints("EndToEnd",rec)!=0);}}if(reverse){if(moveEnd){if(direction==-1){r.setEnd(ec,0);}else{r.setEnd(ec,ec.childNodes.length);}move=true;}if(moveStart){if(direction==1){s.extend(sc,sc.childNodes.length);move=false;}else{s.extend(sc,0);move=false;}}}else{if(moveStart){if(direction==1){if(r.setStart){r.setStart(sc,sc.childNodes.length);}else{r.setEndPoint("StartToEnd",rsc);}}else{if(r.setStart){r.setStart(sc,0);}else{r.setEndPoint("StartToStart",rsc);}}move=true;}if(moveEnd){if(direction==-1){if(r.setEnd){s.extend(ec,0);}else{r.setEndPoint("EndToStart",rec);move=true;}}else{if(r.setEnd){s.extend(ec,ec.childNodes.length);}else{r.setEndPoint("EndToEnd",rec);move=true;}}}}}if(move){ed.selection.setRng(r);}}});ed.onKeyDown.addToTop(function(ed,e){var k=e.keyCode,atomicClass;atomicClass=ed.getParam("atomic_atomic_class","mceAtomic");hasClicked=false;direction=0;if(k==37||k==38){direction=-1;}else if(k==39||k==40){direction=1;}else if(k==8){if(!atEnd){var s=ed.selection.getSel();var select=s.type?(s.type!="None"):(s.anchorNode!=s.focusNode||s.anchorOffset!=s.focusOffset);if(!select){var ep=s.focusNode?((s.focusNode.nodeType==3&&s.focusOffset!=0)?null:s.focusNode):ed.selection.getStart();if(ed.dom.hasClass(ep,atomicClass)){atEnd=ep;}else if(ep){var rc;if(!s.focusNode){var r=ed.selection.getRng();var r2=ed.getBody().createTextRange();r2.moveToElementText(ep);if(r2.compareEndPoints("StartToStart",r)==0){rc=t._leftNeighbour(ed,ep);}else{rc=ep.firstChild;while(rc){if(rc.nodeType==1){r2.moveToElementText(rc);if(r.compareEndPoints("EndToEnd",r2)==0){break;}}rc=rc.nextSibling;}}}else{if(ep.nodeType==1&&s.focusOffset!=0){rc=ep.childNodes[s.focusOffset-1];}else{rc=t._leftNeighbour(ed,ep);}}if(rc){while(rc&&!ed.dom.hasClass(rc,atomicClass)){rc=rc.lastChild;}if(rc){atEnd=rc;}}}}}if(atEnd){ed.dom.remove(atEnd);atEnd=null;return Event.cancel(e);}}else if(k==46){if(!atStart){var s=ed.selection.getSel();var select=s.type?(s.type!="None"):(s.anchorNode!=s.focusNode||s.anchorOffset!=s.focusOffset);if(!select){var ep=s.focusNode?((s.focusNode.nodeType==3&&s.focusOffset!=s.focusNode.length)?null:s.focusNode):ed.selection.getEnd();if(ed.dom.hasClass(ep,atomicClass)){atStart=ep;}else if(ep){var lc;if(!s.focusNode){var r=ed.selection.getRng();var r2=ed.getBody().createTextRange();r2.moveToElementText(ep);if(r2.compareEndPoints("EndToEnd",r)==0){lc=t._rightNeighbour(ed,ep);}else{lc=ep.firstChild;while(lc){if(lc.nodeType==1){r2.moveToElementText(lc);if(r.compareEndPoints("StartToStart",r2)==0){break;}}lc=lc.nextSibling;}}}else{if(ep.nodeType==1&&s.focusOffset!=ep.childNodes.length){lc=ep.childNodes[s.focusOffset];}else{lc=t._rightNeighbour(ed,ep);}}if(lc){while(lc&&!ed.dom.hasClass(lc,atomicClass)){lc=lc.firstChild;}if(lc){atStart=lc;}}}}}if(atStart){ed.dom.remove(atStart);atStart=null;return Event.cancel(e);}}});ed.onMouseDown.addToTop(t._onClick);ed.onMouseUp.addToTop(t._onClick);},getInfo:function(){return{longname:'Atomic elements',author:'Sander Kruger',authorurl:'http://www.3gsp.eu',infourl:'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/atomic',version:tinymce.majorVersion+"."+tinymce.minorVersion};},_onClick:function(ed,e){direction=0;hasClicked=true;atStart=null;atEnd=null;},_leftNeighbour:function(ed,e){var l=ed.dom.getParent(e,function(n){return e.previousSibling!=null;});if(l){return l.previousSibling;}return null;},_rightNeighbour:function(ed,e){var r=ed.dom.getParent(e,function(n){return e.nextSibling!=null;});if(r){return r.nextSibling;}return null;}});tinymce.PluginManager.add('atomic',tinymce.plugins.AtomicPlugin);})();
\ No newline at end of file
--- a/js/tinymce/atomic/editor_plugin_src.js	Tue Mar 17 21:29:00 2009 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,558 +0,0 @@
-/**
- *
- * @author Sander Kruger
- * @copyright Copyright © 2009, Sander Kruger, All rights reserved.
- */
-
-(function() {
-	var Event = tinymce.dom.Event;
-	var direction = 0;	// Track cursor direction for skipping
-	var hasClicked = false;	// Track mouseclicks for selecting
-	var atStart = null;	// Cache if the cursor is at the start or at the end of
-	var atEnd = null;	// an atom.
-
-	tinymce.create('tinymce.plugins.AtomicPlugin', {
-		init : function(ed, url) {
-			var t = this, atomicClass;
-
-			t.editor = ed;
-			atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic");
-
-			ed.onNodeChange.addToTop(function(ed, cm, n) {
-				var sc, ec, wasEnd=false;
-
-				// Check if the start or end of the selection is in an atom
-				sc = ed.dom.getParent(ed.selection.getStart(), function(n) {
-					return ed.dom.hasClass(n, atomicClass);
-				});
-
-				ec = ed.dom.getParent(ed.selection.getEnd(), function(n) {
-					return ed.dom.hasClass(n, atomicClass);
-				});
-				if (atEnd)
-				{
-					wasEnd = true;
-				}
-				atStart = null;
-				atEnd = null;
-				// Check situation to move the cursor or selection.
-				if (sc || ec)
-				{
-					var s = ed.selection.getSel();
-					var r = ed.selection.getRng();
-					var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
-					var move = false;
-					if (!select)
-					{
-						// The user has not selected anything but is moving the
-						// cursor or clicked on an atom.
-						if (direction == -1)
-						{
-							if (r.setStart)
-							{
-								var lc = t._leftNeighbour( ed, sc );
-								if (lc)
-								{
-									// Make sure that when skipping to the left,
-									// the next input doesn't go into the atom.
-									var offset = lc.nodeType==3?lc.length:1;
-									r.setStart( lc, offset );
-									r.setEnd( lc, offset );
-								}
-								else
-								{
-									r.setStart( sc, 0 );
-									r.setEnd( sc, 0 );
-								}
-								atStart = sc;
-								move = true;
-							}
-							else
-							{
-								// ie (check if the caret is not just after the
-								// atom)
-								var r2 = ed.getBody().createTextRange();
-								r2.moveToElementText( ec );
-								r2.collapse(false);
-								if (!r2.isEqual( r ))
-								{
-									r.moveToElementText( sc );
-									r.collapse();
-									atStart = sc;
-									move = true;
-								}
-								else
-								{
-									var tempEl = ed.dom.create("span", null, "&shy;");
-									ec.insertAdjacentElement("afterEnd", tempEl);
-
-									r.moveToElementText(tempEl);
-									r.collapse(false);
-									ed.selection.setRng(r);
-
-									ed.dom.remove(tempEl);
-									atEnd = ec;
-								}
-							}
-						}
-						else if (direction == 1)
-						{
-							if (r.setStartAfter)
-							{
-								r.setStartAfter( ec );
-								r.setEndAfter( ec );
-								move = true;
-							}
-							else
-							{
-								if (wasEnd)
-								{
-									r.move( 'character' );
-									ed.selection.setRng(r);
-									ec = null;
-								}
-								else
-								{
-									var tempEl = ed.dom.create("span", null, "&shy;");
-									ec.insertAdjacentElement("afterEnd", tempEl);
-
-									r.moveToElementText(tempEl);
-									r.collapse(false);
-									ed.selection.setRng(r);
-
-									ed.dom.remove(tempEl);
-								}
-							}
-							atEnd = ec;
-						}
-						else if (hasClicked)
-						{
-							if (r.setStart)
-							{
-								r.setStart( sc, 0 );
-								r.setEnd( ec, ec.childNodes.length );
-							}
-							else
-							{
-								// ie
-								r.moveToElementText( sc );
-							}
-							move = true;
-						}
-						else if (wasEnd && !r.setStart)
-						{
-							// ie
-							var tempEl = ed.dom.create("span", null, "&shy;");
-							ec.insertAdjacentElement("afterEnd", tempEl);
-
-							r.moveToElementText(tempEl);
-							r.collapse(false);
-							ed.selection.setRng(r);
-
-							ed.dom.remove(tempEl);
-							atEnd = ec;
-						}
-					}
-					else
-					{
-						// Reverse selections have the anchor at the end and the
-						// focus at the start. This happens when selecting
-						// backwards. Make sure the extended selection keeps the
-						// same direction.
-						// *** Issue: ie doesn't support backward selections
-						// programmatically (or at least I have not found how to
-						// do this).
-						var reverse = false;
-						if (s.anchorNode && s.anchorNode == r.endContainer && s.anchorOffset == r.endOffset)
-						{
-							reverse = true;
-						}
-						var moveStart = false;
-						var moveEnd = false;
-						var rsc, rec;
-						if (sc)
-						{
-							if (s.anchorNode)
-							{
-								if (sc != r.startContainer || (r.startOffset != 0 && r.startOffset != sc.childNodes.length))
-								{
-									// The start pos is not where it should be.
-									moveStart = true;
-								}
-							}
-							else
-							{
-								// ie
-								rsc = ed.getBody().createTextRange();
-								rsc.moveToElementText( sc );
-								moveStart = (r.compareEndPoints( "StartToStart", rsc ) != 0);
-							}
-						}
-						if (ec)
-						{
-							if (s.anchorNode)
-							{
-								if (ec != r.endContainer || (r.endOffset != 0 && r.endOffset != ec.childNodes.length))
-								{
-									// The end pos is not where it should be
-									moveEnd = true;
-								}
-							}
-							else
-							{
-								// ie
-								rec = ed.getBody().createTextRange();
-								rec.moveToElementText( ec );
-								moveEnd = (r.compareEndPoints( "EndToEnd", rec ) != 0);
-							}
-						}
-						if (reverse)
-						{
-							// Start with the end. Since 'selection direction detection'
-							// doesn't work on IE, don't bother with the IE code here.
-							if (moveEnd)
-							{
-								// If both start and end move and they move leftwards, set start first and then extend instead of starting with setend
-								if (direction == -1)
-								{
-									r.setEnd( ec, 0 );
-								}
-								else
-								{
-									r.setEnd( ec, ec.childNodes.length );
-								}
-								move = true;
-							}
-							if (moveStart)
-							{
-								// Use the extend method to advance the focus
-								// position of the selection.
-								if (direction == 1)
-								{
-									s.extend( sc, sc.childNodes.length );
-									move = false;
-								}
-								else
-								{
-									s.extend( sc, 0 );
-									move = false;
-								}
-							}
-						}
-						else
-						{
-							if (moveStart)
-							{
-								if (direction == 1)
-								{
-									if (r.setStart)
-									{
-										r.setStart( sc, sc.childNodes.length );
-									}
-									else
-									{
-										// ie
-										r.setEndPoint( "StartToEnd", rsc );
-									}
-								}
-								else
-								{
-									if (r.setStart)
-									{
-										r.setStart( sc, 0 );
-									}
-									else
-									{
-										// ie
-										r.setEndPoint( "StartToStart", rsc );
-									}
-								}
-								move = true;
-							}
-							if (moveEnd)
-							{
-								if (direction == -1)
-								{
-									if (r.setEnd)
-									{
-										s.extend( ec, 0 );
-									}
-									else
-									{
-										// ie
-										r.setEndPoint( "EndToStart", rec );
-										move = true;
-									}
-								}
-								else
-								{
-									if (r.setEnd)
-									{
-										s.extend( ec, ec.childNodes.length );
-									}
-									else
-									{
-										// ie
-										r.setEndPoint( "EndToEnd", rec );
-										move = true;
-									}
-								}
-//								move = false;
-							}
-						}
-					}
-					if (move)
-					{
-						// Make sure other modules can react to this event.
-						ed.selection.setRng( r );
-					}
-				}
-			});
-
-			ed.onKeyDown.addToTop( function(ed, e) {
-				var k = e.keyCode, atomicClass;
-				atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic");
-				hasClicked = false;
-				direction = 0;
-				if (k == 37 || k == 38)
-				{
-					direction = -1;
-				}
-				else if (k == 39 || k == 40)
-				{
-					direction = 1;
-				}
-				else if (k == 8)
-				{
-					// Check if an atom is being 'backspace'd
-					if (!atEnd)
-					{
-						var s = ed.selection.getSel();
-						var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
-						if (!select)
-						{
-							var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != 0)?null:s.focusNode):ed.selection.getStart();
-							if (ed.dom.hasClass(ep, atomicClass))
-							{
-								atEnd = ep;
-							}
-							else if (ep)
-							{
-								var rc;
-								if (!s.focusNode)
-								{
-									// ie
-									// focusOffset is not given. So, we need to find
-									// it by walking through the children and comparing
-									// end points.
-									var r = ed.selection.getRng();
-									var r2 = ed.getBody().createTextRange();
-									r2.moveToElementText( ep );
-									if (r2.compareEndPoints( "StartToStart", r ) == 0)
-									{
-										// The caret is at the start of this element.
-										// Find the left neighbouring element
-										rc = t._leftNeighbour( ed, ep );
-									}
-									else
-									{
-										// The focus is not at the start of the parent.
-										// Find the child left of the caret.
-										rc = ep.firstChild;
-										while (rc)
-										{
-											// We are not interested in text nodes
-											// because they cannot be atoms (all by
-											// themselves)
-											if (rc.nodeType == 1)
-											{
-												r2.moveToElementText( rc );
-												if (r.compareEndPoints( "EndToEnd", r2 ) == 0)
-												{
-													break;
-												}
-											}
-											rc = rc.nextSibling;
-										}
-									}
-								}
-								else
-								{
-									if (ep.nodeType == 1 && s.focusOffset != 0)
-									{
-										// ep encloses the current caret position.
-										// select the child to the left of the caret.
-										rc = ep.childNodes[s.focusOffset-1];
-									}
-									else
-									{
-										// The caret is on the left-most end of ep.
-										// Find it's left neighbour.
-										rc = t._leftNeighbour( ed, ep );
-									}
-								}
-								if (rc)
-								{
-									// Now see if this element has a right-most
-									// descendent that is an atom
-									while (rc && !ed.dom.hasClass(rc, atomicClass))
-									{
-										rc = rc.lastChild;
-									}
-									if (rc)
-									{
-										atEnd = rc;
-									}
-								}
-							}
-						}
-					}
-					if (atEnd)
-					{
-						ed.dom.remove( atEnd );
-						atEnd = null;
-						return Event.cancel(e);
-					}
-				}
-				else if (k == 46)
-				{
-					// Check if an atom is being 'delete'd
-					if (!atStart)
-					{
-						var s = ed.selection.getSel();
-						var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
-						if (!select)
-						{
-							var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != s.focusNode.length)?null:s.focusNode):ed.selection.getEnd();
-							if (ed.dom.hasClass(ep, atomicClass))
-							{
-								atStart = ep;
-							}
-							else if (ep)
-							{
-								var lc;
-								if (!s.focusNode)
-								{
-									// ie
-									// focusOffset is not given. So, we need to find
-									// it by walking through the children and comparing
-									// end points.
-									var r = ed.selection.getRng();
-									var r2 = ed.getBody().createTextRange();
-									r2.moveToElementText( ep );
-									if (r2.compareEndPoints( "EndToEnd", r ) == 0)
-									{
-										// The caret is at the start of this element.
-										// Find the right neighbouring element
-										lc = t._rightNeighbour( ed, ep );
-									}
-									else
-									{
-										// The focus is not at the end of the parent.
-										// Find the child right of the caret.
-										lc = ep.firstChild;
-										while (lc)
-										{
-											// We are not interested in text nodes
-											// because they cannot be atoms (all by
-											// themselves)
-											if (lc.nodeType == 1)
-											{
-												r2.moveToElementText( lc );
-												if (r.compareEndPoints( "StartToStart", r2 ) == 0)
-												{
-													break;
-												}
-											}
-											lc = lc.nextSibling;
-										}
-									}
-								}
-								else
-								{
-									if (ep.nodeType == 1 && s.focusOffset != ep.childNodes.length)
-									{
-										// ep encloses the current caret position.
-										// select the child to the right of the caret.
-										lc = ep.childNodes[s.focusOffset];
-									}
-									else
-									{
-										// The caret is on the right-most end of ep.
-										// Find it's right neighbour.
-										lc = t._rightNeighbour( ed, ep );
-									}
-								}
-								if (lc)
-								{
-									// Now see if this element has a left-most
-									// descendent that is an atom
-									while (lc && !ed.dom.hasClass(lc, atomicClass))
-									{
-										lc = lc.firstChild;
-									}
-									if (lc)
-									{
-										atStart = lc;
-									}
-								}
-							}
-						}
-					}
-					if (atStart)
-					{
-						ed.dom.remove( atStart );
-						atStart = null;
-						return Event.cancel(e);
-					}
-				}
-			});
-
-			ed.onMouseDown.addToTop(t._onClick);
-			ed.onMouseUp.addToTop(t._onClick);
-		},
-
-		getInfo : function() {
-			return {
-				longname : 'Atomic elements',
-				author : 'Sander Kruger',
-				authorurl : 'http://www.3gsp.eu',
-				infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/atomic',
-				version : tinymce.majorVersion + "." + tinymce.minorVersion
-			};
-		},
-
-		_onClick : function(ed, e) {
-			direction = 0;
-			hasClicked = true;
-			atStart = null;
-			atEnd = null;
-		},
-		
-		// Helper methods
-		_leftNeighbour : function( ed, e ) {
-			var l = ed.dom.getParent(e, function(n) {
-				return e.previousSibling != null;
-			});
-			if (l)
-			{
-				return l.previousSibling;
-			}
-			return null;
-		},
-
-		_rightNeighbour : function( ed, e ) {
-			var r = ed.dom.getParent(e, function(n) {
-				return e.nextSibling != null;
-			});
-			if (r)
-			{
-				return r.nextSibling;
-			}
-			return null;
-		}
-	});
-
-	// Register plugin
-	tinymce.PluginManager.add('atomic', tinymce.plugins.AtomicPlugin);
-})();
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/js/tinymce/forcecontainer/editor_plugin_src.js	Thu Mar 19 15:58:36 2009 -0500
@@ -0,0 +1,144 @@
+/**
+ * $Id$
+ *
+ * A plugin to handle forcing a selection to an outer container with a given
+ * class.
+ *
+ * Options
+ *
+ * forcecontainer_class:  
+ *
+ *   Elements with this class will be forced to the outer container on certain 
+ *   events.
+ *
+ * forcecontainer_trigger_dnd:
+ *   
+ *   Custom option -- enables triggering of a custom event via jQuery for the
+ *   Drag and Drop Library.
+ */
+
+(function() {
+	var Event = tinymce.dom.Event;
+
+	tinymce.create('tinymce.plugins.ForceContainerPlugin', {
+		getInfo : function() {
+			return {
+				longname : 'Force an element to its outer container',
+				author : 'David Eads',
+				authorurl : 'http://invisibleinstitute.com/eads',
+				infourl : '',
+				version : tinymce.majorVersion + "." + tinymce.minorVersion
+			};
+		},
+
+		init : function(ed, url) {
+			var t = this, forceContainerClass;
+
+			t.editor = ed;
+			forceContainerClass = ed.getParam("forcecontainer_class", "mceForceContainer");
+
+			ed.onNodeChange.addToTop(function(ed, cm, n) {
+				var sc, ec, c;
+
+				// Block if start or end is inside a non editable element
+				sc = ed.dom.getParent(ed.selection.getStart(), function(n) {
+					return ed.dom.hasClass(n, forceContainerClass) && !ed.dom.hasClass(n, 'force-container-processed');
+				});
+
+				ec = ed.dom.getParent(ed.selection.getEnd(), function(n) {
+					return ed.dom.hasClass(n, forceContainerClass) && !ed.dom.hasClass(n, 'force-container-processed');
+				});
+
+				// Block or unblock
+				if (sc || ec) {
+          c = sc ? sc : ec;
+          ed.selection.select(c);
+					t._setDisabled(1);
+					return false;
+				} else
+					t._setDisabled(0);
+			});
+		},
+
+		_block : function(ed, e) {
+			var k = e.keyCode, s = ed.selection, n = s.getNode(), reparent, forceContainerClass;
+
+      // Reparent node 
+      forceContainerClass = ed.getParam("forcecontainer_class", "mceForceContainer");
+
+      // Block if start or end is inside a non editable element
+      sc = ed.dom.getParent(ed.selection.getStart(), function(n) {
+        return ed.dom.hasClass(n, forceContainerClass) && !ed.dom.hasClass(n, 'force-container-processed');
+      });
+
+      ec = ed.dom.getParent(ed.selection.getEnd(), function(n) {
+        return ed.dom.hasClass(n, forceContainerClass) && !ed.dom.hasClass(n, 'force-container-processed');
+      });
+
+      if (sc || ec) {
+        n = (sc) ? sc : ec;
+      }
+
+      // Pass F1-F12, alt, ctrl, shift, page up, page down, arrow keys
+      if ((k > 111 && k < 124) || k == 16 || k == 17 || k == 18 || k == 27 || (k > 32 && k < 41)) {
+        return;
+      }
+      
+      // Step out to parent and delete
+      if (k == 8 || k == 46) {
+			  if (ed.getParam("forcecontainer_trigger_dnd", false)) {
+          // @TODO -- this is getting called twice!!!
+          $('#' + ed.id + '-wrapper iframe').trigger('dnd_delete', { 'node' : n });
+        }
+        ed.execCommand('Delete', false, null);
+      }
+
+      // Typing some common characters 
+      if (k == 13 || (k > 47 && k < 91) || (k > 95 && k < 112) || (k > 185 && k < 223)) {
+        var c = ed.dom.get('__caret'), p;
+        if (!c) {
+          p = ed.dom.create('p', {}, ((k != 13) ? String.fromCharCode(k) : '') + '<span id="__caret">_</span>');
+          ed.dom.insertAfter(p, n);
+          s.select(c);
+        } else {
+          s.select(c);
+          ed.execCommand('Delete', false, null);
+        }
+      }
+      
+			return Event.cancel(e);
+		},
+
+
+		_setDisabled : function(s) {
+			var t = this, ed = t.editor, n = t.container;
+
+			tinymce.each(ed.controlManager.controls, function(c) {
+        c.setDisabled(s);
+			});
+
+			if (s !== t.disabled) {
+				if (s) {
+					ed.onClick.addToTop(t._block);
+					ed.onMouseDown.addToTop(t._block);
+					ed.onKeyDown.addToTop(t._block);
+					ed.onKeyPress.addToTop(t._block);
+					ed.onKeyUp.addToTop(t._block);
+					ed.onPaste.addToTop(t._block);
+				} else {
+					ed.onClick.remove(t._block); // @TODO causing buggy behavior if you click twice
+					ed.onMouseDown.remove(t._block);
+					ed.onKeyDown.remove(t._block);
+					ed.onKeyPress.remove(t._block);
+					ed.onKeyUp.remove(t._block);
+					ed.onPaste.remove(t._block);
+				}
+
+				t.disabled = s;
+			}
+		}
+	});
+
+	// Register plugin
+	tinymce.PluginManager.add('forcecontainer', tinymce.plugins.ForceContainerPlugin);
+})();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/dnd_test/dnd-library-footer.tpl.php	Thu Mar 19 15:58:36 2009 -0500
@@ -0,0 +1,13 @@
+<div class="footer">
+  <div class="pager">
+    <ul>
+      <?php for ($i=1; $i < 6; $i++) {
+        $opts = array('query' => array('page' => $i));
+        if ($page == $i) {
+          $opts['class'] = 'active';
+        };
+        print l($i, 'dnd-test/library', $opts) .' ';
+      } ?>
+    </ul>
+  </div>
+</div>
--- a/modules/dnd_test/dnd_test.module	Tue Mar 17 21:29:00 2009 -0500
+++ b/modules/dnd_test/dnd_test.module	Thu Mar 19 15:58:36 2009 -0500
@@ -53,6 +53,14 @@
       'arguments' => array('i' => NULL),
       'template'  => 'dnd-library-preview',
     ),
+    'dnd_library_header' => array(
+      'arguments' => array('page' => NULL),
+      'template' => 'dnd-library-header',
+    ),
+    'dnd_library_footer' => array(
+      'arguments' => array('page' => NULL),
+      'template' => 'dnd-library-footer',
+    ),
   );
 }
 
@@ -80,6 +88,8 @@
     $editor_representations += dnd_editor_items($i);
     $library_previews['dnd-test-'. $i] = theme('dnd_library_preview', $i);
   }
+  $library .= theme('dnd_library_footer', $page);
+
   return array(
     'library' => $library,
     'editor_representations' => $editor_representations,
@@ -162,3 +172,5 @@
 }
 
 function template_preprocess_dnd_library_header(&$variables) {}
+
+function template_preprocess_dnd_library_footer(&$variables) {}