eads@3: /* jQuery Drag and Drop Library for Rich Editors
eads@3: *
eads@12: * A helper library which provides the ability to drag and drop images to Rich
eads@12: * Text Editors (RTEs) that use the embedded iframe + DesignMode method, and
eads@12: * provides a simple "clicky" interface for inserting the same markup directly
eads@12: * into a textarea.
eads@3: *
eads@3: * Basic usage:
eads@3: *
eads@12: * $('img.my-class').dnd({targets: $('#my-iframe')});
eads@3: *
eads@3: * Options:
eads@3: *
eads@3: * targets
eads@3: * A jQuery object corresponding to the proper iframe(s) to enable dragging
eads@3: * and dropping for. [Required]
eads@3: *
eads@3: * dropWrapper
eads@3: * An html snippet to wrap the entire inserted content with in the editor
eads@3: * representation (i.e. '
')
eads@3: *
eads@3: * insertBefore:
eads@3: * Markup to insert before drop (i.e. '
')
eads@3: *
eads@3: * insertAfter:
eads@3: * Markup to insert after drop (i.e. '')
eads@3: *
eads@3: * processedClass:
eads@3: * The class to apply to links and images tagged as droppable. This class
eads@3: * should have a style rule in the editor that sets display to 'none' for
eads@3: * the best experience.
eads@3: *
eads@3: * idSelector:
eads@3: * A callback that parses a unique id out of a droppable element. B y default
eads@3: * this uses the id of the element, but one could parse out an ID based on
eads@3: * any part of the URL, interior markup, etc.
eads@3: *
eads@3: * renderRepresentation:
eads@3: * A callback that defines the mechanism for rendering a representation of
eads@3: * the content. The default is currently essential useless.
eads@3: *
eads@3: * preprocessDrop:
eads@3: * A callback that preprocesses the dropped snippet in the iframe, before
eads@3: * replacing it. By default this uses a little logic to walk up the DOM
eads@3: * tree to topmost parent of the place in the source where the item was
eads@3: * dropped, and add the dropped element after that parent instead of
eads@3: * inside it.
eads@3: *
eads@3: * postprocessDrop:
eads@3: * A callback that postprocesses the iframe.
eads@3: *
eads@12: * interval:
eads@12: * How often to check the iframe for a drop, in milliseconds.
eads@12: *
eads@12: * Usage notes:
eads@12: *
eads@12: * This is a very tricky problem and to achieve cross browser (as of writing,
eads@12: * IE, Safari, and Firefox) support, severe limitations must be made on the
eads@12: * nature of DOM elements that are dropped.
eads@12: *
eads@12: * Droppable elements must be
tags. Internet Explorer will not accept
eads@12: * anchors, and Safari strips attributes and additional markup from
eads@12: * dropped anchors, images, and snippets.
eads@12: *
eads@12: * While the idSelector option allows you to parse the dropped element for
eads@12: * any attribute that "comes along" with the element when it is dropped in a
eads@12: * designmode-enabled iframe, the only safe attribute to scan is the 'src'
eads@12: * attribute, and to avoid strange "relativization" of links, the src should
eads@12: * always be expressed as an absolute url with a fully qualified domain name.
eads@12: *
eads@12: * Implementation notes and todos:
eads@12: *
eads@12: * Currently, there is no garbage collection instituted for the many many
eads@12: * timers that are created, so memory usage could become as issue in scenarios
eads@12: * where the user does a lot of paging or otherwise winds up invoking
eads@12: * drag and drop on large numbers of elements.
eads@3: */
eads@3:
eads@3: (function($) {
eads@3: $.fn.dnd = function(opt) {
eads@3: opt = $.extend({}, {
eads@4: dropWrapper: '',
eads@12: insertBefore: false,
eads@12: insertAfter: false,
eads@3: processedClass: 'dnd-processed',
eads@12: interval: 100,
eads@3:
eads@3: processTargets: function(targets) {
eads@3: return targets.each(function() {
eads@12: //$('head', $(this).contents()).append('');
eads@12: //@TODO use jQuery.rules()
eads@3: return this;
eads@3: });
eads@3: },
eads@3:
eads@3: // Must return a string
eads@3: idSelector: function(element) {
eads@12: if ($(element).is('img')) {
eads@12: return $.url.setUrl(element.src).param('dnd_id');
eads@3: }
eads@3: },
eads@3:
eads@3: // @TODO: Target should be jQuery object
eads@3: targets: $('iframe, textarea'),
eads@3:
eads@3: // Must return a string that DOES NOT share the id of any droppable item
eads@3: // living outside the iframe
eads@3: renderRepresentation: function(target, drop, representation_id) {
eads@3: // Keep a counter of how many times this element was used
eads@3: var count = $.data(target, representation_id +'_count');
eads@3: if (!count) {
eads@3: count = 1;
eads@3: } else {
eads@3: count++;
eads@3: }
eads@3: $.data(target, representation_id +'_count', count);
eads@3: return '' + representation_id + '';
eads@3: },
eads@3:
eads@3: // Back out markup to render in place after parent container
eads@3: preprocessDrop: function(target, drop) {
eads@12: return drop;
eads@3: var old_parent = false;
eads@3: var element_id = '';
eads@3:
eads@3: var parents = drop.parents();
eads@11: for (var i=0; i < parents.length; i++) {
eads@3: if ($(parents[i]).is('body')) {
eads@3: element_id = $(drop).get(0).id;
eads@3: $(old_parent).after(drop.clone());
eads@3: drop.remove();
eads@3: }
eads@3: old_parent = parents[i];
eads@3: }
eads@3: return $('#'+ element_id, $(target).contents());
eads@3: },
eads@3:
eads@4: postprocessDrop: function(target, drop, element) {
eads@4: $(element).addClass('dnd-inserted');
eads@4: }
eads@3:
eads@3: }, opt);
eads@3:
eads@3: // Initialize plugin
eads@3: var targets = opt.processTargets(opt.targets);
eads@3:
eads@3: // Process!
eads@3: return this.each(function() {
eads@12: if ($(this).is('img')) {
eads@12: var element = this;
eads@3:
eads@12: // If we don't have a proper id, bail
eads@12: var representation_id = opt.idSelector(element);
eads@3:
eads@12: if (!representation_id) {
eads@12: return this;
eads@12: };
eads@12:
eads@12: // Add some UI sugar and a special class
eads@12: $(element)
eads@12: .css('cursor', 'move')
eads@12: .addClass(opt.processedClass);
eads@12:
eads@12: // We need to differentiate behavior based on the targets... I guess.
eads@12: targets.each(function() {
eads@12: if ($(this).is('iframe')) {
eads@12: var target = this;
eads@12: var selector = 'img[src='+ element.src +']';
eads@12:
eads@12: // Watch the iframe for changes
eads@12: var t = setInterval(function() {
eads@14: $('img', $(target).contents()).each(function() {
eads@14: if (opt.idSelector(this) == representation_id) {
eads@14: var drop = opt.preprocessDrop(target, $(this)); // Must return a jquery object
eads@14: var representation = opt.renderRepresentation(target, drop, representation_id);
eads@14: if (representation) {
eads@14: if (opt.dropWrapper) {
eads@14: drop.wrap(opt.dropWrapper);
eads@14: }
eads@14: if (opt.insertBefore) {
eads@14: drop.before(opt.insertBefore);
eads@14: }
eads@14: if (opt.insertAfter) {
eads@14: drop.after(opt.insertAfter);
eads@14: }
eads@14: drop.replaceWith(representation);
eads@14: opt.postprocessDrop(target, drop, element);
eads@12: }
eads@12: }
eads@14: });
eads@12: }, opt.interval);
eads@12: // @TODO track the timer with $.data() so we can clear it?
eads@12: } else if ($(this).is('textarea')) {
eads@12: //console.log('@TODO handle textareas via.... regexp?');
eads@12: }
eads@12: });
eads@3: }
eads@3: });
eads@11: };
eads@3: })(jQuery);