|
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); |