diff options
Diffstat (limited to 'js/assets/nouislider/5.0.0/full/jquery.nouislider.js')
-rw-r--r-- | js/assets/nouislider/5.0.0/full/jquery.nouislider.js | 1420 |
1 files changed, 1420 insertions, 0 deletions
diff --git a/js/assets/nouislider/5.0.0/full/jquery.nouislider.js b/js/assets/nouislider/5.0.0/full/jquery.nouislider.js new file mode 100644 index 00000000000..a5ffa0edc92 --- /dev/null +++ b/js/assets/nouislider/5.0.0/full/jquery.nouislider.js @@ -0,0 +1,1420 @@ +/*! $.noUiSlider + @version 5.0.0 + @author Leon Gersen https://twitter.com/LeonGersen + @license WTFPL http://www.wtfpl.net/about/ + @documentation http://refreshless.com/nouislider/ +*/ + +// ==ClosureCompiler== +// @externs_url http://refreshless.com/externs/jquery-1.8.js +// @compilation_level ADVANCED_OPTIMIZATIONS +// @warning_level VERBOSE +// ==/ClosureCompiler== + +/*jshint laxcomma: true */ +/*jshint smarttabs: true */ +/*jshint sub: true */ + +/*jslint browser: true */ +/*jslint continue: true */ +/*jslint plusplus: true */ +/*jslint white: true */ +/*jslint sub: true */ + +(function( $ ){ + + 'use strict'; + + if ( $['zepto'] && !$.fn.removeData ) { + throw new ReferenceError('Zepto is loaded without the data module.'); + } + + $.fn['noUiSlider'] = function( options, rebuild ){ + + var + // Cache the document and body selectors; + doc = $(document) + ,body = $('body') + + // Namespace for binding and unbinding slider events; + ,namespace = '.nui' + + // Copy of the current value function; + ,$VAL = $.fn.val + + // Re-usable list of classes; + ,clsList = [ + /* 0 */ 'noUi-base' + /* 1 */ ,'noUi-origin' + /* 2 */ ,'noUi-handle' + /* 3 */ ,'noUi-input' + /* 4 */ ,'noUi-active' + /* 5 */ ,'noUi-state-tap' + /* 6 */ ,'noUi-target' + /* 7 */ ,'-lower' + /* 8 */ ,'-upper' + /* 9 */ ,'noUi-connect' + /* 10 */ ,'noUi-horizontal' + /* 11 */ ,'noUi-vertical' + /* 12 */ ,'noUi-background' + /* 13 */ ,'noUi-stacking' + /* 14 */ ,'noUi-block' + /* 15 */ ,'noUi-state-blocked' + /* 16 */ ,'noUi-ltr' + /* 17 */ ,'noUi-rtl' + /* 18 */ ,'noUi-dragable' + /* 19 */ ,'noUi-extended' + /* 20 */ ,'noUi-state-drag' + ] + + // Determine the events to bind. IE11 implements pointerEvents without + // a prefix, which breaks compatibility with the IE10 implementation. + ,actions = window.navigator['pointerEnabled'] ? { + start: 'pointerdown' + ,move: 'pointermove' + ,end: 'pointerup' + } : window.navigator['msPointerEnabled'] ? { + start: 'MSPointerDown' + ,move: 'MSPointerMove' + ,end: 'MSPointerUp' + } : { + start: 'mousedown touchstart' + ,move: 'mousemove touchmove' + ,end: 'mouseup touchend' + }; + + +// Percentage calculation + + // (percentage) How many percent is this value of this range? + function fromPercentage ( range, value ) { + return (value * 100) / ( range[1] - range[0] ); + } + + // (percentage) Where is this value on this range? + function toPercentage ( range, value ) { + return fromPercentage( range, range[0] < 0 ? + value + Math.abs(range[0]) : + value - range[0] ); + } + + // (value) How much is this percentage on this range? + function isPercentage ( range, value ) { + return ((value * ( range[1] - range[0] )) / 100) + range[0]; + } + + +// Type tests + + // Test in an object is an instance of jQuery or Zepto. + function isInstance ( a ) { + return a instanceof $ || ( $['zepto'] && $['zepto']['isZ'](a) ); + } + + // Checks whether a value is numerical. + function isNumeric ( a ) { + return !isNaN( parseFloat( a ) ) && isFinite( a ); + } + + +// General helper functions + + // Test an array of objects, and calls them if they are a function. + function call ( functions, scope ) { + + // Allow the passing of an unwrapped function. + // Leaves other code a more comprehensible. + if( !$.isArray( functions ) ){ + functions = [ functions ]; + } + + $.each( functions, function(){ + if (typeof this === 'function') { + this.call(scope); + } + }); + } + + // Returns a proxy to set a target using the public value method. + function setN ( target, number ) { + + return function(){ + + // Determine the correct position to set, + // leave the other one unchanged. + var val = [null, null]; + val[ number ] = $(this).val(); + + // Trigger the 'set' callback + target.val(val, true); + }; + } + + // Round a value to the closest 'to'. + function closest ( value, to ){ + return Math.round(value / to) * to; + } + + // Format output value to specified standards. + function format ( value, options ) { + + // Round the value to the resolution that was set + // with the serialization options. + value = value.toFixed( options['decimals'] ); + + // Rounding away decimals might cause a value of -0 + // when using very small ranges. Remove those cases. + if ( parseFloat(value) === 0 ) { + value = value.replace('-0', '0'); + } + + // Apply the proper decimal mark to the value. + return value.replace( '.', options['serialization']['mark'] ); + } + + // Determine the handle closest to an event. + function closestHandle ( handles, location, style ) { + + if ( handles.length === 1 ) { + return handles[0]; + } + + var total = handles[0].offset()[style] + + handles[1].offset()[style]; + + return handles[ location < total / 2 ? 0 : 1 ]; + } + + // Round away small numbers in floating point implementation. + function digits ( value, round ) { + return parseFloat(value.toFixed(round)); + } + +// Event abstraction + + // Provide a clean event with standardized offset values. + function fixEvent ( e ) { + + // Prevent scrolling and panning on touch events, while + // attempting to slide. The tap event also depends on this. + e.preventDefault(); + + // Filter the event to register the type, which can be + // touch, mouse or pointer. Offset changes need to be + // made on an event specific basis. + var touch = e.type.indexOf('touch') === 0 + ,mouse = e.type.indexOf('mouse') === 0 + ,pointer = e.type.indexOf('pointer') === 0 + ,x,y, event = e; + + // IE10 implemented pointer events with a prefix; + if ( e.type.indexOf('MSPointer') === 0 ) { + pointer = true; + } + + // Get the originalEvent, if the event has been wrapped + // by jQuery. Zepto doesn't wrap the event. + if ( e.originalEvent ) { + e = e.originalEvent; + } + + if ( touch ) { + // noUiSlider supports one movement at a time, + // so we can select the first 'changedTouch'. + x = e.changedTouches[0].pageX; + y = e.changedTouches[0].pageY; + } + if ( mouse || pointer ) { + + // Polyfill the pageXOffset and pageYOffset + // variables for IE7 and IE8; + if( !pointer && window.pageXOffset === undefined ){ + window.pageXOffset = document.documentElement.scrollLeft; + window.pageYOffset = document.documentElement.scrollTop; + } + + x = e.clientX + window.pageXOffset; + y = e.clientY + window.pageYOffset; + } + + return $.extend( event, { + 'pointX': x + ,'pointY': y + ,cursor: mouse + }); + } + + // Handler for attaching events trough a proxy + function attach ( events, element, callback, pass ) { + + var target = pass.target; + + // Add the noUiSlider namespace to all events. + events = events.replace( /\s/g, namespace + ' ' ) + namespace; + + // Bind a closure on the target. + return element.on( events, function( e ){ + + // jQuery and Zepto handle unset attributes differently. + var disabled = target.attr('disabled'); + disabled = !( disabled === undefined || disabled === null ); + + // Test if there is anything that should prevent an event + // from being handled, such as a disabled state or an active + // 'tap' transition. + if( target.hasClass('noUi-state-tap') || disabled ) { + return false; + } + + // Call the event handler with three arguments: + // - The event; + // - An object with data for the event; + // - The slider options; + // Having the slider options as a function parameter prevents + // getting it in every function, which muddies things up. + callback ( + fixEvent( e ) + ,pass + ,target.data('base').data('options') + ); + }); + } + + +// Serialization and value storage + + // Store a value on all serialization targets, or get the current value. + function serialize ( a ) { + + /*jshint validthis: true */ + + // Re-scope target for availability within .each; + var target = this.target; + + // Get the value for this handle + if ( a === undefined ) { + return this.element.data('value'); + } + + // Write the value to all serialization objects + // or store a new value on the handle + if ( a === true ) { + a = this.element.data('value'); + } else { + this.element.data('value', a); + } + + // Prevent a serialization call if the value wasn't initialized. + if ( a === undefined ) { + return; + } + + // If the provided element was a function, + // call it with the slider as scope. Otherwise, + // simply call the function on the object. + $.each( this.elements, function() { + if ( typeof this === 'function' ) { + this.call(target, a); + } else { + this[0][this[1]](a); + } + }); + } + + // Map serialization to [ element, method ]. Attach events where required. + function storeElement ( handle, item, number ) { + + // Add a change event to the supplied jQuery objects, + // which triggers the value-setting function on the target. + if ( isInstance( item ) ) { + + var elements = [], target = handle.data('target'); + + // Link the field to the other handle if the + // slider is inverted. + if ( handle.data('options').direction ) { + number = number ? 0 : 1; + } + + // Loop all items so the change event is properly bound, + // and the items can individually be added to the array. + item.each(function(){ + + // Bind the change event. + $(this).on('change' + namespace, setN( target, number )); + + // Store the element with the proper handler. + elements.push([ $(this), 'val' ]); + }); + + return elements; + } + + // Append a new input to the noUiSlider base. + // Prevent the change event from flowing upward. + if ( typeof item === 'string' ) { + + item = [ $('<input type="hidden" name="'+ item +'">') + .appendTo(handle) + .addClass(clsList[3]) + .change(function ( e ) { + e.stopPropagation(); + }), 'val']; + } + + return [item]; + } + + // Access point and abstraction for serialization. + function store ( handle, i, serialization ) { + + var elements = []; + + // Loops all items in the provided serialization setting, + // add the proper events to them or create new input fields, + // and add them as data to the handle so they can be kept + // in sync with the slider value. + $.each( serialization['to'][i], function( index ){ + elements = elements.concat( + storeElement( handle, serialization['to'][i][index], i ) + ); + }); + + return { + element: handle + ,elements: elements + ,target: handle.data('target') + ,'val': serialize + }; + } + + +// Handle placement + + // Fire callback on unsuccessful handle movement. + function block ( base, stateless ) { + + var target = base.data('target'); + + if ( !target.hasClass(clsList[14]) ){ + + // The visual effects should not always be applied. + if ( !stateless ) { + target.addClass(clsList[15]); + setTimeout(function(){ + target.removeClass(clsList[15]); + }, 450); + } + + target.addClass(clsList[14]); + call( base.data('options').block, target ); + } + } + + // Change inline style and apply proper classes. + function placeHandle ( handle, to ) { + + var settings = handle.data('options'); + + to = digits(to, 7); + + // If the slider can move, remove the class + // indicating the block state. + handle.data('target').removeClass(clsList[14]); + + // Set handle to new location + handle.css( settings['style'], to + '%' ).data('pct', to); + + // Force proper handle stacking + if ( handle.is(':first-child') ) { + handle.toggleClass(clsList[13], to > 50 ); + } + + if ( settings['direction'] ) { + to = 100 - to; + } + + // Write the value to the serialization object. + handle.data('store').val( + format ( isPercentage( settings['range'], to ), settings ) + ); + } + + // Test suggested values and apply margin, step. + function setHandle ( handle, to ) { + + var base = handle.data('base'), settings = base.data('options'), + handles = base.data('handles'), lower = 0, upper = 100; + + // Catch invalid user input + if ( !isNumeric( to ) ){ + return false; + } + + // Handle the step option. + if ( settings['step'] ){ + to = closest( to, settings['step'] ); + } + + if ( handles.length > 1 ){ + if ( handle[0] !== handles[0][0] ) { + lower = digits(handles[0].data('pct')+settings['margin'],7); + } else { + upper = digits(handles[1].data('pct')-settings['margin'],7); + } + } + + // Limit position to boundaries. When the handles aren't set yet, + // they return -1 as a percentage value. + to = Math.min( Math.max( to, lower ), upper < 0 ? 100 : upper ); + + // Stop handling this call if the handle can't move past another. + // Return an array containing the hit limit, so the caller can + // provide feedback. ( block callback ). + if ( to === handle.data('pct') ) { + return [!lower ? false : lower, upper === 100 ? false : upper]; + } + + placeHandle ( handle, to ); + return true; + } + + // Handles movement by tapping + function jump ( base, handle, to, callbacks ) { + + // 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); + + // Move the handle to the new position. + setHandle( handle, to ); + + // Trigger the 'slide' and 'set' callbacks, + // pass the target so that it is 'this'. + call( callbacks, base.data('target') ); + + base.data('target').change(); + } + + +// Event handlers + + // Handle movement on document for handle and range drag. + function move ( event, Dt, Op ) { + + // Map event movement to a slider percentage. + var handles = Dt.handles, limits, + proposal = event[ Dt.point ] - Dt.start[ Dt.point ]; + + proposal = ( proposal * 100 ) / Dt.size; + + if ( handles.length === 1 ) { + + // Run handle placement, receive true for success or an + // array with potential limits. + limits = setHandle( handles[0], Dt.positions[0] + proposal ); + + if ( limits !== true ) { + + if ( $.inArray ( handles[0].data('pct'), limits ) >= 0 ){ + block ( Dt.base, !Op['margin'] ); + } + return; + } + + } else { + + // Dragging the range could be implemented by forcing the + // 'move' event on both handles, but this solution proved + // lagging on slower devices, resulting in range errors. The + // slightly ugly solution below is considerably faster, and + // it can't move the handle out of sync. Bypass the standard + // setting method, as other checks are needed. + + var l1, u1, l2, u2; + + // Round the proposal to the step setting. + if ( Op['step'] ) { + proposal = closest( proposal, Op['step'] ); + } + + // Determine the new position, store it twice. Once for + // limiting, once for checking whether placement should occur. + l1 = l2 = Dt.positions[0] + proposal; + u1 = u2 = Dt.positions[1] + proposal; + + // Round the values within a sensible range. + if ( l1 < 0 ) { + u1 += -1 * l1; + l1 = 0; + } else if ( u1 > 100 ) { + l1 -= ( u1 - 100 ); + u1 = 100; + } + + // Don't perform placement if no handles are to be changed. + // Check if the lowest value is set to zero. + if ( l2 < 0 && !l1 && !handles[0].data('pct') ) { + return; + } + // The highest value is limited to 100%. + if ( u1 === 100 && u2 > 100 && handles[1].data('pct') === 100 ){ + return; + } + + placeHandle ( handles[0], l1 ); + placeHandle ( handles[1], u1 ); + } + + // Trigger the 'slide' event, if the handle was moved. + call( Op['slide'], Dt.target ); + } + + // Unbind move events on document, call callbacks. + function end ( event, Dt, Op ) { + + // The handle is no longer active, so remove the class. + if ( Dt.handles.length === 1 ) { + Dt.handles[0].data('grab').removeClass(clsList[4]); + } + + // Remove cursor styles and text-selection events bound to the body. + if ( event.cursor ) { + body.css('cursor', '').off( namespace ); + } + + // Unbind the move and end events, which are added on 'start'. + doc.off( namespace ); + + // Trigger the change event. + Dt.target.removeClass( clsList[14] +' '+ clsList[20]).change(); + + // Trigger the 'end' callback. + call( Op['set'], Dt.target ); + } + + // Bind move events on document. + function start ( event, Dt, Op ) { + + // Mark the handle as 'active' so it can be styled. + if( Dt.handles.length === 1 ) { + Dt.handles[0].data('grab').addClass(clsList[4]); + } + + // A drag should never propagate up to the 'tap' event. + event.stopPropagation(); + + // Attach the move event. + attach ( actions.move, doc, move, { + start: event + ,base: Dt.base + ,target: Dt.target + ,handles: Dt.handles + ,positions: [ Dt.handles[0].data('pct') + ,Dt.handles[ Dt.handles.length - 1 ].data('pct') ] + ,point: Op['orientation'] ? 'pointY' : 'pointX' + ,size: Op['orientation'] ? Dt.base.height() : Dt.base.width() + }); + + // Unbind all movement when the drag ends. + attach ( actions.end, doc, end, { + target: Dt.target + ,handles: Dt.handles + }); + + // Text selection isn't an issue on touch devices, + // so adding additional callbacks isn't required. + if ( event.cursor ) { + + // Prevent the 'I' cursor and extend the range-drag cursor. + body.css('cursor', $(event.target).css('cursor')); + + // Mark the target with a dragging state. + if ( Dt.handles.length > 1 ) { + Dt.target.addClass(clsList[20]); + } + + // Prevent text selection when dragging the handles. + body.on('selectstart' + namespace, function( ){ + return false; + }); + } + } + + // Move closest handle to tapped location. + function tap ( event, Dt, Op ) { + + var base = Dt.base, handle, to, point, size; + + // The tap event shouldn't propagate up to trigger 'edge'. + event.stopPropagation(); + + // Determine the direction of the slider. + if ( Op['orientation'] ) { + point = event['pointY']; + size = base.height(); + } else { + point = event['pointX']; + size = base.width(); + } + + // Find the closest handle and calculate the tapped point. + handle = closestHandle( base.data('handles'), point, Op['style'] ); + to = (( point - base.offset()[ Op['style'] ] ) * 100 ) / size; + + // The set handle to the new position. + jump( base, handle, to, [ Op['slide'], Op['set'] ]); + } + + // Move handle to edges when target gets tapped. + function edge ( event, Dt, Op ) { + + var handles = Dt.base.data('handles'), to, i; + + i = Op['orientation'] ? event['pointY'] : event['pointX']; + i = i < Dt.base.offset()[Op['style']]; + + to = i ? 0 : 100; + i = i ? 0 : handles.length - 1; + + jump ( Dt.base, handles[i], to, [ Op['slide'], Op['set'] ]); + } + +// API + + // Validate and standardize input. + function test ( input, sliders ){ + + /* Every input option is tested and parsed. This'll prevent + endless validation in internal methods. These tests are + structured with an item for every option available. An + option can be marked as required by setting the 'r' flag. + The testing function is provided with three arguments: + - The provided value for the option; + - A reference to the options object; + - The name for the option; + + The testing function returns false when an error is detected, + or true when everything is OK. It can also modify the option + object, to make sure all values can be correctly looped elsewhere. */ + + function values ( a ) { + + if ( a.length !== 2 ){ + return false; + } + + // Convert the array to floats + a = [ parseFloat(a[0]), parseFloat(a[1]) ]; + + // Test if all values are numerical + if( !isNumeric(a[0]) || !isNumeric(a[1]) ){ + return false; + } + + // The lowest value must really be the lowest value. + if( a[1] < a[0] ){ + return false; + } + + return a; + } + + var serialization = { + resolution: function(q,o){ + + // Parse the syntactic sugar that is the serialization + // resolution option to a usable integer. + // Checking for a string '1', since the resolution needs + // to be cast to a string to split in on the period. + switch( q ){ + case 1: + case 0.1: + case 0.01: + case 0.001: + case 0.0001: + case 0.00001: + q = q.toString().split('.'); + o['decimals'] = q[0] === '1' ? 0 : q[1].length; + break; + case undefined: + o['decimals'] = 2; + break; + default: + return false; + } + + return true; + } + ,mark: function(q,o,w){ + + if ( !q ) { + o[w]['mark'] = '.'; + return true; + } + + switch( q ){ + case '.': + case ',': + return true; + default: + return false; + } + } + ,to: function(q,o,w){ + + // Checks whether a variable is a candidate to be a + // valid serialization target. + function ser(r){ + return isInstance ( r ) || + typeof r === 'string' || + typeof r === 'function' || + r === false || + ( isInstance ( r[0] ) && + typeof r[0][r[1]] === 'function' ); + } + + // Flatten the serialization array into a reliable + // set of elements, which can be tested and looped. + function filter ( value ) { + + var items = [[],[]]; + + // If a single value is provided it can be pushed + // immediately. + if ( ser(value) ) { + items[0].push(value); + } else { + + // Otherwise, determine whether this is an + // array of single elements or sets. + $.each(value, function(i, val) { + + // Don't handle an overflow of elements. + if( i > 1 ){ + return; + } + + // Decide if this is a group or not + if( ser(val) ){ + items[i].push(val); + } else { + items[i] = items[i].concat(val); + } + }); + } + + return items; + } + + if ( !q ) { + o[w]['to'] = [[],[]]; + } else { + + var i, j; + + // Flatten the serialization array + q = filter ( q ); + + // Reverse the API for RTL sliders. + if ( o['direction'] && q[1].length ) { + q.reverse(); + } + + // Test all elements in the flattened array. + for ( i = 0; i < o['handles']; i++ ) { + for ( j = 0; j < q[i].length; j++ ) { + + // Return false on invalid input + if( !ser(q[i][j]) ){ + return false; + } + + // Remove 'false' elements, since those + // won't be handled anyway. + if( !q[i][j] ){ + q[i].splice(j, 1); + } + } + } + + // Write the new values back + o[w]['to'] = q; + } + + return true; + } + }, tests = { + /* Handles. + * Has default, can be 1 or 2. + */ + 'handles': { + 'r': true + ,'t': function(q){ + q = parseInt(q, 10); + return ( q === 1 || q === 2 ); + } + } + /* Range. + * Must be an array of two numerical floats, + * which can't be identical. + */ + ,'range': { + 'r': true + ,'t': function(q,o,w){ + + o[w] = values(q); + + // The values can't be identical. + return o[w] && o[w][0] !== o[w][1]; + } + } + /* Start. + * Must be an array of two numerical floats when handles = 2; + * Uses 'range' test. + * When handles = 1, a single float is also allowed. + */ + ,'start': { + 'r': true + ,'t': function(q,o,w){ + if( o['handles'] === 1 ){ + if( $.isArray(q) ){ + q = q[0]; + } + q = parseFloat(q); + o.start = [q]; + return isNumeric(q); + } + + o[w] = values(q); + return !!o[w]; + } + } + /* Connect. + * Must be true or false when handles = 2; + * Can use 'lower' and 'upper' when handles = 1. + */ + ,'connect': { + 'r': true + ,'t': function(q,o,w){ + + if ( q === 'lower' ) { + o[w] = 1; + } else if ( q === 'upper' ) { + o[w] = 2; + } else if ( q === true ) { + o[w] = 3; + } else if ( q === false ) { + o[w] = 0; + } else { + return false; + } + + return true; + } + } + /* Connect. + * Will default to horizontal, not required. + */ + ,'orientation': { + 't': function(q,o,w){ + switch (q){ + case 'horizontal': + o[w] = 0; + break; + case 'vertical': + o[w] = 1; + break; + default: return false; + } + return true; + } + } + /* Margin. + * Must be a float, has a default value. + */ + ,'margin': { + 'r': true + ,'t': function(q,o,w){ + q = parseFloat(q); + o[w] = fromPercentage(o['range'], q); + return isNumeric(q); + } + } + /* Direction. + * Required, can be 'ltr' or 'rtl'. + */ + ,'direction': { + 'r': true + ,'t': function(q,o,w){ + + switch ( q ) { + case 'ltr': o[w] = 0; + break; + case 'rtl': o[w] = 1; + // Invert connection for RTL sliders; + o['connect'] = [0,2,1,3][o['connect']]; + break; + default: + return false; + } + + return true; + } + } + /* Behaviour. + * Required, defines responses to tapping and + * dragging elements. + */ + ,'behaviour': { + 'r': true + ,'t': function(q,o,w){ + + o[w] = { + 'tap': q !== (q = q.replace('tap', '')) + ,'extend': q !== (q = q.replace('extend', '')) + ,'drag': q !== (q = q.replace('drag', '')) + ,'fixed': q !== (q = q.replace('fixed', '')) + }; + + return !q.replace('none','').replace(/\-/g,''); + } + } + /* Serialization. + * Required, but has default. Must be an array + * when using two handles, can be a single value when using + * one handle. 'mark' can be period (.) or comma (,). + */ + ,'serialization': { + 'r': true + ,'t': function(q,o,w){ + + return serialization.to( q['to'], o, w ) && + serialization.resolution( q['resolution'], o ) && + serialization.mark( q['mark'], o, w ); + } + } + /* Slide. + * Not required. Must be a function. + */ + ,'slide': { + 't': function(q){ + return $.isFunction(q); + } + } + /* Set. + * Not required. Must be a function. + * Tested using the 'slide' test. + */ + ,'set': { + 't': function(q){ + return $.isFunction(q); + } + } + /* Block. + * Not required. Must be a function. + * Tested using the 'slide' test. + */ + ,'block': { + 't': function(q){ + return $.isFunction(q); + } + } + /* Step. + * Not required. + */ + ,'step': { + 't': function(q,o,w){ + q = parseFloat(q); + o[w] = fromPercentage ( o['range'], q ); + return isNumeric(q); + } + } + }; + + $.each( tests, function( name, test ){ + + /*jslint devel: true */ + + var value = input[name], isSet = value !== undefined; + + // If the value is required but not set, fail. + if( ( test['r'] && !isSet ) || + // If the test returns false, fail. + ( isSet && !test['t']( value, input, name ) ) ){ + + // For debugging purposes it might be very useful to know + // what option caused the trouble. Since throwing an error + // will prevent further script execution, log the error + // first. Test for console, as it might not be available. + if( console && console.log && console.group ){ + console.group( 'Invalid noUiSlider initialisation:' ); + console.log( 'Option:\t', name ); + console.log( 'Value:\t', value ); + console.log( 'Slider(s):\t', sliders ); + console.groupEnd(); + } + + throw new RangeError('noUiSlider'); + } + }); + } + + // Parse options, add classes, attach events, create HTML. + function create ( options ) { + + /*jshint validthis: true */ + + // Store the original set of options on all targets, + // so they can be re-used and re-tested later. + // Make sure to break the relation with the options, + // which will be changed by the 'test' function. + this.data('options', $.extend(true, {}, options)); + + // Set defaults where applicable; + options = $.extend({ + 'handles': 2 + ,'margin': 0 + ,'connect': false + ,'direction': 'ltr' + ,'behaviour': 'tap' + ,'orientation': 'horizontal' + }, options); + + // Make sure the test for serialization runs. + options['serialization'] = options['serialization'] || {}; + + // 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, this ); + + // Pre-define the styles. + options['style'] = options['orientation'] ? 'top' : 'left'; + + return this.each(function(){ + + var target = $(this), i, dragable, handles = [], handle, + base = $('<div/>').appendTo(target); + + // Throw an error if the slider was already initialized. + if ( target.data('base') ) { + throw new Error('Slider was already initialized.'); + } + + // Apply classes and data to the target. + target.data('base', base).addClass([ + clsList[6] + ,clsList[16 + options['direction']] + ,clsList[10 + options['orientation']] ].join(' ')); + + for (i = 0; i < options['handles']; i++ ) { + + handle = $('<div><div/></div>').appendTo(base); + + // Add all default and option-specific classes to the + // origins and handles. + handle.addClass( clsList[1] ); + + handle.children().addClass([ + clsList[2] + ,clsList[2] + clsList[ 7 + options['direction'] + + ( options['direction'] ? -1 * i : i ) ]].join(' ') ); + + // Make sure every handle has access to all variables. + handle.data({ + 'base': base + ,'target': target + ,'options': options + ,'grab': handle.children() + ,'pct': -1 + }).attr('data-style', options['style']); + + // Every handle has a storage point, which takes care + // of triggering the proper serialization callbacks. + handle.data({ + 'store': store(handle, i, options['serialization']) + }); + + // Store handles on the base + handles.push(handle); + } + + // Apply the required connection classes to the elements + // that need them. Some classes are made up for several + // segments listed in the class list, to allow easy + // renaming and provide a minor compression benefit. + switch ( options['connect'] ) { + case 1: target.addClass( clsList[9] ); + handles[0].addClass( clsList[12] ); + break; + case 3: handles[1].addClass( clsList[12] ); + /* falls through */ + case 2: handles[0].addClass( clsList[9] ); + /* falls through */ + case 0: target.addClass(clsList[12]); + break; + } + + // Merge base classes with default, + // and store relevant data on the base element. + base.addClass( clsList[0] ).data({ + 'target': target + ,'options': options + ,'handles': handles + }); + + // Use the public value method to set the start values. + target.val( options['start'] ); + + // Attach the standard drag event to the handles. + if ( !options['behaviour']['fixed'] ) { + for ( i = 0; i < handles.length; i++ ) { + + // These events are only bound to the visual handle + // element, not the 'real' origin element. + attach ( actions.start, handles[i].children(), start, { + base: base + ,target: target + ,handles: [ handles[i] ] + }); + } + } + + // Attach the tap event to the slider base. + if ( options['behaviour']['tap'] ) { + attach ( actions.start, base, tap, { + base: base + ,target: target + }); + } + + // Extend tapping behaviour to target + if ( options['behaviour']['extend'] ) { + + target.addClass( clsList[19] ); + + if ( options['behaviour']['tap'] ) { + attach ( actions.start, target, edge, { + base: base + ,target: target + }); + } + } + + // Make the range dragable. + if ( options['behaviour']['drag'] ){ + + dragable = base.find('.'+clsList[9]).addClass(clsList[18]); + + // When the range is fixed, the entire range can + // be dragged by the handles. The handle in the first + // origin will propagate the start event upward, + // but it needs to be bound manually on the other. + if ( options['behaviour']['fixed'] ) { + dragable = dragable + .add( base.children().not(dragable).data('grab') ); + } + + attach ( actions.start, dragable, start, { + base: base + ,target: target + ,handles: handles + }); + } + }); + } + + // Return value for the slider, relative to 'range'. + function getValue ( ) { + + /*jshint validthis: true */ + + var base = $(this).data('base'), answer = []; + + // Loop the handles, and get the value from the input + // for every handle on its' own. + $.each( base.data('handles'), function(){ + answer.push( $(this).data('store').val() ); + }); + + // If the slider has just one handle, return a single value. + // Otherwise, return an array, which is in reverse order + // if the slider is used RTL. + if ( answer.length === 1 ) { + return answer[0]; + } + + if ( base.data('options').direction ) { + return answer.reverse(); + } + + return answer; + } + + // Set value for the slider, relative to 'range'. + function setValue ( args, set ) { + + /*jshint validthis: true */ + + // If the value is to be set to a number, which is valid + // when using a one-handle slider, wrap it in an array. + if( !$.isArray(args) ){ + args = [args]; + } + + // Setting is handled properly for each slider in the data set. + return this.each(function(){ + + var b = $(this).data('base'), to, i, + handles = Array.prototype.slice.call(b.data('handles'),0), + settings = b.data('options'); + + // If there are multiple handles to be set run the setting + // mechanism twice for the first handle, to make sure it + // can be bounced of the second one properly. + if ( handles.length > 1) { + handles[2] = handles[0]; + } + + // The RTL settings is implemented by reversing the front-end, + // internal mechanisms are the same. + if ( settings['direction'] ) { + args.reverse(); + } + + for ( i = 0; i < handles.length; i++ ){ + + // Calculate a new position for the handle. + to = args[ i%2 ]; + + // The set request might want to ignore this handle. + // Test for 'undefined' too, as a two-handle slider + // can still be set with an integer. + if( to === null || to === undefined ) { + continue; + } + + // Add support for the comma (,) as a decimal symbol. + // Replace it by a period so it is handled properly by + // parseFloat. Omitting this would result in a removal + // of decimals. This way, the developer can also + // input a comma separated string. + if( $.type(to) === 'string' ) { + to = to.replace(',', '.'); + } + + // Calculate the new handle position + to = toPercentage( settings['range'], parseFloat( to ) ); + + // Invert the value if this is an right-to-left slider. + if ( settings['direction'] ) { + to = 100 - to; + } + + // If the value of the input doesn't match the slider, + // reset it. Sometimes the input is changed to a value the + // slider has rejected. This can occur when using 'select' + // or 'input[type="number"]' elements. In this case, set + // the value back to the input. + if ( setHandle( handles[i], to ) !== true ){ + handles[i].data('store').val( true ); + } + + // Optionally trigger the 'set' event. + if( set === true ) { + call( settings['set'], $(this) ); + } + } + }); + } + + // Unbind all attached events, remove classed and HTML. + function destroy ( target ) { + + // Start the list of elements to be unbound with the target. + var elements = [[target,'']]; + + // Get the fields bound to both handles. + $.each(target.data('base').data('handles'), function(){ + elements = elements.concat( $(this).data('store').elements ); + }); + + // Remove all events added by noUiSlider. + $.each(elements, function(){ + if( this.length > 1 ){ + this[0].off( namespace ); + } + }); + + // Remove all classes from the target. + target.removeClass(clsList.join(' ')); + + // Empty the target and remove all data. + target.empty().removeData('base options'); + } + + // Merge options with current initialization, destroy slider + // and reinitialize. + function build ( options ) { + + /*jshint validthis: true */ + + return this.each(function(){ + + // When uninitialised, jQuery will return '', + // Zepto returns undefined. Both are falsy. + var values = $(this).val() || false, + current = $(this).data('options'), + // Extend the current setup with the new options. + setup = $.extend( {}, current, options ); + + // If there was a slider initialised, remove it first. + if ( values !== false ) { + destroy( $(this) ); + } + + // Make the destroy method publicly accessible. + if( !options ) { + return; + } + + // Create a new slider + $(this)['noUiSlider']( setup ); + + // Set the slider values back. If the start options changed, + // it gets precedence. + if ( values !== false && setup.start === current.start ) { + $(this).val( values ); + } + }); + } + + // Overwrite the native jQuery value function + // with a simple handler. noUiSlider will use the internal + // value method, anything else will use the standard method. + $.fn.val = function(){ + + // If the function is called without arguments, + // act as a 'getter'. Call the getValue function + // in the same scope as this call. + if ( this.hasClass( clsList[6] ) ){ + return arguments.length ? + setValue.apply( this, arguments ) : + getValue.apply( this ); + } + + // If this isn't noUiSlider, continue with jQuery's + // original method. + return $VAL.apply( this, arguments ); + }; + + return ( rebuild ? build : create ).call( this, options ); + }; + +}( window['jQuery'] || window['Zepto'] )); |