summaryrefslogtreecommitdiff
path: root/toolkit/jetpack/sdk/ui/button
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/ui/button')
-rw-r--r--toolkit/jetpack/sdk/ui/button/action.js114
-rw-r--r--toolkit/jetpack/sdk/ui/button/contract.js73
-rw-r--r--toolkit/jetpack/sdk/ui/button/toggle.js127
-rw-r--r--toolkit/jetpack/sdk/ui/button/view.js243
-rw-r--r--toolkit/jetpack/sdk/ui/button/view/events.js18
5 files changed, 575 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/button/action.js b/toolkit/jetpack/sdk/ui/button/action.js
new file mode 100644
index 0000000000..dfb092d0c3
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/button/action.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Class } = require('../../core/heritage');
+const { merge } = require('../../util/object');
+const { Disposable } = require('../../core/disposable');
+const { on, off, emit, setListeners } = require('../../event/core');
+const { EventTarget } = require('../../event/target');
+const { getNodeView } = require('../../view/core');
+
+const view = require('./view');
+const { buttonContract, stateContract } = require('./contract');
+const { properties, render, state, register, unregister,
+ getDerivedStateFor } = require('../state');
+const { events: stateEvents } = require('../state/events');
+const { events: viewEvents } = require('./view/events');
+const events = require('../../event/utils');
+
+const { getActiveTab } = require('../../tabs/utils');
+
+const { id: addonID } = require('../../self');
+const { identify } = require('../id');
+
+const buttons = new Map();
+
+const toWidgetId = id =>
+ ('action-button--' + addonID.toLowerCase()+ '-' + id).
+ replace(/[^a-z0-9_-]/g, '');
+
+const ActionButton = Class({
+ extends: EventTarget,
+ implements: [
+ properties(stateContract),
+ state(stateContract),
+ Disposable
+ ],
+ setup: function setup(options) {
+ let state = merge({
+ disabled: false
+ }, buttonContract(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() {
+ return this.state().id;
+ },
+
+ click: function click() { view.click(toWidgetId(this.id)) }
+});
+exports.ActionButton = ActionButton;
+
+identify.define(ActionButton, ({id}) => toWidgetId(id));
+
+getNodeView.define(ActionButton, button =>
+ view.nodeFor(toWidgetId(button.id))
+);
+
+var actionButtonStateEvents = events.filter(stateEvents,
+ e => e.target instanceof ActionButton);
+
+var actionButtonViewEvents = events.filter(viewEvents,
+ e => buttons.has(e.target));
+
+var clickEvents = events.filter(actionButtonViewEvents, e => e.type === 'click');
+var updateEvents = events.filter(actionButtonViewEvents, e => e.type === 'update');
+
+on(clickEvents, 'data', ({target: id, window}) => {
+ let button = buttons.get(id);
+ let state = getDerivedStateFor(button, getActiveTab(window));
+
+ emit(button, 'click', state);
+});
+
+on(updateEvents, 'data', ({target: id, window}) => {
+ render(buttons.get(id), window);
+});
+
+on(actionButtonStateEvents, '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.setBadge(id, window, state.badge, state.badgeColor);
+});
diff --git a/toolkit/jetpack/sdk/ui/button/contract.js b/toolkit/jetpack/sdk/ui/button/contract.js
new file mode 100644
index 0000000000..ce6e33d958
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/button/contract.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { contract } = require('../../util/contract');
+const { isLocalURL } = require('../../url');
+const { isNil, isObject, isString } = require('../../lang/type');
+const { required, either, string, boolean, object, number } = require('../../deprecated/api-utils');
+const { merge } = require('../../util/object');
+const { freeze } = Object;
+
+const isIconSet = (icons) =>
+ Object.keys(icons).
+ every(size => String(size >>> 0) === size && isLocalURL(icons[size]));
+
+var iconSet = {
+ is: either(object, string),
+ map: v => isObject(v) ? freeze(merge({}, v)) : v,
+ ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
+ msg: 'The option "icon" must be a local URL or an object with ' +
+ 'numeric keys / local URL values pair.'
+}
+
+var id = {
+ is: string,
+ ok: v => /^[a-z-_][a-z0-9-_]*$/i.test(v),
+ msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
+ 'underscores are allowed).'
+};
+
+var label = {
+ is: string,
+ ok: v => isNil(v) || v.trim().length > 0,
+ msg: 'The option "label" must be a non empty string'
+}
+
+var badge = {
+ is: either(string, number),
+ msg: 'The option "badge" must be a string or a number'
+}
+
+var badgeColor = {
+ is: string,
+ msg: 'The option "badgeColor" must be a string'
+}
+
+var stateContract = contract({
+ label: label,
+ icon: iconSet,
+ disabled: boolean,
+ badge: badge,
+ badgeColor: badgeColor
+});
+
+exports.stateContract = stateContract;
+
+var buttonContract = contract(merge({}, stateContract.rules, {
+ id: required(id),
+ label: required(label),
+ icon: required(iconSet)
+}));
+
+exports.buttonContract = buttonContract;
+
+exports.toggleStateContract = contract(merge({
+ checked: boolean
+}, stateContract.rules));
+
+exports.toggleButtonContract = contract(merge({
+ checked: boolean
+}, buttonContract.rules));
+
diff --git a/toolkit/jetpack/sdk/ui/button/toggle.js b/toolkit/jetpack/sdk/ui/button/toggle.js
new file mode 100644
index 0000000000..a226b32126
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/button/toggle.js
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Class } = require('../../core/heritage');
+const { merge } = require('../../util/object');
+const { Disposable } = require('../../core/disposable');
+const { on, off, emit, setListeners } = require('../../event/core');
+const { EventTarget } = require('../../event/target');
+const { getNodeView } = require('../../view/core');
+
+const view = require('./view');
+const { toggleButtonContract, toggleStateContract } = require('./contract');
+const { properties, render, state, register, unregister,
+ setStateFor, getStateFor, getDerivedStateFor } = require('../state');
+const { events: stateEvents } = require('../state/events');
+const { events: viewEvents } = require('./view/events');
+const events = require('../../event/utils');
+
+const { getActiveTab } = require('../../tabs/utils');
+
+const { id: addonID } = require('../../self');
+const { identify } = require('../id');
+
+const buttons = new Map();
+
+const toWidgetId = id =>
+ ('toggle-button--' + addonID.toLowerCase()+ '-' + id).
+ replace(/[^a-z0-9_-]/g, '');
+
+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({ type: 'checkbox' }, state, { id: id }));
+ },
+
+ dispose: function dispose() {
+ let id = toWidgetId(this.id);
+ buttons.delete(id);
+
+ off(this);
+
+ view.dispose(id);
+
+ unregister(this);
+ },
+
+ get id() {
+ return this.state().id;
+ },
+
+ click: function click() {
+ return view.click(toWidgetId(this.id));
+ }
+});
+exports.ToggleButton = ToggleButton;
+
+identify.define(ToggleButton, ({id}) => toWidgetId(id));
+
+getNodeView.define(ToggleButton, button =>
+ view.nodeFor(toWidgetId(button.id))
+);
+
+var toggleButtonStateEvents = events.filter(stateEvents,
+ e => e.target instanceof ToggleButton);
+
+var toggleButtonViewEvents = events.filter(viewEvents,
+ e => buttons.has(e.target));
+
+var clickEvents = events.filter(toggleButtonViewEvents, e => e.type === 'click');
+var 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);
+ view.setBadge(id, window, state.badge, state.badgeColor);
+});
+
+on(clickEvents, 'data', ({target: id, window, checked }) => {
+ 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));
+
+ emit(button, 'click', state);
+
+ emit(button, 'change', state);
+});
+
+on(updateEvents, 'data', ({target: id, window}) => {
+ render(buttons.get(id), window);
+});
diff --git a/toolkit/jetpack/sdk/ui/button/view.js b/toolkit/jetpack/sdk/ui/button/view.js
new file mode 100644
index 0000000000..63b7aea31b
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/button/view.js
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '> 28'
+ }
+};
+
+const { Cu } = require('chrome');
+const { on, off, emit } = require('../../event/core');
+
+const { data } = require('sdk/self');
+
+const { isObject, isNil } = require('../../lang/type');
+
+const { getMostRecentBrowserWindow } = require('../../window/utils');
+const { ignoreWindow } = require('../../private-browsing/utils');
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
+
+const { events: viewEvents } = require('./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('../../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, badge } = 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);
+
+ if (ignoreWindow(window))
+ node.style.display = 'none';
+
+ node.setAttribute('id', this.id);
+ node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional badged-button');
+ node.setAttribute('type', type);
+ node.setAttribute('label', label);
+ node.setAttribute('tooltiptext', label);
+ node.setAttribute('image', image);
+ node.setAttribute('constrain-size', 'true');
+
+ views.set(id, {
+ area: this.currentArea,
+ icon: icon,
+ label: label
+ });
+
+ node.addEventListener('command', function(event) {
+ if (views.has(id)) {
+ emit(viewEvents, 'data', {
+ type: 'click',
+ target: id,
+ window: event.view,
+ checked: node.checked
+ });
+ }
+ });
+
+ 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 setBadge(id, window, badge, color) {
+ let node = nodeFor(id, window);
+
+ if (node) {
+ // `Array.from` is needed to handle unicode symbol properly:
+ // '𝐀𝐁'.length is 4 where Array.from('𝐀𝐁').length is 2
+ let text = isNil(badge)
+ ? ''
+ : Array.from(String(badge)).slice(0, 4).join('');
+
+ node.setAttribute('badge', text);
+
+ let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
+ 'class', 'toolbarbutton-badge');
+
+ if (badgeNode)
+ badgeNode.style.backgroundColor = isNil(color) ? '' : color;
+ }
+}
+exports.setBadge = setBadge;
+
+function click(id) {
+ let node = nodeFor(id);
+
+ if (node)
+ node.click();
+}
+exports.click = click;
diff --git a/toolkit/jetpack/sdk/ui/button/view/events.js b/toolkit/jetpack/sdk/ui/button/view/events.js
new file mode 100644
index 0000000000..98909656a6
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/button/view/events.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+module.metadata = {
+ 'stability': 'experimental',
+ 'engines': {
+ 'Firefox': '*',
+ 'SeaMonkey': '*',
+ 'Thunderbird': '*'
+ }
+};
+
+var channel = {};
+
+exports.events = channel;