PDA

View Full Version : Mouse proximity based fadeout.



Animal
6 Feb 2009, 3:26 AM
Ever seen the MS Word feature where, if you select some text by double click or by dragging, a faintly opaque "Font Style" menu appears above the selection which becomes fully opaque if you mouse over it, but fades if you mouse away?

Here's a class (which may also be used as a plugin to fade an already shown Component) which does that.

It manages the opacity of one Component, but has two ways of triggering the fade. If you configure it with a trigger element, it uses proximity to that to fade in/out the Component.

If you do not specify a trigger, it uses proximity to the Component itself and it's up to you to show the component. The code below illustrates usage. Thanks to MJLecomte BTW, for making me think about this. I'm trying to think of a useful way of applying this to showing the grid filtering Panel in a smooth and unobtrusive way.

Drop the code below into examples/simple-widgets. It's a version of the qtips example.

The closable QuickTip has a ProximityFader as a plugin. It fades away as you move the mouse away from it. When you get to 100 pixels from any border, it hides.



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)));
}
});

Ext.override(Ext.Element, {
/**
* Returns shortest distance between this Element and the specified point
* @param {Number} x The x coordinate.
* @param {Number} y The y coordinate.
* @return {Number} The shortest distance in pixels between this Element and the specified point.
*/
getDistanceTo: function(x, y) {
return this.getRegion().getDistanceBetween(new Ext.lib.Point(x, y));
},

/**
* Returns the shortest distance between this Element and another Element.
* @param {Element/DOMElement/String} el The other Element, or its ID.
* @return {Number} The shortest distance in pixels between the two Elements.
*/
getDistanceBetween: function(el) {
return this.getRegion().getDistanceBetween(Ext.fly(el).getRegion());
}
});

/**
* @class Ext.ux.ProximityFader
* Manages visibility of a Component based on the proximity of the mouse to a configured trigger Element:<pre><code>
new Ext.ux.ProximityFader({
threshold: 100, // When within 100 pixels of
trigger: proximityTriggerEl, // this Element,
component: myFloatingPanel // Begin fading in this Component.
});
*/
Ext.ux.ProximityFader = Ext.extend(Object, {
constructor: function(config) {
Ext.apply(this, config);
if (this.component) {
this.init(this.component);
}
},

init: function(component) {
this.component = component;
if (component.rendered) {
this.onComponentRender(component);
} else {
component.on({
render: this.onComponentRender,
single: true,
scope: this,
delay: 1
});
}

// If we have been configured with a trigger, always listen for proximity
if (this.trigger) {
Ext.getDoc().on('mousemove', this.onMouseMove, this);
} else {
// Otherwise the trigger is the Component's Element. Only listen while it's visible
component.on({
show: this.onShow,
hide: this.onHide,
scope: this
});
}
},

onMouseMove: function(e) {
var o = 1, d = this.el.getDistanceTo.apply(this.trigger, e.getXY());
if (d > this.threshold) {
this.component.hide();
} else if (d > 0) {

// Mouse is within range of the trigger, so show the Component if its not already visible
if (this.trigger && !this.component.isVisible()) {
this.component.show();
}
var o = 1 - (d / this.threshold);
}
this.el.setOpacity(o);
if (this.shadow) {
this.shadow.setOpacity(o);
}
},

onComponentRender: function(c) {
if (!this.trigger) {
this.trigger = c.el;
}
this.el = c.el;
if (this.el.shadow) {
this.shadow = this.el.shadow.el;
}
},

onShow: function() {
Ext.getDoc().on('mousemove', this.onMouseMove, this);
},

onHide: function() {
Ext.getDoc().un('mousemove', this.onMouseMove, this);
}
});

Ext.onReady(function(){
new Ext.ToolTip({
target: 'tip1',
html: 'A very simple tooltip'
});

new Ext.ToolTip({
target: 'ajax-tip',
width: 200,
autoLoad: {url: 'ajax-tip.html'},
dismissDelay: 15000 // auto hide after 15 seconds
});

new Ext.ToolTip({
target: 'tip2',
html: 'Click the X to close me',
title: 'My Tip Title',
autoHide: false,
closable: true,
draggable: true,
plugins: new Ext.ux.ProximityFader({
threshold: 100
}),
});

new Ext.ToolTip({
target: 'track-tip',
title: 'Mouse Track',
width:200,
html: 'This tip will follow the mouse while it is over the element',
trackMouse:true
});
Ext.QuickTips.init();
});

mjlecomte
8 Feb 2009, 9:34 PM
Just wanted to say thanks for posting this. It works really well so far.

Also, thought I'd post the implementation I was playing around with. I just wired this up for the paging grid demo to fade in/out the bottom toolbar as the mouse gets proximity to the grid.

A couple thoughts, just for discussion of impressions of GUI feel, etc.:

Bottom toolbar had hidden:true to initially hide the bottom toolbar.

In this particular implementation, I think it works good to visually show the bottom toolbar as you get closer to that grid. However, thinking about MS office behavior, I was thinking that maybe the behavior would be to show the component at say 0.2 opacity as you get close to the grid. That would hint to the bottom toolbar. Then, if the user wants to see more of the bottom toolbar, the proximity should shift to monitoring the 'hidden' component. I can see extending the config of hiding/showing further with some timers to autoshow or autohide as well (

The implementation shown here is very sparse. I think the benefit of hiding portions of "cluttered interfaces" is when there is more going on. So envision a grids on top of each other north/center/south. I'm thinking the fade in/out show probably take the original position of the toolbar and actually offset onto the body of the grid, hovering above it per se. So that would involve moving the "original" position of it provided by default and perhaps resizing it (reducing the width) in this case.

As an aside, it just dawned on me it would probably be nice instead of having a button to toggle on/off details of each post to use mouseover to do the same.

I suppose we should be able to wire this up to one of the busier border layouts and have the moused over region be fully visible and the others will fade out to 0.2 or something.



<html>
<head>
<title>Component Fade-in</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.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)));
}
});

Ext.override(Ext.Element, {
/**
* Returns shortest distance between this Element and the specified point
* @param {Number} x The x coordinate.
* @param {Number} y The y coordinate.
* @return {Number} The shortest distance in pixels between this Element and the specified point.
*/
getDistanceTo: function(x, y) {
return this.getRegion().getDistanceBetween(new Ext.lib.Point(x, y));
},

/**
* Returns the shortest distance between this Element and another Element.
* @param {Element/DOMElement/String} el The other Element, or its ID.
* @return {Number} The shortest distance in pixels between the two Elements.
*/
getDistanceBetween: function(el) {
return this.getRegion().getDistanceBetween(Ext.fly(el).getRegion());
}
});

/**
* @class Ext.ux.ProximityFader
* Manages visibility of a Component based on the proximity of the mouse to a configured
* trigger Element:<pre><code>
new Ext.ux.ProximityFader({
threshold: 100, // When within 100 pixels of
trigger: proximityTriggerEl, // this Element,
component: myFloatingPanel // Begin fading in this Component.
});
*/
Ext.ux.ProximityFader = Ext.extend(Object, {
constructor: function(config) {
Ext.apply(this, config);
if (this.component) {
this.init(this.component);
}
},

init: function(component) {
this.component = component;
if (component.rendered) {
this.onComponentRender(component);
} else {
component.on({
render: this.onComponentRender,
single: true,
scope: this,
delay: 1
});
}

// If we have been configured with a trigger, always listen for proximity
if (this.trigger) {
Ext.getDoc().on('mousemove', this.onMouseMove, this);
} else {
// Otherwise the trigger is the Component's Element. Only listen while it's visible
component.on({
show: this.onShow,
hide: this.onHide,
scope: this
});
}
},

onMouseMove: function(e) {
var o = 1, d = this.el.getDistanceTo.apply(this.trigger, e.getXY());
if (d > this.threshold) {
this.component.hide();
} else if (d > 0) {

// Mouse is within range of the trigger, so show the Component if its not already visible
if (this.trigger && !this.component.isVisible()) {
this.component.show();
}
var o = 1 - (d / this.threshold);
}
this.el.setOpacity(o);
if (this.shadow) {
this.shadow.setOpacity(o);
}
},

onComponentRender: function(c) {
if (!this.trigger) {
this.trigger = c.el;
}
this.el = c.el;
if (this.el.shadow) {
this.shadow = this.el.shadow.el;
}
},

onShow: function() {
Ext.getDoc().on('mousemove', this.onMouseMove, this);
},

onHide: function() {
Ext.getDoc().un('mousemove', this.onMouseMove, this);
}
});

// ----------------------------------------------------------------------------

Ext.onReady(function(){

// create the Data Store
var store = new Ext.data.JsonStore({
root: 'topics',
totalProperty: 'totalCount',
idProperty: 'threadid',
remoteSort: true,

fields: ['title', 'forumtitle', 'forumid', 'author', {
name: 'replycount',
type: 'int'
}, {
name: 'lastpost',
mapping: 'lastpost',
type: 'date',
dateFormat: 'timestamp'
}, 'lastposter', 'excerpt'],

// load using script tags for cross domain, if the data in on the same domain as
// this page, an HttpProxy would be better
proxy: new Ext.data.ScriptTagProxy({
url: 'http://extjs.com/forum/topics-browse-remote.php'
})
});
store.setDefaultSort('lastpost', 'desc');


// pluggable renders
function renderTopic(value, p, record){
return String.format('<b><a href="http://extjs.com/forum/showthread.php?t={2}" target="_blank">{0}</a></b><a href="http://extjs.com/forum/forumdisplay.php?f={3}" target="_blank">{1} Forum</a>', value, record.data.forumtitle, record.id, record.data.forumid);
}

function renderLast(value, p, r){
return String.format('{0}<br/>by {1}', value.dateFormat('M j, Y, g:i a'), r.data['lastposter']);
}

var bottomBar = new Ext.PagingToolbar({
hidden: true,
id: 'b',

pageSize: 25,
store: store,
displayInfo: true,
displayMsg: 'Displaying topics {0} - {1} of {2}',
emptyMsg: "No topics to display",
items:[
'-',
{
pressed: true,
enableToggle:true,
text: 'Show Preview',
cls: 'x-btn-text-icon details',
toggleHandler: function(btn, pressed){
var view = grid.getView();
view.showPreview = pressed;
view.refresh();
}
}
]
})


var grid = new Ext.grid.GridPanel({
//el:'topic-grid',
width: 700,
height: 300,
title: 'ExtJS.com - Browse Forums',
store: store,
trackMouseOver: false,
disableSelection: true,
loadMask: true,

// grid columns
columns: [{
id: 'topic', // id assigned so we can apply custom css (e.g. .x-grid-col-topic b { color:#333 })
header: "Topic",
dataIndex: 'title',
width: 420,
renderer: renderTopic,
sortable: true
}, {
header: "Author",
dataIndex: 'author',
width: 100,
hidden: true,
sortable: true
}, {
header: "Replies",
dataIndex: 'replycount',
width: 70,
align: 'right',
sortable: true
}, {
id: 'last',
header: "Last Post",
dataIndex: 'lastpost',
width: 150,
renderer: renderLast,
sortable: true
}],

// customize view config
viewConfig: {
forceFit: true,
enableRowBody: true,
showPreview: true,
getRowClass: function(record, rowIndex, p, store){
if (this.showPreview) {
p.body = '<p>' + record.data.excerpt + '</p>';
return 'x-grid3-row-expanded';
}
return 'x-grid3-row-collapsed';
}
},
// paging bar on the bottom
bbar: bottomBar
});

grid.on({
render:{
fn: function() {
//var triggerEl = this.header;
var triggerEl = this.el;
console.info(triggerEl);

new Ext.ux.ProximityFader({
threshold: 100, // When within 100 pixels of
trigger: triggerEl, // this Element,
component: bottomBar // Begin fading in this Component.
});
},
scope: grid
},
afterrender:{
fn: function() {
var b = bottomBar;
var w = b.ownerCt.getWidth();
var l = 50
b.setWidth(w-l);
b.doLayout();
b.setPosition(l/2, -10);
//b.setZIndex(b.getZIndex() + 3);
var zindex = 25000;
b.el.setStyle("z-index", zindex);
},
scope: grid
}
});

// render it
grid.render(Ext.getBody());

// trigger the data store load
store.load({
params: {
start: 0,
limit: 25
}
});
});

</script>
</head>
<body></body>
</html>

Remy
9 Feb 2009, 4:34 AM
Excellent idea, this could definately be used in busy interfaces to retrieve lost screen real estate.=D>

Animal
9 Feb 2009, 4:51 AM
Code updated to use a generalized Region method which calculates the distance between any two Regions (A Region may have zero width and height, and may therefore be a Point)

mjlecomte
9 Feb 2009, 10:39 AM
Hint/Suggestion on how to set the z-index? I modified prior example to shrink the width of the bottom toolbar and offset it. But it's showing up behind the grid, so I was trying to bump up the z-index unsuccessfully.

Animal
9 Feb 2009, 12:21 PM
setStyle of "z-index" should work AFAIK. You'd have to step into that call and follow it down into where it actually modifies the DOM element's style property and see what it really does.

mjlecomte
9 Feb 2009, 1:13 PM
Sorry, I should have mentioned that the property was applied successfully. I just don't see it making a visual difference. Some firebug shots attached.

I'll poke around some more. I'm probably guilty of thread hijacking now...sorry about that.

Animal
10 Feb 2009, 12:51 AM
Setting the z-index of an element nested inside an element that is itself behind another element won't change any appearance.

It's the div.x-panel-bbar that you want to bring to the top.

But the thing is, it's not actually floating. It's in the document flow, so it won't be affected by z-index.

What I think you need is a special Panel plugin which creates a toolbar based upon an Ext.Layer which is absolutely positioned, aligned "bl-tl" with the body element.

It will have to be realigned on show in case the body element moves (by layouts changing, collapsing etc)

mjlecomte
10 Feb 2009, 3:38 AM
Thanks, those were my suspicions (z-index, Layer, etc.). I was already eyeing Window.anchorTo for reference on the resize issue.

My original hack was headed down the path of using a plugin for the parent container (the GridPanel in the implementation here), but I put that to the back burner to see if a less invasive approach would work.

mjlecomte
10 Feb 2009, 6:08 PM
Fooling around with this I noticed that if the trigger element is within the threshold (say 100) of the browser edge, the mouse may exit the browser window and leave the fader at some visible opacity. As an end user I think I may expect that once the mouse leaves the browser that the component would be hidden (but maybe rather than an immediate hide might be desirable to institute a fadeout to give a similar impression of the gradual fade).