OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @fileoverview Base class for all login WebUI screens. | |
7 */ | |
8 cr.define('login', function() { | |
9 /** @const */ var CALLBACK_USER_ACTED = 'userActed'; | |
10 /** @const */ var CALLBACK_CONTEXT_CHANGED = 'contextChanged'; | |
11 | |
12 function doNothing() {}; | |
13 | |
14 var querySelectorAll = HTMLDivElement.prototype.querySelectorAll; | |
15 | |
16 var Screen = function(sendPrefix) { | |
17 this.sendPrefix_ = sendPrefix; | |
18 this.screenContext_ = null; | |
19 this.contextObservers_ = {}; | |
20 }; | |
21 | |
22 Screen.prototype = { | |
23 __proto__: HTMLDivElement.prototype, | |
24 | |
25 /** | |
26 * Prefix added to sent to Chrome messages' names. | |
27 */ | |
28 sendPrefix_: null, | |
29 | |
30 /** | |
31 * Context used by this screen. | |
32 */ | |
33 screenContext_: null, | |
34 | |
35 get context() { | |
36 return this.screenContext_; | |
37 }, | |
38 | |
39 /** | |
40 * Dictionary of context observers that are methods of |this| bound to | |
41 * |this|. | |
42 */ | |
43 contextObservers_: null, | |
44 | |
45 /** | |
46 * Called during screen registration. | |
47 */ | |
48 decorate: doNothing, | |
49 | |
50 /** | |
51 * Returns minimal size that screen prefers to have. Default implementation | |
52 * returns current screen size. | |
53 * @return {{width: number, height: number}} | |
54 */ | |
55 getPreferredSize: function() { | |
56 return {width: this.offsetWidth, height: this.offsetHeight}; | |
57 }, | |
58 | |
59 /** | |
60 * Called for currently active screen when screen size changed. | |
61 */ | |
62 onWindowResize: doNothing, | |
63 | |
64 /** | |
65 * @final | |
66 */ | |
67 initialize: function() { | |
68 return this.initializeImpl_.apply(this, arguments); | |
69 }, | |
70 | |
71 /** | |
72 * @final | |
73 */ | |
74 send: function() { | |
75 return this.sendImpl_.apply(this, arguments); | |
76 }, | |
77 | |
78 /** | |
79 * @final | |
80 */ | |
81 addContextObserver: function() { | |
82 return this.addContextObserverImpl_.apply(this, arguments); | |
83 }, | |
84 | |
85 /** | |
86 * @final | |
87 */ | |
88 removeContextObserver: function() { | |
89 return this.removeContextObserverImpl_.apply(this, arguments); | |
90 }, | |
91 | |
92 /** | |
93 * @final | |
94 */ | |
95 commitContextChanges: function() { | |
96 return this.commitContextChangesImpl_.apply(this, arguments); | |
97 }, | |
98 | |
99 /** | |
100 * @override | |
101 * @final | |
102 */ | |
103 querySelectorAll: function() { | |
104 return this.querySelectorAllImpl_.apply(this, arguments); | |
105 }, | |
106 | |
107 /** | |
108 * Does the following things: | |
109 * * Creates screen context. | |
110 * * Looks for elements having "alias" property and adds them as the | |
111 * proprties of the screen with name equal to value of "alias", i.e. HTML | |
112 * element <div alias="myDiv"></div> will be stored in this.myDiv. | |
113 * * Looks for buttons having "action" properties and adds click handlers | |
114 * to them. These handlers send |CALLBACK_USER_ACTED| messages to | |
115 * C++ with "action" property's value as payload. | |
116 * @private | |
117 */ | |
118 initializeImpl_: function() { | |
119 this.screenContext_ = new login.ScreenContext(); | |
120 this.querySelectorAllImpl_('[alias]').forEach(function(element) { | |
121 var alias = element.getAttribute('alias'); | |
122 if (alias in this) | |
123 throw Error('Alias "' + alias + '" of "' + this.name() + '" screen ' + | |
124 'shadows or redefines property that is already defined.'); | |
125 this[alias] = element; | |
126 this[element.getAttribute('alias')] = element; | |
127 }, this); | |
128 var self = this; | |
129 this.querySelectorAllImpl_('button[action]').forEach(function(button) { | |
130 button.addEventListener('click', function(e) { | |
131 var action = this.getAttribute('action'); | |
132 self.send(CALLBACK_USER_ACTED, action); | |
133 e.stopPropagation(); | |
134 }); | |
135 }); | |
136 }, | |
137 | |
138 /** | |
139 * Sends message to Chrome, adding needed prefix to message name. All | |
140 * arguments after |messageName| are packed into message parameters list. | |
141 * | |
142 * @param {string} messageName Name of message without a prefix. | |
143 * @param {...*} varArgs parameters for message. | |
144 * @private | |
145 */ | |
146 sendImpl_: function(messageName, varArgs) { | |
147 if (arguments.length == 0) | |
148 throw Error('Message name is not provided.'); | |
149 var fullMessageName = this.sendPrefix_ + messageName; | |
150 var payload = Array.prototype.slice.call(arguments, 1); | |
151 chrome.send(fullMessageName, payload); | |
152 }, | |
153 | |
154 /** | |
155 * Starts observation of property with |key| of the context attached to | |
156 * current screen. This method differs from "login.ScreenContext" in that | |
157 * it automatically detects if observer is method of |this| and make | |
158 * all needed actions to make it work correctly. So it's no need for client | |
159 * to bind methods to |this| and keep resulting callback for | |
160 * |removeObserver| call: | |
161 * | |
162 * this.addContextObserver('key', this.onKeyChanged_); | |
163 * ... | |
164 * this.removeContextObserver('key', this.onKeyChanged_); | |
165 * @private | |
166 */ | |
167 addContextObserverImpl_: function(key, observer) { | |
168 var realObserver = observer; | |
169 var propertyName = this.getPropertyNameOf_(observer); | |
170 if (propertyName) { | |
171 if (!this.contextObservers_.hasOwnProperty(propertyName)) | |
172 this.contextObservers_[propertyName] = observer.bind(this); | |
173 realObserver = this.contextObservers_[propertyName]; | |
174 } | |
175 this.screenContext_.addObserver(key, realObserver); | |
176 }, | |
177 | |
178 /** | |
179 * Removes |observer| from the list of context observers. Supports not only | |
180 * regular functions but also screen methods (see comment to | |
181 * |addContextObserver|). | |
182 * @private | |
183 */ | |
184 removeContextObserverImpl_: function(observer) { | |
185 var realObserver = observer; | |
186 var propertyName = this.getPropertyNameOf_(observer); | |
187 if (propertyName) { | |
188 if (!this.contextObservers_.hasOwnProperty(propertyName)) | |
189 return; | |
190 realObserver = this.contextObservers_[propertyName]; | |
191 delete this.contextObservers_[propertyName]; | |
192 } | |
193 this.screenContext_.removeObserver(realObserver); | |
194 }, | |
195 | |
196 /** | |
197 * Sends recent context changes to C++ handler. | |
198 * @private | |
199 */ | |
200 commitContextChangesImpl_: function() { | |
201 if (!this.screenContext_.hasChanges()) | |
202 return; | |
203 this.sendImpl_(CALLBACK_CONTEXT_CHANGED, | |
204 this.screenContext_.getChangesAndReset()); | |
205 }, | |
206 | |
207 /** | |
208 * Calls standart |querySelectorAll| method and returns its result converted | |
209 * to Array. | |
210 * @private | |
211 */ | |
212 querySelectorAllImpl_: function(selector) { | |
213 var list = querySelectorAll.call(this, selector); | |
214 return Array.prototype.slice.call(list); | |
215 }, | |
216 | |
217 /** | |
218 * Called when context changes are recieved from C++. | |
219 * @private | |
220 */ | |
221 contextChanged_: function(diff) { | |
222 this.screenContext_.applyChanges(diff); | |
223 }, | |
224 | |
225 /** | |
226 * If |value| is the value of some property of |this| returns property's | |
227 * name. Otherwise returns empty string. | |
228 * @private | |
229 */ | |
230 getPropertyNameOf_: function(value) { | |
231 for (var key in this) | |
232 if (this[key] === value) | |
233 return key; | |
234 return ''; | |
235 } | |
236 }; | |
237 | |
238 Screen.CALLBACK_USER_ACTED = CALLBACK_USER_ACTED; | |
239 | |
240 return { | |
241 Screen: Screen | |
242 }; | |
243 }); | |
244 | |
245 cr.define('login', function() { | |
246 return { | |
247 /** | |
248 * Creates class and object for screen. | |
249 * Methods specified in EXTERNAL_API array of prototype | |
250 * will be available from C++ part. | |
251 * Example: | |
252 * login.createScreen('ScreenName', 'screen-id', { | |
253 * foo: function() { console.log('foo'); }, | |
254 * bar: function() { console.log('bar'); } | |
255 * EXTERNAL_API: ['foo']; | |
256 * }); | |
257 * login.ScreenName.register(); | |
258 * var screen = $('screen-id'); | |
259 * screen.foo(); // valid | |
260 * login.ScreenName.foo(); // valid | |
261 * screen.bar(); // valid | |
262 * login.ScreenName.bar(); // invalid | |
263 * | |
264 * @param {string} name Name of created class. | |
265 * @param {string} id Id of div representing screen. | |
266 * @param {(function()|Object)} proto Prototype of object or function that | |
267 * returns prototype. | |
268 */ | |
269 createScreen: function(name, id, template) { | |
270 if (typeof template == 'function') | |
271 template = template(); | |
272 | |
273 var apiNames = template.EXTERNAL_API || []; | |
274 for (var i = 0; i < apiNames.length; ++i) { | |
275 var methodName = apiNames[i]; | |
276 if (typeof template[methodName] !== 'function') | |
277 throw Error('External method "' + methodName + '" for screen "' + | |
278 name + '" not a function or undefined.'); | |
279 } | |
280 | |
281 function checkPropertyAllowed(propertyName) { | |
282 if (propertyName.charAt(propertyName.length - 1) === '_' && | |
283 (propertyName in login.Screen.prototype)) { | |
284 throw Error('Property "' + propertyName + '" of "' + id + '" ' + | |
285 'shadows private property of login.Screen prototype.'); | |
286 } | |
287 }; | |
288 | |
289 var Constructor = function() { | |
290 login.Screen.call(this, 'login.' + name + '.'); | |
291 }; | |
292 Constructor.prototype = Object.create(login.Screen.prototype); | |
293 var api = {}; | |
294 | |
295 Object.getOwnPropertyNames(template).forEach(function(propertyName) { | |
296 if (propertyName === 'EXTERNAL_API') | |
297 return; | |
298 | |
299 checkPropertyAllowed(propertyName); | |
300 | |
301 var descriptor = | |
302 Object.getOwnPropertyDescriptor(template, propertyName); | |
303 Object.defineProperty(Constructor.prototype, propertyName, descriptor); | |
304 | |
305 if (apiNames.indexOf(propertyName) >= 0) { | |
306 api[propertyName] = function() { | |
307 var screen = $(id); | |
308 return screen[propertyName].apply(screen, arguments); | |
309 }; | |
310 } | |
311 }); | |
312 | |
313 Constructor.prototype.name = function() { return id; }; | |
314 | |
315 api.contextChanged = function() { | |
316 var screen = $(id); | |
317 screen.contextChanged_.apply(screen, arguments); | |
318 } | |
319 | |
320 api.register = function() { | |
321 var screen = $(id); | |
322 screen.__proto__ = new Constructor(); | |
323 screen.decorate(); | |
324 Oobe.getInstance().registerScreen(screen); | |
325 }; | |
326 | |
327 cr.define('login', function() { | |
328 var result = {}; | |
329 result[name] = api; | |
330 return result; | |
331 }); | |
332 } | |
333 }; | |
334 }); | |
335 | |
OLD | NEW |