summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/ui/toolbar/model.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/ui/toolbar/model.js')
-rw-r--r--toolkit/jetpack/sdk/ui/toolbar/model.js151
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);