addons-server/media/js/zamboni/buttons.js

605 строки
23 KiB
JavaScript

(function() {
/* Call this with something like $('.install').installButton(); */
z.button = {};
/* A library of callbacks that may be run after InstallTrigger succeeds.
* ``this`` will be bound to the .install button.
*/
z.button.after = {'contrib': function(xpi_url, status) {
if (status === 0) { //success
document.location = $(this).attr('data-developers');
}
}};
var notavail = '<div class="extra"><span class="notavail">{0}</span></div>',
incompat = '<div class="extra"><span class="notavail acr-incompat">{0}</span></div>',
noappsupport = '<div class="extra"><span class="notsupported">{0}</span></div>',
download_re = new RegExp('(/downloads/(?:latest|file)/\\d+)');
// The lowest maxVersion an app has to support to allow default-to-compatible.
var D2C_MAX_VERSIONS = {
firefox: '4.0',
mobile: '11.0',
seamonkey: '2.1',
thunderbird: '5.0'
};
var webappButton = function() {
var $this = $(this),
premium = $this.hasClass('premium'),
manifestURL = $this.attr('data-manifest-url');
if (manifestURL) {
$this.find('.button')
.removeClass('disabled')
.addClass('add')
.click(function(e) {
e.preventDefault();
purchases.record($this, function(receipt) {
purchases.install_app(manifestURL, receipt);
});
});
}
if (premium) {
return premiumButton.call($this);
}
};
var premiumButton = function() {
// Pass in the button wrapper and this will check to see if its been
// purchased and alter if appropriate. Will return the purchase state.
var $this = $(this),
addon = $this.attr('data-addon'),
$button = $this.find('.button');
if($.inArray(parseInt(addon, 10), addons_purchased) >= 0) {
purchases.reset($button);
return false;
} else {
$button.addPaypal();
return true;
}
};
/* Called by the jQuery plugin to set up a single button. */
var installButton = function() {
// Create a bunch of data and helper functions, then drive the buttons
// based on the button type at the end.
var self = this,
$this = $(this),
$button = $this.find('.button');
if ($this.hasClass('webapp')) {
webappButton.call(this);
return;
}
// Unreviewed and self-hosted buttons point to the add-on detail page for
// non-js safety. Flip them to the real xpi url here.
$button.each(function() {
var $this = $(this);
if ($this.hasattr('data-realurl')) {
$this.attr('href', $this.attr('data-realurl'));
}
/* If we're on the mobile site but it's not a mobile browser, force
* the download url to type:attachment.
*/
if (z.app === 'mobile' && !z.appMatchesUserAgent) {
var href = $this.attr('href');
$this.attr('href', href.replace(download_re, '$1/type:attachment'));
}
});
var addon = $this.attr('data-addon'),
min = $this.attr('data-min'),
max = $this.attr('data-max'),
name = $this.attr('data-name'),
icon = $this.attr('data-icon'),
after = $this.attr('data-after'),
search = $this.hasattr('data-search'),
premium = $this.hasClass('premium'),
accept_eula = $this.hasClass('accept'),
webapp = $this.hasattr('data-manifest-url'),
compatible = $this.attr('data-is-compatible') == 'true',
compatible_app = $this.attr('data-is-compatible-app') == 'true',
waffle_d2c_buttons = $this.hasattr('data-waffle-d2c-buttons'),
has_overrides = $this.hasattr('data-compat-overrides'),
versions_url = $this.attr('data-versions'),
// L10n: {0} is an app name like Firefox.
_s = accept_eula ? gettext('Accept and Install') : gettext('Add to {0}'),
addto = format(_s, [z.appName]),
// params is used for popup variable interpolation.
// The `url` key is set after we've messed with the buttons.
params = {addon: addon,
msg: z.appMatchesUserAgent ? addto : gettext('Download Now')},
appSupported = z.appMatchesUserAgent && min && max,
$body = $(document.body),
$d2c_reasons = $this.closest('.install-shell').find('.d2c-reasons-popup ul'),
olderBrowser,
newerBrowser;
// If we have os-specific buttons, check that one of them matches the
// current platform.
var badPlatform = ($button.find('.os').length &&
!$button.hasClass(z.platform));
// Only show default-to-compatible reasons if the add-on has the minimum
// required maxVersion to support it.
var is_d2c = false;
if (max) {
if (z.browser.firefox && VersionCompare.compareVersions(max, D2C_MAX_VERSIONS.firefox) >= 0) {
is_d2c = true;
} else if (z.browser.mobile && VersionCompare.compareVersions(max, D2C_MAX_VERSIONS.mobile) >= 0) {
is_d2c = true;
} else if (z.browser.seamonkey && VersionCompare.compareVersions(max, D2C_MAX_VERSIONS.seamonkey) >= 0) {
is_d2c = true;
} else if (z.browser.thunderbird && VersionCompare.compareVersions(max, D2C_MAX_VERSIONS.thunderbird) >= 0) {
is_d2c = true;
}
}
// min and max only exist if the add-on is compatible with request[APP].
if (appSupported) {
// The user *has* an older/newer browser.
olderBrowser = VersionCompare.compareVersions(z.browserVersion, min) < 0;
newerBrowser = VersionCompare.compareVersions(z.browserVersion, max) > 0;
if (olderBrowser) {
// Make sure we show the "Not available for ..." messaging.
compatible = false;
}
}
// Default to compatible checking.
if (waffle_d2c_buttons && is_d2c && compatible) {
if (!compatible_app) {
$d2c_reasons.append($('<li>', {text: gettext('Add-on has not been updated to support default-to-compatible.')}));
compatible = false;
}
// TODO: Figure out if this needs to handle other apps.
if (z.browserVersion != 0 && VersionCompare.compareVersions(z.browserVersion, '10.0') < 0) {
$d2c_reasons.append($('<li>', {text: gettext('You need to be using Firefox 10.0 or higher.')}));
compatible = false;
}
// If it's still compatible, check the overrides.
if (compatible && has_overrides) {
var overrides = JSON.parse($this.attr('data-compat-overrides'));
_.each(overrides, function(override) {
var _min = override[0],
_max = override[1];
if (VersionCompare.compareVersions(z.browserVersion, _min) >= 0 ||
VersionCompare.compareVersions(z.browserVersion, _max) <= 0) {
compatible = false;
$d2c_reasons.append($('<li>', {text: gettext('Mozilla has marked this version as incompatible with your Firefox version.')}));
return;
}
});
}
} else {
compatible = false; // We always assumed not compatible before.
}
// Helper for dealing with lazy-loaded z.button.messages.
var message = function(msg) {
return function(){
// Get the xpi link for the first visible button.
params.url = escape_($button.filter(':visible').attr('href'));
return format(z.button.messages[msg], params);
}
};
var addWarning = function(msg, type) {
$this.parent().append(format(type || notavail, [msg]));
};
// Change the button text to "Add to Firefox".
var addToApp = function() {
if (appSupported || (search && z.appMatchesUserAgent)) {
$button.addClass('add').removeClass('download')
.find('span').text(addto);
}
};
// Calls InstallTrigger.install or AddSearchProvider if we capture a click
// on something with a .installer class.
var clickHijack = function() {
if (!appSupported && !search || !("InstallTrigger" in window)) return;
$this.addClass('clickHijack'); // So we can disable pointer events
$this.bind('mousedown focus', function(e) {
$this.addClass('active');
}).bind('mouseup blur', function(e) {
$this.removeClass('active');
}).click(function(e) {
// If the click was on a.installer or a child, call the special
// install method. We can't bind this directly because we add
// more .installers dynamically.
var $target = $(e.target);
if ($target.hasClass('installer')) {
var installer = $target;
} else {
var installer = $target.parents('.installer').first();
if (_.indexOf($this.find('.installer'), installer[0]) == -1) {
return;
}
}
e.preventDefault();
// map download url => file hash.
var hashes = {};
$this.find('.button[data-hash]').each(function() {
hashes[$(this).attr('href')] = $(this).attr('data-hash');
});
// For premium add-ons this will be undefined.
var hash = hashes[installer.attr('href')];
var f = _.haskey(z.button.after, after)
? z.button.after[after] : _.identity,
callback = _.bind(f, self),
install = search ? z.installSearch : z.installAddon;
install(name, installer[0].href, icon, hash, callback);
});
};
// Gather the available platforms.
var platforms = $button.map(function() {
var name = $(this).find('.os').attr('data-os'),
text = z.appMatchesUserAgent ?
/* L10n: {0} is an platform like Windows or Linux. */
gettext('Install for {0} anyway') : gettext('Download for {0} anyway');
return {
href: $(this).attr('href'),
msg: format(text, [name])
};
});
// Add version and platform warnings and (optionally) popups. This is one
// big function since we merge the messaging when bad platform and version
// occur simultaneously. Returns true if a popup was added.
var versionsAndPlatforms = function(options) {
var opts = $.extend({addPopup: true, addWarning: true, extra: ''},
options);
warn = opts.addWarning ? addWarning : _.identity;
var addExtra = function(f) {
/* Decorator to add extra content to a message. */
return function() {
var extra = $.isFunction(opts.extra) ? opts.extra()
: opts.extra;
return $(f.apply(this, arguments)).append(extra);
};
};
// Popup message helpers.
var pmsg = addExtra(function() {
var links = $.map(platforms, function(o) {
return format(z.button.messages['platform_link'], o);
});
return format(z.button.messages['bad_platform'],
{platforms: links.join('')});
});
var vmsg = addExtra(function() {
params['new_version'] = max;
params['old_version'] = z.browserVersion;
return message(newerBrowser ? 'not_updated' : 'newer_version')();
});
var nocompat = addExtra(function() {
return message('not_compatible')();
});
var nocompat_noreason = addExtra(function() {
return message('not_compatible_no_reasons')();
});
var merge = addExtra(function() {
// Prepend the platform message to the version message. We only
// want to move the installer when we're looking at an older
// version of the add-on.
var p = $(pmsg()), v = $(vmsg());
v.prepend(p.find('.msg').clone());
if (this.switchInstaller) {
v.find('.installer').parent().html(p.find('ul').clone());
}
return v;
});
// Do badPlatform prep out here since we need it in all branches.
if (badPlatform) {
warn(gettext('Not available for your platform'));
$button.addClass('concealed');
$button.first().css('display', 'inherit');
}
if (appSupported && !compatible && (olderBrowser || newerBrowser)) {
if (waffle_d2c_buttons && is_d2c) {
// If it's a bad platform, don't bother also showing the
// incompatible reasons.
if (!badPlatform) {
// L10n: {0} is an app name, {1} is the app version.
var warn_txt = gettext('Not available for {0} {1}');
if ($d2c_reasons.children().length) {
warn_txt += '<span><a class="d2c-reasons-help" href="#">?</a></span>';
}
warn(format(warn_txt, [z.appName, z.browserVersion]));
}
$button.closest('div').attr('data-version-supported', false);
$button.addClass('concealed');
var $ishell = $button.closest('.install-shell');
if (!compatible && $d2c_reasons.children().length) {
$ishell.find('.d2c-reasons-popup').popup(
$ishell.find('.d2c-reasons-help'), {
callback: function(obj) {
return {pointTo: $(obj.click_target)};
}
}
);
}
if (!opts.addPopup) return;
if (badPlatform) {
$button.addPopup(pmsg);
} else if (!compatible) {
// Show compatibility message.
params['versions_url'] = versions_url;
params['reasons'] = $d2c_reasons.html();
$button.addPopup(params['reasons'] ? nocompat : nocompat_noreason);
} else {
// Bad version.
$button.addPopup(vmsg);
}
return true;
} else {
// L10n: {0} is an app name, {1} is the app version.
warn(format(gettext('Not available for {0} {1}'),
[z.appName, z.browserVersion]));
$button.closest('div').attr('data-version-supported', false);
$button.addClass('concealed');
if (!opts.addPopup) return;
if (badPlatform && olderBrowser) {
$button.addPopup(merge);
} else if (badPlatform && newerBrowser) {
$button.addPopup(_.bind(merge, {switchInstaller: true}));
} else {
// Bad version.
$button.addPopup(vmsg);
}
return true;
}
} else if (badPlatform && opts.addPopup) {
// Only bad platform is possible.
$button.addPopup(pmsg);
$button.closest('div').attr('data-version-supported', true);
return true;
} else if (!unreviewed && (appSupported || search)) {
// Good version, good platform.
$button.addClass('installer');
$button.closest('div').attr('data-version-supported', true);
} else if (!appSupported) {
var tpl = template(gettext('Works with {app} {min} - {max}') +
'<span class="more-versions"><a href="{versions_url}">' +
gettext('View other versions') + '</a></span>');
var context = {'app': z.appName, 'min': min, 'max': max,
'versions_url': versions_url};
addWarning(tpl(context), noappsupport);
}
return false;
};
// What kind of button are we dealing with?
var selfhosted = $this.hasClass('selfhosted'),
beta = $this.hasClass('beta');
unreviewed = $this.hasClass('unreviewed') && !beta,
persona = $this.hasClass('persona'),
contrib = $this.hasClass('contrib'),
search = $this.hasattr('data-search'),
eula = $this.hasClass('eula');
if (unreviewed && !(selfhosted || eula || contrib || beta || webapp)) {
$button.addPopup(message('unreviewed'));
}
// Drive the install button based on its type.
if (selfhosted) {
$button.addPopup(message('selfhosted'));
} else if (eula || contrib) {
versionsAndPlatforms({addPopup: false});
} else if (premium) {
premiumButton.call($this);
versionsAndPlatforms({addPopup: false});
} else if (persona) {
$button.removeClass('download').addClass('add').find('span').text(addto);
if ($.hasPersonas()) {
$button.personasButton();
} else {
$button.addClass('concealed');
if (z.appMatchesUserAgent) {
// Need upgrade
params['old_version'] = z.browserVersion;
$button.addPopup(message('personas_need_upgrade'));
} else {
$button.addPopup(message('learn_more_personas'));
}
}
} else if (z.appMatchesUserAgent) {
clickHijack();
addToApp();
var opts = search ? {addPopup: false, addWarning: false} : {};
versionsAndPlatforms(opts);
} else if (z.app == 'firefox') {
$button.addPopup(message('learn_more')).addClass('concealed');
versionsAndPlatforms({addPopup: false});
} else if (z.app == 'thunderbird') {
var msg = function() {
return $(message('learn_more')()).html();
};
if (!versionsAndPlatforms({extra: msg})) {
$button.addPopup(message('learn_more'), true);
}
} else {
clickHijack();
addToApp();
versionsAndPlatforms();
}
};
var data_purchases = $('body').attr('data-purchases') || "",
addons_purchased = $.map(data_purchases.split(','),
function(v) { return parseInt(v, 10) });
jQuery.fn.installButton = function() {
return this.each(installButton);
};
jQuery.fn.showBackupButton = function() {
this.each(function() {
var $src, $dest,
$this = $(this),
$current = $this.parent().find('.install'),
attr = 'data-version-supported';
if ($this.find('.install').attr(attr) == 'true' &&
$current.attr(attr) == 'false') {
$current.closest('.install-shell').first().addClass('hidden');
$this.removeClass('hidden').show();
// Alter other elements of the page, if they exist.
$dest = $('#addon-summary table');
if ($dest.exists()) {
$src = $this.find('div.install');
$dest.find('.addon-compatible td')
.text($src.attr('data-compatible-apps'));
$dest.find('.addon-updated time')
.attr('datetime', $src.attr('data-lastupdated-isotime'))
.text($src.attr('data-lastupdated-datetime'));
$('h2.addon span.version').text($src.attr('data-version'));
}
}
});
}
jQuery.fn.addPaypal = function(html, allowClick) {
function checkForAddon(el) {
var $this = $(el);
// Focus on the username field if it exists.
$('#id_username', $this).focus();
if ($('#addon_info').exists()) {
purchases.reset(purchases.find_button($this.closest('body')), $this);
purchases.trigger($this);
}
}
return this.click(_pd(function() {
var $install = $(this).closest('.install'),
url = $install.attr('data-start-purchase');
if (url) {
modalFromURL(url, {'callback': function() {
var $modal = $(this);
checkForAddon(this);
$('.browserid-login', this).bind('login-complete', function(){
$(this).addClass('loading-submit');
$('.ajax-submit', $modal).load(url, function() {
checkForAddon(this);
});
});
}, 'data': {'realurl': $install.find('a.premium').attr('data-realurl')}});
};
}));
}
// Create a popup box when the element is clicked. html can be a function.
jQuery.fn.addPopup = function(html, allowClick) {
return this.each(function() {
var $this = $(this),
self = this,
$body = $(document.body);
if (this.hasPopup) {
// We've been here before, queue a follow-up button.
$this.bind('newPopup', function(e, popup) {
$this.unbind('newPopup');
$(popup).find('.installer').click(function(e) {
$this.unbind('click'); // Drop the current popup.
self.hasPopup = false;
var next = self.popupQueue.pop();
if (!next[1]) { // allowClick
e.preventDefault();
e.stopPropagation();
}
jQuery.fn.addPopup.apply($this, next);
$this.click();
});
});
self.popupQueue = self.popupQueue || [];
self.popupQueue.push([html, allowClick]);
} else {
this.hasPopup = true;
$this.click(function(e) {
var _html = $($.isFunction(html) ? html() : html),
popup_root = _html.get(0);
if (!$this.filter(':visible').length) { return; }
if (!allowClick) { e.preventDefault(); }
if ($this.offset().left > $(window).width() / 2) {
_html.addClass('left');
}
$this.trigger('newPopup', [_html]);
$this.after(_html);
$body.trigger('newStatic');
// Callback to destroy the popup on the first click outside the popup.
var cb = function(e) {
// Bail if the click was somewhere on the popup.
if (e.type == 'click' &&
popup_root == e.target ||
_.indexOf($(e.target).parents(), popup_root) != -1) {
return;
}
_html.remove();
$body.unbind('click newPopup', cb);
$body.trigger('closeStatic');
}
// Trampoline the binding so it isn't triggered by the current click.
setTimeout(function(){ $body.bind('click newPopup', cb); }, 0);
});
}
});
}
/* Install an XPI or a JAR (or something like that).
*
* hash and callback are optional. callback is triggered after the
* installation is complete.
*/
z.installAddon = function(name, url, icon, hash, callback) {
var params = {};
params[name] = {
URL: url,
IconURL: icon,
toString: function() { return url; }
};
if (hash) {
params[name]['Hash'] = hash;
}
// InstallTrigger is a Gecko API.
InstallTrigger.install(params, callback);
};
z.installSearch = function(name, url, icon, hash, callback) {
if (window.external && window.external.AddSearchProvider) {
window.external.AddSearchProvider(url);
callback();
} else {
// Alert! Deal with it.
alert(gettext('Sorry, you need a Mozilla-based browser (such as Firefox) to install a search plugin.'));
}
};
})();