From 845d569f1871667070bd29d0b8e5296aa1b0a030 Mon Sep 17 00:00:00 2001 From: Leon Gersen Date: Sun, 22 Sep 2013 20:33:51 +0200 Subject: [PATCH] 4.0 rewrite --- .gitignore | 2 - README.md | 41 +- jquery.nouislider.css | 194 +++--- jquery.nouislider.js | 1232 ++++++++++++++++++++++--------------- jquery.nouislider.min.css | 1 + jquery.nouislider.min.js | 16 + nouislider.jquery.json | 2 +- 7 files changed, 879 insertions(+), 609 deletions(-) delete mode 100644 .gitignore create mode 100644 jquery.nouislider.min.css 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 = $('
').appendTo(base); + + // Add all default and option-specific classes to the + // origins and handles. + handle.addClass(cls.origin[i].join(" ")); + handle.children().addClass(cls.handle[i].join(" ")); + + // These events are only bound to the visual handle element, + // not the `real` origin element. + handle.children() + .on(actions.start, { base: base, handle: handle }, start) + .on(actions.end, { base: base, handle: handle }, selfEnd); + + // Make sure every handle has access to all primary + // variables. Can't uses jQuery's .data( obj ) structure + // here, as `store` needs some values from the `nui` object. + handle.data('nui', { + target: target + ,decimals: decimals + ,options: options + ,base: base + ,style: style + ,number: i + }).data('store', store ( + handle + ,options.S + )); - if(handles.length == 1){ - handle = handles[0]; - } else { - handle = (cur_location[orientation] < (handles[0].offset()[pos] + handles[1].offset()[pos]) / 2 ? handles[0] : handles[1]); - } + // Attach a function to the native DOM element, + // since jQuery wont let me get the current value in percentages. + handle[0].getPercentage = getPercentage; + + // Make handles loop-able + handles.push(handle); - // move handle, fire events - place(handle,pos,correct(cur_proposal, base, handle), base); - call([options.slide,options.__DB], base.parent()); - base.parent().change(); + // Set the handle to its initial position; + setHandle(handle, percentage.to(options.range, options.start[i])); + } + + // The base could use the handles too; + base.data({ + options: options + ,handles: handles + ,style: style + }); + + target.data({ + base: base + ,handles: handles }); + + // The tap event. + base.on(actions.end, { base: base, handles: handles }, tap); }); - + } - ,val: function(){ - - // split between getting/setting - if(arguments.length){ - // store calling arguments, standarize input - var args = typeof arguments[0] == "number" ? [arguments[0]] : arguments[0]; - - // set for each slider + function val ( args, ignore ) { + + // Setter + if( args && args.length ){ + + // Setting is handled properly for each slider in the data set. return this.each(function(){ - - // get base - var base = $(this).children(); - - base.children('b').each(function(i,v){ - - var handle = $(this); - - // if ignore, stop running - if(args[i] == null) + + $.each($(this).data(clsList[12]), function(i, handle){ + + // The set request might want to ignore this handle. + if( args[i] === null ) { return; + } + + // Calculate a new position for the handle. + var value, current + ,range = handle.data('nui').options.range + ,to = percentage.to( + range + ,parseFloat(args[i]) + ), + + // Set handle to new location, and make sure developer + // input is always accepted. The ignore flag indicates + // input from user facing elements. + result = setHandle(handle, to, (ignore === true ? false : true)); + + // If the value of the input doesn't match the slider, + // reset it. + if(!result){ + + value = handle.data('store').val(); + current = percentage.is(range, + handle[0].getPercentage(handle.data('nui').style) + ).toFixed(handle.data('nui').decimals); + + if(value !== current){ + handle.data('store').val(current); + } + } - // place handle, which sets value to input too. - place(handle, base.data(store_pos), correct(percentage.to(base.data(store_options).range, args[i]), base, handle, true), base); }); - - }); - - } else { - - // build array - var re = []; - - // loop handles - this.find('b').each(function(){ - // get value - re.push($(this).data('input').val()); + }); - - // return single value or array - return re.length == 1 ? re[0] : re; - + } + + // Or, if the function was called without arguments, + // act as a 'getter'; + var re = []; + + // Loop the handles, and get the value from the input + // for every handle on its' own. + $.each($(this).data(clsList[12]), function(i, handle){ + re.push( handle.data('store').val() ); + }); + + // If the slider has just one handle, return a single value. + // Otherwise, return an array. + return ( re.length === 1 ? re[0] : re) ; + } - }; - // overwrite the native jQuery val() function. - jQuery.fn.val = function(){ - return this.hasClass(classes[0]) - ? methods.val.apply(this, arguments) - : $VAL.apply(this, arguments); - } + // Overwrite the native jQuery val() function + // with a simple handler. noUiSlider will use the internal + // value method, anything else will use the standard method. + $.fn.val = function(){ + return this.hasClass(clsList[6]) + ? val.apply(this, arguments) + : $VAL.apply(this, arguments); + }; - return methods.create.apply(this); + return create.apply(this, arguments); + + }; -}})(jQuery); +}(jQuery)); diff --git a/jquery.nouislider.min.css b/jquery.nouislider.min.css new file mode 100644 index 00000000..f6fde5bc --- /dev/null +++ b/jquery.nouislider.min.css @@ -0,0 +1 @@ +.noUi-target *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-touch-callout:none;-ms-touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;cursor:default}.noUi-base{height:40px;width:300px;position:relative;max-width:100%;border:1px solid #bfbfbf;z-index:1}.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}.noUi-connect{background:Teal}.noUi-background{background:#DDD}.noUi-origin{position:absolute;right:0;top:0;bottom:0;z-index:0}.noUi-origin-upper{background:inherit!important}.noUi-z-index{z-index:10}.noUi-vertical{height:300px;width:40px;max-height:100%}.noUi-vertical .noUi-origin{bottom:0;left:0}.noUi-vertical .noUi-handle{margin:-23px 0 0 -3px}.noUi-target[disabled] .noUi-base{background:#999}.noUi-target[disabled] .noUi-connect{background:#BBB}.noUi-state-tap .noUi-origin{-webkit-transition:left .3s,top .3s;transition:left .3s,top .3s} \ No newline at end of file diff --git a/jquery.nouislider.min.js b/jquery.nouislider.min.js index e69de29b..4c93ccd6 100644 --- a/jquery.nouislider.min.js +++ b/jquery.nouislider.min.js @@ -0,0 +1,16 @@ +/** noUiSlider 4.0.0 */ +(function(f,u){f.fn.noUiSlider=function(l){function z(a){a.stopPropagation()}function v(a,c,d){f.each(a,function(a,k){"function"===typeof k&&k.call(c,d)})}function q(a){return a.data.base.data("target").is('[class*="noUi-state-"], [disabled]')}function s(a,c){c&&a.preventDefault();var d=a,h=0===a.type.indexOf("touch"),k=0===a.type.indexOf("mouse"),b=0===a.type.indexOf("MSPointer"),r,e;a=a.originalEvent;h&&(r=a.changedTouches[0].pageX,e=a.changedTouches[0].pageY);k&&(window.pageXOffset===u&&(window.pageXOffset= +document.documentElement.scrollLeft,window.pageYOffset=document.documentElement.scrollTop),r=a.clientX+window.pageXOffset,e=a.clientY+window.pageYOffset);b&&(r=a.pageX,e=a.pageY);return{pass:d.data,e:a,x:r,y:e,t:[h,k,b]}}function A(a){return parseFloat(this.style[a])}function B(a,c){function d(a){return a instanceof f||"string"===typeof a||!1===a}var h={handles:{r:!0,t:function(a,b){b=parseInt(b,10);return 1===b||2===b}},range:{r:!0,t:function(a,b,c){if(2!==b.length)return!1;b=[parseFloat(b[0]),parseFloat(b[1])]; +if(isNaN(b[0])||!isFinite(b[0])||isNaN(b[1])||!isFinite(b[1]))return!1;a[c]=b;return!0}},start:{r:!0,t:function(a,b,c){return 1===a.handles?(f.isArray(b)&&(b=b[0]),b=parseFloat(b),a.start=[b],!isNaN(b)&&isFinite(b)):this.parent.range.t(a,b,c)}},connect:{t:function(a,b){return!0===b||!1===b||"lower"===b&&1===a.handles||"upper"===b&&1===a.handles}},orientation:{t:function(a,b){return"horizontal"===b||"vertical"===b}},margin:{r:!0,t:function(a,b,c){b=parseFloat(b);a[c]=b;return!isNaN(b)&&isFinite(b)}}, +serialization:{r:!0,t:function(a,b){if(b.resolution)switch(b.resolution){case 1:case 0.1:case 0.01:case 0.001:case 1E-4:case 1E-5:break;default:return!1}else a.serialization.resolution=0.01;return b.to?1===a.handles?(f.isArray(b.to)||(b.to=[b.to]),a.serialization.to=b.to,d(b.to[0])):2===b.to.length&&d(b.to[0])&&d(b.to[1]):!1}},slide:{t:function(a,b){return"function"===typeof b}},step:{t:function(a,b,c){return this.parent.margin.t(a,b,c)}},init:function(){var a=this;f.each(a,function(b,c){c.parent= +a});delete this.init;return this}}.init();f.each(h,function(d,b){if(b.r&&!a[d]&&0!==a[d]||(a[d]||0===a[d])&&!b.t(a,a[d],d))return console&&console.log&&console.log("Slider:\t\t\t",c,"\nOption:\t\t\t",d,"\nValue:\t\t\t",a[d]),f.error("Error on noUiSlider initialisation."),!1})}function t(a,c,d){var h=a.data("nui").options,k=a.data("nui").base.data(e[12]),b=a.data("nui").style,r=a.data("nui").decimals;if(c===a[0].getPercentage(b))return!1;c=0>c?0:100d?d:c),c===a[0].getPercentage(b)))return!1;0===a.data("nui").number&&95').appendTo(a).change(z);if(!1===c.to[d])return{val:function(a){if(a===u)return this._handle.data("nui-val");this._handle.data("nui-val",a)},hasClass:function(){return!1},_handle:a}}function D(a){if(a=s(a,!0)){var c=a.pass.base,d=c.data("style"),e=a.x-a.pass.startEvent.x,c="left"===d?c.width():c.height();"top"===d&&(e=a.y-a.pass.startEvent.y); +e=a.pass.position+100*e/c;t(a.pass.handle,e);v([a.pass.base.data("options").slide],a.pass.base.data("target"))}}function w(a){q(a)||(a.data.handle.children().removeClass(e[4]),p.off(m.move),p.off(m.end),f("body").off(g),a.data.base.data("target").change())}function E(a){if(!q(a)&&(a=s(a))){var c=a.pass.handle,d=c[0].getPercentage(c.data("nui").style);c.children().addClass("noUi-active");p.on(m.move,{startEvent:a,position:d,base:a.pass.base,handle:c},D);p.on(m.end,{base:a.pass.base,handle:c},w);f("body").on("selectstart"+ +g,function(){return!1})}}function F(a){w({data:{base:a.data.base,handle:a.data.handle}});a.stopPropagation()}function G(a){if(!q(a)&&!a.data.base.find("."+e[4]).length&&(a=s(a))){var c,d,h=a.pass.base;d=a.pass.handles;var k=h.data("style"),b=a["left"===k?"x":"y"],f="left"===k?h.width():h.height(),g=a.t[2]?window.pageXOffset:0,m=a.t[2]?window.pageYOffset:0,n=[];a={left:h.offset().left-g,top:h.offset().top-m};for(c=0;ca[0]?c+Math.abs(a[0]):c-a[0];return 100*c/this._length(a)},from:function(a,c){return 100*c/this._length(a)},is:function(a,c){return c*this._length(a)/100+a[0]},_length:function(a){return a[0]>a[1]?a[0]-a[1]:a[1]-a[0]}};window.navigator.msPointerEnabled&&(m={start:"MSPointerDown"+g,move:"MSPointerMove"+g,end:"MSPointerUp"+g});f.fn.val=function(){return this.hasClass(e[6])?H.apply(this,arguments):I.apply(this,arguments)};return function(){return this.each(function(a, +c){c=f(c);c.addClass(e[6]);var d,h,k,b,g=f("
").appendTo(c),p=[];d=J;var q=[x.concat([e[1]+e[7]]),x.concat([e[1]+e[8]])],s=[y.concat([e[2]+e[7]]),y.concat([e[2]+e[8]])];l=f.extend({handles:2,margin:0,orientation:"horizontal"},l)||{};l.serialization||(l.serialization={to:[!1,!1],resolution:0.01});B(l,c);l.S=l.serialization;l.connect&&(q[0].push(e[9]),"lower"===l.connect?(d.push(e[9],e[9]+e[7]),q[0].push(e[13])):d.push(e[9]+e[8]));h="vertical"===l.orientation?"top":"left";k=l.S.resolution.toString().split("."); +k=1==k[0]?0:k[1].length;"vertical"===l.orientation?d.push(e[10]):d.push(e[11]);g.addClass(d.join(" ")).data("target",c);for(d=0;d
").appendTo(g),b.addClass(q[d].join(" ")),b.children().addClass(s[d].join(" ")),b.children().on(m.start,{base:g,handle:b},E).on(m.end,{base:g,handle:b},F),b.data("nui",{target:c,decimals:k,options:l,base:g,style:h,number:d}).data("store",C(b,l.S)),b[0].getPercentage=A,p.push(b),t(b,n.to(l.range,l.start[d]));g.data({options:l,handles:p, +style:h});c.data({base:g,handles:p});g.on(m.end,{base:g,handles:p},G)})}.apply(this,arguments)}})(jQuery); diff --git a/nouislider.jquery.json b/nouislider.jquery.json index 37b44644..159d84f1 100644 --- a/nouislider.jquery.json +++ b/nouislider.jquery.json @@ -16,7 +16,7 @@ "url": "http://refreshless.com/nouislider/license.txt" } ], - "version": "3.5.0", + "version": "4.0.0", "author": { "name": "Léon Gersen", "url": "https://twitter.com/LeonGersen"