Браузерная кнопка для Firefox. Кастомизируем поведение.

Пример, когда необходимо кастомизировать поведение браузерной кнопки — стандартный функционал Firefox не предоставляет возможность обрабатывать отдельно клик левой и правой кнопки мыши.

Есть несколько способов создать Extension с браузерной кнопкой, самые простые — использовав Firefox SDK и Web Extension. Но возможность кастомизировать есть только у Firefox SDK Extension.

Для этого необходимо реализовать свой класс Кнопки Браузера

// 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);
});

Использование созданного 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);
  }
}

Код созданного проекта вы можете найти в Bitbucket по ссылке: Ссылка на репозиторий

19 лучших расширений для вашего браузера

19 лучших расширений для вашего браузера

Сегодня, мы бы хотели сделать небольшой обзор популярных браузерных расширений для Google Chrome или Firefox. Если вы никогда не сталкивались с такой…

Мобильные приложения для бизнеса

Мобильные приложения для бизнеса

По прогнозам экспертов, к 2018 году мобильные телефоны станут в 6 раз популярнее стационарных компьютеров и ноутбуков. Количество скачиваний мобильных приложений из AppStore и Google Play в конце…

Интервью с директором

Интервью с директором

Алексей Казбаев — один из соучредителей компании ООО «Макте», работающей под брендом Macte! Labs. За плечами: — Более 10 лет работы с проектами на международном рынке заказного…