eads@3: /* jQuery Drag and Drop Library for Rich Editors
eads@3: *
eads@3: * This library exists to provide a jQuery plugin that manages dragging
eads@3: * and dropping of page assets into textareas and rich-text editors which use
eads@3: * the iframe + designMode method.
eads@3: *
eads@3: * This plugin has a rather elaborate set of considerations based on common
eads@3: * configurations of rich text editors and serious disparity in how browsers
eads@3: * handle dragging and dropping assets into an iframe.
eads@3: *
eads@3: * The plugin scans an iframe with an embedded document with designMode enabled
eads@3: * and tries to detect content that was injected by dragging and dropping
eads@3: * within the browser. If it detects an injection of content, it attempts to
eads@3: * conjure up an "editor representation" of the content based on the markup
eads@3: * passed in.
eads@3: *
eads@3: * This works because links and images drop with their full HTML syntax in-tact
eads@3: * across most browsers and platforms. This means we can set a timer on the
eads@3: * iframe that scans for the insertion of the markup and replace it with
eads@3: * another HTML snippet, as well as triggering actions inside the editor itself
eads@3: * (this will perhaps be handled by a set of editor-specific plugins).
eads@3: *
eads@3: * Because of the mechanism of operation, it is expected that the iframe
eads@3: * contain CSS which hides the markup dropped in to create the illusion of
eads@3: * seamlessly dropping in the "editor representation" of the dropped item.
eads@3: *
eads@3: * It is expected that the implementer will be parsing the input text for
eads@3: * the proper markup on the server side and making some decisions about how
eads@3: * to parse and handle that markup on load and save.
eads@3: *
eads@3: * Of special interest is graceful degradation. There is no ideal graceful
eads@3: * degradation path at this time. Every mainstream browser except IE 6 and
eads@3: * IE 7 drop links and images into textareas with their href and src URIs,
eads@3: * respectively, including querystrings. That means that the image or link
eads@3: * url can be used for parsing, or a querystring included. But it won't work
eads@3: * in Internet Explorer, and probably will require a server-side browser check
eads@3: * in full blown implementations of the library system.
eads@3: *
eads@3: * Basic usage:
eads@3: *
eads@3: * $('a.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@3: */
eads@3:
eads@3: (function($) {
eads@3: $.fn.dnd = function(opt) {
eads@3: opt = $.extend({}, {
eads@3: dropWrapper: '',
eads@3: insertBefore: '',
eads@3: insertAfter: '',
eads@3: processedClass: 'dnd-processed',
eads@3:
eads@3: processTargets: function(targets) {
eads@3: return targets.each(function() {
eads@3: $('head', $(this).contents()).append('');
eads@3: return this;
eads@3: });
eads@3: },
eads@3:
eads@3: // Must return a string
eads@3: idSelector: function(element) {
eads@3: if (!element.id) {
eads@3: // @TODO sanitize output here
eads@3: if ($(element).is('a')) {
eads@3: return element.href;
eads@3: }
eads@3: if ($(element).is('img')) {
eads@3: return element.src;
eads@3: }
eads@3: }
eads@3: return element.id;
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@3: var old_parent = false;
eads@3: var element_id = '';
eads@3:
eads@3: var parents = drop.parents();
eads@3: for (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@3: postprocessDrop: function(target, drop) { return; }
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@3:
eads@3: var element = this;
eads@3: var representation_id = opt.idSelector(element);
eads@3:
eads@3: if (!element.id) {
eads@3: element.id = element.tagName.toLowerCase() + '-' + representation_id;
eads@3: }
eads@3:
eads@3: // Add some UI sugar and a special class
eads@3: $(element)
eads@3: .css('cursor', 'move')
eads@3: .addClass(opt.processedClass);
eads@3:
eads@3: // We need to differentiate behavior based on the targets... I guess.
eads@3: targets.each(function() {
eads@3: if ($(this).is('iframe')) {
eads@3: var target = this;
eads@3:
eads@3: // Watch the iframe for changes
eads@3: t = setInterval(function() {
eads@3: var match = $('#' + element.id, $(target).contents());
eads@3: if (match.length > 0) {
eads@3: drop = opt.preprocessDrop(target, match); // Must return a jquery object
eads@3: drop
eads@3: .before(opt.insertBefore)
eads@3: .after(opt.insertAfter)
eads@3: .wrap(opt.dropWrapper)
eads@3: .replaceWith(opt.renderRepresentation(target, drop, representation_id));
eads@3: opt.postprocessDrop(target, drop);
eads@3: }
eads@3: }, 100);
eads@3: // @TODO track the timer with $.data() so we can clear it?
eads@3: } else if ($(this).is('textarea')) {
eads@3: console.log('@TODO handle textareas via.... regexp?');
eads@3: }
eads@3: });
eads@3: });
eads@3: }
eads@3: })(jQuery);