eads@3
|
1 /* jQuery Drag and Drop Library for Rich Editors |
eads@3
|
2 * |
eads@3
|
3 * This library exists to provide a jQuery plugin that manages dragging |
eads@3
|
4 * and dropping of page assets into textareas and rich-text editors which use |
eads@3
|
5 * the iframe + designMode method. |
eads@3
|
6 * |
eads@3
|
7 * This plugin has a rather elaborate set of considerations based on common |
eads@3
|
8 * configurations of rich text editors and serious disparity in how browsers |
eads@3
|
9 * handle dragging and dropping assets into an iframe. |
eads@3
|
10 * |
eads@3
|
11 * The plugin scans an iframe with an embedded document with designMode enabled |
eads@3
|
12 * and tries to detect content that was injected by dragging and dropping |
eads@3
|
13 * within the browser. If it detects an injection of content, it attempts to |
eads@3
|
14 * conjure up an "editor representation" of the content based on the markup |
eads@3
|
15 * passed in. |
eads@3
|
16 * |
eads@3
|
17 * This works because links and images drop with their full HTML syntax in-tact |
eads@3
|
18 * across most browsers and platforms. This means we can set a timer on the |
eads@3
|
19 * iframe that scans for the insertion of the markup and replace it with |
eads@3
|
20 * another HTML snippet, as well as triggering actions inside the editor itself |
eads@3
|
21 * (this will perhaps be handled by a set of editor-specific plugins). |
eads@3
|
22 * |
eads@3
|
23 * Because of the mechanism of operation, it is expected that the iframe |
eads@3
|
24 * contain CSS which hides the markup dropped in to create the illusion of |
eads@3
|
25 * seamlessly dropping in the "editor representation" of the dropped item. |
eads@3
|
26 * |
eads@3
|
27 * It is expected that the implementer will be parsing the input text for |
eads@3
|
28 * the proper markup on the server side and making some decisions about how |
eads@3
|
29 * to parse and handle that markup on load and save. |
eads@3
|
30 * |
eads@3
|
31 * Of special interest is graceful degradation. There is no ideal graceful |
eads@3
|
32 * degradation path at this time. Every mainstream browser except IE 6 and |
eads@3
|
33 * IE 7 drop links and images into textareas with their href and src URIs, |
eads@3
|
34 * respectively, including querystrings. That means that the image or link |
eads@3
|
35 * url can be used for parsing, or a querystring included. But it won't work |
eads@3
|
36 * in Internet Explorer, and probably will require a server-side browser check |
eads@3
|
37 * in full blown implementations of the library system. |
eads@3
|
38 * |
eads@3
|
39 * Basic usage: |
eads@3
|
40 * |
eads@3
|
41 * $('a.my-class').dnd({targets: $('#my-iframe')}); |
eads@3
|
42 * |
eads@3
|
43 * Options: |
eads@3
|
44 * |
eads@3
|
45 * targets |
eads@3
|
46 * A jQuery object corresponding to the proper iframe(s) to enable dragging |
eads@3
|
47 * and dropping for. [Required] |
eads@3
|
48 * |
eads@3
|
49 * dropWrapper |
eads@3
|
50 * An html snippet to wrap the entire inserted content with in the editor |
eads@3
|
51 * representation (i.e. '<p class="foo"></p>') |
eads@3
|
52 * |
eads@3
|
53 * insertBefore: |
eads@3
|
54 * Markup to insert before drop (i.e. '<hr />') |
eads@3
|
55 * |
eads@3
|
56 * insertAfter: |
eads@3
|
57 * Markup to insert after drop (i.e. '<div class="clearfix"></div>') |
eads@3
|
58 * |
eads@3
|
59 * processedClass: |
eads@3
|
60 * The class to apply to links and images tagged as droppable. This class |
eads@3
|
61 * should have a style rule in the editor that sets display to 'none' for |
eads@3
|
62 * the best experience. |
eads@3
|
63 * |
eads@3
|
64 * idSelector: |
eads@3
|
65 * A callback that parses a unique id out of a droppable element. B y default |
eads@3
|
66 * this uses the id of the element, but one could parse out an ID based on |
eads@3
|
67 * any part of the URL, interior markup, etc. |
eads@3
|
68 * |
eads@3
|
69 * renderRepresentation: |
eads@3
|
70 * A callback that defines the mechanism for rendering a representation of |
eads@3
|
71 * the content. The default is currently essential useless. |
eads@3
|
72 * |
eads@3
|
73 * preprocessDrop: |
eads@3
|
74 * A callback that preprocesses the dropped snippet in the iframe, before |
eads@3
|
75 * replacing it. By default this uses a little logic to walk up the DOM |
eads@3
|
76 * tree to topmost parent of the place in the source where the item was |
eads@3
|
77 * dropped, and add the dropped element after that parent instead of |
eads@3
|
78 * inside it. |
eads@3
|
79 * |
eads@3
|
80 * postprocessDrop: |
eads@3
|
81 * A callback that postprocesses the iframe. |
eads@3
|
82 * |
eads@3
|
83 */ |
eads@3
|
84 |
eads@3
|
85 (function($) { |
eads@3
|
86 $.fn.dnd = function(opt) { |
eads@3
|
87 opt = $.extend({}, { |
eads@4
|
88 dropWrapper: '<p class="dnd-dropped"></p>', |
eads@3
|
89 insertBefore: '', |
eads@3
|
90 insertAfter: '', |
eads@3
|
91 processedClass: 'dnd-processed', |
eads@4
|
92 disableClick: true, |
eads@3
|
93 |
eads@3
|
94 processTargets: function(targets) { |
eads@3
|
95 return targets.each(function() { |
eads@3
|
96 $('head', $(this).contents()).append('<style type="text/css">.dnd-processed { display: none; }</style>'); |
eads@3
|
97 return this; |
eads@3
|
98 }); |
eads@3
|
99 }, |
eads@3
|
100 |
eads@3
|
101 // Must return a string |
eads@3
|
102 idSelector: function(element) { |
eads@3
|
103 if (!element.id) { |
eads@3
|
104 // @TODO sanitize output here |
eads@3
|
105 if ($(element).is('a')) { |
eads@3
|
106 return element.href; |
eads@3
|
107 } |
eads@3
|
108 if ($(element).is('img')) { |
eads@3
|
109 return element.src; |
eads@3
|
110 } |
eads@3
|
111 } |
eads@3
|
112 return element.id; |
eads@3
|
113 }, |
eads@3
|
114 |
eads@3
|
115 // @TODO: Target should be jQuery object |
eads@3
|
116 targets: $('iframe, textarea'), |
eads@3
|
117 |
eads@3
|
118 // Must return a string that DOES NOT share the id of any droppable item |
eads@3
|
119 // living outside the iframe |
eads@3
|
120 renderRepresentation: function(target, drop, representation_id) { |
eads@3
|
121 // Keep a counter of how many times this element was used |
eads@3
|
122 var count = $.data(target, representation_id +'_count'); |
eads@3
|
123 if (!count) { |
eads@3
|
124 count = 1; |
eads@3
|
125 } else { |
eads@3
|
126 count++; |
eads@3
|
127 } |
eads@3
|
128 $.data(target, representation_id +'_count', count); |
eads@3
|
129 return '<span id="dnd-' + representation_id +'-'+ count +'">' + representation_id + '</span>'; |
eads@3
|
130 }, |
eads@3
|
131 |
eads@3
|
132 // Back out markup to render in place after parent container |
eads@3
|
133 preprocessDrop: function(target, drop) { |
eads@3
|
134 var old_parent = false; |
eads@3
|
135 var element_id = ''; |
eads@3
|
136 |
eads@3
|
137 var parents = drop.parents(); |
eads@3
|
138 for (i=0; i < parents.length; i++) { |
eads@3
|
139 if ($(parents[i]).is('body')) { |
eads@3
|
140 element_id = $(drop).get(0).id; |
eads@3
|
141 $(old_parent).after(drop.clone()); |
eads@3
|
142 drop.remove(); |
eads@3
|
143 } |
eads@3
|
144 old_parent = parents[i]; |
eads@3
|
145 } |
eads@3
|
146 return $('#'+ element_id, $(target).contents()); |
eads@3
|
147 }, |
eads@3
|
148 |
eads@4
|
149 postprocessDrop: function(target, drop, element) { |
eads@4
|
150 $(element).addClass('dnd-inserted'); |
eads@4
|
151 } |
eads@3
|
152 |
eads@3
|
153 }, opt); |
eads@3
|
154 |
eads@3
|
155 // Initialize plugin |
eads@3
|
156 var targets = opt.processTargets(opt.targets); |
eads@4
|
157 if (opt.disableClick) { this.click(function() { return false; }) } |
eads@3
|
158 |
eads@3
|
159 // Process! |
eads@3
|
160 return this.each(function() { |
eads@3
|
161 |
eads@3
|
162 var element = this; |
eads@3
|
163 var representation_id = opt.idSelector(element); |
eads@3
|
164 |
eads@3
|
165 if (!element.id) { |
eads@3
|
166 element.id = element.tagName.toLowerCase() + '-' + representation_id; |
eads@3
|
167 } |
eads@3
|
168 |
eads@3
|
169 // Add some UI sugar and a special class |
eads@3
|
170 $(element) |
eads@3
|
171 .css('cursor', 'move') |
eads@3
|
172 .addClass(opt.processedClass); |
eads@3
|
173 |
eads@3
|
174 // We need to differentiate behavior based on the targets... I guess. |
eads@3
|
175 targets.each(function() { |
eads@3
|
176 if ($(this).is('iframe')) { |
eads@3
|
177 var target = this; |
eads@3
|
178 |
eads@3
|
179 // Watch the iframe for changes |
eads@3
|
180 t = setInterval(function() { |
eads@3
|
181 var match = $('#' + element.id, $(target).contents()); |
eads@3
|
182 if (match.length > 0) { |
eads@3
|
183 drop = opt.preprocessDrop(target, match); // Must return a jquery object |
eads@3
|
184 drop |
eads@3
|
185 .before(opt.insertBefore) |
eads@3
|
186 .after(opt.insertAfter) |
eads@3
|
187 .wrap(opt.dropWrapper) |
eads@3
|
188 .replaceWith(opt.renderRepresentation(target, drop, representation_id)); |
eads@4
|
189 opt.postprocessDrop(target, drop, element); |
eads@3
|
190 } |
eads@3
|
191 }, 100); |
eads@3
|
192 // @TODO track the timer with $.data() so we can clear it? |
eads@3
|
193 } else if ($(this).is('textarea')) { |
eads@3
|
194 console.log('@TODO handle textareas via.... regexp?'); |
eads@3
|
195 } |
eads@3
|
196 }); |
eads@3
|
197 }); |
eads@3
|
198 } |
eads@3
|
199 })(jQuery); |