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.

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 following the link..