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

Paradise for the UI/UX designer: Sketch 3

Paradise for the UI/UX designer: Sketch 3

That's not a secret the UI/UX designers use in their work such products like Adobe Photoshop or Sketch 3. Why…

Read more
19 best extensions for your browser

19 best extensions for your browser

Today we would like to do a small review of popular browser extensions for Google Chrome or Firefox. If before…

Read more
Mobile Apps for Business

Mobile Apps for Business

According to experts’ forecasts, in 2018 mobile phones will be 6 times more popular than PCs and laptops. The number of downloads from AppStore and…

Read more