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.
6 * @fileoverview Base class for all login WebUI screens.
8 cr
.define('login', function() {
9 /** @const */ var CALLBACK_CONTEXT_CHANGED
= 'contextChanged';
10 /** @const */ var CALLBACK_USER_ACTED
= 'userActed';
12 function doNothing() {};
14 function alwaysTruePredicate() { return true; }
16 var querySelectorAll
= HTMLDivElement
.prototype.querySelectorAll
;
18 var Screen = function(sendPrefix
) {
19 this.sendPrefix_
= sendPrefix
;
20 this.screenContext_
= null;
21 this.contextObservers_
= {};
25 __proto__
: HTMLDivElement
.prototype,
28 * Prefix added to sent to Chrome messages' names.
33 * Context used by this screen.
38 return this.screenContext_
;
42 * Dictionary of context observers that are methods of |this| bound to
45 contextObservers_
: null,
48 * Called during screen initialization.
53 * Returns minimal size that screen prefers to have. Default implementation
54 * returns current screen size.
55 * @return {{width: number, height: number}}
57 getPreferredSize: function() {
58 return {width
: this.offsetWidth
, height
: this.offsetHeight
};
62 * Called for currently active screen when screen size changed.
64 onWindowResize
: doNothing
,
69 initialize: function() {
70 return this.initializeImpl_
.apply(this, arguments
);
77 return this.sendImpl_
.apply(this, arguments
);
83 addContextObserver: function() {
84 return this.addContextObserverImpl_
.apply(this, arguments
);
90 removeContextObserver: function() {
91 return this.removeContextObserverImpl_
.apply(this, arguments
);
97 commitContextChanges: function() {
98 return this.commitContextChangesImpl_
.apply(this, arguments
);
102 * Creates and returns new button element with given identifier
103 * and on-click event listener, which sends notification about
104 * user action to the C++ side.
106 * @param {string} id Identifier of a button.
107 * @param {string} opt_action_id Identifier of user action.
110 declareButton: function(id
, opt_action_id
) {
111 var button
= this.ownerDocument
.createElement('button');
113 this.declareUserAction(button
,
114 { action_id
: opt_action_id
,
121 * Adds event listener to an element which sends notification
122 * about event to the C++ side.
124 * @param {Element} element An DOM element
125 * @param {Object} options A dictionary of optional arguments:
126 * {string} event: name of event that will be listened,
128 * {string} action_id: name of an action which will be sent to
130 * {function} condition: a one-argument function which takes
131 * event as an argument, notification is sent to the
132 * C++ side iff condition is true, default: constant
136 declareUserAction: function(element
, options
) {
138 options
= options
|| {};
140 var event
= options
.event
|| 'click';
141 var action_id
= options
.action_id
|| element
.id
;
142 var condition
= options
.condition
|| alwaysTruePredicate
;
144 element
.addEventListener(event
, function(e
) {
146 self
.sendImpl_(CALLBACK_USER_ACTED
, action_id
);
155 querySelectorAll: function() {
156 return this.querySelectorAllImpl_
.apply(this, arguments
);
160 * Does the following things:
161 * * Creates screen context.
162 * * Looks for elements having "alias" property and adds them as the
163 * proprties of the screen with name equal to value of "alias", i.e. HTML
164 * element <div alias="myDiv"></div> will be stored in this.myDiv.
165 * * Looks for buttons having "action" properties and adds click handlers
166 * to them. These handlers send |CALLBACK_USER_ACTED| messages to
167 * C++ with "action" property's value as payload.
170 initializeImpl_: function() {
172 this.screenContext_
= new login
.ScreenContext();
176 this.querySelectorAllImpl_('[alias]').forEach(function(element
) {
177 var alias
= element
.getAttribute('alias');
179 throw Error('Alias "' + alias
+ '" of "' + this.name() + '" screen ' +
180 'shadows or redefines property that is already defined.');
181 this[alias
] = element
;
182 this[element
.getAttribute('alias')] = element
;
185 this.querySelectorAllImpl_('button[action]').forEach(function(button
) {
186 button
.addEventListener('click', function(e
) {
187 var action
= this.getAttribute('action');
188 self
.send(CALLBACK_USER_ACTED
, action
);
195 * Sends message to Chrome, adding needed prefix to message name. All
196 * arguments after |messageName| are packed into message parameters list.
198 * @param {string} messageName Name of message without a prefix.
199 * @param {...*} varArgs parameters for message.
202 sendImpl_: function(messageName
, varArgs
) {
203 if (arguments
.length
== 0)
204 throw Error('Message name is not provided.');
205 var fullMessageName
= this.sendPrefix_
+ messageName
;
206 var payload
= Array
.prototype.slice
.call(arguments
, 1);
207 chrome
.send(fullMessageName
, payload
);
211 * Starts observation of property with |key| of the context attached to
212 * current screen. This method differs from "login.ScreenContext" in that
213 * it automatically detects if observer is method of |this| and make
214 * all needed actions to make it work correctly. So it's no need for client
215 * to bind methods to |this| and keep resulting callback for
216 * |removeObserver| call:
218 * this.addContextObserver('key', this.onKeyChanged_);
220 * this.removeContextObserver('key', this.onKeyChanged_);
223 addContextObserverImpl_: function(key
, observer
) {
224 var realObserver
= observer
;
225 var propertyName
= this.getPropertyNameOf_(observer
);
227 if (!this.contextObservers_
.hasOwnProperty(propertyName
))
228 this.contextObservers_
[propertyName
] = observer
.bind(this);
229 realObserver
= this.contextObservers_
[propertyName
];
231 this.screenContext_
.addObserver(key
, realObserver
);
235 * Removes |observer| from the list of context observers. Supports not only
236 * regular functions but also screen methods (see comment to
237 * |addContextObserver|).
240 removeContextObserverImpl_: function(observer
) {
241 var realObserver
= observer
;
242 var propertyName
= this.getPropertyNameOf_(observer
);
244 if (!this.contextObservers_
.hasOwnProperty(propertyName
))
246 realObserver
= this.contextObservers_
[propertyName
];
247 delete this.contextObservers_
[propertyName
];
249 this.screenContext_
.removeObserver(realObserver
);
253 * Sends recent context changes to C++ handler.
256 commitContextChangesImpl_: function() {
257 if (!this.screenContext_
.hasChanges())
259 this.sendImpl_(CALLBACK_CONTEXT_CHANGED
,
260 this.screenContext_
.getChangesAndReset());
264 * Calls standart |querySelectorAll| method and returns its result converted
268 querySelectorAllImpl_: function(selector
) {
269 var list
= querySelectorAll
.call(this, selector
);
270 return Array
.prototype.slice
.call(list
);
274 * Called when context changes are recieved from C++.
277 contextChanged_: function(diff
) {
278 this.screenContext_
.applyChanges(diff
);
282 * If |value| is the value of some property of |this| returns property's
283 * name. Otherwise returns empty string.
286 getPropertyNameOf_: function(value
) {
287 for (var key
in this)
288 if (this[key
] === value
)
294 Screen
.CALLBACK_USER_ACTED
= CALLBACK_USER_ACTED
;
301 cr
.define('login', function() {
304 * Creates class and object for screen.
305 * Methods specified in EXTERNAL_API array of prototype
306 * will be available from C++ part.
308 * login.createScreen('ScreenName', 'screen-id', {
309 * foo: function() { console.log('foo'); },
310 * bar: function() { console.log('bar'); }
311 * EXTERNAL_API: ['foo'];
313 * login.ScreenName.register();
314 * var screen = $('screen-id');
315 * screen.foo(); // valid
316 * login.ScreenName.foo(); // valid
317 * screen.bar(); // valid
318 * login.ScreenName.bar(); // invalid
320 * @param {string} name Name of created class.
321 * @param {string} id Id of div representing screen.
322 * @param {(function()|Object)} proto Prototype of object or function that
325 createScreen: function(name
, id
, template
) {
326 if (typeof template
== 'function')
327 template
= template();
329 var apiNames
= template
.EXTERNAL_API
|| [];
330 for (var i
= 0; i
< apiNames
.length
; ++i
) {
331 var methodName
= apiNames
[i
];
332 if (typeof template
[methodName
] !== 'function')
333 throw Error('External method "' + methodName
+ '" for screen "' +
334 name
+ '" not a function or undefined.');
337 function checkPropertyAllowed(propertyName
) {
338 if (propertyName
.charAt(propertyName
.length
- 1) === '_' &&
339 (propertyName
in login
.Screen
.prototype)) {
340 throw Error('Property "' + propertyName
+ '" of "' + id
+ '" ' +
341 'shadows private property of login.Screen prototype.');
345 var Constructor = function() {
346 login
.Screen
.call(this, 'login.' + name
+ '.');
348 Constructor
.prototype = Object
.create(login
.Screen
.prototype);
351 Object
.getOwnPropertyNames(template
).forEach(function(propertyName
) {
352 if (propertyName
=== 'EXTERNAL_API')
355 checkPropertyAllowed(propertyName
);
358 Object
.getOwnPropertyDescriptor(template
, propertyName
);
359 Object
.defineProperty(Constructor
.prototype, propertyName
, descriptor
);
361 if (apiNames
.indexOf(propertyName
) >= 0) {
362 api
[propertyName
] = function() {
364 return screen
[propertyName
].apply(screen
, arguments
);
369 Constructor
.prototype.name = function() { return id
; };
371 api
.contextChanged = function() {
373 screen
.contextChanged_
.apply(screen
, arguments
);
376 api
.register = function(opt_lazy_init
) {
378 screen
.__proto__
= new Constructor();
380 if (opt_lazy_init
!== undefined && opt_lazy_init
)
381 screen
.deferredInitialization = function() { screen
.initialize(); }
384 Oobe
.getInstance().registerScreen(screen
);
387 cr
.define('login', function() {