PDA

View Full Version : A Class for "hold down" buttons.


Animal
01-26-2007, 03:47 PM
I have written a new class which wraps any element to make it into the kind of button that you click and hold to perform a continuous operation.

While the mouse is held "mousedowned" on the element, the class fires a "fire" event which can be used to change a continuous value, move or scroll an element etc.

Here's the code embedded in a working example (apart from the fact that you'll need to put in your own URLs for utilities.js and logger.js):

It's an Ext 0.40+ only class BTW!


<html>
<head>
<script type="text/javascript" src="js/utilities.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="http://www.yui-ext.com/deploy/yui-ext.0.40-alpha/yui-ext-40a1.php"></script>
<script type="text/javascript">
/**
@class Ext.HoldButton
@extends Ext.util.Observable

A wrapper class which can be applied to any element. Fires a "fire" event while the
mouse is pressed. The interval between firings may be specified in the config but
defaults to 10 milliseconds.

Optionally, a CSS class may be applied to the element during the time it is pressed.

@config el The element to act as a button.
@config interval The interval between firings of the "fire" event. Default 10 ms.
@config pressClass A CSS class name to be applied to the element while pressed.
*/
Ext.HoldButton = function(config)
{
Ext.util.Config.apply(this, config, {interval: 10});
this.el = getEl(this.el);
this.el.unselectable();
this.events = {
/**
* @event fire
* Fires on a specified interval during the time the element is pressed.
* @param {YAHOO.ext.TabPanel} this
*/
'fire' : new YAHOO.util.CustomEvent('fire')
};
this.el.on('mousedown', this.handleMouseDown, this, true);
};

Ext.extend(Ext.HoldButton, Ext.util.Observable, {

docEl : getEl(document),

handleMouseDown : function() {
if (this.pressClass)
this.el.addClass(this.pressClass);
this.timer = setInterval(this.fire.createDelegate(this), this.interval);
this.docEl.on("mouseup", this.handleMouseUp, this, true);
this.el.on("mouseout", this.handleMouseOut, this, true);
},

fire : function() {
if (!this.paused) {
this.fireEvent("fire", this);
}
},

handleMouseOut : function() {
this.paused = true;
if (this.pressClass)
this.el.removeClass(this.pressClass);
this.el.on("mouseover", this.handleMouseReturn, this, true);
},

handleMouseReturn : function() {
this.el.removeListener("mouseover", this.handleMouseReturn);
if (this.pressClass)
this.el.addClass(this.pressClass);
this.paused = false;
},

handleMouseUp : function() {
clearInterval(this.timer);
if (this.paused)
this.el.removeListener("mouseover", this.handleMouseReturn);
else if (this.pressClass)
this.el.removeClass(this.pressClass);
this.el.removeListener("mouseout", this.handleMouseOut);
this.docEl.removeListener("mouseup", this.handleMouseUp);
delete this.paused;
}
});
</script>
<title>Test HoldButton</title>
<style type="text/css">
.red {
background-color:red;
}
</style>
<script type="text/javascript">
YAHOO.util.Event.on(window, "load", function() {
YAHOO.widget.Logger.enableBrowserConsole();
var h = new Ext.HoldButton( {el:"element-id", pressClass:"red"});
h.on("fire", function(){YAHOO.log("fired");});
});
</script>
</head>
<body>
<span id="element-id">Click and press on me</span>
</body>
</html>

Animal
01-27-2007, 04:36 AM
OK, here's the latest version. It has an optional delay before the continuous event starts firing.

And, if there is a delay, an optional "immediate" config option which means that one fire of the event occurs oninitial mousedown. So that's perfect for spinners where one click increments by one, and click and hold spins:


<html>
<head>
<script type="text/javascript" src="js/utilities.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="js/yui-ext.js"></script>
<script type="text/javascript">
/**
@class Ext.HoldButton
@extends Ext.util.Observable

A wrapper class which can be applied to any element. Fires a "fire" event while the
mouse is pressed. The interval between firings may be specified in the config but
defaults to 10 milliseconds.

Optionally, a CSS class may be applied to the element during the time it is pressed.

@config el The element to act as a button.
@config delay The initial delay before the repeating event begins firing.
Similar to an autorepeat key delay.
@config immediate If an initial delay is requested, force one immediate fire on mousedown
@config interval The interval between firings of the "fire" event. Default 10 ms.
@config pressClass A CSS class name to be applied to the element while pressed.
*/
Ext.HoldButton = function(config)
{
Ext.util.Config.apply(this, config, {interval: 10});
this.el = getEl(this.el);
this.el.unselectable();
this.events = {
/**
* @event fire
* Fires on a specified interval during the time the element is pressed.
* @param {Ext.HoldButton} this
*/
'fire' : new YAHOO.util.CustomEvent('fire')
};
this.fireDelegate = this.fire.createDelegate(this);
this.el.on('mousedown', this.handleMouseDown, this, true);
};

Ext.extend(Ext.HoldButton, Ext.util.Observable, {

/** @private */
docEl : getEl(document),

/** @private */
handleMouseDown : function() {
try {
this.el.dom.blur();
}
catch (e) {YAHOO.log(e);}
if (this.pressClass)
this.el.addClass(this.pressClass);
if (this.delay) {
if (this.immediate) {
this.fireEvent("fire", this);
}
this.timer = this.fireDelegate.defer(this.delay);
}
else
this.fireDelegate();
this.docEl.on("mouseup", this.handleMouseUp, this, true);
this.el.on("mouseout", this.handleMouseOut, this, true);
},

/** @private */
fire : function() {
this.fireEvent("fire", this);
this.timer = this.fireDelegate.defer(this.interval);
},

/** @private */
handleMouseOut : function() {
clearTimeout(this.timer);
if (this.pressClass)
this.el.removeClass(this.pressClass);
this.el.on("mouseover", this.handleMouseReturn, this, true);
},

/** @private */
handleMouseReturn : function() {
this.el.removeListener("mouseover", this.handleMouseReturn);
if (this.pressClass)
this.el.addClass(this.pressClass);
this.fireDelegate();
},

/** @private */
handleMouseUp : function() {
clearTimeout(this.timer);
this.el.removeListener("mouseover", this.handleMouseReturn);
this.el.removeListener("mouseout", this.handleMouseOut);
this.docEl.removeListener("mouseup", this.handleMouseUp);
this.el.removeClass(this.pressClass);
}
});
</script>
<title>Test HoldButton</title>
<style type="text/css">
.red {
background-color:red;
}
.spinner-button {
background-color:buttonface;
border-width:1px;
border-style:solid;
border-color:buttonhighlight buttonshadow buttonshadow buttonhighlight;
font-size:0.4em;
padding:1px 4px 2px 4px;
cursor:default;
}
.pressed {
border-color:buttonshadow buttonhighlight buttonhighlight buttonshadow;
}
</style>
<script type="text/javascript">
YAHOO.util.Event.on(window, "load", function() {
YAHOO.widget.Logger.enableBrowserConsole();
var inc = new Ext.HoldButton( {
el:"inc",
delay:1000,
pressClass:"pressed",
immediate: true
});
var dec = new Ext.HoldButton( {
el:"dec",
delay:1000,
pressClass:"pressed",
immediate: true
});

var c = document.getElementById("counter");
inc.on("fire", spin.createDelegate(c, [1]));
dec.on("fire", spin.createDelegate(c, [-1]));
});
function spin(p)
{
var v = parseInt(this.value);
if (isNaN(v)) v = 0;
this.value = v + p;
}
</script>
</head>
<body>


Click and hold the spinner buttons to autorepeat</p>
<table cellspacing="0" cellpadding="0">
<tr>
<td rowspan="2">
<input id="counter" value="0">
</td>
<td>
<span class="spinner-button" id="inc">▲</span>
<td>
</tr>
<tr>
<td>
<span class="spinner-button" id="dec">▼</span>
</td>
<tr>
</table>
</body>
</html>

brian.moeskau
01-27-2007, 11:37 AM
Hey Animal,

Pretty cool buttons. Btw, I'm not sure where your little arrows came from, but I can't find them in HTML refs and they don't copy (they paste as ? for me). I replaced them with + and - and set the font to courier for demo purposes, and that works OK. Also, I would remove the dependency on Logger by default, since it's only used in the single catch anyway. One less thing to configure (I just commented it out to get it working -- I don't ever use YUI logger!).

Thanks!

Animal
01-27-2007, 12:29 PM
Uparrow is ampersand #9650;

Downarrow is ampersand #9660;

The code I pasted contained the correctly formatted HTML entities, but it looks like the PHPBB code tag doesn't make ampersands safe, and the actual glyphs end up in the page. I can't post anything that looks like HTML entities either! But I'm sure you get the idea!

brian.moeskau
01-27-2007, 12:56 PM
Hey, cool -- that's much better! I actually looked briefly at a couple of html refs, but did not see those specific arrow codes, and even in source view they still displayed as arrows. The control works really nicely, thanks again.

Animal
01-29-2007, 10:14 AM
Bit of help needed here from any mathematics whizzes out there. I'm just a coder - no mathematics!

I'm trying to get the HoldButton to do easing in. That is, start repeating slowly, and accelerate the rate of firing the longer the button is held.

So I now have


/** @private */
fire : function() {
this.fireEvent("fire", this);
this.timer = this.fireDelegate.defer(this.getInterval());
},

getInterval: function()
{
if (!this.accelerate)
return this.interval;
var pressTime = new Date().getTime() - this.mousedownTime;
return this.interval; // How can I calculate gradually decreasing intervals?
},


The interval can be calculated, but I've no idea how to calculate something like this? can anyone help?

brian.moeskau
01-29-2007, 10:41 AM
I'll reply with the obvious... have you tried adapting YUI's anim ocde?
/**
* Begins slowly and accelerates towards end. (quadratic)
* @method easeIn
* @param {Number} t Time value used to compute current value
* @param {Number} b Starting value
* @param {Number} c Delta between start and end values
* @param {Number} d Total length of animation
* @return {Number} The computed value for the current animation frame
*/
easeIn: function (t, b, c, d) {
return c*(t/=d)*t + b;
},

Animal
01-29-2007, 10:54 AM
Yes, I looked at that code.

It's based upon there being a defined time in which the animation must complete.

In my code, I'm coming from the other side - the delay depends upon already elapsed time.

There might be a relationship between these two problems, and the solution might lie in a similar algorithm, but I'm too much of a mathmatical fud to figure it out!

TommyMaintz
01-29-2007, 11:05 AM
Have you tried something like


/** @private */
fire : function() {
this.fireEvent("fire", this);
this.timer = this.fireDelegate.defer(this.getInterval());
},

getInterval: function()
{
if (!this.accelerate)
return this.interval;
var pressTime = new Date().getTime() - this.mousedownTime;
this.interval *= 1 - (this.mousedownTime/10000);
return this.interval;

// you should maybe set a stop somewhere cause it will go too fast in the end :)
// if this calculation doesnt suit maybe try an exponential function? those tend to give nice curves.
}


this will give you something like this (with a starting interval of 1s)
time elapsed interval
1s 900ms
1,9s 729ms
2,629s 537ms

Animal
01-29-2007, 11:46 AM
Thanks for that. It does start slowly and accelerate, but it starts too slowly, then soon accelerates too quickly. I'd like something that has a nice curve. I'll keep thinking about this. There must be something from "O" level maths that I can remember!

TommyMaintz
01-29-2007, 12:33 PM
Ok, here is another try using an exponential function


/** @private */
fire : function() {
this.fireEvent("fire", this);
this.timer = this.fireDelegate.defer(this.getInterval());
},

getInterval: function()
{
if (!this.accelerate)
return this.interval;
var pressTime = new Date().getTime() - this.mousedownTime;

this.interval *= Math.pow(0.9, pressTime/1000);
return this.interval;
}


You can make it go faster by lowering the 0.9 value.
Btw, its harder then i thought to make such a formula without being able to test it immediatly :p

simeon
01-29-2007, 03:33 PM
I have used a simple addition technique in the past to accelerate and decelerate during page up and page down in a div with overflow. It can help you generate the acceleration curve you are looking for simply.

Its based on the following concept:
1 + 2 + 3 + 4 + 5 + 6 +7 + 8 + 9 + 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 100

You loop through the number sequence summing the numbers. That will give you a percentage of some base incriment to scroll, move, etc. during each iteration.

example:
A div container is 100 px high with style="overflow-y:scroll". Content in the div is 1000px high.

If you wanted to scroll the content in that div by 100 px but you wanted to accelerate it and decelerate it so your eye can follow your current position in the text as it scrolls.

user clicks page down:
method will loop until the div has been scrolled at least 100 px.
loop#1: 1 x 100px = 1 px height change. wait 100ms for timeout to start second loop
loop#2: (1+2) x 100px = 3 px height change. wait 100ms for timeout to start third loop
loop#3: (1+2+3) x 100px = 6 px height change
Rinse and repeat...you get the idea.

the math smoothly approximates acceleration and deceleration, but you can stay with the ration at the midpoint to maintain a max speed until the mouse is released. Setting a timeout for each loop controls the speed of scroll. shorter timeout = faster scroll.

For your use case you could use the first half of the math (1 -> 10) on mousedown and the second half on mouse up (10 -> 1)

Simeon

Animal
01-30-2007, 05:12 AM
OK, I've come up with a "brute force" method of accelerating the repeat, it's not very smooth, but in my spinner test, it feels just about OK:


getInterval: function()
{
if (!this.accelerate)
return this.interval;
var pressTime = new Date().getTime() - this.mousedownTime;
if (pressTime < 500)
return 400;
else if (pressTime < 1700)
return 320;
else if (pressTime < 2600)
return 250;
else if (pressTime < 3500)
return 180;
else if (pressTime < 4400)
return 140;
else if (pressTime < 5300)
return 80;
else if (pressTime < 6200)
return 50;
else
return 10;
},

Animal
01-30-2007, 09:44 AM
The latest version also has the "mousedown" and "mouseup" events. This come in handy. For example, creating a numeric spinner, you must fire the input field's change event on mouseup of the spinner buttons.


/**
@class Ext.HoldButton
@extends Ext.util.Observable

A wrapper class which can be applied to any element. Fires a "fire" event while the
mouse is pressed. The interval between firings may be specified in the config but
defaults to 10 milliseconds.

Optionally, a CSS class may be applied to the element during the time it is pressed.

@config el The element to act as a button.
@config delay The initial delay before the repeating event begins firing.
Similar to an autorepeat key delay.
@config immediate If an initial delay is requested, force one immediate fire on mousedown
@config interval The interval between firings of the "fire" event. Default 10 ms.
@config pressClass A CSS class name to be applied to the element while pressed.
@config accelerate True if autorepeating should start slowly and accelerate.
* "interval" and "delay" are ignored. "immediate" is honoured.
*/
Ext.HoldButton = function(config)
{
Ext.util.Config.apply(this, config, {interval: 10});
this.el = getEl(this.el);
this.el.unselectable();
this.events = {
/**
* @event mousedown
* Fires when the mouse button is depressed.
* @param {Ext.HoldButton} this
*/
'mousedown' : new YAHOO.util.CustomEvent('mousedown'),
/**
* @event fire
* Fires on a specified interval during the time the element is pressed.
* @param {Ext.HoldButton} this
*/
'fire' : new YAHOO.util.CustomEvent('fire'),
/**
* @event mouseup
* Fires when the mouse key is released.
* @param {Ext.HoldButton} this
*/
'mouseup' : new YAHOO.util.CustomEvent('mouseup')
};
this.fireDelegate = this.fire.createDelegate(this);
this.el.on('mousedown', this.handleMouseDown, this, true);
};

Ext.extend(Ext.HoldButton, Ext.util.Observable, {

/** @private */
docEl : getEl(document),

/** @private */
handleMouseDown : function() {
try {
this.el.dom.blur();
}
catch (e) {}
if (this.pressClass)
this.el.addClass(this.pressClass);
this.mousedownTime = new Date().getTime();
this.fireEvent("mousedown", this);
if (this.delay) {
if (this.immediate) {
this.fireEvent("fire", this);
}
this.timer = this.fireDelegate.defer(this.delay);
}
else
this.fireDelegate();
this.docEl.on("mouseup", this.handleMouseUp, this, true);
this.el.on("mouseout", this.handleMouseOut, this, true);
},

/** @private */
fire : function() {
this.fireEvent("fire", this);
this.timer = this.fireDelegate.defer(this.getInterval());
},

getInterval: function()
{
if (!this.accelerate)
return this.interval;
var pressTime = new Date().getTime() - this.mousedownTime;
if (pressTime < 500)
return 400;
else if (pressTime < 1700)
return 320;
else if (pressTime < 2600)
return 250;
else if (pressTime < 3500)
return 180;
else if (pressTime < 4400)
return 140;
else if (pressTime < 5300)
return 80;
else if (pressTime < 6200)
return 50;
else
return 10;
},

/** @private */
handleMouseOut : function() {
clearTimeout(this.timer);
if (this.pressClass)
this.el.removeClass(this.pressClass);
this.el.on("mouseover", this.handleMouseReturn, this, true);
},

/** @private */
handleMouseReturn : function() {
this.el.removeListener("mouseover", this.handleMouseReturn);
if (this.pressClass)
this.el.addClass(this.pressClass);
this.fireDelegate();
},

/** @private */
handleMouseUp : function() {
clearTimeout(this.timer);
this.el.removeListener("mouseover", this.handleMouseReturn);
this.el.removeListener("mouseout", this.handleMouseOut);
this.docEl.removeListener("mouseup", this.handleMouseUp);
this.el.removeClass(this.pressClass);
this.fireEvent("mouseup", this);
}
});

Animal
01-30-2007, 11:33 AM
So using that, it's simple to create a Spinner class, here embedded in a test page:


<html>
<head>
<script type="text/javascript" src="js/utilities.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="js/yui-ext.js"></script>
<script type="text/javascript" src="js/HoldButton.js"></script>
<title>Test HoldButton</title>
<style type="text/css">
.red {
background-color:red;
}
.yui-spinner-button {
background-color:buttonface;
border-width:1px;
border-style:solid;
border-color:buttonhighlight buttonshadow buttonshadow buttonhighlight;
font-size:0.4em;
padding:1px 4px 2px 4px;
cursor:default;
}
.yui-button-pressed {
border-color:buttonshadow buttonhighlight buttonhighlight buttonshadow;
}
</style>
<script type="text/javascript">
String.prototype.startsWith = function(prefix)
{
return this.substr(0, prefix.length) == prefix;
};

fireEvent = function(targetElement, eventType)
{
targetElement = YAHOO.util.Dom.get(targetElement);
eventType = eventType.toLowerCase();
if (document.createEvent)
{
if (eventType.startsWith("on"))
{
eventType = eventType.substr(2);
}
var changeEvent = document.createEvent("Events");
changeEvent.initEvent(eventType, false, true);
targetElement.dispatchEvent(changeEvent);
}
else if (document.createEventObject)
{
if (!eventType.startsWith("on"))
{
eventType = "on" + eventType;
}
targetElement.fireEvent(eventType, document.createEventObject());
}
};

YAHOO.util.Event.on(window, "load", function() {
new Ext.Spinner("counter", function (p) {
var v = parseInt(this.value);
if (isNaN(v)) v = 0;
this.value = v + p;
});
var c = getEl("counter");
c.on("change", function(){alert("Changed to: " + c.dom.value)});
});

/**
@class Ext.Spinner

Wraps a given input field and adds up/down buttons and scroll wheel incrementing.
Fires the field's onchange event 500 milliseconds after the last spin/scroll was made.

@constructor
@param el {String|HtmlElement|Element} The input field
@param incDecFn {Function} The function to increment or decrement the field.
The function is called in the scope of the input field, and is passed +1 or -1
*/
Ext.Spinner = function(el, incDecFn) {
this.input = getEl(el);
this.table = Ext.DomHelper.insertBefore(this.input, {
tag: "table",
cellspacing: "0",
cellpadding: "0",
children: [ {
tag: "tr",
children: [ {
tag: "td",
rowspan: "2",
},
{
tag: "td",
children: [ {
tag: "span",
cls: "yui-spinner-button",
html: "&" + "#9650;"
}]
}]
},
{
tag: "tr",
children: [ {
tag: "td",
children: [ {
tag: "span",
cls: "yui-spinner-button",
html: "&" + "#9660;"
}]
}]
}]
}, true);
this.table.dom.firstChild.firstChild.firstChild.appendChild(this.input.dom);
var inc = new Ext.HoldButton( {
el: this.table.dom.firstChild.firstChild.childNodes[1].firstChild,
pressClass: "yui-button-pressed",
immediate: true,
accelerate: true
});
var dec = new Ext.HoldButton( {
el: this.table.dom.firstChild.childNodes[1].firstChild.firstChild,
pressClass:"yui-button-pressed",
immediate: true,
accelerate: true
});

// Monitor scrollwheel. 500ms after stopping scrolling, fire the field's onchange
this.incDecFn = incDecFn;
this.input.mon("mousewheel", this.onWheel, this, true);

// Monitor incr/decr buttons.
inc.on("fire", incDecFn.createDelegate(this.input.dom, [1]));
dec.on("fire", incDecFn.createDelegate(this.input.dom, [-1]));

// 500ms after stopping spinning, fire the field's onchange
inc.on("mouseup", this.onMouseup, this, true);
dec.on("mouseup", this.onMouseup, this, true);

// Mousedown cancels a pending onchange
inc.on("mousedown", this.onMousedown, this, true);
dec.on("mousedown", this.onMousedown, this, true);
};
Ext.Spinner.prototype = {

onWheel: function(e) {
if (this.onchangeTimer)
clearTimeout(this.onchangeTimer);
this.incDecFn.call(this.input.dom, Math.round(e.getWheelDelta()));
this.onchangeTimer = this.fireOnchange.defer(500, this);
},

onMousedown: function() {
if (this.onchangeTimer)
clearTimeout(this.onchangeTimer);
},

onMouseup: function() {
this.onchangeTimer = this.fireOnchange.defer(500, this);
},

fireOnchange: function() {
delete this.onchangeTimer;
fireEvent(this.input.dom, "change");
}
};
</script>
</head>
<body>


Click and hold the spinner buttons to autorepeat</p>
<input id="counter" value="0">
</body>
</html>

jack.slocum
02-03-2007, 12:40 AM
I pulled this code in. I had some problems in FF Mac and Safari so I did a little refactoring. Please let me know what you think. There are also some standard default values now. A delay on the mousedown is imperative on the Mac FF and Safari otherwise you can't get a single click. Here's the code, although it uses some newer Ext 1.0 stuff:

The @history doc item is something new I am adding specifically so people who contributed stuff directly can get their attribution and so file edit history can be tracked.

el and config were split to stick with the Ext standard.

Event renamed to click to to go with the new ClickRepeater name (that ok?).

Adding stopClick cfg opion (useful for bogus links).

Immediate option removed (doesnt work as expected on Mac).


/**
@class Ext.util.ClickRepeater
@extends Ext.util.Observable

A wrapper class which can be applied to any element. Fires a "click" event while the
mouse is pressed. The interval between firings may be specified in the config but
defaults to 10 milliseconds.

Optionally, a CSS class may be applied to the element during the time it is pressed.

@cfg {String/HTMLElement/Element} el The element to act as a button.
@cfg {Number} delay The initial delay before the repeating event begins firing.
Similar to an autorepeat key delay.
@cfg {Number} interval The interval between firings of the "fire" event. Default 10 ms.
@cfg {String} pressClass A CSS class name to be applied to the element while pressed.
@cfg {Boolean} accelerate True if autorepeating should start slowly and accelerate.
"interval" and "delay" are ignored. "immediate" is honored.
@cfg {Boolean} stopClick True to stop the default click event

@history
2007-02-02 jvs Original code contributed by Nige "Animal" White
2007-02-02 jvs Renamed to ClickRepeater
2007-02-03 jvs Modifications for FF Mac and Safari

@constructor
@param {String/HTMLElement/Element} el The element to listen on
@param {Object} config
*/
Ext.util.ClickRepeater = function(el, config)
{
this.el = Ext.get(el);
this.el.unselectable();

Ext.apply(this, config);

this.events = {
/**
* @event mousedown
* Fires when the mouse button is depressed.
* @param {Ext.util.ClickRepeater} this
*/
'mousedown' : true,
/**
* @event click
* Fires on a specified interval during the time the element is pressed.
* @param {Ext.util.ClickRepeater} this
*/
'click' : true,
/**
* @event mouseup
* Fires when the mouse key is released.
* @param {Ext.util.ClickRepeater} this
*/
'mouseup' : true
};

this.el.on('mousedown', this.handleMouseDown, this, true);
if(this.stopClick){
this.el.swallowEvent('click', true);
}

// allow inline handler
if(this.handler){
this.on('click', this.handler, this.scope || this, true);
}
};

Ext.extend(Ext.util.ClickRepeater, Ext.util.Observable, {
interval : 20,
delay: 250,
stopClick : true,
timer : 0,
docEl : Ext.get(document),

handleMouseDown : function(){
clearTimeout(this.timer);
this.el.blur();
if(this.pressClass){
this.el.addClass(this.pressClass);
}
this.mousedownTime = new Date();

this.docEl.on("mouseup", this.handleMouseUp, this, true);
this.el.on("mouseout", this.handleMouseOut, this, true);

this.fireEvent("mousedown", this);
this.fireEvent("click", this);

this.timer = this.click.defer(this.delay || this.interval, this);
},

click : function(){
this.fireEvent("click", this);
this.timer = this.click.defer(this.getInterval(), this);
},

getInterval: function(){
if(!this.accelerate){
return this.interval;
}
var pressTime = this.mousedownTime.getElapsed();
if(pressTime < 500){
return 400;
}else if(pressTime < 1700){
return 320;
}else if(pressTime < 2600){
return 250;
}else if(pressTime < 3500){
return 180;
}else if(pressTime < 4400){
return 140;
}else if(pressTime < 5300){
return 80;
}else if(pressTime < 6200){
return 50;
}else{
return 10;
}
},

handleMouseOut : function(){
clearTimeout(this.timer);
if(this.pressClass){
this.el.removeClass(this.pressClass);
}
this.el.on("mouseover", this.handleMouseReturn, this, true);
},

handleMouseReturn : function(){
this.el.removeListener("mouseover", this.handleMouseReturn);
if(this.pressClass){
this.el.addClass(this.pressClass);
}
this.click();
},

handleMouseUp : function(){
clearTimeout(this.timer);
this.el.removeListener("mouseover", this.handleMouseReturn);
this.el.removeListener("mouseout", this.handleMouseOut);
this.docEl.removeListener("mouseup", this.handleMouseUp);
this.el.removeClass(this.pressClass);
this.fireEvent("mouseup", this);
}
});

aconran
02-03-2007, 12:46 AM
Very cool, I'd also like to see the PreferenceDialog which I think this was initially written for added to Ext. Again, maybe you could name it something a little more generic

PreferenceDialog: http://www.yui-ext.com/forum/viewtopic.php?t=2320&highlight=preference

Animal
02-03-2007, 04:19 AM
Nice job. Could this behaviour be added as an option to Ext.Button?

I think I see one problem wit hthis code though! The problem with having interval, delay and stopClick in the prototype is that they're shared.

OK, I know that only 1 button will be firing at once, but if an option is set through the config in one button, and left as default in another, then "another" will end up using the first one's set option.

"timer" of course can be shared because that's transient.

jack.slocum
02-03-2007, 04:53 AM
> I think I see one problem wit hthis code though! The problem with having interval, delay and stopClick in the prototype is that they're shared.

Right, they share the default value if they aren't specified. If one is specified, then the property on that instance (and only that instance) is set. It doesn't overwrite the prototype value or change the value on other instances.

You wouldn't want to put an object (like a CustomEvent) on the prototype because you don't want to share it. But intrinsic values are fine. This is actually in use throughout Ext. ;)

Think of it like this - when you call this.foo = 3 (similar to Ext.apply) , it is not the same as calling SomeClass.prototype.foo = 3. It only sets the value on this instance. But if foo was an object and you called this.foo.someValue = 3, then that would indeed be shared across instances.

jack.slocum
02-03-2007, 09:42 AM
Small changes to stopClick (split into prevent + stop).


/**
@class Ext.util.ClickRepeater
@extends Ext.util.Observable

A wrapper class which can be applied to any element. Fires a "click" event while the
mouse is pressed. The interval between firings may be specified in the config but
defaults to 10 milliseconds.

Optionally, a CSS class may be applied to the element during the time it is pressed.

@cfg {String/HTMLElement/Element} el The element to act as a button.
@cfg {Number} delay The initial delay before the repeating event begins firing.
Similar to an autorepeat key delay.
@cfg {Number} interval The interval between firings of the "fire" event. Default 10 ms.
@cfg {String} pressClass A CSS class name to be applied to the element while pressed.
@cfg {Boolean} accelerate True if autorepeating should start slowly and accelerate.
"interval" and "delay" are ignored. "immediate" is honored.
@cfg {Boolean} preventDefault True to prevent the default click event
@cfg {Boolean} stopDefault True to stop the default click event

@history
2007-02-02 jvs Original code contributed by Nige "Animal" White
2007-02-02 jvs Renamed to ClickRepeater
2007-02-03 jvs Modifications for FF Mac and Safari

@constructor
@param {String/HTMLElement/Element} el The element to listen on
@param {Object} config
*/
Ext.util.ClickRepeater = function(el, config)
{
this.el = Ext.get(el);
this.el.unselectable();

Ext.apply(this, config);

this.events = {
/**
* @event mousedown
* Fires when the mouse button is depressed.
* @param {Ext.util.ClickRepeater} this
*/
'mousedown' : true,
/**
* @event click
* Fires on a specified interval during the time the element is pressed.
* @param {Ext.util.ClickRepeater} this
*/
'click' : true,
/**
* @event mouseup
* Fires when the mouse key is released.
* @param {Ext.util.ClickRepeater} this
*/
'mouseup' : true
};

this.el.on('mousedown', this.handleMouseDown, this, true);
if(this.preventDefault || this.stopDefault){
this.el.mon('click', function(e){
if(this.preventDefault){
e.preventDefault();
}
if(this.stopDefault){
e.stopEvent();
}
}, this, true);
}

// allow inline handler
if(this.handler){
this.on('click', this.handler, this.scope || this, true);
}
};

Ext.extend(Ext.util.ClickRepeater, Ext.util.Observable, {
interval : 20,
delay: 250,
preventDefault : true,
stopDefault : false,
timer : 0,
docEl : Ext.get(document),

handleMouseDown : function(){
clearTimeout(this.timer);
this.el.blur();
if(this.pressClass){
this.el.addClass(this.pressClass);
}
this.mousedownTime = new Date();

this.docEl.on("mouseup", this.handleMouseUp, this, true);
this.el.on("mouseout", this.handleMouseOut, this, true);

this.fireEvent("mousedown", this);
this.fireEvent("click", this);

this.timer = this.click.defer(this.delay || this.interval, this);
},

click : function(){
this.fireEvent("click", this);
this.timer = this.click.defer(this.getInterval(), this);
},

getInterval: function(){
if(!this.accelerate){
return this.interval;
}
var pressTime = this.mousedownTime.getElapsed();
if(pressTime < 500){
return 400;
}else if(pressTime < 1700){
return 320;
}else if(pressTime < 2600){
return 250;
}else if(pressTime < 3500){
return 180;
}else if(pressTime < 4400){
return 140;
}else if(pressTime < 5300){
return 80;
}else if(pressTime < 6200){
return 50;
}else{
return 10;
}
},

handleMouseOut : function(){
clearTimeout(this.timer);
if(this.pressClass){
this.el.removeClass(this.pressClass);
}
this.el.on("mouseover", this.handleMouseReturn, this, true);
},

handleMouseReturn : function(){
this.el.removeListener("mouseover", this.handleMouseReturn);
if(this.pressClass){
this.el.addClass(this.pressClass);
}
this.click();
},

handleMouseUp : function(){
clearTimeout(this.timer);
this.el.removeListener("mouseover", this.handleMouseReturn);
this.el.removeListener("mouseout", this.handleMouseOut);
this.docEl.removeListener("mouseup", this.handleMouseUp);
this.el.removeClass(this.pressClass);
this.fireEvent("mouseup", this);
}
});

jack.slocum
02-03-2007, 09:47 AM
BTW, this is getElapsed(). I got sick of typing new Date().getTime()-otherDate.getTime(). A better name wound probably be diff() but I didn't want inspire feature requests. ;)


/*
Returns the number of milliseconds between this date and date
@param {Date} date (optional) Defaults to now
@return {Number} The diff in milliseconds
*/
Date.prototype.getElapsed = function(date) {
return Math.abs((date || new Date()).getTime()-this.getTime());
};

verkniss
10-30-2007, 05:58 PM
Hey guys,

I didn't really understand how to use this class, at start of the thread I saw you
have used "YAHOO.util.Event.on" to "register" the event and later on, you removed those lines from the code.

How should I use this class? I would really iappreciate it, if someone can give an example of how using this class.
Just to mention, I need this event to create 2 buttons for zoom out/in an image :)

Thank you very much.

Animal
10-31-2007, 09:21 AM
Use Ext.util.ClickRepeater. It's fully documented.

verkniss
11-02-2007, 04:04 AM
I have took a look at Ext.util.ClickRepeater and still didn't really understand how to define the event. a little example will really be helpful :)

Animal
11-02-2007, 10:57 AM
You don't define any event, just create a ClickRepeater out of any HTML element, and add a listener for its "click" event just the same as you would add a listener on any Observable.

verkniss
11-03-2007, 11:23 AM
<html>
<head>
<script type="text/javascript" src="ext.js"></script>
<script type="text/javascript">
function scaleIt(v)
{
var scalePhoto = document.getElementById("theImage");
var originalWidth = document.getElementById("imageOriginalWidth").value;

document.getElementById("Zoom").value = v;

v = originalWidth * (v / 100);
scalePhoto.style.width = Math.floor(v) + 'px';
}
window.onload = function()
{
var pic = document.getElementById("theImage");
document.getElementById("imageOriginalWidth").value = pic.offsetWidth;

onMouseDown = function(){
var v = parseInt(document.getElementById("Zoom").value) + 5;
scaleIt(v);
};

var el = Ext.get('xxx');
var repeater = new Ext.util.ClickRepeater(el,{interval:5});
repeater.on('mousedown', onMouseDown );
}
</script>
</head>
<body>
original size <input type="text" id="imageOriginalWidth"/>
zoom <input type="text" id="Zoom" value="100"/>
<input id="xxx" type="button"/>
<img id="theImage">
</body>
</html>


I have already tryed what you said, but it just doesn't resize the image when I hold the mouse button. Can you tell me what i'm doing wrong?

Animal
11-03-2007, 12:03 PM
It handles mousedown for you. That's the whole point of it. It fires "click" events as for as long as you hold the mouse down. click events. It's a ClickRepeater.

Animal
11-03-2007, 12:07 PM
Resizing images???

Ext has classes for most things: http://www.extjs.com/deploy/dev/examples/resizable/basic.html

You should read some tutorials, go through the examples, and poke at the example code live using Firebug.

Ext.getDom() is easiest for accessing basic DOM elements. To get something that is actually useful, use Ext.get which returns a wrapper class for DOM elements which has a lot of capabilities: http://www.extjs.com/deploy/dev/docs/?class=Ext.Element

verkniss
11-03-2007, 12:25 PM
I'm not sure you didnt really understand what the purpose of what I'm trying to do.
I want to create a button who zoom the image when I hold the button down.