diff --git a/docs/src/examples/QRange/MaximumRange.vue b/docs/src/examples/QRange/MaximumRange.vue new file mode 100644 index 00000000000..9d4b49894a3 --- /dev/null +++ b/docs/src/examples/QRange/MaximumRange.vue @@ -0,0 +1,37 @@ + + + diff --git a/docs/src/examples/QRange/MinimumRange.vue b/docs/src/examples/QRange/MinimumRange.vue new file mode 100644 index 00000000000..b360f51c16f --- /dev/null +++ b/docs/src/examples/QRange/MinimumRange.vue @@ -0,0 +1,37 @@ + + + diff --git a/docs/src/pages/vue-components/range.md b/docs/src/pages/vue-components/range.md index 03cead1e74c..0af1d281632 100644 --- a/docs/src/pages/vue-components/range.md +++ b/docs/src/pages/vue-components/range.md @@ -93,6 +93,12 @@ Use the `drag-range` or `drag-only-range` props to allow the user to move the se +### Range limits + + + + + ### Lazy input diff --git a/ui/src/components/range/QRange.js b/ui/src/components/range/QRange.js index b918783055f..49fd42331a8 100644 --- a/ui/src/components/range/QRange.js +++ b/ui/src/components/range/QRange.js @@ -28,6 +28,17 @@ export default createComponent({ validator: v => 'min' in v && 'max' in v }, + minRange: { + type: Number, + default: 0, + validator: v => v >= 0 + }, + maxRange: { + type: Number, + default: null, + validator: v => v === null || v >= 0 + }, + dragRange: Boolean, dragOnlyRange: Boolean, @@ -63,13 +74,54 @@ export default createComponent({ const model = ref({ min: 0, max: 0 }) function normalizeModel () { - model.value.min = props.modelValue.min === null + let min = props.modelValue.min === null ? state.innerMin.value : between(props.modelValue.min, state.innerMin.value, state.innerMax.value) - model.value.max = props.modelValue.max === null + let max = props.modelValue.max === null ? state.innerMax.value : between(props.modelValue.max, state.innerMin.value, state.innerMax.value) + + // Calculate effective constraints (handle edge cases where constraints don't fit) + const sliderRange = state.innerMax.value - state.innerMin.value + const effectiveMinRange = Math.min(props.minRange, sliderRange) + const effectiveMaxRange = props.maxRange !== null + ? Math.min(props.maxRange, sliderRange) + : null + + // Apply minRange constraint - if range is too narrow, expand it + const currentRange = max - min + if (currentRange < effectiveMinRange) { + const deficit = effectiveMinRange - currentRange + + // Try to expand max first + const maxExpansion = Math.min(deficit, state.innerMax.value - max) + max += maxExpansion + + // If still need more, expand min downward + const remainingDeficit = effectiveMinRange - (max - min) + if (remainingDeficit > 0) { + min = Math.max(state.innerMin.value, max - effectiveMinRange) + } + } + + // Apply maxRange constraint - if range is too wide, shrink it + if (effectiveMaxRange !== null && currentRange > effectiveMaxRange) { + const excess = currentRange - effectiveMaxRange + + // Try to shrink max first (move it down) + const maxShrink = Math.min(excess, max - state.innerMin.value - effectiveMaxRange) + max -= maxShrink + + // If still need more shrinking, move min up + const remainingExcess = (max - min) - effectiveMaxRange + if (remainingExcess > 0) { + min = Math.min(state.innerMax.value - effectiveMaxRange, max - effectiveMaxRange) + } + } + + model.value.min = min + model.value.max = max } watch( @@ -242,23 +294,61 @@ export default createComponent({ const ratio = methods.getDraggingRatio(event, dragging) const localModel = methods.convertRatioToModel(ratio) + const sliderRange = state.innerMax.value - state.innerMin.value + const effectiveMinRange = Math.min(props.minRange, sliderRange) + const effectiveMaxRange = props.maxRange !== null + ? Math.min(props.maxRange, sliderRange) + : null + switch (dragging.type) { case dragType.MIN: if (ratio <= dragging.ratioMax) { + // Moving min thumb towards left + const maxAllowedMin = dragging.valueMax - effectiveMinRange + const minAllowedMin = effectiveMaxRange !== null + ? Math.max(dragging.valueMax - effectiveMaxRange, state.innerMin.value) + : state.innerMin.value + + let constrainedMin = between(localModel, minAllowedMin, maxAllowedMin) + + // Ensure we don't go below slider min + constrainedMin = Math.max(constrainedMin, state.innerMin.value) + + // If this would push max beyond slider max, adjust min + if (constrainedMin + effectiveMinRange > state.innerMax.value) { + constrainedMin = state.innerMax.value - effectiveMinRange + } + pos = { - minR: ratio, + minR: methods.convertModelToRatio(constrainedMin), maxR: dragging.ratioMax, - min: localModel, + min: constrainedMin, max: dragging.valueMax } state.focus.value = 'min' } else { + // Thumb crossed over + const minAllowedMax = dragging.valueMax + effectiveMinRange + const maxAllowedMax = effectiveMaxRange !== null + ? Math.min(dragging.valueMax + effectiveMaxRange, state.innerMax.value) + : state.innerMax.value + + let constrainedMax = between(localModel, minAllowedMax, maxAllowedMax) + + // Ensure we don't go above slider max + constrainedMax = Math.min(constrainedMax, state.innerMax.value) + + // If this would push min below slider min, adjust max + if (constrainedMax - effectiveMinRange < state.innerMin.value) { + constrainedMax = state.innerMin.value + effectiveMinRange + } + pos = { minR: dragging.ratioMax, - maxR: ratio, + maxR: methods.convertModelToRatio(constrainedMax), min: dragging.valueMax, - max: localModel + max: constrainedMax } state.focus.value = 'max' } @@ -266,19 +356,51 @@ export default createComponent({ case dragType.MAX: if (ratio >= dragging.ratioMin) { + // Moving max thumb towards right + const minAllowedMax = dragging.valueMin + effectiveMinRange + const maxAllowedMax = effectiveMaxRange !== null + ? Math.min(dragging.valueMin + effectiveMaxRange, state.innerMax.value) + : state.innerMax.value + + let constrainedMax = between(localModel, minAllowedMax, maxAllowedMax) + + // Ensure we don't go above slider max + constrainedMax = Math.min(constrainedMax, state.innerMax.value) + + // If this would push min below slider min, adjust max + if (constrainedMax - effectiveMinRange < state.innerMin.value) { + constrainedMax = state.innerMin.value + effectiveMinRange + } + pos = { minR: dragging.ratioMin, - maxR: ratio, + maxR: methods.convertModelToRatio(constrainedMax), min: dragging.valueMin, - max: localModel + max: constrainedMax } state.focus.value = 'max' } else { + // Thumb crossed over + const maxAllowedMin = dragging.valueMin - effectiveMinRange + const minAllowedMin = effectiveMaxRange !== null + ? Math.max(dragging.valueMin - effectiveMaxRange, state.innerMin.value) + : state.innerMin.value + + let constrainedMin = between(localModel, minAllowedMin, maxAllowedMin) + + // Ensure we don't go below slider min + constrainedMin = Math.max(constrainedMin, state.innerMin.value) + + // If this would push max above slider max, adjust min + if (constrainedMin + effectiveMinRange > state.innerMax.value) { + constrainedMin = state.innerMax.value - effectiveMinRange + } + pos = { - minR: ratio, + minR: methods.convertModelToRatio(constrainedMin), maxR: dragging.ratioMin, - min: localModel, + min: constrainedMin, max: dragging.valueMin } state.focus.value = 'min' @@ -349,14 +471,52 @@ export default createComponent({ } else { const which = state.focus.value + const proposedValue = state.roundValueFn.value(model.value[ which ] + offset) + + const sliderRange = state.innerMax.value - state.innerMin.value + const effectiveMinRange = Math.min(props.minRange, sliderRange) + const effectiveMaxRange = props.maxRange !== null + ? Math.min(props.maxRange, sliderRange) + : null + + let constrainedValue + if (which === 'min') { + // Moving min thumb - ensure range stays between minRange and maxRange + let maxAllowed = model.value.max - effectiveMinRange + let minAllowed = effectiveMaxRange !== null + ? model.value.max - effectiveMaxRange + : state.innerMin.value + + // Ensure bounds don't go outside slider range + minAllowed = Math.max(minAllowed, state.innerMin.value) + + // If minRange can't fit, adjust maxAllowed + if (maxAllowed < state.innerMin.value) { + maxAllowed = state.innerMin.value + } + + constrainedValue = between(proposedValue, minAllowed, maxAllowed) + } else { + // Moving max thumb - ensure range stays between minRange and maxRange + let minAllowed = model.value.min + effectiveMinRange + let maxAllowed = effectiveMaxRange !== null + ? model.value.min + effectiveMaxRange + : state.innerMax.value + + // Ensure bounds don't go outside slider range + maxAllowed = Math.min(maxAllowed, state.innerMax.value) + + // If minRange can't fit, adjust minAllowed + if (minAllowed > state.innerMax.value) { + minAllowed = state.innerMax.value + } + + constrainedValue = between(proposedValue, minAllowed, maxAllowed) + } model.value = { ...model.value, - [ which ]: between( - state.roundValueFn.value(model.value[ which ] + offset), - which === 'min' ? state.innerMin.value : model.value.min, - which === 'max' ? state.innerMax.value : model.value.max - ) + [ which ]: constrainedValue } } diff --git a/ui/src/components/range/QRange.json b/ui/src/components/range/QRange.json index 6a7165de00e..e9b4d7ccba0 100644 --- a/ui/src/components/range/QRange.json +++ b/ui/src/components/range/QRange.json @@ -24,6 +24,22 @@ "examples": [ "# v-model=\"positionModel\"" ] }, + "min-range": { + "type": "Number", + "default": "0", + "desc": "Minimum allowed difference between the max and min values", + "category": "model", + "addedIn": "v2.18.7" + }, + + "max-range": { + "type": [ "Number", "null" ], + "default": "null", + "desc": "Maximum allowed difference between the max and min values", + "category": "model", + "addedIn": "v2.18.7" + }, + "drag-range": { "type": "Boolean", "desc": "User can drag range instead of just the two thumbs",