michael@0: // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: // michael@0: // USE OF THIS API FOR DRAG AND DROP IS DEPRECATED! michael@0: // Do not use this file for new code. michael@0: // michael@0: // For documentation about what to use instead, see: michael@0: // http://developer.mozilla.org/En/DragDrop/Drag_and_Drop michael@0: // michael@0: //////////////////////////////////////////////////////////////////////// michael@0: michael@0: michael@0: /** michael@0: * nsTransferable - a wrapper for nsITransferable that simplifies michael@0: * javascript clipboard and drag&drop. for use in michael@0: * these situations you should use the nsClipboard michael@0: * and nsDragAndDrop wrappers for more convenience michael@0: **/ michael@0: michael@0: var nsTransferable = { michael@0: /** michael@0: * nsITransferable set (TransferData aTransferData) ; michael@0: * michael@0: * Creates a transferable with data for a list of supported types ("flavours") michael@0: * michael@0: * @param TransferData aTransferData michael@0: * a javascript object in the format described above michael@0: **/ michael@0: set: function (aTransferDataSet) michael@0: { michael@0: var trans = this.createTransferable(); michael@0: for (var i = 0; i < aTransferDataSet.dataList.length; ++i) michael@0: { michael@0: var currData = aTransferDataSet.dataList[i]; michael@0: var currFlavour = currData.flavour.contentType; michael@0: trans.addDataFlavor(currFlavour); michael@0: var supports = null; // nsISupports data michael@0: var length = 0; michael@0: if (currData.flavour.dataIIDKey == "nsISupportsString") michael@0: { michael@0: supports = Components.classes["@mozilla.org/supports-string;1"] michael@0: .createInstance(Components.interfaces.nsISupportsString); michael@0: michael@0: supports.data = currData.supports; michael@0: length = supports.data.length; michael@0: } michael@0: else michael@0: { michael@0: // non-string data. michael@0: supports = currData.supports; michael@0: length = 0; // kFlavorHasDataProvider michael@0: } michael@0: trans.setTransferData(currFlavour, supports, length * 2); michael@0: } michael@0: return trans; michael@0: }, michael@0: michael@0: /** michael@0: * TransferData/TransferDataSet get (FlavourSet aFlavourSet, michael@0: * Function aRetrievalFunc, Boolean aAnyFlag) ; michael@0: * michael@0: * Retrieves data from the transferable provided in aRetrievalFunc, formatted michael@0: * for more convenient access. michael@0: * michael@0: * @param FlavourSet aFlavourSet michael@0: * a FlavourSet object that contains a list of supported flavours. michael@0: * @param Function aRetrievalFunc michael@0: * a reference to a function that returns a nsISupportsArray of nsITransferables michael@0: * for each item from the specified source (clipboard/drag&drop etc) michael@0: * @param Boolean aAnyFlag michael@0: * a flag specifying whether or not a specific flavour is requested. If false, michael@0: * data of the type of the first flavour in the flavourlist parameter is returned, michael@0: * otherwise the best flavour supported will be returned. michael@0: **/ michael@0: get: function (aFlavourSet, aRetrievalFunc, aAnyFlag) michael@0: { michael@0: if (!aRetrievalFunc) michael@0: throw "No data retrieval handler provided!"; michael@0: michael@0: var supportsArray = aRetrievalFunc(aFlavourSet); michael@0: var dataArray = []; michael@0: var count = supportsArray.Count(); michael@0: michael@0: // Iterate over the number of items returned from aRetrievalFunc. For michael@0: // clipboard operations, this is 1, for drag and drop (where multiple michael@0: // items may have been dragged) this could be >1. michael@0: for (var i = 0; i < count; i++) michael@0: { michael@0: var trans = supportsArray.GetElementAt(i); michael@0: if (!trans) continue; michael@0: trans = trans.QueryInterface(Components.interfaces.nsITransferable); michael@0: michael@0: var data = { }; michael@0: var length = { }; michael@0: michael@0: var currData = null; michael@0: if (aAnyFlag) michael@0: { michael@0: var flavour = { }; michael@0: trans.getAnyTransferData(flavour, data, length); michael@0: if (data && flavour) michael@0: { michael@0: var selectedFlavour = aFlavourSet.flavourTable[flavour.value]; michael@0: if (selectedFlavour) michael@0: dataArray[i] = FlavourToXfer(data.value, length.value, selectedFlavour); michael@0: } michael@0: } michael@0: else michael@0: { michael@0: var firstFlavour = aFlavourSet.flavours[0]; michael@0: trans.getTransferData(firstFlavour, data, length); michael@0: if (data && firstFlavour) michael@0: dataArray[i] = FlavourToXfer(data.value, length.value, firstFlavour); michael@0: } michael@0: } michael@0: return new TransferDataSet(dataArray); michael@0: }, michael@0: michael@0: /** michael@0: * nsITransferable createTransferable (void) ; michael@0: * michael@0: * Creates and returns a transferable object. michael@0: **/ michael@0: createTransferable: function () michael@0: { michael@0: const kXferableContractID = "@mozilla.org/widget/transferable;1"; michael@0: const kXferableIID = Components.interfaces.nsITransferable; michael@0: var trans = Components.classes[kXferableContractID].createInstance(kXferableIID); michael@0: trans.init(null); michael@0: return trans; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A FlavourSet is a simple type that represents a collection of Flavour objects. michael@0: * FlavourSet is constructed from an array of Flavours, and stores this list as michael@0: * an array and a hashtable. The rationale for the dual storage is as follows: michael@0: * michael@0: * Array: Ordering is important when adding data flavours to a transferable. michael@0: * Flavours added first are deemed to be 'preferred' by the client. michael@0: * Hash: Convenient lookup of flavour data using the content type (MIME type) michael@0: * of data as a key. michael@0: */ michael@0: function FlavourSet(aFlavourList) michael@0: { michael@0: this.flavours = aFlavourList || []; michael@0: this.flavourTable = { }; michael@0: michael@0: this._XferID = "FlavourSet"; michael@0: michael@0: for (var i = 0; i < this.flavours.length; ++i) michael@0: this.flavourTable[this.flavours[i].contentType] = this.flavours[i]; michael@0: } michael@0: michael@0: FlavourSet.prototype = { michael@0: appendFlavour: function (aFlavour, aFlavourIIDKey) michael@0: { michael@0: var flavour = new Flavour (aFlavour, aFlavourIIDKey); michael@0: this.flavours.push(flavour); michael@0: this.flavourTable[flavour.contentType] = flavour; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A Flavour is a simple type that represents a data type that can be handled. michael@0: * It takes a content type (MIME type) which is used when storing data on the michael@0: * system clipboard/drag and drop, and an IIDKey (string interface name michael@0: * which is used to QI data to an appropriate form. The default interface is michael@0: * assumed to be wide-string. michael@0: */ michael@0: function Flavour(aContentType, aDataIIDKey) michael@0: { michael@0: this.contentType = aContentType; michael@0: this.dataIIDKey = aDataIIDKey || "nsISupportsString"; michael@0: michael@0: this._XferID = "Flavour"; michael@0: } michael@0: michael@0: function TransferDataBase() {} michael@0: TransferDataBase.prototype = { michael@0: push: function (aItems) michael@0: { michael@0: this.dataList.push(aItems); michael@0: }, michael@0: michael@0: get first () michael@0: { michael@0: return "dataList" in this && this.dataList.length ? this.dataList[0] : null; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * TransferDataSet is a list (array) of TransferData objects, which represents michael@0: * data dragged from one or more elements. michael@0: */ michael@0: function TransferDataSet(aTransferDataList) michael@0: { michael@0: this.dataList = aTransferDataList || []; michael@0: michael@0: this._XferID = "TransferDataSet"; michael@0: } michael@0: TransferDataSet.prototype = TransferDataBase.prototype; michael@0: michael@0: /** michael@0: * TransferData is a list (array) of FlavourData for all the applicable content michael@0: * types associated with a drag from a single item. michael@0: */ michael@0: function TransferData(aFlavourDataList) michael@0: { michael@0: this.dataList = aFlavourDataList || []; michael@0: michael@0: this._XferID = "TransferData"; michael@0: } michael@0: TransferData.prototype = { michael@0: __proto__: TransferDataBase.prototype, michael@0: michael@0: addDataForFlavour: function (aFlavourString, aData, aLength, aDataIIDKey) michael@0: { michael@0: this.dataList.push(new FlavourData(aData, aLength, michael@0: new Flavour(aFlavourString, aDataIIDKey))); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * FlavourData is a type that represents data retrieved from the system michael@0: * clipboard or drag and drop. It is constructed internally by the Transferable michael@0: * using the raw (nsISupports) data from the clipboard, the length of the data, michael@0: * and an object of type Flavour representing the type. Clients implementing michael@0: * IDragDropObserver receive an object of this type in their implementation of michael@0: * onDrop. They access the 'data' property to retrieve data, which is either data michael@0: * QI'ed to a usable form, or unicode string. michael@0: */ michael@0: function FlavourData(aData, aLength, aFlavour) michael@0: { michael@0: this.supports = aData; michael@0: this.contentLength = aLength; michael@0: this.flavour = aFlavour || null; michael@0: michael@0: this._XferID = "FlavourData"; michael@0: } michael@0: michael@0: FlavourData.prototype = { michael@0: get data () michael@0: { michael@0: if (this.flavour && michael@0: this.flavour.dataIIDKey != "nsISupportsString") michael@0: return this.supports.QueryInterface(Components.interfaces[this.flavour.dataIIDKey]); michael@0: michael@0: var supports = this.supports; michael@0: if (supports instanceof Components.interfaces.nsISupportsString) michael@0: return supports.data.substring(0, this.contentLength/2); michael@0: michael@0: return supports; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Create a TransferData object with a single FlavourData entry. Used when michael@0: * unwrapping data of a specific flavour from the drag service. michael@0: */ michael@0: function FlavourToXfer(aData, aLength, aFlavour) michael@0: { michael@0: return new TransferData([new FlavourData(aData, aLength, aFlavour)]); michael@0: } michael@0: michael@0: var transferUtils = { michael@0: michael@0: retrieveURLFromData: function (aData, flavour) michael@0: { michael@0: switch (flavour) { michael@0: case "text/unicode": michael@0: case "text/plain": michael@0: case "text/x-moz-text-internal": michael@0: return aData.replace(/^\s+|\s+$/g, ""); michael@0: case "text/x-moz-url": michael@0: return ((aData instanceof Components.interfaces.nsISupportsString) ? aData.toString() : aData).split("\n")[0]; michael@0: case "application/x-moz-file": michael@0: var ioService = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService); michael@0: var fileHandler = ioService.getProtocolHandler("file") michael@0: .QueryInterface(Components.interfaces.nsIFileProtocolHandler); michael@0: return fileHandler.getURLSpecFromFile(aData); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: } michael@0: michael@0: /** michael@0: * nsDragAndDrop - a convenience wrapper for nsTransferable, nsITransferable michael@0: * and nsIDragService/nsIDragSession. michael@0: * michael@0: * Use: map the handler functions to the 'ondraggesture', 'ondragover' and michael@0: * 'ondragdrop' event handlers on your XML element, e.g. michael@0: * michael@0: * michael@0: * You need to create an observer js object with the following member michael@0: * functions: michael@0: * Object onDragStart (event) // called when drag initiated, michael@0: * // returns flavour list with data michael@0: * // to stuff into transferable michael@0: * void onDragOver (Object flavour) // called when element is dragged michael@0: * // over, so that it can perform michael@0: * // any drag-over feedback for provided michael@0: * // flavour michael@0: * void onDrop (Object data) // formatted data object dropped. michael@0: * Object getSupportedFlavours () // returns a flavour list so that michael@0: * // nsTransferable can determine michael@0: * // whether or not to accept drop. michael@0: **/ michael@0: michael@0: var nsDragAndDrop = { michael@0: michael@0: _mDS: null, michael@0: get mDragService() michael@0: { michael@0: if (!this._mDS) michael@0: { michael@0: const kDSContractID = "@mozilla.org/widget/dragservice;1"; michael@0: const kDSIID = Components.interfaces.nsIDragService; michael@0: this._mDS = Components.classes[kDSContractID].getService(kDSIID); michael@0: } michael@0: return this._mDS; michael@0: }, michael@0: michael@0: /** michael@0: * void startDrag (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * called when a drag on an element is started. michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by the drag init michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: startDrag: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!("onDragStart" in aDragDropObserver)) michael@0: return; michael@0: michael@0: const kDSIID = Components.interfaces.nsIDragService; michael@0: var dragAction = { action: kDSIID.DRAGDROP_ACTION_COPY + kDSIID.DRAGDROP_ACTION_MOVE + kDSIID.DRAGDROP_ACTION_LINK }; michael@0: michael@0: var transferData = { data: null }; michael@0: try michael@0: { michael@0: aDragDropObserver.onDragStart(aEvent, transferData, dragAction); michael@0: } michael@0: catch (e) michael@0: { michael@0: return; // not a draggable item, bail! michael@0: } michael@0: michael@0: if (!transferData.data) return; michael@0: transferData = transferData.data; michael@0: michael@0: var dt = aEvent.dataTransfer; michael@0: var count = 0; michael@0: do { michael@0: var tds = transferData._XferID == "TransferData" michael@0: ? transferData michael@0: : transferData.dataList[count] michael@0: for (var i = 0; i < tds.dataList.length; ++i) michael@0: { michael@0: var currData = tds.dataList[i]; michael@0: var currFlavour = currData.flavour.contentType; michael@0: var value = currData.supports; michael@0: if (value instanceof Components.interfaces.nsISupportsString) michael@0: value = value.toString(); michael@0: dt.mozSetDataAt(currFlavour, value, count); michael@0: } michael@0: michael@0: count++; michael@0: } michael@0: while (transferData._XferID == "TransferDataSet" && michael@0: count < transferData.dataList.length); michael@0: michael@0: dt.effectAllowed = "all"; michael@0: // a drag targeted at a tree should instead use the treechildren so that michael@0: // the current selection is used as the drag feedback michael@0: dt.addElement(aEvent.originalTarget.localName == "treechildren" ? michael@0: aEvent.originalTarget : aEvent.target); michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * void dragOver (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * called when a drag passes over this element michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by passing over the element michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: dragOver: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!("onDragOver" in aDragDropObserver)) michael@0: return; michael@0: if (!this.checkCanDrop(aEvent, aDragDropObserver)) michael@0: return; michael@0: var flavourSet = aDragDropObserver.getSupportedFlavours(); michael@0: for (var flavour in flavourSet.flavourTable) michael@0: { michael@0: if (this.mDragSession.isDataFlavorSupported(flavour)) michael@0: { michael@0: aDragDropObserver.onDragOver(aEvent, michael@0: flavourSet.flavourTable[flavour], michael@0: this.mDragSession); michael@0: aEvent.stopPropagation(); michael@0: aEvent.preventDefault(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: mDragSession: null, michael@0: michael@0: /** michael@0: * void drop (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * called when the user drops on the element michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by the drop michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: drop: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!("onDrop" in aDragDropObserver)) michael@0: return; michael@0: if (!this.checkCanDrop(aEvent, aDragDropObserver)) michael@0: return; michael@0: michael@0: var flavourSet = aDragDropObserver.getSupportedFlavours(); michael@0: michael@0: var dt = aEvent.dataTransfer; michael@0: var dataArray = []; michael@0: var count = dt.mozItemCount; michael@0: for (var i = 0; i < count; ++i) { michael@0: var types = dt.mozTypesAt(i); michael@0: for (var j = 0; j < flavourSet.flavours.length; j++) { michael@0: var type = flavourSet.flavours[j].contentType; michael@0: // dataTransfer uses text/plain but older code used text/unicode, so michael@0: // switch this for compatibility michael@0: var modtype = (type == "text/unicode") ? "text/plain" : type; michael@0: if (Array.indexOf(types, modtype) >= 0) { michael@0: var data = dt.mozGetDataAt(modtype, i); michael@0: if (data) { michael@0: // Non-strings need some non-zero value used for their data length. michael@0: const kNonStringDataLength = 4; michael@0: michael@0: var length = (typeof data == "string") ? data.length : kNonStringDataLength; michael@0: dataArray[i] = FlavourToXfer(data, length, flavourSet.flavourTable[type]); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: var transferData = new TransferDataSet(dataArray) michael@0: michael@0: // hand over to the client to respond to dropped data michael@0: var multiple = "canHandleMultipleItems" in aDragDropObserver && aDragDropObserver.canHandleMultipleItems; michael@0: var dropData = multiple ? transferData : transferData.first.first; michael@0: aDragDropObserver.onDrop(aEvent, dropData, this.mDragSession); michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * void dragExit (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * called when a drag leaves this element michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by leaving the element michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: dragExit: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!this.checkCanDrop(aEvent, aDragDropObserver)) michael@0: return; michael@0: if ("onDragExit" in aDragDropObserver) michael@0: aDragDropObserver.onDragExit(aEvent, this.mDragSession); michael@0: }, michael@0: michael@0: /** michael@0: * void dragEnter (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * called when a drag enters in this element michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by entering in the element michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: dragEnter: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!this.checkCanDrop(aEvent, aDragDropObserver)) michael@0: return; michael@0: if ("onDragEnter" in aDragDropObserver) michael@0: aDragDropObserver.onDragEnter(aEvent, this.mDragSession); michael@0: }, michael@0: michael@0: /** michael@0: * Boolean checkCanDrop (DOMEvent aEvent, Object aDragDropObserver) ; michael@0: * michael@0: * Sets the canDrop attribute for the drag session. michael@0: * returns false if there is no current drag session. michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by the drop michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: **/ michael@0: checkCanDrop: function (aEvent, aDragDropObserver) michael@0: { michael@0: if (!this.mDragSession) michael@0: this.mDragSession = this.mDragService.getCurrentSession(); michael@0: if (!this.mDragSession) michael@0: return false; michael@0: this.mDragSession.canDrop = this.mDragSession.sourceNode != aEvent.target; michael@0: if ("canDrop" in aDragDropObserver) michael@0: this.mDragSession.canDrop &= aDragDropObserver.canDrop(aEvent, this.mDragSession); michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Do a security check for drag n' drop. Make sure the source document michael@0: * can load the dragged link. michael@0: * michael@0: * @param DOMEvent aEvent michael@0: * the DOM event fired by leaving the element michael@0: * @param Object aDragDropObserver michael@0: * javascript object of format described above that specifies michael@0: * the way in which the element responds to drag events. michael@0: * @param String aDraggedText michael@0: * the text being dragged michael@0: **/ michael@0: dragDropSecurityCheck: function (aEvent, aDragSession, aDraggedText) michael@0: { michael@0: // Strip leading and trailing whitespace, then try to create a michael@0: // URI from the dropped string. If that succeeds, we're michael@0: // dropping a URI and we need to do a security check to make michael@0: // sure the source document can load the dropped URI. We don't michael@0: // so much care about creating the real URI here michael@0: // (i.e. encoding differences etc don't matter), we just want michael@0: // to know if aDraggedText really is a URI. michael@0: michael@0: aDraggedText = aDraggedText.replace(/^\s*|\s*$/g, ''); michael@0: michael@0: var uri; michael@0: var ioService = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService); michael@0: try { michael@0: uri = ioService.newURI(aDraggedText, null, null); michael@0: } catch (e) { michael@0: } michael@0: michael@0: if (!uri) michael@0: return; michael@0: michael@0: // aDraggedText is a URI, do the security check. michael@0: const nsIScriptSecurityManager = Components.interfaces michael@0: .nsIScriptSecurityManager; michael@0: var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] michael@0: .getService(nsIScriptSecurityManager); michael@0: michael@0: if (!aDragSession) michael@0: aDragSession = this.mDragService.getCurrentSession(); michael@0: michael@0: var sourceDoc = aDragSession.sourceDocument; michael@0: // Use "file:///" as the default sourceURI so that drops of file:// URIs michael@0: // are always allowed. michael@0: var principal = sourceDoc ? sourceDoc.nodePrincipal michael@0: : secMan.getSimpleCodebasePrincipal(ioService.newURI("file:///", null, null)); michael@0: michael@0: try { michael@0: secMan.checkLoadURIStrWithPrincipal(principal, aDraggedText, michael@0: nsIScriptSecurityManager.STANDARD); michael@0: } catch (e) { michael@0: // Stop event propagation right here. michael@0: aEvent.stopPropagation(); michael@0: michael@0: throw "Drop of " + aDraggedText + " denied."; michael@0: } michael@0: } michael@0: }; michael@0: