annotate js/dnd-library.js @ 30:2d49adbd8992

Start of applying a last layer of polish: starting the process of cleaning up the library code.
author David Eads <eads@chicagotech.org>
date Tue, 17 Mar 2009 21:29:00 -0500
parents 7d6bf2dca269
children 767ebf925654
rev   line source
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@16 20 '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) {
eads@16 33 $('.dnd-library-wrapper', context).each(function() {
eads@4 34 var $this = $(this);
eads@2 35
eads@4 36 // This is a bad hack to lop off '-dnd-library' from the id to get the editor name
eads@17 37 var $editor = $('#' + this.id.slice(0, -12));
eads@17 38
eads@30 39 // Set up some initial settings for BeautyTips
eads@30 40 var settings = Drupal.settings.dndEnabledLibraries[$editor.get(0).id] = $.extend({
eads@20 41 'trigger': 'none',
eads@21 42 'width': 375,
eads@20 43 'spikeLength': 7,
eads@20 44 'spikeGirth': 9,
eads@18 45 'corner-radius' : 3,
eads@20 46 'strokeWidth': 1,
eads@20 47 'fill': '#ffd',
eads@18 48 'strokeStyle': '#555'
eads@30 49 }, Drupal.settings.dndEnabledLibraries[$editor.get(0).id]);
eads@24 50
eads@24 51 // Bind Drag and Drop plugin invocation to events emanating from Wysiwyg
eads@24 52 $editor.bind('wysiwygAttach', Drupal.behaviors.dndLibrary.attach_library);
eads@24 53 $editor.bind('wysiwygDetach', Drupal.behaviors.dndLibrary.detach_library);
eads@24 54
eads@30 55 // Set up empty objects to keep track of things
eads@30 56 Drupal.settings.dndEditorRepresentations = {};
eads@30 57 Drupal.settings.dndLibraryPreviews = {};
eads@30 58
eads@30 59 // Populate
eads@30 60 $.getJSON(Drupal.settings.basePath + settings.url, function(data) {
eads@30 61 Drupal.behaviors.dndLibrary.renderLibrary.call($this.get(0), data, $editor);
eads@17 62 });
eads@4 63
eads@4 64 });
eads@4 65 }
eads@2 66
eads@30 67 Drupal.behaviors.dndLibrary.renderLibrary = function(data, editor) {
eads@17 68 $this = $(this);
eads@17 69
eads@30 70 $this.html(data.library);
eads@17 71
eads@30 72 var settings = Drupal.settings.dndEnabledLibraries[editor.get(0).id];
eads@17 73 var params = Drupal.wysiwyg.instances[editor.get(0).id];
eads@30 74
eads@17 75 editor.trigger('wysiwygDetach', params);
eads@17 76 editor.trigger('wysiwygAttach', params);
eads@17 77
eads@17 78 for (editor_id in data.editor_representations) {
eads@17 79 Drupal.settings.dndEditorRepresentations[editor_id] = data.editor_representations[editor_id];
eads@17 80 }
eads@17 81 for (preview_id in data.library_previews) {
eads@17 82 Drupal.settings.dndLibraryPreviews[preview_id] = data.library_previews[preview_id];
eads@17 83 }
eads@19 84
eads@30 85 // Add preview behavior to editor items (thanks, BeautyTips!)
eads@30 86 $('.editor-item', $this).each(function () {
eads@30 87 $(this).bt(Drupal.settings.dndLibraryPreviews[this.id], settings.bt_settings);
eads@30 88 var hover_opts = $.extend({
eads@30 89 'interval': 500,
eads@30 90 'timeout' : 0,
eads@30 91 'over': function() {
eads@30 92 var $this = $(this);
eads@30 93 this.btOn();
eads@30 94 // Remove the preview once dragging of any image has commenced
eads@30 95 $('img', $this).bind('drag', function(e) {
eads@30 96 $this.btOff();
eads@30 97 });
eads@30 98 $('img', $this).bind('click', function(e) {
eads@30 99 $this.btOff();
eads@30 100 });
eads@30 101 },
eads@30 102 'out': function() { this.btOff(); }
eads@30 103 }, settings.libraryHoverIntentOpts);
eads@30 104 $(this).hoverIntent(hover_opts);
eads@30 105 });
eads@30 106
eads@30 107 // Preload images in editor representations
eads@30 108 var cached = $.data($(editor), 'dnd_preload') || {};
eads@30 109 /*for (editor_id in Drupal.settings.dndEditorRepresentations) {
eads@30 110 if (!cached[editor_id]) {
eads@30 111 $representation = $(Drupal.settings.dndEditorRepresentations[editor_id].body);
eads@30 112 if ($representation.is('img') && $representation.get(0).src) {
eads@30 113 $representation.attr('src', $representation.get(0).src);
eads@30 114 } else {
eads@30 115 $('img', $representation).each(function() {
eads@30 116 this.attr('src', this.src);
eads@30 117 });
eads@30 118 }
eads@30 119 }
eads@30 120 }
eads@30 121 $.data($(editor), 'dnd_preload', cached);*/
eads@30 122
eads@17 123 }
eads@17 124
eads@17 125
eads@16 126 // Dynamically compose a callback based on the editor name
eads@4 127 Drupal.behaviors.dndLibrary.attach_library = function(e, data) {
eads@17 128 var settings = $.extend({idSelector: Drupal.behaviors.dndLibrary.idSelector}, Drupal.settings.dndEnabledLibraries[data.field]);
eads@4 129 var editor_fn = 'attach_' + data.editor;
eads@4 130 if ($.isFunction(window.Drupal.behaviors.dndLibrary[editor_fn])) {
eads@17 131 window.Drupal.behaviors.dndLibrary[editor_fn](data, settings);
eads@2 132 }
eads@2 133 }
eads@2 134
eads@16 135 // Do garbage collection on detach
eads@4 136 Drupal.behaviors.dndLibrary.detach_library = function(e, data) {
eads@15 137 for (t in $(document).data('dnd_timers')) {
eads@15 138 clearInterval(t);
eads@15 139 }
eads@15 140 $(document).removeData('dnd_timers');
eads@4 141 }
eads@4 142
eads@16 143 // Basic textareas
eads@16 144 Drupal.behaviors.dndLibrary.attach_none = function(data, settings) {
eads@16 145 settings = $.extend({
eads@16 146 targets: $('#'+ data.field),
eads@25 147 processTextAreaClick: function(clicked, representation_id, e, data) {
eads@25 148 var target = this, $target = $(target);
eads@25 149
eads@25 150 // Update element count
eads@25 151 Drupal.behaviors.dndLibrary.countElements.call(target, representation_id);
eads@25 152
eads@24 153 var snippet = '<p class="dnd-dropped-wrapper">' + Drupal.settings.dndEditorRepresentations[representation_id].body + '</p>';
eads@25 154 $target.replaceSelection(snippet, true);
eads@16 155 }
eads@16 156 }, settings);
eads@16 157 $(settings.drop_selector).dnd(settings);
eads@16 158 }
eads@4 159
eads@16 160 // Attach TinyMCE
eads@2 161 Drupal.behaviors.dndLibrary.attach_tinymce = function(data, settings) {
eads@4 162 var tiny_instance = tinyMCE.getInstanceById(data.field);
eads@4 163
eads@4 164 // If the Tiny instance exists, attach directly, otherwise wait until Tiny
eads@4 165 // has registered a new instance.
eads@4 166 if (tiny_instance) {
eads@4 167 Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance);
eads@4 168 } else {
eads@4 169 var t = setInterval(function() {
eads@4 170 var tiny_instance = tinyMCE.getInstanceById(data.field);
eads@4 171 if (tiny_instance) {
eads@4 172 Drupal.behaviors.dndLibrary._attach_tinymce(data, settings, tiny_instance);
eads@24 173 $('#'+ data.field +'-wrapper').trigger('dnd_attach_library');
eads@4 174 clearInterval(t);
eads@2 175 }
eads@4 176 }, 100);
eads@4 177 }
eads@4 178 }
eads@2 179
eads@17 180 Drupal.behaviors.dndLibrary.idSelector = function(element) {
eads@17 181 if ($(element).is('img')) {
eads@17 182 return $.url.setUrl(element.src).param('dnd_id');
eads@17 183 }
eads@17 184 return false;
eads@17 185 }
eads@17 186
eads@16 187 // Really attach TinyMCE
eads@4 188 Drupal.behaviors.dndLibrary._attach_tinymce = function(data, settings, tiny_instance) {
eads@16 189 var ed = tiny_instance, dom = ed.dom, s = ed.selection;
eads@16 190
eads@4 191 settings = $.extend({
eads@4 192 targets: $('#'+ data.field +'-wrapper iframe'),
eads@20 193 processIframeDrop: function(drop, id_selector) {
eads@20 194 var representation_id = id_selector.call(this, drop);
eads@24 195 var representation = Drupal.settings.dndEditorRepresentations[representation_id].body;
eads@20 196 var target = this, $target = $(target), $drop = $(drop), block;
eads@15 197
eads@25 198 // Update element count
eads@27 199 //Drupal.behaviors.dndLibrary.countElements.call(target, representation_id);
eads@24 200
eads@16 201 // Search through block level parents
eads@20 202 $drop.parents().each(function() {
eads@16 203 var $this = $(this);
eads@16 204 if ($this.css('display') == 'block') {
eads@16 205 block = this;
eads@16 206 return false;
eads@16 207 }
eads@16 208 });
eads@16 209
eads@16 210 // Remove dropped item
eads@20 211 $drop.remove();
eads@16 212
eads@16 213 // Create an element to insert
eads@16 214 var insert = dom.create('p', {'class' : 'dnd-dropped-wrapper', 'id' : 'dnd-inserted'}, representation);
eads@16 215
eads@16 216 // The no-parent case
eads@16 217 if ($(block).is('body')) {
eads@16 218 s.setNode(insert);
eads@16 219 }
eads@16 220 else {
eads@16 221 var old_id = block.id;
eads@16 222 block.id = 'target-block';
eads@16 223 $block = $('#target-block', $target.contents());
eads@16 224
eads@16 225 // @TODO is finding the parent broken in safari??
eads@16 226 $block.after('<p class="dnd-dropped-wrapper" id="dnd-inserted">' + representation + '</p>');
eads@16 227
eads@16 228 // The active target block should be empty
eads@16 229 if ($('#target-block:empty', $target.contents()).length > 0) {
eads@17 230 $('#target-block', $target.contents()).remove();
eads@16 231 } else if (old_id) {
eads@16 232 block.id = old_id;
eads@16 233 } else {
eads@16 234 $block.removeAttr('id');
eads@16 235 }
eads@16 236 }
eads@16 237
eads@16 238 var $inserted = $('#dnd-inserted', $target.contents());
eads@16 239 var inserted = $inserted.get(0);
eads@16 240
eads@16 241 // Look behind in the DOM
eads@16 242 var previous = $inserted.prev().get(0);
eads@16 243
eads@16 244 // If the previous element is also an editor representation, we need to
eads@16 245 // put a dummy paragraph between the elements to prevent editor errors.
eads@16 246 if (previous && $(previous).hasClass('dnd-dropped-wrapper')) {
eads@16 247 $inserted.before('<p><span id="__spacer">_</span></p>');
eads@16 248 c = dom.get('__spacer');
eads@16 249 s.select(c);
eads@16 250 ed.execCommand('Delete', false, null);
eads@16 251 dom.remove(c);
eads@16 252 }
eads@16 253
eads@16 254 // Look ahead in the DOM
eads@16 255 var next = $inserted.next().get(0);
eads@16 256
eads@16 257 // If the next item exists and isn't an editor representation, drop the
eads@16 258 // caret at the beginning of the element, otherwise make a new paragraph
eads@16 259 // to advance the caret to.
eads@28 260 if (next && !$(next).hasClass('dnd-dropped-wrapper')) {
eads@16 261 $(next).prepend('<span id="__caret">_</span>');
eads@16 262 }
eads@16 263 else {
eads@16 264 var after = dom.create('p', {}, '<span id="__caret">_</span>');
eads@16 265 dom.insertAfter(after, 'dnd-inserted');
eads@16 266 }
eads@16 267
eads@16 268 // Clear the ID for the next drop
eads@16 269 $inserted.removeAttr('id');
eads@16 270
eads@16 271 // Force selection to reset the caret
eads@16 272 var c = dom.get('__caret');
eads@16 273 s.select(c);
eads@16 274 ed.execCommand('Delete', false, null);
eads@16 275 dom.remove(c);
eads@4 276 }
eads@4 277 }, settings);
eads@4 278
eads@4 279 $(settings.drop_selector).dnd(settings);
eads@2 280 }
eads@25 281
eads@25 282
eads@25 283 // Keep a counter of times a representation ID has been used
eads@25 284 Drupal.behaviors.dndLibrary.countElements = function(representation_id) {
eads@25 285 // We need to track element usage betwen all editors, so we look for the
eads@25 286 // parent form item
eads@25 287 $target = $(this).parents('.form-item');
eads@25 288 var counter = $target.data('representation_counter');
eads@25 289 if (!counter) {
eads@25 290 counter = {}
eads@25 291 counter[representation_id] = 1;
eads@25 292 } else if (counter && !counter[representation_id]) {
eads@25 293 counter[representation_id] = 1;
eads@25 294 } else {
eads@25 295 counter[representation_id] = counter[representation_id] + 1;
eads@25 296 }
eads@25 297 $target.data('representation_counter', counter);
eads@25 298 }