View Full Version : Ext.ux.GhostBar - A space-saving, fade-in Toolbar
Animal
05-23-2009, 05:47 AM
When you need to see a lot of data in your Component, but any Toolbar Buttons are secondary, and may not be wanted, this class fades in a top or bottom Toolbar whenever the mouse approaches the region where the top or bottom toolbar would be.
It is a Toolbar subclass which functions as a plugin (http://extjs.com/deploy/ext-3.0-rc1.1/docs/?class=Ext.BoxComponent&member=plugins) to any BoxComponent (http://extjs.com/deploy/ext-3.0-rc1.1/docs/?class=Ext.BoxComponent)
To test it, simply:
new Ext.Panel({
renderTo: document.body,
title: 'Test',
width: 600,
height: 400,
plugins: [ new Ext.ux.GhostBar([{ text: 'Click Me' }]) ]
});
Ext.override(Ext.lib.Region, {
/**
* Returns the shortest distance between this Region and another Region.
* Either or both Regions may be Points.
* @param {Region} r The other Region
* @return {Number} The shortest distance in pixels between the two Regions.
*/
getDistanceBetween: function(r) {
// We may need to mutate r, so make a copy.
r = Ext.apply({}, r);
// Translate r to the left of this
if (r.left > this.right) {
var rWidth = r.right - r.left;
r.left = this.left - (r.left - this.right) - rWidth;
r.right = r.left + rWidth;
}
// Translate r above this
if (r.top > this.bottom) {
var rHeight = r.bottom - r.top;
r.top = this.top - (r.top - this.bottom) - rHeight;
r.bottom = r.top + rHeight;
}
// If r is directly above
if (r.right > this.left) {
return this.top - r.bottom;
}
// If r is directly to the left
if (r.bottom > this.top) {
return this.left - r.right;
}
// r is on a diagonal path
return Math.round(Math.sqrt(Math.pow(this.top - r.bottom, 2) + Math.pow(this.left - r.right, 2)));
}
});
/**
* @class Ext.ux.GhostBar
* @extends Ext.Toolbar
* A Toolbar class which attaches as a plugin to any BoxComponent, and fades in at the configured
* position whenever the mouse is brought within a configurable threshold. eg: <code><pre>
new Ext.Panel({
renderTo: document.body,
title: 'Test',
width: 600,
height: 400,
plugins: [ new Ext.ux.GhostBar([{ text: 'Click Me' }]) ]
});
</pre></code>
*/
Ext.ux.GhostBar = Ext.extend(Ext.Toolbar, {
listenerAdded: false,
cache: [],
/**
* @cfg {Number} threshold The number of pixels around the toolbar position in which
* fading is performed.
*/
threshold: 100,
/**
* @cfg {String} position The alignment of this Toolbar, <code><b>top</b></code>, or <code><b>bottom</b></code>.
* Defaults to <code><b>bottom</b></code>.
*/
/**
* @cfg {Array} offsets A two element Array containing the [x, y] offset from the default position
* in which to display the Toolbar.
*/
initComponent: function() {
// Only use one mousemove listener. Check the cache of GhostBars for proximity on each firing
if (!this.listenerAdded) {
Ext.getDoc().on('mousemove', Ext.ux.GhostBar.prototype.onDocMouseMove, Ext.ux.GhostBar.prototype);
this.listenerAdded = true;
}
this.renderTo = document.body;
this.hideMode = 'visibility';
Ext.ux.GhostBar.superclass.initComponent.apply(this, arguments);
},
onRender: function() {
Ext.ux.GhostBar.superclass.onRender.apply(this, arguments);
this.el.setStyle({
position: 'absolute'
});
this.hide();
this.cache.push(this);
},
init: function(c) {
this.ownerCt = c;
c.on({
render: this.onClientRender,
scope: this,
single: true
});
c.onPosition = c.onPosition.createSequence(this.onClientPosition, this);
c.onResize = c.onResize.createSequence(this.onClientResize, this);
},
onClientRender: function() {
this.clientEl = this.ownerCt.getLayoutTarget ? this.ownerCt.getLayoutTarget() : this.ownerCt.el;
},
onClientResize: function() {
this.setWidth(this.clientEl.getWidth(true));
this.syncPosition();
},
onClientPosition: function() {
this.syncPosition();
},
syncPosition: function() {
var offsets = [this.clientEl.getBorderWidth('l'), 0];
if (this.offsets) {
offsets[0] += this.offsets[0];
offsets[1] += this.offsets[1];
}
this.el.alignTo(this.clientEl, (this.position == 'top') ? 'tl-tl' : 'bl-bl', offsets);
this.region = this.el.getRegion();
},
onDocMouseMove: function(e) {
for (var i = 0; i < this.cache.length; i++) {
this.checkMousePosition.call(this.cache[i], e);
}
},
checkMousePosition: function(e) {
this.syncPosition();
var o = 1, d = this.region.getDistanceBetween(e.getPoint());
if (d > this.threshold) {
this.hide();
} else if (d > 0) {
// Mouse is within range of this Toolbar, so show it if its not already visible
if (!this.isVisible()) {
this.show();
}
o = 1 - (d / this.threshold);
}
var z = Ext.num(this.ownerCt.el.getStyle('zIndex'));
this.el.setStyle({
opacity: o,
'zIndex': (typeof z == 'number') ? z + 3 : 'auto'
});
},
onDestroy: function() {
// Uncache this Toolbar when we are destroyed
this.cache.splice(this.cache.indexOf(this), 1);
Ext.ux.GhostBar.superclass.onDestroy.apply(this, arguments);
}
});
galdaka
05-23-2009, 09:28 AM
Excellent work Animal (As always) ;)
I have strange issue in IE6. I attach screenshot and example to extract in examples\panel directory of Ext 3.0 distribution.
Greetings,
Animal
05-24-2009, 03:47 AM
I think there is a bug with Ext 3.0 mousemove handling which is breaking things on IE.
http://extjs.com/forum/showthread.php?p=333612
Anyway, I have refactored and posted a more efficient version which only uses one mousemove listener, and in the handler, checks through a cache of GhostBars checking each for mouse proximity.
Also, I sync the toolbar with its client Element on each mouse move so that collapsing other elements in the page does not affect where the toolbar should be.
mjlecomte
05-26-2009, 09:26 AM
If you have a couple of these plugged into a window, for example, you may not want the z-index hardcoded. I corrected a couple of typos and modified for dynamic z-index in code below.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Ghost components</title>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all.js"></script>
<script type="text/javascript">
Ext.BLANK_IMAGE_URL = '../../resources/images/default/s.gif';
Ext.override(Ext.lib.Region, {
/**
* Returns the shortest distance between this Region and another Region.
* Either or both Regions may be Points.
* @param {Region} r The other Region
* @return {Number} The shortest distance in pixels between the two Regions.
*/
getDistanceBetween: function(r) {
// We may need to mutate r, so make a copy.
r = Ext.apply({}, r);
// Translate r to the left of this
if (r.left > this.right) {
var rWidth = r.right - r.left;
r.left = this.left - (r.left - this.right) - rWidth;
r.right = r.left + rWidth;
}
// Translate r above this
if (r.top > this.bottom) {
var rHeight = r.bottom - r.top;
r.top = this.top - (r.top - this.bottom) - rHeight;
r.bottom = r.top + rHeight;
}
// If r is directly above
if (r.right > this.left) {
return this.top - r.bottom;
}
// If r is directly to the left
if (r.bottom > this.top) {
return this.left - r.right;
}
// r is on a diagonal path
return Math.round(Math.sqrt(Math.pow(this.top - r.bottom, 2) + Math.pow(this.left - r.right, 2)));
}
});
/**
* @class Ext.ux.GhostBar
* @extends Ext.Toolbar
* A Toolbar class which attaches as a plugin to any BoxComponent, and fades in at the configured
* position whenever the mouse is brought within a configurable threshold. eg: <code><pre>
new Ext.Panel({
renderTo: document.body,
title: 'Test',
width: 600,
height: 400,
plugins: [ new Ext.ux.GhostBar([{ text: 'Click Me' }]) ]
});
</pre></code>
*/
Ext.ux.GhostBar = Ext.extend(Ext.Toolbar, {
listenerAdded: false,
cache: [],
/**
* @cfg {Number} threshold The number of pixels around the toolbar position in which
* fading is performed.
*/
threshold: 100,
/**
* @cfg {String} position The alignment of this Toolbar, <code><b>top</b></code>, or <code><b>bottom</b></code>.
* Defaults to <code><b>bottom</b></code>.
*/
/**
* @cfg {Array} offsets A two element Array containing the [x, y] offset from the default position
* in which to display the Toolbar.
*/
initComponent: function() {
// Only use one mousemove listener. Check the cache of GhostBars for proximity on each firing
if (!this.listenerAdded) {
Ext.getDoc().on('mousemove', Ext.ux.GhostBar.prototype.onDocMouseMove, Ext.ux.GhostBar.prototype);
this.listenerAdded = true;
}
this.renderTo = document.body;
this.hideMode = 'visibility';
Ext.ux.GhostBar.superclass.initComponent.apply(this, arguments);
},
onRender: function() {
Ext.ux.GhostBar.superclass.onRender.apply(this, arguments);
this.el.setStyle({
position: 'absolute'
});
this.hide();
this.cache.push(this);
},
init: function(c) {
this.ownerCt = c;
c.on({
render: this.onClientRender,
scope: this,
single: true
});
c.onPosition = c.onPosition.createSequence(this.onClientPosition, this);
c.onResize = c.onResize.createSequence(this.onClientResize, this);
},
onClientRender: function() {
this.clientEl = this.ownerCt.getLayoutTarget ? this.ownerCt.getLayoutTarget() : this.ownerCt.el;
},
onClientResize: function() {
this.setWidth(this.clientEl.getWidth(true));
this.syncPosition();
},
onClientPosition: function() {
this.syncPosition();
},
syncPosition: function() {
var offsets = [this.clientEl.getBorderWidth('l'), 0];
if (this.offsets) {
offsets[0] += this.offsets[0];
offsets[1] += this.offsets[1];
}
this.el.alignTo(this.clientEl, (this.position == 'top') ? 'tl-tl' : 'bl-bl', offsets);
this.region = this.el.getRegion();
},
onDocMouseMove: function(e) {
for (var i = 0; i < this.cache.length; i++) {
this.checkMousePosition.call(this.cache[i], e);
}
},
checkMousePosition: function(e) {
this.syncPosition();
var o = 1, d = this.region.getDistanceBetween(e.getPoint());
if (d > this.threshold) {
this.hide();
} else if (d > 0) {
// Mouse is within range of this Toolbar, so show it if its not already visible
if (!this.isVisible()) {
this.show();
}
o = 1 - (d / this.threshold);
}
this.el.setStyle({
opacity: o,
'z-index': this.ownerCt.el.zindex+3
});
},
onDestroy: function() {
// Uncache this Toolbar when we are destroyed
this.cache.splice(this.cache.indexOf(this), 1);
Ext.ux.GhostBar.superclass.onDestroy.apply(this, arguments);
}
});
Ext.onReady(function(){
new Ext.Window({
renderTo: document.body,
title: 'Test',
html: 'test',
width: 600,
height: 400,
plugins: [ new Ext.ux.GhostBar([{ text: 'Click Me' }]) ]
}).show();
new Ext.Window({
renderTo: document.body,
title: 'Test',
html: 'test',
width: 600,
height: 400,
plugins: [
new Ext.ux.GhostBar({
items:[
{ text: 'Click Me' }
]
})
]
}).show();
});
</script>
</head>
<body></body>
</html>
Animal
05-26-2009, 12:38 PM
Excellent work MJ, I've incorporated your fixes and enhancements into post #1
acidtonic
03-02-2010, 02:55 PM
I've found this tremendously useful and wanted to say thanks!
I'm having a few issues though... when used as a plugin for a Panel that can shade, the toolbar will appear at the top left of the page as if anchored to 0,0 whenever the panel is shaded. But when the panel is unshaded, it goes back to where it should be.
More importantly though, I'm trying to use this inside a custom panel that I extended which works great. But as soon as I put that panel inside a tabpanel i get errors.
"var offsets = [this.clientEl.getBorderWidth('l'), 0];" with the error "this.clientEL is undefined."
Any thoughts? I can provide a link to the live dev site if you want to see it misbehaving.
acidtonic
03-03-2010, 09:43 AM
I ended up solving the big issue.... All that's remaining is the GhostBar showing up at the top of the screen for containers that are hidden or shaded.
Example to reproduce....
Make two panels that can shade, put a ghostbar in each with just a checkbox inside the ghostbar. Now check the checkbox in one ghostbar and shade that panel..... Now you'll find the ghostbar appearing at the top left of the page and you can tell its the one from the panel because the checkbox is checked.
Same thing happens for having this inside a tab panel. Whatever ghostbars are in tabs not currently shown will show up at the top of the page.
Also is there any easy way to prevent the toolbar from shading, but rather have it fully display when the mouse is close enough?
EDIT: The ghostbar appearing at the top issue only seems to happen on firefox but not IE6 or 7.
Animal
03-03-2010, 10:56 AM
It needs this to only check for proximity if its owner is visible:
onDocMouseMove: function(e) {
for (var i = 0; i < this.cache.length; i++) {
if (this.cache[i].ownerCt.isVisible()) {
this.checkMousePosition.call(this.cache[i], e);
}
}
},
Animal
03-03-2010, 11:06 AM
OK, here's the full thing.
Now with a fullVisibilityZone config so you can specify a proximity zone in which you want 100% opacity. Defaults to 50px.
So fading now is from 51px to 150px
Ext.override(Ext.lib.Region, {
/**
* Returns the shortest distance between this Region and another Region.
* Either or both Regions may be Points.
* @param {Region} r The other Region
* @return {Number} The shortest distance in pixels between the two Regions.
*/
getDistanceBetween: function(r) {
// We may need to mutate r, so make a copy.
r = Ext.apply({}, r);
// Translate r to the left of this
if (r.left > this.right) {
var rWidth = r.right - r.left;
r.left = this.left - (r.left - this.right) - rWidth;
r.right = r.left + rWidth;
}
// Translate r above this
if (r.top > this.bottom) {
var rHeight = r.bottom - r.top;
r.top = this.top - (r.top - this.bottom) - rHeight;
r.bottom = r.top + rHeight;
}
// If r is directly above
if (r.right > this.left) {
return this.top - r.bottom;
}
// If r is directly to the left
if (r.bottom > this.top) {
return this.left - r.right;
}
// r is on a diagonal path
return Math.round(Math.sqrt(Math.pow(this.top - r.bottom, 2) + Math.pow(this.left - r.right, 2)));
}
});
/**
* @class Ext.ux.GhostBar
* @extends Ext.Toolbar
* A Toolbar class which attaches as a plugin to any BoxComponent, and fades in at the configured
* position whenever the mouse is brought within a configurable threshold. eg: <code><pre>
new Ext.Panel({
renderTo: document.body,
title: 'Test',
width: 600,
height: 400,
plugins: [ new Ext.ux.GhostBar([{ text: 'Click Me' }]) ]
});
</pre></code>
*/
Ext.ux.GhostBar = Ext.extend(Ext.Toolbar, {
listenerAdded: false,
cache: [],
/**
* @cfg {Number} threshold The number of pixels around the toolbar position in which
* opacity is 100%.
*/
fullVisibilityZone: 50,
/**
* @cfg {Number} threshold The number of pixels around the full visibility zone in which
* fading is performed.
*/
threshold: 100,
/**
* @cfg {String} position The alignment of this Toolbar, <code><b>top</b></code>, or <code><b>bottom</b></code>.
* Defaults to <code><b>bottom</b></code>.
*/
/**
* @cfg {Array} offsets A two element Array containing the [x, y] offset from the default position
* in which to display the Toolbar.
*/
initComponent: function() {
// Only use one mousemove listener. Check the cache of GhostBars for proximity on each firing
if (!this.listenerAdded) {
Ext.getDoc().on('mousemove', Ext.ux.GhostBar.prototype.onDocMouseMove, Ext.ux.GhostBar.prototype);
this.listenerAdded = true;
}
this.renderTo = document.body;
this.hideMode = 'visibility';
Ext.ux.GhostBar.superclass.initComponent.apply(this, arguments);
},
onRender: function() {
Ext.ux.GhostBar.superclass.onRender.apply(this, arguments);
this.el.setStyle({
position: 'absolute'
});
this.hide();
this.cache.push(this);
},
init: function(c) {
this.ownerCt = c;
c.on({
render: this.onClientRender,
scope: this,
single: true
});
c.onPosition = c.onPosition.createSequence(this.onClientPosition, this);
c.onResize = c.onResize.createSequence(this.onClientResize, this);
},
onClientRender: function() {
this.clientEl = this.ownerCt.getLayoutTarget ? this.ownerCt.getLayoutTarget() : this.ownerCt.el;
},
onClientResize: function() {
this.setWidth(this.clientEl.getWidth(true));
this.syncPosition();
},
onClientPosition: function() {
this.syncPosition();
},
syncPosition: function() {
var offsets = [this.clientEl.getBorderWidth('l'), 0];
if (this.offsets) {
offsets[0] += this.offsets[0];
offsets[1] += this.offsets[1];
}
this.el.alignTo(this.clientEl, (this.position == 'top') ? 'tl-tl' : 'bl-bl', offsets);
this.region = this.el.getRegion();
},
onDocMouseMove: function(e) {
for (var i = 0; i < this.cache.length; i++) {
if (this.cache[i].ownerCt.isVisible()) {
this.checkMousePosition.call(this.cache[i], e);
}
}
},
checkMousePosition: function(e) {
this.syncPosition();
var o = 1, d = this.region.getDistanceBetween(e.getPoint());
if (d > this.threshold + this.fullVisibilityZone) {
this.hide();
} else if ((d -= this.fullVisibilityZone) > 0) {
// Mouse is within range of this Toolbar, so show it if its not already visible
if (!this.isVisible()) {
this.show();
}
o = 1 - (d / this.threshold);
}
this.el.setStyle({
opacity: o,
'z-index': this.ownerCt.el.zindex+3
});
},
onDestroy: function() {
// Uncache this Toolbar when we are destroyed
this.cache.splice(this.cache.indexOf(this), 1);
Ext.ux.GhostBar.superclass.onDestroy.apply(this, arguments);
}
});
acidtonic
03-03-2010, 12:37 PM
The new fading is really nice.
I do however still have the bug about ghostbars showing up when a panel with one is collapsed.
The same exact URL I pm'ed you previously shows that behavior in firefox. Just navigate to another tab or shade any panel and move the mouse to the top left of the browser and the panels will appear when they shouldnt.
acidtonic
03-03-2010, 01:57 PM
From what I'm finding, this.ownerCt.isVisible() returns true even when it really isnt.
I'm confused because the ghostbar is a plugin to a panel which is inside a tab panel.
When either the panel shades OR the current tab is changed on the tabpanel, the ghostbar still renders but realigns to (0,0) placing it in the upper left.
I'm not sure why it realigns up there though. It's almost like this.ownerCt is returning the parent of the parent which would be the tab panel, which is visible and thus the toolbar gets drawn.
Animal
03-03-2010, 02:05 PM
The ownerCt Panel may be visible, but ITS ownerCt may be hidden. There will have to be a "recursive" parameter to Component.isVisible which goes all the way up the chain.
I'll leave that as an exercise for you.
acidtonic
03-03-2010, 03:12 PM
In my case I *always* use the plugin inside a shadeable panel.
My hack was to simply check twice and never any higher. Also added a hide for when it fails the test yet it's visible.
The other thought was to also check the parent component for the "beforeCollapse" event, and if it exists, listen to it and hide() on invocation.
My updated code is here for anyone else solving these problems.
checkMousePosition: function(e) {
this.syncPosition();
var o = 1, d = this.region.getDistanceBetween(e.getPoint());
if (d > this.threshold + this.fullVisibilityZone) {
this.hide();
} else if ((d -= this.fullVisibilityZone) > 0) {
// Mouse is within range of this Toolbar, so show it if its not already visible
if (!this.isVisible()) {
if (this.ownerCt.isVisible() && this.ownerCt.ownerCt.isVisible()) {
this.show();
}
} else {
if (!this.ownerCt.isVisible() || !this.ownerCt.ownerCt.isVisible()) {
this.hide();
}
}
o = 1 - (d / this.threshold);
}
this.el.setStyle({
opacity: o,
//'z-index': this.ownerCt.el.zindex+3
'z-index': 20
});
},
Animal
03-03-2010, 03:18 PM
How about an override of Component.isVisible?
acidtonic
03-03-2010, 04:04 PM
How about an override of Component.isVisible?
That's what I instantly thought, but remembered how much help I got simply trying to extend my first component(zero), and decided I wouldn't undertake that one.
Animal
03-03-2010, 04:35 PM
You're going to have to do it sooner or later. It's not as if there isn't deep and complete documentation, and hundreds of examples.
vBulletin® v3.8.4, Copyright ©2000-2010, Jelsoft Enterprises Ltd.