PDA

View Full Version : Key mapping of Buttons in Toolbar.



Animal
5 May 2007, 2:04 AM
People keep asking for keyboard navigation, and it's an easy change to allow Toolbar buttons to include key specifications. I've already done it.

My additions to the Toolbar are in bold. Key specs are copied from button configs/objects into the Toolbar's KeyMap when added to the Toolbar.

The Toolbar must be configured with the focussable element to which to add the listener using the "for" config option.



/**
* @class Ext.Toolbar
* Basic Toolbar class.
* @constructor
* Creates a new Toolbar
* @param {String/HTMLElement/Element} container The id or element that will contain the toolbar
* @param {Array} buttons (optional) array of button configs or elements to add
* @param {Object} config The config object
*/
Ext.Toolbar = function(container, buttons, config){
if(container instanceof Array){ // omit the container for later rendering
buttons = container;
config = buttons;
container = null;
}
Ext.apply(this, config);
this.buttons = buttons;
if(container){
this.render(container);
}
};

Ext.Toolbar.prototype = {

render : function(ct){
this.el = Ext.get(ct);
if(this.cls){
this.el.addClass(this.cls);
}
// using a table allows for vertical alignment
this.el.update('<div class="x-toolbar x-small-editor"><table cellspacing="0"><tr></tr></table></div>');
this.tr = this.el.child("tr", true);
var autoId = 0;
this.items = new Ext.util.MixedCollection(false, function(o){
return o.id || ("item" + (++autoId));
});
if (this.for) {
this.keyMap = new Ext.KeyMap(this.for, []);
}
if(this.buttons){
this.add.apply(this, this.buttons);
delete this.buttons;
}
},

addItemToCollection: function(item) {
this.items.add(item);
if (item.key && this.keyMap) {
var keyFn = item.onClick || item.handleClick;
this.keyMap.addBinding({
stopEvent:true,
key: item.key,
shift: item.shift,
ctrl: item.ctrl,
alt: item.alt,
fn: function(k,e){
keyFn.call(this, e);
},
scope: item
});
}
return item;
},

/**
* Adds element(s) to the toolbar - this function takes a variable number of
* arguments of mixed type and adds them to the toolbar.
* @param {Mixed} arg1 If arg is a Toolbar.Button, it is added. If arg is a string, it is wrapped
* in a ytb-text element and added unless the text is "separator" in which case a separator
* is added. Otherwise, it is assumed the element is an HTMLElement and it is added directly.
* @param {Mixed} arg2
* @param {Mixed} etc
*/
add : function(){
var a = arguments, l = a.length;
for(var i = 0; i < l; i++){
var el = a[i];
if(el.applyTo){ // some kind of form field
this.addField(el);
}else if(el.render){ // some kind of Toolbar.Item
this.addItem(el);
}else if(typeof el == "string"){ // string
if(el == "separator" || el == "-"){
this.addSeparator();
}else if(el == " "){
this.addSpacer();
}else{
this.addText(el);
}
}else if(el.tagName){ // element
this.addElement(el);
}else if(typeof el == "object"){ // must be button config?
this.addButton(el);
}
}
},

/**
* Returns the element for this toolbar
* @return {Ext.Element}
*/
getEl : function(){
return this.el;
},

/**
* Adds a separator
* @return {Ext.Toolbar.Item} The separator item
*/
addSeparator : function(){
return this.addItem(new Ext.Toolbar.Separator());
},

/**
* Adds a spacer element
* @return {Ext.Toolbar.Item} The spacer item
*/
addSpacer : function(){
return this.addItem(new Ext.Toolbar.Spacer());
},

/**
* Adds any standard HTML element to the toolbar
* @param {String/HTMLElement/Element} el The element or id of the element to add
* @return {Ext.Toolbar.Item} The element's item
*/
addElement : function(el){
return this.addItem(new Ext.Toolbar.Item(el));
},

/**
* Adds any Toolbar.Item or subclass
* @param {Toolbar.Item} item
* @return {Ext.Toolbar.Item} The item
*/
addItem : function(item){
var td = this.nextBlock();
item.render(td);
return this.addItemToCollection(item);
},

/**
* Add a button (or buttons), see {@link Ext.Toolbar.Button} for more info on the config
* @param {Object/Array} config A button config or array of configs
* @return {Ext.Toolbar.Button/Array}
*/
addButton : function(config){
if(config instanceof Array){
var buttons = [];
for(var i = 0, len = config.length; i < len; i++) {
buttons.push(this.addButton(config[i]));
}
return buttons;
}
var b = config;
if(!(config instanceof Ext.Toolbar.Button)){
b = new Ext.Toolbar.Button(config);
}
var td = this.nextBlock();
b.render(td);
return this.addItemToCollection(b);
},

/**
* Adds text to the toolbar
* @param {String} text The text to add
* @return {Ext.Toolbar.Item} The element's item
*/
addText : function(text){
return this.addItem(new Ext.Toolbar.TextItem(text));
},

/**
* Inserts any Toolbar.Item/Toolbar.Button at the specified index
* @param {Number} index The index where the item is to be inserted
* @param {Object/Toolbar.Item/Toolbar.Button (may be Array)} item The button, or button config object to be inserted.
* @return {Ext.Toolbar.Button/Item}
*/
insertButton : function(index, item){
if(item instanceof Array){
var buttons = [];
for(var i = 0, len = item.length; i < len; i++) {
buttons.push(this.insertButton(index + i, item[i]));
}
return buttons;
}
if (!(item instanceof Ext.Toolbar.Button)){
item = new Ext.Toolbar.Button(item);
}
var td = document.createElement("td");
this.tr.insertBefore(td, this.tr.childNodes[index]);
item.render(td);
this.items.insert(index, item);
return this.addItemToCollection(item);
},

/**
* Adds a new element to the toolbar from the passed DomHelper config
* @param {Object} config
* @return {Ext.Toolbar.Item} The element's item
*/
addDom : function(config, returnEl){
var td = this.nextBlock();
Ext.DomHelper.overwrite(td, config);
var ti = new Ext.Toolbar.Item(td.firstChild);
ti.render(td);
this.items.add(ti);
return ti;
},

/**
* Add a dynamically rendered Ext.form field (TextField, ComboBox, etc). Note: the field should not have
* been rendered yet. For a field that has already been rendered, use addElement.
* @param {Field} field
* @return {ToolbarItem}
*/
addField : function(field){
var td = this.nextBlock();
field.render(td);
var ti = new Ext.Toolbar.Item(td.firstChild);
ti.render(td);
this.items.add(ti);
return ti;
},

// private
nextBlock : function(){
var td = document.createElement("td");
this.tr.appendChild(td);
return td;
}
};

/**
* @class Ext.Toolbar.Item
* The base class that other classes should extend in order to get some basic common toolbar item functionality.
* @constructor
* Creates a new Item
* @param {HTMLElement} el
*/
Ext.Toolbar.Item = function(el){
this.el = Ext.getDom(el);
this.id = Ext.id(this.el);
this.hidden = false;
};

Ext.Toolbar.Item.prototype = {

/**
* Get this item's HTML Element
* @return {HTMLElement}
*/
getEl : function(){
return this.el;
},

// private
render : function(td){
this.td = td;
td.appendChild(this.el);
},

/**
* Remove and destroy this button
*/
destroy : function(){
this.td.parentNode.removeChild(this.td);
},

/**
* Show this item
*/
show: function(){
this.hidden = false;
this.td.style.display = "";
},

/**
* Hide this item
*/
hide: function(){
this.hidden = true;
this.td.style.display = "none";
},

/**
* Convenience function for boolean show/hide
* @param {Boolean} visible true to show/false to hide
*/
setVisible: function(visible){
if(visible) {
this.show();
}else{
this.hide();
}
},

/**
* Try to focus this item
*/
focus : function(){
Ext.fly(this.el).focus();
},

/**
* Disable this item
*/
disable : function(){
Ext.fly(this.td).addClass("x-item-disabled");
this.disabled = true;
this.el.disabled = true;
},

/**
* Enable this item
*/
enable : function(){
Ext.fly(this.td).removeClass("x-item-disabled");
this.disabled = false;
this.el.disabled = false;
}
};


/**
* @class Ext.Toolbar.Separator
* @extends Ext.Toolbar.Item
* A simple toolbar separator class
* @constructor
* Creates a new Separator
*/
Ext.Toolbar.Separator = function(){
var s = document.createElement("span");
s.className = "ytb-sep";
Ext.Toolbar.Separator.superclass.constructor.call(this, s);
};
Ext.extend(Ext.Toolbar.Separator, Ext.Toolbar.Item, {
enable:Ext.emptyFn,
disable:Ext.emptyFn,
focus:Ext.emptyFn
});

/**
* @class Ext.Toolbar.Spacer
* @extends Ext.Toolbar.Item
* A simple element that adds extra horizontal space to a toolbar.
* @constructor
* Creates a new Spacer
*/
Ext.Toolbar.Spacer = function(){
var s = document.createElement("div");
s.className = "ytb-spacer";
Ext.Toolbar.Separator.superclass.constructor.call(this, s);
};
Ext.extend(Ext.Toolbar.Spacer, Ext.Toolbar.Item, {
enable:Ext.emptyFn,
disable:Ext.emptyFn,
focus:Ext.emptyFn
});

/**
* @class Ext.Toolbar.TextItem
* @extends Ext.Toolbar.Item
* A simple class that renders text directly into a toolbar.
* @constructor
* Creates a new TextItem
* @param {String} text
*/
Ext.Toolbar.TextItem = function(text){
var s = document.createElement("span");
s.className = "ytb-text";
s.innerHTML = text;
Ext.Toolbar.TextItem.superclass.constructor.call(this, s);
};
Ext.extend(Ext.Toolbar.TextItem, Ext.Toolbar.Item, {
enable:Ext.emptyFn,
disable:Ext.emptyFn,
focus:Ext.emptyFn
});

/**
* @class Ext.Toolbar.Button
* @extends Ext.Button
* A button that renders into a toolbar.
* @constructor
* Creates a new Button
* @param {Object} config A standard {@link Ext.Button} config object
*/
Ext.Toolbar.Button = function(config){
Ext.Toolbar.Button.superclass.constructor.call(this, null, config);
};
Ext.extend(Ext.Toolbar.Button, Ext.Button, {
render : function(td){
this.td = td;
Ext.Toolbar.Button.superclass.render.call(this, td);
},

/**
* Remove and destroy this button
*/
destroy : function(){
Ext.Toolbar.Button.superclass.destroy.call(this);
this.td.parentNode.removeChild(this.td);
},

/**
* Show this button
*/
show: function(){
this.hidden = false;
this.td.style.display = "";
},

/**
* Hide this button
*/
hide: function(){
this.hidden = true;
this.td.style.display = "none";
},

/**
* Disable this item
*/
disable : function(){
Ext.fly(this.td).addClass("x-item-disabled");
this.disabled = true;
},

/**
* Enable this item
*/
enable : function(){
Ext.fly(this.td).removeClass("x-item-disabled");
this.disabled = false;
}
});
// backwards compat
Ext.ToolbarButton = Ext.Toolbar.Button;

/**
* @class Ext.Toolbar.MenuButton
* @extends Ext.MenuButton
* A menu button that renders into a toolbar.
* @constructor
* Creates a new MenuButton
* @param {Object} config A standard {@link Ext.MenuButton} config object
*/
Ext.Toolbar.MenuButton = function(config){
Ext.Toolbar.MenuButton.superclass.constructor.call(this, null, config);
};
Ext.extend(Ext.Toolbar.MenuButton, Ext.MenuButton, {
render : function(td){
this.td = td;
Ext.Toolbar.MenuButton.superclass.render.call(this, td);
},

/**
* Remove and destroy this button
*/
destroy : function(){
Ext.Toolbar.MenuButton.superclass.destroy.call(this);
this.td.parentNode.removeChild(this.td);
},

/**
* Show this button
*/
show: function(){
this.hidden = false;
this.td.style.display = "";
},

/**
* Hide this button
*/
hide: function(){
this.hidden = true;
this.td.style.display = "none";
}
});



Then, you can just do



myToolbar = new Ext.Toolbar('tb-container', null, {for:'myForm'});
myToolBar.add({{
text: 'ALT/G',
handler: function(){alert("ALT/G")},
pressed: true,
key:'G',
alt:true
});


And when form id "myForm" has focus, you can Alt/G.

Animal
5 May 2007, 2:15 AM
The same concept could be applied to Menus (and any upcoming MenuBar)

The KeyMap would have to be owned by the top parent (The root) of the menu being added to, so whenever an Ext.Menu.Item is added to a Menu, key specs would have to be copied and a binding added to the root menu's KeyMap. That would have to be enabled when the root menu is shown and disabled when hidden.

The root menu would have to be given a "for" config.

Obviously a MenuBar implementation doesn't need a "for", and doesn't need to enable/disable a KeyMap. Its listener would be on the document, and always enabled because it's always shown.

skippy
5 Jun 2007, 12:38 PM
So my key maps are functioning as desired, but I'd like to display the shortcut next to the menu item (e.g. "Shift + F" - right aligned of course).

brian.moeskau
5 Jun 2007, 1:28 PM
There's no direct way to add shortcut text to a menu. You might consider extending Ext.menu.Item and overriding the onRender method. It currently renders a link that contains an optional icon and the menu text. You'd need some kind of container around the icon/text so you could float your shortcut text to the right and keep it in line with the main text. Not sure what issues you'll hit, but that might get you started at least.

skippy
6 Jun 2007, 10:11 AM
Thanks Brian, but I opted for the total hack solution! I wrapped a span tag around the shortcut text and added it to the menu item text...


var insertMenu = new Ext.menu.Menu({
minWidth:150,
items: [
new Ext.menu.Item({text: 'Firm<span id="shortcut">Shift+F</span>',
handler: showFirmDetail,
cls: 'new-firm'})


and then added styles...


#shortcut {
position:absolute;
right:12px;
}
#shortcut a {
text-decoration: none;
color: black;
}


I also had to deal with the menu minWidth issue in non-IE browsers (see thread (http://extjs.com/forum/showthread.php?t=2631&highlight=menu+minwidth)), but all-in-all I'm pretty happy with the solution!

Thanks Again,
skip

Animal
23 Nov 2007, 8:45 AM
To get back on topic...

It's time to reawaken this. Now that Toolbar has "initMenuTracking" which it calls for every button added, it could easily add key activation for buttons using this:



initMenuTracking : function(item){
if (this.keyMap && item.key) {
this.keyMap.addBinding({
stopEvent: true,
key: item.key,
shift: item.shift,
ctrl: item.ctrl,
alt: item.alt,
fn: item.handler || item.fn,
scope: item.scope || item
});
}
if(this.trackMenus && item.menu){
item.on({
'menutriggerover' : this.onButtonTriggerOver,
'menushow' : this.onButtonMenuShow,
'menuhide' : this.onButtonMenuHide,
scope: this
})
}
},


Of course I haven't looked at the other side of that if. If there is a Menu attached to a button, it should expand the Menu if the button's configured key is pressed.

What this really needs to make it great is for the Panel.onRender to create a KeyMap, and configure the top and bottom Toolbars with it.

So now I add my "New" button like this:



var b = this.toolbar.addButton({
id: this.id + 'NewButton',
text: AU.getMessage("button.new"),
key: 'N',
ctrl: true,
this.reset,
scope: this
});