Mercurial > defr > drupal > scald > dnd
comparison js/dnd.js @ 16:bb68dc3ad56f
Major refactor to provide better TinyMCE support and less configuration options, added new jquery dependency, etc.
author | David Eads <eads@chicagotech.org> |
---|---|
date | Tue, 03 Mar 2009 16:57:39 -0600 |
parents | 7a5f74482ee3 |
children | 1a77f87927dd |
comparison
equal
deleted
inserted
replaced
15:7a5f74482ee3 | 16:bb68dc3ad56f |
---|---|
1 /* jQuery Drag and Drop Library for Rich Editors | 1 /* jQuery Drag and Drop Library for Rich Editors |
2 * | 2 * |
3 * A helper library which provides the ability to drag and drop images to Rich | 3 * A helper library which provides the ability to drag and drop images to Rich |
4 * Text Editors (RTEs) that use the embedded iframe + DesignMode method, and | 4 * Text Editors (RTEs) that use the embedded iframe + DesignMode method. DnD |
5 * provides a simple "clicky" interface for inserting the same markup directly | 5 * also provides a simple "clicky" interface for inserting the same markup |
6 * into a textarea. | 6 * directly into a textarea. |
7 * | 7 * |
8 * Basic usage: | 8 * Basic usage: |
9 * | 9 * |
10 * $('img.my-class').dnd({targets: $('#my-iframe')}); | 10 * $('img.my-class').dnd({targets: $('#my-iframe')}); |
11 * | 11 * |
12 * Options: | 12 * Options: |
13 * | 13 * |
14 * targets | 14 * targets (Required): |
15 * A jQuery object corresponding to the proper iframe(s) to enable dragging | 15 * A jQuery object corresponding to the proper iframe(s) and/or textarea(s) |
16 * and dropping for. [Required] | 16 * that are allowed drop targets for the current set of elements. |
17 * | 17 * |
18 * dropWrapper | 18 * idSelector: |
19 * An html snippet to wrap the entire inserted content with in the editor | 19 * A callback that parses out the unique ID of an image that is dropped in an |
20 * representation (i.e. '<p class="foo"></p>') | 20 * iframe. While some browsers (such as Firefox and Internet Explorer) allow |
21 * markup to be copied when dragging and dropping, Safari (and assumably | |
22 * other webkit based browsers) don't. The upshot is that the safest bet is | |
23 * to parse the element's src URL. Because querystrings seem to drop | |
24 * consistently across | |
21 * | 25 * |
22 * insertBefore: | 26 * processIframeDrop: |
23 * Markup to insert before drop (i.e. '<hr />') | 27 * A callback that defines the mechanism for inserting and rendering the |
28 * dropped item in an iframe. The typical usage pattern I expect to see is | |
29 * that implementers will listen for their RTE to load and then invoke DnD | |
30 * with a processIframeDrop appropriate to their editor. | |
24 * | 31 * |
25 * insertAfter: | 32 * processTextAreaDrop: |
26 * Markup to insert after drop (i.e. '<div class="clearfix"></div>') | 33 * A callback that defines the mechanism for inserting and rendering the |
27 * | 34 * clicked item in a textarea. The default function just tries to insert |
28 * processedClass: | 35 * some markup at the caret location in the current textarea. |
29 * The class to apply to links and images tagged as droppable. This class | |
30 * should have a style rule in the editor that sets display to 'none' for | |
31 * the best experience. | |
32 * | |
33 * idSelector: | |
34 * A callback that parses a unique id out of a droppable element. By default | |
35 * this uses the id of the element, but one could parse out an ID based on | |
36 * any part of the URL, interior markup, etc. | |
37 * | |
38 * renderRepresentation: | |
39 * A callback that defines the mechanism for rendering a representation of | |
40 * the content. The default is currently essential useless. | |
41 * | |
42 * preprocessDrop: | |
43 * A callback that preprocesses the dropped snippet in the iframe, before | |
44 * replacing it. By default this uses a little logic to walk up the DOM | |
45 * tree to topmost parent of the place in the source where the item was | |
46 * dropped, and add the dropped element after that parent instead of | |
47 * inside it. | |
48 * | |
49 * postprocessDrop: | |
50 * A callback that postprocesses the iframe. | |
51 * | 36 * |
52 * interval: | 37 * interval: |
53 * How often to check the iframe for a drop, in milliseconds. | 38 * How often to check the iframe for a drop, in milliseconds. |
54 * | 39 * |
40 * | |
41 * | |
55 * Usage notes: | 42 * Usage notes: |
56 * | 43 * |
57 * This is a very tricky problem and to achieve cross browser (as of writing, | 44 * Due to cross browser flakiness, there are many many limitations on what is |
58 * IE, Safari, and Firefox) support, severe limitations must be made on the | 45 * possible in terms of dragging and dropping to DesignMode enabled iframes. |
59 * nature of DOM elements that are dropped. | |
60 * | 46 * |
61 * Droppable elements must be <img> tags. Internet Explorer will not accept | 47 * To make this work, your "droppable" elements must be image tags. No ifs, ands, |
62 * anchors, and Safari strips attributes and additional markup from | 48 * or buts: image tags have the best cross browser behavior when dragging. |
63 * dropped anchors, images, and snippets. | |
64 * | 49 * |
65 * While the idSelector option allows you to parse the dropped element for | 50 * When DnD is initialized, it begins timers which periodically check your |
66 * any attribute that "comes along" with the element when it is dropped in a | 51 * editor iframe. If an image is dropped in, DnD takes the element and |
67 * designmode-enabled iframe, the only safe attribute to scan is the 'src' | 52 * attempts to parse it for a unique ID which you can use to do a lookup for |
68 * attribute, and to avoid strange "relativization" of links, the src should | 53 * an "editor representation." If an editor representation is found, |
69 * always be expressed as an absolute url with a fully qualified domain name. | 54 * typically the image is replaced with the representation snippet, but you are |
55 * free to do whatever you want. | |
70 * | 56 * |
71 * Implementation notes and todos: | 57 * Because of browser limitations, the safest way to parse the element is to |
58 * look at the img's src attribute and use some or all of the image URL. | |
59 * In my experience, the best way to deal with this is simply to parse out | |
60 * generate a query string in the src attribute server side that corresponds | |
61 * to the proper representation ID. | |
72 * | 62 * |
73 * Currently, there is no garbage collection instituted for the many many | 63 * If the target is not an iframe but a textarea, DnD provides a very minimal |
74 * timers that are created, so memory usage could become as issue in scenarios | 64 * system for clicking what would otherwise be dragged to insert markup into |
75 * where the user does a lot of paging or otherwise winds up invoking | 65 * the textarea. |
76 * drag and drop on large numbers of elements. | 66 * |
67 * DnD is purely a utility library: to make it work for any particular editor, | |
68 * CMS, etc: It is very unlikely it will provide much benefit out of the box. | |
69 * | |
70 * Because DnD spawns so many timers, they are stored in a $.data element | |
71 * attached to the parent document body. Implementers should consider clearing | |
72 * the timers when building systems such as dynamic searches or paging | |
73 * functionality to ensure memory usage and performance remains stable. | |
77 */ | 74 */ |
78 | 75 |
79 (function($) { | 76 (function($) { |
80 $.fn.dnd = function(opt) { | 77 $.fn.dnd = function(opt) { |
81 opt = $.extend({}, { | 78 opt = $.extend({}, { |
82 interval: 250, | 79 interval: 250, |
83 dropWrapper: '<p class="dnd-dropped-wrapper"></p>', | 80 targets: $('iframe, textarea'), |
84 insertBefore: false, | |
85 insertAfter: false, | |
86 processedClass: 'dnd-processed', | |
87 ignoreClass: 'dnd-dropped', | |
88 processTargets: function(targets) { | 81 processTargets: function(targets) { |
89 return targets.each(function() { | 82 return targets.each(function() { |
90 $('head', $(this).contents()).append('<style type="text/css">img { display: none; } img.dnd-dropped {display: block; }</style>'); | 83 $('head', $(this).contents()).append('<style type="text/css">img { display: none; } img.dnd-dropped {display: block; }</style>'); |
91 return this; | 84 return this; |
92 }); | 85 }); |
93 }, | 86 }, |
94 | |
95 // Must return a string | |
96 idSelector: function(element) { | 87 idSelector: function(element) { |
97 if ($(element).is('img')) { | 88 if ($(element).is('img')) { |
98 return $.url.setUrl(element.src).param('dnd_id'); | 89 return element.src; |
99 } | 90 } |
91 return false; | |
100 }, | 92 }, |
101 | 93 processIframeDrop: function(target, dragged, dropped, representation_id) { |
102 // @TODO: Target should be jQuery object | |
103 targets: $('iframe, textarea'), | |
104 | |
105 // Must return a string that DOES NOT share the id of any droppable item | |
106 // living outside the iframe | |
107 renderRepresentation: function(target, drop, representation_id) { | |
108 // Keep a counter of how many times this element was used | 94 // Keep a counter of how many times this element was used |
109 var count = $.data(target, representation_id +'_count'); | 95 var count = $.data(target, representation_id +'_count'); |
110 if (!count) { | 96 if (!count) { |
111 count = 1; | 97 count = 1; |
112 } else { | 98 } else { |
113 count++; | 99 count++; |
114 } | 100 } |
115 $.data(target, representation_id +'_count', count); | 101 $.data(target, representation_id +'_count', count); |
116 return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>'; | 102 $(dropped).replaceWith('<p id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</p>'); |
117 }, | 103 }, |
104 processTextAreaDrop: function(target, clicked, representation_id, e, data) { | |
105 var snippet = '<div><img src="'+ representation_id +'" /></div>'; | |
106 $(target).replaceSelection(snippet, true); | |
107 e.preventDefault(); | |
108 } | |
118 | 109 |
119 preprocessDrop: function(target, drop) { return drop; }, | |
120 postprocessDrop: function(target, drop, element) { | |
121 $(element).addClass('dnd-inserted'); | |
122 } | |
123 | |
124 }, opt); | 110 }, opt); |
125 | 111 |
126 // Initialize plugin | 112 // Initialize plugin |
127 var targets = opt.processTargets(opt.targets); | 113 var targets = opt.processTargets(opt.targets); |
128 | 114 |
129 // Process! | 115 // Process! |
130 return this.each(function() { | 116 return this.each(function() { |
131 if ($(this).is('img')) { | 117 if ($(this).is('img')) { |
132 var element = this; | 118 var element = this, $element = $(element); |
133 | 119 |
134 // If we don't have a proper id, bail | 120 // If we don't have a proper id, bail |
135 var representation_id = opt.idSelector(element); | 121 var representation_id = opt.idSelector(element); |
136 | 122 |
137 if (!representation_id) { | 123 if (!representation_id) { |
138 return this; | 124 return this; |
139 }; | 125 }; |
140 | 126 |
141 // Add some UI sugar and a special class | 127 // Add a special class |
142 $(element) | 128 $(element).addClass('dnd-processed'); |
143 .css('cursor', 'move') | |
144 .addClass(opt.processedClass); | |
145 | 129 |
146 // We need to differentiate behavior based on the targets... I guess. | 130 // We need to differentiate behavior based on the targets... I guess. |
147 targets.each(function() { | 131 targets.each(function() { |
148 if ($(this).is('iframe')) { | 132 var target = this, $target = $(target); |
149 var target = this; | 133 if ($target.is('iframe')) { |
150 | 134 |
151 var selector = 'img:not(.' + opt.ignoreClass + ')'; | 135 // Indicate this element is draggy |
136 $element.css('cursor', 'move'); | |
137 | |
152 | 138 |
153 // Watch the iframe for changes | 139 // Watch the iframe for changes |
154 var t = setInterval(function() { | 140 var t = setInterval(function() { |
155 $(selector, $(target).contents()).each(function() { | 141 $('img:not(.dnd-dropped)', $(target).contents()).each(function() { |
156 if (opt.idSelector(this) == representation_id) { | 142 var dropped = this; |
157 var drop = opt.preprocessDrop(target, $(this)); // Must return a jquery object | 143 if (opt.idSelector(dropped) == representation_id) { |
158 var representation = opt.renderRepresentation(target, drop, representation_id); | 144 opt.processIframeDrop(target, element, dropped, representation_id); |
159 if (representation) { | 145 } |
160 // Breaks IE 7! | |
161 /*if (opt.dropWrapper) { | |
162 drop.wrap(opt.dropWrapper); | |
163 }*/ | |
164 if (opt.insertBefore) { | |
165 drop.before(opt.insertBefore); | |
166 } | |
167 if (opt.insertAfter) { | |
168 drop.after(opt.insertAfter); | |
169 } | |
170 drop.replaceWith(representation); | |
171 opt.postprocessDrop(target, drop, element); | |
172 } | |
173 } | |
174 }); | 146 }); |
175 }, opt.interval); | 147 }, opt.interval); |
176 | 148 |
177 // Track current active timers -- this means you can implement | 149 // Track current active timers -- developers working with DnD |
178 // your own garbage collection for specific interactions, such | 150 // can implement their own garbage collection for specific |
179 // as paging. | 151 // interactions, such as paging or live search. |
180 var data = $(document).data('dnd_timers'); | 152 var data = $(document).data('dnd_timers'); |
181 if (data) { | 153 if (data) { |
182 data[data.length] = t; | 154 data[data.length] = t; |
183 } else { | 155 } else { |
184 data = new Array(); | 156 data = new Array(); |
185 data[0] = t; | 157 data[0] = t; |
186 } | 158 } |
187 $(document).data('dnd_timers', data); | 159 $(document).data('dnd_timers', data); |
188 | 160 |
189 } else if ($(this).is('textarea')) { | 161 } else if ($target.is('textarea')) { |
190 //console.log('@TODO handle textareas via.... regexp?'); | 162 $(element).click(function(e, data) { |
163 opt.processTextAreaDrop(target, element, representation_id, e, data); | |
164 }); | |
191 } | 165 } |
192 }); | 166 }); |
193 } | 167 } |
194 }); | 168 }); |
195 }; | 169 }; |