eads@16
|
1 /** |
eads@16
|
2 * Drag and Drop Library For Drupal |
eads@16
|
3 * |
eads@16
|
4 * This builds on the DnD jQuery plugin written to provide drag and drop media |
eads@16
|
5 * handling to Rich Text Editors to consume, display, and attach behavior to |
eads@16
|
6 * a "media library" provided via JSON and implemented for Drupal running |
eads@16
|
7 * the Wysiwyg plugin. |
eads@16
|
8 */ |
eads@16
|
9 |
eads@16
|
10 /** |
eads@16
|
11 * Extend jQuery a bit |
eads@16
|
12 * |
eads@16
|
13 * We add a selector to look for "empty" elements (empty elements in TinyMCE |
eads@17
|
14 * often have non-breaking spaces and <br /> tags). An exception is required |
eads@17
|
15 * to make this work in IE. |
eads@16
|
16 */ |
eads@16
|
17 (function($) { |
eads@16
|
18 // Custom selectors |
eads@16
|
19 $.extend($.expr[":"], { |
eads@31
|
20 'dnd_empty' : function(a, i, m) { |
eads@17
|
21 return !$(a).filter(function(i) { |
eads@17
|
22 return !$(this).is('br'); |
eads@30
|
23 }).length && !$.trim(a.textContent || a.innerText||$(a).text() || ""); |
eads@16
|
24 } |
eads@16
|
25 }); |
eads@16
|
26 }) (jQuery); |
eads@16
|
27 |
eads@30
|
28 /** |
eads@30
|
29 * Initialize and load drag and drop library and pass off rendering and |
eads@30
|
30 * behavior attachment. |
eads@30
|
31 */ |
eads@2
|
32 Drupal.behaviors.dndLibrary = function(context) { |
franck@51
|
33 if (!Drupal.settings.dndEnabledLibraries) { |
franck@51
|
34 return; |
franck@51
|
35 } |
eads@16
|
36 $('.dnd-library-wrapper', context).each(function() { |
eads@4
|
37 var $this = $(this); |
eads@2
|
38 |
eads@4
|
39 // This is a bad hack to lop off '-dnd-library' from the id to get the editor name |
eads@17
|
40 var $editor = $('#' + this.id.slice(0, -12)); |
eads@17
|
41 |
eads@30
|
42 // Set up some initial settings for BeautyTips |
eads@30
|
43 var settings = Drupal.settings.dndEnabledLibraries[$editor.get(0).id] = $.extend({ |
eads@31
|
44 'btSettings' : { |
franck@35
|
45 'trigger': ['click'], |
franck@41
|
46 'width': 480, |
eads@20
|
47 'spikeLength': 7, |
eads@20
|
48 'spikeGirth': 9, |
eads@18
|
49 'corner-radius' : 3, |
eads@20
|
50 'strokeWidth': 1, |
eads@20
|
51 'fill': '#ffd', |
franck@35
|
52 'strokeStyle': '#555', |
franck@35
|
53 'closeWhenOthersOpen': true |
eads@31
|
54 }, |
eads@31
|
55 'libraryHoverIntentSettings' : { |
eads@31
|
56 'interval': 500, |
eads@31
|
57 'timeout' : 0, |
eads@31
|
58 'over': function() { |
eads@31
|
59 var $this = $(this); |
eads@31
|
60 this.btOn(); |
eads@31
|
61 // Remove the preview once dragging of any image has commenced |
eads@31
|
62 $('img', $this).bind('drag', function(e) { |
eads@31
|
63 $this.btOff(); |
eads@31
|
64 }); |
eads@31
|
65 $('img', $this).bind('click', function(e) { |
eads@31
|
66 $this.btOff(); |
eads@31
|
67 }); |
eads@31
|
68 }, |
eads@31
|
69 'out': function() { this.btOff(); } |
eads@31
|
70 } |
eads@30
|
71 }, Drupal.settings.dndEnabledLibraries[$editor.get(0).id]); |
eads@24
|
72 |
eads@24
|
73 // Bind Drag and Drop plugin invocation to events emanating from Wysiwyg |
eads@24
|
74 $editor.bind('wysiwygAttach', Drupal.behaviors.dndLibrary.attach_library); |
eads@24
|
75 $editor.bind('wysiwygDetach', Drupal.behaviors.dndLibrary.detach_library); |
eads@24
|
76 |
eads@30
|
77 // Set up empty objects to keep track of things |
eads@30
|
78 Drupal.settings.dndEditorRepresentations = {}; |
eads@30
|
79 Drupal.settings.dndLibraryPreviews = {}; |
franck@36
|
80 |
franck@36
|
81 // Clean up the url if needed (this happen in case drupal_add_js is called |
franck@36
|
82 // multiple time for the page) |
franck@36
|
83 if (settings.url instanceof Object) { |
franck@36
|
84 settings.url = settings.url[0]; |
franck@36
|
85 } |
eads@30
|
86 |
eads@31
|
87 // Initialize the library |
defr@47
|
88 var wrapper = $this.get(0); |
defr@47
|
89 wrapper.library_url = Drupal.settings.basePath + settings.url; |
eads@30
|
90 $.getJSON(Drupal.settings.basePath + settings.url, function(data) { |
defr@47
|
91 Drupal.behaviors.dndLibrary.renderLibrary.call(wrapper, data, $editor); |
eads@17
|
92 }); |
eads@4
|
93 |
eads@4
|
94 }); |
eads@4
|
95 } |
eads@2
|
96 |
eads@30
|
97 Drupal.behaviors.dndLibrary.renderLibrary = function(data, editor) { |
eads@17
|
98 $this = $(this); |
eads@17
|
99 |
eads@30
|
100 $this.html(data.library); |
eads@17
|
101 |
eads@30
|
102 var settings = Drupal.settings.dndEnabledLibraries[editor.get(0).id]; |
eads@17
|
103 var params = Drupal.wysiwyg.instances[editor.get(0).id]; |
eads@30
|
104 |
eads@17
|
105 editor.trigger('wysiwygDetach', params); |
eads@17
|
106 editor.trigger('wysiwygAttach', params); |
eads@17
|
107 |
eads@17
|
108 for (editor_id in data.editor_representations) { |
eads@17
|
109 Drupal.settings.dndEditorRepresentations[editor_id] = data.editor_representations[editor_id]; |
eads@17
|
110 } |
eads@17
|
111 for (preview_id in data.library_previews) { |
eads@17
|
112 Drupal.settings.dndLibraryPreviews[preview_id] = data.library_previews[preview_id]; |
eads@17
|
113 } |
eads@19
|
114 |
eads@30
|
115 // Add preview behavior to editor items (thanks, BeautyTips!) |
eads@30
|
116 $('.editor-item', $this).each(function () { |
eads@31
|
117 $(this).bt(Drupal.settings.dndLibraryPreviews[this.id], settings.btSettings); |
franck@43
|
118 //$(this).hoverIntent(settings.libraryHoverIntentSettings); |
eads@30
|
119 }); |
eads@30
|
120 |
eads@30
|
121 // Preload images in editor representations |
eads@30
|
122 var cached = $.data($(editor), 'dnd_preload') || {}; |
eads@31
|
123 for (editor_id in Drupal.settings.dndEditorRepresentations) { |
eads@30
|
124 if (!cached[editor_id]) { |
eads@30
|
125 $representation = $(Drupal.settings.dndEditorRepresentations[editor_id].body); |
eads@30
|
126 if ($representation.is('img') && $representation.get(0).src) { |
eads@30
|
127 $representation.attr('src', $representation.get(0).src); |
eads@30
|
128 } else { |
eads@30
|
129 $('img', $representation).each(function() { |
eads@31
|
130 $(this).attr('src', this.src); |
eads@30
|
131 }); |
eads@30
|
132 } |
eads@30
|
133 } |
eads@30
|
134 } |
eads@31
|
135 $.data($(editor), 'dnd_preload', cached); |
eads@30
|
136 |
eads@31
|
137 $('.pager a', $this).click(function() { |
franck@42
|
138 // At page switching, close all opened BeautyTips. |
franck@42
|
139 $('.editor-item.bt-active').btOff(); |
defr@47
|
140 $this.get(0).library_url = this.href; |
eads@31
|
141 $.getJSON(this.href, function(data) { |
eads@31
|
142 Drupal.behaviors.dndLibrary.renderLibrary.call($this.get(0), data, $(editor)); |
eads@31
|
143 }); |
eads@31
|
144 return false; |
eads@31
|
145 }); |
eads@31
|
146 $('.view-filters input[type=submit]', $this).click(function() { |
defr@44
|
147 var submit = $(this); |
franck@32
|
148 $('.view-filters form', $this).ajaxSubmit({ |
eads@31
|
149 'url' : Drupal.settings.basePath + settings.url, |
eads@31
|
150 'dataType' : 'json', |
eads@31
|
151 'success' : function(data) { |
defr@47
|
152 var target = submit.parents('div.dnd-library-wrapper').get(0); |
defr@47
|
153 target.library_url = this.url; |
defr@44
|
154 Drupal.behaviors.dndLibrary.renderLibrary.call(target, data, $(editor)); |
eads@31
|
155 } |
eads@31
|
156 }); |
eads@31
|
157 return false; |
eads@31
|
158 }); |
defr@46
|
159 $('.view-filters input[type=reset]', $this).click(function() { |
defr@46
|
160 var reset = $(this); |
defr@46
|
161 $('.view-filters form', $this).ajaxSubmit({ |
defr@46
|
162 'url' : Drupal.settings.basePath + settings.url, |
defr@46
|
163 'dataType' : 'json', |
defr@46
|
164 'success' : function(data) { |
defr@47
|
165 var target = reset.parents('div.dnd-library-wrapper').get(0); |
defr@47
|
166 target.library_url = Drupal.dndEnabledLibraries[editor].url; |
defr@46
|
167 Drupal.behaviors.dndLibrary.renderLibrary.call(target, data, $(editor)); |
defr@46
|
168 }, |
defr@46
|
169 'beforeSubmit': function (data, form, options) { |
defr@46
|
170 // Can't use data = [], otherwise we're creating a new array |
defr@46
|
171 // instead of modifying the existing one. |
defr@46
|
172 data.splice(0, data.length); |
defr@46
|
173 } |
defr@46
|
174 }); |
defr@46
|
175 return false; |
defr@46
|
176 }); |
franck@37
|
177 Drupal.attachBehaviors($this); |
eads@17
|
178 } |
eads@17
|
179 |
eads@16
|
180 // Dynamically compose a callback based on the editor name |
eads@4
|
181 Drupal.behaviors.dndLibrary.attach_library = function(e, data) { |
eads@17
|
182 var settings = $.extend({idSelector: Drupal.behaviors.dndLibrary.idSelector}, Drupal.settings.dndEnabledLibraries[data.field]); |
eads@4
|
183 var editor_fn = 'attach_' + data.editor; |
eads@4
|
184 if ($.isFunction(window.Drupal.behaviors.dndLibrary[editor_fn])) { |
eads@17
|
185 window.Drupal.behaviors.dndLibrary[editor_fn](data, settings); |
eads@2
|
186 } |
eads@2
|
187 } |
eads@2
|
188 |
eads@16
|
189 // Do garbage collection on detach |
eads@31
|
190 Drupal.behaviors.dndLibrary.detach_library = function(e, data) {} |
eads@4
|
191 |
eads@16
|
192 // Basic textareas |
eads@16
|
193 Drupal.behaviors.dndLibrary.attach_none = function(data, settings) { |
eads@16
|
194 settings = $.extend({ |
eads@16
|
195 targets: $('#'+ data.field), |
eads@25
|
196 processTextAreaClick: function(clicked, representation_id, e, data) { |
eads@25
|
197 var target = this, $target = $(target); |
eads@25
|
198 |
eads@25
|
199 // Update element count |
eads@25
|
200 Drupal.behaviors.dndLibrary.countElements.call(target, representation_id); |
eads@25
|
201 |
franck@38
|
202 var rep = Drupal.settings.dndEditorRepresentations[representation_id]; |
franck@38
|
203 var snippet = '<div class="dnd-drop-wrapper">' + rep.body + '</div>'; |
franck@38
|
204 if (rep.meta.legend) { |
franck@38
|
205 snippet += rep.meta.legend; |
franck@38
|
206 } |
eads@25
|
207 $target.replaceSelection(snippet, true); |
eads@16
|
208 } |
eads@16
|
209 }, settings); |
eads@16
|
210 $(settings.drop_selector).dnd(settings); |
eads@16
|
211 } |
eads@4
|
212 |
eads@16
|
213 // Attach TinyMCE |
eads@2
|
214 Drupal.behaviors.dndLibrary.attach_tinymce = function(data, settings) { |
eads@4
|
215 var tiny_instance = tinyMCE.getInstanceById(data.field); |
eads@4
|
216 |
eads@4
|
217 // If the Tiny instance exists, attach directly, otherwise wait until Tiny |
eads@4
|
218 // has registered a new instance. |
eads@4
|
219 if (tiny_instance) { |
eads@4
|
220 Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance); |
eads@4
|
221 } else { |
eads@4
|
222 var t = setInterval(function() { |
eads@4
|
223 var tiny_instance = tinyMCE.getInstanceById(data.field); |
eads@4
|
224 if (tiny_instance) { |
eads@4
|
225 Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance); |
eads@24
|
226 $('#'+ data.field +'-wrapper').trigger('dnd_attach_library'); |
eads@4
|
227 clearInterval(t); |
eads@2
|
228 } |
eads@4
|
229 }, 100); |
eads@4
|
230 } |
eads@4
|
231 } |
eads@2
|
232 |
eads@17
|
233 Drupal.behaviors.dndLibrary.idSelector = function(element) { |
eads@17
|
234 if ($(element).is('img')) { |
eads@17
|
235 return $.url.setUrl(element.src).param('dnd_id'); |
eads@17
|
236 } |
eads@17
|
237 return false; |
eads@17
|
238 } |
eads@17
|
239 |
eads@16
|
240 // Really attach TinyMCE |
eads@4
|
241 Drupal.behaviors.dndLibrary._attach_tinymce = function(data, settings, tiny_instance) { |
eads@16
|
242 var ed = tiny_instance, dom = ed.dom, s = ed.selection; |
eads@16
|
243 |
eads@4
|
244 settings = $.extend({ |
eads@4
|
245 targets: $('#'+ data.field +'-wrapper iframe'), |
eads@31
|
246 processTargets: function(targets) { |
eads@31
|
247 return targets.each(function() { |
eads@31
|
248 var target = this |
eads@31
|
249 // Decrement counter on delete |
eads@31
|
250 $(target).bind('dnd_delete', function(e, data) { |
eads@31
|
251 Drupal.behaviors.dndLibrary.countElements(target, $(data.node).attr('dnd_id'), true); |
eads@31
|
252 }); |
defr@45
|
253 $('head', $(this).contents()).append('<style type="text/css">img.drop { display: none; } div.dnd-drop-wrapper {background: #efe; border: 1px #090 solid;}</style>'); |
franck@52
|
254 $('body', $(this).contents()).addClass($('body').attr('class')); |
eads@31
|
255 return this; |
eads@31
|
256 }); |
eads@31
|
257 }, |
eads@20
|
258 processIframeDrop: function(drop, id_selector) { |
eads@20
|
259 var representation_id = id_selector.call(this, drop); |
defr@45
|
260 if (!Drupal.settings.dndEditorRepresentations[representation_id]) return; |
eads@24
|
261 var representation = Drupal.settings.dndEditorRepresentations[representation_id].body; |
franck@38
|
262 var legend = Drupal.settings.dndEditorRepresentations[representation_id].meta.legend; |
eads@20
|
263 var target = this, $target = $(target), $drop = $(drop), block; |
eads@15
|
264 |
eads@25
|
265 // Update element count |
eads@31
|
266 Drupal.behaviors.dndLibrary.countElements(target, representation_id); |
eads@24
|
267 |
eads@16
|
268 // Search through block level parents |
eads@20
|
269 $drop.parents().each(function() { |
eads@16
|
270 var $this = $(this); |
eads@16
|
271 if ($this.css('display') == 'block') { |
eads@16
|
272 block = this; |
eads@16
|
273 return false; |
eads@16
|
274 } |
eads@16
|
275 }); |
eads@16
|
276 |
eads@16
|
277 // Remove dropped item |
eads@20
|
278 $drop.remove(); |
eads@16
|
279 |
eads@16
|
280 // Create an element to insert |
franck@38
|
281 var insert = dom.create('div', {'class' : 'dnd-drop-wrapper', 'id' : 'dnd-inserted'}, representation); |
eads@16
|
282 |
eads@16
|
283 // The no-parent case |
eads@16
|
284 if ($(block).is('body')) { |
franck@39
|
285 // Never seem to be hit ? |
eads@16
|
286 s.setNode(insert); |
eads@16
|
287 } |
eads@16
|
288 else { |
eads@16
|
289 var old_id = block.id; |
eads@16
|
290 block.id = 'target-block'; |
eads@16
|
291 $block = $('#target-block', $target.contents()); |
eads@16
|
292 |
eads@16
|
293 // @TODO is finding the parent broken in safari?? |
franck@38
|
294 var snip = '<div class="dnd-drop-wrapper" id="dnd-inserted">' + representation + '</div>'; |
franck@38
|
295 if (legend) { |
franck@38
|
296 snip += legend; |
franck@38
|
297 } |
franck@38
|
298 $block.after(snip); |
eads@16
|
299 |
eads@16
|
300 // The active target block should be empty |
eads@31
|
301 if ($('#target-block:dnd_empty', $target.contents()).length > 0) { |
eads@17
|
302 $('#target-block', $target.contents()).remove(); |
eads@16
|
303 } else if (old_id) { |
eads@16
|
304 block.id = old_id; |
eads@16
|
305 } else { |
eads@16
|
306 $block.removeAttr('id'); |
eads@16
|
307 } |
eads@31
|
308 } |
eads@16
|
309 |
eads@16
|
310 var $inserted = $('#dnd-inserted', $target.contents()); |
eads@16
|
311 var inserted = $inserted.get(0); |
eads@16
|
312 |
eads@16
|
313 // Look behind in the DOM |
eads@16
|
314 var previous = $inserted.prev().get(0); |
eads@16
|
315 |
eads@16
|
316 // If the previous element is also an editor representation, we need to |
eads@16
|
317 // put a dummy paragraph between the elements to prevent editor errors. |
eads@31
|
318 if (previous ) { |
eads@31
|
319 $inserted.before('<p></p>'); |
eads@16
|
320 } |
eads@16
|
321 |
eads@16
|
322 // Look ahead in the DOM |
eads@16
|
323 var next = $inserted.next().get(0); |
eads@16
|
324 |
eads@16
|
325 // If the next item exists and isn't an editor representation, drop the |
eads@16
|
326 // caret at the beginning of the element, otherwise make a new paragraph |
eads@16
|
327 // to advance the caret to. |
eads@31
|
328 if (next && !$(next).hasClass('dnd-drop-wrapper')) { |
eads@16
|
329 $(next).prepend('<span id="__caret">_</span>'); |
eads@16
|
330 } |
eads@31
|
331 else if (!$(next).hasClass('dnd-drop-wrapper')) { |
eads@16
|
332 var after = dom.create('p', {}, '<span id="__caret">_</span>'); |
eads@16
|
333 dom.insertAfter(after, 'dnd-inserted'); |
eads@16
|
334 } |
eads@31
|
335 |
eads@16
|
336 // Force selection to reset the caret |
eads@16
|
337 var c = dom.get('__caret'); |
eads@31
|
338 if (c) { |
eads@31
|
339 s.select(c); |
eads@31
|
340 ed.execCommand('Delete', false, null); |
eads@31
|
341 dom.remove(c); |
eads@31
|
342 } |
eads@31
|
343 |
eads@31
|
344 // Unset id for next drop and add special dnd attribute for counting |
eads@31
|
345 // purposes |
eads@31
|
346 $inserted |
eads@31
|
347 .removeAttr('id') |
eads@31
|
348 .attr('dnd_id', representation_id); |
eads@31
|
349 |
eads@4
|
350 } |
eads@4
|
351 }, settings); |
eads@4
|
352 |
eads@4
|
353 $(settings.drop_selector).dnd(settings); |
eads@2
|
354 } |
eads@25
|
355 |
eads@25
|
356 |
eads@25
|
357 // Keep a counter of times a representation ID has been used |
eads@31
|
358 Drupal.behaviors.dndLibrary.countElements = function(target, representation_id, decrement) { |
eads@31
|
359 var counter = $(target).data('dnd_representation_counter'); |
eads@25
|
360 if (!counter) { |
eads@25
|
361 counter = {} |
eads@25
|
362 counter[representation_id] = 1; |
eads@25
|
363 } else if (counter && !counter[representation_id]) { |
eads@25
|
364 counter[representation_id] = 1; |
eads@25
|
365 } else { |
eads@31
|
366 counter[representation_id] = counter[representation_id] + ((decrement) ? -1 : 1); |
eads@25
|
367 } |
eads@31
|
368 $(target).data('dnd_representation_counter', counter); |
eads@31
|
369 return counter[representation_id]; |
eads@25
|
370 } |
defr@47
|
371 |
defr@47
|
372 /** |
defr@47
|
373 * Refresh the library. |
defr@47
|
374 */ |
defr@47
|
375 Drupal.dnd = {} |
defr@47
|
376 Drupal.dnd.refreshLibraries = function() { |
defr@47
|
377 var settings = Drupal.settings.dndEnabledLibraries; |
defr@47
|
378 for (editor_id in settings) { |
defr@47
|
379 var elem = $("#" + settings[editor_id].library_id).get(0); |
defr@47
|
380 var $editor = $("#" + editor_id); |
defr@47
|
381 $.getJSON(elem.library_url, function (data) { |
defr@47
|
382 Drupal.behaviors.dndLibrary.renderLibrary.call(elem, data, $editor); |
defr@47
|
383 }); |
defr@47
|
384 } |
defr@47
|
385 } |