blob: 960632a6e25496beac66bb7193a772b4a5259e5b [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Base class for all login WebUI screens.
*/
cr.define('login', function() {
/** @const */ var CALLBACK_CONTEXT_CHANGED = 'contextChanged';
/** @const */ var CALLBACK_USER_ACTED = 'userActed';
function doNothing() {};
function alwaysTruePredicate() { return true; }
var querySelectorAll = HTMLDivElement.prototype.querySelectorAll;
var Screen = function(sendPrefix) {
this.sendPrefix_ = sendPrefix;
this.screenContext_ = null;
this.contextObservers_ = {};
};
Screen.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* Prefix added to sent to Chrome messages' names.
*/
sendPrefix_: null,
/**
* Context used by this screen.
*/
screenContext_: null,
get context() {
return this.screenContext_;
},
/**
* Dictionary of context observers that are methods of |this| bound to
* |this|.
*/
contextObservers_: null,
/**
* Called during screen initialization.
*/
decorate: doNothing,
/**
* Returns minimal size that screen prefers to have. Default implementation
* returns current screen size.
* @return {{width: number, height: number}}
*/
getPreferredSize: function() {
return {width: this.offsetWidth, height: this.offsetHeight};
},
/**
* Called for currently active screen when screen size changed.
*/
onWindowResize: doNothing,
/**
* @final
*/
initialize: function() {
return this.initializeImpl_.apply(this, arguments);
},
/**
* @final
*/
send: function() {
return this.sendImpl_.apply(this, arguments);
},
/**
* @final
*/
addContextObserver: function() {
return this.addContextObserverImpl_.apply(this, arguments);
},
/**
* @final
*/
removeContextObserver: function() {
return this.removeContextObserverImpl_.apply(this, arguments);
},
/**
* @final
*/
commitContextChanges: function() {
return this.commitContextChangesImpl_.apply(this, arguments);
},
/**
* Creates and returns new button element with given identifier
* and on-click event listener, which sends notification about
* user action to the C++ side.
*
* @param {string} id Identifier of a button.
* @param {string} opt_action_id Identifier of user action.
* @final
*/
declareButton: function(id, opt_action_id) {
var button = this.ownerDocument.createElement('button');
button.id = id;
this.declareUserAction(button,
{ action_id: opt_action_id,
event: 'click'
});
return button;
},
/**
* Adds event listener to an element which sends notification
* about event to the C++ side.
*
* @param {Element} element An DOM element
* @param {Object} options A dictionary of optional arguments:
* {string} event: name of event that will be listened,
* default: 'click'.
* {string} action_id: name of an action which will be sent to
* the C++ side.
* {function} condition: a one-argument function which takes
* event as an argument, notification is sent to the
* C++ side iff condition is true, default: constant
* true function.
* @final
*/
declareUserAction: function(element, options) {
var self = this;
options = options || {};
var event = options.event || 'click';
var action_id = options.action_id || element.id;
var condition = options.condition || alwaysTruePredicate;
element.addEventListener(event, function(e) {
if (condition(e))
self.sendImpl_(CALLBACK_USER_ACTED, action_id);
e.stopPropagation();
});
},
/**
* @override
* @final
*/
querySelectorAll: function() {
return this.querySelectorAllImpl_.apply(this, arguments);
},
/**
* Does the following things:
* * Creates screen context.
* * Looks for elements having "alias" property and adds them as the
* proprties of the screen with name equal to value of "alias", i.e. HTML
* element <div alias="myDiv"></div> will be stored in this.myDiv.
* * Looks for buttons having "action" properties and adds click handlers
* to them. These handlers send |CALLBACK_USER_ACTED| messages to
* C++ with "action" property's value as payload.
* @private
*/
initializeImpl_: function() {
if (cr.isChromeOS)
this.screenContext_ = new login.ScreenContext();
this.decorate();
this.querySelectorAllImpl_('[alias]').forEach(function(element) {
var alias = element.getAttribute('alias');
if (alias in this)
throw Error('Alias "' + alias + '" of "' + this.name() + '" screen ' +
'shadows or redefines property that is already defined.');
this[alias] = element;
this[element.getAttribute('alias')] = element;
}, this);
var self = this;
this.querySelectorAllImpl_('button[action]').forEach(function(button) {
button.addEventListener('click', function(e) {
var action = this.getAttribute('action');
self.send(CALLBACK_USER_ACTED, action);
e.stopPropagation();
});
});
},
/**
* Sends message to Chrome, adding needed prefix to message name. All
* arguments after |messageName| are packed into message parameters list.
*
* @param {string} messageName Name of message without a prefix.
* @param {...*} varArgs parameters for message.
* @private
*/
sendImpl_: function(messageName, varArgs) {
if (arguments.length == 0)
throw Error('Message name is not provided.');
var fullMessageName = this.sendPrefix_ + messageName;
var payload = Array.prototype.slice.call(arguments, 1);
chrome.send(fullMessageName, payload);
},
/**
* Starts observation of property with |key| of the context attached to
* current screen. This method differs from "login.ScreenContext" in that
* it automatically detects if observer is method of |this| and make
* all needed actions to make it work correctly. So it's no need for client
* to bind methods to |this| and keep resulting callback for
* |removeObserver| call:
*
* this.addContextObserver('key', this.onKeyChanged_);
* ...
* this.removeContextObserver('key', this.onKeyChanged_);
* @private
*/
addContextObserverImpl_: function(key, observer) {
var realObserver = observer;
var propertyName = this.getPropertyNameOf_(observer);
if (propertyName) {
if (!this.contextObservers_.hasOwnProperty(propertyName))
this.contextObservers_[propertyName] = observer.bind(this);
realObserver = this.contextObservers_[propertyName];
}
this.screenContext_.addObserver(key, realObserver);
},
/**
* Removes |observer| from the list of context observers. Supports not only
* regular functions but also screen methods (see comment to
* |addContextObserver|).
* @private
*/
removeContextObserverImpl_: function(observer) {
var realObserver = observer;
var propertyName = this.getPropertyNameOf_(observer);
if (propertyName) {
if (!this.contextObservers_.hasOwnProperty(propertyName))
return;
realObserver = this.contextObservers_[propertyName];
delete this.contextObservers_[propertyName];
}
this.screenContext_.removeObserver(realObserver);
},
/**
* Sends recent context changes to C++ handler.
* @private
*/
commitContextChangesImpl_: function() {
if (!this.screenContext_.hasChanges())
return;
this.sendImpl_(CALLBACK_CONTEXT_CHANGED,
this.screenContext_.getChangesAndReset());
},
/**
* Calls standart |querySelectorAll| method and returns its result converted
* to Array.
* @private
*/
querySelectorAllImpl_: function(selector) {
var list = querySelectorAll.call(this, selector);
return Array.prototype.slice.call(list);
},
/**
* Called when context changes are recieved from C++.
* @private
*/
contextChanged_: function(diff) {
this.screenContext_.applyChanges(diff);
},
/**
* If |value| is the value of some property of |this| returns property's
* name. Otherwise returns empty string.
* @private
*/
getPropertyNameOf_: function(value) {
for (var key in this)
if (this[key] === value)
return key;
return '';
}
};
Screen.CALLBACK_USER_ACTED = CALLBACK_USER_ACTED;
return {
Screen: Screen
};
});
cr.define('login', function() {
return {
/**
* Creates class and object for screen.
* Methods specified in EXTERNAL_API array of prototype
* will be available from C++ part.
* Example:
* login.createScreen('ScreenName', 'screen-id', {
* foo: function() { console.log('foo'); },
* bar: function() { console.log('bar'); }
* EXTERNAL_API: ['foo'];
* });
* login.ScreenName.register();
* var screen = $('screen-id');
* screen.foo(); // valid
* login.ScreenName.foo(); // valid
* screen.bar(); // valid
* login.ScreenName.bar(); // invalid
*
* @param {string} name Name of created class.
* @param {string} id Id of div representing screen.
* @param {(function()|Object)} proto Prototype of object or function that
* returns prototype.
*/
createScreen: function(name, id, template) {
if (typeof template == 'function')
template = template();
var apiNames = template.EXTERNAL_API || [];
for (var i = 0; i < apiNames.length; ++i) {
var methodName = apiNames[i];
if (typeof template[methodName] !== 'function')
throw Error('External method "' + methodName + '" for screen "' +
name + '" not a function or undefined.');
}
function checkPropertyAllowed(propertyName) {
if (propertyName.charAt(propertyName.length - 1) === '_' &&
(propertyName in login.Screen.prototype)) {
throw Error('Property "' + propertyName + '" of "' + id + '" ' +
'shadows private property of login.Screen prototype.');
}
};
var Constructor = function() {
login.Screen.call(this, 'login.' + name + '.');
};
Constructor.prototype = Object.create(login.Screen.prototype);
var api = {};
Object.getOwnPropertyNames(template).forEach(function(propertyName) {
if (propertyName === 'EXTERNAL_API')
return;
checkPropertyAllowed(propertyName);
var descriptor =
Object.getOwnPropertyDescriptor(template, propertyName);
Object.defineProperty(Constructor.prototype, propertyName, descriptor);
if (apiNames.indexOf(propertyName) >= 0) {
api[propertyName] = function() {
var screen = $(id);
return screen[propertyName].apply(screen, arguments);
};
}
});
Constructor.prototype.name = function() { return id; };
api.contextChanged = function() {
var screen = $(id);
screen.contextChanged_.apply(screen, arguments);
}
api.register = function(opt_lazy_init) {
var screen = $(id);
screen.__proto__ = new Constructor();
if (opt_lazy_init !== undefined && opt_lazy_init)
screen.deferredInitialization = function() { screen.initialize(); }
else
screen.initialize();
Oobe.getInstance().registerScreen(screen);
};
cr.define('login', function() {
var result = {};
result[name] = api;
return result;
});
}
};
});