Skip to content

Instantly share code, notes, and snippets.

@alistairstead
Created April 10, 2012 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alistairstead/2351965 to your computer and use it in GitHub Desktop.
Save alistairstead/2351965 to your computer and use it in GitHub Desktop.
Spinner.html
<!DOCTYPE html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Widget</title>
<script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script>
<link rel="stylesheet" href="http://yuilibrary.com/yui/docs/assets/node/node.css">
<style type="text/css" scoped>
.yui3-js-enabled .yui3-spinner-loading {
display:none;
}
.yui3-spinner-hidden {
display:none;
}
.yui3-spinner {
display:-moz-inline-stack;
display:inline-block;
zoom:1;
*display:inline;
vertical-align:middle;
}
.yui3-spinner-content {
padding:1px;
position:relative;
}
.yui3-spinner-value {
width:2em;
height:1.5em;
text-align:right;
margin-right:22px;
vertical-align:top;
border:1px solid #000;
padding:2px;
}
.yui3-spinner-increment, .yui3-spinner-decrement {
position:absolute;
height:1em;
width:22px;
overflow:hidden;
text-indent:-10em;
border:1px solid #999;
margin:0;
padding:0px;
}
.yui3-spinner-increment {
top:1px;
*top:2px;
right:1px;
background:#ddd url(http://yuilibrary.com/yui/docs/assets/widget/arrows.png) no-repeat 50% 0px;
}
.yui3-spinner-decrement {
bottom:1px;
*bottom:2px;
right:1px;
background:#ddd url(http://yuilibrary.com/yui/docs/assets/widget/arrows.png) no-repeat 50% -20px;
}
#widget-extend-example {
padding:5px;
}
#widget-extend-example .hint {
margin-top:10px;
font-size:85%;
color:#00a;
}
</style>
</head>
<body>
<div id="widget-extend-example">
A basic spinner widget: <input type="text" id="numberField" class="yui3-spinner-loading" value="50" />
<p class="hint">Click the buttons, or the arrow up/down and page up/down keys on your keyboard to change the spinner's value</p>
<div id="new-spinner">Spinner with no markup</div>
</div>
<script type="text/javascript">
YUI().use("event-key", "widget", function(Y) {
var Lang = Y.Lang,
Widget = Y.Widget,
Node = Y.Node;
/* Spinner class constructor */
function Spinner(config) {
Spinner.superclass.constructor.apply(this, arguments);
}
/*
* Required NAME static field, to identify the Widget class and
* used as an event prefix, to generate class names etc. (set to the
* class name in camel case).
*/
Spinner.NAME = "spinner";
/*
* The attribute configuration for the Spinner widget. Attributes can be
* defined with default values, get/set functions and validator functions
* as with any other class extending Base.
*/
Spinner.ATTRS = {
// The minimum value for the spinner.
min : {
value:0
},
// The maximum value for the spinner.
max : {
value:100
},
// The current value of the spinner.
value : {
value:0,
validator: function(val) {
return this._validateValue(val);
}
},
// Amount to increment/decrement the spinner when the buttons or arrow up/down keys are pressed.
minorStep : {
value:1
},
// Amount to increment/decrement the spinner when the page up/down keys are pressed.
majorStep : {
value:10
},
// override default ("null"), required for focus()
tabIndex: {
value: 0
},
// The strings for the spinner UI. This attribute is
// defined by the base Widget class but has an empty value. The
// spinner is simply providing a default value for the attribute.
strings: {
value: {
tooltip: "Press the arrow up/down keys for minor increments, page up/down for major increments.",
increment: "Increment",
decrement: "Decrement"
}
}
};
/* Static constant used to identify the classname applied to the spinners value field */
Spinner.INPUT_CLASS = Y.ClassNameManager.getClassName(Spinner.NAME, "value");
/* Static constants used to define the markup templates used to create Spinner DOM elements */
Spinner.INPUT_TEMPLATE = '<input type="text" class="' + Spinner.INPUT_CLASS + '">';
Spinner.BTN_TEMPLATE = '<button type="button"></button>';
/*
* The HTML_PARSER static constant is used by the Widget base class to populate
* the configuration for the spinner instance from markup already on the page.
*
* The Spinner class attempts to set the value of the spinner widget if it
* finds the appropriate input element on the page.
*/
Spinner.HTML_PARSER = {
value: function (srcNode) {
var val = parseInt(srcNode.get("value"));
return Y.Lang.isNumber(val) ? val : null;
}
};
/* Spinner extends the base Widget class */
Y.extend(Spinner, Widget, {
/*
* initializer is part of the lifecycle introduced by
* the Widget class. It is invoked during construction,
* and can be used to setup instance specific state.
*
* The Spinner class does not need to perform anything
* specific in this method, but it is left in as an example.
*/
initializer: function() {
// Not doing anything special during initialization
},
/*
* destructor is part of the lifecycle introduced by
* the Widget class. It is invoked during destruction,
* and can be used to cleanup instance specific state.
*
* The spinner cleans up any node references it's holding
* onto. The Widget classes destructor will purge the
* widget's bounding box of event listeners, so spinner
* only needs to clean up listeners it attaches outside of
* the bounding box.
*/
destructor : function() {
this._documentMouseUpHandle.detach();
this.inputNode = null;
this.incrementNode = null;
this.decrementNode = null;
},
/*
* renderUI is part of the lifecycle introduced by the
* Widget class. Widget's renderer method invokes:
*
* renderUI()
* bindUI()
* syncUI()
*
* renderUI is intended to be used by the Widget subclass
* to create or insert new elements into the DOM.
*
* For spinner the method adds the input (if it's not already
* present in the markup), and creates the inc/dec buttons
*/
renderUI : function() {
this._renderInput();
this._renderButtons();
},
/*
* bindUI is intended to be used by the Widget subclass
* to bind any event listeners which will drive the Widget UI.
*
* It will generally bind event listeners for attribute change
* events, to update the state of the rendered UI in response
* to attribute value changes, and also attach any DOM events,
* to activate the UI.
*
* For spinner, the method:
*
* - Sets up the attribute change listener for the "value" attribute
*
* - Binds key listeners for the arrow/page keys
* - Binds mouseup/down listeners on the boundingBox, document respectively.
* - Binds a simple change listener on the input box.
*/
bindUI : function() {
this.after("valueChange", this._afterValueChange);
var boundingBox = this.get("boundingBox");
// Looking for a key event which will fire continously across browsers while the key is held down. 38, 40 = arrow up/down, 33, 34 = page up/down
var keyEventSpec = (!Y.UA.opera) ? "down:" : "press:";
keyEventSpec += "38, 40, 33, 34";
Y.on("key", Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec);
Y.on("mousedown", Y.bind(this._onMouseDown, this), boundingBox);
this._documentMouseUpHandle = Y.on("mouseup", Y.bind(this._onDocMouseUp, this), boundingBox.get("ownerDocument"));
Y.on("change", Y.bind(this._onInputChange, this), this.inputNode);
},
/*
* syncUI is intended to be used by the Widget subclass to
* update the UI to reflect the current state of the widget.
*
* For spinner, the method sets the value of the input field,
* to match the current state of the value attribute.
*/
syncUI : function() {
this._uiSetValue(this.get("value"));
},
/*
* Creates the input control for the spinner and adds it to
* the widget's content box, if not already in the markup.
*/
_renderInput : function() {
var contentBox = this.get("contentBox"),
input = contentBox.one("." + Spinner.INPUT_CLASS),
strings = this.get("strings");
if (!input) {
input = Node.create(Spinner.INPUT_TEMPLATE);
contentBox.appendChild(input);
}
input.set("title", strings.tooltip);
this.inputNode = input;
},
/*
* Creates the button controls for the spinner and add them to
* the widget's content box, if not already in the markup.
*/
_renderButtons : function() {
var contentBox = this.get("contentBox"),
strings = this.get("strings");
var inc = this._createButton(strings.increment, this.getClassName("increment"));
var dec = this._createButton(strings.decrement, this.getClassName("decrement"));
this.incrementNode = contentBox.appendChild(inc);
this.decrementNode = contentBox.appendChild(dec);
},
/*
* Utility method, to create a spinner button
*/
_createButton : function(text, className) {
var btn = Y.Node.create(Spinner.BTN_TEMPLATE);
btn.set("innerHTML", text);
btn.set("title", text);
btn.addClass(className);
return btn;
},
/*
* Bounding box mouse down handler. Will determine if the mouse down
* is on one of the spinner buttons, and increment/decrement the value
* accordingly.
*
* The method also sets up a timer, to support the user holding the mouse
* down on the spinner buttons. The timer is cleared when a mouse up event
* is detected.
*/
_onMouseDown : function(e) {
var node = e.target,
dir,
handled = false,
currVal = this.get("value"),
minorStep = this.get("minorStep");
if (node.hasClass(this.getClassName("increment"))) {
this.set("value", currVal + minorStep);
dir = 1;
handled = true;
} else if (node.hasClass(this.getClassName("decrement"))) {
this.set("value", currVal - minorStep);
dir = -1;
handled = true;
}
if (handled) {
this._setMouseDownTimers(dir, minorStep);
}
},
/*
* Override the default content box value, since we don't want the srcNode
* to be the content box for spinner.
*/
_defaultCB : function() {
return null;
},
/*
* Document mouse up handler. Clears the timers supporting
* the "mouse held down" behavior.
*/
_onDocMouseUp : function(e) {
this._clearMouseDownTimers();
},
/*
* Bounding box Arrow up/down, Page up/down key listener.
*
* Increments/Decrement the spinner value, based on the key pressed.
*/
_onDirectionKey : function(e) {
e.preventDefault();
var currVal = this.get("value"),
newVal = currVal,
minorStep = this.get("minorStep"),
majorStep = this.get("majorStep");
switch (e.charCode) {
case 38:
newVal += minorStep;
break;
case 40:
newVal -= minorStep;
break;
case 33:
newVal += majorStep;
newVal = Math.min(newVal, this.get("max"));
break;
case 34:
newVal -= majorStep;
newVal = Math.max(newVal, this.get("min"));
break;
}
if (newVal !== currVal) {
this.set("value", newVal);
}
},
/*
* Simple change handler, to make sure user does not input an invalid value
*/
_onInputChange : function(e) {
if (!this._validateValue(this.inputNode.get("value"))) {
this.syncUI();
}
},
/*
* Initiates mouse down timers, to increment slider, while mouse button
* is held down
*/
_setMouseDownTimers : function(dir, step) {
this._mouseDownTimer = Y.later(500, this, function() {
this._mousePressTimer = Y.later(100, this, function() {
this.set("value", this.get("value") + (dir * step));
}, null, true)
});
},
/*
* Clears timers used to support the "mouse held down" behavior
*/
_clearMouseDownTimers : function() {
if (this._mouseDownTimer) {
this._mouseDownTimer.cancel();
this._mouseDownTimer = null;
}
if (this._mousePressTimer) {
this._mousePressTimer.cancel();
this._mousePressTimer = null;
}
},
/*
* value attribute change listener. Updates the
* value in the rendered input box, whenever the
* attribute value changes.
*/
_afterValueChange : function(e) {
this._uiSetValue(e.newVal);
},
/*
* Updates the value of the input box to reflect
* the value passed in
*/
_uiSetValue : function(val) {
this.inputNode.set("value", val);
},
/*
* value attribute default validator. Verifies that
* the value being set lies between the min/max value
*/
_validateValue: function(val) {
var min = this.get("min"),
max = this.get("max");
return (Lang.isNumber(val) && val >= min && val <= max);
}
});
// Create a new Spinner instance, drawing it's
// starting value from an input field already on the
// page (the #numberField text box)
var spinner = new Spinner({
srcNode: "#numberField",
max: 100,
min: 0
});
spinner.render();
spinner.focus();
var spinner2 = new Spinner({
srcNode: "#new-spinner",
max: 10,
min: 0
});
spinner2.render();
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment