view js/dnd.js @ 4:c2eb995212bf

TONS of fixes in this commit.
author David Eads <eads@chicagotech.org>
date Thu, 19 Feb 2009 12:33:19 -0600
parents 5df0783706f7
children 99ba5941779c
line wrap: on
line source
/* 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-dropped"></p>',
      insertBefore: '',
      insertAfter: '',
      processedClass:  'dnd-processed',
      disableClick: true,

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

    // Initialize plugin
    var targets = opt.processTargets(opt.targets);
    if (opt.disableClick) { this.click(function() { return false; }) }

    // 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, element);
            }
          }, 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);