changeset 3:5df0783706f7

Reorganized layout of module and repo a bit.
author David Eads <eads@chicagotech.org>
date Tue, 17 Feb 2009 15:46:36 -0600
parents 5a44c430b7ac
children c2eb995212bf
files dnd.module js/dnd.js
diffstat 2 files changed, 197 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/dnd.module	Tue Feb 17 15:34:56 2009 -0600
+++ b/dnd.module	Tue Feb 17 15:46:36 2009 -0600
@@ -77,8 +77,8 @@
 function dnd_process_textarea($element, $form_state) {
   if ($element['#dnd-enabled']) {
 
-    drupal_add_js(drupal_get_path('module', 'dnd') .'/dnd/dnd.js');
-    drupal_add_js(drupal_get_path('module', 'dnd') .'/js/dnd-library.js', 'footer');
+    drupal_add_js(drupal_get_path('module', 'dnd') .'/js/dnd.js');
+    drupal_add_js(drupal_get_path('module', 'dnd') .'/js/dnd-library.js');
 
     $settings = array();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/js/dnd.js	Tue Feb 17 15:46:36 2009 -0600
@@ -0,0 +1,195 @@
+/* jQuery Drag and Drop Library for Rich Editors
+ *
+ * This library exists to provide a jQuery plugin that manages dragging
+ * and dropping of page assets into textareas and rich-text editors which use
+ * the iframe + designMode method.
+ *
+ * This plugin has a rather elaborate set of considerations based on common
+ * configurations of rich text editors and serious disparity in how browsers
+ * handle dragging and dropping assets into an iframe.
+ *
+ * The plugin scans an iframe with an embedded document with designMode enabled
+ * and tries to detect content that was injected by dragging and dropping
+ * within the browser.  If it detects an injection of content, it attempts to 
+ * conjure up an "editor representation" of the content based on the markup
+ * passed in.
+ *
+ * This works because links and images drop with their full HTML syntax in-tact
+ * across most browsers and platforms.  This means we can set a timer on the
+ * iframe that scans for the insertion of the markup and replace it with
+ * another HTML snippet, as well as triggering actions inside the editor itself
+ * (this will perhaps be handled by a set of editor-specific plugins).
+ *
+ * Because of the mechanism of operation, it is expected that the iframe
+ * contain CSS which hides the markup dropped in to create the illusion of
+ * seamlessly dropping in the "editor representation" of the dropped item.
+ *
+ * It is expected that the implementer will be parsing the input text for 
+ * the proper markup on the server side and making some decisions about how 
+ * to parse and handle that markup on load and save.  
+ *
+ * Of special interest is graceful degradation.  There is no ideal graceful
+ * degradation path at this time.  Every mainstream browser except IE 6 and 
+ * IE 7 drop links and images into textareas with their href and src URIs, 
+ * respectively, including querystrings.  That means that the image or link
+ * url can be used for parsing, or a querystring included.  But it won't work
+ * in Internet Explorer, and probably will require a server-side browser check
+ * in full blown implementations of the library system.
+ *
+ * Basic usage:
+ *
+ * $('a.my-class').dnd({targets: $('#my-iframe')});
+ *
+ * Options:
+ *
+ * targets
+ *   A jQuery object corresponding to the proper iframe(s) to enable dragging
+ *   and dropping for. [Required]
+ *
+ * dropWrapper
+ *   An html snippet to wrap the entire inserted content with in the editor 
+ *   representation (i.e. '<p class="foo"></p>')
+ *
+ * insertBefore: 
+ *   Markup to insert before drop (i.e. '<hr />')
+ *
+ * insertAfter:  
+ *   Markup to insert after drop (i.e. '<div class="clearfix"></div>')
+ *
+ * processedClass:  
+ *   The class to apply to links and images tagged as droppable.  This class 
+ *   should have a style rule in the editor that sets display to 'none' for
+ *   the best experience.
+ *
+ * idSelector:  
+ *   A callback that parses a unique id out of a droppable element. B y default
+ *   this uses the id of the element, but one could parse out an ID based on 
+ *   any part of the URL, interior markup, etc.
+ *
+ * renderRepresentation:
+ *   A callback that defines the mechanism for rendering a representation of 
+ *   the content.  The default is currently essential useless.
+ *
+ * preprocessDrop:
+ *   A callback that preprocesses the dropped snippet in the iframe, before 
+ *   replacing it.  By default this uses a little logic to walk up the DOM 
+ *   tree to topmost parent of the place in the source where the item was 
+ *   dropped, and add the dropped element after that parent instead of 
+ *   inside it.
+ *
+ * postprocessDrop:
+ *   A callback that postprocesses the iframe.
+ *
+ */
+
+(function($) {
+  $.fn.dnd = function(opt) {
+    opt = $.extend({}, {
+      dropWrapper: '<p class="dnd-inserted"></p>',
+      insertBefore: '',
+      insertAfter: '',
+      processedClass:  'dnd-processed',
+
+      processTargets: function(targets) {
+        return targets.each(function() {
+          $('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>');
+          return this;
+        });
+      },
+
+      // Must return a string
+      idSelector: function(element) { 
+        if (!element.id) {
+          // @TODO sanitize output here
+          if ($(element).is('a')) {
+            return element.href;
+          }
+          if ($(element).is('img')) {
+            return element.src;
+          }
+        }
+        return element.id; 
+      }, 
+
+      // @TODO:  Target should be jQuery object
+      targets: $('iframe, textarea'),
+
+      // Must return a string that DOES NOT share the id of any droppable item
+      // living outside the iframe
+      renderRepresentation: function(target, drop, representation_id) {
+        // Keep a counter of how many times this element was used
+        var count = $.data(target, representation_id +'_count');
+        if (!count) { 
+          count = 1; 
+        } else { 
+          count++;
+        }
+        $.data(target, representation_id +'_count', count);
+        return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>';
+      },
+
+      // Back out markup to render in place after parent container
+      preprocessDrop: function(target, drop) {
+        var old_parent = false;
+        var element_id = '';
+
+        var parents = drop.parents();
+        for (i=0; i < parents.length; i++) {
+          if ($(parents[i]).is('body')) {
+            element_id = $(drop).get(0).id;
+            $(old_parent).after(drop.clone());
+            drop.remove();
+          }
+          old_parent = parents[i];
+        }
+        return $('#'+ element_id, $(target).contents());
+      },
+
+      postprocessDrop: function(target, drop) { return; }
+                      
+    }, opt);
+
+    // Initialize plugin
+    var targets = opt.processTargets(opt.targets);
+
+    // Process!
+    return this.each(function() {
+
+      var element = this;
+      var representation_id = opt.idSelector(element);
+
+      if (!element.id) {
+        element.id = element.tagName.toLowerCase() + '-' + representation_id;
+      }
+
+      // Add some UI sugar and a special class
+      $(element)
+        .css('cursor', 'move')
+        .addClass(opt.processedClass);
+
+      // We need to differentiate behavior based on the targets... I guess.
+      targets.each(function() {
+        if ($(this).is('iframe')) {
+          var target = this;
+
+          // Watch the iframe for changes
+          t = setInterval(function() {
+            var match = $('#' + element.id, $(target).contents());
+            if (match.length > 0) {
+              drop = opt.preprocessDrop(target, match); // Must return a jquery object
+              drop
+                .before(opt.insertBefore)
+                .after(opt.insertAfter)
+                .wrap(opt.dropWrapper)
+                .replaceWith(opt.renderRepresentation(target, drop, representation_id));
+              opt.postprocessDrop(target, drop);
+            }
+          }, 100);
+          // @TODO track the timer with $.data() so we can clear it?
+        } else if ($(this).is('textarea')) {
+          console.log('@TODO handle textareas via.... regexp?');
+        }
+      });
+    });
+  }
+})(jQuery);