diff --git a/documentation/behaviour-option.php b/documentation/behaviour-option.php index c892b1d..e242046 100644 --- a/documentation/behaviour-option.php +++ b/documentation/behaviour-option.php @@ -10,9 +10,9 @@
noUiSlider offers several ways to handle user interaction. The range can be made draggable, or handles can move to tapped positions. All these effects are optional, and can be enable by adding their keyword to the behaviour
option.
noUiSlider offers several ways to handle user interaction. The range can be made draggable, or handles can move to tapped positions. All these effects are optional, and can be enabled by adding their keyword to the behaviour
option.
This option accepts a "-"
separated list of "drag"
, "drag-all"
, "tap"
, "fixed"
, "snap"
, "unconstrained"
or "none"
.
This option accepts a "-"
separated list of "drag"
, "drag-all"
, "tap"
, "fixed"
, "snap"
, "unconstrained"
, "invert-connects"
or "none"
.
With this option set, connects invert when handles pass each other.
+ +Requires the unconstrained
behaviour and the connect
option. This option is only applicable for sliders with two handles.
noUiSlider has an update method that can change the 'margin'
, 'padding'
, 'limit'
, 'step'
, 'range'
, 'pips'
, 'tooltips'
, 'animate'
and 'snap'
options.
noUiSlider has an update method that can change the 'margin'
, 'padding'
, 'limit'
, 'step'
, 'range'
, 'pips'
, 'tooltips'
, 'connect'
, 'animate'
and 'snap'
options.
All other options require changes to the slider's HTML or event bindings.
diff --git a/src/nouislider.ts b/src/nouislider.ts index a59b868..e7ee831 100644 --- a/src/nouislider.ts +++ b/src/nouislider.ts @@ -131,11 +131,11 @@ interface UpdatableOptions { format?: Formatter; tooltips?: boolean | PartialFormatter | (boolean | PartialFormatter)[]; animate?: boolean; + connect?: "lower" | "upper" | boolean | boolean[]; } export interface Options extends UpdatableOptions { range: Range; - connect?: "lower" | "upper" | boolean | boolean[]; orientation?: "vertical" | "horizontal"; direction?: "ltr" | "rtl"; behaviour?: string; @@ -160,6 +160,7 @@ interface Behaviour { snap: boolean; hover: boolean; unconstrained: boolean; + invertConnects: boolean; } interface ParsedOptions { @@ -1136,6 +1137,7 @@ function testBehaviour(parsed: ParsedOptions, entry: unknown): void { const snap = entry.indexOf("snap") >= 0; const hover = entry.indexOf("hover") >= 0; const unconstrained = entry.indexOf("unconstrained") >= 0; + const invertConnects = entry.indexOf("invert-connects") >= 0; const dragAll = entry.indexOf("drag-all") >= 0; const smoothSteps = entry.indexOf("smooth-steps") >= 0; @@ -1148,6 +1150,10 @@ function testBehaviour(parsed: ParsedOptions, entry: unknown): void { testMargin(parsed, parsed.start[1] - parsed.start[0]); } + if (invertConnects && parsed.handles !== 2) { + throw new Error("noUiSlider: 'invert-connects' behaviour must be used with 2 handles"); + } + if (unconstrained && (parsed.margin || parsed.limit)) { throw new Error("noUiSlider: 'unconstrained' behaviour cannot be used with margin or limit"); } @@ -1161,6 +1167,7 @@ function testBehaviour(parsed: ParsedOptions, entry: unknown): void { snap: snap, hover: hover, unconstrained: unconstrained, + invertConnects: invertConnects, }; } @@ -1367,6 +1374,7 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O // Slider DOM Nodes const scope_Target = target; let scope_Base: HTMLElement; + let scope_ConnectBase: HTMLElement; let scope_Handles: Origin[]; let scope_Connects: (HTMLElement | false)[]; let scope_Pips: HTMLElement | null; @@ -1379,6 +1387,7 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O const scope_HandleNumbers: number[] = []; let scope_ActiveHandlesCount = 0; const scope_Events: { [key: string]: EventCallback[] } = {}; + let scope_ConnectsInverted = false; // Document Nodes const scope_Document = target.ownerDocument; @@ -1452,12 +1461,12 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O // Add handles to the slider base. function addElements(connectOptions: boolean[], base: HTMLElement): void { - const connectBase = addNodeTo(base, options.cssClasses.connects); + scope_ConnectBase = addNodeTo(base, options.cssClasses.connects); scope_Handles = []; scope_Connects = []; - scope_Connects.push(addConnect(connectBase, connectOptions[0])); + scope_Connects.push(addConnect(scope_ConnectBase, connectOptions[0])); // [::::O====O====O====] // connectOptions = [0, 1, 1, 1] @@ -1466,7 +1475,7 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O // Keep a list of all added handles. scope_Handles.push(addOrigin(base, i)); scope_HandleNumbers[i] = i; - scope_Connects.push(addConnect(connectBase, connectOptions[i + 1])); + scope_Connects.push(addConnect(scope_ConnectBase, connectOptions[i + 1])); } } @@ -2666,8 +2675,31 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O (scope_Handles[handleNumber].style as CSSStyleDeclarationIE10)[options.transformRule] = translateRule; + // sanity check for at least 2 handles (e.g. during setup) + if (options.events.invertConnects && scope_Locations.length > 1) { + // check if handles passed each other, but don't match the ConnectsInverted state + const handlesAreInOrder = scope_Locations.every( + (position: number, index: number, locations: number[]): boolean => + index === 0 || position >= locations[index - 1] + ); + + if (scope_ConnectsInverted !== !handlesAreInOrder) { + // invert connects when handles pass each other + invertConnects(); + + // invertConnects already updates all connect elements + return; + } + } + updateConnect(handleNumber); updateConnect(handleNumber + 1); + + if (scope_ConnectsInverted) { + // When connects are inverted, we also have to update adjacent connects + updateConnect(handleNumber - 1); + updateConnect(handleNumber + 2); + } } // Handles before the slider middle are stacked later = higher, @@ -2719,15 +2751,24 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O return; } + // Create a copy of locations, so we can sort them for the local scope logic + const locations = scope_Locations.slice(); + + if (scope_ConnectsInverted) { + locations.sort(function (a, b) { + return a - b; + }); + } + let l = 0; let h = 100; if (index !== 0) { - l = scope_Locations[index - 1]; + l = locations[index - 1]; } if (index !== scope_Connects.length - 1) { - h = scope_Locations[index]; + h = locations[index]; } // We use two rules: @@ -2968,6 +3009,7 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O "format", "pips", "tooltips", + "connect", ]; // Only change options that we're actually passed to update. @@ -3012,6 +3054,39 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O scope_Locations = []; valueSet(isSet(optionsToUpdate.start) ? optionsToUpdate.start : v, fireSetEvent); + + // Update connects only if it was set + if (optionsToUpdate.connect) { + updateConnectOption(); + } + } + + function updateConnectOption() { + // IE supported way of removing children including event handlers + while (scope_ConnectBase.firstChild) { + scope_ConnectBase.removeChild(scope_ConnectBase.firstChild); + } + + // Adding new connects according to the new connect options + for (let i = 0; i <= options.handles; i++) { + scope_Connects[i] = addConnect(scope_ConnectBase, options.connect[i]); + updateConnect(i); + } + + // re-adding drag events for the new connect elements + // to ignore the other events we have to negate the 'if (!behaviour.fixed)' check + bindSliderEvents({ drag: options.events.drag, fixed: true } as Behaviour); + } + + // Invert options for connect handles + function invertConnects() { + scope_ConnectsInverted = !scope_ConnectsInverted; + testConnect( + options, + // inverse the connect boolean array + options.connect.map((b: boolean) => !b) + ); + updateConnectOption(); } // Initialization steps