javascripts/base.js
/**
* Client-side code for [Netzke::Base](http://www.rubydoc.info/github/netzke/netzke-core/Netzke/Base)
* @class Netzke.Base
*/
Ext.define("Netzke.Base", {
extend: 'Ext.Mixin',
mixinConfig: {
before: {
constructor: 'netzkeBeforeConstructor',
initComponent: 'netzkeBeforeInitComponent'
},
after: {
constructor: 'netzkeAfterConstructor',
initComponent: 'netzkeAfterInitComponent'
}
},
/**
* This is `true` for all Netzke components.
* @property isNetzke
* @type boolean
*/
isNetzke: true,
/**
* Override this property globally if you to use a custom notifier class.
* @property netzkeNotifier
* @type Netzke.Notifier
*/
netzkeNotifier: Ext.create('Netzke.Notifier'),
/**
* Called before constructor. Implements all kinds of Netzke component initializations. Override as needed.
* @method netzkeBeforeConstructor
* @param config {Object} Passed configuration
*/
netzkeBeforeConstructor: function(config){
this.server = {}; // namespace for endpoint functions
this.netzkeComponents = config.netzkeComponents;
this.passedConfig = config;
this.netzkeProcessEndpoints(config);
this.netzkeProcessPlugins(config);
this.netzkeNormalizeActions(config);
this.netzkeNormalizeConfig(config);
this.serverConfig = config.clientConfig || {};
},
/**
* Called after constructor. Override as needed.
* @method netzkeAfterConstructor
* @param config {Object} Passed configuration
*/
netzkeAfterConstructor: function(config){
},
/**
* Called before `initComponent`. Override as needed.
* @method netzkeBeforeInitComponent
*/
netzkeBeforeInitComponent: function(){
},
/**
* Called after `initComponent`. Override as needed.
* @method netzkeAfterInitComponent
*/
netzkeAfterInitComponent: function(){
},
/**
* Evaluates CSS passed from the server.
* @method netzkeEvalCss
* @param code {String} CSS code
*/
netzkeEvalCss : function(code){
var head = Ext.fly(document.getElementsByTagName('head')[0]);
Ext.core.DomHelper.append(head, {
tag: 'style',
type: 'text/css',
html: code
});
},
/**
* Evaluates Javascript passed from the server.
* @method netzkeEvalJs
* @param code {String} Javascript code
*/
netzkeEvalJs : function(code){
eval(code);
},
/**
* Executes a bunch of methods. This method is called almost every time a communication to the server takes place.
* Thus the server side of a component can provide any set of commands to its client side.
*
* @method netzkeBulkExecute
*
* @param {Array|Object} instructions
* 1) a hash of instructions, where the key is the method name, and value - the argument that method will be called with (thus, these methods are expected to *only* receive 1 argument). In this case, the methods will be executed in no particular order.
* 2) an array of hashes of instructions. They will be executed in order.
* Arrays and hashes may be nested at will.
* If the key in the instructions hash refers to a child Netzke component, netzkeBulkExecute will be called on that component with the value passed as the argument.
* @example
*
* // executes as this.feedback("Your order is accepted");
* {feedback: "You order is accepted"}
* // executes as: this.setTitle('Suprise!'); this.setDisabled(true);
* [{setTitle:'Suprise!'}, {setDisabled:true}]
* // executes as: this.netzkeGetComponent('users').netzkeBulkExecute([{setTitle:'Suprise!'}, {setDisabled:true}]);
* {users: [{setTitle:'Suprise!'}, {setDisabled:true}] }
*/
netzkeBulkExecute : function(instructions){
if (Ext.isArray(instructions)) {
Ext.each(instructions, function(instruction){ this.netzkeBulkExecute(instruction)}, this);
} else {
for (var instr in instructions) {
var args = instructions[instr];
if(args instanceof Object && (Ext.Object.getSize(args)==0))
args = [];
if (Ext.isFunction(this[instr])) {
// Executing the method.
this[instr].apply(this, args);
} else {
var childComponent = this.netzkeGetComponent(instr);
if (childComponent) {
childComponent.netzkeBulkExecute(args);
} else if (Ext.isArray(args)) { // only consider those calls that have arguments wrapped in an array; the only (probably) case when they are not, is with 'success' property set to true in a non-ajax form submit - silently ignore that
throw "Netzke: Unknown method or child component '" + instr + "' in component '" + this.path + "'"
}
}
}
}
},
/**
* Called by the server side to set the return value of an endpoint call; to be reworked.
* @method netzkeSetResult
* @param result {Any}
* @private
*/
netzkeSetResult: function(result) {
this.latestResult = result;
},
/**
* Called by the server when the component to which an endpoint call was directed to, is not in the session anymore.
* @method netzkeSessionExpired
* @private
*/
netzkeOnSessionExpired: function() {
this.netzkeSessionIsExpired = true;
this.netzkeOnSessionExpired();
},
/**
* Override this method to handle session expiration. E.g. you may want to inform the user that they will be redirected to the login page.
* @method onSessionExpired
* @private
*/
netzkeOnSessionExpired: function() {
Netzke.warning("Component not in session. Override `netzkeOnSessionExpired` to handle this.");
},
/**
* Returns a URL for old-fashion requests (used at multi-part form non-AJAX submissions).
* @method netzkeEndpointUrl
* @param endpoint {String}
*/
netzkeEndpointUrl: function(endpoint){
return Netzke.ControllerUrl + "dispatcher?address=" + this.id + "__" + endpoint;
},
/**
* Processes items.
* @method netzkeNormalizeConfigArray
* @param items {Array} Items
* @private
*/
netzkeNormalizeConfigArray: function(items){
var cfg, ref, cmpName, cmpCfg, actName, actCfg;
Ext.each(items, function(item, i){
cfg = item;
if (cfg.action) {
if (!this.actions[cfg.action]) throw "Netzke: unknown action " + cfg.action;
items[i] = this.actions[cfg.action];
} else if (cfg.netzkeComponent) {
// replace with component config
cmpName = cfg.netzkeComponent;
cmpCfg = this.netzkeComponents[cmpName.camelize(true)];
if (!cmpCfg) throw "Netzke: unknown component " + cmpName;
cmpCfg.netzkeParent = this;
items[i] = Ext.apply(cmpCfg, cfg);
} else if (Ext.isString(cfg) && Ext.isFunction(this[cfg.camelize(true)+"Config"])) { // replace with config referred to on the Ruby side as a symbol
// pre-built config
items[i] = Ext.apply(this[cfg.camelize(true)+"Config"](this.passedConfig), {netzkeParent: this});
} else {
// recursion
for (key in cfg) {
if (Ext.isArray(cfg[key])) {
this.netzkeNormalizeConfigArray(cfg[key]);
}
}
}
}, this);
},
/**
* Runs through initial config options and does the following:
*
* * detects component placeholders and replaces them with full component config found in `netzkeComponents`
* * detects action placeholders and replaces them with instances of Ext actions found in `this.actions`
*
* @method netzkeNormalizeConfig
* @param config {Object}
*/
netzkeNormalizeConfig: function(config) {
for (key in config) {
if (Ext.isArray(config[key])) this.netzkeNormalizeConfigArray(config[key]);
}
},
/**
* Dynamically creates methods for endpoints, so that we could later call them like: this.myEndpointMethod()
* @method netzkeProcessEndpoints
* @param config {Object}
*/
netzkeProcessEndpoints: function(config){
var endpoints = config.endpoints || [], that = this;
Ext.each(endpoints, function(methodName){
Netzke.directProvider.addRemotingMethodToComponent(config, methodName);
// define endpoint function
this.server[methodName] = function(){
var args = Array.prototype.slice.call(arguments), callback, serverConfigs, scope = that;
if (Ext.isFunction(args[args.length - 2])) {
scope = args.pop();
callback = args.pop();
}
if (Ext.isFunction(args[args.length - 1])) {
callback = args.pop();
}
var cfgs = that.netzkeBuildParentConfigs();
var remotingArgs = {args: args, configs: cfgs};
// call Direct function
that.netzkeGetDirectFunction(methodName).call(scope, remotingArgs, function(response, event) {
that.netzkeProcessDirectResponse(response, event, callback, scope);
}, that);
}
}, this);
},
/**
* TODO
* @method netzkeProcessDirectResponse
* @param response {Object}
* @param event {Object}
* @param callback {Function}
* @param scope {Object}
*/
netzkeProcessDirectResponse: function(response, event, callback, scope){
var callbackParams,
result; // endpoint response
// no server exception?
if (Ext.getClass(event) == Ext.direct.RemotingEvent) {
// process response and get endpoint return value
this.netzkeBulkExecute(response);
result = this.latestResult;
// endpoint returns an error?
if (result && result.error) {
this.netzkeHandleEndpointError(callback, result);
// no error
} else {
if (callback) callback.apply(scope, [result, true]) != false
}
// got Direct exception?
} else {
this.netzkeHandleDirectError(callback, event);
}
},
/**
* TODO
* @method netzkeHandleEndpointError
*/
netzkeHandleEndpointError: function(callback, result){
var shouldFireGlobalEvent = true;
if (callback) {
shouldFireGlobalEvent = callback.apply(this, [result.error, false]) != false;
}
if (shouldFireGlobalEvent) {
Netzke.GlobalEvents.fireEvent('endpointexception', result.error);
}
},
/**
* TODO
* @method netzkeHandleDirectError
* @param callback {Function}
* @param event {Object}
*/
netzkeHandleDirectError: function(callback, event){
var shouldFireGlobalEvent = true;
callbackParams = event;
callbackParams.type = 'DIRECT_EXCEPTION';
// First invoke the callback, and if that allows, call generic exception handler
if (callback) {
shouldFireGlobalEvent = callback.apply(this, [callbackParams, false]) != false;
}
if (shouldFireGlobalEvent) {
Netzke.GlobalEvents.fireEvent('endpointexception', callbackParams);
}
},
/**
* Returns direct function by endpoint name and optional component's config (if not provided, component's instance
* will be used instead)
* @method netzkeGetDirectFunction
* @param methodName {String}
* @param {Object} [config]
*/
netzkeGetDirectFunction: function(methodName, config) {
config = config || this;
return Netzke.remotingMethods[config.id][methodName];
},
/**
* Reversed array of server configs for each parent component up the tree
* @method netzkeBuildParentConfigs
*/
netzkeBuildParentConfigs: function() {
var res = [],
parent = this;
while (parent) {
var cfg = Ext.clone(parent.serverConfig);
res.unshift(cfg);
parent = parent.netzkeGetParentComponent();
}
return res;
},
/**
* Replaces actions configs with Ext.Action instances, assigning default handler to them
* @method netzkeNormalizeActions
* @param config {Object}
*/
netzkeNormalizeActions : function(config){
var normActions = {};
for (var name in config.actions) {
// Configure the action
var actionConfig = Ext.apply({}, config.actions[name]); // do not modify original this.actions
actionConfig.customHandler = actionConfig.handler;
actionConfig.handler = Ext.Function.bind(this.netzkeActionHandler, this); // handler common for all actions
actionConfig.name = name;
// instantiate Ext.Action
normActions[name] = new Ext.Action(actionConfig);
}
this.actions = normActions;
delete(config.actions);
},
/**
* Dynamically loads child Netzke component
* @method netzkeLoadComponent
* @param {String} name Component name as declared in the Ruby class with `component` DSL
* @param {Object} [options] May contain the following optional keys:
* * **container** {Ext.container.Container|Integer}
*
* The instance (or id) of a container with the "fit" layout where the loaded component will be added to; the previously existing component will be destroyed
*
* * **append** {Boolean}
*
* If set to `true`, do not clear the container before adding the loaded component
*
* * **configOnly** {Boolean}
*
* If set to `true`, do not instantiate/insert the component, instead pass its config to the callback function
*
* * **serverConfig** {Object}
*
* Config accessible inside the `component` DSL block as `client_config`; this allows reconfiguring child components by the client-side code
*
* * **callback** {Function}
*
* Function that gets called after the component is loaded; it receives the component's instance (or component config if `configOnly` is set) as parameter; if the function returns `false`, the loaded component will not be automatically inserted or (in case of window) shown.
*
* * **scope** {Object}
*
* Scope for the callback; defaults to the instance of the component.
*
* @example
*
* Loads 'info' and adds it to `this` container, removing anything from it first:
*
* this.netzkeLoadComponent('info');
*
* Loads 'info' and adds it to `win` container, envoking a callback in `this` scope, passing it an instance of 'info':
*
* this.netzkeLoadComponent('info', { container: win, callback: function(instance){} });
*
* Loads configuration for the 'info' component, envoking a callback in `this` scope, passing it the loaded config for 'info'.
*
* this.netzkeLoadComponent('info', { configOnly: true, callback: function(config){} });
*
* Loads two 'info' instances in different containers and with different configurations:
*
* this.netzkeLoadComponent('info', {
* container: 'tab1',
* serverConfig: { user: 'john' } // on the server: client_config[:user] == 'john'
* });
*
* this.netzkeLoadComponent('info', {
* container: 'tab2',
* serverConfig: { user: 'bill' } // on the server: client_config[:user] == 'bill'
* });
*/
netzkeLoadComponent: function(name, options){
var container, serverParams, containerEl;
options = options || {};
container = this.netzkeChooseContainer(options);
serverParams = this.netzkeBuildServerLoadingParams(name, options);
this.netzkeShowLoadingMask(container);
// Call the endpoint
this.server.deliverComponent(serverParams, function(result, success) {
this.netzkeHideLoadingMask(container);
if (success) {
this.netzkeHandleLoadingResponse(container, result, options);
} else {
this.netzkeHandleLoadingError(result);
}
});
},
/**
* Handles loading error
* @method netzkeHandleLoadingError
*/
netzkeHandleLoadingError: function(error){
this.netzkeNotify(error);
},
/**
* TODO
* @method netzkeBuildServerLoadingParams
*/
netzkeBuildServerLoadingParams: function(name, params) {
return Ext.apply(params.serverParams || {}, {
name: name,
client_config: params.serverConfig,
item_id: params.itemId || name, // TODO: make optional
cache: Netzke.cache.join() // coma-separated list of xtypes of already loaded classes
});
},
/**
* Decides, based on params passed to `netzkeLoadComponent`, what container the component should be loaded into.
* @method netzkeChooseContainer
* @param params Object
*/
netzkeChooseContainer: function(params) {
if (!params.container) return this;
return Ext.isString(params.container) ? Ext.getCmp(params.container) : params.container;
},
/**
* Handles regular server response (may include error)
* @method netzkeHandleLoadingResponse
*/
netzkeHandleLoadingResponse: function(container, result, params){
if (result.error) {
this.netzkeNotify(result.error);
} else {
this.netzkeProcessDeliveredComponent(container, result, params);
}
},
/**
* Processes delivered component
* @method netzkeProcessDeliveredComponent
*/
netzkeProcessDeliveredComponent: function(container, result, params){
var config = result.config, instance, doNotInsert, currentInstance;
config.netzkeParent = this;
this.netzkeEvalJs(result.js);
this.netzkeEvalCss(result.css);
if (params.configOnly) {
if (params.callback) params.callback.apply((params.scope || this), [config, params]);
} else {
// we must destroy eventual existing component with the same ID
currentInstance = Ext.getCmp(config.id);
if (currentInstance) currentInstance.destroy();
instance = Ext.create(config);
if (params.callback) {
doNotInsert = params.callback.apply((params.scope || this), [instance, params]) == false;
}
if (doNotInsert) return;
if (instance.isFloating()) { // windows are not containable
instance.show();
} else {
if (params.replace) {
this.netzkeReplaceChild(params.replace, instance)
} else {
if (!params.append) container.removeAll();
container.add(instance);
}
}
}
},
/**
* Masks container in which a child component is being loaded
* @method netzkeShowLoadingMask
*/
netzkeShowLoadingMask: function(container){
if (container.rendered) container.mask();
},
/**
* Unmasks loading container
* @method netzkeHideLoadingMask
*/
netzkeHideLoadingMask: function(container){
if (container.rendered) container.unmask();
},
/**
* Returns parent Netzke component
* @method netzkeGetParentComponent
*/
netzkeGetParentComponent: function(){
return this.netzkeParent;
},
/**
* Reloads itself by instructing the parent to call `netzkeLoadComponent`.
* Note: in order for this to work, the component must be nested in a container with the 'fit' layout.
* @method netzkeReload
*/
netzkeReload: function(){
var parent = this.netzkeGetParentComponent();
if (parent) {
parent.netzkeReloadChild(this);
} else {
window.location.reload();
}
},
/**
* Given child component and new serverConfig, reloads the component
* @method netzkeReloadChild
* @param child Netzke.Base
* @param serverConfig Object
*/
netzkeReloadChild: function(child, serverConfig){
this.netzkeLoadComponent(child.name, {
configOnly: true,
serverConfig: serverConfig,
callback: function(cfg) {
this.netzkeReplaceChild(child, cfg);
}
});
},
/**
* Replaces given (Netzke or Ext JS) component and new config, replaces former with latter, by instructing the parent
* component to re-insert the component at the same index. Override if you need something more fancy (e.g. active tab
* when it gets re-inserted)
* @method netzkeReplaceChild
* @param child {Netzke.Base}
* @param config {Obect}
*/
netzkeReplaceChild: function(child, config){
var parent = child.up();
if (!parent) return;
var index = parent.items.indexOf(child);
Ext.suspendLayouts();
parent.remove(child);
var res = parent.insert(index, config);
Ext.resumeLayouts(true);
return res;
},
/**
* Instantiates and returns a Netzke component by its name.
* @method netzkeInstantiateComponent
* @param name {String} Child component's name/itemId
*/
netzkeInstantiateComponent: function(name) {
name = name.camelize(true);
var cfg = this.netzkeComponents[name];
return Ext.createByAlias(this.netzkeComponents[name].alias, cfg)
},
/**
* Returns *instantiated* child component by its relative path
* @method netzkeGetComponent
* @param path {String} Component path, which may contain the 'parent' for walking up the hierarchy, e.g.
* `parent__sibling`. If this is empty, the method will return `this`.
*/
netzkeGetComponent: function(path){
if (path === "") {return this};
path = path.underscore();
var split = path.split("__"), res;
if (split[0] === 'parent') {
split.shift();
var childInParentScope = split.join("__");
res = this.netzkeGetParentComponent().netzkeGetComponent(childInParentScope);
} else {
res = Ext.getCmp(this.id+"__"+path);
}
return res;
},
/**
* Triggers a notification unless `quiet` config option is `true`.
* @method netzkeNotify
* @param {String} msg Notification body
* @param {Object} options Notification options (such as `title`, `delay`)
*/
netzkeNotify: function(msg, options){
if (this.quiet !== true) this.netzkeNotifier.msg(msg, options);
},
/**
* Common handler for all netzke's actions.
* @method netzkeActionHandler
* @param {Ext.Component} comp Component that triggered the action (e.g. button or menu item)
*/
netzkeActionHandler: function(comp){
var actionName = comp.name;
// If firing corresponding event doesn't return false, call the handler
if (this.fireEvent(actionName+'click', comp)) {
var action = this.actions[actionName];
var customHandler = action.initialConfig.customHandler;
var methodName = (customHandler && customHandler.camelize(true)) || "netzkeOn" + actionName.camelize();
if (!this[methodName]) {throw "Netzke: handler '" + methodName + "' is undefined in '" + this.id + "'";}
// call the handler passing it the triggering component
this[methodName](comp);
}
},
/**
* TODO
* @method netzkeProcessPlugins
*/
netzkeProcessPlugins: function(config) {
if (config.netzkePlugins) {
if (!this.plugins) this.plugins = [];
Ext.each(config.netzkePlugins, function(p){
this.plugins.push(this.netzkeInstantiateComponent(p));
}, this);
delete config.netzkePlugins;
}
}
});