Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/src/examples/QRange/MaximumRange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="q-pa-lg">
<q-badge color="secondary" class="q-mb-lg">
Model: {{ model.min }} to {{ model.max }} (0 to 10 w/ max-range 4 or 5)
</q-badge>

<q-range
v-model="model"
markers
:min="0"
:max="10"
:max-range="4"
/>

<q-range
v-model="model"
markers
:min="0"
:max="10"
:max-range="5"
/>
</div>
</template>

<script>
import { ref } from 'vue'

export default {
setup () {
return {
model: ref({
min: 3, max: 8
})
}
}
}
</script>
37 changes: 37 additions & 0 deletions docs/src/examples/QRange/MinimumRange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="q-pa-lg">
<q-badge color="secondary" class="q-mb-lg">
Model: {{ model.min }} to {{ model.max }} (0 to 10 w/ min-range 2 or 3)
</q-badge>

<q-range
v-model="model"
markers
:min="0"
:max="10"
:min-range="2"
/>

<q-range
v-model="model"
markers
:min="0"
:max="10"
:min-range="3"
/>
</div>
</template>

<script>
import { ref } from 'vue'

export default {
setup () {
return {
model: ref({
min: 5, max: 7
})
}
}
}
</script>
6 changes: 6 additions & 0 deletions docs/src/pages/vue-components/range.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ Use the `drag-range` or `drag-only-range` props to allow the user to move the se

<DocExample title="Drag only range (fixed interval)" file="DragOnly" />

### Range limits <q-badge label="v2.18.7+" />

<DocExample title="Minimum range" file="MinimumRange" />

<DocExample title="Maximum range" file="MaximumRange" />

### Lazy input

<DocExample title="Lazy input" file="Lazy" />
Expand Down
190 changes: 175 additions & 15 deletions ui/src/components/range/QRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -242,43 +294,113 @@ 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'
}
break

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'
Expand Down Expand Up @@ -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
}
}

Expand Down
16 changes: 16 additions & 0 deletions ui/src/components/range/QRange.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading