diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7ebe30bc..00000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.jar -*.bat \ No newline at end of file diff --git a/README.md b/README.md index e3f90555..06d580d7 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,39 @@ # noUiSlider -_Current version: 3.2.0_ +_Current version: 4.0.0_ -noUiSlider is a little jQuery plugin that allows you to create range sliders. +noUiSlider is a super tiny jQuery plugin that allows you to create range sliders. It fully supports touch, and it is way(!) less bloated than the jQueryUI library. A full documentation, including examples, is available on the [noUiSlider documentation page](http://refreshless.com/nouislider/). -**Changelog for version 3.2.1:** -_[latest minor release]_ +Changes +------- +**Changelog for version 4:** -* Fixed an issue when initializing a slider with two handles, both on 100%. - -**Changelog for version 3:** _[current major release]_ -* Added responsive design support -* Added Windows Pointer Events support -* Fixed issues -* Reduced file size \ No newline at end of file +* Massive update overhauling the entire code style +* Better styling possibilties +* Brand new Flat theme +* Windows Phone 8 support +* Performance improvements +* New way of handling disabled sliders +* Internal option testing provides feedback on issues + +Compression and Error checking +------------------------------ + +**CSS** +The stylesheet is compressed using: +[CSSMinifier](http://cssminifier.com/) + +**JS** +The plugin is compressed using the Google Closure compiler, using the 'simple' optimalization option. +[Google Closure Compiler](http://closure-compiler.appspot.com/home) + +**Code** +The plugin code is checked using JsLint, with the following options: +`/*jslint browser: true, devel: true, nomen: true, plusplus: true, unparam: true, sloppy: true, todo: true, white: true */` + +Please note that while some errors remain, these are not issues as they are merely a difference in coding style. +[JsLint](http://jslint.com/) diff --git a/jquery.nouislider.css b/jquery.nouislider.css index 8a4f8969..d4315dea 100644 --- a/jquery.nouislider.css +++ b/jquery.nouislider.css @@ -1,123 +1,111 @@ - - /* Body, root elements - * Sets a default cursor on the body, blocks text selection. - */ - .noUi-root * { + +/* General CSS resets; + * The target itself is not affected, allowing + * the remainder of the document to use an + * alternate box-sizing model; + * Support for box-sizing is wide spread: + * http://caniuse.com/#search=box-sizing + */ + .noUi-target * { +-webkit-box-sizing: border-box; + -moz-box-sizing: border-box; box-sizing: border-box; - display: block; - margin: 0; - padding: 0; - border: 0; +-webkit-touch-callout: none; + -ms-touch-action: none; +-webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + cursor: default; } - .noUi-root *, - body[data-nouislider-active] { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -ms-touch-action: none; + +/* Main slider bar; + */ + .noUi-base { + height: 40px; + width: 300px; + position: relative; + max-width: 100%; + border: 1px solid #bfbfbf; + z-index: 1; } - body[data-nouislider-active] * { - cursor: default !important; + +/* Handles + active state; + */ + .noUi-handle { + background: #EEE; + height: 44px; + width: 44px; + border: 1px solid #BFBFBF; + margin: -3px 0 0 -23px; + } + .noUi-active { + background: #E9E9E9; + } + .noUi-active:after { + content: ""; + display: block; + height: 100%; + border: 1px solid #DDD; } - /* Basics - * - */ - .noUi-slider { - position: relative; +/* Styling-only classes; + * Structured to prevent double declarations + * for various states of the slider. + */ + .noUi-connect { + background: Teal; + } + .noUi-background { + background: #DDD; } - .noUi-slider b { + +/* Functional styles for handle positioning; + * Note that the origins have z-index 0, the base has + * z-index 1; This fixes a bug where borders become invisible. + */ + .noUi-origin { position: absolute; - z-index: 1; + right: 0; + top: 0; + bottom: 0; + z-index: 0; } - .noUi-slider b + b { - /* Fallback for older browsers... :( */ - background: #d9d7cb !important; + .noUi-origin-upper { background: inherit !important; } - .noUi-horizontal { - width: 100%; - height: 12px; - } - .noUi-horizontal b { - height: 100%; - right: 0; + .noUi-z-index { + z-index: 10; } + +/* Adaptations for the vertical slider; + */ .noUi-vertical { - height: 100%; - width: 12px; + height: 300px; + width: 40px; + max-height: 100%; } - .noUi-vertical b { - width: 100%; + .noUi-vertical .noUi-origin { bottom: 0; + left: 0; } - - /* Looks - * - */ - .noUi-slider { - border: 1px solid #908d84; - border-radius: 3px; - } - .noUi-slider.noUi-connect.noUi-lower, - .noUi-slider.noUi-connect b { - background: #b2a98f; - } - .noUi-slider, - .noUi-slider.noUi-connect.noUi-lower b { - background: #d9d7cb; - box-shadow: inset 0px 1px 7px #b6b4a8; - } - .noUi-slider b { - border-radius: 2px; - } - .noUi-slider i { - width: 18px; - height: 18px; - border: 1px solid #999; - border-radius: 3px; - background: #efefef; - -webkit-transition: all 0.2s; - transition: all 0.2s; - } - .noUi-horizontal i { - margin: -4px 0 0 -9px; - } - .noUi-vertical i { - margin: -9px 0 0 -4px; - } - - /* Hover and active states - * - */ - .noUi-slider .noUi-base-active { - z-index: 3 !important; - } - .noUi-slider i.noUi-active, - .noUi-slider i:hover { - border-color: #aaa; - background: #fff; - width: 26px; - height: 26px; - margin: -8px 0 0 -13px; - } - .noUi-vertical i.noUi-active, - .noUi-vertical i:hover { - margin: -4px 0 0 -9px; + .noUi-vertical .noUi-handle { + margin: -23px 0 0 -3px; } - /* Disabled - * - */ - .noUi-root[disabled="disabled"] .noUi-slider { - background: #ccc; - } - .noUi-root[disabled="disabled"] i:hover, - .noUi-root[disabled="disabled"] i { +/* Various alternate slider states; + * Support for transition is widely available, + * Only IE7, IE8 and IE9 will ignore these rules. + * Since this is merely a progressive enhancement, + * this is no problem at all. + * http://caniuse.com/#search=transition + */ + .noUi-target[disabled] .noUi-base { background: #999; - cursor: not-allowed; - border-color: #333; } - .noUi-root:disabled { - display: none; + .noUi-target[disabled] .noUi-connect { + background: #BBB; + } + .noUi-state-tap .noUi-origin { + -webkit-transition: left 0.3s, top 0.3s; + transition: left 0.3s, top 0.3s; } diff --git a/jquery.nouislider.js b/jquery.nouislider.js index 6e0b75ec..e615a0df 100644 --- a/jquery.nouislider.js +++ b/jquery.nouislider.js @@ -1,573 +1,821 @@ -/* noUiSlider 3.5.0 */ -(function($){ -$.fn.noUiSlider = function(options){ - - var - // Default test and correction set. - // Might extend the plugin and documentation to make this optional/external. - // Requirements: - // - Item for every option used. - // - 'r' sets 'required' - // - 't' provides a testing function - // arguments(reference to options object, value [, option name]) - // returns false on error, else true. - // - 'init' method that appends the parent object to all children. - testCorrectionSet = { - "handles": { - r: true // has default - ,t: function(o,q){ - q = parseInt(q); - return ( q === 1 || q === 2 ); +/* noUiSlider 4.0.0 */ +(function($, UNDEF){ + + $.fn.noUiSlider = function( options ){ + + var namespace = '.nui' + // Create a shorthand for document event binding + ,all = $(document) + // Create a map of touch and mouse actions + ,actions = { + start: 'mousedown' + namespace + ' touchstart' + namespace + ,move: 'mousemove' + namespace + ' touchmove' + namespace + ,end: 'mouseup' + namespace + ' touchend' + namespace + } + // Make a copy of the current val function. + ,$VAL = $.fn.val + // Define a set of standard HTML classes for + // the various structures noUiSlider uses. + ,clsList = [ + 'noUi-base' // 0 + ,'noUi-origin' // 1 + ,'noUi-handle' // 2 + ,'noUi-input' // 3 + ,'noUi-active' // 4 + ,'noUi-state-tap' // 5 + ,'noUi-target' // 6 + ,'-lower' // 7 + ,'-upper' // 8 + ,'noUi-connect' // 9 + ,'noUi-vertical' // 10 + ,'noUi-horizontal' // 11 + ,'handles' // 12 + ,'noUi-background' // 13 + ,'noUi-z-index' // 14 + ] + ,stdCls = { + base: [clsList[0], clsList[13]] + ,origin: [clsList[1]] + ,handle: [clsList[2]] } + ,percentage = { + to : function (range, value) { + value = range[0] < 0 ? value + Math.abs(range[0]) : value - range[0]; + return (value * 100) / this._length(range); + }, + from : function (range, value) { + return (value * 100) / this._length(range); + }, + is : function (range, value) { + return ((value * this._length(range)) / 100) + range[0]; + }, + _length : function (range) { + return (range[0] > range[1] ? range[0] - range[1] : range[1] - range[0]); + } + }; + + if ( window.navigator.msPointerEnabled ) { + actions = { + start: 'MSPointerDown' + namespace + ,move: 'MSPointerMove' + namespace + ,end: 'MSPointerUp' + namespace + }; } - ,"range": { - r: true - ,t: function(o,q,w){ - if(q.length!=2) - return false; - q = [parseFloat(q[0]),parseFloat(q[1])]; - if(!num(q[0])||!num(q[1])) - return false; - o[w]=q; - return true; + + function __sp ( e ) { + e.stopPropagation(); + } + + function call ( f, scope, args ) { + $.each(f,function(i,q){ + if (typeof q === "function") { + q.call(scope, args); + } + }); + } + + function blocked ( e ) { + return ( e.data.base.data('target').is('[class*="noUi-state-"], [disabled]') ); + } + + function fixEvent ( e, preventDefault ) { + + // Required (in at the very least Chrome) to prevent + // scrolling and panning while attempting to slide. + // The tap event also depends on this. + if( preventDefault ) { + e.preventDefault(); } - } - ,"start": { - r: true - ,t: function(o,q,w){ - if(o.handles === 1){ - if($.isArray(q)){ - q=q[0]; - } - q = parseFloat(q); - o.start = [q]; - return num(q); - } else { - return this.parent.range.t(o,q,w); + + var jQueryEvent = e + ,touch = e.type.indexOf('touch') === 0 + ,mouse = e.type.indexOf('mouse') === 0 + ,pointer = e.type.indexOf('MSPointer') === 0 + ,x,y; + + e = e.originalEvent; + + if (touch) { + x = e.changedTouches[0].pageX; + y = e.changedTouches[0].pageY; + } + if (mouse) { + + // Polyfill the pageXOffset and pageYOffset + // variables for IE7 and IE8; + if(window.pageXOffset === UNDEF){ + window.pageXOffset = document.documentElement.scrollLeft; + window.pageYOffset = document.documentElement.scrollTop; } + + x = e.clientX + window.pageXOffset; + y = e.clientY + window.pageYOffset; + } + if (pointer) { + x = e.pageX; + y = e.pageY; } + + return { pass: jQueryEvent.data, e:e, x:x, y:y, t: [touch, mouse, pointer] }; + } - ,"connect": { - t: function(o,q){ - return ( q === true || q === false || q === 'lower' || q === 'upper' ); - } + + function getPercentage( a ){ + return parseFloat(this.style[a]); } - ,"orientation": { - t: function(o,q){ - return ( q == "horizontal" || q == "vertical" ); + + function test ( o, set ){ + + // checks is number is numerical + function num(e){ + return !isNaN(e) && isFinite(e); } - } - ,"margin": { - r: true // has default - ,t: function(o,q,w){ - q = parseFloat(q); - o[w]=q; - return num(q); + function ser(r){ + return ( r instanceof $ || typeof r === 'string' || r === false ); } - } - ,"serialization": { - r: true // has default - ,t: function(o,q){ + - if(!q.resolution){ - o.serialization.resolution == 0.01; - } else { - switch(q.resolution){ - case 1: - case 0.1: - case 0.01: - case 0.001: - case 0.0001: - case 0.00001: - break; - default: + /** + These tests are structured with an item for every option available. + Every item contains an 'r' flag, which marks a required option, and + a 't' function, which in turn takes some arguments: + - a reference to options object + - the value for the option + - the option name (optional); + The testing function returns false when an error is detected, + or true when everything is OK. Every test also has an 'init' + method which appends the parent object to all children. + **/ + var TESTS = { + "handles": { + r: true // has default + ,t: function(o,q){ + q = parseInt(q, 10); + return ( q === 1 || q === 2 ); + } + } + ,"range": { + r: true + ,t: function(o,q,w){ + if(q.length!==2){ return false; + } + q = [parseFloat(q[0]),parseFloat(q[1])]; + if(!num(q[0])||!num(q[1])){ + return false; + } + o[w]=q; + return true; + } + } + ,"start": { + r: true + ,t: function(o,q,w){ + if(o.handles === 1){ + if($.isArray(q)){ + q=q[0]; + } + q = parseFloat(q); + o.start = [q]; + return num(q); + } + return this.parent.range.t(o,q,w); } } - - if(q.to){ - - function i(r){ - return ( r instanceof jQuery || typeof r == 'string' || r === false ); + ,"connect": { + t: function(o,q){ + return ( q === true + || q === false + || ( q === 'lower' && o.handles === 1) + || ( q === 'upper' && o.handles === 1)); + } + } + ,"orientation": { + t: function(o,q){ + return ( q === "horizontal" || q === "vertical" ); } - - if(o.handles === 1){ - if(!$.isArray(q.to)){ - q.to = [q.to]; + } + ,"margin": { + r: true // has default + ,t: function(o,q,w){ + q = parseFloat(q); + o[w]=q; + return num(q); + } + } + ,"serialization": { + r: true // has default + ,t: function(o,q){ + + if(!q.resolution){ + o.serialization.resolution = 0.01; + } else { + switch(q.resolution){ + case 1: + case 0.1: + case 0.01: + case 0.001: + case 0.0001: + case 0.00001: + break; + default: + return false; + } } - o.serialization.to = q.to; - return i(q.to[0]); - } else { - return (q.to.length == 2 && i(q.to[0]) && i(q.to[1])); + + if(q.to){ + + if(o.handles === 1){ + if(!$.isArray(q.to)){ + q.to = [q.to]; + } + o.serialization.to = q.to; + return ser(q.to[0]); + } + return (q.to.length === 2 && ser(q.to[0]) && ser(q.to[1])); + + } + + return false; + } + } + ,"slide": { + t: function(o,q){ + return typeof q === "function"; + } + } + ,"step": { + t: function(o,q,w){ + return this.parent.margin.t(o,q,w); + } + } + ,"init": function(){ + var obj = this; + $.each(obj,function(i,c){ + c.parent = obj; + }); + delete this.init; + return this; + } + }, + + // Prepare a set of tests, by adding some internal reference + // values not available in native Javascript object implementation. + a = TESTS.init(); + + // Loop all provided tests; + // v is the option set, i is the index for the current test. + $.each(a, function( i, v ){ + + // If the value is required but not set, + // or if the test fails, throw an error. + if((v.r && (!o[i] && o[i] !== 0)) || ((o[i] || o[i] === 0) && !v.t(o,o[i],i))){ - } else { + // For debugging purposes it might be very useful + // to know what option caused the trouble. + if(console&&console.log){ + console.log( + "Slider:\t\t\t", set, + "\nOption:\t\t\t", i, + "\nValue:\t\t\t", o[i] + ); + } + $.error("Error on noUiSlider initialisation."); return false; } - - } - } - ,"slide": { - t: function(o,q){ - return typeof q === "function"; - } - } - ,"step": { - t: function(o,q,w){ - return this.parent.margin.t(o,q,w); - } - } - ,"init": function(){ - var obj = this; - $.each(obj,function(i,c){ - c.parent = obj; + }); - delete this.init; - return this; - } - } - ,classes = [ - /*[ 0]*/ "noUi-root" - /*[ 1]*/ ,"noUi-slider" - /*[ 2]*/ ,"noUi-horizontal" - /*[ 3]*/ ,"noUi-vertical" - /*[ 4]*/ ,"noUi-connect" - /*[ 5]*/ ,"noUi-lower" - /*[ 6]*/ ,"noUi-active" - /*[ 7]*/ ,"noUi-disabled" - /*[ 8]*/ ,"noUi-handle-one" - /*[ 9]*/ ,"noUi-handle-two" - /*[10]*/ ,"noUi-base-active" - ] - ,_evnt = (window.navigator.msPointerEnabled ? 1 : 'ontouchend' in document ? 2 : 0) - ,$VAL = jQuery.fn.val - ,active = 'data-nouislider-active' - ,store_res = 'noui-res' - ,store_options = 'noui-options' - ,store_pos = 'noui-pos' - ,bind = '.noUiSlider' - ,call = function(f, scope, args) { - $.each(f,function(i,q){ - if (typeof q === "function") { - q.call(scope, args); - } - }); - } - ,place = function(handle, pos, to, base){ - // set handle, set coupled input - handle.css(pos, to + '%').data('input').val(percentage.is(base.data(store_options).range, to).toFixed(handle.data(store_res))); + } - // set z-index - handle.css('z-index', (base.children().length == 2 && to == 100 && !handle.prev('b').length) ? 2 : 1); + function closest( value, to ){ + return Math.round(value / to) * to; + } - } - ,input = function(i, handle, base, serialization, initialize){ - - var split = (serialization.resolution = serialization.resolution || 0.01).toString().split('.'); - handle.data(store_res,(split[0] == 1 ? 0 : split[1].length)); + function setHandle ( handle, to, forgive ) { - // if name - if (typeof serialization.to[i] == 'string') { - - // create new input, prevent change event flowing up - return base.prepend('').find('input:last').change(function(a){ - a.stopPropagation(); - }); + var nui = handle.data('nui').options + // Get the array of handles from the base. + // Will be undefined at initialisation. + ,handles = handle.data('nui').base.data(clsList[12]) + // Get some settings from the handle + ,style = handle.data('nui').style + ,dec = handle.data('nui').decimals + ,hLimit; + + // Ignore the call if the handle won't move anyway. + if(to === handle[0].getPercentage(style)) { + return false; + } - // if false - } else if (serialization.to[i] == false) { + // Limit `to` to 0 - 100 + to = to < 0 ? 0 : to > 100 ? 100 : to; + + // Handle the step option, or ignore it. + if( nui.step && !forgive ){ + to = closest( to, percentage.from(nui.range, nui.step)); + } + + // Stop handling this call if the handle won't step to a new value. + if(to === handle[0].getPercentage(style)) { + return false; + } - // create faux object - return { - // store value as data - val : function(a) { - if (typeof a != 'undefined') { - this._handle.data('noUi-value', a); - } else { - return this._handle.data('noUi-value'); - } + // We're done if this is the only handle, + // if the handle bounce is trusted to the user + // or on initialisation when handles isn't defined yet. + if( handle.siblings('.' + clsList[1]).length && !forgive && handles ){ + + // Otherwise, the handle should bounce, + // and stop at the other handle. + if ( handle.data('nui').number ) { + hLimit = handles[0][0].getPercentage(style) + nui.margin; + to = to < hLimit ? hLimit : to; + } else { + hLimit = handles[1][0].getPercentage(style) - nui.margin; + to = to > hLimit ? hLimit : to; } - // prevent val errors - ,hasClass: function(){ + + // Stop handling this call if the handle can't move past another. + if(to === handle[0].getPercentage(style)) { return false; } - // keep access to handle - ,_handle: handle - }; + + } + + // Fix for the z-index issue where the lower handle gets stuck + // below the upper one. Since this function is called for every + // movement, toggleClass cannot be used. + if(handle.data('nui').number === 0 && to > 95){ + handle.addClass(clsList[14]); + } else { + handle.removeClass(clsList[14]); + } + + // Set handle to new location + handle.css( style , to + '%'); + + // Write the value to the serialization object. + handle.data('store').val(percentage.is(nui.range, to).toFixed(dec)); + + return true; + + } - // if jQuery object - } else { + function store ( handle, S ) { - // trigger slider change on change - return serialization.to[i].change(function(){ - var arr = [null, null]; - arr[i] = $(this).val(); - base.parent().val(arr); - }); + var i = handle.data('nui').number; + + if( S.to[i] instanceof $ ) { + + // Attach a change event to the supplied jQuery object, + // which will just trigger the val function on the parent. + // In some cases, the change event will not fire on select elements, + // so listen to 'blur' too. + return S.to[i].on('change'+namespace+' blur'+namespace, function(){ + var arr = [null, null]; + arr[i] = $(this).val(); + handle.data('nui').target.val(arr, true); + }); + + } + + if ( typeof S.to[i] === "string" ) { + + // Append a new object to the noUiSlider base, + // prevent change events flowing upward. + return $('') + .appendTo(handle).change(__sp); + + } + + if ( S.to[i] === false ) { + + // Create an object capable of handling all jQuery calls. + return { + // The value will be stored a data on the handle. + val : function(a) { + // Value function provides a getter and a setter. + // Can't just test for !a, as a might be 0. + if ( a === UNDEF ) { + // Either set... + return this._handle.data('nui-val'); + } + // ... or return; + this._handle.data('nui-val', a); + } + // The object could be mistaken for a jQuery object, + // make sure that doesn't trigger any errors. + ,hasClass: function(){ + return false; + } + // The val function needs access to the handle. + ,_handle: handle + }; + } } - } - ,correct = function(proposal, base, handle, forgive){ + function move( event ) { - var other - ,options = base.data(store_options) - ,pos = base.data(store_pos); + // This function is called often, keep it light. + + event = fixEvent( event, true ); - // fit bounds - proposal = proposal < 0 ? 0 : proposal > 100 ? 100 : proposal; + if(!event) { + return; + } - // handle step option - if(options.step){ - var per = percentage.from(options.range, options.step); - proposal = Math.round(proposal / per) * per; - } - - // we're done if this is the only handle - // or if the handle bounce is trusted to the user - if(!handle.siblings('b').length || forgive) - return proposal; + var base = event.pass.base + ,style = base.data('style') + // Subtract the initial movement from the current event, + // while taking vertical sliders into account. + ,proposal = event.x - event.pass.startEvent.x + ,baseSize = style === 'left' ? base.width() : base.height(); + + if(style === 'top') { + proposal = event.y - event.pass.startEvent.y; + } + + proposal = event.pass.position + ( ( proposal * 100 ) / baseSize ); + + setHandle( event.pass.handle, proposal ); + + // Trigger the 'slide' event, pass the target so that it is 'this'. + call( + [ event.pass.base.data('options').slide ] + ,event.pass.base.data('target') + ); - // bounce off other handle - if (handle.prev('b').length) { - other = parseFloat(handle.prev()[0].style[pos]) + options.margin; - proposal = proposal < other ? other : proposal; - } else { - other = parseFloat(handle.next()[0].style[pos]) - options.margin; - proposal = proposal > other ? other : proposal; } - - return proposal; - } - ,num = function (e){ - // checks is number is numerical - return !isNaN(e) && isFinite(e); - } - ,test = function(o,set){ + function end ( event ) { - // Aquire a test and correction set from the main scope. - var a = testCorrectionSet.init(); - - // run all tests - $.each(a,function(i,v){ - - // if value is required but not set, - // or if the test fails - if((v.r && (!o[i] && o[i] !== 0)) || ((o[i] || o[i] == 0) && !v.t(o,o[i],i))){ - // if available, log error. - if(console&&console.log){ - console.log( - "Slider:\t\t\t", set, - "\nOption:\t\t\t", i, - "\nValue:\t\t\t", o[i] - ); - } - $.error("Error on noUiSlider initialisation."); - return false; + if ( blocked( event ) ) { + return; } - }); - - } - ,div = function(c){ - return '
'; - } - ,location = function(e){ - - try { + // Handle is no longer active; + event.data.handle.children().removeClass(clsList[4]); - return [ - ( e.pageX || e.originalEvent.pageX || e.originalEvent.touches[0].pageX ), - ( e.pageY || e.originalEvent.pageY || e.originalEvent.touches[0].pageY ), - ]; + // Unbind move and end events, to prevent + // them stacking up over and over; + all.off(actions.move); + all.off(actions.end); + $('body').off(namespace); - }catch (e) { - - return ['x','y']; + event.data.base.data('target').change(); + + } + + function start ( event ) { + + // When the slider is in a transitional state, stop. + // Also prevents interaction with disabled sliders. + if ( blocked( event ) ) { + return; + } + + event = fixEvent( event ); + + if(!event) { + return; + } + + var handle = event.pass.handle + ,position = handle[0].getPercentage( handle.data('nui').style ); + + handle.children().addClass('noUi-active'); + + // Attach the move event handler, while + // passing all relevant information along. + all.on(actions.move, { + startEvent: event + ,position: position + ,base: event.pass.base + ,handle: handle + }, move); + + all.on(actions.end, { base: event.pass.base, handle: handle }, end); + + $('body').on('selectstart' + namespace, function(){ return false; }); } - } - ,isTrue = function(a){ - return typeof a !== 'undefined' && typeof a !== false - } - ,substract = function(a,b){ - // a and b are passed by reference - if(a.length!=b.length) - return; - $.each(a,function(i,v){ - a[i] -= b[i]; - }); - } - ,percentage = { - to : function (range, value) { - value = range[0] < 0 ? value + Math.abs(range[0]) : value - range[0]; - return (value * 100) / this._length(range); - }, - from : function (range, value) { - return (value * 100) / this._length(range); - }, - is : function (range, value) { - return ((value * this._length(range)) / 100) + range[0]; - }, - _length : function (range) { - return (range[0] > range[1] ? range[0] - range[1] : range[1] - range[0]); + function selfEnd( event ) { + // Trigger the end handler. Supply correct data using a + // fake object that contains all required information; + end({ data: { base: event.data.base, handle: event.data.handle } }); + // Stop propagation so that the tap handler doesn't interfere; + event.stopPropagation(); } - } - ,map = { - on: ['mousedown','MSPointerDown','touchstart'] - ,move: ['mousemove','MSPointerMove','touchmove'] - ,off: ['mouseup','MSPointerUp','touchend'] - } - ,events = { - on: map.on[_evnt]+bind+'X' - ,move: map.move[_evnt]+bind - ,off: map.off[_evnt]+bind - ,select: 'selectstart'+bind - ,click: 'click'+bind - } - ,methods = { - create: function(){ - - // array of classes to add - var set = [] - // set styling positions - ,pos = ['left','top'] - ,orientation; - - // set defaults by extending options object - // extend static options - options = $.extend({ - handles: 2 - ,margin: 0 - }, options) || {}; - - // create default serialization - if(!options.serialization){ - // set default serialization - options.serialization = { - to : [false, false] - ,resolution : 0.01 - } + + function tap ( event ) { + + if ( blocked( event ) || event.data.base.find('.' + clsList[4]).length ) { + return; } - - // Run options tests, test method will throw errors - // so there is no need to capture the result of this call. - test(options,$(this)); - - // bring margin to scale - options.margin = percentage.from(options.range, options.margin); - - // Prepare an array of classes - if(options.connect){ - set.push(classes[4]); - if(options.connect=="lower"){ - set.push(classes[5]); - } + + event = fixEvent( event ); + + // The event handler might have rejected this event. + if(!event) { + return; } - // test orientation - if(options.orientation=="vertical"){ - // set orientation variable and position test - set.push(classes[3]); - pos = pos[orientation = 1]; - } else { - set.push(classes[2]); - pos = pos[orientation = 0]; + + // Getting variables from the event is not required, but + // shortens other expressions and is far more convenient; + var i, handle, base = event.pass.base + ,handles = event.pass.handles + ,style = base.data('style') + ,eventXY = event[style === 'left' ? 'x' : 'y'] + ,baseSize = style === 'left' ? base.width() : base.height() + // Create a standard set off offsets compensated with the + // scroll distance. When required, correct for scrolling. + ,correction = { + x: ( event.t[2] ? window.pageXOffset : 0 ) + ,y: ( event.t[2] ? window.pageYOffset : 0 ) + } + ,offset = { + handles: [] + ,base: { + left: base.offset().left - correction.x + ,top: base.offset().top// - correction.y + } + }; + + // Loop handles and add data to the offset list. + for (i = 0; i < handles.length; i++ ) { + offset.handles.push({ + left: handles[i].offset().left - correction.x + ,top: handles[i].offset().top// - correction.y + }); } - return this.each(function(){ + console.log('Correction: ' + correction.y + ' Handle offset: ' + offset.handles[0].top, ' Event: ' + eventXY + ' Base offset: ' + offset.base.top); + + // Calculate the central point between the handles; + var handleCenter = handles.length === 1 ? 0 : + (( offset.handles[0][style] + offset.handles[1][style] ) / 2 ); - // Create variable set - // add classes, store options - var base = $(this).addClass(classes[0]).append(div(classes[1])).children().data(store_options,options).data(store_pos,pos) - // loopable handles - ,handles = []; + // If there is just one handle, + // or the lower handles in closest to the event, + // select the first handle. Otherwise, pick the second. + if ( handles.length === 1 || eventXY < handleCenter ){ + handle = handles[0]; + } else { + handle = handles[1]; + } + + // Flag the slider as it is now in a transitional state. + // Transition takes 300 ms, so re-enable the slider afterwards. + base.addClass(clsList[5]); + setTimeout(function(){ + base.removeClass(clsList[5]); + }, 300); - // Add classes to root slider - $.each(set,function(a,b){ - base.addClass(b); - }); + // Calculate the new position for the handle and + // trigger the movement. + setHandle( + handle + ,(((eventXY - offset.base[style]) * 100) / baseSize) + ); + + // Trigger the 'slide' event, pass the target so that it is 'this'. + call( + [ handle.data('nui').options.slide ] + ,base.data('target') + ); + + base.data('target').change(); + + } + + function create ( ) { + + return this.each(function( index, target ){ + + // Target is the wrapper that will receive all external + // scripting interaction. It has no styling and serves no + // other function. + target = $(target); + target.addClass(clsList[6]); - for (var i = 0; i < options.handles; i++) { + // Base is the internal main 'bar'. + var i, style, decimals, handle + ,base = $('').appendTo(target) + ,handles = [] + ,cls = { + base: stdCls.base + ,origin: [ + stdCls.origin.concat([clsList[1] + clsList[7]]) + ,stdCls.origin.concat([clsList[1] + clsList[8]]) + ] + ,handle: [ + stdCls.handle.concat([clsList[2] + clsList[7]]) + ,stdCls.handle.concat([clsList[2] + clsList[8]]) + ] + }; + + // Set defaults where applicable; + options = $.extend({ + handles: 2 + ,margin: 0 + ,orientation: "horizontal" + }, options) || {}; - // create handle html and store it - handles.push(base.append('').children().last()); - - // create inputs for handles and initialize them - handles[i].data('input', input( - i - ,handles[i] - ,base - ,options.serialization - )); - - // set handle to initial position - place(handles[i],pos,percentage.to(options.range, options.start[i]),base); - - // bind starting event - handles[i].find('i').addClass(classes[8+i]).on(events.on,function(e){ - - // if disabled, stop - if(isTrue(base.parent().attr('disabled'))) - return; - - // Location = coordinates for 'mouse'/'touch' - // Position = offset of handle on bar - // Proposal = suggested new position of handle - - // rescope handle - var bas_handle = $(this).addClass(classes[6]) - ,cur_handle = bas_handle.parent().addClass(classes[10]) - ,ori_location = location(e) - // initialise previous location on start location - ,pre_location = ori_location - // get from native js element - ,ori_position = parseFloat(cur_handle[0].style[pos]) - // there is no previous proposal - ,pre_proposal = false; - - // prevent text selection while dragging - $('body').attr(active,'').on(events.select,function(f){ - return false; - }); - - $(document).on(events.move,function(f){ - - // what do I prevent? - f.preventDefault(); - - var cur_location = location(f) - ,cur_proposal; - - // ie7 is crazy. - if(cur_location[0]=='x') - return; - - substract(cur_location,ori_location); - - // come up with a new proposal - cur_proposal = ori_position + ((cur_location[orientation] * 100) / (orientation ? base.height() : base.width())); - // correct proposal - cur_proposal = correct(cur_proposal,base,cur_handle); - - // if the 'mouse'/'touch' didn't move in the right direction, - // or the proposal is the same as last time, stop right here. - if( pre_location[orientation] == cur_location[orientation] || cur_proposal == pre_proposal ) - return; - - // store current location - // it shouldn't matter this doesn't happen on the above return. - pre_location = cur_location; - // store the new proposal - pre_proposal = cur_proposal; - - // set the handle - // this function will also handle setting z-index and such. - place(cur_handle,pos,cur_proposal,base); - call([options.slide,options.__DB], base.parent()); - - }).on(events.off,function(f){ - - // remove classes from body and handle - bas_handle.removeClass(classes[6]); - cur_handle.removeClass(classes[10]); - // unbind events - $(document).add($('body').removeAttr(active)).off(bind); - // trigger change event - base.parent().change(); - - }); - - }).on(events.click, function(e){ - e.stopPropagation(); - }); + // Set a default for serialization; + if(!options.serialization){ + options.serialization = { + to : [false, false] + ,resolution : 0.01 + }; + } + + // Run all options through a testing mechanism to ensure correct + // input. The test function will throw errors, so there is + // no need to capture the result of this call. It should be noted + // that options might get modified to be handled properly. E.g. + // wrapping integers in arrays. + test(options, target); + + // I can't type serialization any more, and it doesn't compress + // very well, so shorten it. + options.S = options.serialization; + + // INCOMPLETE + if( options.connect ) { + cls.origin[0].push(clsList[9]); + if( options.connect === "lower" ){ + // Add some styling classes to the base; + cls.base.push(clsList[9], clsList[9] + clsList[7]); + // When using the option 'Lower', there is only one + // handle, and thus only one origin. + cls.origin[0].push(clsList[13]); + } else { + cls.base.push(clsList[9] + clsList[8]); + } } + + // Parse the syntactic sugar that is the serialization + // resolution option to a usable integer. + style = options.orientation === 'vertical' ? 'top' : 'left'; - // allow the slider to be moved by clicking - base.on(events.click, function(e){ + decimals = options.S.resolution.toString().split('.'); + // Test ==, not === + decimals = decimals[0] == 1 ? 0 : decimals[1].length; - // if disabled, stop - if(isTrue(base.parent().attr('disabled'))) - return; + // Add classes for horizontal and vertical sliders. + // The horizontal class is provided for completeness, + // as it isn't used in the default theme. + if( options.orientation === "vertical" ){ + cls.base.push(clsList[10]); + } else { + cls.base.push(clsList[11]); + } - // determine new position - var cur_location = location(e) - ,cur_proposal = ((cur_location[orientation] - base.offset()[pos]) * 100) / (orientation ? base.height() : base.width()) - ,handle; + // Merge base classes with default; + base.addClass(cls.base.join(" ")).data('target', target); + + for (i = 0; i < options.handles; i++ ) { + + handle = $('