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