2012-01-17 23:56:50 -05:00
|
|
|
/**
|
|
|
|
* @fileOverview Popup Notification (Door Hanger) Based Confirmation Library for Firefox 4.0 or later
|
|
|
|
* @author SHIMODA "Piro" Hiroshi
|
2012-01-20 09:58:22 -05:00
|
|
|
* @version 3
|
2012-01-17 23:56:50 -05:00
|
|
|
* Basic usage:
|
|
|
|
*
|
|
|
|
* @example
|
2012-01-20 09:58:22 -05:00
|
|
|
* Usage:
|
|
|
|
* Components.utils.import('resource://my-modules/confirmWithPopup.js');
|
2012-01-17 23:56:50 -05:00
|
|
|
*
|
2012-01-20 09:58:22 -05:00
|
|
|
* confirmWithPopup({
|
|
|
|
* browser : gBrowser.selectedBrowser, // the related browser
|
|
|
|
* label : 'Ara you ready?', // the message
|
|
|
|
* value : 'treestyletab-undo-close-tree', // the internal key (optional)
|
|
|
|
* anchor : '....', // the ID of the anchor element (optional)
|
|
|
|
* image : 'chrome://....png', // the icon (optional)
|
|
|
|
* buttons : ['Yes', 'Yes forever', 'No forever], // button labels
|
|
|
|
* // Native options for PopupNotifications.show() :
|
|
|
|
* // persistence (integer)
|
|
|
|
* // timeout (integer)
|
|
|
|
* // persistWhileVisible (boolean)
|
|
|
|
* // dismissed (boolean)
|
|
|
|
* // eventCallback (function)
|
|
|
|
* // neverShow (boolean)
|
|
|
|
* // popupIconURL (string) : will be used instead of "image" option.
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* JSDeferred style:
|
|
|
|
* confirmWithPopup(...)
|
|
|
|
* .next(function(aButtonIndex) {
|
|
|
|
* // the next callback receives the index of the clicked button.
|
|
|
|
* switch (aButtonIndex) {
|
|
|
|
* case 0: return YesAction();
|
|
|
|
* case 1: return YesForeverAction();
|
|
|
|
* case 2: return NoForeverAction();
|
|
|
|
* }
|
|
|
|
* })
|
|
|
|
* .error(function(aError) {
|
|
|
|
* // dismissed or removed (not called if any button is chosen)
|
|
|
|
* ...
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* without JSDeferred:
|
|
|
|
* confirmWithPopup({ ...,
|
|
|
|
* // Yes, Yes Forever, or No Forever
|
|
|
|
* callback : function(aButtonIndex) { ... },
|
|
|
|
* // dismissed or removed (not called if any button is chosen)
|
|
|
|
* onerror : function(aError) { ... }
|
|
|
|
* });
|
2012-01-17 23:56:50 -05:00
|
|
|
*
|
|
|
|
* @license
|
2012-01-18 12:56:55 -05:00
|
|
|
* The MIT License, Copyright (c) 2011-2012 SHIMODA "Piro" Hiroshi
|
2012-01-17 23:56:50 -05:00
|
|
|
* http://github.com/piroor/fxaddonlibs/blob/master/license.txt
|
|
|
|
* @url http://github.com/piroor/fxaddonlibs/blob/master/confirmWithPopup.js
|
|
|
|
* @url http://github.com/piroor/fxaddonlibs
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (typeof window == 'undefined')
|
|
|
|
this.EXPORTED_SYMBOLS = ['confirmWithPopup'];
|
|
|
|
|
|
|
|
// var namespace;
|
|
|
|
if (typeof namespace == 'undefined') {
|
|
|
|
// If namespace.jsm is available, export symbols to the shared namespace.
|
|
|
|
// See: http://github.com/piroor/fxaddonlibs/blob/master/namespace.jsm
|
|
|
|
try {
|
|
|
|
let ns = {};
|
|
|
|
Components.utils.import('resource://treestyletab-modules/lib/namespace.jsm', ns);
|
|
|
|
namespace = ns.getNamespaceFor('piro.sakura.ne.jp');
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
namespace = (typeof window != 'undefined' ? window : null ) || {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
// This can be extended by JSDeferred.
|
2012-01-17 23:56:50 -05:00
|
|
|
// See: https://github.com/cho45/jsdeferred
|
2012-01-20 10:13:27 -05:00
|
|
|
try {
|
|
|
|
if (typeof namespace.Deferred == 'undefined')
|
|
|
|
Components.utils.import('resource://treestyletab-modules/lib/jsdeferred.js', namespace);
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
}
|
2012-01-17 23:56:50 -05:00
|
|
|
|
|
|
|
var available = false;
|
|
|
|
try {
|
|
|
|
Components.utils.import('resource://gre/modules/PopupNotifications.jsm');
|
|
|
|
available = true;
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
}
|
|
|
|
|
|
|
|
var confirmWithPopup;
|
|
|
|
(function() {
|
2012-01-20 09:58:22 -05:00
|
|
|
const currentRevision = 3;
|
2012-01-17 23:56:50 -05:00
|
|
|
|
|
|
|
var loadedRevision = 'confirmWithPopup' in namespace ?
|
|
|
|
namespace.confirmWithPopup.revision :
|
|
|
|
0 ;
|
|
|
|
if (loadedRevision && loadedRevision > currentRevision) {
|
|
|
|
confirmWithPopup = namespace.confirmWithPopup;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!available)
|
|
|
|
return confirmWithPopup = undefined;
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
const Cc = Components.classes;
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
|
2012-01-17 23:56:50 -05:00
|
|
|
const DEFAULT_ANCHOR_ICON = 'default-notification-icon';
|
2012-01-20 09:58:22 -05:00
|
|
|
const NATIVE_OPTIONS = 'persistence,timeout,persistWhileVisible,dismissed,eventCallback,neverShow,popupIconURL'.split(',');
|
|
|
|
const OPTIONS = 'browser,tab,label,value,anchor,image,imageWidth,imageHeight,button,buttons,callback,onerror'.split(',');
|
2012-01-17 23:56:50 -05:00
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
function next(aCallback) {
|
|
|
|
Cc['@mozilla.org/timer;1']
|
|
|
|
.createInstance(Ci.nsITimer)
|
|
|
|
.init(aCallback, 0, Ci.nsITimer.TYPE_ONE_SHOT);
|
2012-01-17 23:56:50 -05:00
|
|
|
}
|
|
|
|
|
2012-01-18 12:56:55 -05:00
|
|
|
// We have to use a custom stylesheet to show the anchor element.
|
2012-01-17 23:56:50 -05:00
|
|
|
function addStyleSheet(aDocument, aOptions) {
|
|
|
|
var uri = 'data:text/css,'+encodeURIComponent(
|
2012-01-20 09:58:22 -05:00
|
|
|
(aOptions.image ?
|
|
|
|
'.popup-notification-icon[popupid="'+aOptions.id+'"] {'+
|
|
|
|
' list-style-image: url("'+aOptions.image+'");'+
|
2012-01-20 10:33:19 -05:00
|
|
|
(aOptions.imageWidth ? 'width: '+aOptions.imageWidth+'px;' : '' )+
|
|
|
|
(aOptions.imageHeight ? 'height: '+aOptions.imageHeight+'px;' : '' )+
|
2012-01-20 09:58:22 -05:00
|
|
|
'}' :
|
|
|
|
''
|
|
|
|
)+
|
2012-01-17 23:56:50 -05:00
|
|
|
'#notification-popup-box[anchorid="'+aOptions.anchor+'"] > #'+aOptions.anchor+' {'+
|
|
|
|
' display: -moz-box;'+
|
|
|
|
'}'
|
|
|
|
);
|
|
|
|
|
|
|
|
var styleSheets = aDocument.styleSheets;
|
|
|
|
for (var i = 0, maxi = styleSheets.length; i < maxi; i++) {
|
|
|
|
if (styleSheets[i].href == uri)
|
|
|
|
return styleSheets[i].ownerNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
var style = aDocument.createProcessingInstruction('xml-stylesheet',
|
|
|
|
'type="text/css" href="'+uri+'"'
|
|
|
|
);
|
|
|
|
aDocument.insertBefore(style, aDocument.documentElement);
|
|
|
|
return style;
|
|
|
|
}
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
function normalizeOptions(aOptions) {
|
|
|
|
var options = collectNativeOptions(aOptions);
|
|
|
|
OPTIONS.forEach(function(aOption) {
|
|
|
|
options[aOption] = aOptions[aOption] ;
|
|
|
|
});
|
|
|
|
|
|
|
|
options.image = options.popupIconURL || options.image ;
|
|
|
|
|
|
|
|
// we should accept single button type popup
|
|
|
|
if (!options.buttons && options.button)
|
|
|
|
options.buttons = [options.button];
|
|
|
|
|
|
|
|
if (!options.browser && options.tab)
|
|
|
|
options.browser = options.tab.linkedBrowser;
|
|
|
|
|
|
|
|
if (!options.anchor)
|
|
|
|
options.anchor = DEFAULT_ANCHOR_ICON;
|
|
|
|
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
function collectNativeOptions(aOptions) {
|
|
|
|
var options = {};
|
|
|
|
NATIVE_OPTIONS.forEach(function(aOption) {
|
|
|
|
options[aOption] = aOptions.option && aOptions.option[aOption] || aOptions[aOption] ;
|
|
|
|
});
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
2012-01-17 23:56:50 -05:00
|
|
|
confirmWithPopup = function confirmWithPopup(aOptions)
|
|
|
|
{
|
2012-01-20 09:58:22 -05:00
|
|
|
var options = normalizeOptions(aOptions);
|
|
|
|
var nativeOptions = collectNativeOptions(options);
|
2012-01-18 13:23:06 -05:00
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
var deferred = namespace.Deferred ?
|
|
|
|
new namespace.Deferred() :
|
|
|
|
{ // fake deferred
|
|
|
|
next : next,
|
|
|
|
call : function(aValue) { options.callback && options.callback.call(aOptions, aValue); },
|
|
|
|
fail : function(aError) { options.onerror && options.onerror.call(aOptions, aError); }
|
|
|
|
};
|
2012-01-18 13:23:06 -05:00
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
if (!options.buttons) {
|
2012-01-17 23:56:50 -05:00
|
|
|
return deferred
|
|
|
|
.next(function() {
|
2012-01-18 13:23:06 -05:00
|
|
|
throw new Error('confirmWithPopup requires any button!');
|
2012-01-17 23:56:50 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
if (!options.browser)
|
2012-01-17 23:56:50 -05:00
|
|
|
return deferred
|
|
|
|
.next(function() {
|
|
|
|
throw new Error('confirmWithPopup requires a <xul:browser/>!');
|
|
|
|
});
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
var doc = options.browser.ownerDocument;
|
2012-01-17 23:56:50 -05:00
|
|
|
var style;
|
2012-01-20 09:58:22 -05:00
|
|
|
var done = false;
|
2012-01-20 10:10:57 -05:00
|
|
|
var postProcess = function() {
|
|
|
|
if (doc && style) {
|
|
|
|
doc.removeChild(style);
|
|
|
|
style = null;
|
|
|
|
}
|
2012-01-20 09:58:22 -05:00
|
|
|
};
|
2012-01-17 23:56:50 -05:00
|
|
|
var showPopup = function() {
|
|
|
|
var accessKeys = [];
|
|
|
|
var numericAccessKey = 0;
|
2012-01-20 09:58:22 -05:00
|
|
|
var buttons = options.buttons.map(function(aLabel, aIndex) {
|
2012-01-17 23:56:50 -05:00
|
|
|
var accessKey;
|
|
|
|
var match = aLabel.match(/\s*\(&([0-9a-z])\)/i);
|
|
|
|
if (match) {
|
|
|
|
accessKey = match[1];
|
|
|
|
aLabel = aLabel.replace(match[0], '');
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let lastUniqueKey;
|
|
|
|
let sanitizedLabel = [];
|
|
|
|
for (let i = 0, maxi = aLabel.length; i < maxi; i++)
|
|
|
|
{
|
|
|
|
let possibleAccessKey = aLabel.charAt(i);
|
|
|
|
if (possibleAccessKey == '&' && i < maxi-1) {
|
|
|
|
possibleAccessKey = aLabel.charAt(i+1);
|
|
|
|
if (possibleAccessKey != '&') {
|
|
|
|
accessKey = possibleAccessKey;
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
else if (!lastUniqueKey &&
|
|
|
|
accessKeys.indexOf(possibleAccessKey) < 0) {
|
|
|
|
lastUniqueKey = possibleAccessKey;
|
|
|
|
}
|
|
|
|
sanitizedLabel.push(possibleAccessKey);
|
|
|
|
}
|
|
|
|
if (!accessKey)
|
|
|
|
accessKey = lastUniqueKey;
|
|
|
|
if (!accessKey || !/[0-9a-z]/i.test(accessKey))
|
|
|
|
accessKey = ++numericAccessKey;
|
|
|
|
|
|
|
|
aLabel = sanitizedLabel.join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
accessKeys.push(accessKey);
|
|
|
|
|
|
|
|
return {
|
|
|
|
label : aLabel,
|
|
|
|
accessKey : accessKey,
|
|
|
|
callback : function() {
|
2012-01-20 09:58:22 -05:00
|
|
|
done = true;
|
2012-01-20 10:10:57 -05:00
|
|
|
try {
|
|
|
|
deferred.call(aIndex);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
postProcess();
|
|
|
|
}
|
2012-01-17 23:56:50 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
var primaryAction = buttons[0];
|
|
|
|
var secondaryActions = buttons.length > 1 ? buttons.slice(1) : null ;
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
options.id = options.value || 'confirmWithPopup-'+encodeURIComponent(options.label);
|
|
|
|
|
|
|
|
style = addStyleSheet(doc, options);
|
2012-01-17 23:56:50 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
/**
|
|
|
|
* 1st try: Prepare the anchor icon. If the icon isn't shown,
|
|
|
|
* the popup is wrongly positioned to the current tab
|
|
|
|
* by PopupNotifications.show().
|
|
|
|
*/
|
|
|
|
doc.defaultView.PopupNotifications.show(
|
2012-01-20 09:58:22 -05:00
|
|
|
options.browser,
|
|
|
|
options.id,
|
|
|
|
options.label,
|
|
|
|
options.anchor,
|
2012-01-17 23:56:50 -05:00
|
|
|
primaryAction,
|
|
|
|
secondaryActions,
|
|
|
|
{
|
2012-01-20 09:58:22 -05:00
|
|
|
__proto__ : nativeOptions,
|
2012-01-17 23:56:50 -05:00
|
|
|
dismissed : true
|
|
|
|
}
|
|
|
|
);
|
2012-01-20 09:58:22 -05:00
|
|
|
if (!options.dismissed) {
|
2012-01-17 23:56:50 -05:00
|
|
|
/**
|
|
|
|
* 2nd try: Show the popup.
|
|
|
|
*/
|
2012-01-20 09:58:22 -05:00
|
|
|
let secondTry = function() {
|
2012-01-17 23:56:50 -05:00
|
|
|
doc.defaultView.PopupNotifications.show(
|
2012-01-20 09:58:22 -05:00
|
|
|
options.browser,
|
|
|
|
options.id,
|
|
|
|
options.label,
|
|
|
|
options.anchor,
|
2012-01-17 23:56:50 -05:00
|
|
|
primaryAction,
|
|
|
|
secondaryActions,
|
|
|
|
{
|
2012-01-20 09:58:22 -05:00
|
|
|
__proto__ : nativeOptions,
|
2012-01-17 23:56:50 -05:00
|
|
|
eventCallback : function(aEventType) {
|
2012-01-20 10:10:57 -05:00
|
|
|
try {
|
|
|
|
if (!done && (aEventType == 'removed' || aEventType == 'dismissed'))
|
|
|
|
deferred.fail(aEventType);
|
|
|
|
if (options.eventCallback)
|
|
|
|
options.eventCallback.call(aOptions.options || aOptions, aEventType);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
if (aEventType == 'removed')
|
|
|
|
postProcess();
|
|
|
|
}
|
2012-01-17 23:56:50 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2012-01-20 09:58:22 -05:00
|
|
|
};
|
|
|
|
if (namespace.Deferred)
|
|
|
|
namespace.Deferred.next(secondTry);
|
|
|
|
else
|
|
|
|
next(secondTry);
|
2012-01-17 23:56:50 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
deferred.fail(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
if (options.image && (!options.imageWidth || !options.imageHeight)) {
|
|
|
|
let loader = new doc.defaultView.Image();
|
|
|
|
loader.src = options.image;
|
|
|
|
loader.onload = function() {
|
|
|
|
options.imageWidth = loader.width;
|
|
|
|
options.imageHeight = loader.height;
|
|
|
|
showPopup();
|
|
|
|
};
|
|
|
|
loader.onerror = function() {
|
|
|
|
showPopup();
|
|
|
|
};
|
2012-01-17 23:56:50 -05:00
|
|
|
}
|
2012-01-20 09:58:22 -05:00
|
|
|
else if (namespace.Deferred) {
|
2012-01-17 23:56:50 -05:00
|
|
|
namespace.Deferred.next(showPopup);
|
|
|
|
}
|
2012-01-20 09:58:22 -05:00
|
|
|
else {
|
|
|
|
next(showPopup);
|
|
|
|
}
|
2012-01-17 23:56:50 -05:00
|
|
|
|
2012-01-20 09:58:22 -05:00
|
|
|
if (namespace.Deferred) {
|
2012-01-20 10:10:57 -05:00
|
|
|
return deferred;
|
2012-01-20 09:58:22 -05:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
let originalCall = deferred.call;
|
|
|
|
deferred.call = function(aButtonIndex) {
|
2012-01-20 10:10:57 -05:00
|
|
|
try {
|
|
|
|
originalCall(aButtonIndex);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
postProcess();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let originalFail = deferred.fail;
|
|
|
|
deferred.fail = function(aError) {
|
|
|
|
try {
|
|
|
|
originalFail(aError);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
postProcess();
|
|
|
|
}
|
2012-01-20 09:58:22 -05:00
|
|
|
};
|
|
|
|
}
|
2012-01-17 23:56:50 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
namespace.confirmWithPopup = confirmWithPopup;
|
|
|
|
})();
|