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