From 4697c1829acaf182a2b6536299dc271de2bc6529 Mon Sep 17 00:00:00 2001 From: 5andr0 Date: Mon, 10 Jun 2024 19:42:42 +0300 Subject: [PATCH 1/2] Inverted Connects for unconstrained behavior This implementation allows connects to invert when handles pass each other in unconstrained behavior. Added possibility to manually update connects via updateOptions without having to destroy and recreate the slider (which would lose the drag). Also added invertConnects to the API for more control. Set behavior to "unconstrained-invert-connects" to enable this feature. --- documentation/behaviour-option.php | 30 ++++++++ .../behaviour-option/invert-connects-link.js | 9 +++ .../behaviour-option/invert-connects.js | 12 ++++ src/nouislider.ts | 72 +++++++++++++++++-- 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 documentation/behaviour-option/invert-connects-link.js create mode 100644 documentation/behaviour-option/invert-connects.js diff --git a/documentation/behaviour-option.php b/documentation/behaviour-option.php index c892b1d..c9df959 100644 --- a/documentation/behaviour-option.php +++ b/documentation/behaviour-option.php @@ -36,6 +36,7 @@
  • Hover
  • Unconstrained
  • Smooth steps
  • +
  • Invert connects
  • @@ -262,3 +263,32 @@ + + + +

    Invert Connects

    + +
    + +
    +

    With this option set, connects invert when handles pass each other.

    + +

    Requires the unconstrained behaviour and the connect option.

    +
    +
    + + + +
    +
    + +
    + + +
    Show the slider value
    + +
    + +
    +
    +
    diff --git a/documentation/behaviour-option/invert-connects-link.js b/documentation/behaviour-option/invert-connects-link.js new file mode 100644 index 0000000..26cd2dd --- /dev/null +++ b/documentation/behaviour-option/invert-connects-link.js @@ -0,0 +1,9 @@ +var invertConnectsValues = document.getElementById('invert-connects-values'); + +invertConnectsSlider.noUiSlider.on('update', function (values) { + var minToHHMM = function(mins) { + var pad = function(n) { return ('0'+n).slice(-2); }; + return [mins / 60 ^ 0, mins % 60].map(pad).join(':'); + }; + invertConnectsValues.innerHTML = values.map(minToHHMM).join(' - '); +}); \ No newline at end of file diff --git a/documentation/behaviour-option/invert-connects.js b/documentation/behaviour-option/invert-connects.js new file mode 100644 index 0000000..236687b --- /dev/null +++ b/documentation/behaviour-option/invert-connects.js @@ -0,0 +1,12 @@ +var invertConnectsSlider = document.getElementById('invert-connects'); + +noUiSlider.create(invertConnectsSlider, { + start: [20*60, 8*60], + behaviour: 'unconstrained-invert-connects', + step: 15, + connect: true, + range: { + 'min': 0, + 'max': 24*60 + } +}); \ No newline at end of file diff --git a/src/nouislider.ts b/src/nouislider.ts index a59b868..5978522 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; @@ -1161,6 +1163,7 @@ function testBehaviour(parsed: ParsedOptions, entry: unknown): void { snap: snap, hover: hover, unconstrained: unconstrained, + invertConnects: invertConnects, }; } @@ -1367,6 +1370,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 +1383,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: boolean = false; // Document Nodes const scope_Document = target.ownerDocument; @@ -1452,12 +1457,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 +1471,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 +2671,27 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O (scope_Handles[handleNumber].style as CSSStyleDeclarationIE10)[options.transformRule] = translateRule; + if( + options.events.invertConnects && + // sanity check for at least 2 handles + scope_Locations.length > 1 && + // check if handles passed each other, but don't match the ConnectsInverted state + scope_ConnectsInverted !== !scope_Locations.every( + (position: number, index: number, locations: number[]): boolean => + index === 0 || position >= locations[index - 1] + )) { + // when invertConnects is set, automatically invert connects when handles pass each other + invertConnects(); + } + 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 +2743,23 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O return; } + // Create a copy of locations, so we can sort them for the local scope logic + let 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 +3000,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 +3045,33 @@ 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) { + // 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); + } + + // readding 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; + updateOptions({ + // inverse the connect boolean array + connect: options.connect.map((b: boolean) => !b) + }, false); // don't fire the set event } // Initialization steps From 3406bc1075f969626ce6e99d6ad381054b999a52 Mon Sep 17 00:00:00 2001 From: lgersen Date: Mon, 10 Jun 2024 20:55:12 +0200 Subject: [PATCH 2/2] Bypass full updateOptions, fix eslint, check for two handles --- documentation/behaviour-option.php | 6 +- .../behaviour-option/invert-connects-link.js | 2 +- .../behaviour-option/invert-connects.js | 2 +- documentation/more.php | 2 +- src/nouislider.ts | 71 +++++++++++-------- 5 files changed, 49 insertions(+), 34 deletions(-) diff --git a/documentation/behaviour-option.php b/documentation/behaviour-option.php index c9df959..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".

    @@ -273,7 +273,7 @@

    With this option set, connects invert when handles pass each other.

    -

    Requires the unconstrained behaviour and the connect option.

    +

    Requires the unconstrained behaviour and the connect option. This option is only applicable for sliders with two handles.

    diff --git a/documentation/behaviour-option/invert-connects-link.js b/documentation/behaviour-option/invert-connects-link.js index 26cd2dd..f9453f7 100644 --- a/documentation/behaviour-option/invert-connects-link.js +++ b/documentation/behaviour-option/invert-connects-link.js @@ -6,4 +6,4 @@ invertConnectsSlider.noUiSlider.on('update', function (values) { return [mins / 60 ^ 0, mins % 60].map(pad).join(':'); }; invertConnectsValues.innerHTML = values.map(minToHHMM).join(' - '); -}); \ No newline at end of file +}); diff --git a/documentation/behaviour-option/invert-connects.js b/documentation/behaviour-option/invert-connects.js index 236687b..52d2a2a 100644 --- a/documentation/behaviour-option/invert-connects.js +++ b/documentation/behaviour-option/invert-connects.js @@ -9,4 +9,4 @@ noUiSlider.create(invertConnectsSlider, { 'min': 0, 'max': 24*60 } -}); \ No newline at end of file +}); diff --git a/documentation/more.php b/documentation/more.php index 1cea5eb..b15d106 100644 --- a/documentation/more.php +++ b/documentation/more.php @@ -75,7 +75,7 @@
    -

    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 5978522..e7ee831 100644 --- a/src/nouislider.ts +++ b/src/nouislider.ts @@ -1150,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"); } @@ -1383,7 +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: boolean = false; + let scope_ConnectsInverted = false; // Document Nodes const scope_Document = target.ownerDocument; @@ -2671,23 +2675,27 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O (scope_Handles[handleNumber].style as CSSStyleDeclarationIE10)[options.transformRule] = translateRule; - if( - options.events.invertConnects && - // sanity check for at least 2 handles - scope_Locations.length > 1 && + // 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 - scope_ConnectsInverted !== !scope_Locations.every( - (position: number, index: number, locations: number[]): boolean => + const handlesAreInOrder = scope_Locations.every( + (position: number, index: number, locations: number[]): boolean => index === 0 || position >= locations[index - 1] - )) { - // when invertConnects is set, automatically invert connects when handles pass each other + ); + + 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) { + + if (scope_ConnectsInverted) { // When connects are inverted, we also have to update adjacent connects updateConnect(handleNumber - 1); updateConnect(handleNumber + 2); @@ -2744,9 +2752,10 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O } // Create a copy of locations, so we can sort them for the local scope logic - let locations = scope_Locations.slice(); + const locations = scope_Locations.slice(); + if (scope_ConnectsInverted) { - locations.sort(function(a, b) { + locations.sort(function (a, b) { return a - b; }); } @@ -3048,30 +3057,36 @@ function scope(target: TargetElement, options: ParsedOptions, originalOptions: O // Update connects only if it was set if (optionsToUpdate.connect) { - // IE supported way of removing children including event handlers - while (scope_ConnectBase.firstChild) { - scope_ConnectBase.removeChild(scope_ConnectBase.firstChild); - } + updateConnectOption(); + } + } - // 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); - } + function updateConnectOption() { + // IE supported way of removing children including event handlers + while (scope_ConnectBase.firstChild) { + scope_ConnectBase.removeChild(scope_ConnectBase.firstChild); + } - // readding 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); + // 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; - updateOptions({ + testConnect( + options, // inverse the connect boolean array - connect: options.connect.map((b: boolean) => !b) - }, false); // don't fire the set event + options.connect.map((b: boolean) => !b) + ); + updateConnectOption(); } // Initialization steps