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