view js/jquery.draganddrop.js @ 19:d83073a08b25

Added beautytips, and advanced library behavior to the library prototype.
author David Eads <eads@chicagotech.org>
date Fri, 06 Mar 2009 14:26:06 -0600
parents 0d557e6e73f7
children 89fe0aca43d4
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. DnD
 * also 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 (Required):
 *   A jQuery object corresponding to the proper iframe(s) and/or textarea(s)
 *   that are allowed drop targets for the current set of elements.
 *
 * idSelector:
 *  A callback that parses out the unique ID of an image that is dropped in an 
 *  iframe.  While some browsers (such as Firefox and Internet Explorer) allow
 *  markup to be copied when dragging and dropping, Safari (and assumably
 *  other webkit based browsers) don't.  The upshot is that the safest bet is
 *  to parse the element's src URL.  Because querystrings seem to drop 
 *  consistently across 
 *
 * processIframeDrop:
 *   A callback that defines the mechanism for inserting and rendering the 
 *   dropped item in an iframe.  The typical usage pattern I expect to see is
 *   that implementers will listen for their RTE to load and then invoke DnD
 *   with a processIframeDrop appropriate to their editor.
 *
 * processTextAreaDrop:
 *   A callback that defines the mechanism for inserting and rendering the
 *   clicked item in a textarea.  The default function just tries to insert
 *   some markup at the caret location in the current textarea.
 *
 * interval:
 *   How often to check the iframe for a drop, in milliseconds.
 *
 * 
 *
 * Usage notes:
 *
 * Due to cross browser flakiness, there are many many limitations on what is
 * possible in terms of dragging and dropping to DesignMode enabled iframes.
 *
 * To make this work, your "droppable" elements must be image tags.  No ifs, ands, 
 * or buts:  image tags have the best cross browser behavior when dragging.
 *
 * When DnD is initialized, it begins timers which periodically check your 
 * editor iframe.  If an image is dropped in, DnD takes the element and 
 * attempts to parse it for a unique ID which you can use to do a lookup for 
 * an "editor representation."  If an editor representation is found, 
 * typically the image is replaced with the representation snippet, but you are
 * free to do whatever you want.
 *
 * Because of browser limitations, the safest way to parse the element is to
 * look at the img's src attribute and use some or all of the image URL. 
 * In my experience, the best way to deal with this is simply to parse out
 * generate a query string in the src attribute server side that corresponds 
 * to the proper representation ID.
 *
 * If the target is not an iframe but a textarea, DnD provides a very minimal
 * system for clicking what would otherwise be dragged to insert markup into 
 * the textarea.
 *
 * DnD is purely a utility library:  to make it work for any particular editor,
 * CMS, etc:  It is very unlikely it will provide much benefit out of the box.
 *
 * Because DnD spawns so many timers, they are stored in a $.data element 
 * attached to the parent document body.  Implementers should consider clearing
 * the timers when building systems such as dynamic searches or paging 
 * functionality to ensure memory usage and performance remains stable.
 */

(function($) {
  $.fn.dnd = function(opt) {
    opt = $.extend({}, {
      interval: 250,
      targets: $('iframe, textarea'),
      processTargets: function(targets) {
        return targets.each(function() {
          $('head', $(this).contents()).append('<style type="text/css">img { display: none; } img.dnd-dropped {display: block; }</style>');
          return this;
        });
      },
      idSelector: function(element) { 
        if ($(element).is('img')) {
          return element.src;
        }
        return false;
      }, 
      processIframeDrop: function(target, dragged, dropped, 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);
        $(dropped).replaceWith('<p id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</p>');
      },
      processTextAreaDrop: function(target, clicked, representation_id, e, data) {
        var snippet = '<div><img src="'+ representation_id +'" /></div>';
        $(target).replaceSelection(snippet, true);
        e.preventDefault();
      }

    }, opt);

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

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

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

        if (!representation_id) {
          return this;
        };

        // Add a special class
        $(element).addClass('dnd-processed');

        // We need to differentiate behavior based on the targets... I guess.
        targets.each(function() {
          var target = this, $target = $(target);
          if ($target.is('iframe')) {

            // Indicate this element is draggy
            $element.css('cursor', 'move');

            // Watch the iframe for changes
            var t = setInterval(function() {              
              $('img:not(.dnd-dropped)', $(target).contents()).each(function() {
                var dropped = this;
                if (opt.idSelector(dropped) == representation_id) {
                  opt.processIframeDrop(target, element, dropped, representation_id);
                }
              });
            }, opt.interval);

            // Track current active timers -- developers working with DnD
            // can implement their own garbage collection for specific 
            // interactions, such as paging or live search.
            var data = $(document).data('dnd_timers');
            if (data) {
              data[data.length] = t;
            } else {
              data = new Array();
              data[0] = t;
            }
            $(document).data('dnd_timers', data);

          } else if ($target.is('textarea')) {
            $element.css('cursor', 'pointer');
            $(element).click(function(e, data) {
              opt.processTextAreaDrop(target, element, representation_id, e, data);
            });
          }
        });
      }
    });
  };
})(jQuery);