3 * Downward compatible, touchable dial
6 * Requires: jQuery v1.7+
8 * Copyright (c) 2012 Anthony Terrien
9 * Under MIT License (http://www.opensource.org/licenses/mit-license.php)
11 * Thanks to vor, eskimoblood, spiffistan, FabrizioC
14 if (typeof exports
=== 'object') {
16 module
.exports
= factory(require('jquery'));
17 } else if (typeof define
=== 'function' && define
.amd
) {
18 // AMD. Register as an anonymous module.
19 define(['jquery'], factory
);
32 * Definition of globals and core
34 var k
= {}, // kontrol
40 k
.c
.t = function (e
) {
41 return e
.originalEvent
.touches
.length
- 1;
47 * Definition of an abstract UI control
49 * Each concrete component must call this one.
57 this.o
= null; // array of options
58 this.$ = null; // jQuery wrapped element
59 this.i
= null; // mixed HTMLInputElement or array of HTMLInputElement
60 this.g
= null; // deprecated 2D graphics context for 'pre-rendering'
61 this.v
= null; // value ; mixed array or integer
62 this.cv
= null; // change value ; not commited value
63 this.x
= 0; // canvas x position
64 this.y
= 0; // canvas y position
65 this.w
= 0; // canvas width
66 this.h
= 0; // canvas height
67 this.$c
= null; // jQuery canvas element
68 this.c
= null; // rendered canvas context
69 this.t
= 0; // touches index
71 this.fgColor
= null; // main color
72 this.pColor
= null; // previous color
73 this.dH
= null; // draw hook
74 this.cH
= null; // change hook
75 this.eH
= null; // cancel hook
76 this.rH
= null; // release hook
77 this.scale
= 1; // scale factor
78 this.relative
= false;
79 this.relativeWidth
= false;
80 this.relativeHeight
= false;
81 this.$div
= null; // component div
83 this.run = function () {
84 var cf = function (e
, conf
) {
94 if (this.$.data('kontroled')) return;
95 this.$.data('kontroled', true);
100 min
: this.$.data('min') !== undefined ? this.$.data('min') : 0,
101 max
: this.$.data('max') !== undefined ? this.$.data('max') : 100,
103 readOnly
: this.$.data('readonly') || (this.$.attr('readonly') === 'readonly'),
106 cursor
: this.$.data('cursor') === true && 30
107 || this.$.data('cursor') || 0,
108 thickness
: this.$.data('thickness')
109 && Math
.max(Math
.min(this.$.data('thickness'), 1), 0.01)
111 lineCap
: this.$.data('linecap') || 'butt',
112 width
: this.$.data('width') || 200,
113 height
: this.$.data('height') || 200,
114 displayInput
: this.$.data('displayinput') == null || this.$.data('displayinput'),
115 displayPrevious
: this.$.data('displayprevious'),
116 fgColor
: this.$.data('fgcolor') || '#87CEEB',
117 inputColor
: this.$.data('inputcolor'),
118 font
: this.$.data('font') || 'Arial',
119 fontWeight
: this.$.data('font-weight') || 'bold',
121 step
: this.$.data('step') || 1,
122 rotation
: this.$.data('rotation'),
125 draw
: null, // function () {}
126 change
: null, // function (value) {}
127 cancel
: null, // function () {}
128 release
: null, // function (value) {}
130 // Output formatting, allows to add unit: %, ms ...
131 format: function(v
) {
134 parse: function (v
) {
135 return parseFloat(v
);
141 this.o
.flip
= this.o
.rotation
=== 'anticlockwise' || this.o
.rotation
=== 'acw';
142 if (!this.o
.inputColor
) {
143 this.o
.inputColor
= this.o
.fgColor
;
147 if (this.$.is('fieldset')) {
149 // fieldset = array of integer
151 this.i
= this.$.find('input');
152 this.i
.each(function(k
) {
155 s
.v
[k
] = s
.o
.parse($this.val());
161 val
[k
] = $this.val();
162 s
.val(s
._validate(val
));
166 this.$.find('legend').remove();
171 this.v
= this.o
.parse(this.$.val());
172 this.v
=== '' && (this.v
= this.o
.min
);
176 s
.val(s
._validate(s
.o
.parse(s
.$.val())));
182 !this.o
.displayInput
&& this.$.hide();
184 // adds needed DOM elements (canvas, div)
185 this.$c
= $(document
.createElement('canvas')).attr({
187 height
: this.o
.height
190 // wraps all elements in a div
191 // add to DOM before Canvas init is triggered
192 this.$div
= $('<div style="'
193 + (this.o
.inline
? 'display:inline;' : '')
194 + 'width:' + this.o
.width
+ 'px;height:' + this.o
.height
+ 'px;'
197 this.$.wrap(this.$div
).before(this.$c
);
198 this.$div
= this.$.parent();
200 if (typeof G_vmlCanvasManager
!== 'undefined') {
201 G_vmlCanvasManager
.initElement(this.$c
[0]);
204 this.c
= this.$c
[0].getContext
? this.$c
[0].getContext('2d') : null;
208 name
: "CanvasNotSupportedException",
209 message
: "Canvas not supported. Please use excanvas on IE8.0.",
210 toString: function(){return this.name
+ ": " + this.message
}
215 this.scale
= (window
.devicePixelRatio
|| 1) / (
216 this.c
.webkitBackingStorePixelRatio
||
217 this.c
.mozBackingStorePixelRatio
||
218 this.c
.msBackingStorePixelRatio
||
219 this.c
.oBackingStorePixelRatio
||
220 this.c
.backingStorePixelRatio
|| 1
223 // detects relative width / height
224 this.relativeWidth
= this.o
.width
% 1 !== 0
225 && this.o
.width
.indexOf('%');
226 this.relativeHeight
= this.o
.height
% 1 !== 0
227 && this.o
.height
.indexOf('%');
228 this.relative
= this.relativeWidth
|| this.relativeHeight
;
230 // computes size and carves the component
233 // prepares props for transaction
234 if (this.v
instanceof Object
) {
236 this.copy(this.v
, this.cv
);
241 // binds configure event
243 .bind("configure", cf
)
245 .bind("configure", cf
);
255 this.$.val(this.o
.format(this.v
));
261 this._carve = function() {
263 var w
= this.relativeWidth
?
264 this.$div
.parent().width() *
265 parseInt(this.o
.width
) / 100
266 : this.$div
.parent().width(),
267 h
= this.relativeHeight
?
268 this.$div
.parent().height() *
269 parseInt(this.o
.height
) / 100
270 : this.$div
.parent().height();
273 this.w
= this.h
= Math
.min(w
, h
);
275 this.w
= this.o
.width
;
276 this.h
= this.o
.height
;
281 'width': this.w
+ 'px',
282 'height': this.h
+ 'px'
285 // finalize canvas with computed width
292 if (this.scale
!== 1) {
293 this.$c
[0].width
= this.$c
[0].width
* this.scale
;
294 this.$c
[0].height
= this.$c
[0].height
* this.scale
;
295 this.$c
.width(this.w
);
296 this.$c
.height(this.h
);
302 this._draw = function () {
304 // canvas pre-rendering
311 s
.dH
&& (d
= s
.dH());
313 d
!== false && s
.draw();
316 this._touch = function (e
) {
317 var touchMove = function (e
) {
319 e
.originalEvent
.touches
[s
.t
].pageX
,
320 e
.originalEvent
.touches
[s
.t
].pageY
323 if (v
== s
.cv
) return;
325 if (s
.cH
&& s
.cH(v
) === false) return;
327 s
.change(s
._validate(v
));
337 // Touch events listeners
339 .bind("touchmove.k", touchMove
)
343 k
.c
.d
.unbind('touchmove.k touchend.k');
351 this._mouse = function (e
) {
352 var mouseMove = function (e
) {
353 var v
= s
.xy2val(e
.pageX
, e
.pageY
);
355 if (v
== s
.cv
) return;
357 if (s
.cH
&& (s
.cH(v
) === false)) return;
359 s
.change(s
._validate(v
));
366 // Mouse events listeners
368 .bind("mousemove.k", mouseMove
)
370 // Escape key cancel current change
373 if (e
.keyCode
=== 27) {
374 k
.c
.d
.unbind("mouseup.k mousemove.k keyup.k");
376 if (s
.eH
&& s
.eH() === false)
386 k
.c
.d
.unbind('mousemove.k mouseup.k keyup.k');
394 this._xy = function () {
395 var o
= this.$c
.offset();
402 this._listen = function () {
403 if (!this.o
.readOnly
) {
422 this.$.attr('readonly', 'readonly');
426 $(window
).resize(function() {
435 this._configure = function () {
438 if (this.o
.draw
) this.dH
= this.o
.draw
;
439 if (this.o
.change
) this.cH
= this.o
.change
;
440 if (this.o
.cancel
) this.eH
= this.o
.cancel
;
441 if (this.o
.release
) this.rH
= this.o
.release
;
443 if (this.o
.displayPrevious
) {
444 this.pColor
= this.h2rgba(this.o
.fgColor
, "0.4");
445 this.fgColor
= this.h2rgba(this.o
.fgColor
, "0.6");
447 this.fgColor
= this.o
.fgColor
;
453 this._clear = function () {
454 this.$c
[0].width
= this.$c
[0].width
;
457 this._validate = function (v
) {
458 var val
= (~~ (((v
< 0) ? -0.5 : 0.5) + (v
/this.o
.step
))) * this.o
.step
;
459 return Math
.round(val
* 100) / 100;
463 this.listen = function () {}; // on start, one time
464 this.extend = function () {}; // each time configure triggered
465 this.init = function () {}; // each time configure triggered
466 this.change = function (v
) {}; // on change
467 this.val = function (v
) {}; // on release
468 this.xy2val = function (x
, y
) {}; //
469 this.draw = function () {}; // on change / on release
470 this.clear = function () { this._clear(); };
473 this.h2rgba = function (h
, a
) {
477 parseInt(h
.substring(0,2), 16),
478 parseInt(h
.substring(2,4), 16),
479 parseInt(h
.substring(4,6), 16)
482 return "rgba(" + rgb
[0] + "," + rgb
[1] + "," + rgb
[2] + "," + a
+ ")";
485 this.copy = function (f
, t
) {
496 k
.Dial = function () {
499 this.startAngle
= null;
502 this.lineWidth
= null;
503 this.cursorExt
= null;
505 this.PI2
= 2*Math
.PI
;
507 this.extend = function () {
509 bgColor
: this.$.data('bgcolor') || '#EEEEEE',
510 angleOffset
: this.$.data('angleoffset') || 0,
511 angleArc
: this.$.data('anglearc') || 360,
516 this.val = function (v
, triggerRelease
) {
522 if (triggerRelease
!== false
525 && this.rH(v
) === false) { return; }
527 this.cv
= this.o
.stopper
? max(min(v
, this.o
.max
), this.o
.min
) : v
;
529 this.$.val(this.o
.format(this.v
));
536 this.xy2val = function (x
, y
) {
540 x
- (this.x
+ this.w2
),
541 - (y
- this.y
- this.w2
)
542 ) - this.angleOffset
;
545 a
= this.angleArc
- a
- this.PI2
;
548 if (this.angleArc
!= this.PI2
&& (a
< 0) && (a
> -0.5)) {
550 // if isset angleArc option, set to min if .5 under min
556 ret
= (a
* (this.o
.max
- this.o
.min
) / this.angleArc
) + this.o
.min
;
558 this.o
.stopper
&& (ret
= max(min(ret
, this.o
.max
), this.o
.min
));
563 this.listen = function () {
566 var s
= this, mwTimerStop
,
571 var ori
= e
.originalEvent
,
572 deltaX
= ori
.detail
|| ori
.wheelDeltaX
,
573 deltaY
= ori
.detail
|| ori
.wheelDeltaY
,
574 v
= s
._validate(s
.o
.parse(s
.$.val()))
576 deltaX
> 0 || deltaY
> 0
578 : deltaX
< 0 || deltaY
< 0 ? -s
.o
.step
: 0
581 v
= max(min(v
, s
.o
.max
), s
.o
.min
);
586 // Handle mousewheel stop
587 clearTimeout(mwTimerStop
);
588 mwTimerStop
= setTimeout(function () {
593 // Handle mousewheel releases
594 if (!mwTimerRelease
) {
595 mwTimerRelease
= setTimeout(function () {
598 mwTimerRelease
= null;
620 if (kc
>= 96 && kc
<= 105) {
621 kc
= e
.keyCode
= kc
- 48;
624 kval
= parseInt(String
.fromCharCode(kc
));
632 || s
.$.val().match(/\./)) // . allowed once
633 && e
.preventDefault();
636 if ($.inArray(kc
,[37,38,39,40]) > -1) {
639 var v
= s
.o
.parse(s
.$.val()) + kv
[kc
] * m
;
640 s
.o
.stopper
&& (v
= max(min(v
, s
.o
.max
), s
.o
.min
));
642 s
.change(s
._validate(v
));
645 // long time keydown speed-up
646 to
= window
.setTimeout(function () {
658 window
.clearTimeout(to
);
665 (s
.$.val() > s
.o
.max
&& s
.$.val(s
.o
.max
))
666 || (s
.$.val() < s
.o
.min
&& s
.$.val(s
.o
.min
));
671 this.$c
.bind("mousewheel DOMMouseScroll", mw
);
672 this.$.bind("mousewheel DOMMouseScroll", mw
)
675 this.init = function () {
676 if (this.v
< this.o
.min
677 || this.v
> this.o
.max
) { this.v
= this.o
.min
; }
680 this.w2
= this.w
/ 2;
681 this.cursorExt
= this.o
.cursor
/ 100;
682 this.xy
= this.w2
* this.scale
;
683 this.lineWidth
= this.xy
* this.o
.thickness
;
684 this.lineCap
= this.o
.lineCap
;
685 this.radius
= this.xy
- this.lineWidth
/ 2;
688 && (this.o
.angleOffset
= isNaN(this.o
.angleOffset
) ? 0 : this.o
.angleOffset
);
691 && (this.o
.angleArc
= isNaN(this.o
.angleArc
) ? this.PI2
: this.o
.angleArc
);
694 this.angleOffset
= this.o
.angleOffset
* Math
.PI
/ 180;
695 this.angleArc
= this.o
.angleArc
* Math
.PI
/ 180;
697 // compute start and end angles
698 this.startAngle
= 1.5 * Math
.PI
+ this.angleOffset
;
699 this.endAngle
= 1.5 * Math
.PI
+ this.angleOffset
+ this.angleArc
;
702 String(Math
.abs(this.o
.max
)).length
,
703 String(Math
.abs(this.o
.min
)).length
,
709 'width' : ((this.w
/ 2 + 4) >> 0) + 'px',
710 'height' : ((this.w
/ 3) >> 0) + 'px',
711 'position' : 'absolute',
712 'vertical-align' : 'middle',
713 'margin-top' : ((this.w
/ 3) >> 0) + 'px',
714 'margin-left' : '-' + ((this.w
* 3 / 4 + 2) >> 0) + 'px',
716 'background' : 'none',
717 'font' : this.o
.fontWeight
+ ' ' + ((this.w
/ s
) >> 0) + 'px ' + this.o
.font
,
718 'text-align' : 'center',
719 'color' : this.o
.inputColor
|| this.o
.fgColor
,
721 '-webkit-appearance': 'none'
724 'visibility': 'hidden'
728 this.change = function (v
) {
730 this.$.val(this.o
.format(v
));
733 this.angle = function (v
) {
734 return (v
- this.o
.min
) * this.angleArc
/ (this.o
.max
- this.o
.min
);
737 this.arc = function (v
) {
741 sa
= this.endAngle
+ 0.00001;
742 ea
= sa
- v
- 0.00001;
744 sa
= this.startAngle
- 0.00001;
745 ea
= sa
+ v
+ 0.00001;
748 && (sa
= ea
- this.cursorExt
)
749 && (ea
= ea
+ this.cursorExt
);
754 d
: this.o
.flip
&& !this.o
.cursor
758 this.draw = function () {
759 var c
= this.g
, // context
760 a
= this.arc(this.cv
), // Arc
764 c
.lineWidth
= this.lineWidth
;
765 c
.lineCap
= this.lineCap
;
767 if (this.o
.bgColor
!== "none") {
769 c
.strokeStyle
= this.o
.bgColor
;
770 c
.arc(this.xy
, this.xy
, this.radius
, this.endAngle
- 0.00001, this.startAngle
+ 0.00001, true);
774 if (this.o
.displayPrevious
) {
775 pa
= this.arc(this.v
);
777 c
.strokeStyle
= this.pColor
;
778 c
.arc(this.xy
, this.xy
, this.radius
, pa
.s
, pa
.e
, pa
.d
);
780 r
= this.cv
== this.v
;
784 c
.strokeStyle
= r
? this.o
.fgColor
: this.fgColor
;
785 c
.arc(this.xy
, this.xy
, this.radius
, a
.s
, a
.e
, a
.d
);
789 this.cancel = function () {
794 $.fn
.dial
= $.fn
.knob = function (o
) {
797 var d
= new k
.Dial();