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() {