PDA

View Full Version : Ext.ux.ButtonCalculator



magicfrog
18 Jun 2008, 7:45 AM
Hi

As promised, here is my very first extension.
I guess it s not the cleanest Extjs ux in the world but it works.
This is a toolbar button that pops up a calculator when clicked.
It also support keyboard typing.
(I extended Ext.Toolbar.Button because that was my need but maybe it would be more appropriate to extend Ext.Pane. I tried to but as i don t manage to make the KeyMap work i prefer posting a working stuff)
Hope that will help, and pls let me know about some way to improve it.




Ext.ns('Ext.ux');
Ext.ux.ButtonCalculator = Ext.extend(Ext.Toolbar.Button, {

errorText: (this.errorText)?this.errorText:'Error',

typeVal: function(options) {
var key;
if(options.text) {
key = options.text;
} else {
key = options;
}
var field = this.field;
var value = field.getValue();

var isOperand = this.operands.indexOf(key) > -1;

// If last character is an operand and again operand is specified, ignore it
if(isOperand && this.lastIsOperand() && key!='-') {
return;
}

if(value=='0' || value==this.errorText) {
value = '';
}
result = value + key;
field.setValue(result);
},

lastIsOperand: function() {
var isOperand = false;
var value = this.field.getValue();
var lastChar = value.substr(value.length-1,1);
if(this.operands.indexOf(lastChar) > -1) {
isOperand = true;
}
return isOperand;
},

calculate: function() {
if(this.lastIsOperand()) {
return;
}
var field = this.field;
try {
var value = eval(field.getValue());
if (value) {
field.setValue(value);
} else {
throw this.errorText;
}
}
catch (ex) {
field.setValue(this.errorText);
}
},

resetVal: function() {
this.field.setValue('0');
},

operands: ['+','-','*','/'],

handler: function() {
var firstTime = false;

if(!this.calcWin) {
firstTime = true;

this.field = new Ext.form.Field({
colspan:5,
border:false,
xtype:'field',
readOnly:true,
value:'0',
width:180,
handler: null,
style: 'text-align:right; margin-bottom: 5px;'
});

var calcWin = new Ext.Window({
iconCls: (this.iconCls)?this.iconCls:null,
title: (this.title)?this.title:'Calculator',
resizable: false,
width: 204,
height: 152,
closeAction: 'hide',
bodyStyle: { padding: '5px' },
layout:'table',
layoutConfig:{columns: 5},
defaults: {layout: 'fit', xtype: 'button', minWidth: 35, handler: this.typeVal, scope: this},
items: [this.field,
{
text:'7'
},{
text:'8'
},{
text:'9'
},{
text:'/'
},{
text:'('
},{
text:'4'
},{
text:'5'
},{
text:'6'
},{
text:'*'
},{
text:')'
},{
text:'1'
},{
text:'2'
},{
text:'3'
},{
text:'-'
},{
text:'C',
handler: this.resetVal
},{
text:'0'
},{
text:'00'
},{
text:'.'
},{
text:'+'
},{
text:'=',
handler: this.calculate
}]
});
this.calcWin = calcWin;

}
this.calcWin.show();

if(firstTime) {
new Ext.KeyMap(
this.calcWin.el,
[
{
key:[40,41,42,43,45,46,47,48,49,50,51,52,53,54,55,56,57],
fn: function(key,e){
this.typeVal(String.fromCharCode(e.getKey()));
},
scope: this
},{
key:[8,127],
fn: function(key,e){
e.stopEvent();
this.resetVal();
},
scope: this
},{
key:[13],
fn: function(key,e){
e.stopEvent();
this.calculate();
},
scope: this
}
],
'keypress'
)
}
}
});

Ext.reg('calculator', Ext.ux.ButtonCalculator);
The component now suports title, iconCls and errorText external options :


{
xtype: 'calculator',
title: 'Calculatrice',
iconCls: 'btn_calculator',
errorText: 'Erreur',
}
Updated 19/06/08
- Original code replaced by durlabh's optimized version
- Added support for negative numbers
- Added iconCls & title config options

watrboy00
18 Jun 2008, 9:28 AM
Very nice...will have to try this out. Thanks.

mystix
18 Jun 2008, 9:30 AM
neat one :)

you might want to include


Ext.ns('Ext.ux');

right at the top just in case (for all who copy-paste blindly, of which i too am guilty of sometimes :">)

magicfrog
18 Jun 2008, 10:42 AM
Done ;)

durlabh
18 Jun 2008, 12:58 PM
Thanks for the great sample. I made some modifications to suit my own coding style. Just in case somebody finds it useful, here it is:



Ext.ns('Ext.ux');
Ext.ux.ButtonCalculator = Ext.extend(Ext.Toolbar.Button, {

errorText: 'Error',

typeVal: function(options) {
var key;
if(options.text) {
key = options.text;
} else {
key = options;
}
var field = this.field;
var value = field.getValue();

var isOperand = this.operands.indexOf(key) > -1;

// If last character is an operand and again operand is specified, ignore it
if(isOperand && this.lastIsOperand()) {
return;
}

if(value=='0' || value==this.errorText) {
value = '';
}
result = value + key;
field.setValue(result);
},

lastIsOperand: function() {
var isOperand = false;
var value = this.field.getValue();
var lastChar = value.substr(value.length-1,1);
if(this.operands.indexOf(lastChar) > -1) {
isOperand = true;
}
return isOperand;
},

calculate: function() {
if(this.lastIsOperand()) {
return;
}
var field = this.field;
try {
var value = eval(field.getValue());
if (value) {
field.setValue(value);
} else {
throw this.errorText;
}
}
catch (ex) {
field.setValue(this.errorText);
}
},

resetVal: function() {
this.field.setValue('0');
},

operands: ['+','-','*','/'],

handler: function() {
var firstTime = false;

if(!this.calcWin) {
firstTime = true;

this.field = new Ext.form.Field({
colspan:5,
border:false,
xtype:'field',
readOnly:true,
value:'0',
width:180,
handler: null,
style: 'text-align:right; margin-bottom: 5px;'
});

var calcWin = new Ext.Window({
resizable: false,
width: 204,
height: 152,
closeAction: 'hide',
bodyStyle: { padding: '5px' },
layout:'table',
layoutConfig:{columns: 5},
defaults: {layout: 'fit', xtype: 'button', minWidth: 35, handler: this.typeVal, scope: this},
items: [this.field,
{
text:'7'
},{
text:'8'
},{
text:'9'
},{
text:'/'
},{
text:'('
},{
text:'4'
},{
text:'5'
},{
text:'6'
},{
text:'*'
},{
text:')'
},{
text:'1'
},{
text:'2'
},{
text:'3'
},{
text:'-'
},{
text:'C',
handler: this.resetVal
},{
text:'0'
},{
text:'00'
},{
text:'.'
},{
text:'+'
},{
text:'=',
handler: this.calculate
}]
});
this.calcWin = calcWin;

}
this.calcWin.show();

if(firstTime) {
new Ext.KeyMap(
this.calcWin.el,
[
{
key:[40,41,42,43,45,46,47,48,49,50,51,52,53,54,55,56,57],
fn: function(key,e){
this.typeVal(String.fromCharCode(e.getKey()));
},
scope: this
},{
key:[8,127],
fn: function(key,e){
e.stopEvent();
this.resetVal();
},
scope: this
},{
key:[13],
fn: function(key,e){
e.stopEvent();
this.calculate();
},
scope: this
}
],
'keypress'
)
}
}
});

Ext.reg('calculator', Ext.ux.ButtonCalculator);


Some of the changes I did are:

I try to avoid Ext.getCmp and Ext.get as far as possible
Instead of lot of anonymous methods, I used 1 method for typeVal
Minor if blocks to avoid multiple operands in successionThanks!

ajaxvador
18 Jun 2008, 2:11 PM
good job =D>

mystix
18 Jun 2008, 5:48 PM
hi @dulabh, some questions to satisfy the cat in me:



I try to avoid Ext.getCmp and Ext.get as far as possible


why?




Minor if blocks to avoid multiple operands in succession


by "multiple operands in succession", did you mean


if ((a || b && (!c || !d)) || f || g || h) {
...
}

??

durlabh
18 Jun 2008, 8:27 PM
I try to avoid Ext.getCmp and Ext.get because if we are concatenating the Id, why not just keep a reference instead of creating an additional Id! Not to pin-point this specific code, but just for example sake, in original code, in many methods, we needed to access the text field. So, either we can do Ext.getCmp and Ext.get multiple times, or we can just refer it as this.field in our case now. If we use Ext.getCmp or Ext.get, even then, we'll need to know the instanceId and concatenate. In my opinion, this is particularly useful in complex object (having multiple Ext components).

Again, just my opinion and coding style. If you think I'm doing something wrong, please let me know.

As for multiple operands in succession I meant, if you try to enter "4+" and then press "+", calculator won't accept it as "4++" may not be evaluated properly if user presses 5 then making the string as "4++5".

mystix
18 Jun 2008, 8:48 PM
I try to avoid Ext.getCmp and Ext.get because if we are concatenating the Id, why not just keep a reference instead of creating an additional Id! Not to pin-point this specific code, but just for example sake, in original code, in many methods, we needed to access the text field. So, either we can do Ext.getCmp and Ext.get multiple times, or we can just refer it as this.field in our case now. If we use Ext.getCmp or Ext.get, even then, we'll need to know the instanceId and concatenate. In my opinion, this is particularly useful in complex object (having multiple Ext components).

makes sense.


As for multiple operands in succession I meant, if you try to enter "4+" and then press "+", calculator won't accept it as "4++" may not be evaluated properly if user presses 5 then making the string as "4++5".

got it.

thanks for explaining :)

magicfrog
18 Jun 2008, 9:53 PM
Hi

Thanks a lot for your improvements, that will help me for next time :).
Just one detail about "multiple operands in succession"
One small change can be done in multiple operand blocking :
The - character shouldn t be blocked because of negative values
As 3*-2 can be evaluated, that very small change :

if(isOperand && this.lastIsOperand() && key!='-') {
return;
}will allow negative values to be used.

mystix
18 Jun 2008, 10:10 PM
for that matter


3*+2

is also valid :-?

DigitalSkyline
19 Jun 2008, 10:35 AM
very nice UX... thanks for sharing... I'm sure this will come in handy some day in my billing apps.

durlabh
19 Jun 2008, 10:41 AM
for that matter


3*+2
is also valid :-?

It is. I just wanted to show we can validate as we type...

magicfrog
29 Jun 2008, 1:51 AM
BTW : no MR M+ MC... buttons for a good reason :
I think using brackets is a more intuitive and simple way to do the same thing ;)

dkwiebe
16 Jul 2008, 7:47 PM
This is great! I'm going to be using this in the accounting app I'm working on.