addon-sdk/source/lib/sdk/ui/toolbar/model.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:72538a1dbc12
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
5
6 module.metadata = {
7 "stability": "experimental",
8 "engines": {
9 "Firefox": "> 28"
10 }
11 };
12
13 const { Class } = require("../../core/heritage");
14 const { EventTarget } = require("../../event/target");
15 const { off, setListeners, emit } = require("../../event/core");
16 const { Reactor, foldp, merges, send } = require("../../event/utils");
17 const { Disposable } = require("../../core/disposable");
18 const { InputPort } = require("../../input/system");
19 const { OutputPort } = require("../../output/system");
20 const { identify } = require("../id");
21 const { pairs, object, map, each } = require("../../util/sequence");
22 const { patch, diff } = require("diffpatcher/index");
23 const { contract } = require("../../util/contract");
24 const { id: addonID } = require("../../self");
25
26 // Input state is accumulated from the input received form the toolbar
27 // view code & local output. Merging local output reflects local state
28 // changes without complete roundloop.
29 const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" }));
30 const output = new OutputPort({ id: "toolbar-change" });
31
32 // Takes toolbar title and normalizes is to an
33 // identifier, also prefixes with add-on id.
34 const titleToId = title =>
35 ("toolbar-" + addonID + "-" + title).
36 toLowerCase().
37 replace(/\s/g, "-").
38 replace(/[^A-Za-z0-9_\-]/g, "");
39
40 const validate = contract({
41 title: {
42 is: ["string"],
43 ok: x => x.length > 0,
44 msg: "The `option.title` string must be provided"
45 },
46 items: {
47 is:["undefined", "object", "array"],
48 msg: "The `options.items` must be iterable sequence of items"
49 },
50 hidden: {
51 is: ["boolean", "undefined"],
52 msg: "The `options.hidden` must be boolean"
53 }
54 });
55
56 // Toolbars is a mapping between `toolbar.id` & `toolbar` instances,
57 // which is used to find intstance for dispatching events.
58 let toolbars = new Map();
59
60 const Toolbar = Class({
61 extends: EventTarget,
62 implements: [Disposable],
63 initialize: function(params={}) {
64 const options = validate(params);
65 const id = titleToId(options.title);
66
67 if (toolbars.has(id))
68 throw Error("Toolbar with this id already exists: " + id);
69
70 // Set of the items in the toolbar isn't mutable, as a matter of fact
71 // it just defines desired set of items, actual set is under users
72 // control. Conver test to an array and freeze to make sure users won't
73 // try mess with it.
74 const items = Object.freeze(options.items ? [...options.items] : []);
75
76 const initial = {
77 id: id,
78 title: options.title,
79 // By default toolbars are visible when add-on is installed, unless
80 // add-on authors decides it should be hidden. From that point on
81 // user is in control.
82 collapsed: !!options.hidden,
83 // In terms of state only identifiers of items matter.
84 items: items.map(identify)
85 };
86
87 this.id = id;
88 this.items = items;
89
90 toolbars.set(id, this);
91 setListeners(this, params);
92
93 // Send initial state to the host so it can reflect it
94 // into a user interface.
95 send(output, object([id, initial]));
96 },
97
98 get title() {
99 const state = reactor.value[this.id];
100 return state && state.title;
101 },
102 get hidden() {
103 const state = reactor.value[this.id];
104 return state && state.collapsed;
105 },
106
107 destroy: function() {
108 send(output, object([this.id, null]));
109 },
110 // `JSON.stringify` serializes objects based of the return
111 // value of this method. For convinienc we provide this method
112 // to serialize actual state data. Note: items will also be
113 // serialized so they should probably implement `toJSON`.
114 toJSON: function() {
115 return {
116 id: this.id,
117 title: this.title,
118 hidden: this.hidden,
119 items: this.items
120 };
121 }
122 });
123 exports.Toolbar = Toolbar;
124 identify.define(Toolbar, toolbar => toolbar.id);
125
126 const dispose = toolbar => {
127 toolbars.delete(toolbar.id);
128 emit(toolbar, "detach");
129 off(toolbar);
130 };
131
132 const reactor = new Reactor({
133 onStep: (present, past) => {
134 const delta = diff(past, present);
135
136 each(([id, update]) => {
137 const toolbar = toolbars.get(id);
138
139 // Remove
140 if (!update)
141 dispose(toolbar);
142 // Add
143 else if (!past[id])
144 emit(toolbar, "attach");
145 // Update
146 else
147 emit(toolbar, update.collapsed ? "hide" : "show", toolbar);
148 }, pairs(delta));
149 }
150 });
151 reactor.run(input);

mercurial