Mercurial > defr > drupal > scald > dnd
comparison js/dnd.js @ 12:a5b2b9fa2a1a
Cross browser compatibility changes -- winnowing the scope of possible configurations.
| author | David Eads <eads@chicagotech.org> |
|---|---|
| date | Fri, 27 Feb 2009 11:50:59 -0600 |
| parents | 99ba5941779c |
| children | ef7ad7b5baa4 |
comparison
equal
deleted
inserted
replaced
| 11:99ba5941779c | 12:a5b2b9fa2a1a |
|---|---|
| 1 /* jQuery Drag and Drop Library for Rich Editors | 1 /* jQuery Drag and Drop Library for Rich Editors |
| 2 * | 2 * |
| 3 * This library exists to provide a jQuery plugin that manages dragging | 3 * A helper library which provides the ability to drag and drop images to Rich |
| 4 * and dropping of page assets into textareas and rich-text editors which use | 4 * Text Editors (RTEs) that use the embedded iframe + DesignMode method, and |
| 5 * the iframe + designMode method. | 5 * provides a simple "clicky" interface for inserting the same markup directly |
| 6 * | 6 * into a textarea. |
| 7 * This plugin has a rather elaborate set of considerations based on common | |
| 8 * configurations of rich text editors and serious disparity in how browsers | |
| 9 * handle dragging and dropping assets into an iframe. | |
| 10 * | |
| 11 * The plugin scans an iframe with an embedded document with designMode enabled | |
| 12 * and tries to detect content that was injected by dragging and dropping | |
| 13 * within the browser. If it detects an injection of content, it attempts to | |
| 14 * conjure up an "editor representation" of the content based on the markup | |
| 15 * passed in. | |
| 16 * | |
| 17 * This works because links and images drop with their full HTML syntax in-tact | |
| 18 * across most browsers and platforms. This means we can set a timer on the | |
| 19 * iframe that scans for the insertion of the markup and replace it with | |
| 20 * another HTML snippet, as well as triggering actions inside the editor itself | |
| 21 * (this will perhaps be handled by a set of editor-specific plugins). | |
| 22 * | |
| 23 * Because of the mechanism of operation, it is expected that the iframe | |
| 24 * contain CSS which hides the markup dropped in to create the illusion of | |
| 25 * seamlessly dropping in the "editor representation" of the dropped item. | |
| 26 * | |
| 27 * It is expected that the implementer will be parsing the input text for | |
| 28 * the proper markup on the server side and making some decisions about how | |
| 29 * to parse and handle that markup on load and save. | |
| 30 * | |
| 31 * Of special interest is graceful degradation. There is no ideal graceful | |
| 32 * degradation path at this time. Every mainstream browser except IE 6 and | |
| 33 * IE 7 drop links and images into textareas with their href and src URIs, | |
| 34 * respectively, including querystrings. That means that the image or link | |
| 35 * url can be used for parsing, or a querystring included. But it won't work | |
| 36 * in Internet Explorer, and probably will require a server-side browser check | |
| 37 * in full blown implementations of the library system. | |
| 38 * | 7 * |
| 39 * Basic usage: | 8 * Basic usage: |
| 40 * | 9 * |
| 41 * $('a.my-class').dnd({targets: $('#my-iframe')}); | 10 * $('img.my-class').dnd({targets: $('#my-iframe')}); |
| 42 * | 11 * |
| 43 * Options: | 12 * Options: |
| 44 * | 13 * |
| 45 * targets | 14 * targets |
| 46 * A jQuery object corresponding to the proper iframe(s) to enable dragging | 15 * A jQuery object corresponding to the proper iframe(s) to enable dragging |
| 78 * inside it. | 47 * inside it. |
| 79 * | 48 * |
| 80 * postprocessDrop: | 49 * postprocessDrop: |
| 81 * A callback that postprocesses the iframe. | 50 * A callback that postprocesses the iframe. |
| 82 * | 51 * |
| 52 * interval: | |
| 53 * How often to check the iframe for a drop, in milliseconds. | |
| 54 * | |
| 55 * Usage notes: | |
| 56 * | |
| 57 * This is a very tricky problem and to achieve cross browser (as of writing, | |
| 58 * IE, Safari, and Firefox) support, severe limitations must be made on the | |
| 59 * nature of DOM elements that are dropped. | |
| 60 * | |
| 61 * Droppable elements must be <img> tags. Internet Explorer will not accept | |
| 62 * anchors, and Safari strips attributes and additional markup from | |
| 63 * dropped anchors, images, and snippets. | |
| 64 * | |
| 65 * While the idSelector option allows you to parse the dropped element for | |
| 66 * any attribute that "comes along" with the element when it is dropped in a | |
| 67 * designmode-enabled iframe, the only safe attribute to scan is the 'src' | |
| 68 * attribute, and to avoid strange "relativization" of links, the src should | |
| 69 * always be expressed as an absolute url with a fully qualified domain name. | |
| 70 * | |
| 71 * Implementation notes and todos: | |
| 72 * | |
| 73 * Currently, there is no garbage collection instituted for the many many | |
| 74 * timers that are created, so memory usage could become as issue in scenarios | |
| 75 * where the user does a lot of paging or otherwise winds up invoking | |
| 76 * drag and drop on large numbers of elements. | |
| 83 */ | 77 */ |
| 84 | 78 |
| 85 (function($) { | 79 (function($) { |
| 86 $.fn.dnd = function(opt) { | 80 $.fn.dnd = function(opt) { |
| 87 opt = $.extend({}, { | 81 opt = $.extend({}, { |
| 88 dropWrapper: '<p class="dnd-dropped"></p>', | 82 dropWrapper: '<p class="dnd-dropped"></p>', |
| 89 insertBefore: '', | 83 insertBefore: false, |
| 90 insertAfter: '', | 84 insertAfter: false, |
| 91 processedClass: 'dnd-processed', | 85 processedClass: 'dnd-processed', |
| 92 disableClick: true, | 86 interval: 100, |
| 93 | 87 |
| 94 processTargets: function(targets) { | 88 processTargets: function(targets) { |
| 95 return targets.each(function() { | 89 return targets.each(function() { |
| 96 $('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>'); | 90 //$('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>'); |
| 91 //@TODO use jQuery.rules() | |
| 97 return this; | 92 return this; |
| 98 }); | 93 }); |
| 99 }, | 94 }, |
| 100 | 95 |
| 101 // Must return a string | 96 // Must return a string |
| 102 idSelector: function(element) { | 97 idSelector: function(element) { |
| 103 if (!element.id) { | 98 if ($(element).is('img')) { |
| 104 // @TODO sanitize output here | 99 return $.url.setUrl(element.src).param('dnd_id'); |
| 105 if ($(element).is('a')) { | |
| 106 return element.href; | |
| 107 } | |
| 108 if ($(element).is('img')) { | |
| 109 return element.src; | |
| 110 } | |
| 111 } | 100 } |
| 112 return element.id; | |
| 113 }, | 101 }, |
| 114 | 102 |
| 115 // @TODO: Target should be jQuery object | 103 // @TODO: Target should be jQuery object |
| 116 targets: $('iframe, textarea'), | 104 targets: $('iframe, textarea'), |
| 117 | 105 |
| 129 return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>'; | 117 return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>'; |
| 130 }, | 118 }, |
| 131 | 119 |
| 132 // Back out markup to render in place after parent container | 120 // Back out markup to render in place after parent container |
| 133 preprocessDrop: function(target, drop) { | 121 preprocessDrop: function(target, drop) { |
| 122 return drop; | |
| 134 var old_parent = false; | 123 var old_parent = false; |
| 135 var element_id = ''; | 124 var element_id = ''; |
| 136 | 125 |
| 137 var parents = drop.parents(); | 126 var parents = drop.parents(); |
| 138 for (var i=0; i < parents.length; i++) { | 127 for (var i=0; i < parents.length; i++) { |
| 152 | 141 |
| 153 }, opt); | 142 }, opt); |
| 154 | 143 |
| 155 // Initialize plugin | 144 // Initialize plugin |
| 156 var targets = opt.processTargets(opt.targets); | 145 var targets = opt.processTargets(opt.targets); |
| 157 if (opt.disableClick) { this.click(function() { return false; }); } | |
| 158 | 146 |
| 159 // Process! | 147 // Process! |
| 160 return this.each(function() { | 148 return this.each(function() { |
| 161 | 149 if ($(this).is('img')) { |
| 162 var element = this; | 150 var element = this; |
| 163 var representation_id = opt.idSelector(element); | 151 |
| 164 | 152 // If we don't have a proper id, bail |
| 165 if (!element.id) { | 153 var representation_id = opt.idSelector(element); |
| 166 element.id = element.tagName.toLowerCase() + '-' + representation_id; | 154 |
| 155 if (!representation_id) { | |
| 156 return this; | |
| 157 }; | |
| 158 | |
| 159 // Add some UI sugar and a special class | |
| 160 $(element) | |
| 161 .css('cursor', 'move') | |
| 162 .addClass(opt.processedClass); | |
| 163 | |
| 164 // We need to differentiate behavior based on the targets... I guess. | |
| 165 targets.each(function() { | |
| 166 if ($(this).is('iframe')) { | |
| 167 var target = this; | |
| 168 var selector = 'img[src='+ element.src +']'; | |
| 169 | |
| 170 // Watch the iframe for changes | |
| 171 var t = setInterval(function() { | |
| 172 var match = $(selector, $(target).contents()); | |
| 173 if (match.length > 0) { | |
| 174 var drop = opt.preprocessDrop(target, match); // Must return a jquery object | |
| 175 var representation = opt.renderRepresentation(target, drop, representation_id); | |
| 176 | |
| 177 if (representation) { | |
| 178 if (opt.dropWrapper) { | |
| 179 drop.wrap(opt.dropWrapper); | |
| 180 } | |
| 181 if (opt.insertBefore) { | |
| 182 drop.before(opt.insertBefore); | |
| 183 } | |
| 184 if (opt.insertAfter) { | |
| 185 drop.after(opt.insertAfter); | |
| 186 } | |
| 187 drop.replaceWith(representation); | |
| 188 opt.postprocessDrop(target, drop, element); | |
| 189 } | |
| 190 } | |
| 191 }, opt.interval); | |
| 192 // @TODO track the timer with $.data() so we can clear it? | |
| 193 } else if ($(this).is('textarea')) { | |
| 194 //console.log('@TODO handle textareas via.... regexp?'); | |
| 195 } | |
| 196 }); | |
| 167 } | 197 } |
| 168 | |
| 169 // Add some UI sugar and a special class | |
| 170 $(element) | |
| 171 .css('cursor', 'move') | |
| 172 .addClass(opt.processedClass); | |
| 173 | |
| 174 // We need to differentiate behavior based on the targets... I guess. | |
| 175 targets.each(function() { | |
| 176 if ($(this).is('iframe')) { | |
| 177 var target = this; | |
| 178 | |
| 179 // Watch the iframe for changes | |
| 180 var t = setInterval(function() { | |
| 181 var match = $('#' + element.id, $(target).contents()); | |
| 182 if (match.length > 0) { | |
| 183 var drop = opt.preprocessDrop(target, match); // Must return a jquery object | |
| 184 drop | |
| 185 .before(opt.insertBefore) | |
| 186 .after(opt.insertAfter) | |
| 187 .wrap(opt.dropWrapper) | |
| 188 .replaceWith(opt.renderRepresentation(target, drop, representation_id)); | |
| 189 opt.postprocessDrop(target, drop, element); | |
| 190 } | |
| 191 }, 100); | |
| 192 // @TODO track the timer with $.data() so we can clear it? | |
| 193 } else if ($(this).is('textarea')) { | |
| 194 //console.log('@TODO handle textareas via.... regexp?'); | |
| 195 } | |
| 196 }); | |
| 197 }); | 198 }); |
| 198 }; | 199 }; |
| 199 })(jQuery); | 200 })(jQuery); |
