annotate 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
rev   line source
eads@3 1 /* jQuery Drag and Drop Library for Rich Editors
eads@3 2 *
eads@3 3 * This library exists to provide a jQuery plugin that manages dragging
eads@3 4 * and dropping of page assets into textareas and rich-text editors which use
eads@3 5 * the iframe + designMode method.
eads@3 6 *
eads@3 7 * This plugin has a rather elaborate set of considerations based on common
eads@3 8 * configurations of rich text editors and serious disparity in how browsers
eads@3 9 * handle dragging and dropping assets into an iframe.
eads@3 10 *
eads@3 11 * The plugin scans an iframe with an embedded document with designMode enabled
eads@3 12 * and tries to detect content that was injected by dragging and dropping
eads@3 13 * within the browser. If it detects an injection of content, it attempts to
eads@3 14 * conjure up an "editor representation" of the content based on the markup
eads@3 15 * passed in.
eads@3 16 *
eads@3 17 * This works because links and images drop with their full HTML syntax in-tact
eads@3 18 * across most browsers and platforms. This means we can set a timer on the
eads@3 19 * iframe that scans for the insertion of the markup and replace it with
eads@3 20 * another HTML snippet, as well as triggering actions inside the editor itself
eads@3 21 * (this will perhaps be handled by a set of editor-specific plugins).
eads@3 22 *
eads@3 23 * Because of the mechanism of operation, it is expected that the iframe
eads@3 24 * contain CSS which hides the markup dropped in to create the illusion of
eads@3 25 * seamlessly dropping in the "editor representation" of the dropped item.
eads@3 26 *
eads@3 27 * It is expected that the implementer will be parsing the input text for
eads@3 28 * the proper markup on the server side and making some decisions about how
eads@3 29 * to parse and handle that markup on load and save.
eads@3 30 *
eads@3 31 * Of special interest is graceful degradation. There is no ideal graceful
eads@3 32 * degradation path at this time. Every mainstream browser except IE 6 and
eads@3 33 * IE 7 drop links and images into textareas with their href and src URIs,
eads@3 34 * respectively, including querystrings. That means that the image or link
eads@3 35 * url can be used for parsing, or a querystring included. But it won't work
eads@3 36 * in Internet Explorer, and probably will require a server-side browser check
eads@3 37 * in full blown implementations of the library system.
eads@3 38 *
eads@3 39 * Basic usage:
eads@3 40 *
eads@3 41 * $('a.my-class').dnd({targets: $('#my-iframe')});
eads@3 42 *
eads@3 43 * Options:
eads@3 44 *
eads@3 45 * targets
eads@3 46 * A jQuery object corresponding to the proper iframe(s) to enable dragging
eads@3 47 * and dropping for. [Required]
eads@3 48 *
eads@3 49 * dropWrapper
eads@3 50 * An html snippet to wrap the entire inserted content with in the editor
eads@3 51 * representation (i.e. '<p class="foo"></p>')
eads@3 52 *
eads@3 53 * insertBefore:
eads@3 54 * Markup to insert before drop (i.e. '<hr />')
eads@3 55 *
eads@3 56 * insertAfter:
eads@3 57 * Markup to insert after drop (i.e. '<div class="clearfix"></div>')
eads@3 58 *
eads@3 59 * processedClass:
eads@3 60 * The class to apply to links and images tagged as droppable. This class
eads@3 61 * should have a style rule in the editor that sets display to 'none' for
eads@3 62 * the best experience.
eads@3 63 *
eads@3 64 * idSelector:
eads@3 65 * A callback that parses a unique id out of a droppable element. B y default
eads@3 66 * this uses the id of the element, but one could parse out an ID based on
eads@3 67 * any part of the URL, interior markup, etc.
eads@3 68 *
eads@3 69 * renderRepresentation:
eads@3 70 * A callback that defines the mechanism for rendering a representation of
eads@3 71 * the content. The default is currently essential useless.
eads@3 72 *
eads@3 73 * preprocessDrop:
eads@3 74 * A callback that preprocesses the dropped snippet in the iframe, before
eads@3 75 * replacing it. By default this uses a little logic to walk up the DOM
eads@3 76 * tree to topmost parent of the place in the source where the item was
eads@3 77 * dropped, and add the dropped element after that parent instead of
eads@3 78 * inside it.
eads@3 79 *
eads@3 80 * postprocessDrop:
eads@3 81 * A callback that postprocesses the iframe.
eads@3 82 *
eads@3 83 */
eads@3 84
eads@3 85 (function($) {
eads@3 86 $.fn.dnd = function(opt) {
eads@3 87 opt = $.extend({}, {
eads@4 88 dropWrapper: '<p class="dnd-dropped"></p>',
eads@3 89 insertBefore: '',
eads@3 90 insertAfter: '',
eads@3 91 processedClass: 'dnd-processed',
eads@4 92 disableClick: true,
eads@3 93
eads@3 94 processTargets: function(targets) {
eads@3 95 return targets.each(function() {
eads@3 96 $('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>');
eads@3 97 return this;
eads@3 98 });
eads@3 99 },
eads@3 100
eads@3 101 // Must return a string
eads@3 102 idSelector: function(element) {
eads@3 103 if (!element.id) {
eads@3 104 // @TODO sanitize output here
eads@3 105 if ($(element).is('a')) {
eads@3 106 return element.href;
eads@3 107 }
eads@3 108 if ($(element).is('img')) {
eads@3 109 return element.src;
eads@3 110 }
eads@3 111 }
eads@3 112 return element.id;
eads@3 113 },
eads@3 114
eads@3 115 // @TODO: Target should be jQuery object
eads@3 116 targets: $('iframe, textarea'),
eads@3 117
eads@3 118 // Must return a string that DOES NOT share the id of any droppable item
eads@3 119 // living outside the iframe
eads@3 120 renderRepresentation: function(target, drop, representation_id) {
eads@3 121 // Keep a counter of how many times this element was used
eads@3 122 var count = $.data(target, representation_id +'_count');
eads@3 123 if (!count) {
eads@3 124 count = 1;
eads@3 125 } else {
eads@3 126 count++;
eads@3 127 }
eads@3 128 $.data(target, representation_id +'_count', count);
eads@3 129 return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>';
eads@3 130 },
eads@3 131
eads@3 132 // Back out markup to render in place after parent container
eads@3 133 preprocessDrop: function(target, drop) {
eads@3 134 var old_parent = false;
eads@3 135 var element_id = '';
eads@3 136
eads@3 137 var parents = drop.parents();
eads@3 138 for (i=0; i < parents.length; i++) {
eads@3 139 if ($(parents[i]).is('body')) {
eads@3 140 element_id = $(drop).get(0).id;
eads@3 141 $(old_parent).after(drop.clone());
eads@3 142 drop.remove();
eads@3 143 }
eads@3 144 old_parent = parents[i];
eads@3 145 }
eads@3 146 return $('#'+ element_id, $(target).contents());
eads@3 147 },
eads@3 148
eads@4 149 postprocessDrop: function(target, drop, element) {
eads@4 150 $(element).addClass('dnd-inserted');
eads@4 151 }
eads@3 152
eads@3 153 }, opt);
eads@3 154
eads@3 155 // Initialize plugin
eads@3 156 var targets = opt.processTargets(opt.targets);
eads@4 157 if (opt.disableClick) { this.click(function() { return false; }) }
eads@3 158
eads@3 159 // Process!
eads@3 160 return this.each(function() {
eads@3 161
eads@3 162 var element = this;
eads@3 163 var representation_id = opt.idSelector(element);
eads@3 164
eads@3 165 if (!element.id) {
eads@3 166 element.id = element.tagName.toLowerCase() + '-' + representation_id;
eads@3 167 }
eads@3 168
eads@3 169 // Add some UI sugar and a special class
eads@3 170 $(element)
eads@3 171 .css('cursor', 'move')
eads@3 172 .addClass(opt.processedClass);
eads@3 173
eads@3 174 // We need to differentiate behavior based on the targets... I guess.
eads@3 175 targets.each(function() {
eads@3 176 if ($(this).is('iframe')) {
eads@3 177 var target = this;
eads@3 178
eads@3 179 // Watch the iframe for changes
eads@3 180 t = setInterval(function() {
eads@3 181 var match = $('#' + element.id, $(target).contents());
eads@3 182 if (match.length > 0) {
eads@3 183 drop = opt.preprocessDrop(target, match); // Must return a jquery object
eads@3 184 drop
eads@3 185 .before(opt.insertBefore)
eads@3 186 .after(opt.insertAfter)
eads@3 187 .wrap(opt.dropWrapper)
eads@3 188 .replaceWith(opt.renderRepresentation(target, drop, representation_id));
eads@4 189 opt.postprocessDrop(target, drop, element);
eads@3 190 }
eads@3 191 }, 100);
eads@3 192 // @TODO track the timer with $.data() so we can clear it?
eads@3 193 } else if ($(this).is('textarea')) {
eads@3 194 console.log('@TODO handle textareas via.... regexp?');
eads@3 195 }
eads@3 196 });
eads@3 197 });
eads@3 198 }
eads@3 199 })(jQuery);