Customizing Firefox browser button “behavior”
Here’s the example when it is necessary to customize the browser button behaviour — the standard Firefox functional doesn’t provide an opportunity to process separately the left and right button click of a mouse.
There are a few ways to create Extension with a browser button, the most simple ones are to use Firefox SDK и Web Extension. But only Firefox SDK Extension has a possibility to customize.
There are a few ways to create Extension with a browser button, the most simple ones are to use Firefox SDK and Web Extension. But only Firefox SDK Extension has a possibility to customize.
In order to do this it is necessary to realize your Browser Button class.
// custom-button.js
module.metadata = {
'stability': 'experimental',
'engines': {
'Firefox': '> 28'
}
};
const { Class } = require('sdk/core/heritage');
const { merge } = require('sdk/util/object');
const { Disposable } = require('sdk/core/disposable');
const { on, off, emit, setListeners } = require('sdk/event/core');
const { EventTarget } = require('sdk/event/target');
const { getNodeView } = require('sdk/view/core');
const view = require('./view');
const { toggleButtonContract, toggleStateContract } = require('sdk/ui/button/contract');
const { properties, render, state, register, unregister,
setStateFor, getStateFor, getDerivedStateFor } = require('sdk/ui/state');
const { events: stateEvents } = require('sdk/ui/state/events');
const { events: viewEvents } = require('sdk/ui/button/view/events');
const events = require('sdk/event/utils');
const { getActiveTab } = require('sdk/tabs/utils');
const { id: addonID } = require('sdk/self');
const { identify } = require('sdk/ui/id');
const buttons = new Map();
const toWidgetId = id => ('toggle-button--' + addonID.toLowerCase()+ '-' + id).replace(/[^a-z0-9_-]/g, '');
// Define custom ToggleButton Class.
const ToggleButton = Class({
extends: EventTarget,
implements: [
properties(toggleStateContract),
state(toggleStateContract),
Disposable
],
setup: function setup(options) {
let state = merge({
disabled: false,
checked: false
}, toggleButtonContract(options));
let id = toWidgetId(options.id);
register(this, state);
// Setup listeners.
setListeners(this, options);
buttons.set(id, this);
view.create(merge({ }, state, { id: id }));
},
dispose: function dispose() {
let id = toWidgetId(this.id);
buttons.delete(id);
off(this);
view.dispose(id);
unregister(this);
},
get id() this.state().id,
click: function click() view.click(toWidgetId(this.id))
});
exports.CustomButton = ToggleButton;
identify.define(ToggleButton, ({id}) => toWidgetId(id));
getNodeView.define(ToggleButton, button => view.nodeFor(toWidgetId(button.id)));
let toggleButtonStateEvents = events.filter(stateEvents, e => e.target instanceof ToggleButton);
let toggleButtonViewEvents = events.filter(viewEvents, e => buttons.has(e.target));
let clickEvents = events.filter(toggleButtonViewEvents, e => e.type === 'click');
let updateEvents = events.filter(toggleButtonViewEvents, e => e.type === 'update');
on(toggleButtonStateEvents, 'data', ({target, window, state}) => {
let id = toWidgetId(target.id);
view.setIcon(id, window, state.icon);
view.setLabel(id, window, state.label);
view.setDisabled(id, window, state.disabled);
view.setChecked(id, window, state.checked);
});
on(clickEvents, 'data', ({target: id, window, checked, typeClick }) => {
let button = buttons.get(id);
let windowState = getStateFor(button, window);
let newWindowState = merge({}, windowState, { checked: checked });
setStateFor(button, window, newWindowState);
let state = getDerivedStateFor(button, getActiveTab(window));
console.log('typeClick', typeClick);
emit(button, 'click', state, typeClick);
});
How to use custom button:
const { CustomButton } = require('./custom-button');
// Init toolbarbutton.
let toolbarButton = CustomButton({
id: 'mullvad-toolbar-button',
label: DISABLED_STATE.label,
icon: DISABLED_STATE.icon,
onClick: onTButtonClick
});
// view.js
module.metadata = {
'stability': 'experimental',
'engines': {
'Firefox': '> 28'
}
};
const { Cu } = require('chrome');
const { on, off, emit } = require('sdk/event/core');
const { data } = require('sdk/self');
const { isObject } = require('sdk/lang/type');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const { ignoreWindow } = require('sdk/private-browsing/utils');
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
const { events: viewEvents } = require('sdk/ui/button/view/events');
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const views = new Map();
const customizedWindows = new WeakMap();
const buttonListener = {
onCustomizeStart: window => {
for (let [id, view] of views) {
setIcon(id, window, view.icon);
setLabel(id, window, view.label);
}
customizedWindows.set(window, true);
},
onCustomizeEnd: window => {
customizedWindows.delete(window);
for (let [id, ] of views) {
let placement = CustomizableUI.getPlacementOfWidget(id);
if (placement){
emit(viewEvents, 'data', { type: 'update', target: id, window: window });
}
}
},
onWidgetAfterDOMChange: (node, nextNode, container) => {
let { id } = node;
let view = views.get(id);
let window = node.ownerDocument.defaultView;
if (view) {
emit(viewEvents, 'data', { type: 'update', target: id, window: window });
}
}
};
CustomizableUI.addListener(buttonListener);
require('sdk/system/unload').when( _ => CustomizableUI.removeListener(buttonListener));
function getNode(id, window) {
return !views.has(id) || ignoreWindow(window)
? null
: CustomizableUI.getWidget(id).forWindow(window).node
};
function isInToolbar(id) {
let placement = CustomizableUI.getPlacementOfWidget(id);
return placement && CustomizableUI.getAreaType(placement.area) === 'toolbar';
}
function getImage(icon, isInToolbar, pixelRatio) {
let targetSize = (isInToolbar ? 18 : 32) * pixelRatio;
let bestSize = 0;
let image = icon;
if (isObject(icon)) {
for (let size of Object.keys(icon)) {
size = +size;
let offset = targetSize - size;
if (offset === 0) {
bestSize = size;
break;
}
let delta = Math.abs(offset) - Math.abs(targetSize - bestSize);
if (delta < 0){
bestSize = size;
}
}
image = icon[bestSize];
}
if (image.indexOf('./') === 0){
return data.url(image.substr(2));
}
return image;
}
function nodeFor(id, window=getMostRecentBrowserWindow()) {
return customizedWindows.has(window) ? null : getNode(id, window);
};
exports.nodeFor = nodeFor;
function create(options) {
let { id, label, icon, type } = options;
if (views.has(id))
throw new Error('The ID "' + id + '" seems already used.');
CustomizableUI.createWidget({
id: id,
type: 'custom',
removable: true,
defaultArea: AREA_NAVBAR,
allowedAreas: [ AREA_PANEL, AREA_NAVBAR ],
onBuild: function(document) {
let window = document.defaultView;
let node = document.createElementNS(XUL_NS, 'toolbarbutton');
let image = getImage(icon, true, window.devicePixelRatio);
const MouseEvents = {0: 1, 2: 1};
if (ignoreWindow(window)) {
node.style.display = 'none';
}
node.setAttribute('id', this.id);
node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional');
node.setAttribute('type', type);
node.setAttribute('label', label);
node.setAttribute('tooltiptext', label);
node.setAttribute('image', image);
node.setAttribute('sdk-button', 'true');
views.set(id, {
area: this.currentArea,
icon: icon,
label: label
});
node.addEventListener('contextmenu', function(event){
event.preventDefault();
event.stopPropagation();
return false;
},true);
node.addEventListener('click', function(event) {
if ( !(event.button in MouseEvents) ) return;
if (views.has(id)) {
emit(viewEvents, 'data', {
type: 'click',
target: id,
window: event.view,
checked: node.checked,
typeClick: event.button
});
}
});
return node;
}
});
};
exports.create = create;
function dispose(id) {
if (!views.has(id)) return;
views.delete(id);
CustomizableUI.destroyWidget(id);
}
exports.dispose = dispose;
function setIcon(id, window, icon) {
let node = getNode(id, window);
if (node) {
icon = customizedWindows.has(window) ? views.get(id).icon : icon;
let image = getImage(icon, isInToolbar(id), window.devicePixelRatio);
node.setAttribute('image', image);
}
}
exports.setIcon = setIcon;
function setLabel(id, window, label) {
let node = nodeFor(id, window);
if (node) {
node.setAttribute('label', label);
node.setAttribute('tooltiptext', label);
}
}
exports.setLabel = setLabel;
function setDisabled(id, window, disabled) {
let node = nodeFor(id, window);
if (node){
node.disabled = disabled;
}
}
exports.setDisabled = setDisabled;
function setChecked(id, window, checked) {
let node = nodeFor(id, window);
if (node){
node.checked = checked;
}
}
exports.setChecked = setChecked;
function click(id) {
let node = nodeFor(id);
if (node){
node.click();
}
}
exports.click = click;
// index.js
const RIGHT_CLICK = 2;
const LEFT_CLICK = 0;
/**
* Handler for toolbar button click.
* @param {object} state current state of toolbar button.
* @param {number} typeClick Mouse Event type: 0 - left click, 2 - rigth click.
*/
function onTButtonClick(state, typeClick){
if (typeClick === RIGHT_CLICK){
openPopup(toolbarButton)
} else if (typeClick === LEFT_CLICK) {
changeState(toolbarButton, state);
}
}
You can find the code of the created project in Bitbucket with following link.