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