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