• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
8cr.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
245cr.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
336