view js/dnd-library.js @ 17:1a77f87927dd

Fixed :empty custom selector behavior in IE, a little refactoring, etc.
author David Eads <eads@chicagotech.org>
date Wed, 04 Mar 2009 13:29:31 -0600
parents bb68dc3ad56f
children 0d557e6e73f7
line wrap: on
line source
/**
 * 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).  An exception is required
 *  to make this work in IE.
 */
(function($) {
  // Custom selectors
  $.extend($.expr[":"], {
    'empty' : function(a, i, m) {
      return !$(a).filter(function(i) {
        return !$(this).is('br');
      }).length && !$.trim(a.textContent || a.innerText||$(a).text()||"");
    }
  });
}) (jQuery);

Drupal.behaviors.dndLibrary = function(context) {
  $('.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)); 


    // Bind Drag and Drop plugin invocation to events emanating from Wysiwyg
    $editor.bind('wysiwygAttach', Drupal.behaviors.dndLibrary.attach_library);
    $editor.bind('wysiwygDetach', Drupal.behaviors.dndLibrary.detach_library);

    // Add basic hover behavior to editor items
    $('.editor-item', context).hover(function() {
      var $this = $(this);
      var p = $('#edit-body-wrapper').position();
      var preview = $(Drupal.settings.dndLibraryPreviews[this.id]).css({
        'position' : 'absolute',
        'top'      : p.top + 150,
        'left'     : p.left + $('#edit-body-wrapper').width() + 150,
        'display'  : 'none',
        'width'    : '300px',
        'background-color' : '#ddd',
        'border' : '3px solid #999',
        'padding' : '4px',
        'z-index'  : 10
      });
      $('body').prepend(preview);
      preview.fadeIn('slow');
    }, function() {
      $('#' + this.id.replace(/test/,'preview')).fadeOut('fast', function() { $(this).remove(); });
    });

    // Ajax pager
    $('.pager a', $this).click(function() {
      $.getJSON(this.href, function(data) {
        Drupal.behaviors.dndLibrary.refreshLibrary.call($this.get(0), data, $editor);
      });

      // Reattach behaviors
      Drupal.behaviors.dndLibrary(); 
      
      return false;
    });

    // Preload images in editor representations
    var cached = $.data($editor, 'dnd_preload') || {};
    for (editor_id in Drupal.settings.dndEditorRepresentations) {
      if (!cached[editor_id]) {
        $representation = $(Drupal.settings.dndEditorRepresentations[editor_id]);
        if ($representation.is('img') && $representation.get(0).src) { 
          $representation.attr('src', $representation.get(0).src);
        } else {
          $('img', $representation).each(function() {
            this.attr('src', this.src);
          });
        }
      }
    }
    $.data($editor, 'dnd_preload', cached);
  });
}

Drupal.behaviors.dndLibrary.refreshLibrary = function(data, editor) {
  $this = $(this);

  $('.header', $this).html(data.header);
  $('.library', $this).html(data.library);
  $('.footer', $this).html(data.footer);

  var params = Drupal.wysiwyg.instances[editor.get(0).id];
  editor.trigger('wysiwygDetach', params);
  editor.trigger('wysiwygAttach', params);

  for (editor_id in data.editor_representations) {
    Drupal.settings.dndEditorRepresentations[editor_id] = data.editor_representations[editor_id];
  }
  for (preview_id in data.library_previews) {
    Drupal.settings.dndLibraryPreviews[preview_id] = data.library_previews[preview_id];
  }
}


// Dynamically compose a callback based on the editor name
Drupal.behaviors.dndLibrary.attach_library = function(e, data) {
  var settings = $.extend({idSelector: Drupal.behaviors.dndLibrary.idSelector}, 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); 
  }
}

// Do garbage collection on detach
Drupal.behaviors.dndLibrary.detach_library = function(e, data) {
  for (t in $(document).data('dnd_timers')) {
    clearInterval(t);
  }
  $(document).removeData('dnd_timers');
}

// Basic textareas
Drupal.behaviors.dndLibrary.attach_none = function(data, settings) {
  settings = $.extend({
    targets: $('#'+ data.field),
    processTextAreaDrop: function(target, clicked, representation_id, e, data) {
      var snippet = '<p class="dnd-dropped-wrapper">' + Drupal.settings.dndEditorRepresentations[representation_id] + '</p>';
      $(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
  // has registered a new instance.
  if (tiny_instance) { 
    Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance);
  } else {
    var t = setInterval(function() {
      var tiny_instance = tinyMCE.getInstanceById(data.field);
      if (tiny_instance) {
        Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance);
        clearInterval(t);
      }
    }, 100);
  }
}

Drupal.behaviors.dndLibrary.idSelector = function(element) { 
  if ($(element).is('img')) {
    return $.url.setUrl(element.src).param('dnd_id');
  }
  return false;
}

// 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'),
    interval: 100,
    processIframeDrop: function(target, dragged, dropped, representation_id) {
      var representation = Drupal.settings.dndEditorRepresentations[representation_id];
      var $target = $(target), $dropped = $(dropped), $dragged = $(dragged), block;

      // 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) {
          $('#target-block', $target.contents()).remove();
        } 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
      $(dragged).addClass('dnd-inserted'); 
      drag_parents = $(dragged).parents('.editor-item');
      drag_parents.addClass('dnd-child-inserted');
    }
  }, settings);

  $(settings.drop_selector).dnd(settings);
}