michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental", michael@0: "engines": { michael@0: "Firefox": "> 28" michael@0: } michael@0: }; michael@0: michael@0: const { Class } = require("../../core/heritage"); michael@0: const { EventTarget } = require("../../event/target"); michael@0: const { off, setListeners, emit } = require("../../event/core"); michael@0: const { Reactor, foldp, merges, send } = require("../../event/utils"); michael@0: const { Disposable } = require("../../core/disposable"); michael@0: const { InputPort } = require("../../input/system"); michael@0: const { OutputPort } = require("../../output/system"); michael@0: const { identify } = require("../id"); michael@0: const { pairs, object, map, each } = require("../../util/sequence"); michael@0: const { patch, diff } = require("diffpatcher/index"); michael@0: const { contract } = require("../../util/contract"); michael@0: const { id: addonID } = require("../../self"); michael@0: michael@0: // Input state is accumulated from the input received form the toolbar michael@0: // view code & local output. Merging local output reflects local state michael@0: // changes without complete roundloop. michael@0: const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" })); michael@0: const output = new OutputPort({ id: "toolbar-change" }); michael@0: michael@0: // Takes toolbar title and normalizes is to an michael@0: // identifier, also prefixes with add-on id. michael@0: const titleToId = title => michael@0: ("toolbar-" + addonID + "-" + title). michael@0: toLowerCase(). michael@0: replace(/\s/g, "-"). michael@0: replace(/[^A-Za-z0-9_\-]/g, ""); michael@0: michael@0: const validate = contract({ michael@0: title: { michael@0: is: ["string"], michael@0: ok: x => x.length > 0, michael@0: msg: "The `option.title` string must be provided" michael@0: }, michael@0: items: { michael@0: is:["undefined", "object", "array"], michael@0: msg: "The `options.items` must be iterable sequence of items" michael@0: }, michael@0: hidden: { michael@0: is: ["boolean", "undefined"], michael@0: msg: "The `options.hidden` must be boolean" michael@0: } michael@0: }); michael@0: michael@0: // Toolbars is a mapping between `toolbar.id` & `toolbar` instances, michael@0: // which is used to find intstance for dispatching events. michael@0: let toolbars = new Map(); michael@0: michael@0: const Toolbar = Class({ michael@0: extends: EventTarget, michael@0: implements: [Disposable], michael@0: initialize: function(params={}) { michael@0: const options = validate(params); michael@0: const id = titleToId(options.title); michael@0: michael@0: if (toolbars.has(id)) michael@0: throw Error("Toolbar with this id already exists: " + id); michael@0: michael@0: // Set of the items in the toolbar isn't mutable, as a matter of fact michael@0: // it just defines desired set of items, actual set is under users michael@0: // control. Conver test to an array and freeze to make sure users won't michael@0: // try mess with it. michael@0: const items = Object.freeze(options.items ? [...options.items] : []); michael@0: michael@0: const initial = { michael@0: id: id, michael@0: title: options.title, michael@0: // By default toolbars are visible when add-on is installed, unless michael@0: // add-on authors decides it should be hidden. From that point on michael@0: // user is in control. michael@0: collapsed: !!options.hidden, michael@0: // In terms of state only identifiers of items matter. michael@0: items: items.map(identify) michael@0: }; michael@0: michael@0: this.id = id; michael@0: this.items = items; michael@0: michael@0: toolbars.set(id, this); michael@0: setListeners(this, params); michael@0: michael@0: // Send initial state to the host so it can reflect it michael@0: // into a user interface. michael@0: send(output, object([id, initial])); michael@0: }, michael@0: michael@0: get title() { michael@0: const state = reactor.value[this.id]; michael@0: return state && state.title; michael@0: }, michael@0: get hidden() { michael@0: const state = reactor.value[this.id]; michael@0: return state && state.collapsed; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: send(output, object([this.id, null])); michael@0: }, michael@0: // `JSON.stringify` serializes objects based of the return michael@0: // value of this method. For convinienc we provide this method michael@0: // to serialize actual state data. Note: items will also be michael@0: // serialized so they should probably implement `toJSON`. michael@0: toJSON: function() { michael@0: return { michael@0: id: this.id, michael@0: title: this.title, michael@0: hidden: this.hidden, michael@0: items: this.items michael@0: }; michael@0: } michael@0: }); michael@0: exports.Toolbar = Toolbar; michael@0: identify.define(Toolbar, toolbar => toolbar.id); michael@0: michael@0: const dispose = toolbar => { michael@0: toolbars.delete(toolbar.id); michael@0: emit(toolbar, "detach"); michael@0: off(toolbar); michael@0: }; michael@0: michael@0: const reactor = new Reactor({ michael@0: onStep: (present, past) => { michael@0: const delta = diff(past, present); michael@0: michael@0: each(([id, update]) => { michael@0: const toolbar = toolbars.get(id); michael@0: michael@0: // Remove michael@0: if (!update) michael@0: dispose(toolbar); michael@0: // Add michael@0: else if (!past[id]) michael@0: emit(toolbar, "attach"); michael@0: // Update michael@0: else michael@0: emit(toolbar, update.collapsed ? "hide" : "show", toolbar); michael@0: }, pairs(delta)); michael@0: } michael@0: }); michael@0: reactor.run(input);