/** * 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; } } });