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);