diff --git a/.editorconfig b/.editorconfig index 2c665c4c..1b63c278 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,5 @@ root = true charset = utf-8 indent_style = space indent_size = 4 +end_of_line = lf +insert_final_newline = true diff --git a/README.md b/README.md index c85cd3be..48dfcf6c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ noUiSlider is a lightweight JavaScript range slider. - All modern browsers and IE > 9 are supported - Fully **responsive** - **Multi-touch support** on Android, iOS and Windows devices +- Accessible with `aria` and keyboard support - Tons of [examples](https://refreshless.com/nouislider/examples) and answered [Stack Overflow questions](https://stackoverflow.com/questions/tagged/nouislider) License @@ -19,6 +20,15 @@ An extensive documentation, including **examples**, **options** and **configurat Changelog --------- +### 13.0.0 (*???*) +noUiSlider 13 does not include any breaking API changes. +Keyboard support is now built-in, so any custom implementations should be removed when upgrading. +Alternatively, built-in keyboard support can be disabled using `keyboardSupport: false`. +- Added: Built-in keyboard support (#724) +- Added: `.noUi-touch-area` element (#924) +- Fixed: Dragging a range does not check for handle disabled state (#938) +- Fixed: Incorrect CSS transform in pips (#931) + ### 12.1.0 (*2018-10-25*) - Added: `unconstrained` behaviour (#747, #815, #913) - Added: `setHandle` API (#917) @@ -159,4 +169,5 @@ is enough: ``` import 'nouislider'; +import 'nouislider/distribute/nouislider.css'; ``` diff --git a/distribute/nouislider.css b/distribute/nouislider.css index cb9d5ff3..55ef90af 100644 --- a/distribute/nouislider.css +++ b/distribute/nouislider.css @@ -1,4 +1,4 @@ -/*! nouislider - 12.1.0 - 10/25/2018 */ +/*! nouislider - 12.1.0 - 2/6/2019 */ /* Functional styling; * These styles are required for noUiSlider to function. * You don't need to change these rules to apply your design. @@ -64,6 +64,10 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-origin { .noUi-handle { position: absolute; } +.noUi-touch-area { + height: 100%; + width: 100%; +} .noUi-state-tap .noUi-connect, .noUi-state-tap .noUi-origin { -webkit-transition: transform 0.3s; @@ -242,7 +246,7 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-handle { } .noUi-value-vertical { -webkit-transform: translate(0, -50%); - transform: translate(0, -50%, 0); + transform: translate(0, -50%); padding-left: 25px; } .noUi-rtl .noUi-value-vertical { diff --git a/distribute/nouislider.js b/distribute/nouislider.js index 3bf6352d..58ed5eb3 100644 --- a/distribute/nouislider.js +++ b/distribute/nouislider.js @@ -1,4 +1,4 @@ -/*! nouislider - 12.1.0 - 10/25/2018 */ +/*! nouislider - 12.1.0 - 2/6/2019 */ (function(factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. @@ -423,6 +423,17 @@ return value; }; + Spectrum.prototype.getDefaultStep = function(value, isDown, size) { + var j = getJ(value, this.xPct); + + // When at the top or stepping down, look at the previous sub-range + if (value === 100 || (isDown && value === this.xPct[j - 1])) { + j = Math.max(j - 1, 1); + } + + return (this.xVal[j] - this.xVal[j - 1]) / size; + }; + Spectrum.prototype.getNearbySteps = function(value) { var j = getJ(value, this.xPct); @@ -861,6 +872,7 @@ handle: "handle", handleLower: "handle-lower", handleUpper: "handle-upper", + touchArea: "touch-area", horizontal: "horizontal", vertical: "vertical", background: "background", @@ -939,18 +951,29 @@ var supportsPassive = supportsTouchActionNone && getSupportsPassive(); // All variables local to 'scope' are prefixed with 'scope_' + + // Slider DOM Nodes var scope_Target = target; - var scope_Locations = []; var scope_Base; var scope_Handles; - var scope_HandleNumbers = []; - var scope_ActiveHandlesCount = 0; var scope_Connects; + var scope_Pips; + + // Override for the 'animate' option + var scope_ShouldAnimate = true; + + // Slider state values var scope_Spectrum = options.spectrum; var scope_Values = []; + var scope_Locations = []; + var scope_HandleNumbers = []; + var scope_ActiveHandlesCount = 0; var scope_Events = {}; + + // Exposed API var scope_Self; - var scope_Pips; + + // Document Nodes var scope_Document = target.ownerDocument; var scope_DocumentElement = options.documentElement || scope_Document.documentElement; var scope_Body = scope_Document.body; @@ -983,12 +1006,17 @@ var origin = addNodeTo(base, options.cssClasses.origin); var handle = addNodeTo(origin, options.cssClasses.handle); + addNodeTo(handle, options.cssClasses.touchArea); + handle.setAttribute("data-handle", handleNumber); if (options.keyboardSupport) { // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex // 0 = focusable and reachable handle.setAttribute("tabindex", "0"); + handle.addEventListener("keydown", function(event) { + return eventKeydown(event, handleNumber); + }); } handle.setAttribute("role", "slider"); @@ -1060,6 +1088,12 @@ return addNodeTo(handle.firstChild, options.cssClasses.tooltip); } + // Disable the slider dragging if any handle is disabled + function isHandleDisabled(handleNumber) { + var handleOrigin = scope_Handles[handleNumber]; + return handleOrigin.hasAttribute("disabled"); + } + // The tooltips option is a shorthand for using the 'update' event. function tooltips() { // Tooltips are added with options.tooltips in original order. @@ -1508,7 +1542,7 @@ scope_Handles.forEach(function(handle, index) { // Disabled handles are ignored - if (handle.hasAttribute("disabled")) { + if (isHandleDisabled(index)) { return; } @@ -1584,15 +1618,16 @@ // Bind move events on document. function eventStart(event, data) { + // Ignore event if any handle is disabled + if (data.handleNumbers.some(isHandleDisabled)) { + return false; + } + var handle; + if (data.handleNumbers.length === 1) { var handleOrigin = scope_Handles[data.handleNumbers[0]]; - // Ignore 'disabled' handles - if (handleOrigin.hasAttribute("disabled")) { - return false; - } - handle = handleOrigin.children[0]; scope_ActiveHandlesCount += 1; @@ -1715,6 +1750,61 @@ }); } + // Handles keydown on focused handles + // Don't move the document when pressing arrow keys on focused handles + function eventKeydown(event, handleNumber) { + if (isHandleDisabled(handleNumber)) { + return false; + } + + var horizontalKeys = ["Left", "Right"]; + var verticalKeys = ["Down", "Up"]; + + if (options.dir && !options.ort) { + // On an right-to-left slider, the left and right keys act inverted + horizontalKeys.reverse(); + } else if (options.ort && !options.dir) { + // On a top-to-bottom slider, the up and down keys act inverted + verticalKeys.reverse(); + } + + // Strip "Arrow" for IE compatibility. https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + var key = event.key.replace("Arrow", ""); + var isDown = key === verticalKeys[0] || key === horizontalKeys[0]; + var isUp = key === verticalKeys[1] || key === horizontalKeys[1]; + + if (!isDown && !isUp) { + return true; + } + + event.preventDefault(); + + var direction = isDown ? 0 : 1; + var steps = getNextStepsForHandle(handleNumber); + var step = steps[direction]; + + // At the edge of a slider, do nothing + if (step === null) { + return false; + } + + // No step set, use the default of 10% of the sub-range + if (step === false) { + step = scope_Spectrum.getDefaultStep(scope_Locations[handleNumber], isDown, 10); + } + + // Decrement for down steps + step = (isDown ? -1 : 1) * step; + + scope_ShouldAnimate = false; + + valueSetHandle(handleNumber, scope_Values[handleNumber] + step, true); + + scope_ShouldAnimate = true; + + return false; + } + // Attach events to several slider parts. function bindSliderEvents(behaviour) { // Attach the standard drag event to the handles. @@ -1826,10 +1916,6 @@ }); } - function toPct(pct) { - return pct + "%"; - } - // Split out the handle positioning logic so the Move event can use it, too function checkHandlePosition(reference, handleNumber, to, lookBackward, lookForward, getValue) { // For sliders with multiple handles, limit movement to the other handle. @@ -1964,7 +2050,7 @@ // Convert the value to the slider stepping/range. scope_Values[handleNumber] = scope_Spectrum.fromStepping(to); - var rule = "translate(" + inRuleOrder(toPct(transformDirection(to, 0) - scope_DirOffset), "0") + ")"; + var rule = "translate(" + inRuleOrder(transformDirection(to, 0) - scope_DirOffset + "%", "0") + ")"; scope_Handles[handleNumber].style[options.transformRule] = rule; updateConnect(handleNumber); @@ -2018,7 +2104,7 @@ // '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 translateRule = "translate(" + inRuleOrder(transformDirection(l, connectWidth) + "%", "0") + ")"; var scaleRule = "scale(" + inRuleOrder(connectWidth / 100, "1") + ")"; scope_Connects[index].style[options.transformRule] = translateRule + " " + scaleRule; @@ -2058,7 +2144,7 @@ // Animation is optional. // Make sure the initial values were set before using animated placement. - if (options.animate && !isInit) { + if (options.animate && !isInit && scope_ShouldAnimate) { addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); } @@ -2137,57 +2223,58 @@ delete scope_Target.noUiSlider; } - // Get the current step size for the slider. - function getCurrentStep() { - // Check all locations, map them to their stepping point. - // Get the step point, then find it in the input list. - return scope_Locations.map(function(location, index) { - var nearbySteps = scope_Spectrum.getNearbySteps(location); - var value = scope_Values[index]; - var increment = nearbySteps.thisStep.step; - var decrement = null; - - // If the next value in this step moves into the next step, - // the increment is the start of the next step - the current value - if (increment !== false) { - if (value + increment > nearbySteps.stepAfter.startValue) { - increment = nearbySteps.stepAfter.startValue - value; - } - } + function getNextStepsForHandle(handleNumber) { + var location = scope_Locations[handleNumber]; + var nearbySteps = scope_Spectrum.getNearbySteps(location); + var value = scope_Values[handleNumber]; + var increment = nearbySteps.thisStep.step; + var decrement = null; - // If the value is beyond the starting point - if (value > nearbySteps.thisStep.startValue) { - decrement = nearbySteps.thisStep.step; - } else if (nearbySteps.stepBefore.step === false) { - decrement = false; + // If the next value in this step moves into the next step, + // the increment is the start of the next step - the current value + if (increment !== false) { + if (value + increment > nearbySteps.stepAfter.startValue) { + increment = nearbySteps.stepAfter.startValue - value; } + } - // If a handle is at the start of a step, it always steps back into the previous step first - else { - decrement = value - nearbySteps.stepBefore.highestStep; - } + // If the value is beyond the starting point + if (value > nearbySteps.thisStep.startValue) { + decrement = nearbySteps.thisStep.step; + } else if (nearbySteps.stepBefore.step === false) { + decrement = false; + } - // Now, if at the slider edges, there is not in/decrement - if (location === 100) { - increment = null; - } else if (location === 0) { - decrement = null; - } + // If a handle is at the start of a step, it always steps back into the previous step first + else { + decrement = value - nearbySteps.stepBefore.highestStep; + } - // As per #391, the comparison for the decrement step can have some rounding issues. - var stepDecimals = scope_Spectrum.countStepDecimals(); + // Now, if at the slider edges, there is no in/decrement + if (location === 100) { + increment = null; + } else if (location === 0) { + decrement = null; + } - // Round per #391 - if (increment !== null && increment !== false) { - increment = Number(increment.toFixed(stepDecimals)); - } + // As per #391, the comparison for the decrement step can have some rounding issues. + var stepDecimals = scope_Spectrum.countStepDecimals(); - if (decrement !== null && decrement !== false) { - decrement = Number(decrement.toFixed(stepDecimals)); - } + // Round per #391 + if (increment !== null && increment !== false) { + increment = Number(increment.toFixed(stepDecimals)); + } - return [decrement, increment]; - }); + if (decrement !== null && decrement !== false) { + decrement = Number(decrement.toFixed(stepDecimals)); + } + + return [decrement, increment]; + } + + // Get the current step size for the slider. + function getNextSteps() { + return scope_HandleNumbers.map(getNextStepsForHandle); } // Updateable: margin, limit, padding, step, range, animate, snap @@ -2232,21 +2319,37 @@ valueSet(optionsToUpdate.start || v, fireSetEvent); } - // Create the base element, initialize HTML and set classes. - // Add handles and connect elements. - scope_Base = addSlider(scope_Target); - addElements(options.connect, scope_Base); + // Initialization steps + function setupSlider() { + // Create the base element, initialize HTML and set classes. + // Add handles and connect elements. + scope_Base = addSlider(scope_Target); + + addElements(options.connect, scope_Base); + + // Attach user events. + bindSliderEvents(options.events); + + // Use the public value method to set the start values. + valueSet(options.start); + + if (options.pips) { + pips(options.pips); + } - // Attach user events. - bindSliderEvents(options.events); + if (options.tooltips) { + tooltips(); + } + + aria(); + } - // Use the public value method to set the start values. - valueSet(options.start); + setupSlider(); // noinspection JSUnusedGlobalSymbols scope_Self = { destroy: destroy, - steps: getCurrentStep, + steps: getNextSteps, on: bindEvent, off: removeEvent, get: valueGet, @@ -2264,16 +2367,6 @@ pips: pips // Issue #594 }; - if (options.pips) { - pips(options.pips); - } - - if (options.tooltips) { - tooltips(); - } - - aria(); - return scope_Self; } diff --git a/distribute/nouislider.min.css b/distribute/nouislider.min.css index ea1d6dd7..a53678d8 100644 --- a/distribute/nouislider.min.css +++ b/distribute/nouislider.min.css @@ -1,2 +1,2 @@ -/*! nouislider - 12.1.0 - 10/25/2018 */ -.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative;direction:ltr}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;left:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;transform-origin:0 0}html:not([dir=rtl]) .noUi-horizontal .noUi-origin{left:auto;right:0}.noUi-vertical .noUi-origin{width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{position:absolute}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;left:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;left:-6px;top:-17px}html:not([dir=rtl]) .noUi-horizontal .noUi-handle{right:-17px;left:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%,0);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%} \ No newline at end of file +/*! nouislider - 12.1.0 - 2/6/2019 */ +.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative;direction:ltr}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;left:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;transform-origin:0 0}html:not([dir=rtl]) .noUi-horizontal .noUi-origin{left:auto;right:0}.noUi-vertical .noUi-origin{width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;left:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;left:-6px;top:-17px}html:not([dir=rtl]) .noUi-horizontal .noUi-handle{right:-17px;left:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%} \ No newline at end of file diff --git a/distribute/nouislider.min.js b/distribute/nouislider.min.js index 6e3307c2..640826ef 100644 --- a/distribute/nouislider.min.js +++ b/distribute/nouislider.min.js @@ -1,2 +1,2 @@ -/*! nouislider - 12.1.0 - 10/25/2018 */ -!function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():window.noUiSlider=t()}(function(){"use strict";var et="12.1.0";function s(t){return null!=t}function rt(t){t.preventDefault()}function i(t){return"number"==typeof t&&!isNaN(t)&&isFinite(t)}function nt(t,e,r){0=e[r];)r+=1;return r}function r(t,e,r){if(r>=t.slice(-1)[0])return 100;var n,i,o=f(r,t),a=t[o-1],s=t[o],l=e[o-1],u=e[o];return l+(i=r,p(n=[a,s],n[0]<0?i+Math.abs(n[0]):i-n[0])/c(l,u))}function n(t,e,r,n){if(100===n)return n;var i,o,a=f(n,t),s=t[a-1],l=t[a];return r?(l-s)/2= 2) required for mode 'count'.");var n=e-1,i=100/n;for(e=[];n--;)e[n]=n*i;e.push(100),t="positions"}return"positions"===t?e.map(function(t){return E.fromStepping(r?E.getStep(t):t)}):"values"===t?r?e.map(function(t){return E.fromStepping(E.getStep(E.toStepping(t)))}):e:void 0}(n,t.values||!1,t.stepped||!1),s=(m=i,g=n,v=a,b={},e=E.xVal[0],r=E.xVal[E.xVal.length-1],w=S=!1,x=0,(v=v.slice().sort(function(t,e){return t-e}).filter(function(t){return!this[t]&&(this[t]=!0)},{}))[0]!==e&&(v.unshift(e),S=!0),v[v.length-1]!==r&&(v.push(r),w=!0),v.forEach(function(t,e){var r,n,i,o,a,s,l,u,c,p,f=t,d=v[e+1],h="steps"===g;if(h&&(r=E.xNumSteps[e]),r||(r=d-f),!1!==f&&void 0!==d)for(r=Math.max(r,1e-7),n=f;n<=d;n=(n+r).toFixed(7)/1){for(u=(a=(o=E.toStepping(n))-x)/m,p=a/(c=Math.round(u)),i=1;i<=c;i+=1)b[(s=x+i*p).toFixed(5)]=[E.fromStepping(s),0];l=-1r.stepAfter.startValue&&(i=r.stepAfter.startValue-n),o=n>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&n-r.stepBefore.highestStep,100===t?i=null:0===t&&(o=null);var a=E.countStepDecimals();return null!==i&&!1!==i&&(i=Number(i.toFixed(a))),null!==o&&!1!==o&&(o=Number(o.toFixed(a))),[o,i]})},on:X,off:function(t){var n=t&&t.split(".")[0],i=n&&t.substring(n.length);Object.keys(S).forEach(function(t){var e=t.split(".")[0],r=t.substring(e.length);n&&n!==e||i&&i!==r||delete S[t]})},get:tt,set:Z,setHandle:function(t,e,r){var n=[];if(!(0<=(t=Number(t))&&t=e[r];)r+=1;return r}function r(t,e,r){if(r>=t.slice(-1)[0])return 100;var n,i,o=f(r,t),a=t[o-1],s=t[o],l=e[o-1],u=e[o];return l+(i=r,p(n=[a,s],n[0]<0?i+Math.abs(n[0]):i-n[0])/c(l,u))}function n(t,e,r,n){if(100===n)return n;var i,o,a=f(n,t),s=t[a-1],l=t[a];return r?(l-s)/2= 2) required for mode 'count'.");var n=e-1,i=100/n;for(e=[];n--;)e[n]=n*i;e.push(100),t="positions"}return"positions"===t?e.map(function(t){return E.fromStepping(r?E.getStep(t):t)}):"values"===t?r?e.map(function(t){return E.fromStepping(E.getStep(E.toStepping(t)))}):e:void 0}(n,t.values||!1,t.stepped||!1),s=(m=i,g=n,v=a,b={},e=E.xVal[0],r=E.xVal[E.xVal.length-1],w=S=!1,x=0,(v=v.slice().sort(function(t,e){return t-e}).filter(function(t){return!this[t]&&(this[t]=!0)},{}))[0]!==e&&(v.unshift(e),S=!0),v[v.length-1]!==r&&(v.push(r),w=!0),v.forEach(function(t,e){var r,n,i,o,a,s,l,u,c,p,f=t,d=v[e+1],h="steps"===g;if(h&&(r=E.xNumSteps[e]),r||(r=d-f),!1!==f&&void 0!==d)for(r=Math.max(r,1e-7),n=f;n<=d;n=(n+r).toFixed(7)/1){for(u=(a=(o=E.toStepping(n))-x)/m,p=a/(c=Math.round(u)),i=1;i<=c;i+=1)b[(s=x+i*p).toFixed(5)]=[E.fromStepping(s),0];l=-1r.stepAfter.startValue&&(i=r.stepAfter.startValue-n),o=n>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&n-r.stepBefore.highestStep,100===e?i=null:0===e&&(o=null);var a=E.countStepDecimals();return null!==i&&!1!==i&&(i=Number(i.toFixed(a))),null!==o&&!1!==o&&(o=Number(o.toFixed(a))),[o,i]}return ct(e=y,f.cssClasses.target),0===f.dir?ct(e,f.cssClasses.ltr):ct(e,f.cssClasses.rtl),0===f.ort?ct(e,f.cssClasses.horizontal):ct(e,f.cssClasses.vertical),l=V(e,f.cssClasses.base),function(t,e){var r=V(e,f.cssClasses.connects);u=[],(a=[]).push(L(r,t[0]));for(var n=0;n + + + + + +
+

Horizontal - LTR

+
+
+
+

Horizontal - RTL

+
+
+
+

Vertical - LTR

+
+
+
+

Vertical - RTL

+
+
+ + + + + + diff --git a/documentation/configurations/horizontal-ltr.js b/documentation/configurations/horizontal-ltr.js new file mode 100644 index 00000000..7ac5b678 --- /dev/null +++ b/documentation/configurations/horizontal-ltr.js @@ -0,0 +1,15 @@ +var slider = document.getElementById('horizontal-ltr'); + +noUiSlider.create(slider, { + start: [20, 80], + connect: true, + direction: "ltr", + orientation: "horizontal", + behaviour: "drag-range", + range: { + 'min': 0, + 'max': 100 + } +}); + +document.querySelector('#horizontal-ltr .noUi-origin').setAttribute('disabled', 'disabled'); diff --git a/documentation/configurations/horizontal-rtl.js b/documentation/configurations/horizontal-rtl.js new file mode 100644 index 00000000..2d1a4e05 --- /dev/null +++ b/documentation/configurations/horizontal-rtl.js @@ -0,0 +1,12 @@ +var slider = document.getElementById('horizontal-rtl'); + +noUiSlider.create(slider, { + start: [20, 80], + connect: true, + direction: "rtl", + orientation: "horizontal", + range: { + 'min': 0, + 'max': 100 + } +}); diff --git a/documentation/configurations/vertical-ltr.js b/documentation/configurations/vertical-ltr.js new file mode 100644 index 00000000..9a680e36 --- /dev/null +++ b/documentation/configurations/vertical-ltr.js @@ -0,0 +1,12 @@ +var slider = document.getElementById('vertical-ltr'); + +noUiSlider.create(slider, { + start: [20, 80], + connect: true, + direction: "ltr", + orientation: "vertical", + range: { + 'min': 0, + 'max': 100 + } +}); diff --git a/documentation/configurations/vertical-rtl.js b/documentation/configurations/vertical-rtl.js new file mode 100644 index 00000000..d7d72428 --- /dev/null +++ b/documentation/configurations/vertical-rtl.js @@ -0,0 +1,12 @@ +var slider = document.getElementById('vertical-rtl'); + +noUiSlider.create(slider, { + start: [20, 80], + connect: true, + direction: "rtl", + orientation: "vertical", + range: { + 'min': 0, + 'max': 100 + } +}); diff --git a/documentation/contact.php b/documentation/contact.php index 08f51e5f..23b33468 100644 --- a/documentation/contact.php +++ b/documentation/contact.php @@ -38,6 +38,6 @@
-

Besides noUiSlider, I've written more on tech-related subjects, ranging from PHP to Embedded subjects. Give my blog a try if you are interested! +

Besides noUiSlider, I've written more on tech-related subjects, ranging from PHP to Embedded subjects. Give my blog a try if you are interested!

diff --git a/documentation/events-callbacks.php b/documentation/events-callbacks.php index bab97f96..ae0d12ea 100644 --- a/documentation/events-callbacks.php +++ b/documentation/events-callbacks.php @@ -115,6 +115,15 @@ No No + + A handle is moved by arrow keys + Yes + No + Yes + No + No + No + diff --git a/documentation/examples-content/keyboard.php b/documentation/examples-content/keyboard.php deleted file mode 100644 index dd568dec..00000000 --- a/documentation/examples-content/keyboard.php +++ /dev/null @@ -1,33 +0,0 @@ - -

Adding keyboard support

- -
- -
- -

Handles can be focused, but noUiSlider does not offer keyboard support by default. It can be added by adding a keypress listener on a handle.

- -
- -
- - -
-
- -
- -
Initializing the slider
- -
- -
- -
Listen to keypress on the handle
- -
- -
- -
-
diff --git a/documentation/examples-content/keypress.php b/documentation/examples-content/steps-api.php similarity index 72% rename from documentation/examples-content/keypress.php rename to documentation/examples-content/steps-api.php index eb995607..12900af8 100644 --- a/documentation/examples-content/keypress.php +++ b/documentation/examples-content/steps-api.php @@ -1,5 +1,5 @@ - -

Changing the slider by key press

+ +

Using the steps API

@@ -11,14 +11,12 @@

We'll listen to keydown on the input element, and pass the event to a function so we can read the code that identifies the key.

-

Note that the slider value will be a string, so we'll need to parse it to an integer.

-
-
+
- - + +
@@ -27,13 +25,13 @@
Initializing the slider and linking the input
- +
Listen to keypress on the input
- +
diff --git a/documentation/examples.php b/documentation/examples.php index 285620f9..afd8047f 100644 --- a/documentation/examples.php +++ b/documentation/examples.php @@ -6,7 +6,6 @@
- @@ -36,7 +34,7 @@ - + diff --git a/documentation/examples/keyboard-slider.js b/documentation/examples/keyboard-slider.js deleted file mode 100644 index 33697f4f..00000000 --- a/documentation/examples/keyboard-slider.js +++ /dev/null @@ -1,10 +0,0 @@ -var keyboardSlider = document.getElementById('keyboard'); - -noUiSlider.create(keyboardSlider, { - start: 10, - step: 10, - range: { - 'min': 0, - 'max': 100 - } -}); diff --git a/documentation/examples/keyboard.js b/documentation/examples/keyboard.js deleted file mode 100644 index 3c299d1c..00000000 --- a/documentation/examples/keyboard.js +++ /dev/null @@ -1,14 +0,0 @@ -var handle = keyboardSlider.querySelector('.noUi-handle'); - -handle.addEventListener('keydown', function (e) { - - var value = Number(keyboardSlider.noUiSlider.get()); - - if (e.which === 37) { - keyboardSlider.noUiSlider.set(value - 10); - } - - if (e.which === 39) { - keyboardSlider.noUiSlider.set(value + 10); - } -}); diff --git a/documentation/examples/keypress-event.js b/documentation/examples/steps-event.js similarity index 74% rename from documentation/examples/keypress-event.js rename to documentation/examples/steps-event.js index 5f4f1a2b..28a9ef13 100644 --- a/documentation/examples/keypress-event.js +++ b/documentation/examples/steps-event.js @@ -2,16 +2,16 @@ inputs.forEach(function (input, handle) { input.addEventListener('change', function () { - keypressSlider.noUiSlider.setHandle(handle, this.value); + stepsSlider.noUiSlider.setHandle(handle, this.value); }); input.addEventListener('keydown', function (e) { - var values = keypressSlider.noUiSlider.get(); + var values = stepsSlider.noUiSlider.get(); var value = Number(values[handle]); // [[handle0_down, handle0_up], [handle1_down, handle1_up]] - var steps = keypressSlider.noUiSlider.steps(); + var steps = stepsSlider.noUiSlider.steps(); // [down, up] var step = steps[handle]; @@ -24,7 +24,7 @@ inputs.forEach(function (input, handle) { switch (e.which) { case 13: - keypressSlider.noUiSlider.setHandle(handle, this.value); + stepsSlider.noUiSlider.setHandle(handle, this.value); break; case 38: @@ -39,7 +39,7 @@ inputs.forEach(function (input, handle) { // null = edge of slider if (position !== null) { - keypressSlider.noUiSlider.setHandle(handle, value + position); + stepsSlider.noUiSlider.setHandle(handle, value + position); } break; @@ -53,7 +53,7 @@ inputs.forEach(function (input, handle) { } if (position !== null) { - keypressSlider.noUiSlider.setHandle(handle, value - position); + stepsSlider.noUiSlider.setHandle(handle, value - position); } break; diff --git a/documentation/examples/keypress-slider.js b/documentation/examples/steps-slider.js similarity index 72% rename from documentation/examples/keypress-slider.js rename to documentation/examples/steps-slider.js index 4d7be9ee..294931de 100644 --- a/documentation/examples/keypress-slider.js +++ b/documentation/examples/steps-slider.js @@ -1,9 +1,9 @@ -var keypressSlider = document.getElementById('keypress'); +var stepsSlider = document.getElementById('steps-slider'); var input0 = document.getElementById('input-with-keypress-0'); var input1 = document.getElementById('input-with-keypress-1'); var inputs = [input0, input1]; -noUiSlider.create(keypressSlider, { +noUiSlider.create(stepsSlider, { start: [20, 80], connect: true, tooltips: [true, wNumb({decimals: 1})], @@ -16,6 +16,6 @@ noUiSlider.create(keypressSlider, { } }); -keypressSlider.noUiSlider.on('update', function (values, handle) { +stepsSlider.noUiSlider.on('update', function (values, handle) { inputs[handle].value = values[handle]; }); diff --git a/documentation/index.php b/documentation/index.php index 0105e4c3..ebcbcca6 100644 --- a/documentation/index.php +++ b/documentation/index.php @@ -1,6 +1,6 @@
@@ -10,16 +10,16 @@

noUiSlider: lightweight JavaScript range slider with full touch support

    -
  • Responsive design friendly
  • -
  • Touch support for iOS, Android & Windows (phone)
  • -
  • ARIA support
  • -
  • No dependencies!
  • -
  • Tested in IE9 - IE11, Edge, Chrome, Opera, Firefox & Safari
  • +
  • Multi-Touch support for iOS, Android & Windows (phone)
  • +
  • Accessible with ARIA and keyboard support
  • +
  • Responsive design friendly
  • +
  • No dependencies
  • +
  • Tested in IE9 - IE11, Edge, Chrome, Firefox & Safari
Download noUiSlider -

noUiSlider is a lightweight range slider with full touch support and a ton of features. It works with pretty much any device, whether it has a mouse, touchscreen or both, and it'll work great in responsive designs. Have you tried this documentation on your phone?

+

noUiSlider is a lightweight range slider with multi-touch support and a ton of features. It supports non-linear ranges, requires no external dependencies, has keyboard support, and it works great in responsive designs. Have you tried this documentation on your phone?

diff --git a/documentation/more.php b/documentation/more.php index c6e2a064..40cbdc92 100644 --- a/documentation/more.php +++ b/documentation/more.php @@ -79,7 +79,7 @@

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, // Object
 	true // Boolean 'fireSetEvent'
@@ -150,7 +150,7 @@
 
-

Styling noUiSlider is easy. The default stylesheet contains helpful comments to get a head start.

+

If you want to style noUiSlider, 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.

@@ -189,6 +189,10 @@ .noUi-handle The actual, visible handles. Style these any way you like! + + .noUi-touch-area + An empty div withing .noUi-handle. Can be styled larger if desired. + .noUi-connect Styling class for setting properties related to the slider connect segment. diff --git a/documentation/slider-options.php b/documentation/slider-options.php index 36af588b..3fd59e84 100644 --- a/documentation/slider-options.php +++ b/documentation/slider-options.php @@ -423,10 +423,12 @@
-

Handles in the slider can receive keyboard focus by default. This can be turned off.

+

Handles in the slider can receive keyboard focus and be moved by arrow keys.

-

Example: Adding keyboard support

-

Example: Using the .steps() API to determine the next step value

+

When moved by the arrow keys on a keyboard, handles obey the step value for the range they are in. + When moving in a range that has no step value set, handles move by 10% of the range they are in.

+ +

Keyboard support can be disabled:

diff --git a/documentation/slider-values/non-linear-step-link.js b/documentation/slider-values/non-linear-step-link.js index 2f580e17..a6aa3f81 100644 --- a/documentation/slider-values/non-linear-step-link.js +++ b/documentation/slider-values/non-linear-step-link.js @@ -1,5 +1,5 @@ var nonLinearStepSliderValueElement = document.getElementById('slider-non-linear-step-value'); -nonLinearStepSlider.noUiSlider.on('update', function (values, handle) { - nonLinearStepSliderValueElement.innerHTML = values[handle]; +nonLinearStepSlider.noUiSlider.on('update', function (values) { + nonLinearStepSliderValueElement.innerHTML = values.join(' - '); }); diff --git a/src/nouislider.core.less b/src/nouislider.core.less index c0121ad9..bcc8f7a3 100644 --- a/src/nouislider.core.less +++ b/src/nouislider.core.less @@ -65,6 +65,10 @@ html:not([dir="rtl"]) .@{noUi-css-prefix}-horizontal .@{noUi-css-prefix}-origin .@{noUi-css-prefix}-handle { position: absolute; } +.@{noUi-css-prefix}-touch-area { + height: 100%; + width: 100%; +} .@{noUi-css-prefix}-state-tap .@{noUi-css-prefix}-connect, .@{noUi-css-prefix}-state-tap .@{noUi-css-prefix}-origin { -webkit-transition: transform 0.3s; diff --git a/src/nouislider.js b/src/nouislider.js index 5c983d2c..7b9e3689 100644 --- a/src/nouislider.js +++ b/src/nouislider.js @@ -422,6 +422,17 @@ return value; }; + Spectrum.prototype.getDefaultStep = function(value, isDown, size) { + var j = getJ(value, this.xPct); + + // When at the top or stepping down, look at the previous sub-range + if (value === 100 || (isDown && value === this.xPct[j - 1])) { + j = Math.max(j - 1, 1); + } + + return (this.xVal[j] - this.xVal[j - 1]) / size; + }; + Spectrum.prototype.getNearbySteps = function(value) { var j = getJ(value, this.xPct); @@ -860,6 +871,7 @@ handle: "handle", handleLower: "handle-lower", handleUpper: "handle-upper", + touchArea: "touch-area", horizontal: "horizontal", vertical: "vertical", background: "background", @@ -938,18 +950,29 @@ var supportsPassive = supportsTouchActionNone && getSupportsPassive(); // All variables local to 'scope' are prefixed with 'scope_' + + // Slider DOM Nodes var scope_Target = target; - var scope_Locations = []; var scope_Base; var scope_Handles; - var scope_HandleNumbers = []; - var scope_ActiveHandlesCount = 0; var scope_Connects; + var scope_Pips; + + // Override for the 'animate' option + var scope_ShouldAnimate = true; + + // Slider state values var scope_Spectrum = options.spectrum; var scope_Values = []; + var scope_Locations = []; + var scope_HandleNumbers = []; + var scope_ActiveHandlesCount = 0; var scope_Events = {}; + + // Exposed API var scope_Self; - var scope_Pips; + + // Document Nodes var scope_Document = target.ownerDocument; var scope_DocumentElement = options.documentElement || scope_Document.documentElement; var scope_Body = scope_Document.body; @@ -982,12 +1005,17 @@ var origin = addNodeTo(base, options.cssClasses.origin); var handle = addNodeTo(origin, options.cssClasses.handle); + addNodeTo(handle, options.cssClasses.touchArea); + handle.setAttribute("data-handle", handleNumber); if (options.keyboardSupport) { // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex // 0 = focusable and reachable handle.setAttribute("tabindex", "0"); + handle.addEventListener("keydown", function(event) { + return eventKeydown(event, handleNumber); + }); } handle.setAttribute("role", "slider"); @@ -1059,6 +1087,12 @@ return addNodeTo(handle.firstChild, options.cssClasses.tooltip); } + // Disable the slider dragging if any handle is disabled + function isHandleDisabled(handleNumber) { + var handleOrigin = scope_Handles[handleNumber]; + return handleOrigin.hasAttribute("disabled"); + } + // The tooltips option is a shorthand for using the 'update' event. function tooltips() { // Tooltips are added with options.tooltips in original order. @@ -1507,7 +1541,7 @@ scope_Handles.forEach(function(handle, index) { // Disabled handles are ignored - if (handle.hasAttribute("disabled")) { + if (isHandleDisabled(index)) { return; } @@ -1583,15 +1617,16 @@ // Bind move events on document. function eventStart(event, data) { + // Ignore event if any handle is disabled + if (data.handleNumbers.some(isHandleDisabled)) { + return false; + } + var handle; + if (data.handleNumbers.length === 1) { var handleOrigin = scope_Handles[data.handleNumbers[0]]; - // Ignore 'disabled' handles - if (handleOrigin.hasAttribute("disabled")) { - return false; - } - handle = handleOrigin.children[0]; scope_ActiveHandlesCount += 1; @@ -1714,6 +1749,61 @@ }); } + // Handles keydown on focused handles + // Don't move the document when pressing arrow keys on focused handles + function eventKeydown(event, handleNumber) { + if (isHandleDisabled(handleNumber)) { + return false; + } + + var horizontalKeys = ["Left", "Right"]; + var verticalKeys = ["Down", "Up"]; + + if (options.dir && !options.ort) { + // On an right-to-left slider, the left and right keys act inverted + horizontalKeys.reverse(); + } else if (options.ort && !options.dir) { + // On a top-to-bottom slider, the up and down keys act inverted + verticalKeys.reverse(); + } + + // Strip "Arrow" for IE compatibility. https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + var key = event.key.replace("Arrow", ""); + var isDown = key === verticalKeys[0] || key === horizontalKeys[0]; + var isUp = key === verticalKeys[1] || key === horizontalKeys[1]; + + if (!isDown && !isUp) { + return true; + } + + event.preventDefault(); + + var direction = isDown ? 0 : 1; + var steps = getNextStepsForHandle(handleNumber); + var step = steps[direction]; + + // At the edge of a slider, do nothing + if (step === null) { + return false; + } + + // No step set, use the default of 10% of the sub-range + if (step === false) { + step = scope_Spectrum.getDefaultStep(scope_Locations[handleNumber], isDown, 10); + } + + // Decrement for down steps + step = (isDown ? -1 : 1) * step; + + scope_ShouldAnimate = false; + + valueSetHandle(handleNumber, scope_Values[handleNumber] + step, true); + + scope_ShouldAnimate = true; + + return false; + } + // Attach events to several slider parts. function bindSliderEvents(behaviour) { // Attach the standard drag event to the handles. @@ -1825,10 +1915,6 @@ }); } - function toPct(pct) { - return pct + "%"; - } - // Split out the handle positioning logic so the Move event can use it, too function checkHandlePosition(reference, handleNumber, to, lookBackward, lookForward, getValue) { // For sliders with multiple handles, limit movement to the other handle. @@ -1963,7 +2049,7 @@ // Convert the value to the slider stepping/range. scope_Values[handleNumber] = scope_Spectrum.fromStepping(to); - var rule = "translate(" + inRuleOrder(toPct(transformDirection(to, 0) - scope_DirOffset), "0") + ")"; + var rule = "translate(" + inRuleOrder(transformDirection(to, 0) - scope_DirOffset + "%", "0") + ")"; scope_Handles[handleNumber].style[options.transformRule] = rule; updateConnect(handleNumber); @@ -2017,7 +2103,7 @@ // '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 translateRule = "translate(" + inRuleOrder(transformDirection(l, connectWidth) + "%", "0") + ")"; var scaleRule = "scale(" + inRuleOrder(connectWidth / 100, "1") + ")"; scope_Connects[index].style[options.transformRule] = translateRule + " " + scaleRule; @@ -2057,7 +2143,7 @@ // Animation is optional. // Make sure the initial values were set before using animated placement. - if (options.animate && !isInit) { + if (options.animate && !isInit && scope_ShouldAnimate) { addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration); } @@ -2136,57 +2222,58 @@ delete scope_Target.noUiSlider; } - // Get the current step size for the slider. - function getCurrentStep() { - // Check all locations, map them to their stepping point. - // Get the step point, then find it in the input list. - return scope_Locations.map(function(location, index) { - var nearbySteps = scope_Spectrum.getNearbySteps(location); - var value = scope_Values[index]; - var increment = nearbySteps.thisStep.step; - var decrement = null; - - // If the next value in this step moves into the next step, - // the increment is the start of the next step - the current value - if (increment !== false) { - if (value + increment > nearbySteps.stepAfter.startValue) { - increment = nearbySteps.stepAfter.startValue - value; - } - } + function getNextStepsForHandle(handleNumber) { + var location = scope_Locations[handleNumber]; + var nearbySteps = scope_Spectrum.getNearbySteps(location); + var value = scope_Values[handleNumber]; + var increment = nearbySteps.thisStep.step; + var decrement = null; - // If the value is beyond the starting point - if (value > nearbySteps.thisStep.startValue) { - decrement = nearbySteps.thisStep.step; - } else if (nearbySteps.stepBefore.step === false) { - decrement = false; + // If the next value in this step moves into the next step, + // the increment is the start of the next step - the current value + if (increment !== false) { + if (value + increment > nearbySteps.stepAfter.startValue) { + increment = nearbySteps.stepAfter.startValue - value; } + } - // If a handle is at the start of a step, it always steps back into the previous step first - else { - decrement = value - nearbySteps.stepBefore.highestStep; - } + // If the value is beyond the starting point + if (value > nearbySteps.thisStep.startValue) { + decrement = nearbySteps.thisStep.step; + } else if (nearbySteps.stepBefore.step === false) { + decrement = false; + } - // Now, if at the slider edges, there is not in/decrement - if (location === 100) { - increment = null; - } else if (location === 0) { - decrement = null; - } + // If a handle is at the start of a step, it always steps back into the previous step first + else { + decrement = value - nearbySteps.stepBefore.highestStep; + } - // As per #391, the comparison for the decrement step can have some rounding issues. - var stepDecimals = scope_Spectrum.countStepDecimals(); + // Now, if at the slider edges, there is no in/decrement + if (location === 100) { + increment = null; + } else if (location === 0) { + decrement = null; + } - // Round per #391 - if (increment !== null && increment !== false) { - increment = Number(increment.toFixed(stepDecimals)); - } + // As per #391, the comparison for the decrement step can have some rounding issues. + var stepDecimals = scope_Spectrum.countStepDecimals(); - if (decrement !== null && decrement !== false) { - decrement = Number(decrement.toFixed(stepDecimals)); - } + // Round per #391 + if (increment !== null && increment !== false) { + increment = Number(increment.toFixed(stepDecimals)); + } - return [decrement, increment]; - }); + if (decrement !== null && decrement !== false) { + decrement = Number(decrement.toFixed(stepDecimals)); + } + + return [decrement, increment]; + } + + // Get the current step size for the slider. + function getNextSteps() { + return scope_HandleNumbers.map(getNextStepsForHandle); } // Updateable: margin, limit, padding, step, range, animate, snap @@ -2231,21 +2318,37 @@ valueSet(optionsToUpdate.start || v, fireSetEvent); } - // Create the base element, initialize HTML and set classes. - // Add handles and connect elements. - scope_Base = addSlider(scope_Target); - addElements(options.connect, scope_Base); + // Initialization steps + function setupSlider() { + // Create the base element, initialize HTML and set classes. + // Add handles and connect elements. + scope_Base = addSlider(scope_Target); + + addElements(options.connect, scope_Base); + + // Attach user events. + bindSliderEvents(options.events); + + // Use the public value method to set the start values. + valueSet(options.start); + + if (options.pips) { + pips(options.pips); + } - // Attach user events. - bindSliderEvents(options.events); + if (options.tooltips) { + tooltips(); + } + + aria(); + } - // Use the public value method to set the start values. - valueSet(options.start); + setupSlider(); // noinspection JSUnusedGlobalSymbols scope_Self = { destroy: destroy, - steps: getCurrentStep, + steps: getNextSteps, on: bindEvent, off: removeEvent, get: valueGet, @@ -2263,16 +2366,6 @@ pips: pips // Issue #594 }; - if (options.pips) { - pips(options.pips); - } - - if (options.tooltips) { - tooltips(); - } - - aria(); - return scope_Self; } diff --git a/src/nouislider.pips.less b/src/nouislider.pips.less index 4f737b9d..3b7bb656 100644 --- a/src/nouislider.pips.less +++ b/src/nouislider.pips.less @@ -82,7 +82,7 @@ } .@{noUi-css-prefix}-value-vertical { -webkit-transform: translate(0, -50%); - transform: translate(0,-50%,0); + transform: translate(0, -50%); padding-left: 25px; .@{noUi-css-prefix}-rtl & { diff --git a/tests/slider.html b/tests/slider.html index cd59a5c6..eae97830 100644 --- a/tests/slider.html +++ b/tests/slider.html @@ -23,7 +23,18 @@ - + @@ -38,6 +49,7 @@ + diff --git a/tests/slider_binding.js b/tests/slider_binding.js index 586a5b2f..4704571b 100644 --- a/tests/slider_binding.js +++ b/tests/slider_binding.js @@ -70,11 +70,7 @@ QUnit.test("Binding", function (assert) { // Do this async, because we can't click the slider before it paints. setTimeout(function () { - simulant.fire(slider.querySelectorAll('.noUi-origin')[1], 'mousedown', { - button: 1, // middle-click - clientX: offset(slider).left + 100, - clientY: offset(slider).top + 8 - }); + simulateMousedown(slider.querySelectorAll('.noUi-origin')[1], offset(slider).left + 100, offset(slider).top + 8); slider.noUiSlider.off('.namespace'); diff --git a/tests/slider_contained_handles.js b/tests/slider_contained_handles.js index b2e3751e..68c2cfdd 100644 --- a/tests/slider_contained_handles.js +++ b/tests/slider_contained_handles.js @@ -1,14 +1,3 @@ -function simulateMousedown(clickTarget, x, y) { - // Based on https://stackoverflow.com/a/19570419/1367431 - var clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent( - 'mousedown', true, true, window, 0, - 0, 0, x, y, false, false, - false, false, 0, null - ); - clickTarget.dispatchEvent(clickEvent); -} - QUnit.test("Slider with contained handles", function (assert) { document.getElementById('qunit-fixture').innerHTML = '\ diff --git a/tests/slider_keyboard.js b/tests/slider_keyboard.js new file mode 100644 index 00000000..d4d28424 --- /dev/null +++ b/tests/slider_keyboard.js @@ -0,0 +1,87 @@ +QUnit.test("Keyboard support", function (assert) { + + function up(element) { + element.dispatchEvent(new KeyboardEvent('keydown', {'key': 'ArrowUp'})); + } + + function down(element) { + element.dispatchEvent(new KeyboardEvent('keydown', {'key': 'ArrowDown'})); + } + + document.getElementById('qunit-fixture').innerHTML = '
'; + + var slider = document.getElementById('qunit-fixture').querySelector('.slider'); + + noUiSlider.create(slider, { + start: [165], + connect: true, + range: { + 'min': 50, + '10%': 100, + '30%': [500, 50], + '60%': [1000, 10], + '90%': 1250, + 'max': 1300 + } + }); + + var handle0 = slider.querySelector('[data-handle="0"]'); + + assert.equal(slider.noUiSlider.get(), '165.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '125.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '85.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '80.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '75.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '80.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '85.00'); + + slider.noUiSlider.set(600); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '550.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '500.00'); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '460.00'); + + slider.noUiSlider.set(1000); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '950.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '1000.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '1010.00'); + + slider.noUiSlider.set(1230); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '1240.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '1250.00'); + + up(handle0); + assert.equal(slider.noUiSlider.get(), '1255.00'); + + slider.noUiSlider.set(55); + + down(handle0); + assert.equal(slider.noUiSlider.get(), '50.00'); +}); diff --git a/tests/slider_three_or_more_handles.js b/tests/slider_three_or_more_handles.js index bc5a26a9..fb27ef09 100644 --- a/tests/slider_three_or_more_handles.js +++ b/tests/slider_three_or_more_handles.js @@ -1,14 +1,3 @@ -function simulateMousedown(clickTarget, x, y) { - // Based on https://stackoverflow.com/a/19570419/1367431 - var clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent( - 'mousedown', true, true, window, 0, - 0, 0, x, y, false, false, - false, false, 0, null - ); - clickTarget.dispatchEvent(clickEvent); -} - QUnit.test("Slider with three or more handles", function (assert) { document.getElementById('qunit-fixture').innerHTML = '\ @@ -64,8 +53,8 @@ QUnit.test("Slider with three or more handles", function (assert) { var middleHandleX = (middleHandlePos.right + middleHandlePos.left) / 2; var middleHandleY = (middleHandlePos.top + middleHandlePos.bottom) / 2; - var selectedByClick = document.elementFromPoint(middleHandleX, middleHandleY); - assert.strictEqual(selectedByClick, middleHandle, "Middle handle should be selected by click as rightmost handle is unmovable move") + var selectedByClick = document.elementFromPoint(middleHandleX, middleHandleY).parentElement; + assert.strictEqual(selectedByClick, middleHandle, "Middle handle should be selected by click as rightmost handle is unmovable move"); // xnakos also spotted a bug where clicking