noUiSlider is open source, and you can use it for free in any personal or commercial product. No attribution required. Both the uncompressed and compressed version of noUiSlider are available in a .zip release, which is hosted by Github and available over https.
You can also visit the repository: noUiSlider on gitHub. If you need help implementing noUiSlider in your website, or if you'd like to see a new feature, feel free to ask me on twitter: @LeonGersen.
+
You can visit the repository: noUiSlider on gitHub. If you need help implementing noUiSlider in your website, or if you'd like to see a new feature, feel free to ask me on twitter: @LeonGersen.
Nested namespaces ('slide.something.else') are not supported, and are threated as a single namespace (so '.a.b' isn't related to '.a').
-
Event callbacks receive three arguments. values is always an array, for both one-handle and two-handle sliders. It contains the current slider values, with formatting applied. handle is 0 or 1 and indicates the handle that caused the event. values[handle] gives the value for the current handle. Should you need it, unencodedValues contains the slider values without any formatting.
+
Event callbacks receive three arguments. values is always an array, for both one-handle and two-handle sliders. It contains the current slider values, with formatting applied. handle is the index of the handle that caused the event, starting at zero. values[handle] gives the value for the handle that triggered the event. unencodedValues contains the slider values without any formatting.
For all events, this is set to the current slider's public API, containing (among others) the 'get' and 'set' methods.
-
-
Quick note: When any of the events fire, the slider state has updated. However, it's visualmight not have been updated yet. This happens asynchronically to increase paint performance.
noUiSlider is perfectly fine serializing values to any element with a .val() method, so lets try using type="number" and <select>.
-
Note that if your browser doesn't support an input type, it will just assume "text". If you'd like to know more, consider reading this article.
-
-
We'll append <option> elements to the <select> dynamically.
+
noUiSlider's 'update' method is useful for synchronizing with other elements, such as <input> (type="number") and <select>.
@@ -114,7 +113,7 @@
-
Linking the <select> and <input>
+
Updating the <select> and <input>
@@ -136,7 +135,7 @@
-
One of noUiSlider's core features is the ability to divide the range in a non-linear fashion. Stepping can be applied, too! The example on the right shows where the handles are on the slider range in values and percentages.
+
One of noUiSlider's core features is the ability to divide the range in a non-linear fashion. Stepping can be applied. The example on the right shows where the handles are on the slider range in values and percentages.
@@ -218,6 +217,59 @@
+
+
Moving the slider by clicking pips
+
+
+
+
+
+
Issue #733 asks about clicking pips to move the slider to their value. noUiSlider 11 adds a data-value attribute to all .noUi-value elements that makes this easy.
+
+
+
+
+
+
+
+
+
+
+
+
Setup
+
+
+
+
+
+
+
+
+
+
+
+
Only showing tooltips when sliding handles
+
+
+
+
+
+
Issue #836 requested a way to toggle tooltips after slider creation. This effect can be achieved by using the .noUi-active class to show and hide the tooltips. No additional JavaScript is involved.
Disabling a slider is identical to disabling a checkbox or textarea; simply set the disabled attribute.
+
Disabling a slider is identical to disabling a checkbox or textarea; add the disabled attribute.
+
+
A disabled slider can't be changed by user interaction (sliding, clicking or touching), but you can still change its value using the .set() method.
-
A disabled slider can't be changed by sliding, click or touching, but you can still change its value using the .set() method. You can use CSS to show the disabled state. The default theme also sets a not-allowed cursor.
+
CSS can be used to show the disabled state. The default stylesheet also sets a not-allowed cursor.
-
The slider below is disabled when the checkbox gets checked, and re-enabled when it is unchecked.
+
The slider below is disabled when the checkbox is checked, and re-enabled when it is unchecked.
-
Individual handles can also be disabled by setting the disabled attribute on a .noUi-origin element.
+
Individual handles can be disabled by adding a disabled attribute to a .noUi-origin element.
@@ -63,9 +72,15 @@
-
noUiSlider has an updateOptions(newOptions, fireSetEvent) method that can change the 'margin', 'limit', 'step', 'range', 'animate' and 'snap' options. All other options require changes to the slider's HTML or event bindings.
+
noUiSlider has an update method that can change the 'margin', 'limit', 'step', 'range', 'animate' and 'snap' options.
-
To update any other option, destroy the slider using slider.noUiSlider.destroy() and create a new one. Note that events are not unbound when destroying a slider.
+
All other options require changes to the slider's HTML or event bindings.
+
+
To update any other option, destroy the slider using slider.noUiSlider.destroy() and create a new one. Events are unbound when destroying a slider.
+
+
The update method can be called as slider.noUiSlider.updateOptions(newOptions, [fireSetEvent]).
+
+
Options that can not be updated will be ignored without errors.
The 'update' event fires after updating the slider.
@@ -73,8 +88,10 @@
The 'set' event fires when the slider values are restored. If this is unwanted, you can pass false as the second parameter, fireSetEvent.
-
Options can be read from the slider using the slider.noUiSlider.options property. This property contains a reference to the options object passed when creating the slider. This object is modified when calling updateOptions. Note that if you initiate multiple sliders using the same options object and update a subset of them later, this will move the options property out of sync with the actual slider options.
-
+
Options can be read from the slider using the slider.noUiSlider.options property. This property contains a reference to the options object passed when creating the slider. This object is modified when calling updateOptions.
+
+
Note that if you initiate multiple sliders using the same options object and update a subset of them later, this will move the options property out of sync with the actual slider options.
+
@@ -89,7 +106,7 @@
-
The HTML for this example
+
HTML for this example
@@ -128,17 +145,13 @@
-
Styling noUiSlider is easy. The default stylesheet contains helpful comments to get a head start. I strongly recommend using the default stylesheet as a starting point when re-styling noUiSlider.
+
Styling noUiSlider is easy. The default stylesheet contains helpful comments to get a head start.
+
+
It is recommended to use the default stylesheet, overriding where necessary, as a starting point when re-styling noUiSlider.
If your styling system doesn't match the convention in noUiSlider, you can use the cssPrefix and cssClasses options to reconfigure the markup.
-
-
Things to watch out for
-
-
-
If you want the handles to stay within the slider bar (instead of the default overlap), have a look at the CSS detailed to the right. Add the styles to your stylesheet and give your element the noUi-extended class for this effect.
-
The .noUi-connect and .noUi-background classes are applied to various elements.
-
To position your handles on the center of its relative position, it should have a negative offset of half the handle width. See the default theme for reference.
-
+
+
noUiSlider listens to events on the .noUi-base element. To add padding on the .noUi-target element and contain handles within the slider width, .noUi-base needs to be extended. This can be done using CSS :before and :after pseudo-elements. An example is included to the right.
@@ -187,23 +200,18 @@
-
Overriding classes
+
Add padding to slider base
-
+
-
Containing handles within the slider bar (horizontal)
+
Overriding classes
-
+
-
Containing handles within the slider bar (vertical)
-
-
-
-
diff --git a/documentation/more/classes.js b/documentation/more/classes.js
index b565a802..fdb8340e 100644
--- a/documentation/more/classes.js
+++ b/documentation/more/classes.js
@@ -6,7 +6,8 @@ noUiSlider.create(slider, {
},
cssPrefix: 'noUi-', // defaults to 'noUi-',
cssClasses: {
- // Full list of classnames to override. Does NOT extend the default classes.
+ // Full list of classnames to override.
+ // Does NOT extend the default classes.
// Have a look at the source for the full, current list:
// https://github.com/leongersen/noUiSlider/blob/master/src/js/options.js#L398
}
diff --git a/documentation/more/horizontal-contain.css b/documentation/more/horizontal-contain.css
deleted file mode 100644
index 28448746..00000000
--- a/documentation/more/horizontal-contain.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.noUi-horizontal.noUi-extended {
- padding-right: 32px;
-}
-.noUi-horizontal.noUi-extended .noUi-handle {
- left: -1px;
-}
-.noUi-horizontal.noUi-extended .noUi-origin {
- right: -32px;
-}
diff --git a/documentation/more/padding.css b/documentation/more/padding.css
new file mode 100644
index 00000000..8d9c8fbb
--- /dev/null
+++ b/documentation/more/padding.css
@@ -0,0 +1,18 @@
+.noUi-target {
+ padding: 0 17px;
+}
+.noUi-base:before,
+.noUi-base:after {
+ width: 17px;
+ content: "";
+ position: absolute;
+ top: 0;
+ height: 100%;
+ display: block;
+}
+.noUi-base:before {
+ left: -17px;
+}
+.noUi-base:after {
+ left: 100%;
+}
diff --git a/documentation/more/update-setup.js b/documentation/more/update-setup.js
index 8d628278..62010d0a 100644
--- a/documentation/more/update-setup.js
+++ b/documentation/more/update-setup.js
@@ -1,5 +1,5 @@
-var updateSlider = document.getElementById('slider-update'),
- updateSliderValue = document.getElementById('slider-update-value');
+var updateSlider = document.getElementById('slider-update');
+var updateSliderValue = document.getElementById('slider-update-value');
noUiSlider.create(updateSlider, {
range: {
diff --git a/documentation/more/vertical-contain.css b/documentation/more/vertical-contain.css
deleted file mode 100644
index 4200e13c..00000000
--- a/documentation/more/vertical-contain.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.noUi-vertical.noUi-extended {
- padding-bottom: 32px;
-}
-.noUi-vertical.noUi-extended .noUi-handle {
- top: -1px;
-}
-.noUi-vertical.noUi-extended .noUi-origin {
- bottom: -32px;
-}
diff --git a/documentation/pips.php b/documentation/pips.php
index 69f84b3c..1f17bf10 100644
--- a/documentation/pips.php
+++ b/documentation/pips.php
@@ -12,6 +12,8 @@
This feature allows you to generate points along the slider.
Five options can be set: mode to determine where to place pips, values as additional options for mode, stepped to round pip values to the slider stepping, density to pre-scale the number of pips, and filter to manually modify pip size.
+
The density value controls how many pips are placed on one percent of the slider range. With the default value of 1, there is one pip per percent. For a value of 2, a pip is placed for every 2 percent. A value below one will place more than one pip per percentage.
+
All sliders on the page use the same range, as displayed to the right.
noUiSlider can be configured with a wide variety of options, which can be use to customize the slider in to doing exactly what you want. For options regarding the slider range, see slider values.
+
noUiSlider can be configured with a wide variety of options, which can be use to customize a slider's behaviour.
+
+
For options regarding the slider range, see slider values.
The connect setting can be used to control the (green) bar between the handles, or the edges of the slider.
+
The connect option can be used to control the bar between the handles or the edges of the slider.
Pass an array with a boolean for every connecting element, including the edges of the slider. The length of this array must match the handle count + 1.
By default, the slider slides fluently. In order to make the handles jump between intervals, you can use this option. The step option is relative to the values provided to range.
+
By default, the slider slides fluently. In order to make the handles jump between intervals, you can use the step option.
@@ -318,7 +340,7 @@
-
noUiSlider can provide a basic tooltip without using its events system. Set the tooltips option to true to enable. This option can also accept formatting options to format the tooltips content. In that case, pass an array with a formatter for each handle, true to use the default or false to display no tooltip.
+
noUiSlider can provide a basic tooltip using the tooltips option. This option can also accept formatting options to format the tooltips content. In that case, pass an array with a formatter for each handle, true to use the default or false to display no tooltip.
@@ -330,7 +352,7 @@
false
Accepted values
-
false, true, formatter, array[formatter or false]
+
false, true, formatter, array[formatter or true or false, ...]
@@ -371,7 +393,7 @@
-
The animationDuration option can be used to set the animation speed assumed by the slider library. In addition to this, you must manually set the CSS (-webkit-)transition property for the .noUi-state-tap .noUi-origin selector.
+
The animationDuration option can be used to set the animation speed assumed by the slider library. In addition to this, you must manually alter the CSS (-webkit-)transition property for the .noUi-state-tap .noUi-origin selector.
Default
@@ -387,34 +409,3 @@
-
-
-
-
-
Multitouch
-
-
-
-
Set the multitouch option to true to allow simultaneous interaction with a slider and other content on the page (e.g. another slider). It is even possible to simultaneously control several handles of the same slider.
For one-handle sliders, calling .get() will return the value. For two-handle sliders, an array[value, value] will be returned.
+
For one-handle sliders, calling .get() will return the value as a 'string'. For multi-handle sliders, an array['string', 'string', ...] will be returned.
@@ -38,7 +38,7 @@
Within an array, you can set one position to null if you want to leave a handle unchanged.
-
To return to the initial slider values, you can use the .reset() method. This will only reset the slider values.
+
To return to the initial slider values, you can use the .reset() method. This will only reset the slider values.
diff --git a/package.json b/package.json
index d887024c..692448e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nouislider",
- "version": "10.1.0",
+ "version": "11.0.0",
"main": "distribute/nouislider",
"style": "distribute/nouislider.min.css",
"license": "WTFPL",
@@ -30,5 +30,8 @@
"repository": {
"type": "git",
"url": "git://github.com/leongersen/noUiSlider.git"
- }
+ },
+ "files": [
+ "distribute"
+ ]
}
diff --git a/src/js/options.js b/src/js/options.js
index bd15fc45..1e05c21f 100644
--- a/src/js/options.js
+++ b/src/js/options.js
@@ -188,25 +188,34 @@
function testPadding ( parsed, entry ) {
- if ( !isNumeric(entry) ){
- throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be numeric.");
+ if ( !isNumeric(entry) && !Array.isArray(entry) ){
+ throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers.");
+ }
+
+ if ( Array.isArray(entry) && !(entry.length == 2 || isNumeric(entry[0]) || isNumeric(entry[1])) ) {
+ throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be numeric or array of exactly 2 numbers.");
}
if ( entry === 0 ) {
return;
}
- parsed.padding = parsed.spectrum.getMargin(entry);
+ if ( !Array.isArray(entry) ) {
+ entry = [entry, entry];
+ }
+
+ // 'getMargin' returns false for invalid values.
+ parsed.padding = [parsed.spectrum.getMargin(entry[0]), parsed.spectrum.getMargin(entry[1])];
- if ( !parsed.padding ) {
+ if ( parsed.padding[0] === false || parsed.padding[1] === false ) {
throw new Error("noUiSlider (" + VERSION + "): 'padding' option is only supported on linear sliders.");
}
- if ( parsed.padding < 0 ) {
- throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be a positive number.");
+ if ( parsed.padding[0] < 0 || parsed.padding[1] < 0 ) {
+ throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be a positive number(s).");
}
- if ( parsed.padding >= 50 ) {
+ if ( parsed.padding[0] >= 50 || parsed.padding[1] >= 50 ) {
throw new Error("noUiSlider (" + VERSION + "): 'padding' option must be less than half the range.");
}
}
@@ -262,14 +271,6 @@
};
}
- function testMultitouch ( parsed, entry ) {
- parsed.multitouch = entry;
-
- if ( typeof entry !== 'boolean' ){
- throw new Error("noUiSlider (" + VERSION + "): 'multitouch' option must be a boolean.");
- }
- }
-
function testTooltips ( parsed, entry ) {
if ( entry === false ) {
@@ -339,14 +340,6 @@
}
}
- function testUseRaf ( parsed, entry ) {
- if ( entry === true || entry === false ) {
- parsed.useRequestAnimationFrame = entry;
- } else {
- throw new Error("noUiSlider (" + VERSION + "): 'useRequestAnimationFrame' option should be true (default) or false.");
- }
- }
-
// Test all developer settings and parse to assumption-safe values.
function testOptions ( options ) {
@@ -379,20 +372,17 @@
'limit': { r: false, t: testLimit },
'padding': { r: false, t: testPadding },
'behaviour': { r: true, t: testBehaviour },
- 'multitouch': { r: true, t: testMultitouch },
'ariaFormat': { r: false, t: testAriaFormat },
'format': { r: false, t: testFormat },
'tooltips': { r: false, t: testTooltips },
'cssPrefix': { r: false, t: testCssPrefix },
- 'cssClasses': { r: false, t: testCssClasses },
- 'useRequestAnimationFrame': { r: false, t: testUseRaf }
+ 'cssClasses': { r: false, t: testCssClasses }
};
var defaults = {
'connect': false,
'direction': 'ltr',
'behaviour': 'tap',
- 'multitouch': false,
'orientation': 'horizontal',
'cssPrefix' : 'noUi-',
'cssClasses': {
@@ -406,6 +396,7 @@
vertical: 'vertical',
background: 'background',
connect: 'connect',
+ connects: 'connects',
ltr: 'ltr',
rtl: 'rtl',
draggable: 'draggable',
@@ -428,8 +419,7 @@
valueNormal: 'value-normal',
valueLarge: 'value-large',
valueSub: 'value-sub'
- },
- 'useRequestAnimationFrame': true
+ }
};
// AriaFormat defaults to regular format, if any.
@@ -458,11 +448,20 @@
// Forward pips options
parsed.pips = options.pips;
+ // All recent browsers accept unprefixed transform.
+ // We need -ms- for IE9 and -webkit- for older Android;
+ // Assume use of -webkit- if unprefixed and -ms- are not supported.
+ // https://caniuse.com/#feat=transforms2d
+ var d = document.createElement("div");
+ var msPrefix = d.style.msTransform !== undefined;
+ var noPrefix = d.style.transform !== undefined;
+
+ parsed.transformRule = noPrefix ? 'transform' : (msPrefix ? 'msTransform' : 'webkitTransform');
+
+ // Pips don't move, so we can place them using left/top.
var styles = [['left', 'top'], ['right', 'bottom']];
- // Pre-define the styles.
parsed.style = styles[parsed.dir][parsed.ort];
- parsed.styleOposite = styles[parsed.dir?0:1][parsed.ort];
return parsed;
}
diff --git a/src/js/pips.js b/src/js/pips.js
index 68cf8cfb..b24b3065 100644
--- a/src/js/pips.js
+++ b/src/js/pips.js
@@ -8,22 +8,23 @@
if ( mode === 'count' ) {
- if ( !values ) {
- throw new Error("noUiSlider (" + VERSION + "): 'values' required for mode 'count'.");
+ if ( values < 2 ) {
+ throw new Error("noUiSlider (" + VERSION + "): 'values' (>= 2) required for mode 'count'.");
}
// Divide 0 - 100 in 'count' parts.
- var spread = ( 100 / (values - 1) );
- var v;
- var i = 0;
+ var interval = values - 1;
+ var spread = ( 100 / interval );
values = [];
// List these parts and have them handled as 'positions'.
- while ( (v = i++ * spread) <= 100 ) {
- values.push(v);
+ while ( interval-- ) {
+ values[ interval ] = ( interval * spread );
}
+ values.push(100);
+
mode = 'positions';
}
@@ -216,6 +217,7 @@
if ( values[1] ) {
node = addNodeTo(element, false);
node.className = getClasses(values[1], options.cssClasses.value);
+ node.setAttribute('data-value', values[0]);
node.style[options.style] = offset + '%';
node.innerText = formatter.to(values[0]);
}
diff --git a/src/js/range.js b/src/js/range.js
index 39922cbb..1b4dc9c8 100644
--- a/src/js/range.js
+++ b/src/js/range.js
@@ -118,7 +118,7 @@
}
// Reject any invalid input, by testing whether value is an array.
- if ( Object.prototype.toString.call( value ) !== '[object Array]' ){
+ if ( !Array.isArray(value) ){
throw new Error("noUiSlider (" + VERSION + "): 'range' contains invalid value.");
}
diff --git a/src/js/scope.js b/src/js/scope.js
index 97ee2828..ed855a20 100644
--- a/src/js/scope.js
+++ b/src/js/scope.js
@@ -34,11 +34,11 @@
if ( options.padding ) {
if ( handleNumber === 0 ) {
- to = Math.max(to, options.padding);
+ to = Math.max(to, options.padding[0]);
}
if ( handleNumber === scope_Handles.length - 1 ) {
- to = Math.min(to, 100 - options.padding);
+ to = Math.min(to, 100 - options.padding[1]);
}
}
@@ -55,10 +55,24 @@
return to;
}
+ // Uses slider orientation to create CSS rules. a = base value;
+ function inRuleOrder ( v, a ) {
+ var o = options.ort;
+ return (o?a:v) + ', ' + (o?v:a);
+ }
+
function toPct ( pct ) {
return pct + '%';
}
+ // Takes a base value and an offset. This offset is used for the connect bar size.
+ // In the initial design for this feature, the origin element was 1% wide.
+ // Unfortunately, a rounding bug in Chrome makes it impossible to implement this feature
+ // in this manner: https://bugs.chromium.org/p/chromium/issues/detail?id=798223
+ function transformDirection ( a, b ) {
+ return options.dir ? 100 - a - b : a;
+ }
+
// Updates scope_Locations and scope_Values, updates visual state
function updateHandlePosition ( handleNumber, to ) {
@@ -68,22 +82,11 @@
// Convert the value to the slider stepping/range.
scope_Values[handleNumber] = scope_Spectrum.fromStepping(to);
- // Called synchronously or on the next animationFrame
- var stateUpdate = function() {
- scope_Handles[handleNumber].style[options.style] = toPct(to);
- updateConnect(handleNumber);
- updateConnect(handleNumber + 1);
- };
-
- // Set the handle to the new position.
- // Use requestAnimationFrame for efficient painting.
- // No significant effect in Chrome, Edge sees dramatic performace improvements.
- // Option to disable is useful for unit tests, and single-step debugging.
- if ( window.requestAnimationFrame && options.useRequestAnimationFrame ) {
- window.requestAnimationFrame(stateUpdate);
- } else {
- stateUpdate();
- }
+ var rule = 'translate(' + inRuleOrder(toPct(transformDirection(to, 0)), '0') + ')';
+ scope_Handles[handleNumber].style[options.transformRule] = rule;
+
+ updateConnect(handleNumber);
+ updateConnect(handleNumber + 1);
}
function setZindex ( ) {
@@ -94,7 +97,7 @@
// [[7] [8] .......... | .......... [5] [4]
var dir = (scope_Locations[handleNumber] > 50 ? -1 : 1);
var zIndex = 3 + (scope_Handles.length + (dir * handleNumber));
- scope_Handles[handleNumber].childNodes[0].style.zIndex = zIndex;
+ scope_Handles[handleNumber].style.zIndex = zIndex;
});
}
@@ -131,31 +134,40 @@
h = scope_Locations[index];
}
- scope_Connects[index].style[options.style] = toPct(l);
- scope_Connects[index].style[options.styleOposite] = toPct(100 - h);
+ // We use two rules:
+ // 'translate' to change the left/top offset;
+ // 'scale' to change the width of the element;
+ // As the element has a width of 100%, a translation of 100% is equal to 100% of the parent (.noUi-base)
+ var connectWidth = h - l;
+ var translateRule = 'translate(' + inRuleOrder(toPct(transformDirection(l, connectWidth)), '0') + ')';
+ var scaleRule = 'scale(' + inRuleOrder(connectWidth / 100, '1') + ')';
+
+ scope_Connects[index].style[options.transformRule] = translateRule + ' ' + scaleRule;
}
- // ...
- function setValue ( to, handleNumber ) {
+ // Parses value passed to .set method. Returns current value if not parseable.
+ function resolveToValue ( to, handleNumber ) {
// Setting with null indicates an 'ignore'.
// Inputting 'false' is invalid.
- if ( to === null || to === false ) {
- return;
+ if ( to === null || to === false || to === undefined ) {
+ return scope_Locations[handleNumber];
}
- // If a formatted number was passed, attemt to decode it.
+ // If a formatted number was passed, attempt to decode it.
if ( typeof to === 'number' ) {
to = String(to);
}
to = options.format.from(to);
+ to = scope_Spectrum.toStepping(to);
- // Request an update for all links if the value was invalid.
- // Do so too if setting the handle fails.
- if ( to !== false && !isNaN(to) ) {
- setHandle(handleNumber, scope_Spectrum.toStepping(to), false, false);
+ // If parsing the number failed, use the current value.
+ if ( to === false || isNaN(to) ) {
+ return scope_Locations[handleNumber];
}
+
+ return to;
}
// Set the slider value.
@@ -167,17 +179,20 @@
// Event fires by default
fireSetEvent = (fireSetEvent === undefined ? true : !!fireSetEvent);
- values.forEach(setValue);
-
// Animation is optional.
// Make sure the initial values were set before using animated placement.
if ( options.animate && !isInit ) {
addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration);
}
- // Now that all base values are set, apply constraints
+ // First pass, without lookAhead but with lookBackward. Values are set from left to right.
+ scope_HandleNumbers.forEach(function(handleNumber){
+ setHandle(handleNumber, resolveToValue(values[handleNumber], handleNumber), true, false);
+ });
+
+ // Second pass. Now that all base values are set, apply constraints
scope_HandleNumbers.forEach(function(handleNumber){
- setHandle(handleNumber, scope_Locations[handleNumber], true, false);
+ setHandle(handleNumber, scope_Locations[handleNumber], true, true);
});
setZindex();
diff --git a/src/js/scope_events.js b/src/js/scope_events.js
index 2bc2bd6c..307bb248 100644
--- a/src/js/scope_events.js
+++ b/src/js/scope_events.js
@@ -105,6 +105,7 @@
target: event.target,
handle: handle,
listeners: listeners,
+ doNotReject: true,
handleNumbers: data.handleNumbers
});
@@ -112,6 +113,7 @@
target: event.target,
handle: handle,
listeners: listeners,
+ doNotReject: true,
handleNumbers: data.handleNumbers
});
diff --git a/src/js/scope_helpers.js b/src/js/scope_helpers.js
index 4e96f0ef..1ae13f7f 100644
--- a/src/js/scope_helpers.js
+++ b/src/js/scope_helpers.js
@@ -13,19 +13,22 @@
var method = function ( e ){
- if ( scope_Target.hasAttribute('disabled') ) {
+ e = fixEvent(e, data.pageOffset, data.target || element);
+
+ // fixEvent returns false if this event has a different target
+ // when handling (multi-) touch events;
+ if ( !e ) {
return false;
}
- // Stop if an active 'tap' transition is taking place.
- if ( hasClass(scope_Target, options.cssClasses.tap) ) {
+ // doNotReject is passed by all end events to make sure released touches
+ // are not rejected, leaving the slider "stuck" to the cursor;
+ if ( scope_Target.hasAttribute('disabled') && !data.doNotReject ) {
return false;
}
- e = fixEvent(e, data.pageOffset, data.target || element);
-
- // Handle reject of multitouch
- if ( !e ) {
+ // Stop if an active 'tap' transition is taking place.
+ if ( hasClass(scope_Target, options.cssClasses.tap) && !data.doNotReject ) {
return false;
}
@@ -86,44 +89,40 @@
// In the event that multitouch is activated, the only thing one handle should be concerned
// about is the touches that originated on top of it.
- if ( touch && options.multitouch ) {
+ if ( touch ) {
+
// Returns true if a touch originated on the target.
var isTouchOnTarget = function (touch) {
return touch.target === target || target.contains(touch.target);
};
+
// In the case of touchstart events, we need to make sure there is still no more than one
// touch on the target so we look amongst all touches.
if (e.type === 'touchstart') {
+
var targetTouches = Array.prototype.filter.call(e.touches, isTouchOnTarget);
+
// Do not support more than one touch per handle.
if ( targetTouches.length > 1 ) {
return false;
}
+
x = targetTouches[0].pageX;
y = targetTouches[0].pageY;
+
} else {
- // In the other cases, find on changedTouches is enough.
+
+ // In the other cases, find on changedTouches is enough.
var targetTouch = Array.prototype.find.call(e.changedTouches, isTouchOnTarget);
+
// Cancel if the target touch has not moved.
if ( !targetTouch ) {
return false;
}
+
x = targetTouch.pageX;
y = targetTouch.pageY;
}
- } else if ( touch ) {
- // Fix bug when user touches with two or more fingers on mobile devices.
- // It's useful when you have two or more sliders on one page,
- // that can be touched simultaneously.
- // #649, #663, #668
- if ( e.touches.length > 1 ) {
- return false;
- }
-
- // noUiSlider supports one movement at a time,
- // so we can select the first 'changedTouch'.
- x = e.changedTouches[0].pageX;
- y = e.changedTouches[0].pageY;
}
pageOffset = pageOffset || getPageOffset(scope_Document);
diff --git a/src/js/structure.js b/src/js/structure.js
index e39d57ed..1a311e10 100644
--- a/src/js/structure.js
+++ b/src/js/structure.js
@@ -51,10 +51,12 @@
// Add handles to the slider base.
function addElements ( connectOptions, base ) {
+ var connectBase = addNodeTo(base, options.cssClasses.connects);
+
scope_Handles = [];
scope_Connects = [];
- scope_Connects.push(addConnect(base, connectOptions[0]));
+ scope_Connects.push(addConnect(connectBase, connectOptions[0]));
// [::::O====O====O====]
// connectOptions = [0, 1, 1, 1]
@@ -63,7 +65,7 @@
// Keep a list of all added handles.
scope_Handles.push(addOrigin(base, i));
scope_HandleNumbers[i] = i;
- scope_Connects.push(addConnect(base, connectOptions[i + 1]));
+ scope_Connects.push(addConnect(connectBase, connectOptions[i + 1]));
}
}
diff --git a/src/nouislider.core.less b/src/nouislider.core.less
index 6939c29a..5196889c 100644
--- a/src/nouislider.core.less
+++ b/src/nouislider.core.less
@@ -20,46 +20,53 @@
position: relative;
direction: ltr;
}
-.@{noUi-css-prefix}-base {
+.@{noUi-css-prefix}-base,
+.@{noUi-css-prefix}-connects {
width: 100%;
height: 100%;
position: relative;
- z-index: 1; /* Fix 401 */
+ z-index: 1;
}
-.@{noUi-css-prefix}-connect {
+/* Wrapper for all connect elements.
+ */
+.@{noUi-css-prefix}-connects {
+ overflow: hidden;
+ z-index: 0;
+}
+.@{noUi-css-prefix}-connect,
+.@{noUi-css-prefix}-origin {
+ will-change: transform;
position: absolute;
- right: 0;
+ z-index: 1;
top: 0;
left: 0;
- bottom: 0;
+ height: 100%;
+ width: 100%;
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
}
-.@{noUi-css-prefix}-origin {
- position: absolute;
- height: 0;
+
+/* Give origins 0 height/width so they don't interfere with clicking the
+ * connect elements.
+ */
+.@{noUi-css-prefix}-vertical .@{noUi-css-prefix}-origin {
width: 0;
}
+.@{noUi-css-prefix}-horizontal .@{noUi-css-prefix}-origin {
+ height: 0;
+}
.@{noUi-css-prefix}-handle {
position: relative;
- z-index: 1;
}
.@{noUi-css-prefix}-state-tap .@{noUi-css-prefix}-connect,
.@{noUi-css-prefix}-state-tap .@{noUi-css-prefix}-origin {
--webkit-transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
- transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
+-webkit-transition: transform 0.3s;
+ transition: transform 0.3s;
}
.@{noUi-css-prefix}-state-drag * {
cursor: inherit !important;
}
-/* Painting and performance;
- * Browsers can paint handles in their own layer.
- */
-.@{noUi-css-prefix}-base,
-.@{noUi-css-prefix}-handle {
- -webkit-transform: translate3d(0,0,0);
- transform: translate3d(0,0,0);
-}
-
/* Slider size and handle placement;
*/
.@{noUi-css-prefix}-horizontal {
@@ -82,6 +89,7 @@
}
/* Styling;
+ * Giving the connect element a border radius causes issues with using transform: scale
*/
.@{noUi-css-prefix}-target {
background: #FAFAFA;
@@ -89,12 +97,11 @@
border: 1px solid #D3D3D3;
box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
}
+.@{noUi-css-prefix}-connects {
+ border-radius: 3px;
+}
.@{noUi-css-prefix}-connect {
background: #3FB8AF;
- border-radius: 4px;
- box-shadow: inset 0 0 3px rgba(51,51,51,0.45);
--webkit-transition: background 450ms;
- transition: background 450ms;
}
/* Handles and cursors;
diff --git a/src/nouislider.pips.less b/src/nouislider.pips.less
index 5f23f24b..4f737b9d 100644
--- a/src/nouislider.pips.less
+++ b/src/nouislider.pips.less
@@ -50,8 +50,13 @@
width: 100%;
}
.@{noUi-css-prefix}-value-horizontal {
- -webkit-transform: translate3d(-50%,50%,0);
- transform: translate3d(-50%,50%,0);
+ -webkit-transform: translate(-50%, 50%);
+ transform: translate(-50%, 50%);
+
+ .@{noUi-css-prefix}-rtl & {
+ -webkit-transform: translate(50%, 50%);
+ transform: translate(50%, 50%);
+ }
}
.@{noUi-css-prefix}-marker-horizontal.@{noUi-css-prefix}-marker {
@@ -76,9 +81,14 @@
left: 100%;
}
.@{noUi-css-prefix}-value-vertical {
- -webkit-transform: translate3d(0,50%,0);
- transform: translate3d(0,50%,0);
+ -webkit-transform: translate(0, -50%);
+ transform: translate(0,-50%,0);
padding-left: 25px;
+
+ .@{noUi-css-prefix}-rtl & {
+ -webkit-transform: translate(0, 50%);
+ transform: translate(0, 50%);
+ }
}
.@{noUi-css-prefix}-marker-vertical.@{noUi-css-prefix}-marker {
diff --git a/tests/addon_pips.js b/tests/addon_pips.js
index 5a8c0f0f..ae8de751 100644
--- a/tests/addon_pips.js
+++ b/tests/addon_pips.js
@@ -24,7 +24,7 @@
// RANGE
- QUnit.test( "Range", function( assert ){
+ QUnit.test( "Pips: Range", function( assert ){
var slider = test_slider({
mode: 'range',
@@ -45,7 +45,7 @@
assert.equal( Q.querySelector('.noUi-value').innerHTML, '0.00' );
});
- QUnit.test( "Steps", function( assert ){
+ QUnit.test( "Pips: Steps", function( assert ){
var slider = test_slider({
mode: 'steps',
@@ -60,7 +60,7 @@
});
- QUnit.test( "Positions", function( assert ){
+ QUnit.test( "Pips: Positions", function( assert ){
var slider = test_slider({
mode: 'positions',
@@ -81,7 +81,7 @@
});
- QUnit.test( "Positions, stepped", function( assert ){
+ QUnit.test( "Pips: Positions, stepped", function( assert ){
expect(0); // TODO
@@ -94,27 +94,36 @@
// POSITIONS (STEPPED)
});
- QUnit.test( "Count", function( assert ){
+ QUnit.test( "Pips: Count", function( assert ){
var slider = test_slider({
mode: 'count',
- values: 8
+ values: 12
});
// COUNT
- assert.equal( Q.querySelectorAll('.noUi-value').length, 8, 'Placed requested number of values' );
+ assert.equal( Q.querySelectorAll('.noUi-value').length, 12, 'Placed requested number of values' );
var pos2 = [];
Array.prototype.forEach.call(Q.querySelectorAll('.noUi-value'), function( el ){
pos2.push(parseInt(el.style.left));
});
- assert.deepEqual(pos2, [0, Math.floor((100/7)*1), Math.floor((100/7)*2), Math.floor((100/7)*3), Math.floor((100/7)*4), Math.floor((100/7)*5), Math.floor((100/7)*6), 100], 'Values spread evenly');
+ assert.deepEqual( pos2, [0, Math.floor( (100 / 11) * 1 ), Math.floor( (100 / 11) * 2 ), Math.floor( (100 / 11) * 3 ), Math.floor( (100 / 11) * 4 ), Math.floor( (100 / 11) * 5 ), Math.floor( (100 / 11) * 6 ), Math.floor( (100 / 11) * 7 ), Math.floor( (100 / 11) * 8 ), Math.floor( (100 / 11) * 9 ), Math.floor( (100 / 11) * 10 ), 100], 'Values spread evenly' );
+
+ } );
+
+ QUnit.test( "Pips: Count, values >= 2", function (assert) {
+
+ assert.throws( function() { test_slider( {
+ mode: 'count',
+ values: 1
+ } ) }, 'Checks minimum number of values' );
});
- QUnit.test( "Count, stepped", function( assert ){
+ QUnit.test( "Pips: Count, stepped", function( assert ){
expect(0); // TODO
@@ -127,7 +136,7 @@
// VALUES
- QUnit.test( "Values", function( assert ){
+ QUnit.test( "Pips: Values", function( assert ){
// #357
var slider = test_slider({
@@ -140,7 +149,7 @@
// VALUES (STEPPED)
- QUnit.test( "Values, stepped", function( assert ){
+ QUnit.test( "Pips: Values, stepped", function( assert ){
var slider = test_slider({
mode: 'values',
@@ -153,7 +162,7 @@
// #528, #532
- QUnit.test( "Values, stepped", function( assert ){
+ QUnit.test( "Pips: Values, stepped", function( assert ){
Q.innerHTML = '';
var slider = Q.querySelector('.slider');
diff --git a/tests/slider.html b/tests/slider.html
index 864f172d..111e9fc7 100644
--- a/tests/slider.html
+++ b/tests/slider.html
@@ -20,7 +20,7 @@
-
+
@@ -68,6 +68,7 @@
+
diff --git a/tests/slider_errors.js b/tests/slider_errors.js
index 2df5b3f7..9a00238d 100644
--- a/tests/slider_errors.js
+++ b/tests/slider_errors.js
@@ -74,17 +74,6 @@
});
});
- assert.throws(function(){
- noUiSlider.create(slider, {
- start: [ 1 ],
- range: {
- 'min': 0,
- 'max': 10
- },
- useRequestAnimationFrame: 'Hello'
- });
- }, "Should error if useRequestAnimationFrame not a boolean.");
-
assert.throws(function(){
noUiSlider.create(slider, {
start: 10,
diff --git a/tests/slider_lookaround.js b/tests/slider_lookaround.js
new file mode 100644
index 00000000..ec64fdf4
--- /dev/null
+++ b/tests/slider_lookaround.js
@@ -0,0 +1,137 @@
+
+ QUnit.test( "Padding and margin (lookaround)", function( assert ){
+
+ Q.innerHTML = '';
+
+ var slider = Q.querySelector('#slider1');
+ var slider2 = Q.querySelector('#slider2');
+ var slider3 = Q.querySelector('#slider3');
+
+ noUiSlider.create(slider, {
+ start: [ 20, 23 ],
+ step: 1,
+ padding: 5,
+ margin: 3,
+ range: {
+ 'min': 10,
+ 'max': 30
+ }
+ });
+
+ function m ( a ) {
+ var values = slider.noUiSlider.get();
+ values[0] = parseInt(values[0]) + a;
+ values[1] = parseInt(values[1]) + a;
+ slider.noUiSlider.set(values);
+ }
+
+ function up ( ) { m(1); }
+ function down ( ) { m(-1); }
+
+ assert.deepEqual( slider.noUiSlider.get(), ['20.00', '23.00']);
+
+ up();
+ assert.deepEqual( slider.noUiSlider.get(), ['21.00', '24.00']);
+
+ up();
+ assert.deepEqual( slider.noUiSlider.get(), ['22.00', '25.00'], 'Highest possible values');
+
+ up();
+ assert.deepEqual( slider.noUiSlider.get(), ['22.00', '25.00'], 'Nothing should have happened');
+
+ down();
+ assert.deepEqual( slider.noUiSlider.get(), ['21.00', '24.00']);
+
+ down();
+ assert.deepEqual( slider.noUiSlider.get(), ['20.00', '23.00']);
+
+ slider.noUiSlider.set([10, 12]);
+ assert.deepEqual( slider.noUiSlider.get(), ['15.00', '18.00'], 'lower bound by padding, upper by margin');
+
+ slider.noUiSlider.set([15, 17]);
+ assert.deepEqual( slider.noUiSlider.get(), ['15.00', '18.00'], 'lower bound ok, but apply margin');
+
+
+
+
+ noUiSlider.create(slider2, {
+ start: [ 20, 23, 27 ],
+ step: 1,
+ padding: 3,
+ limit: 4,
+ margin: 3,
+ range: {
+ 'min': 10,
+ 'max': 40
+ }
+ });
+
+ slider2.noUiSlider.set([20, 24, 27]);
+ assert.deepEqual(slider2.noUiSlider.get(), ['20.00', '24.00', '27.00']);
+
+ slider2.noUiSlider.set([19, 24, 27]);
+ assert.deepEqual(slider2.noUiSlider.get(), ['19.00', '23.00', '27.00'], 'Cannot do 24, exceeds limit');
+
+ slider2.noUiSlider.set([18, 19, 23]);
+ assert.deepEqual(slider2.noUiSlider.get(), ['18.00', '21.00', '24.00'], 'Cannot do 19, exceeds margin. Same for 23 after 19 becomes 21.');
+
+ slider2.noUiSlider.set([18, 19, 29]);
+ assert.deepEqual(slider2.noUiSlider.get(), ['18.00', '21.00', '25.00'], 'Cannot do 25, exceeds limit.');
+
+ slider2.noUiSlider.set([12, 15, 40]);
+ assert.deepEqual(slider2.noUiSlider.get(), ['13.00', '16.00', '20.00'], 'Padding, margin and limit');
+
+
+
+
+ noUiSlider.create(slider3, {
+ start: [ 20, 23, 27 ],
+ step: 3,
+ padding: 6,
+ limit: 9,
+ behaviour: 'drag',
+ margin: 6,
+ range: {
+ 'min': 0,
+ 'max': 100
+ }
+ });
+
+ slider3.noUiSlider.set([20, 24, 27]);
+ assert.deepEqual(slider3.noUiSlider.get(), ['21.00', '27.00', '33.00'], 'Margin and step');
+
+ slider3.noUiSlider.set([21, 27, 36]);
+ assert.deepEqual(slider3.noUiSlider.get(), ['21.00', '27.00', '36.00'], 'Limit');
+
+ slider3.noUiSlider.__moveHandles(true, 10, [0, 1]); // Drag the first two handles up by 10 pct
+ assert.deepEqual(slider3.noUiSlider.get(), ['24.00', '30.00', '36.00'], 'Margin and limit for two handles');
+
+ slider3.noUiSlider.__moveHandles(true, 20, [0, 1]); // Try to push up further
+ assert.deepEqual(slider3.noUiSlider.get(), ['24.00', '30.00', '36.00'], 'Nothing should change');
+
+ slider3.noUiSlider.__moveHandles(true, 10, [1, 2]); // Move the last two handles
+ assert.deepEqual(slider3.noUiSlider.get(), ['24.00', '33.00', '39.00'], 'Can slide once');
+
+ slider3.noUiSlider.__moveHandles(true, 10, [1, 2]); // Move the last two handles
+ assert.deepEqual(slider3.noUiSlider.get(), ['24.00', '33.00', '39.00'], 'Not again, blocked by limit on the 2nd handle.');
+
+ slider3.noUiSlider.set([76, 85, 91]);
+ assert.deepEqual(slider3.noUiSlider.get(), ['75.00', '84.00', '90.00']);
+
+ slider3.noUiSlider.set([78, 85, 91]);
+ assert.deepEqual(slider3.noUiSlider.get(), ['78.00', '84.00', '90.00']);
+
+ slider3.noUiSlider.__moveHandles(true, 10, [1, 2]); // Move the last two handles
+ assert.deepEqual(slider3.noUiSlider.get(), ['78.00', '87.00', '93.00']);
+
+ slider3.noUiSlider.__moveHandles(false, -10, [1, 2]); // Back down
+ assert.deepEqual(slider3.noUiSlider.get(), ['78.00', '84.00', '90.00']);
+
+ slider3.noUiSlider.__moveHandles(false, -10, [0, 1]); // Back down
+ assert.deepEqual(slider3.noUiSlider.get(), ['75.00', '81.00', '90.00']);
+
+ slider3.noUiSlider.__moveHandles(false, -10, [0, 1]); // Back down
+ assert.deepEqual(slider3.noUiSlider.get(), ['75.00', '81.00', '90.00'], 'Cannot, limited by limit between last handles');
+
+
+ });
diff --git a/tests/slider_padding.js b/tests/slider_padding.js
index 147596af..29a19871 100644
--- a/tests/slider_padding.js
+++ b/tests/slider_padding.js
@@ -35,3 +35,33 @@
slider.noUiSlider.set( [ 3, 10 ] );
assert.deepEqual( slider.noUiSlider.get(), ['3.00', '9.00'], 'RTL set.' );
});
+
+ QUnit.test( "Padding option", function( assert ){
+
+ Q.innerHTML = '';
+
+ var settings = {
+ start: [ 0, 100 ],
+ padding: [ 10, 5 ],
+ range: {
+ 'min': 0,
+ 'max': 100
+ }
+ };
+
+ var slider = Q.querySelector('.slider');
+
+ noUiSlider.create(slider, settings);
+
+ assert.deepEqual( slider.noUiSlider.get(), ['10.00', '95.00'], 'Different paddings applied' );
+
+ // =============
+
+ slider.noUiSlider.destroy();
+
+ settings.padding = [0, 10];
+
+ noUiSlider.create(slider, settings);
+ assert.deepEqual( slider.noUiSlider.get(), ['0.00', '90.00'], 'One of the padding values is 0' );
+
+ });
diff --git a/tests/slider_setting-getting.js b/tests/slider_setting-getting.js
index 5838bc76..6aafc029 100644
--- a/tests/slider_setting-getting.js
+++ b/tests/slider_setting-getting.js
@@ -10,7 +10,6 @@
start: [ 0, 10 ],
behaviour: 'drag',
connect: true,
- useRequestAnimationFrame: false,
format: {
to: function(x){
return x.toFixed(1);
diff --git a/tests/slider_three_or_more_handles.js b/tests/slider_three_or_more_handles.js
index 424b9ac7..e750d505 100644
--- a/tests/slider_three_or_more_handles.js
+++ b/tests/slider_three_or_more_handles.js
@@ -49,8 +49,7 @@
'max': [20]
},
animate: false,
- animationDuration: 0,
- useRequestAnimationFrame: false
+ animationDuration: 0
});
var handles2 = slider2.querySelectorAll('.noUi-handle'),
@@ -62,7 +61,7 @@
rightmostHandlePos = rightmostHandle.getBoundingClientRect();
assert.deepEqual(middleHandlePos, rightmostHandlePos, "Two handles in the same location should have the same on-screen position");
- assert.notDeepEqual(middleHandlePos, leftmostHandlePos, "Handles at different ends of the slider should have different positions. This might mean requestAnimationFrame is waiting for a repaint before moving the handles, or the box you're drawing into is off screen.");
+ assert.notDeepEqual(middleHandlePos, leftmostHandlePos, "Handles at different ends of the slider should have different positions.");
var middleHandleX = (middleHandlePos.right+middleHandlePos.left)/2,
middleHandleY = (middleHandlePos.top+middleHandlePos.bottom)/2,
@@ -79,7 +78,7 @@
clickY = middleHandleY,
click1x = leftmostHandlePos.right*0.75+middleHandlePos.left*0.25,
click2x = leftmostHandlePos.right*0.25+middleHandlePos.left*0.75;
-
+
assert.deepEqual(slider2.noUiSlider.get(), [ "10.00", "20.00", "20.00" ], "Checking initial state");
simulateMousedown(clickTarget, click1x, clickY);
diff --git a/tests/slider_update.js b/tests/slider_update.js
index ad0ac0de..c11c9448 100644
--- a/tests/slider_update.js
+++ b/tests/slider_update.js
@@ -17,7 +17,7 @@
slider.noUiSlider.destroy();
- equal(slider.innerHTML, '', 'Slider was cleared');
+ assert.equal(slider.innerHTML, '', 'Slider was cleared');
var settings = {
range: { min: 30, max: 70 },
@@ -35,14 +35,14 @@
slider.noUiSlider.set(40);
assert.deepEqual(slider.noUiSlider.get(), ['40', '70']);
- equal ( slider.querySelectorAll('.noUi-connect').length, 0, 'Slider uses no connection' );
+ assert.equal ( slider.querySelectorAll('.noUi-connect').length, 0, 'Slider uses no connection' );
settings.connect = true;
slider.noUiSlider.destroy();
noUiSlider.create(slider, settings);
- equal ( slider.querySelectorAll('.noUi-connect').length, 1, 'Slider now connects' );
+ assert.equal ( slider.querySelectorAll('.noUi-connect').length, 1, 'Slider now connects' );
assert.deepEqual(slider.noUiSlider.get(), ['30', '60'], 'Value was unchanged');