diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/button')
-rw-r--r-- | toolkit/jetpack/sdk/ui/button/action.js | 114 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/button/contract.js | 73 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/button/toggle.js | 127 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/button/view.js | 243 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/button/view/events.js | 18 |
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 000000000..dfb092d0c --- /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 000000000..ce6e33d95 --- /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 000000000..a226b3212 --- /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 000000000..63b7aea31 --- /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 000000000..98909656a --- /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; |