addon-sdk/source/lib/sdk/ui/state.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:939a9120acf8
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 // The Button module currently supports only Firefox.
7 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
8 module.metadata = {
9 'stability': 'experimental',
10 'engines': {
11 'Firefox': '*'
12 }
13 };
14
15 const { Ci } = require('chrome');
16
17 const events = require('../event/utils');
18 const { events: browserEvents } = require('../browser/events');
19 const { events: tabEvents } = require('../tab/events');
20 const { events: stateEvents } = require('./state/events');
21
22 const { windows, isInteractive, getFocusedBrowser } = require('../window/utils');
23 const { getActiveTab, getOwnerWindow } = require('../tabs/utils');
24
25 const { ignoreWindow } = require('../private-browsing/utils');
26
27 const { freeze } = Object;
28 const { merge } = require('../util/object');
29 const { on, off, emit } = require('../event/core');
30
31 const { add, remove, has, clear, iterator } = require('../lang/weak-set');
32 const { isNil } = require('../lang/type');
33
34 const { viewFor } = require('../view/core');
35
36 const components = new WeakMap();
37
38 const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
39 'The object may be not be registered, or may already have been unloaded.';
40
41 const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
42 'Only window, tab and registered component are valid targets.';
43
44 const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
45 const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
46 const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
47 const isEnumerable = window => !ignoreWindow(window);
48 const browsers = _ =>
49 windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
50 const getMostRecentTab = _ => getActiveTab(getFocusedBrowser());
51
52 function getStateFor(component, target) {
53 if (!isRegistered(component))
54 throw new Error(ERR_UNREGISTERED);
55
56 if (!components.has(component))
57 return null;
58
59 let states = components.get(component);
60
61 if (target) {
62 if (isTab(target) || isWindow(target) || target === component)
63 return states.get(target) || null;
64 else
65 throw new Error(ERR_INVALID_TARGET);
66 }
67
68 return null;
69 }
70 exports.getStateFor = getStateFor;
71
72 function getDerivedStateFor(component, target) {
73 if (!isRegistered(component))
74 throw new Error(ERR_UNREGISTERED);
75
76 if (!components.has(component))
77 return null;
78
79 let states = components.get(component);
80
81 let componentState = states.get(component);
82 let windowState = null;
83 let tabState = null;
84
85 if (target) {
86 // has a target
87 if (isTab(target)) {
88 windowState = states.get(getOwnerWindow(target), null);
89
90 if (states.has(target)) {
91 // we have a tab state
92 tabState = states.get(target);
93 }
94 }
95 else if (isWindow(target) && states.has(target)) {
96 // we have a window state
97 windowState = states.get(target);
98 }
99 }
100
101 return freeze(merge({}, componentState, windowState, tabState));
102 }
103 exports.getDerivedStateFor = getDerivedStateFor;
104
105 function setStateFor(component, target, state) {
106 if (!isRegistered(component))
107 throw new Error(ERR_UNREGISTERED);
108
109 let isComponentState = target === component;
110 let targetWindows = isWindow(target) ? [target] :
111 isActiveTab(target) ? [getOwnerWindow(target)] :
112 isComponentState ? browsers() :
113 isTab(target) ? [] :
114 null;
115
116 if (!targetWindows)
117 throw new Error(ERR_INVALID_TARGET);
118
119 // initialize the state's map
120 if (!components.has(component))
121 components.set(component, new WeakMap());
122
123 let states = components.get(component);
124
125 if (state === null && !isComponentState) // component state can't be deleted
126 states.delete(target);
127 else {
128 let base = isComponentState ? states.get(target) : null;
129 states.set(target, freeze(merge({}, base, state)));
130 }
131
132 render(component, targetWindows);
133 }
134 exports.setStateFor = setStateFor;
135
136 function render(component, targetWindows) {
137 targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
138
139 for (let window of targetWindows.filter(isEnumerable)) {
140 let tabState = getDerivedStateFor(component, getActiveTab(window));
141
142 emit(stateEvents, 'data', {
143 type: 'render',
144 target: component,
145 window: window,
146 state: tabState
147 });
148
149 }
150 }
151 exports.render = render;
152
153 function properties(contract) {
154 let { rules } = contract;
155 let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
156 descriptor[name] = {
157 get: function() { return getDerivedStateFor(this)[name] },
158 set: function(value) {
159 let changed = {};
160 changed[name] = value;
161
162 setStateFor(this, this, contract(changed));
163 }
164 }
165 return descriptor;
166 }, {});
167
168 return Object.create(Object.prototype, descriptor);
169 }
170 exports.properties = properties;
171
172 function state(contract) {
173 return {
174 state: function state(target, state) {
175 let nativeTarget = target === 'window' ? getFocusedBrowser()
176 : target === 'tab' ? getMostRecentTab()
177 : target === this ? null
178 : viewFor(target);
179
180 if (!nativeTarget && target !== this && !isNil(target))
181 throw new Error(ERR_INVALID_TARGET);
182
183 target = nativeTarget || target;
184
185 // jquery style
186 return arguments.length < 2
187 ? getDerivedStateFor(this, target)
188 : setStateFor(this, target, contract(state))
189 }
190 }
191 }
192 exports.state = state;
193
194 const register = (component, state) => {
195 add(components, component);
196 setStateFor(component, component, state);
197 }
198 exports.register = register;
199
200 const unregister = component => {
201 remove(components, component);
202 }
203 exports.unregister = unregister;
204
205 const isRegistered = component => has(components, component);
206 exports.isRegistered = isRegistered;
207
208 let tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect');
209 let tabClose = events.filter(tabEvents, e => e.type === 'TabClose');
210 let windowOpen = events.filter(browserEvents, e => e.type === 'load');
211 let windowClose = events.filter(browserEvents, e => e.type === 'close');
212
213 let close = events.merge([tabClose, windowClose]);
214 let activate = events.merge([windowOpen, tabSelect]);
215
216 on(activate, 'data', ({target}) => {
217 let [window, tab] = isWindow(target)
218 ? [target, getActiveTab(target)]
219 : [getOwnerWindow(target), target];
220
221 if (ignoreWindow(window)) return;
222
223 for (let component of iterator(components)) {
224 emit(stateEvents, 'data', {
225 type: 'render',
226 target: component,
227 window: window,
228 state: getDerivedStateFor(component, tab)
229 });
230 }
231 });
232
233 on(close, 'data', function({target}) {
234 for (let component of iterator(components)) {
235 components.get(component).delete(target);
236 }
237 });

mercurial