/* This file gets loaded along with the rest of Ext library at the initial load */ Ext.BLANK_IMAGE_URL = "/extjs/resources/images/default/s.gif"; Ext.namespace('Ext.netzke'); // namespace for extensions that depend on Ext Ext.namespace('Netzke'); // namespace for extensions that do not depend on Ext Ext.netzke.cache = {}; Ext.QuickTips.init(); // seems obligatory in Ext v2.2.1, otherwise Ext.Component#destroy() stops working properly // To comply with Rails' forgery protection Ext.Ajax.extraParams = { authenticity_token : Ext.authenticityToken }; // Type detection functions Netzke.isObject = function(o) { return (o != null && typeof o == "object" && o.constructor.toString() == Object.toString()); } // Some Ruby-ish String extensions // from http://code.google.com/p/inflection-js/ String.prototype.camelize=function(lowFirstLetter) { var str=this.toLowerCase(); var str_path=str.split('/'); for(var i=0;i<str_path.length;i++) { var str_arr=str_path[i].split('_'); var initX=((lowFirstLetter&&i+1==str_path.length)?(1):(0)); for(var x=initX;x<str_arr.length;x++) str_arr[x]=str_arr[x].charAt(0).toUpperCase()+str_arr[x].substring(1); str_path[i]=str_arr.join(''); } str=str_path.join('::'); return str; }; String.prototype.capitalize=function() { var str=this.toLowerCase(); str=str.substring(0,1).toUpperCase()+str.substring(1); return str; }; String.prototype.humanize=function(lowFirstLetter) { var str=this.toLowerCase(); str=str.replace(new RegExp('_id','g'),''); str=str.replace(new RegExp('_','g'),' '); if(!lowFirstLetter)str=str.capitalize(); return str; }; // Implementation of totalProperty, successProperty and root configuration options for ArrayReader Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, { readRecords : function(o){ var sid = this.meta ? this.meta.id : null; var recordType = this.recordType, fields = recordType.prototype.fields; var records = []; var root = o[this.meta.root] || o, totalRecords = o[this.meta.totalProperty], success = o[this.meta.successProperty]; for(var i = 0; i < root.length; i++){ var n = root[i]; var values = {}; var id = ((sid || sid === 0) && n[sid] !== undefined && n[sid] !== "" ? n[sid] : null); for(var j = 0, jlen = fields.length; j < jlen; j++){ var f = fields.items[j]; var k = f.mapping !== undefined && f.mapping !== null ? f.mapping : j; var v = n[k] !== undefined ? n[k] : f.defaultValue; v = f.convert(v, n); values[f.name] = v; } var record = new recordType(values, id); record.json = n; records[records.length] = record; } return { records : records, totalRecords : totalRecords, success : success }; } }); // Properties/methods common to all widget classes Ext.widgetMixIn = { height: 400, width: 800, border: false, is_netzke: true, // to distinguish Netzke components from regular Ext components /* Loads aggregatee into a container. */ loadAggregatee: function(params){ // params that will be provided for the server API call (load_aggregatee_with_cache); all what's passed in params.params is merged in var apiParams = Ext.apply({id: params.id, container: params.container}, params.params); // build the cached widgets list to send it to the server var cachedWidgetNames = []; for (name in Ext.netzke.cache) { cachedWidgetNames.push(name); } apiParams.cache = Ext.encode(cachedWidgetNames); // remember the passed callback for the future if (params.callback) { this.callbackHash[params.id] = params.callback; // per loaded widget, as there may be simultaneous calls } // visually disable the container while the widget is being loaded // Ext.getCmp(params.container).disable(); Ext.getCmp(params.container).removeChild(); // remove the old widget // do the remote API call this.loadAggregateeWithCache(apiParams); }, /* Called by the server as callback about loaded widget */ widgetLoaded : function(params){ if (this.fireEvent('widgetload')) { // Enable the container after the widget is succesfully loaded // this.getChildWidget(params.id).ownerCt.enable(); // provide the callback to that widget that was loading the child, passing the child itself var callbackFn = this.callbackHash[params.id.camelize(true)]; if (callbackFn) { callbackFn.call(params.scope || this, this.getChildWidget(params.id)); delete this.callbackHash[params.id.camelize(true)]; } } }, /* Returns the parent widget */ getParent: function(){ // simply cutting the last part of the id: some_parent__a_kid__a_great_kid => some_parent__a_kid var idSplit = this.id.split("__"); idSplit.pop(); var parentId = idSplit.join("__"); return parentId === "" ? null : Ext.getCmp(parentId); }, /* Reloads current widget (calls the parent to reload it as its aggregatee) */ reload : function(){ var parent = this.getParent(); if (parent) { parent.loadAggregatee({id:this.localId(parent), container:this.ownerCt.id}); } else { window.location.reload(); } }, /* Gets id in the context of provided parent. For example, the widgets "properties", being a child of "books" has global id "books__properties", which *is* its widegt's real id. This methods, with the instance of "books" passed as parameter, returns "properties". */ localId : function(parent){ return this.id.replace(parent.id + "__", ""); }, /* Instantiates and inserts a widget into a container with layout 'fit'. Arg: an JS object with the following keys: - id: id of the receiving container - config: configuration of the widget to be instantiated and inserted into the container */ renderWidgetInContainer : function(params){ var cont = Ext.getCmp(params.container); cont.instantiateChild(params.config); }, /* Reconfigures the widget */ reconfigure: function(config){ this.ownerCt.instantiateChild(config) }, /* Evaluates CSS */ css : function(code){ var linkTag = document.createElement('style'); linkTag.type = 'text/css'; linkTag.innerHTML = code; document.body.appendChild(linkTag); }, /* Evaluates JS */ js : 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 widget can provide any set of commands to its client side. Args: - instructions: array of methods, in the order of execution. Each item is an object in one of the following 2 formats: 1) {method1:args1, method2:args2}, where methodN is a name of a public method of this widget; these methods are called in no particular order 2) {widget:widget_id, methods:arrayOfMethods}, used for recursive call to bulkExecute on some child widget Example: - [ // the same as this.feedback("Your order is accepted") {feedback: "You order is accepted"}, // the same as this.getChildWidget('users').bulkExecute([{setTitle:'Suprise!'}, {setDisabled:true}]) {widget:'users', methods:[{setTitle:'Suprise!'}, {setDisabled:true}] }, // ... etc: {updateStore:{records:[[1, 'Name1'],[2, 'Name2']], total:10}}, {setColums:[{},{}]}, {setMenus:[{},{}]}, ... ] */ bulkExecute : function(instructions){ if (Ext.isArray(instructions)) { Ext.each(instructions, function(instruction){ this.bulkExecute(instruction)}, this); } else { for (var instr in instructions) { if (this[instr]) { this[instr].apply(this, [instructions[instr]]); } else { var childWidget = this.getChildWidget(instr); if (childWidget) { childWidget.bulkExecute(instructions[instr]); } else { throw "Unknown method or child widget '" + instr +"' in widget '" + this.id + "'" } } } } }, // Get the child widget getChildWidget : function(id){ return id === 'parent' ? this.getParent() : Ext.getCmp(this.id+"__"+id); }, // Common handler for actions actionHandler : function(action){ // If firing corresponding event doesn't return false, call the handler if (this.fireEvent(action.name+'click', action)) { this[(action.fn || action.name)](action); } }, // Common handler for tools toolActionHandler : function(tool){ // If firing corresponding event doesn't return false, call the handler if (this.fireEvent(tool.id+'click')) { this[tool](); } }, // Does the call to the server and processes the response callServer : function(intp, params, callback, scope){ if (!params) params = {}; Ext.Ajax.request({ params : params, url : this.id + "__" + intp, callback : function(options, success, response){ if (success) { // execute commands from server this.bulkExecute(Ext.decode(response.responseText)); // provade callback if needed if (typeof callback == 'function') { if (!scope) scope = this; callback.apply(scope); } } }, scope : this }); }, /* Parse the bbar and tbar (both Arrays), replacing the strings with the corresponding methods. For example: replaceStringsWithActions( ['add', {text:'Menu', menu:['edit', 'delete']}] ) => [scope.actions['add'], {text:'Menu', menu:[scope.actions['edit'], scope.actions['delete']]}] */ normalizeMenuItems: function(arry, scope){ var res = []; // new array Ext.each(arry, function(o){ if (typeof o === "string") { var camelized = o.camelize(true); if (scope.actions[camelized]){ res.push(scope.actions[camelized]); } else { // if there's no action with this name, maybe it's a separator or something res.push(o); } } else if (Netzke.isObject(o)) { // look inside the objects... for (var key in o) { if (Ext.isArray(o[key])) { // ... and recursively process inner arrays found o[key] = this.normalizeMenuItems(o[key], scope); } } res.push(o); } }, this); return res; }, // Every Netzke widget commonBeforeConstructor : function(config){ this.actions = {}; // Generate methods for api points if (!config.api) { config.api = []; } config.api.push('load_aggregatee_with_cache'); // all netzke widgets get this API Ext.each(config.api, function(intp){ this[intp.camelize(true)] = function(args, callback, scope){ this.callServer(intp, args, callback, scope); } }, this); // Create Ext.Actions based on config.actions if (config.actions) { this.testActions = {}; for (var name in config.actions) { // Create an event for each action (so that higher-level widgets could interfere) this.addEvents(name+'click'); // Configure the action var actionConfig = config.actions[name]; actionConfig.handler = this.actionHandler.createDelegate(this); actionConfig.name = name; this.actions[name] = new Ext.Action(actionConfig); } config.bbar = config.bbar && this.normalizeMenuItems(config.bbar, this); config.tbar = config.tbar && this.normalizeMenuItems(config.tbar, this); config.menu = config.menu && this.normalizeMenuItems(config.menu, this); config.contextMenu = config.contextMenu && this.normalizeMenuItems(config.contextMenu, this); // TODO: need to rethink this action related stuff config.actions = this.actions; } // Normalize tools if (config.tools) { var normTools = []; Ext.each(config.tools, function(tool){ // Create an event for each action (so that higher-level widgets could interfere) this.addEvents(tool.id+'click'); var handler = this.toolActionHandler.createDelegate(this, [tool]); normTools.push({id : tool, handler : handler, scope : this}); }, this); config.tools = normTools; } // Set title if (!config.title) config.title = config.id.humanize(); }, // At this moment component is fully initializied commonAfterConstructor : function(config){ // From everywhere accessible FeedbackGhost this.feedbackGhost = Ext.getCmp('feedback_ghost'); // Add the menus if (this.initialConfig.menu) {this.addMenu(this.initialConfig.menu, this);} // generic events this.addEvents( 'widgetload' // fired when a child is dynamically loaded ); // Cleaning up on destroy this.on('beforedestroy', function(){ this.cleanUpMenu(); }, this); this.callbackHash = {}; if (this.afterConstructor) this.afterConstructor(config); }, feedback:function(msg){ if (this.initialConfig && this.initialConfig.quiet) { return false; } if (this.feedbackGhost) { this.feedbackGhost.showFeedback(msg); } else { // there's no application to show the feedback - so, we do it ourselves if (typeof msg == 'string'){ alert(msg); } else { var compoundResponse = ""; Ext.each(msg, function(m){ compoundResponse += m.msg + "\n" }); if (compoundResponse != "") { alert(compoundResponse); } } } }, addMenu : function(menu, owner){ if (!owner) { owner = this; } if (!!this.hostMenu) { this.hostMenu(menu, owner); } else { if (this.ownerWidget) { this.ownerWidget.addMenu(menu, owner); } } }, cleanUpMenu : function(owner){ if (!owner) { owner = this; } if (!!this.unhostMenu) { this.unhostMenu(owner); } else { if (this.ownerWidget) { this.ownerWidget.cleanUpMenu(owner); } } }, onWidgetLoad:Ext.emptyFn // gets overridden }; // Netzke extensions for Ext.Container Ext.override(Ext.Container, { /** Get Netzke widget that this Ext.Container is part of (*not* the parent widget, for which call getParent) It searches up the Ext.Container hierarchy until it finds a Container that has isNetzke property set to true (or until it reaches the top). */ getOwnerWidget : function(){ if (this.initialConfig.isNetzke) { return this; } else { if (this.ownerCt){ return this.ownerCt.getOwnerWidget() } else { return null } } }, // Get the widget that we are hosting getWidget: function(){ return this.items ? this.items.get(0) : null; // need this check in case when the container is not yet rendered, like an inactive tab in the TabPanel }, removeChild : function(){ this.remove(this.getWidget()); }, instantiateChild : function(config){ this.remove(this.getWidget()); // first delete previous widget if (!config) return false; // simply remove current widget if null is passed var instance = new Ext.netzke.cache[config.widgetClassName](config); this.add(instance); this.doLayout(); } });