diff js/dnd-library.js @ 16:bb68dc3ad56f

Major refactor to provide better TinyMCE support and less configuration options, added new jquery dependency, etc.
author David Eads <eads@chicagotech.org>
date Tue, 03 Mar 2009 16:57:39 -0600
parents 7a5f74482ee3
children 1a77f87927dd
line wrap: on
line diff
--- a/js/dnd-library.js	Mon Mar 02 23:22:37 2009 -0600
+++ b/js/dnd-library.js	Tue Mar 03 16:57:39 2009 -0600
@@ -1,18 +1,39 @@
+/**
+ * Drag and Drop Library For Drupal
+ *
+ * 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.
+ */
+
+/**
+ *  Extend jQuery a bit
+ *  
+ *  We add a selector to look for "empty" elements (empty elements in TinyMCE
+ *  often have non-breaking spaces and <br /> tags).
+ */
+(function($) {
+  // Custom selectors
+  $.extend($.expr[":"], {
+    'empty' : function(a, i, m) {
+      var text = $(a).html();
+      text.replace(/\u00a0/g,'');  // Remove &nbsp;
+      $('br', $(text)).remove();   // Remove breaks
+      return !$.trim(text);
+    }
+  });
+}) (jQuery);
+
 Drupal.behaviors.dndLibrary = function(context) {
-  $('.dnd-library-wrapper:not(.dnd-processed)', context).each(function() {
+  $('.dnd-library-wrapper', context).each(function() {
     var $this = $(this);
 
     // This is a bad hack to lop off '-dnd-library' from the id to get the editor name
     var editor = this.id.slice(0, -12); 
 
-    // Attempt to attach library
-    //Drupal.behaviors.dndLibrary.attach_library(false, Drupal.wysiwyg.instances[editor]);
-
-    // Bind Drag and Drop plugin invocation to wywsiwygAttach event
+    // Bind Drag and Drop plugin invocation to events emanating from Wysiwyg
     $('#' + editor).bind('wysiwygAttach', Drupal.behaviors.dndLibrary.attach_library);
-
-    // @TODO track and clear intervals to save memory at the cost of 
-    // more processing?
     $('#' + editor).bind('wysiwygDetach', Drupal.behaviors.dndLibrary.detach_library);
 
     // Ajax pager
@@ -30,25 +51,18 @@
       });
       return false;
     });
-
-    $this.addClass('dnd-processed');
   });
 }
 
+// Dynamically compose a callback based on the editor name
 Drupal.behaviors.dndLibrary.attach_library = function(e, data) {
-  var settings = {
-    renderRepresentation: function(target, drop, representation_id) {
-      return Drupal.settings.dndEditorRepresentations[representation_id];
-    }
-  }
-  settings = $.extend(settings, Drupal.settings.dndEnabledLibraries[data.field]);
-
   var editor_fn = 'attach_' + data.editor;
   if ($.isFunction(window.Drupal.behaviors.dndLibrary[editor_fn])) {
-    window.Drupal.behaviors.dndLibrary[editor_fn](data, settings); 
+    window.Drupal.behaviors.dndLibrary[editor_fn](data, Drupal.settings.dndEnabledLibraries[data.field]); 
   }
 }
 
+// Do garbage collection on detach
 Drupal.behaviors.dndLibrary.detach_library = function(e, data) {
   for (t in $(document).data('dnd_timers')) {
     clearInterval(t);
@@ -56,9 +70,20 @@
   $(document).removeData('dnd_timers');
 }
 
+// Basic textareas
+Drupal.behaviors.dndLibrary.attach_none = function(data, settings) {
+  settings = $.extend({
+    targets: $('#'+ data.field),
+    procressTextAreaDrop: function(target, clicked, representation_id, e, data) {
+      var snippet = Drupal.settings.dndEditorRepresentations[representation_id];
+      $(target).replaceSelection(snippet, true);
+    }
+  }, settings);
+  $(settings.drop_selector).dnd(settings);
+}
 
+// Attach TinyMCE
 Drupal.behaviors.dndLibrary.attach_tinymce = function(data, settings) {
-
   var tiny_instance = tinyMCE.getInstanceById(data.field);
 
   // If the Tiny instance exists, attach directly, otherwise wait until Tiny
@@ -76,27 +101,107 @@
   }
 }
 
+// Really attach TinyMCE
 Drupal.behaviors.dndLibrary._attach_tinymce = function(data, settings, tiny_instance) {
+  var ed = tiny_instance, dom = ed.dom, s = ed.selection;
+
   settings = $.extend({
     targets: $('#'+ data.field +'-wrapper iframe'),
-    insertAfter: '<p><span id="__caret">_</span></p>',
+    idSelector: function(element) { 
+      if ($(element).is('img')) {
+        return $.url.setUrl(element.src).param('dnd_id');
+      }
+      return false;
+    }, 
+    interval: 100,
+    processIframeDrop: function(target, dragged, dropped, representation_id) {
+      var representation = Drupal.settings.dndEditorRepresentations[representation_id];
+      var $target = $(target), $dropped = $(dropped), $dragged = $(dragged), block;
 
-    // Back out markup to render in place after parent container
-    preprocessDrop: function(target, drop) {
-      drop.id = 'DND-TMP-' + $.data(drop);
-      // Do some native tiny manipulations
-      return drop;
-    },
-    postprocessDrop: function(target, drop, element) {
-      // Get our special span, select it, delete it, and hope the caret
-      // resets correctly.
-      tiny_instance.selection.select(tiny_instance.dom.get('__caret'));
-      tiny_instance.execCommand('Delete', false, null);
-      tiny_instance.dom.remove('__caret');
+      // Search through block level parents
+      $dropped.parents().each(function() {
+        var $this = $(this);
+        if ($this.css('display') == 'block') {
+          block = this;
+          return false;
+        }
+      });
+
+      // Remove dropped item
+      $dropped.remove();
+
+      // Create an element to insert
+      var insert = dom.create('p', {'class' : 'dnd-dropped-wrapper', 'id' : 'dnd-inserted'}, representation);
+
+      // The no-parent case
+      if ($(block).is('body')) {
+        
+        s.setNode(insert);
+      }
+      else {
+        var old_id = block.id;
+        block.id = 'target-block';
+        $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>');
+
+        // The active target block should be empty
+        if ($('#target-block:empty', $target.contents()).length > 0) {
+          var c = dom.get('target-block');
+          s.select(dom.get('target-block'));
+          ed.execCommand('Delete', false, null);
+          dom.remove(c);
+        } else if (old_id) {
+          block.id = old_id;
+        } else {
+          $block.removeAttr('id');
+        }
+      }
+
+      var $inserted = $('#dnd-inserted', $target.contents());
+      var inserted = $inserted.get(0);
+
+      // Look behind in the DOM
+      var previous = $inserted.prev().get(0);
+
+      // 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);
+      }
+
+      // Look ahead in the DOM
+      var next = $inserted.next().get(0);
+
+      // 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')) {
+        $(next).prepend('<span id="__caret">_</span>');
+      }
+      else {
+        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);
 
       // Add some classes to the library items
-      $(element).addClass('dnd-inserted'); 
-      $(element).parents('.editor-item').addClass('dnd-child-inserted');
+      $(dragged).addClass('dnd-inserted'); 
+      drag_parents = $(dragged).parents('.editor-item');
+      drag_parents.addClass('dnd-child-inserted');
     }
   }, settings);