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.