eads@20
|
1 /** |
eads@20
|
2 * jQuery Drag and Drop Library for Rich Editors |
eads@3
|
3 * |
eads@12
|
4 * A helper library which provides the ability to drag and drop images to Rich |
eads@16
|
5 * Text Editors (RTEs) that use the embedded iframe + DesignMode method. DnD |
eads@16
|
6 * also provides a simple "clicky" interface for inserting the same markup |
eads@16
|
7 * directly into a textarea. |
eads@3
|
8 * |
eads@3
|
9 * Basic usage: |
eads@3
|
10 * |
eads@20
|
11 * $('img.my-draggable-class').dnd({targets: $('#my-rte-iframe')}); |
eads@3
|
12 * |
eads@3
|
13 * Options: |
eads@3
|
14 * |
eads@16
|
15 * targets (Required): |
eads@16
|
16 * A jQuery object corresponding to the proper iframe(s) and/or textarea(s) |
eads@16
|
17 * that are allowed drop targets for the current set of elements. |
eads@3
|
18 * |
eads@16
|
19 * idSelector: |
eads@16
|
20 * A callback that parses out the unique ID of an image that is dropped in an |
eads@16
|
21 * iframe. While some browsers (such as Firefox and Internet Explorer) allow |
eads@16
|
22 * markup to be copied when dragging and dropping, Safari (and assumably |
eads@16
|
23 * other webkit based browsers) don't. The upshot is that the safest bet is |
eads@16
|
24 * to parse the element's src URL. Because querystrings seem to drop |
eads@20
|
25 * consistently across browsers, encoding the id in a query string is a good |
eads@20
|
26 * option. |
eads@3
|
27 * |
eads@16
|
28 * processIframeDrop: |
eads@16
|
29 * A callback that defines the mechanism for inserting and rendering the |
eads@16
|
30 * dropped item in an iframe. The typical usage pattern I expect to see is |
eads@16
|
31 * that implementers will listen for their RTE to load and then invoke DnD |
eads@20
|
32 * with a processIframeDrop appropriate to their editor. For performance |
eads@20
|
33 * reasons, this only runs once per invocation of DnD -- to compensate, we |
eads@20
|
34 * must pass in an idselector callback as an argument. |
eads@3
|
35 * |
eads@20
|
36 * processTextAreaClick: |
eads@16
|
37 * A callback that defines the mechanism for inserting and rendering the |
eads@16
|
38 * clicked item in a textarea. The default function just tries to insert |
eads@16
|
39 * some markup at the caret location in the current textarea. |
eads@3
|
40 * |
eads@20
|
41 * iframeTargetClass: |
eads@20
|
42 * A class to add to a draggable item if it can be dragged to an iframe. |
eads@20
|
43 * |
eads@20
|
44 * textareaTargetClass: |
eads@20
|
45 * A class to add to a draggable item if it can be dragged to a textarea. |
eads@20
|
46 * |
eads@24
|
47 * processedClass: |
eads@20
|
48 * A class to add to draggable items processed by DnD. |
eads@20
|
49 * |
eads@24
|
50 * droppedClass: |
eads@24
|
51 * A class to add to images that are dropped (by default, this is used to |
eads@24
|
52 * ensure that a successful drop manifests the editor representation only |
eads@24
|
53 * once.) |
eads@24
|
54 * |
eads@12
|
55 * interval: |
eads@12
|
56 * How often to check the iframe for a drop, in milliseconds. |
eads@12
|
57 * |
eads@12
|
58 * Usage notes: |
eads@12
|
59 * |
eads@16
|
60 * Due to cross browser flakiness, there are many many limitations on what is |
eads@16
|
61 * possible in terms of dragging and dropping to DesignMode enabled iframes. |
eads@12
|
62 * |
eads@16
|
63 * To make this work, your "droppable" elements must be image tags. No ifs, ands, |
eads@16
|
64 * or buts: image tags have the best cross browser behavior when dragging. |
eads@12
|
65 * |
eads@20
|
66 * When DnD is invoked, it begins a timer which periodically checks your |
eads@16
|
67 * editor iframe. If an image is dropped in, DnD takes the element and |
eads@16
|
68 * attempts to parse it for a unique ID which you can use to do a lookup for |
eads@16
|
69 * an "editor representation." If an editor representation is found, |
eads@16
|
70 * typically the image is replaced with the representation snippet, but you are |
eads@16
|
71 * free to do whatever you want. |
eads@12
|
72 * |
eads@16
|
73 * Because of browser limitations, the safest way to parse the element is to |
eads@16
|
74 * look at the img's src attribute and use some or all of the image URL. |
eads@16
|
75 * In my experience, the best way to deal with this is simply to parse out |
eads@16
|
76 * generate a query string in the src attribute server side that corresponds |
eads@16
|
77 * to the proper representation ID. |
eads@12
|
78 * |
eads@16
|
79 * If the target is not an iframe but a textarea, DnD provides a very minimal |
eads@16
|
80 * system for clicking what would otherwise be dragged to insert markup into |
eads@16
|
81 * the textarea. |
eads@16
|
82 * |
eads@16
|
83 * Because DnD spawns so many timers, they are stored in a $.data element |
eads@16
|
84 * attached to the parent document body. Implementers should consider clearing |
eads@16
|
85 * the timers when building systems such as dynamic searches or paging |
eads@16
|
86 * functionality to ensure memory usage and performance remains stable. |
eads@3
|
87 */ |
eads@3
|
88 |
eads@3
|
89 (function($) { |
eads@3
|
90 $.fn.dnd = function(opt) { |
eads@3
|
91 opt = $.extend({}, { |
eads@15
|
92 interval: 250, |
eads@16
|
93 targets: $('iframe, textarea'), |
eads@3
|
94 processTargets: function(targets) { |
eads@3
|
95 return targets.each(function() { |
eads@15
|
96 $('head', $(this).contents()).append('<style type="text/css">img { display: none; } img.dnd-dropped {display: block; }</style>'); |
eads@3
|
97 return this; |
eads@3
|
98 }); |
eads@3
|
99 }, |
eads@3
|
100 idSelector: function(element) { |
eads@12
|
101 if ($(element).is('img')) { |
eads@16
|
102 return element.src; |
eads@3
|
103 } |
eads@16
|
104 return false; |
eads@3
|
105 }, |
eads@20
|
106 processIframeDrop: function(drop, id_selector) { |
eads@20
|
107 var representation_id = opt.idSelector(drop); |
eads@24
|
108 $(drop).replaceWith(representation_id).wrap('<p class="'+ opt.droppedClass +'"></p>'); |
eads@3
|
109 }, |
eads@20
|
110 processTextAreaClick: function(target, clicked, representation_id) { |
eads@16
|
111 var snippet = '<div><img src="'+ representation_id +'" /></div>'; |
eads@16
|
112 $(target).replaceSelection(snippet, true); |
eads@16
|
113 e.preventDefault(); |
eads@20
|
114 }, |
eads@20
|
115 processIframeClick: function (target, clicked, representation_id) { return true; }, |
eads@20
|
116 |
eads@20
|
117 iframeTargetClass: 'dnd-iframe-target', |
eads@20
|
118 textareaTargetClass: 'dnd-textarea-target', |
eads@24
|
119 processedClass: 'dnd-processed', |
eads@24
|
120 droppedClass: 'dnd-dropped' |
eads@3
|
121 |
eads@3
|
122 }, opt); |
eads@3
|
123 |
eads@3
|
124 // Initialize plugin |
eads@3
|
125 var targets = opt.processTargets(opt.targets); |
eads@3
|
126 |
eads@20
|
127 // Watch iframes for changes |
eads@20
|
128 $(targets).filter('iframe').each(function() { |
eads@20
|
129 var target = this; |
eads@20
|
130 var t = setInterval(function() { |
defr@48
|
131 var ifr = $(target)[0]; |
defr@48
|
132 // Check that jQuery .contents() can work, and return early otherwise. |
defr@48
|
133 if (!ifr.contentDocument && !ifr.contentWindow) { |
defr@48
|
134 return; |
defr@48
|
135 } |
eads@24
|
136 $('img:not(.'+ opt.droppedClass +')', $(target).contents()).each(function() { |
eads@20
|
137 opt.processIframeDrop.call(target, this, opt.idSelector); |
eads@24
|
138 var data = {'drop': this, 'representation_id': opt.idSelector(this)}; |
eads@24
|
139 |
eads@24
|
140 // Trigger event in container window |
eads@24
|
141 $(target).trigger('dnd_drop', data); |
eads@24
|
142 |
eads@24
|
143 // Trigger event in iframe |
eads@24
|
144 $(this).trigger('dnd_drop', data); |
eads@20
|
145 }); |
eads@20
|
146 }, opt.interval); |
eads@20
|
147 |
eads@20
|
148 // Track current active timers -- developers working with DnD |
eads@20
|
149 // can implement their own garbage collection for specific |
eads@20
|
150 // interactions, such as paging or live search. |
eads@20
|
151 var data = $(document).data('dnd_timers'); |
eads@20
|
152 if (!data) { data = {}; } |
eads@20
|
153 data[target.id] = t; |
eads@20
|
154 $(document).data('dnd_timers', data); |
eads@20
|
155 }); |
eads@20
|
156 |
eads@20
|
157 // Process each draggable element |
eads@3
|
158 return this.each(function() { |
eads@12
|
159 if ($(this).is('img')) { |
eads@16
|
160 var element = this, $element = $(element); |
eads@12
|
161 var representation_id = opt.idSelector(element); |
eads@3
|
162 |
eads@12
|
163 if (!representation_id) { |
eads@12
|
164 return this; |
eads@12
|
165 }; |
eads@12
|
166 |
eads@16
|
167 // Add a special class |
eads@20
|
168 $(element).addClass(opt.processedClass); |
eads@12
|
169 |
eads@25
|
170 // Data for custom event |
eads@25
|
171 var event_data = {'drop': element, 'representation_id': representation_id}; |
eads@25
|
172 |
eads@20
|
173 // We need to differentiate behavior based on the targets |
eads@12
|
174 targets.each(function() { |
eads@16
|
175 var target = this, $target = $(target); |
eads@16
|
176 if ($target.is('iframe')) { |
eads@20
|
177 $(element).addClass(opt.iframeTargetClass); |
eads@20
|
178 $(element).click(function() { |
eads@20
|
179 opt.processIframeClick.call(target, element, representation_id); |
eads@25
|
180 $(target).trigger('dnd_drop', event_data); |
eads@20
|
181 }); |
eads@16
|
182 } else if ($target.is('textarea')) { |
eads@20
|
183 $(element).addClass(opt.textareaTargetClass); |
eads@20
|
184 $(element).click(function() { |
eads@20
|
185 opt.processTextAreaClick.call(target, element, representation_id); |
eads@25
|
186 $(target).trigger('dnd_drop', event_data); |
eads@16
|
187 }); |
eads@12
|
188 } |
eads@12
|
189 }); |
eads@3
|
190 } |
eads@3
|
191 }); |
eads@11
|
192 }; |
eads@3
|
193 })(jQuery); |