!import
1 //@line 39 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/toolkit/content/nsDragAndDrop.js"
2
3 /**
4 * nsTransferable - a wrapper for nsITransferable that simplifies
5 * javascript clipboard and drag&drop. for use in
6 * these situations you should use the nsClipboard
7 * and nsDragAndDrop wrappers for more convenience
8 **/
9
10 var nsTransferable = {
11 /**
12 * nsITransferable set (TransferData aTransferData) ;
13 *
14 * Creates a transferable with data for a list of supported types ("flavours")
15 *
16 * @param TransferData aTransferData
17 * a javascript object in the format described above
18 **/
set
19 set: function (aTransferDataSet)
20 {
21 var trans = this.createTransferable();
22 for (var i = 0; i < aTransferDataSet.dataList.length; ++i)
23 {
24 var currData = aTransferDataSet.dataList[i];
25 var currFlavour = currData.flavour.contentType;
26 trans.addDataFlavor(currFlavour);
27 var supports = null; // nsISupports data
28 var length = 0;
29 if (currData.flavour.dataIIDKey == "nsISupportsString")
30 {
31 supports = Components.classes["@mozilla.org/supports-string;1"]
32 .createInstance(Components.interfaces.nsISupportsString);
33
34 supports.data = currData.supports;
35 length = supports.data.length;
36 }
37 else
38 {
39 // non-string data.
40 supports = currData.supports;
41 length = 0; // kFlavorHasDataProvider
42 }
43 trans.setTransferData(currFlavour, supports, length * 2);
44 }
45 return trans;
46 },
47
48 /**
49 * TransferData/TransferDataSet get (FlavourSet aFlavourSet,
50 * Function aRetrievalFunc, Boolean aAnyFlag) ;
51 *
52 * Retrieves data from the transferable provided in aRetrievalFunc, formatted
53 * for more convenient access.
54 *
55 * @param FlavourSet aFlavourSet
56 * a FlavourSet object that contains a list of supported flavours.
57 * @param Function aRetrievalFunc
58 * a reference to a function that returns a nsISupportsArray of nsITransferables
59 * for each item from the specified source (clipboard/drag&drop etc)
60 * @param Boolean aAnyFlag
61 * a flag specifying whether or not a specific flavour is requested. If false,
62 * data of the type of the first flavour in the flavourlist parameter is returned,
63 * otherwise the best flavour supported will be returned.
64 **/
get
65 get: function (aFlavourSet, aRetrievalFunc, aAnyFlag)
66 {
67 if (!aRetrievalFunc)
68 throw "No data retrieval handler provided!";
69
70 var supportsArray = aRetrievalFunc(aFlavourSet);
71 var dataArray = [];
72 var count = supportsArray.Count();
73
74 // Iterate over the number of items returned from aRetrievalFunc. For
75 // clipboard operations, this is 1, for drag and drop (where multiple
76 // items may have been dragged) this could be >1.
77 for (var i = 0; i < count; i++)
78 {
79 var trans = supportsArray.GetElementAt(i);
80 if (!trans) continue;
81 trans = trans.QueryInterface(Components.interfaces.nsITransferable);
82
83 var data = { };
84 var length = { };
85
86 var currData = null;
87 if (aAnyFlag)
88 {
89 var flavour = { };
90 trans.getAnyTransferData(flavour, data, length);
91 if (data && flavour)
92 {
93 var selectedFlavour = aFlavourSet.flavourTable[flavour.value];
94 if (selectedFlavour)
95 dataArray[i] = FlavourToXfer(data.value, length.value, selectedFlavour);
96 }
97 }
98 else
99 {
100 var firstFlavour = aFlavourSet.flavours[0];
101 trans.getTransferData(firstFlavour, data, length);
102 if (data && firstFlavour)
103 dataArray[i] = FlavourToXfer(data.value, length.value, firstFlavour);
104 }
105 }
106 return new TransferDataSet(dataArray);
107 },
108
109 /**
110 * nsITransferable createTransferable (void) ;
111 *
112 * Creates and returns a transferable object.
113 **/
createTransferable
114 createTransferable: function ()
115 {
116 const kXferableContractID = "@mozilla.org/widget/transferable;1";
117 const kXferableIID = Components.interfaces.nsITransferable;
118 return Components.classes[kXferableContractID].createInstance(kXferableIID);
119 }
120 };
121
122 /**
123 * A FlavourSet is a simple type that represents a collection of Flavour objects.
124 * FlavourSet is constructed from an array of Flavours, and stores this list as
125 * an array and a hashtable. The rationale for the dual storage is as follows:
126 *
127 * Array: Ordering is important when adding data flavours to a transferable.
128 * Flavours added first are deemed to be 'preferred' by the client.
129 * Hash: Convenient lookup of flavour data using the content type (MIME type)
130 * of data as a key.
131 */
FlavourSet
132 function FlavourSet(aFlavourList)
133 {
134 this.flavours = aFlavourList || [];
135 this.flavourTable = { };
136
137 this._XferID = "FlavourSet";
138
139 for (var i = 0; i < this.flavours.length; ++i)
140 this.flavourTable[this.flavours[i].contentType] = this.flavours[i];
141 }
142
143 FlavourSet.prototype = {
appendFlavour
144 appendFlavour: function (aFlavour, aFlavourIIDKey)
145 {
146 var flavour = new Flavour (aFlavour, aFlavourIIDKey);
147 this.flavours.push(flavour);
148 this.flavourTable[flavour.contentType] = flavour;
149 }
150 };
151
152 /**
153 * A Flavour is a simple type that represents a data type that can be handled.
154 * It takes a content type (MIME type) which is used when storing data on the
155 * system clipboard/drag and drop, and an IIDKey (string interface name
156 * which is used to QI data to an appropriate form. The default interface is
157 * assumed to be wide-string.
158 */
Flavour
159 function Flavour(aContentType, aDataIIDKey)
160 {
161 this.contentType = aContentType;
162 this.dataIIDKey = aDataIIDKey || "nsISupportsString";
163
164 this._XferID = "Flavour";
165 }
166
TransferDataBase
167 function TransferDataBase() {}
168 TransferDataBase.prototype = {
push
169 push: function (aItems)
170 {
171 this.dataList.push(aItems);
172 },
173
get_first
174 get first ()
175 {
176 return "dataList" in this && this.dataList.length ? this.dataList[0] : null;
177 }
178 };
179
180 /**
181 * TransferDataSet is a list (array) of TransferData objects, which represents
182 * data dragged from one or more elements.
183 */
TransferDataSet
184 function TransferDataSet(aTransferDataList)
185 {
186 this.dataList = aTransferDataList || [];
187
188 this._XferID = "TransferDataSet";
189 }
190 TransferDataSet.prototype = TransferDataBase.prototype;
191
192 /**
193 * TransferData is a list (array) of FlavourData for all the applicable content
194 * types associated with a drag from a single item.
195 */
TransferData
196 function TransferData(aFlavourDataList)
197 {
198 this.dataList = aFlavourDataList || [];
199
200 this._XferID = "TransferData";
201 }
202 TransferData.prototype = {
203 __proto__: TransferDataBase.prototype,
204
addDataForFlavour
205 addDataForFlavour: function (aFlavourString, aData, aLength, aDataIIDKey)
206 {
207 this.dataList.push(new FlavourData(aData, aLength,
208 new Flavour(aFlavourString, aDataIIDKey)));
209 }
210 };
211
212 /**
213 * FlavourData is a type that represents data retrieved from the system
214 * clipboard or drag and drop. It is constructed internally by the Transferable
215 * using the raw (nsISupports) data from the clipboard, the length of the data,
216 * and an object of type Flavour representing the type. Clients implementing
217 * IDragDropObserver receive an object of this type in their implementation of
218 * onDrop. They access the 'data' property to retrieve data, which is either data
219 * QI'ed to a usable form, or unicode string.
220 */
FlavourData
221 function FlavourData(aData, aLength, aFlavour)
222 {
223 this.supports = aData;
224 this.contentLength = aLength;
225 this.flavour = aFlavour || null;
226
227 this._XferID = "FlavourData";
228 }
229
230 FlavourData.prototype = {
get_data
231 get data ()
232 {
233 if (this.flavour &&
234 this.flavour.dataIIDKey != "nsISupportsString" )
235 return this.supports.QueryInterface(Components.interfaces[this.flavour.dataIIDKey]);
236 else {
237 var unicode = this.supports.QueryInterface(Components.interfaces.nsISupportsString);
238 if (unicode)
239 return unicode.data.substring(0, this.contentLength/2);
240
241 return this.supports;
242 }
243 return "";
244 }
245 }
246
247 /**
248 * Create a TransferData object with a single FlavourData entry. Used when
249 * unwrapping data of a specific flavour from the drag service.
250 */
FlavourToXfer
251 function FlavourToXfer(aData, aLength, aFlavour)
252 {
253 return new TransferData([new FlavourData(aData, aLength, aFlavour)]);
254 }
255
256 var transferUtils = {
257
retrieveURLFromData
258 retrieveURLFromData: function (aData, flavour)
259 {
260 switch (flavour) {
261 case "text/unicode":
262 return aData.replace(/^\s+|\s+$/g, "");
263 case "text/x-moz-url":
264 return aData.toString().split("\n")[0];
265 case "application/x-moz-file":
266 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
267 .getService(Components.interfaces.nsIIOService);
268 var fileHandler = ioService.getProtocolHandler("file")
269 .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
270 return fileHandler.getURLSpecFromFile(aData);
271 }
272 return null;
273 }
274
275 }
276
277 /**
278 * nsDragAndDrop - a convenience wrapper for nsTransferable, nsITransferable
279 * and nsIDragService/nsIDragSession.
280 *
281 * Use: map the handler functions to the 'ondraggesture', 'ondragover' and
282 * 'ondragdrop' event handlers on your XML element, e.g.
283 * <xmlelement ondraggesture="nsDragAndDrop.startDrag(event, observer);"
284 * ondragover="nsDragAndDrop.dragOver(event, observer);"
285 * ondragdrop="nsDragAndDrop.drop(event, observer);"/>
286 *
287 * You need to create an observer js object with the following member
288 * functions:
289 * Object onDragStart (event) // called when drag initiated,
290 * // returns flavour list with data
291 * // to stuff into transferable
292 * void onDragOver (Object flavour) // called when element is dragged
293 * // over, so that it can perform
294 * // any drag-over feedback for provided
295 * // flavour
296 * void onDrop (Object data) // formatted data object dropped.
297 * Object getSupportedFlavours () // returns a flavour list so that
298 * // nsTransferable can determine
299 * // whether or not to accept drop.
300 **/
301
302 var nsDragAndDrop = {
303
304 _mDS: null,
get_mDragService
305 get mDragService()
306 {
307 if (!this._mDS)
308 {
309 const kDSContractID = "@mozilla.org/widget/dragservice;1";
310 const kDSIID = Components.interfaces.nsIDragService;
311 this._mDS = Components.classes[kDSContractID].getService(kDSIID);
312 }
313 return this._mDS;
314 },
315
316 /**
317 * void startDrag (DOMEvent aEvent, Object aDragDropObserver) ;
318 *
319 * called when a drag on an element is started.
320 *
321 * @param DOMEvent aEvent
322 * the DOM event fired by the drag init
323 * @param Object aDragDropObserver
324 * javascript object of format described above that specifies
325 * the way in which the element responds to drag events.
326 **/
startDrag
327 startDrag: function (aEvent, aDragDropObserver)
328 {
329 if (!("onDragStart" in aDragDropObserver))
330 return;
331
332 const kDSIID = Components.interfaces.nsIDragService;
333 var dragAction = { action: kDSIID.DRAGDROP_ACTION_COPY + kDSIID.DRAGDROP_ACTION_MOVE + kDSIID.DRAGDROP_ACTION_LINK };
334
335 var transferData = { data: null };
336 try
337 {
338 aDragDropObserver.onDragStart(aEvent, transferData, dragAction);
339 }
340 catch (e)
341 {
342 return; // not a draggable item, bail!
343 }
344
345 if (!transferData.data) return;
346 transferData = transferData.data;
347
348 var transArray = Components.classes["@mozilla.org/supports-array;1"]
349 .createInstance(Components.interfaces.nsISupportsArray);
350
351 var region = null;
352 if (aEvent.originalTarget.localName == "treechildren") {
353 // let's build the drag region
354 var tree = aEvent.originalTarget.parentNode;
355 try {
356 region = Components.classes["@mozilla.org/gfx/region;1"].createInstance(Components.interfaces.nsIScriptableRegion);
357 region.init();
358 var obo = tree.treeBoxObject;
359 var bo = obo.treeBody.boxObject;
360 var sel= obo.view.selection;
361
362 var rowX = bo.x;
363 var rowY = bo.y;
364 var rowHeight = obo.rowHeight;
365 var rowWidth = bo.width;
366
367 //add a rectangle for each visible selected row
368 for (var i = obo.getFirstVisibleRow(); i <= obo.getLastVisibleRow(); i ++)
369 {
370 if (sel.isSelected(i))
371 region.unionRect(rowX, rowY, rowWidth, rowHeight);
372 rowY = rowY + rowHeight;
373 }
374
375 //and finally, clip the result to be sure we don't spill over...
376 region.intersectRect(bo.x, bo.y, bo.width, bo.height);
377 } catch(ex) {
378 dump("Error while building selection region: " + ex + "\n");
379 region = null;
380 }
381 }
382
383 var count = 0;
384 do
385 {
386 var trans = nsTransferable.set(transferData._XferID == "TransferData"
387 ? transferData
388 : transferData.dataList[count++]);
389 transArray.AppendElement(trans.QueryInterface(Components.interfaces.nsISupports));
390 }
391 while (transferData._XferID == "TransferDataSet" &&
392 count < transferData.dataList.length);
393
394 try {
395 this.mDragService.invokeDragSessionWithImage(aEvent.target, transArray,
396 region, dragAction.action,
397 null, 0, 0, aEvent);
398 }
399 catch(ex) {
400 // this could be because the user pressed escape to
401 // cancel the drag. even if it's not, there's not much
402 // we can do, so be silent.
403 }
404 aEvent.stopPropagation();
405 },
406
407 /**
408 * void dragOver (DOMEvent aEvent, Object aDragDropObserver) ;
409 *
410 * called when a drag passes over this element
411 *
412 * @param DOMEvent aEvent
413 * the DOM event fired by passing over the element
414 * @param Object aDragDropObserver
415 * javascript object of format described above that specifies
416 * the way in which the element responds to drag events.
417 **/
dragOver
418 dragOver: function (aEvent, aDragDropObserver)
419 {
420 if (!("onDragOver" in aDragDropObserver))
421 return;
422 if (!this.checkCanDrop(aEvent, aDragDropObserver))
423 return;
424 var flavourSet = aDragDropObserver.getSupportedFlavours();
425 for (var flavour in flavourSet.flavourTable)
426 {
427 if (this.mDragSession.isDataFlavorSupported(flavour))
428 {
429 aDragDropObserver.onDragOver(aEvent,
430 flavourSet.flavourTable[flavour],
431 this.mDragSession);
432 aEvent.stopPropagation();
433 break;
434 }
435 }
436 },
437
438 mDragSession: null,
439
440 /**
441 * void drop (DOMEvent aEvent, Object aDragDropObserver) ;
442 *
443 * called when the user drops on the element
444 *
445 * @param DOMEvent aEvent
446 * the DOM event fired by the drop
447 * @param Object aDragDropObserver
448 * javascript object of format described above that specifies
449 * the way in which the element responds to drag events.
450 **/
drop
451 drop: function (aEvent, aDragDropObserver)
452 {
453 if (!("onDrop" in aDragDropObserver))
454 return;
455 if (!this.checkCanDrop(aEvent, aDragDropObserver))
456 return;
457 if (this.mDragSession.canDrop) {
458 var flavourSet = aDragDropObserver.getSupportedFlavours();
459 var transferData = nsTransferable.get(flavourSet, this.getDragData, true);
460 // hand over to the client to respond to dropped data
461 var multiple = "canHandleMultipleItems" in aDragDropObserver && aDragDropObserver.canHandleMultipleItems;
462 var dropData = multiple ? transferData : transferData.first.first;
463 aDragDropObserver.onDrop(aEvent, dropData, this.mDragSession);
464 }
465 aEvent.stopPropagation();
466 },
467
468 /**
469 * void dragExit (DOMEvent aEvent, Object aDragDropObserver) ;
470 *
471 * called when a drag leaves this element
472 *
473 * @param DOMEvent aEvent
474 * the DOM event fired by leaving the element
475 * @param Object aDragDropObserver
476 * javascript object of format described above that specifies
477 * the way in which the element responds to drag events.
478 **/
dragExit
479 dragExit: function (aEvent, aDragDropObserver)
480 {
481 if (!this.checkCanDrop(aEvent, aDragDropObserver))
482 return;
483 if ("onDragExit" in aDragDropObserver)
484 aDragDropObserver.onDragExit(aEvent, this.mDragSession);
485 },
486
487 /**
488 * void dragEnter (DOMEvent aEvent, Object aDragDropObserver) ;
489 *
490 * called when a drag enters in this element
491 *
492 * @param DOMEvent aEvent
493 * the DOM event fired by entering in the element
494 * @param Object aDragDropObserver
495 * javascript object of format described above that specifies
496 * the way in which the element responds to drag events.
497 **/
dragEnter
498 dragEnter: function (aEvent, aDragDropObserver)
499 {
500 if (!this.checkCanDrop(aEvent, aDragDropObserver))
501 return;
502 if ("onDragEnter" in aDragDropObserver)
503 aDragDropObserver.onDragEnter(aEvent, this.mDragSession);
504 },
505
506 /**
507 * nsISupportsArray getDragData (Object aFlavourList)
508 *
509 * Creates a nsISupportsArray of all droppable items for the given
510 * set of supported flavours.
511 *
512 * @param FlavourSet aFlavourSet
513 * formatted flavour list.
514 **/
getDragData
515 getDragData: function (aFlavourSet)
516 {
517 var supportsArray = Components.classes["@mozilla.org/supports-array;1"]
518 .createInstance(Components.interfaces.nsISupportsArray);
519
520 for (var i = 0; i < nsDragAndDrop.mDragSession.numDropItems; ++i)
521 {
522 var trans = nsTransferable.createTransferable();
523 for (var j = 0; j < aFlavourSet.flavours.length; ++j)
524 trans.addDataFlavor(aFlavourSet.flavours[j].contentType);
525 nsDragAndDrop.mDragSession.getData(trans, i);
526 supportsArray.AppendElement(trans);
527 }
528 return supportsArray;
529 },
530
531 /**
532 * Boolean checkCanDrop (DOMEvent aEvent, Object aDragDropObserver) ;
533 *
534 * Sets the canDrop attribute for the drag session.
535 * returns false if there is no current drag session.
536 *
537 * @param DOMEvent aEvent
538 * the DOM event fired by the drop
539 * @param Object aDragDropObserver
540 * javascript object of format described above that specifies
541 * the way in which the element responds to drag events.
542 **/
checkCanDrop
543 checkCanDrop: function (aEvent, aDragDropObserver)
544 {
545 if (!this.mDragSession)
546 this.mDragSession = this.mDragService.getCurrentSession();
547 if (!this.mDragSession)
548 return false;
549 this.mDragSession.canDrop = this.mDragSession.sourceNode != aEvent.target;
550 if ("canDrop" in aDragDropObserver)
551 this.mDragSession.canDrop &= aDragDropObserver.canDrop(aEvent, this.mDragSession);
552 return true;
553 },
554
555 /**
556 * Do a security check for drag n' drop. Make sure the source document
557 * can load the dragged link.
558 *
559 * @param DOMEvent aEvent
560 * the DOM event fired by leaving the element
561 * @param Object aDragDropObserver
562 * javascript object of format described above that specifies
563 * the way in which the element responds to drag events.
564 * @param String aDraggedText
565 * the text being dragged
566 **/
dragDropSecurityCheck
567 dragDropSecurityCheck: function (aEvent, aDragSession, aDraggedText)
568 {
569 var sourceDoc = aDragSession.sourceDocument;
570 if (!sourceDoc)
571 return;
572
573 // Strip leading and trailing whitespace, then try to create a
574 // URI from the dropped string. If that succeeds, we're
575 // dropping a URI and we need to do a security check to make
576 // sure the source document can load the dropped URI. We don't
577 // so much care about creating the real URI here
578 // (i.e. encoding differences etc don't matter), we just want
579 // to know if aDraggedText really is a URI.
580
581 aDraggedText = aDraggedText.replace(/^\s*|\s*$/g, '');
582
583 var uri;
584
585 try {
586 uri = Components.classes["@mozilla.org/network/io-service;1"]
587 .getService(Components.interfaces.nsIIOService)
588 .newURI(aDraggedText, null, null);
589 } catch (e) {
590 }
591
592 if (!uri)
593 return;
594
595 // aDraggedText is a URI, do the security check.
596 const nsIScriptSecurityManager = Components.interfaces
597 .nsIScriptSecurityManager;
598 var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
599 .getService(nsIScriptSecurityManager);
600
601 try {
602 secMan.checkLoadURIStr(sourceDoc.documentURI, aDraggedText,
603 nsIScriptSecurityManager.STANDARD);
604 } catch (e) {
605 // Stop event propagation right here.
606 aEvent.stopPropagation();
607
608 throw "Drop of " + aDraggedText + " denied.";
609 }
610 }
611 };