PDA

View Full Version : [3.0] Easy actions



Condor
24 Apr 2009, 7:28 AM
I've done a little experimenting with actions and found some shortcomings:
1. IMHO an action shouldn't be able to execute when it is disabled.
2. There is no easy way to update multiple actions (disabled/hidden) when a state changes.
3. There is a lot of coding required for standard grid actions.

I've come up with an experimental ActionManager and some default actions:

Ext.ns('Ext.ux.grid');
Ext.ux.ActionRegistry = function(){
var types = {};
return {
register: function(atype, cls){
types[atype] = cls;
cls.atype = atype;
},
create: function(config, defaultType){
return config.isAction ? config : new types[config.atype || defaultType](config);
}
};
}();
Ext.ux.Action = Ext.extend(Ext.Action, {
actionProps: [
'atype',
'handler',
'scope',
'tbar',
'bbar',
'buttons',
'cmenu',
'key',
'def'
],
constructor: function(config){
config = Ext.apply({}, config);
var props = this.actionProps;
for(var i = 0, len = props.length; i < len; i++){
var prop = props[i];
if(this[prop] === undefined){
this[prop] = config[prop];
}
delete config[prop];
}
Ext.ux.Action.superclass.constructor.call(this, Ext.apply(config, {
handler: this.execute,
scope: this
}));
},
setHandler: function(fn, scope){
this.handler = fn;
this.scope = scope;
},
execute: function(){
this.update();
if(this.isValid()){
this.handler.apply(this.scope || this, arguments);
}
},
isValid: function(){
return !this.isDisabled();
},
update: function(){
// empty
}
});
Ext.ux.ActionRegistry.register('action', Ext.ux.Action);
Ext.ux.ActionManager = Ext.extend(Object, {
defaultAction: 'action',
separatorRank: {
' ': 1,
'-': 2,
'separator': 2,
'->': 3
},
nonMenuSeparators: {
' ': '-',
'->': '-'
},
constructor: function(config){
Ext.apply(this, config);
var items = this.items;
this.items = [];
if(items){
this.add(items);
}
},
add: function(action){
if(arguments.length > 1 || Ext.isArray(action)){
var args = arguments.length > 1 ? arguments : action;
for(var i = 0, len = args.length; i < len; i++){
this.add(args[i]);
}
return;
}
if(typeof action != 'string'){
action = Ext.ux.ActionRegistry.create(action, this.defaultAction);
action.manager = this;
}
this.items.push(action);
return action;
},
callEach: function(fnName, args){
var items = this.items;
for(var i = 0, len = items.length; i < len; i++){
var item = items[i], fn;
if(typeof item != 'string' && (fn = item[fnName])){
fn.apply(item, args);
}
}
},
setDisabled: function(){
this.callEach('setDisabled', arguments);
},
setHidden: function(){
this.callEach('setHidden', arguments);
},
update: function(){
this.callEach('update', arguments);
},
findAction: function(target){
var items = this.items;
for(var i = 0, len = items.length; i < len; i++){
var item = items[i];
if(typeof item != 'string' && item[target]){
return item;
}
}
},
queryActions: function(target, includeSeparators, forMenu){
var result = [], items = this.items, rank = 0, separator;
for(var i = 0, len = items.length; i < len; i++){
var item = items[i];
if(typeof item == 'string'){
if(includeSeparators){
var irank = this.separatorRank[item] || 0;
if(rank < irank){
rank = irank;
separator = forMenu ? this.nonMenuSeparators[item] || item : item;
}
}
}else if(!target || item[target]){
if(rank > 0){
rank = 0;
result.push(separator);
}
result.push(item);
}
}
return result;
},
executeDefault: function(){
var action = this.findAction('def');
if(action){
action.execute();
}
}
});

Ext.ux.PanelActions = Ext.extend(Object, {
constructor: function(config){
this.actionMgr = new Ext.ux.ActionManager(config);
},
init: function(p){
var actionMgr = this.actionMgr,
tbar = actionMgr.queryActions('tbar', true),
bbar = actionMgr.queryActions('bbar', true),
btns = actionMgr.queryActions('button'),
akeys = actionMgr.queryActions('key');
actionMgr.panel = p;
if(tbar.length){
var topToolbar = p.topToolbar;
if(!topToolbar){
p.topToolbar = tbar;
p.elements += ',tbar';
}else if(Ext.isArray(topToolbar)){
p.topToolbar = topToolbar.concat(tbar);
}else if(topToolbar.add){
topToolbar.add.apply(topToolbar, tbar);
}else if(Ext.isArray(topToolbar.items)){
topToolbar.items = topToolbar.items.concat(tbar);
}
}
if(bbar.length){
var bottomToolbar = p.bottomToolbar;
if(!bottomToolbar){
p.bottomToolbar = bbar;
p.elements += ',bbar';
}else if(Ext.isArray(bottomToolbar)){
p.bottomToolbar = bottomToolbar.concat(bbar);
}else if(bottomToolbar.add){
bottomToolbar.add.apply(bottomToolbar, bbar);
}else if(Ext.isArray(bottomToolbar.items)){
bottomToolbar.items = bottomToolbar.items.concat(bbar);
}
}
if(btns.length){
var buttons = p.buttons;
if(!buttons){
p.buttons = btns;
}else if(Ext.isArray(buttons)){
p.buttons = buttons.concat(btns);
}
}
if(akeys.length){
var keys = p.keys;
if(!keys){
keys = p.keys = [];
}
for(var i = 0, len = akeys.length; i < len; i++){
var action = akeys[i], key = action.key;
if(!Ext.isObject(key) || Ext.isArray(key)){
key = {key: key};
}
keys.push(Ext.apply({
handler: action.execute,
scope: action
}, key));
}
console.log(p.keys);
}
p.on('render', actionMgr.update, actionMgr, {single: true});
}
});
Ext.preg('panelactions', Ext.ux.PanelActions);

Ext.ux.grid.GridActions = Ext.extend(Ext.ux.PanelActions, {
init: function(p){
Ext.ux.grid.GridActions.superclass.init.call(this, p);
var actionMgr = this.actionMgr,
cmenu = this.actionMgr.queryActions('cmenu', true, true);
p.on('rowdblclick', actionMgr.executeDefault, actionMgr);
p.getSelectionModel().on('selectionchange', actionMgr.update, actionMgr, {buffer: 50});
if(cmenu.length){
this.cmenu = new Ext.menu.Menu({
items: cmenu
});
p.on('rowcontextmenu', this.onRowContextMenu, this);
}
},
onRowContextMenu: function(grid, rowIndex, e){
e.stopEvent();
var selModel = grid.getSelectionModel();
if(!selModel.isSelected(rowIndex)){
selModel.selectRow(rowIndex);
}
this.cmenu.showAt(e.getXY());
}
});
Ext.preg('gridactions', Ext.ux.grid.GridActions);

Ext.ux.grid.SingleRowAction = Ext.extend(Ext.ux.Action, {
update: function(){
var sm = this.manager.panel.getSelectionModel();
this.setDisabled(sm.getCount() != 1);
},
getSelected: function(){
return this.manager.panel.getSelectionModel().getSelected();
}
});
Ext.ux.ActionRegistry.register('singlerow', Ext.ux.grid.SingleRowAction);
Ext.ux.grid.MultiRowAction = Ext.extend(Ext.ux.Action, {
update: function(){
var sm = this.manager.panel.getSelectionModel();
this.setDisabled(sm.getCount() < 1);
},
getSelections: function(){
return this.manager.panel.getSelectionModel().getSelections();
}
});
Ext.ux.ActionRegistry.register('multirow', Ext.ux.grid.MultiRowAction);
Ext.ux.grid.SelectAllAction = Ext.extend(Ext.ux.Action, {
constructor: function(config){
Ext.ux.grid.SelectAllAction.superclass.constructor.call(this, Ext.apply({
handler: this.selectAll
}, config));
},
update: function(){
var grid = this.manager.panel;
this.setDisabled(grid.getSelectionModel().getCount() >= grid.getStore().getCount());
},
selectAll: function(){
this.manager.panel.getSelectionModel().selectAll();
}
});
Ext.ux.ActionRegistry.register('selectall', Ext.ux.grid.SelectAllAction);

And here is an example of GridActions 'in action':
- All actions respond to the current grid selection.
- Actions are automatically placed on the top/buttom toolbar, buttons and/or the context menu.
- Shortcut keys are automatically added to the grid.

Ext.onReady(function() {
Ext.QuickTips.init();
var fields = [], data = [], columns = [];
for(var i = 0; i < 10; i++){
fields.push({name: 'field' + i});
var row = [];
for(var j = 0; j < 10; j++){
row.push(i * j);
}
data.push(row);
columns.push({dataIndex: 'field' + i, header: 'Field ' + i, align: 'right'});
}
var win = new Ext.Window({
title: 'ActionManager test',
width: 500,
height: 300,
layout: 'fit',
items: {
xtype: 'grid',
store: new Ext.data.SimpleStore({
fields: fields,
data: data
}),
columns: columns,
plugins: [{
ptype: 'gridactions',
items: [{
text: 'Close',
handler: function(){
var win = this.manager.panel.ownerCt;
win[win.closeAction]();
},
button: true
},{
atype: 'singlerow',
text: 'Details',
tooltip: 'Show row details (Ctrl-Space)',
handler: function(){
alert('Show details of record #' + this.getSelected().id);
},
tbar: true,
cmenu: true,
key: {key: ' '}
},'-',{
text: 'Insert',
tooltip: 'Insert row (Insert)',
handler: function(){
alert('Insert record');
},
tbar: true,
cmenu: true,
key: Ext.EventObject.INSERT
},{
atype: 'singlerow',
text: 'Edit',
tooltip: 'Edit current row (Enter)',
handler: function(){
alert('Edit record #' + this.getSelected().id);
},
def: true,
tbar: true,
cmenu: true,
key: Ext.EventObject.ENTER
},{
atype: 'multirow',
text: 'Delete',
tooltip: 'Delete selected rows (Delete)',
handler: function(){
alert('Delete ' + this.getSelections().length + ' records');
},
tbar: true,
cmenu: true,
key: Ext.EventObject.DELETE
},'->',{
atype: 'selectall',
text: 'Select all',
tooltip: 'Select all rows (Ctrl+A)',
tbar: true,
cmenu: true,
key: {key: 'A', ctrl: true}
}]
}]
}
}).show();
});

Let me know what you think...

jay@moduscreate.com
24 Apr 2009, 10:56 AM
I like the concept dude. I just need to find a place to test it one of my client's applications ;)

chemist458
28 Apr 2009, 11:28 PM
Dear Condor,
I have chucked it into one of my grids, it works well, thanks for that
George

Further edit - Small change and thats very small, you left a console.log on line 211

deanna
30 Apr 2009, 8:24 AM
I may coop some of this if I may. I also use an action Manager. But I have an additional requirement to name the actions. I have some out of stream messages sent from the server to the client (such as new mail notifications, chat requests, ...) and these have actions associated with them. An out of stream message is basically an action name and a data block. I debated on creating a parallel concept to actions for this, but decided there was enough overlap to just extend actions. The action name is also need because when internationalizing the application, the text field can be anything.

Condor
30 Apr 2009, 11:19 AM
I would recommend using the itemId config option to name an action, because itemId is also used to identify all buttons and menu items created from the action.

deanna
30 Apr 2009, 2:14 PM
I don't see itemID as a config option to action. Also I need a a find option in the action manager to find the action based on the name/id.


On edit: Okay, I see itemID in the Action.js file, but it didn't make it into the documentation. Would that be a documentation bug?

Condor
30 Apr 2009, 10:10 PM
1. It's itemId (small d).
2. Yes, it's missing from the Action API docs.
3. But the reason I recommended it is the fact that the same itemId is used as a config option in Component (see API docs (http://extjs.com/deploy/ext-3.0-rc1/docs/?class=Ext.Component&member=itemId)).

Sesshomurai
2 May 2009, 6:41 AM
I worked something like this into my fusion framework, but one thing I noticed.

I made a subclass of Action that has some methods on it. I add the action to menus, buttons, etc. The 'action' object that is passed back through the handler is not the subclass I created.

Is the action object copied behind the scenes and the registered instance lost?

thanks.

Condor
2 May 2009, 6:58 AM
Are we talking about a class that descends from Ext.Action or from my Ext.ux.Action?

A normal Ext.Action handler has no knowledge of the action. 'this' will be the menu item or button that activated the handler.
Ext.ux.Action replaces the handler with it's own execute method and scopes it to the action, so 'this' will refer to the action.

Sesshomurai
2 May 2009, 8:07 PM
I declare a subclass as such:



FusionAction = Ext.extend(Ext.Action, ....


and then use instances of that to create Buttons etc,
The handler callback inside the Action is not the same instance and not the typeof my subclass.

I probably don't understand the behind-the-scenes but I was expecting the action passed to the handler to be of FusionAction type since that was the instance used.

Condor
3 May 2009, 2:57 AM
If you use my code and extend from Ext.ux.Action the handler will be scoped you your action instance ('this' == your action).

Sesshomurai
5 May 2009, 12:10 PM
It would be nice if many of the Ext objects, especially Action allowed for setUserObject() or something. Many other frameworks allows this to associate business objects with the UI, but its rather hard to do in Ext at the moment.

Also, I'm sure your code works, but the basic principle of sublcassing Action, creating an instance and receiving that instance of the subclass in the handler should work regardless - its basic polymophism.

Only if the instance is duped or copied behind the scenes would the subclassing attributes be lost. And that's not good.

EDIT: I was able to work around my issue by using the scope parameter when registering handlers with actions.

christophe.geiser
29 Sep 2010, 7:31 AM
very useful feature

Just need to replace
p.topToolbar = tbar; with
p.topToolbar = new Ext.Toolbar(tbar); for me to work properly under 3.3. (and same with bbar, resp line 165 and 179)

Cheers
C.