view js/dnd.js @ 14:ef7ad7b5baa4

Slightly changed matching mechanism to handle Firefox's crazy desire to make dropped src attributes relative.
author David Eads <eads@chicagotech.org>
date Fri, 27 Feb 2009 12:30:42 -0600
parents a5b2b9fa2a1a
children 7a5f74482ee3
line wrap: on
line source
/* jQuery Drag and Drop Library for Rich Editors
 *
 * A helper library which provides the ability to drag and drop images to Rich 
 * Text Editors (RTEs) that use the embedded iframe + DesignMode method, and
 * provides a simple "clicky" interface for inserting the same markup directly
 * into a textarea.  
 *
 * Basic usage:
 *
 * $('img.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.
 *
 * interval:
 *   How often to check the iframe for a drop, in milliseconds.
 *
 * Usage notes:
 *
 * This is a very tricky problem and to achieve cross browser (as of writing, 
 * IE, Safari, and Firefox) support, severe limitations must be made on the 
 * nature of DOM elements that are dropped.
 *
 * Droppable elements must be <img> tags.  Internet Explorer will not accept
 * anchors, and Safari strips attributes and additional markup from
 * dropped anchors, images, and snippets.
 *
 * While the idSelector option allows you to parse the dropped element for
 * any attribute that "comes along" with the element when it is dropped in a
 * designmode-enabled iframe, the only safe attribute to scan is the 'src' 
 * attribute, and to avoid strange "relativization" of links, the src should
 * always be expressed as an absolute url with a fully qualified domain name.
 *
 * Implementation notes and todos:
 *
 * Currently, there is no garbage collection instituted for the many many
 * timers that are created, so memory usage could become as issue in scenarios
 * where the user does a lot of paging or otherwise winds up invoking 
 * drag and drop on large numbers of elements.
 */

(function($) {
  $.fn.dnd = function(opt) {
    opt = $.extend({}, {
      dropWrapper: '<p class="dnd-dropped"></p>',
      insertBefore: false,
      insertAfter: false,
      processedClass:  'dnd-processed',
      interval: 100,

      processTargets: function(targets) {
        return targets.each(function() {
          //$('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>');
          //@TODO use jQuery.rules()
          return this;
        });
      },

      // Must return a string
      idSelector: function(element) { 
        if ($(element).is('img')) {
          return $.url.setUrl(element.src).param('dnd_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) {
        return drop;
        var old_parent = false;
        var element_id = '';

        var parents = drop.parents();
        for (var 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, element) { 
        $(element).addClass('dnd-inserted'); 
      }
                      
    }, opt);

    // Initialize plugin
    var targets = opt.processTargets(opt.targets);

    // Process!
    return this.each(function() {
      if ($(this).is('img')) {
        var element = this;

        // If we don't have a proper id, bail
        var representation_id = opt.idSelector(element);

        if (!representation_id) {
          return this;
        };

        // 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;
            var selector = 'img[src='+ element.src +']';

            // Watch the iframe for changes
            var t = setInterval(function() {              
              $('img', $(target).contents()).each(function() {
                if (opt.idSelector(this) == representation_id) {
                  var drop = opt.preprocessDrop(target, $(this)); // Must return a jquery object
                  var representation = opt.renderRepresentation(target, drop, representation_id);
                  if (representation) {
                    if (opt.dropWrapper) {
                      drop.wrap(opt.dropWrapper);
                    }
                    if (opt.insertBefore) {
                      drop.before(opt.insertBefore);
                    }
                    if (opt.insertAfter) {
                      drop.after(opt.insertAfter);
                    }
                    drop.replaceWith(representation);
                    opt.postprocessDrop(target, drop, element);
                  }
                }
              });
            }, opt.interval);
            // @TODO track the timer with $.data() so we can clear it?
          } else if ($(this).is('textarea')) {
            //console.log('@TODO handle textareas via.... regexp?');
          }
        });
      }
    });
  };
})(jQuery);