diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/toolbar/model.js')
-rw-r--r-- | toolkit/jetpack/sdk/ui/toolbar/model.js | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/toolbar/model.js b/toolkit/jetpack/sdk/ui/toolbar/model.js new file mode 100644 index 000000000..5c5428606 --- /dev/null +++ b/toolkit/jetpack/sdk/ui/toolbar/model.js @@ -0,0 +1,151 @@ +/* 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 { EventTarget } = require("../../event/target"); +const { off, setListeners, emit } = require("../../event/core"); +const { Reactor, foldp, merges, send } = require("../../event/utils"); +const { Disposable } = require("../../core/disposable"); +const { InputPort } = require("../../input/system"); +const { OutputPort } = require("../../output/system"); +const { identify } = require("../id"); +const { pairs, object, map, each } = require("../../util/sequence"); +const { patch, diff } = require("diffpatcher/index"); +const { contract } = require("../../util/contract"); +const { id: addonID } = require("../../self"); + +// Input state is accumulated from the input received form the toolbar +// view code & local output. Merging local output reflects local state +// changes without complete roundloop. +const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" })); +const output = new OutputPort({ id: "toolbar-change" }); + +// Takes toolbar title and normalizes is to an +// identifier, also prefixes with add-on id. +const titleToId = title => + ("toolbar-" + addonID + "-" + title). + toLowerCase(). + replace(/\s/g, "-"). + replace(/[^A-Za-z0-9_\-]/g, ""); + +const validate = contract({ + title: { + is: ["string"], + ok: x => x.length > 0, + msg: "The `option.title` string must be provided" + }, + items: { + is:["undefined", "object", "array"], + msg: "The `options.items` must be iterable sequence of items" + }, + hidden: { + is: ["boolean", "undefined"], + msg: "The `options.hidden` must be boolean" + } +}); + +// Toolbars is a mapping between `toolbar.id` & `toolbar` instances, +// which is used to find intstance for dispatching events. +var toolbars = new Map(); + +const Toolbar = Class({ + extends: EventTarget, + implements: [Disposable], + initialize: function(params={}) { + const options = validate(params); + const id = titleToId(options.title); + + if (toolbars.has(id)) + throw Error("Toolbar with this id already exists: " + id); + + // Set of the items in the toolbar isn't mutable, as a matter of fact + // it just defines desired set of items, actual set is under users + // control. Conver test to an array and freeze to make sure users won't + // try mess with it. + const items = Object.freeze(options.items ? [...options.items] : []); + + const initial = { + id: id, + title: options.title, + // By default toolbars are visible when add-on is installed, unless + // add-on authors decides it should be hidden. From that point on + // user is in control. + collapsed: !!options.hidden, + // In terms of state only identifiers of items matter. + items: items.map(identify) + }; + + this.id = id; + this.items = items; + + toolbars.set(id, this); + setListeners(this, params); + + // Send initial state to the host so it can reflect it + // into a user interface. + send(output, object([id, initial])); + }, + + get title() { + const state = reactor.value[this.id]; + return state && state.title; + }, + get hidden() { + const state = reactor.value[this.id]; + return state && state.collapsed; + }, + + destroy: function() { + send(output, object([this.id, null])); + }, + // `JSON.stringify` serializes objects based of the return + // value of this method. For convinienc we provide this method + // to serialize actual state data. Note: items will also be + // serialized so they should probably implement `toJSON`. + toJSON: function() { + return { + id: this.id, + title: this.title, + hidden: this.hidden, + items: this.items + }; + } +}); +exports.Toolbar = Toolbar; +identify.define(Toolbar, toolbar => toolbar.id); + +const dispose = toolbar => { + toolbars.delete(toolbar.id); + emit(toolbar, "detach"); + off(toolbar); +}; + +const reactor = new Reactor({ + onStep: (present, past) => { + const delta = diff(past, present); + + each(([id, update]) => { + const toolbar = toolbars.get(id); + + // Remove + if (!update) + dispose(toolbar); + // Add + else if (!past[id]) + emit(toolbar, "attach"); + // Update + else + emit(toolbar, update.collapsed ? "hide" : "show", toolbar); + }, pairs(delta)); + } +}); +reactor.run(input); |