2011-08-29 23:57:29 +09:00

460 lines
14 KiB
JavaScript

/*
Multiple Tabs Drag and Drop Utilities for Firefox 3.5 or later
Usage:
window['piro.sakura.ne.jp'].tabsDragUtils.initTabBrowser(gBrowser);
// in dragstart event listener
window['piro.sakura.ne.jp'].tabsDragUtils.startTabsDrag(aEvent, aArrayOfTabs);
license: The MIT License, Copyright (c) 2010-2011 SHIMODA "Piro" Hiroshi
http://github.com/piroor/fxaddonlibs/blob/master/license.txt
original:
http://github.com/piroor/fxaddonlibs/blob/master/tabsDragUtils.js
*/
(function() {
const currentRevision = 17;
if (!('piro.sakura.ne.jp' in window)) window['piro.sakura.ne.jp'] = {};
var loadedRevision = 'tabsDragUtils' in window['piro.sakura.ne.jp'] ?
window['piro.sakura.ne.jp'].tabsDragUtils.revision :
0 ;
if (loadedRevision && loadedRevision > currentRevision) {
return;
}
if (loadedRevision &&
'destroy' in window['piro.sakura.ne.jp'].tabsDragUtils)
window['piro.sakura.ne.jp'].tabsDragUtils.destroy();
const Cc = Components.classes;
const Ci = Components.interfaces;
const TAB_DROP_TYPE = 'application/x-moz-tabbrowser-tab';
var tabsDragUtils = {
revision : currentRevision,
// "nsDOM" prefix is required!
// https://developer.mozilla.org/en/Creating_Custom_Events_That_Can_Pass_Data
EVENT_TYPE_TABS_DROP : 'nsDOMMultipleTabsDrop',
init : function TDU_init()
{
window.addEventListener('load', this, false);
},
_delayedInit : function TDU_delayedInit()
{
window.removeEventListener('load', this, false);
delete this._delayedInit;
if (
'PlacesControllerDragHelper' in window &&
'onDrop' in PlacesControllerDragHelper &&
PlacesControllerDragHelper.onDrop.toSource().indexOf('tabsDragUtils.DOMDataTransferProxy') < 0
) {
eval('PlacesControllerDragHelper.onDrop = '+
PlacesControllerDragHelper.onDrop.toSource().replace(
// for Firefox 3.5 or later
'var doCopy =',
'var tabsDataTransferProxy = dt = new window["piro.sakura.ne.jp"].tabsDragUtils.DOMDataTransferProxy(dt, insertionPoint); $&'
).replace( // for Tree Style Tab (save tree structure to bookmarks)
'PlacesUIUtils.ptm.doTransaction(txn);',
<![CDATA[
if ('_tabs' in tabsDataTransferProxy &&
'TreeStyleTabBookmarksService' in window)
TreeStyleTabBookmarksService.beginAddBookmarksFromTabs(tabsDataTransferProxy._tabs);
$&
if ('_tabs' in tabsDataTransferProxy &&
'TreeStyleTabBookmarksService' in window)
TreeStyleTabBookmarksService.endAddBookmarksFromTabs();
]]>
)
);
}
if ('TMP_tabDNDObserver' in window) // for Tab Mix Plus
this.initTabDNDObserver(TMP_tabDNDObserver);
else if ('TabDNDObserver' in window) // for old Tab Mix Plus
this.initTabDNDObserver(TabDNDObserver);
},
destroy : function TDU_destroy()
{
if (this._delayedInit)
window.removeEventListener('load', this, false);
},
initTabBrowser : function TDU_initTabBrowser(aTabBrowser)
{
var tabDNDObserver = (aTabBrowser.tabContainer && aTabBrowser.tabContainer.tabbrowser == aTabBrowser) ?
aTabBrowser.tabContainer : // Firefox 4.0 or later
aTabBrowser ; // Firefox 3.5 - 3.6
this.initTabDNDObserver(tabDNDObserver);
},
destroyTabBrowser : function TDU_destroyTabBrowser(aTabBrowser)
{
},
initTabDNDObserver : function TDU_initTabDNDObserver(aObserver)
{
if ('_setEffectAllowedForDataTransfer' in aObserver &&
aObserver._setEffectAllowedForDataTransfer.toSource().indexOf('tabDragUtils') < 0) {
eval('aObserver._setEffectAllowedForDataTransfer = '+
aObserver._setEffectAllowedForDataTransfer.toSource().replace(
'dt.mozItemCount > 1',
'$& && !window["piro.sakura.ne.jp"].tabsDragUtils.isTabsDragging(arguments[0])'
)
);
}
},
startTabsDrag : function TDU_startTabsDrag(aEvent, aTabs)
{
var draggedTab = this.getTabFromEvent(aEvent);
var tabs = aTabs || [];
var index = tabs.indexOf(draggedTab);
if (index < 0)
return;
var dt = aEvent.dataTransfer;
dt.setDragImage(this.createDragFeedbackImage(tabs), 0, 0);
tabs.splice(index, 1);
tabs.unshift(draggedTab);
tabs.forEach(function(aTab, aIndex) {
dt.mozSetDataAt(TAB_DROP_TYPE, aTab, aIndex);
dt.mozSetDataAt('text/x-moz-text-internal', this.getCurrentURIOfTab(aTab), aIndex);
}, this);
// On Firefox 3.6 or older versions on Windows, drag feedback
// image isn't shown if there are multiple drag data...
if (tabs.length <= 1 ||
'mozSourceNode' in dt ||
navigator.platform.toLowerCase().indexOf('win') < 0)
dt.mozCursor = 'default';
aEvent.stopPropagation();
},
createDragFeedbackImage : function TDU_createDragFeedbackImage(aTabs)
{
var previews = aTabs.map(function(aTab) {
return tabPreviews.capture(aTab, false);
}, this);
var offset = 16;
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
canvas.width = previews[0].width + (offset * aTabs.length);
canvas.height = previews[0].height + (offset * aTabs.length);
var ctx = canvas.getContext('2d');
ctx.save();
try {
ctx.clearRect(0, 0, canvas.width, canvas.height);
previews.forEach(function(aPreview, aIndex) {
ctx.drawImage(aPreview, 0, 0);
ctx.translate(offset, offset);
ctx.globalAlpha = 1 / (aIndex+1);
}, this);
}
catch(e) {
}
ctx.restore();
return canvas;
},
getTabFromEvent : function TDU_getTabFromEvent(aEvent, aReallyOnTab)
{
var tab = (aEvent.originalTarget || aEvent.target).ownerDocument.evaluate(
'ancestor-or-self::*[local-name()="tab"]',
aEvent.originalTarget || aEvent.target,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (tab || aReallyOnTab)
return tab;
var b = this.getTabBrowserFromChild(aEvent.originalTarget);
if (b &&
'treeStyleTab' in b &&
'getTabFromTabbarEvent' in b.treeStyleTab) { // Tree Style Tab
return b.treeStyleTab.getTabFromTabbarEvent(aEvent);
}
return null;
},
getTabBrowserFromChild : function TDU_getTabBrowserFromChild(aTabBrowserChild)
{
if (!aTabBrowserChild)
return null;
if (aTabBrowserChild.localName == 'tabbrowser') // itself
return aTabBrowserChild;
if (aTabBrowserChild.tabbrowser) // tabs, Firefox 4.0 or later
return aTabBrowserChild.tabbrowser;
if (aTabBrowserChild.localName == 'toolbar') // tabs toolbar, Firefox 4.0 or later
return aTabBrowserChild.getElementsByTagName('tabs')[0].tabbrowser;
var b = aTabBrowserChild.ownerDocument.evaluate(
'ancestor-or-self::*[local-name()="tabbrowser"] | '+
'ancestor-or-self::*[local-name()="tabs" and @tabbrowser] |'+
'ancestor::*[local-name()="toolbar"]/descendant::*[local-name()="tabs" and @tabbrowser]',
aTabBrowserChild,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
return (b && b.tabbrowser) || b;
},
isTabsDragging : function TDU_isTabsDragging(aEvent)
{
if (!aEvent)
return false;
var dt = aEvent.dataTransfer;
if (dt.mozItemCount < 1)
return false;
for (let i = 0, maxi = dt.mozItemCount; i < maxi; i++)
{
if (!dt.mozTypesAt(i).contains(TAB_DROP_TYPE))
return false;
}
return true;
},
getSelectedTabs : function TDU_getSelectedTabs(aEventOrTabOrTabBrowser)
{
var event = aEventOrTabOrTabBrowser instanceof Components.interfaces.nsIDOMEvent ? aEventOrTabOrTabBrowser : null ;
var b = this.getTabBrowserFromChild(event ? event.target : aEventOrTabOrTabBrowser );
if (!b)
return [];
var w = b.ownerDocument.defaultView;
var tab = (aEventOrTabOrTabBrowser instanceof Components.interfaces.nsIDOMElement &&
aEventOrTabOrTabBrowser.localName == 'tab') ?
aEventOrTabOrTabBrowser :
(event && this.getTabFromEvent(event)) ;
var selectedTabs;
var isMultipleDrag = (
( // Firefox 4.x (https://bugzilla.mozilla.org/show_bug.cgi?id=566510)
'visibleTabs' in b &&
(selectedTabs = b.visibleTabs.filter(function(aTab) {
return aTab.getAttribute('multiselected') == 'true';
})) &&
selectedTabs.length
) ||
( // Tab Utilities
'selectedTabs' in b &&
(selectedTabs = b.selectedTabs) &&
selectedTabs.length
) ||
( // Multiple Tab Handler
tab &&
'MultipleTabService' in w &&
w.MultipleTabService.isSelected(tab) &&
w.MultipleTabService.allowMoveMultipleTabs &&
(selectedTabs = w.MultipleTabService.getSelectedTabs(b)) &&
selectedTabs.length
) ||
( // based on HTML5 drag events
this.isTabsDragging(event) &&
(selectedTabs = this.getDraggedTabs(event)) &&
selectedTabs.length
)
);
return isMultipleDrag ? selectedTabs :
tab ? [tab] :
[] ;
},
getDraggedTabs : function TDU_getDraggedTabs(aEventOrDataTransfer)
{
var dt = aEventOrDataTransfer.dataTransfer || aEventOrDataTransfer;
var tabs = [];
if (dt.mozItemCount < 1 ||
!dt.mozTypesAt(0).contains(TAB_DROP_TYPE))
return tabs;
for (let i = 0, maxi = dt.mozItemCount; i < maxi; i++)
{
tabs.push(dt.mozGetDataAt(TAB_DROP_TYPE, i));
}
return tabs.sort(function(aA, aB) { return aA._tPos - aB._tPos; });
},
getCurrentURIOfTab : function TDU_getCurrentURIOfTab(aTab)
{
// Firefox 4.0-
if (aTab.linkedBrowser.__SS_restoreState == 1) {
let data = aTab.linkedBrowser.__SS_data;
let entry = data.entries[Math.min(data.index, data.entries.length-1)];
return entry.url;
}
return aTab.linkedBrowser.currentURI.spec;
},
// for drop on bookmarks tree
willBeInsertedBeforeExistingNode : function TDU_willBeInsertedBeforeExistingNode(aInsertionPoint)
{
// drop on folder in the bookmarks menu
if (aInsertionPoint.dropNearItemId === void(0))
return false;
// drop on folder in the places organizer
if (aInsertionPoint._index < 0 && aInsertionPoint.dropNearItemId < 0)
return false;
return true;
},
handleEvent : function TDU_handleEvent(aEvent)
{
switch (aEvent.type)
{
case 'load':
return this._delayedInit();
}
},
_fireTabsDropEvent : function TDU_fireTabsDropEvent(aTabs)
{
var event = document.createEvent('DataContainerEvents');
event.initEvent(this.EVENT_TYPE_TABS_DROP, true, true);
event.setData('tabs', aTabs);
// for backward compatibility
event.tabs = aTabs;
return this._dropTarget.dispatchEvent(event);
},
get _dropTarget()
{
return ('PlacesControllerDragHelper' in window ?
PlacesControllerDragHelper.currentDropTarge :
null ) || document;
}
};
function DOMDataTransferProxy(aDataTransfer, aInsertionPoint)
{
// Don't proxy it because it is not a drag of tabs.
if (!aDataTransfer.mozTypesAt(0).contains(TAB_DROP_TYPE))
return aDataTransfer;
var tabs = tabsDragUtils.getDraggedTabs(aDataTransfer);
// Don't proxy it because there is no selection.
if (tabs.length < 2)
return aDataTransfer;
this._source = aDataTransfer;
this._tabs = tabs;
if (!tabsDragUtils._fireTabsDropEvent(tabs))
this._tabs = [tabs[0]];
if (tabsDragUtils.willBeInsertedBeforeExistingNode(aInsertionPoint))
this._tabs.reverse();
}
DOMDataTransferProxy.prototype = {
_apply : function DOMDTProxy__apply(aMethod, aArguments)
{
return this._source[aMethod].apply(this._source, aArguments);
},
// nsIDOMDataTransfer
get dropEffect() { return this._source.dropEffect; },
set dropEffect(aValue) { return this._source.dropEffect = aValue; },
get effectAllowed() { return this._source.effectAllowed; },
set effectAllowed(aValue) { return this._source.effectAllowed = aValue; },
get files() { return this._source.files; },
get types() { return this._source.types; },
clearData : function DOMDTProxy_clearData() { return this._apply('clearData', arguments); },
setData : function DOMDTProxy_setData() { return this._apply('setData', arguments); },
getData : function DOMDTProxy_getData() { return this._apply('getData', arguments); },
setDragImage : function DOMDTProxy_setDragImage() { return this._apply('setDragImage', arguments); },
addElement : function DOMDTProxy_addElement() { return this._apply('addElement', arguments); },
// nsIDOMNSDataTransfer
get mozItemCount()
{
return this._tabs.length;
},
get mozCursor() { return this._source.mozCursor; },
set mozCursor(aValue) { return this._source.mozCursor = aValue; },
mozTypesAt : function DOMDTProxy_mozTypesAt(aIndex)
{
if (aIndex >= this._tabs.length)
return new StringList([]);
// return this._apply('mozTypesAt', [0]);
// I return "text/x-moz-url" as a first type, to override behavior for "to-be-restored" tabs.
return new StringList(['text/x-moz-url', TAB_DROP_TYPE, 'text/x-moz-text-internal']);
},
mozClearDataAt : function DOMDTProxy_mozClearDataAt()
{
this._tabs = [];
return this._apply('mozClearDataAt', [0]);
},
mozSetDataAt : function DOMDTProxy_mozSetDataAt(aFormat, aData, aIndex)
{
this._tabs = [];
return this._apply('mozSetDataAt', [aFormat, aData, 0]);
},
mozGetDataAt : function DOMDTProxy_mozGetDataAt(aFormat, aIndex)
{
if (aIndex >= this._tabs.length)
return null;
var tab = this._tabs[aIndex];
switch (aFormat)
{
case TAB_DROP_TYPE:
return tab;
case 'text/x-moz-url':
return (tabsDragUtils.getCurrentURIOfTab(tab) ||
'about:blank') + '\n' + tab.label;
case 'text/x-moz-text-internal':
return tabsDragUtils.getCurrentURIOfTab(tab) ||
'about:blank';
}
return this._apply('mozGetDataAt', [aFormat, 0]);
},
get mozUserCancelled() { return this._source.mozUserCancelled; }
};
function StringList(aTypes)
{
return {
__proto__ : aTypes,
item : function(aIndex)
{
return this[aIndex];
},
contains : function(aType)
{
return this.indexOf(aType) > -1;
}
};
}
tabsDragUtils.DOMDataTransferProxy = DOMDataTransferProxy;
tabsDragUtils.StringList = StringList;
window['piro.sakura.ne.jp'].tabsDragUtils = tabsDragUtils;
tabsDragUtils.init();
})();