• 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  var Screen = cr.ui.define('div');
10
11  /** @const */ var CALLBACK_USER_ACTED = 'userActed';
12  /** @const */ var CALLBACK_CONTEXT_CHANGED = 'contextChanged';
13
14  function doNothing() {};
15
16  var querySelectorAll = HTMLDivElement.prototype.querySelectorAll;
17
18  Screen.prototype = {
19    __proto__: HTMLDivElement.prototype,
20
21    /**
22     * Prefix added to sent to Chrome messages' names.
23     */
24    sendPrefix_: '',
25
26    /**
27     * Context used by this screen.
28     */
29    context_: null,
30
31    /**
32     * Dictionary of context observers that are methods of |this| bound to
33     * |this|.
34     */
35    contextObservers_: {},
36
37    get context() {
38      return this.context_;
39    },
40
41    /**
42     * Sends recent context changes to C++ handler.
43     */
44    commitContextChanges: function() {
45      if (!this.context_.hasChanges())
46        return;
47      this.send(CALLBACK_CONTEXT_CHANGED, this.context_.getChangesAndReset());
48    },
49
50    /**
51     * Sends message to Chrome, adding needed prefix to message name. All
52     * arguments after |messageName| are packed into message parameters list.
53     *
54     * @param {string} messageName Name of message without a prefix.
55     * @param {...*} varArgs parameters for message.
56     */
57    send: function(messageName, varArgs) {
58      if (arguments.length == 0)
59        throw Error('Message name is not provided.');
60      var fullMessageName = this.sendPrefix_ + messageName;
61      var payload = Array.prototype.slice.call(arguments, 1);
62      chrome.send(fullMessageName, payload);
63    },
64
65    decorate: doNothing,
66
67    /**
68     * Returns minimal size that screen prefers to have. Default implementation
69     * returns current screen size.
70     * @return {{width: number, height: number}}
71     */
72    getPreferredSize: function() {
73      return {width: this.offsetWidth, height: this.offsetHeight};
74    },
75
76    /**
77     * Called for currently active screen when screen size changed.
78     */
79    onWindowResize: doNothing,
80
81    /**
82     * Does the following things:
83     *  * Creates screen context.
84     *  * Looks for elements having "alias" property and adds them as the
85     *    proprties of the screen with name equal to value of "alias", i.e. HTML
86     *    element <div alias="myDiv"></div> will be stored in this.myDiv.
87     *  * Looks for buttons having "action" properties and adds click handlers
88     *    to them. These handlers send |CALLBACK_USER_ACTED| messages to
89     *    C++ with "action" property's value as payload.
90     */
91    initialize: function() {
92      this.context_ = new login.ScreenContext();
93      this.querySelectorAll('[alias]').forEach(function(element) {
94        this[element.getAttribute('alias')] = element;
95      }, this);
96      var self = this;
97      this.querySelectorAll('button[action]').forEach(function(button) {
98        button.addEventListener('click', function(e) {
99          var action = this.getAttribute('action');
100          self.send(CALLBACK_USER_ACTED, action);
101          e.stopPropagation();
102        });
103      });
104    },
105
106    /**
107     * Starts observation of property with |key| of the context attached to
108     * current screen. This method differs from "login.ScreenContext" in that
109     * it automatically detects if observer is method of |this| and make
110     * all needed actions to make it work correctly. So it's no need for client
111     * to bind methods to |this| and keep resulting callback for
112     * |removeObserver| call:
113     *
114     *   this.addContextObserver('key', this.onKeyChanged_);
115     *   ...
116     *   this.removeContextObserver('key', this.onKeyChanged_);
117     */
118    addContextObserver: function(key, observer) {
119      var realObserver = observer;
120      var propertyName = this.getPropertyNameOf_(observer);
121      if (propertyName) {
122        if (!this.contextObservers_.hasOwnProperty(propertyName))
123          this.contextObservers_[propertyName] = observer.bind(this);
124        realObserver = this.contextObservers_[propertyName];
125      }
126      this.context.addObserver(key, realObserver);
127    },
128
129    /**
130     * Removes |observer| from the list of context observers. Supports not only
131     * regular functions but also screen methods (see comment to
132     * |addContextObserver|).
133     */
134    removeContextObserver: function(observer) {
135      var realObserver = observer;
136      var propertyName = this.getPropertyNameOf_(observer);
137      if (propertyName) {
138        if (!this.contextObservers_.hasOwnProperty(propertyName))
139          return;
140        realObserver = this.contextObservers_[propertyName];
141        delete this.contextObservers_[propertyName];
142      }
143      this.context.removeObserver(realObserver);
144    },
145
146    /**
147     * Calls standart |querySelectorAll| method and returns its result converted
148     * to Array.
149     */
150    querySelectorAll: function(selector) {
151      var list = querySelectorAll.call(this, selector);
152      return Array.prototype.slice.call(list);
153    },
154
155    /**
156     * Called when context changes are recieved from C++.
157     * @private
158     */
159    contextChanged_: function(diff) {
160      this.context.applyChanges(diff);
161    },
162
163    /**
164     * If |value| is the value of some property of |this| returns property's
165     * name. Otherwise returns empty string.
166     * @private
167     */
168    getPropertyNameOf_: function(value) {
169      for (var key in this)
170        if (this[key] === value)
171          return key;
172      return '';
173    }
174  };
175
176  Screen.CALLBACK_USER_ACTED = CALLBACK_USER_ACTED;
177
178  return {
179    Screen: Screen
180  };
181});
182
183cr.define('login', function() {
184  return {
185    /**
186     * Creates class and object for screen.
187     * Methods specified in EXTERNAL_API array of prototype
188     * will be available from C++ part.
189     * Example:
190     *     login.createScreen('ScreenName', 'screen-id', {
191     *       foo: function() { console.log('foo'); },
192     *       bar: function() { console.log('bar'); }
193     *       EXTERNAL_API: ['foo'];
194     *     });
195     *     login.ScreenName.register();
196     *     var screen = $('screen-id');
197     *     screen.foo(); // valid
198     *     login.ScreenName.foo(); // valid
199     *     screen.bar(); // valid
200     *     login.ScreenName.bar(); // invalid
201     *
202     * @param {string} name Name of created class.
203     * @param {string} id Id of div representing screen.
204     * @param {(function()|Object)} proto Prototype of object or function that
205     *     returns prototype.
206     */
207    createScreen: function(name, id, proto) {
208      if (typeof proto == 'function')
209        proto = proto();
210      cr.define('login', function() {
211        var api = proto.EXTERNAL_API || [];
212        for (var i = 0; i < api.length; ++i) {
213          var methodName = api[i];
214          if (typeof proto[methodName] !== 'function')
215            throw Error('External method "' + methodName + '" for screen "' +
216                name + '" not a function or undefined.');
217        }
218
219        var constructor = cr.ui.define(login.Screen);
220        constructor.prototype = Object.create(login.Screen.prototype);
221        Object.getOwnPropertyNames(proto).forEach(function(propertyName) {
222          var descriptor =
223              Object.getOwnPropertyDescriptor(proto, propertyName);
224          Object.defineProperty(constructor.prototype,
225              propertyName, descriptor);
226          if (api.indexOf(propertyName) >= 0) {
227            constructor[propertyName] = (function(x) {
228              return function() {
229                var screen = $(id);
230                return screen[x].apply(screen, arguments);
231              };
232            })(propertyName);
233          }
234        });
235        constructor.contextChanged = function() {
236          var screen = $(id);
237          screen.contextChanged_.apply(screen, arguments);
238        }
239        constructor.prototype.name = function() { return id; };
240        constructor.prototype.sendPrefix_ = 'login.' + name + '.';
241
242        constructor.register = function() {
243          var screen = $(id);
244          constructor.decorate(screen);
245          Oobe.getInstance().registerScreen(screen);
246        };
247
248        var result = {};
249        result[name] = constructor;
250        return result;
251      });
252    }
253  };
254});
255
256