eads@20: /** eads@20: * 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@16: * Text Editors (RTEs) that use the embedded iframe + DesignMode method. DnD eads@16: * also provides a simple "clicky" interface for inserting the same markup eads@16: * directly into a textarea. eads@3: * eads@3: * Basic usage: eads@3: * eads@20: * $('img.my-draggable-class').dnd({targets: $('#my-rte-iframe')}); eads@3: * eads@3: * Options: eads@3: * eads@16: * targets (Required): eads@16: * A jQuery object corresponding to the proper iframe(s) and/or textarea(s) eads@16: * that are allowed drop targets for the current set of elements. eads@3: * eads@16: * idSelector: eads@16: * A callback that parses out the unique ID of an image that is dropped in an eads@16: * iframe. While some browsers (such as Firefox and Internet Explorer) allow eads@16: * markup to be copied when dragging and dropping, Safari (and assumably eads@16: * other webkit based browsers) don't. The upshot is that the safest bet is eads@16: * to parse the element's src URL. Because querystrings seem to drop eads@20: * consistently across browsers, encoding the id in a query string is a good eads@20: * option. eads@3: * eads@16: * processIframeDrop: eads@16: * A callback that defines the mechanism for inserting and rendering the eads@16: * dropped item in an iframe. The typical usage pattern I expect to see is eads@16: * that implementers will listen for their RTE to load and then invoke DnD eads@20: * with a processIframeDrop appropriate to their editor. For performance eads@20: * reasons, this only runs once per invocation of DnD -- to compensate, we eads@20: * must pass in an idselector callback as an argument. eads@3: * eads@20: * processTextAreaClick: eads@16: * A callback that defines the mechanism for inserting and rendering the eads@16: * clicked item in a textarea. The default function just tries to insert eads@16: * some markup at the caret location in the current textarea. eads@3: * eads@20: * iframeTargetClass: eads@20: * A class to add to a draggable item if it can be dragged to an iframe. eads@20: * eads@20: * textareaTargetClass: eads@20: * A class to add to a draggable item if it can be dragged to a textarea. eads@20: * eads@24: * processedClass: eads@20: * A class to add to draggable items processed by DnD. eads@20: * eads@24: * droppedClass: eads@24: * A class to add to images that are dropped (by default, this is used to eads@24: * ensure that a successful drop manifests the editor representation only eads@24: * once.) eads@24: * 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@16: * Due to cross browser flakiness, there are many many limitations on what is eads@16: * possible in terms of dragging and dropping to DesignMode enabled iframes. eads@12: * eads@16: * To make this work, your "droppable" elements must be image tags. No ifs, ands, eads@16: * or buts: image tags have the best cross browser behavior when dragging. eads@12: * eads@20: * When DnD is invoked, it begins a timer which periodically checks your eads@16: * editor iframe. If an image is dropped in, DnD takes the element and eads@16: * attempts to parse it for a unique ID which you can use to do a lookup for eads@16: * an "editor representation." If an editor representation is found, eads@16: * typically the image is replaced with the representation snippet, but you are eads@16: * free to do whatever you want. eads@12: * eads@16: * Because of browser limitations, the safest way to parse the element is to eads@16: * look at the img's src attribute and use some or all of the image URL. eads@16: * In my experience, the best way to deal with this is simply to parse out eads@16: * generate a query string in the src attribute server side that corresponds eads@16: * to the proper representation ID. eads@12: * eads@16: * If the target is not an iframe but a textarea, DnD provides a very minimal eads@16: * system for clicking what would otherwise be dragged to insert markup into eads@16: * the textarea. eads@16: * eads@16: * Because DnD spawns so many timers, they are stored in a $.data element eads@16: * attached to the parent document body. Implementers should consider clearing eads@16: * the timers when building systems such as dynamic searches or paging eads@16: * functionality to ensure memory usage and performance remains stable. eads@3: */ eads@3: eads@3: (function($) { eads@3: $.fn.dnd = function(opt) { eads@3: opt = $.extend({}, { eads@15: interval: 250, eads@16: targets: $('iframe, textarea'), eads@3: processTargets: function(targets) { eads@3: return targets.each(function() { eads@15: $('head', $(this).contents()).append(''); eads@3: return this; eads@3: }); eads@3: }, eads@3: idSelector: function(element) { eads@12: if ($(element).is('img')) { eads@16: return element.src; eads@3: } eads@16: return false; eads@3: }, eads@20: processIframeDrop: function(drop, id_selector) { eads@20: var representation_id = opt.idSelector(drop); eads@24: $(drop).replaceWith(representation_id).wrap('

'); eads@3: }, eads@20: processTextAreaClick: function(target, clicked, representation_id) { eads@16: var snippet = '
'; eads@16: $(target).replaceSelection(snippet, true); eads@16: e.preventDefault(); eads@20: }, eads@20: processIframeClick: function (target, clicked, representation_id) { return true; }, eads@20: eads@20: iframeTargetClass: 'dnd-iframe-target', eads@20: textareaTargetClass: 'dnd-textarea-target', eads@24: processedClass: 'dnd-processed', eads@24: droppedClass: 'dnd-dropped' eads@3: eads@3: }, opt); eads@3: eads@3: // Initialize plugin eads@3: var targets = opt.processTargets(opt.targets); eads@3: eads@20: // Watch iframes for changes eads@20: $(targets).filter('iframe').each(function() { eads@20: var target = this; eads@20: var t = setInterval(function() { eads@24: $('img:not(.'+ opt.droppedClass +')', $(target).contents()).each(function() { eads@20: opt.processIframeDrop.call(target, this, opt.idSelector); eads@24: var data = {'drop': this, 'representation_id': opt.idSelector(this)}; eads@24: eads@24: // Trigger event in container window eads@24: $(target).trigger('dnd_drop', data); eads@24: eads@24: // Trigger event in iframe eads@24: $(this).trigger('dnd_drop', data); eads@20: }); eads@20: }, opt.interval); eads@20: eads@20: // Track current active timers -- developers working with DnD eads@20: // can implement their own garbage collection for specific eads@20: // interactions, such as paging or live search. eads@20: var data = $(document).data('dnd_timers'); eads@20: if (!data) { data = {}; } eads@20: data[target.id] = t; eads@20: $(document).data('dnd_timers', data); eads@20: }); eads@20: eads@20: // Process each draggable element eads@3: return this.each(function() { eads@12: if ($(this).is('img')) { eads@16: var element = this, $element = $(element); eads@12: var representation_id = opt.idSelector(element); eads@3: eads@12: if (!representation_id) { eads@12: return this; eads@12: }; eads@12: eads@16: // Add a special class eads@20: $(element).addClass(opt.processedClass); eads@12: eads@25: // Data for custom event eads@25: var event_data = {'drop': element, 'representation_id': representation_id}; eads@25: eads@20: // We need to differentiate behavior based on the targets eads@12: targets.each(function() { eads@16: var target = this, $target = $(target); eads@16: if ($target.is('iframe')) { eads@20: $(element).addClass(opt.iframeTargetClass); eads@20: $(element).click(function() { eads@20: opt.processIframeClick.call(target, element, representation_id); eads@25: $(target).trigger('dnd_drop', event_data); eads@20: }); eads@16: } else if ($target.is('textarea')) { eads@20: $(element).addClass(opt.textareaTargetClass); eads@20: $(element).click(function() { eads@20: opt.processTextAreaClick.call(target, element, representation_id); eads@25: $(target).trigger('dnd_drop', event_data); eads@16: }); eads@12: } eads@12: }); eads@3: } eads@3: }); eads@11: }; eads@3: })(jQuery);