comparison js/dnd.js @ 3:5df0783706f7

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