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