Roll src/third_party/WebKit d9c6159:8139f33 (svn 201974:201975)
[chromium-blink-merge.git] / ui / login / screen.js
blob960632a6e25496beac66bb7193a772b4a5259e5b
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.
5 /**
6  * @fileoverview Base class for all login WebUI screens.
7  */
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_ = {};
22   };
24   Screen.prototype = {
25     __proto__: HTMLDivElement.prototype,
27     /**
28      * Prefix added to sent to Chrome messages' names.
29      */
30     sendPrefix_: null,
32     /**
33      * Context used by this screen.
34      */
35     screenContext_: null,
37     get context() {
38       return this.screenContext_;
39     },
41     /**
42      * Dictionary of context observers that are methods of |this| bound to
43      * |this|.
44      */
45     contextObservers_: null,
47     /**
48      * Called during screen initialization.
49      */
50     decorate: doNothing,
52     /**
53      * Returns minimal size that screen prefers to have. Default implementation
54      * returns current screen size.
55      * @return {{width: number, height: number}}
56      */
57     getPreferredSize: function() {
58       return {width: this.offsetWidth, height: this.offsetHeight};
59     },
61     /**
62      * Called for currently active screen when screen size changed.
63      */
64     onWindowResize: doNothing,
66     /**
67      * @final
68      */
69     initialize: function() {
70       return this.initializeImpl_.apply(this, arguments);
71     },
73     /**
74      * @final
75      */
76     send: function() {
77       return this.sendImpl_.apply(this, arguments);
78     },
80     /**
81      * @final
82      */
83     addContextObserver: function() {
84       return this.addContextObserverImpl_.apply(this, arguments);
85     },
87     /**
88      * @final
89      */
90     removeContextObserver: function() {
91       return this.removeContextObserverImpl_.apply(this, arguments);
92     },
94     /**
95      * @final
96      */
97     commitContextChanges: function() {
98       return this.commitContextChangesImpl_.apply(this, arguments);
99     },
101     /**
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.
105      *
106      * @param {string} id Identifier of a button.
107      * @param {string} opt_action_id Identifier of user action.
108      * @final
109      */
110     declareButton: function(id, opt_action_id) {
111       var button = this.ownerDocument.createElement('button');
112       button.id = id;
113       this.declareUserAction(button,
114                              { action_id: opt_action_id,
115                                event: 'click'
116                              });
117       return button;
118     },
120     /**
121       * Adds event listener to an element which sends notification
122       * about event to the C++ side.
123       *
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,
127       *            default: 'click'.
128       *   {string} action_id: name of an action which will be sent to
129       *                       the C++ side.
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
133       *              true function.
134       * @final
135       */
136     declareUserAction: function(element, options) {
137       var self = this;
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) {
145         if (condition(e))
146           self.sendImpl_(CALLBACK_USER_ACTED, action_id);
147         e.stopPropagation();
148       });
149     },
151     /**
152      * @override
153      * @final
154      */
155     querySelectorAll: function() {
156       return this.querySelectorAllImpl_.apply(this, arguments);
157     },
159     /**
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.
168      * @private
169      */
170     initializeImpl_: function() {
171       if (cr.isChromeOS)
172         this.screenContext_ = new login.ScreenContext();
174       this.decorate();
176       this.querySelectorAllImpl_('[alias]').forEach(function(element) {
177         var alias = element.getAttribute('alias');
178         if (alias in this)
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;
183       }, this);
184       var self = this;
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);
189           e.stopPropagation();
190         });
191       });
192     },
194     /**
195      * Sends message to Chrome, adding needed prefix to message name. All
196      * arguments after |messageName| are packed into message parameters list.
197      *
198      * @param {string} messageName Name of message without a prefix.
199      * @param {...*} varArgs parameters for message.
200      * @private
201      */
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);
208     },
210     /**
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:
217      *
218      *   this.addContextObserver('key', this.onKeyChanged_);
219      *   ...
220      *   this.removeContextObserver('key', this.onKeyChanged_);
221      * @private
222      */
223     addContextObserverImpl_: function(key, observer) {
224       var realObserver = observer;
225       var propertyName = this.getPropertyNameOf_(observer);
226       if (propertyName) {
227         if (!this.contextObservers_.hasOwnProperty(propertyName))
228           this.contextObservers_[propertyName] = observer.bind(this);
229         realObserver = this.contextObservers_[propertyName];
230       }
231       this.screenContext_.addObserver(key, realObserver);
232     },
234     /**
235      * Removes |observer| from the list of context observers. Supports not only
236      * regular functions but also screen methods (see comment to
237      * |addContextObserver|).
238      * @private
239      */
240     removeContextObserverImpl_: function(observer) {
241       var realObserver = observer;
242       var propertyName = this.getPropertyNameOf_(observer);
243       if (propertyName) {
244         if (!this.contextObservers_.hasOwnProperty(propertyName))
245           return;
246         realObserver = this.contextObservers_[propertyName];
247         delete this.contextObservers_[propertyName];
248       }
249       this.screenContext_.removeObserver(realObserver);
250     },
252     /**
253      * Sends recent context changes to C++ handler.
254      * @private
255      */
256     commitContextChangesImpl_: function() {
257       if (!this.screenContext_.hasChanges())
258         return;
259       this.sendImpl_(CALLBACK_CONTEXT_CHANGED,
260                      this.screenContext_.getChangesAndReset());
261     },
263     /**
264      * Calls standart |querySelectorAll| method and returns its result converted
265      * to Array.
266      * @private
267      */
268     querySelectorAllImpl_: function(selector) {
269       var list = querySelectorAll.call(this, selector);
270       return Array.prototype.slice.call(list);
271     },
273     /**
274      * Called when context changes are recieved from C++.
275      * @private
276      */
277     contextChanged_: function(diff) {
278       this.screenContext_.applyChanges(diff);
279     },
281     /**
282      * If |value| is the value of some property of |this| returns property's
283      * name. Otherwise returns empty string.
284      * @private
285      */
286     getPropertyNameOf_: function(value) {
287       for (var key in this)
288         if (this[key] === value)
289           return key;
290       return '';
291     }
292   };
294   Screen.CALLBACK_USER_ACTED = CALLBACK_USER_ACTED;
296   return {
297     Screen: Screen
298   };
301 cr.define('login', function() {
302   return {
303     /**
304      * Creates class and object for screen.
305      * Methods specified in EXTERNAL_API array of prototype
306      * will be available from C++ part.
307      * Example:
308      *     login.createScreen('ScreenName', 'screen-id', {
309      *       foo: function() { console.log('foo'); },
310      *       bar: function() { console.log('bar'); }
311      *       EXTERNAL_API: ['foo'];
312      *     });
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
319      *
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
323      *     returns prototype.
324      */
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.');
335       }
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.');
342         }
343       };
345       var Constructor = function() {
346         login.Screen.call(this, 'login.' + name + '.');
347       };
348       Constructor.prototype = Object.create(login.Screen.prototype);
349       var api = {};
351       Object.getOwnPropertyNames(template).forEach(function(propertyName) {
352         if (propertyName === 'EXTERNAL_API')
353           return;
355         checkPropertyAllowed(propertyName);
357         var descriptor =
358             Object.getOwnPropertyDescriptor(template, propertyName);
359         Object.defineProperty(Constructor.prototype, propertyName, descriptor);
361         if (apiNames.indexOf(propertyName) >= 0) {
362           api[propertyName] = function() {
363               var screen = $(id);
364               return screen[propertyName].apply(screen, arguments);
365           };
366         }
367       });
369       Constructor.prototype.name = function() { return id; };
371       api.contextChanged = function() {
372         var screen = $(id);
373         screen.contextChanged_.apply(screen, arguments);
374       }
376       api.register = function(opt_lazy_init) {
377         var screen = $(id);
378         screen.__proto__ = new Constructor();
380         if (opt_lazy_init !== undefined && opt_lazy_init)
381           screen.deferredInitialization = function() { screen.initialize(); }
382         else
383           screen.initialize();
384         Oobe.getInstance().registerScreen(screen);
385       };
387       cr.define('login', function() {
388         var result = {};
389         result[name] = api;
390         return result;
391       });
392     }
393   };