Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / login / screen.js
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