diff --git a/docs/src/examples/QRange/CustomMarkers.vue b/docs/src/examples/QRange/CustomMarkers.vue
new file mode 100644
index 00000000000..00445e88583
--- /dev/null
+++ b/docs/src/examples/QRange/CustomMarkers.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ Model: {{ model.min }} to {{ model.max }}
+
+
+
+
+
+ {{ min }}
+
+ {{ max }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/examples/QRange/InnerMinMax.vue b/docs/src/examples/QRange/InnerMinMax.vue
new file mode 100644
index 00000000000..1bc72838b5d
--- /dev/null
+++ b/docs/src/examples/QRange/InnerMinMax.vue
@@ -0,0 +1,23 @@
+
+
+
+ Model: {{ model.min }} to {{ model.max }} (0 to 50 w/ selection 10 to 35 or 15 to 40)
+
+
+
+
+
+
+
+
diff --git a/docs/src/examples/QSlider/CustomMarkerIcons.vue b/docs/src/examples/QSlider/CustomMarkerIcons.vue
new file mode 100644
index 00000000000..2271f8e0029
--- /dev/null
+++ b/docs/src/examples/QSlider/CustomMarkerIcons.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+ {{ model }}
+
+
+
+
+
+ editable === true && setFn(0)"
+ />
+
+
+ editable === true && setFn(val - 1)"
+ />
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/examples/QSlider/CustomMarkerLabels.vue b/docs/src/examples/QSlider/CustomMarkerLabels.vue
new file mode 100644
index 00000000000..8d7699dbc20
--- /dev/null
+++ b/docs/src/examples/QSlider/CustomMarkerLabels.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+ {{ model }}
+
+
+
+
+ editable === true && setFn(10 * val)"
+ >{{ val }}
+
+ editable === true && setFn(val)"
+ />
+
+
+
+
+
+
+
+
diff --git a/docs/src/examples/QSlider/CustomMarkerValue.vue b/docs/src/examples/QSlider/CustomMarkerValue.vue
new file mode 100644
index 00000000000..6ba1e6ebc2b
--- /dev/null
+++ b/docs/src/examples/QSlider/CustomMarkerValue.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+ {{ model }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/examples/QSlider/InnerMinMax.vue b/docs/src/examples/QSlider/InnerMinMax.vue
new file mode 100644
index 00000000000..d9dc983e730
--- /dev/null
+++ b/docs/src/examples/QSlider/InnerMinMax.vue
@@ -0,0 +1,20 @@
+
+
+
+ Model: {{ value }} (0 to 50 w/ selection 10 to 35 or 15 to 40)
+
+
+
+
+
+
+
+
diff --git a/docs/src/pages/vue-components/range.md b/docs/src/pages/vue-components/range.md
index 0bdedacf4f1..783f6b1facb 100644
--- a/docs/src/pages/vue-components/range.md
+++ b/docs/src/pages/vue-components/range.md
@@ -24,6 +24,12 @@ Notice we are using an object for the selection, which holds values for both the
+### With inner min/max
+
+Sometimes you need to restrict the model value to an interval inside of the track's length. For this purpose, use `inner-min` and `inner-max` props. First prop needs to be higher or equal to `min` prop while the latter needs to be lower or equal to the `max` prop.
+
+
+
### With step
@@ -52,6 +58,10 @@ The example below is better highlighting how QRange handles label positioning so
+### Custom markers slot
+
+
+
### Dragging range
Use the `drag-range` or `drag-only-range` props to allow the user to move the selected range or only a predetermined range as a whole.
diff --git a/docs/src/pages/vue-components/slider.md b/docs/src/pages/vue-components/slider.md
index d0cb6d8a957..29fb541fb5a 100644
--- a/docs/src/pages/vue-components/slider.md
+++ b/docs/src/pages/vue-components/slider.md
@@ -22,6 +22,12 @@ Also check its “sibling”, the [QRange](/vue-components/range) component.
+### With inner min/max
+
+Sometimes you need to restrict the model value to an interval inside of the track's length. For this purpose, use `inner-min` and `inner-max` props. First prop needs to be higher or equal to `min` prop while the latter needs to be lower or equal to the `max` prop.
+
+
+
### With step
@@ -50,6 +56,14 @@ The example below is better highlighting how QSlider handles label positioning s
+### Custom markers slot
+
+
+
+
+
+
+
### Lazy input
diff --git a/ui/dev/src/pages/form/range.vue b/ui/dev/src/pages/form/range.vue
index 3b3f29ac198..a4f4b71311f 100644
--- a/ui/dev/src/pages/form/range.vue
+++ b/ui/dev/src/pages/form/range.vue
@@ -24,6 +24,16 @@
+
+ Inner min/max
+
+ Model {{ innerMinMax.min }} to {{ innerMinMax.max }} (0 to 50, inner 10 to 35 or 15 to 40)
+
+
+
+
+
+
Reverse
@@ -246,6 +256,15 @@ export default {
max: 35
},
+ nullInnerMinMax: {
+ min: null,
+ max: null
+ },
+ innerMinMax: {
+ min: 20,
+ max: 25
+ },
+
stepZero: {
min: 34.05,
max: 64.023
diff --git a/ui/dev/src/pages/form/slider-markers.vue b/ui/dev/src/pages/form/slider-markers.vue
new file mode 100644
index 00000000000..6d04a13dc64
--- /dev/null
+++ b/ui/dev/src/pages/form/slider-markers.vue
@@ -0,0 +1,196 @@
+
+
+
+ model1: {{ model1 }}
+ model2: {{ model2 }}
+ model3: {{ model3 }}
+
+
+
+
+
+
+
+
+ editable === true && setFn(10 * val)"
+ >{{ val }}
+
+ editable === true && setFn(val)"
+ />
+
+
+
+
+
+
+ editable === true && setFn(0)"
+ />
+
+
+ editable === true && setFn(val - 1)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/dev/src/pages/form/slider-range-limits.vue b/ui/dev/src/pages/form/slider-range-limits.vue
new file mode 100644
index 00000000000..3388fe04a3a
--- /dev/null
+++ b/ui/dev/src/pages/form/slider-range-limits.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+ ModelS: {{ modelS }} ({{ innerMin }} to {{ innerMax }}) / ({{ min }} to {{ max }})
+
+
+
+ ModelR: {{ modelR }} ({{ innerMin }} to {{ innerMax }}) / ({{ min }} to {{ max }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/dev/src/pages/form/slider.vue b/ui/dev/src/pages/form/slider.vue
index 44dd33071f5..ccfb7584ef0 100644
--- a/ui/dev/src/pages/form/slider.vue
+++ b/ui/dev/src/pages/form/slider.vue
@@ -23,6 +23,17 @@
+
+ Inner min/max
+
+ Model {{ innerMinMax }} (0 to 50, inner 10 to 35 or 15 to 40)
+
+
+
+
+
+
+
Reverse
@@ -189,6 +200,8 @@ export default {
nullValue: null,
nullValueMin: null,
standalone: 20,
+ nullInnerMinMax: null,
+ innerMinMax: 25,
stepZero: 30.05,
precision: 0.4,
step: 30,
diff --git a/ui/src/components/range/QRange.js b/ui/src/components/range/QRange.js
index 4c6ef2130c1..b61018b0a40 100644
--- a/ui/src/components/range/QRange.js
+++ b/ui/src/components/range/QRange.js
@@ -7,8 +7,8 @@ import {
keyCodes
} from '../slider/slider-utils.js'
-import { stopAndPrevent } from '../../utils/event.js'
import { between } from '../../utils/format.js'
+import { stopAndPrevent } from '../../utils/event.js'
const dragType = {
MIN: 0,
@@ -33,8 +33,6 @@ export default Vue.extend({
}
},
- name: String,
-
dragRange: Boolean,
dragOnlyRange: Boolean,
@@ -48,11 +46,17 @@ export default Vue.extend({
},
data () {
+ const minModel = isNaN(this.innerMin) === true || this.innerMin < this.min
+ ? this.min
+ : this.innerMin
+ const maxModel = isNaN(this.innerMax) === true || this.innerMax > this.max
+ ? this.max
+ : this.innerMax
+ const min = this.value.min === null ? minModel : this.value.min
+ const max = this.value.max === null ? maxModel : this.value.max
+
return {
- model: {
- min: this.value.min === null ? this.min : this.value.min,
- max: this.value.max === null ? this.max : this.value.max
- },
+ model: { min, max },
curMinRatio: 0,
curMaxRatio: 0
}
@@ -60,70 +64,84 @@ export default Vue.extend({
watch: {
'value.min' (val) {
- this.model.min = val === null
+ const model = val === null
? this.min
- : val
+ : between(val, this.minInnerValue, this.maxInnerValue)
+
+ if (this.model.min !== model) {
+ this.model.min = model
+
+ this.curMinRatio = this.__getModelRatio(model)
+ }
},
'value.max' (val) {
- this.model.max = val === null
+ const model = val === null
? this.max
- : val
+ : between(val, this.minInnerValue, this.maxInnerValue)
+
+ if (this.model.max !== model) {
+ this.model.max = model
+
+ this.curMaxRatio = this.__getModelRatio(model)
+ }
},
- min (value) {
- if (this.model.min < value) {
- this.model.min = value
+ minInnerValue (val) {
+ if (this.model.min < val) {
+ this.model.min = val
}
- if (this.model.max < value) {
- this.model.max = value
+ if (this.model.max < val) {
+ this.model.max = val
}
},
- max (value) {
- if (this.model.min > value) {
- this.model.min = value
+ maxInnerValue (val) {
+ if (this.model.min > val) {
+ this.model.min = val
}
- if (this.model.max > value) {
- this.model.max = value
+ if (this.model.max > val) {
+ this.model.max = val
}
}
},
computed: {
- ratioMin () {
+ minRatio () {
return this.active === true ? this.curMinRatio : this.modelMinRatio
},
- ratioMax () {
+ maxRatio () {
return this.active === true ? this.curMaxRatio : this.modelMaxRatio
},
modelMinRatio () {
- return this.minMaxDiff === 0 ? 0 : (this.model.min - this.min) / this.minMaxDiff
+ return this.__getModelRatio(this.model.min)
},
modelMaxRatio () {
- return this.minMaxDiff === 0 ? 0 : (this.model.max - this.min) / this.minMaxDiff
+ return this.__getModelRatio(this.model.max)
},
trackStyle () {
+ const minRatio = between(this.minRatio, 0, 1)
+
return {
- [ this.positionProp ]: `${100 * this.ratioMin}%`,
- [ this.sizeProp ]: `${100 * (this.ratioMax - this.ratioMin)}%`
+ [ this.positionProp ]: `${100 * minRatio}%`,
+ [ this.sizeProp ]: `${100 * (between(this.maxRatio, 0, 1) - minRatio)}%`
}
},
minThumbStyle () {
return {
- [ this.positionProp ]: `${100 * this.ratioMin}%`,
+ [ this.positionProp ]: `${100 * this.minRatio}%`,
'z-index': this.__nextFocus === 'min' ? 2 : void 0
}
},
maxThumbStyle () {
return {
- [ this.positionProp ]: `${100 * this.ratioMax}%`
+ [ this.positionProp ]: `${100 * this.maxRatio}%`
}
},
@@ -139,47 +157,6 @@ export default Vue.extend({
}
},
- events () {
- if (this.editable === true) {
- if (this.$q.platform.is.mobile === true) {
- return { click: this.__mobileClick }
- }
-
- const evt = { mousedown: this.__activate }
-
- this.dragOnlyRange === true && Object.assign(evt, {
- focus: () => { this.__focus('both') },
- blur: this.__blur,
- keydown: this.__keydown,
- keyup: this.__keyup
- })
-
- return evt
- }
- },
-
- minEvents () {
- if (this.editable === true && this.$q.platform.is.mobile !== true && this.dragOnlyRange !== true) {
- return {
- focus: () => { this.__focus('min') },
- blur: this.__blur,
- keydown: this.__keydown,
- keyup: this.__keyup
- }
- }
- },
-
- maxEvents () {
- if (this.editable === true && this.$q.platform.is.mobile !== true && this.dragOnlyRange !== true) {
- return {
- focus: () => { this.__focus('max') },
- blur: this.__blur,
- keydown: this.__keydown,
- keyup: this.__keyup
- }
- }
- },
-
minPinClass () {
const color = this.leftLabelColor || this.labelColor
if (color) {
@@ -208,26 +185,67 @@ export default Vue.extend({
}
},
- minLabel () {
+ minPinStyle () {
+ const percent = (this.reverse === true ? -this.minRatio : this.minRatio - 1)
+ return this.__getPinStyle(percent, this.minRatio)
+ },
+
+ maxPinStyle () {
+ const percent = (this.reverse === true ? -this.maxRatio : this.maxRatio - 1)
+ return this.__getPinStyle(percent, this.maxRatio)
+ },
+
+ minComputedLabel () {
return this.leftLabelValue !== void 0
? this.leftLabelValue
: this.model.min
},
- maxLabel () {
+ maxComputedLabel () {
return this.rightLabelValue !== void 0
? this.rightLabelValue
: this.model.max
},
- minPinStyle () {
- const percent = (this.reverse === true ? -this.ratioMin : this.ratioMin - 1)
- return this.__getPinStyle(percent, this.ratioMin)
+ events () {
+ if (this.editable === true) {
+ if (this.$q.platform.is.mobile === true) {
+ return { click: this.__mobileClick }
+ }
+
+ const evt = { mousedown: this.__activate }
+
+ this.dragOnlyRange === true && Object.assign(evt, {
+ focus: () => { this.__focus('both') },
+ blur: this.__blur,
+ keydown: this.__keydown,
+ keyup: this.__keyup
+ })
+
+ return evt
+ }
},
- maxPinStyle () {
- const percent = (this.reverse === true ? -this.ratioMax : this.ratioMax - 1)
- return this.__getPinStyle(percent, this.ratioMax)
+ minEvents () {
+ if (this.editable === true && this.$q.platform.is.mobile !== true && this.dragOnlyRange !== true) {
+ return {
+ focus: () => { this.__focus('min') },
+ blur: this.__blur,
+ keydown: this.__keydown,
+ keyup: this.__keyup
+ }
+ }
+ },
+
+ maxEvents () {
+ if (this.editable === true && this.$q.platform.is.mobile !== true && this.dragOnlyRange !== true) {
+ return {
+ focus: () => { this.__focus('max') },
+ blur: this.__blur,
+ keydown: this.__keydown,
+ keyup: this.__keyup
+ }
+ }
},
formAttrs () {
@@ -250,42 +268,43 @@ export default Vue.extend({
__getDragging (event) {
const
{ left, top, width, height } = this.$el.getBoundingClientRect(),
- sensitivity = this.dragOnlyRange === true
+ thumb = this.$refs.minThumb || this.$refs.maxThumb,
+ sensitivity = this.dragOnlyRange === true || thumb === void 0
? 0
: (this.vertical === true
- ? this.$refs.minThumb.offsetHeight / (2 * height)
- : this.$refs.minThumb.offsetWidth / (2 * width)
- )
+ ? thumb.offsetHeight / (2 * height)
+ : thumb.offsetWidth / (2 * width)
+ ) + (this.modelMaxRatio - this.modelMinRatio) / 15
const dragging = {
left,
top,
width,
height,
- valueMin: this.model.min,
- valueMax: this.model.max,
- ratioMin: this.modelMinRatio,
- ratioMax: this.modelMaxRatio
+ minValue: this.model.min,
+ maxValue: this.model.max,
+ minRatio: this.modelMinRatio,
+ maxRatio: this.modelMaxRatio
}
const ratio = getRatio(event, dragging, this.isReversed, this.vertical)
let type
- if (this.dragOnlyRange !== true && ratio < dragging.ratioMin + sensitivity) {
+ if (this.dragOnlyRange !== true && ratio < dragging.minRatio + sensitivity) {
type = dragType.MIN
}
- else if (this.dragOnlyRange === true || ratio < dragging.ratioMax - sensitivity) {
+ else if (this.dragOnlyRange === true || ratio < dragging.maxRatio - sensitivity) {
if (this.dragRange === true || this.dragOnlyRange === true) {
type = dragType.RANGE
Object.assign(dragging, {
offsetRatio: ratio,
offsetModel: getModel(ratio, this.min, this.max, this.step, this.decimals),
- rangeValue: dragging.valueMax - dragging.valueMin,
- rangeRatio: dragging.ratioMax - dragging.ratioMin
+ rangeValue: dragging.maxValue - dragging.minValue,
+ rangeRatio: dragging.maxRatio - dragging.minRatio
})
}
else {
- type = dragging.ratioMax - ratio < ratio - dragging.ratioMin
+ type = dragging.maxRatio - ratio < ratio - dragging.minRatio
? dragType.MAX
: dragType.MIN
}
@@ -308,20 +327,20 @@ export default Vue.extend({
switch (dragging.type) {
case dragType.MIN:
- if (ratio <= dragging.ratioMax) {
+ if (ratio <= dragging.maxRatio) {
pos = {
minR: ratio,
- maxR: dragging.ratioMax,
+ maxR: dragging.maxRatio,
min: model,
- max: dragging.valueMax
+ max: dragging.maxValue
}
this.__nextFocus = 'min'
}
else {
pos = {
- minR: dragging.ratioMax,
+ minR: dragging.maxRatio,
maxR: ratio,
- min: dragging.valueMax,
+ min: dragging.maxValue,
max: model
}
this.__nextFocus = 'max'
@@ -329,11 +348,11 @@ export default Vue.extend({
break
case dragType.MAX:
- if (ratio >= dragging.ratioMin) {
+ if (ratio >= dragging.minRatio) {
pos = {
- minR: dragging.ratioMin,
+ minR: dragging.minRatio,
maxR: ratio,
- min: dragging.valueMin,
+ min: dragging.minValue,
max: model
}
this.__nextFocus = 'max'
@@ -341,9 +360,9 @@ export default Vue.extend({
else {
pos = {
minR: ratio,
- maxR: dragging.ratioMin,
+ maxR: dragging.minRatio,
min: model,
- max: dragging.valueMin
+ max: dragging.minValue
}
this.__nextFocus = 'min'
}
@@ -352,9 +371,9 @@ export default Vue.extend({
case dragType.RANGE:
const
ratioDelta = ratio - dragging.offsetRatio,
- minR = between(dragging.ratioMin + ratioDelta, 0, 1 - dragging.rangeRatio),
+ minR = between(dragging.minRatio + ratioDelta, this.minInnerRatio, this.maxInnerRatio - dragging.rangeRatio),
modelDelta = model - dragging.offsetModel,
- min = between(dragging.valueMin + modelDelta, this.min, this.max - dragging.rangeValue)
+ min = between(dragging.minValue + modelDelta, this.minInnerValue, this.maxInnerValue - dragging.rangeValue)
pos = {
minR,
@@ -366,23 +385,31 @@ export default Vue.extend({
}
this.model = {
- min: pos.min,
- max: pos.max
+ min: this.__nextFocus !== 'max'
+ ? between(pos.min, this.minInnerValue, this.maxInnerValue)
+ : pos.min,
+ max: this.__nextFocus !== 'min'
+ ? between(pos.max, this.minInnerValue, this.maxInnerValue)
+ : pos.max
}
// If either of the values to be emitted are null, set them to the defaults the user has entered.
if (this.model.min === null || this.model.max === null) {
- this.model.min = pos.min || this.min
- this.model.max = pos.max || this.max
+ this.model.min = pos.min || this.minInnerValue
+ this.model.max = pos.max || this.maxInnerValue
}
if (this.snap !== true || this.step === 0) {
- this.curMinRatio = pos.minR
- this.curMaxRatio = pos.maxR
+ this.curMinRatio = this.__nextFocus !== 'max'
+ ? between(pos.minR, this.minInnerRatio, this.maxInnerRatio)
+ : pos.minR
+ this.curMaxRatio = this.__nextFocus !== 'min'
+ ? between(pos.maxR, this.minInnerRatio, this.maxInnerRatio)
+ : pos.maxR
}
else {
- this.curMinRatio = this.minMaxDiff === 0 ? 0 : (this.model.min - this.min) / this.minMaxDiff
- this.curMaxRatio = this.minMaxDiff === 0 ? 0 : (this.model.max - this.min) / this.minMaxDiff
+ this.curMinRatio = this.__getModelRatio(this.model.min)
+ this.curMaxRatio = this.__getModelRatio(this.model.max)
}
},
@@ -408,8 +435,8 @@ export default Vue.extend({
const min = between(
parseFloat((this.model.min + offset).toFixed(this.decimals)),
- this.min,
- this.max - interval
+ this.minInnerValue,
+ this.maxInnerValue - interval
)
this.model = {
@@ -427,8 +454,8 @@ export default Vue.extend({
...this.model,
[which]: between(
parseFloat((this.model[which] + offset).toFixed(this.decimals)),
- which === 'min' ? this.min : this.model.min,
- which === 'max' ? this.max : this.model.max
+ which === 'min' ? this.minInnerValue : this.model.min,
+ which === 'max' ? this.maxInnerValue : this.model.max
)
}
}
@@ -437,6 +464,11 @@ export default Vue.extend({
},
__getThumb (h, which) {
+ const ratio = this[which + 'Ratio']
+ if (ratio < 0 || ratio > 1) {
+ return
+ }
+
const child = [
this.__getThumbSvg(h),
h('div', { staticClass: 'q-slider__focus-ring' })
@@ -457,7 +489,7 @@ export default Vue.extend({
staticClass: 'q-slider__pin-text',
class: this[which + 'PinTextClass']
}, [
- this[which + 'Label']
+ this[which + 'ComputedLabel']
])
])
]),
@@ -481,24 +513,10 @@ export default Vue.extend({
},
render (h) {
- const track = [
- h('div', {
- staticClass: `q-slider__track q-slider__track${this.axis} absolute`,
- style: this.trackStyle
- })
- ]
-
- this.markers !== false && track.push(
- h('div', {
- staticClass: `q-slider__track-markers q-slider__track-markers${this.axis} absolute-full fit`,
- style: this.markerStyle
- })
- )
-
const child = [
h('div', {
staticClass: `q-slider__track-container q-slider__track-container${this.axis} absolute`
- }, track),
+ }, this.__getTrack(h)),
this.__getThumb(h, 'min'),
this.__getThumb(h, 'max')
diff --git a/ui/src/components/range/QRange.json b/ui/src/components/range/QRange.json
index f5c40a6c586..b96a66abb0f 100644
--- a/ui/src/components/range/QRange.json
+++ b/ui/src/components/range/QRange.json
@@ -27,7 +27,7 @@
"min": {
"type": "Number",
- "desc": "Minimum value of the model",
+ "desc": "Minimum value of the model; Set track's minimum value",
"default": 0,
"examples": [ ":min=\"0\"" ],
"category": "model"
@@ -35,7 +35,7 @@
"max": {
"type": "Number",
- "desc": "Maximum value of the model",
+ "desc": "Maximum value of the model; Set track's maximum value",
"default": 100,
"examples": [ ":max=\"100\"" ],
"category": "model"
@@ -49,6 +49,22 @@
"category": "model"
},
+ "inner-min": {
+ "type": "Number",
+ "desc": "Inner minimum value of the model; Use in case you need the model value to be inside of the track's min-max values; Needs to be higher or equal to 'min' prop; Defaults to 'min' prop",
+ "examples": [ ":inner-min=\"0\"" ],
+ "category": "content",
+ "addedIn": "v1.17"
+ },
+
+ "inner-max": {
+ "type": "Number",
+ "desc": "Inner maximum value of the model; Use in case you need the model value to be inside of the track's min-max values; Needs to be lower or equal to 'max' prop; Defaults to 'max' prop",
+ "examples": [ ":max-value=\"100\"" ],
+ "category": "content",
+ "addedIn": "v1.17"
+ },
+
"reverse": {
"type": "Boolean",
"desc": "Work in reverse (changes direction)",
@@ -215,5 +231,113 @@
},
"addedIn": "v1.14.0"
}
+ },
+
+ "scopedSlots": {
+ "markers": {
+ "desc": "Slot for adding markers to the QRange selector",
+ "scope": {
+ "value": {
+ "type": "Object",
+ "desc": "The model of type { min, max }",
+ "definition": {
+ "min": {
+ "type": "Number",
+ "desc": "Smaller end of the range",
+ "examples": [ 2 ]
+ },
+ "max": {
+ "type": "Number",
+ "desc": "Larger end of the range",
+ "examples": [ 12 ]
+ }
+ },
+ "examples": [ "{ min: 2, max: 12 }" ]
+ },
+ "vertical": {
+ "type": "Boolean",
+ "desc": "Component is displayed in vertical direction"
+ },
+ "reverse": {
+ "type": "Boolean",
+ "desc": "Component works in reverse"
+ },
+ "editable": {
+ "type": "Boolean",
+ "desc": "Component is editable"
+ },
+ "min": {
+ "type": "Number",
+ "desc": "Minimum value on the track",
+ "examples": [ 0 ]
+ },
+ "max": {
+ "type": "Number",
+ "desc": "Maximum value on the track",
+ "examples": [ 100 ]
+ },
+ "minValue": {
+ "type": "Number",
+ "desc": "Minimum value of the model",
+ "examples": [ 10 ]
+ },
+ "maxValue": {
+ "type": "Number",
+ "desc": "Maximum value of the model",
+ "examples": [ 90 ]
+ },
+ "styleFn": {
+ "type": "Function",
+ "desc": "Calculates the style (position as %) for a value",
+ "params": {
+ "value": {
+ "type": "Number",
+ "desc": "Value for which to calculate the style",
+ "examples": [ 25 ]
+ }
+ },
+ "returns": {
+ "type": "Object",
+ "desc": "Object with prop (the name of the CSS property), value (the value of the CSS property), and style (the full style as text)",
+ "definition": {
+ "prop": {
+ "type": "String",
+ "desc": "The name of the CSS property",
+ "examples": [ "left", "right", "top", "bottom" ]
+ },
+ "value": {
+ "type": "String",
+ "desc": "The value of the CSS property",
+ "examples": [ "30%" ]
+ },
+ "style": {
+ "type": "String",
+ "desc": "The full style",
+ "examples": [ "left: 30%" ]
+ }
+ },
+ "examples": [ "{ prop: 'left', value: '30%', style: 'left: 30%' }" ]
+ }
+ },
+ "setFn": {
+ "type": "Function",
+ "desc": "Set the internal model (and emits the events if necessary)",
+ "params": {
+ "model": {
+ "type": "Number",
+ "desc": "The new value of the model",
+ "examples": [ "{ min: 10, max: 45 }" ]
+ }
+ },
+ "returns": null
+ },
+ "stopEvents": {
+ "type": "Object",
+ "desc": "Listeners on mousedown and click that can be applied on the markers to stop propagation to the QRange; Can be used as `v-on=\"scope.stopEvents\"`",
+ "__exemption": [ "examples" ]
+ }
+ },
+ "addedIn": "v1.16.0"
+ }
}
}
diff --git a/ui/src/components/slider/QSlider.js b/ui/src/components/slider/QSlider.js
index 766804f7389..f96bdd6a068 100644
--- a/ui/src/components/slider/QSlider.js
+++ b/ui/src/components/slider/QSlider.js
@@ -26,25 +26,36 @@ export default Vue.extend({
},
data () {
+ const minModel = isNaN(this.innerMin) === true || this.innerMin < this.min
+ ? this.min
+ : this.innerMin
+ const model = this.value === null ? minModel : this.value
+
return {
- model: this.value === null ? this.min : this.value,
+ model,
curRatio: 0
}
},
watch: {
- value (v) {
- this.model = v === null
- ? 0
- : between(v, this.min, this.max)
+ value (val) {
+ const model = val === null
+ ? this.minInnerValue
+ : between(val, this.minInnerValue, this.maxInnerValue)
+
+ if (this.model !== model) {
+ this.model = model
+
+ this.curRatio = this.__getModelRatio(model)
+ }
},
- min (v) {
- this.model = between(this.model, v, this.max)
+ minInnerValue (val) {
+ this.model = between(this.model, val, this.maxInnerValue)
},
- max (v) {
- this.model = between(this.model, this.min, v)
+ maxInnerValue (val) {
+ this.model = between(this.model, this.minInnerValue, val)
}
},
@@ -54,13 +65,13 @@ export default Vue.extend({
},
modelRatio () {
- return this.minMaxDiff === 0 ? 0 : (this.model - this.min) / this.minMaxDiff
+ return this.__getModelRatio(this.model)
},
trackStyle () {
return {
[ this.positionProp ]: 0,
- [ this.sizeProp ]: `${100 * this.ratio}%`
+ [ this.sizeProp ]: `${100 * between(this.ratio, 0, 1)}%`
}
},
@@ -87,6 +98,17 @@ export default Vue.extend({
(this.labelTextColor !== void 0 ? ` text-${this.labelTextColor}` : '')
},
+ pinStyle () {
+ const percent = (this.reverse === true ? -this.ratio : this.ratio - 1)
+ return this.__getPinStyle(percent, this.ratio)
+ },
+
+ computedLabel () {
+ return this.labelValue !== void 0
+ ? this.labelValue
+ : this.model
+ },
+
events () {
if (this.editable === true) {
return this.$q.platform.is.mobile === true
@@ -99,17 +121,6 @@ export default Vue.extend({
keyup: this.__keyup
}
}
- },
-
- computedLabel () {
- return this.labelValue !== void 0
- ? this.labelValue
- : this.model
- },
-
- pinStyle () {
- const percent = (this.reverse === true ? -this.ratio : this.ratio - 1)
- return this.__getPinStyle(percent, this.ratio)
}
},
@@ -126,21 +137,21 @@ export default Vue.extend({
},
__updatePosition (event, dragging = this.dragging) {
- const ratio = getRatio(
- event,
- dragging,
- this.isReversed,
- this.vertical
+ const ratio = between(
+ getRatio(event, dragging, this.isReversed, this.vertical),
+ this.minInnerRatio,
+ this.maxInnerRatio
+ )
+
+ this.model = between(
+ getModel(ratio, this.min, this.max, this.step, this.decimals),
+ this.minInnerValue,
+ this.maxInnerValue
)
- this.model = getModel(ratio, this.min, this.max, this.step, this.decimals)
this.curRatio = this.snap !== true || this.step === 0
? ratio
- : (
- this.minMaxDiff === 0
- ? 0
- : (this.model - this.min) / this.minMaxDiff
- )
+ : this.__getModelRatio(this.model)
},
__focus () {
@@ -160,67 +171,75 @@ export default Vue.extend({
this.model = between(
parseFloat((this.model + offset).toFixed(this.decimals)),
- this.min,
- this.max
+ this.minInnerValue,
+ this.maxInnerValue
)
this.__updateValue()
- }
- },
+ },
- render (h) {
- const child = [
- this.__getThumbSvg(h),
- h('div', { staticClass: 'q-slider__focus-ring' })
- ]
+ __getThumb (h) {
+ if (this.ratio < 0 || this.ratio > 1) {
+ return
+ }
+
+ const child = [
+ this.__getThumbSvg(h),
+ h('div', { staticClass: 'q-slider__focus-ring' })
+ ]
- if (this.label === true || this.labelAlways === true) {
- child.push(
- h('div', {
- staticClass: `q-slider__pin q-slider__pin${this.axis} absolute`,
- style: this.pinStyle.pin,
- class: this.pinClass
- }, [
+ if (this.label === true || this.labelAlways === true) {
+ child.push(
h('div', {
- staticClass: `q-slider__pin-text-container q-slider__pin-text-container${this.axis}`,
- style: this.pinStyle.pinTextContainer
+ staticClass: `q-slider__pin q-slider__pin${this.axis} absolute`,
+ style: this.pinStyle.pin,
+ class: this.pinClass
}, [
- h('span', {
- staticClass: 'q-slider__pin-text',
- class: this.pinTextClass
+ h('div', {
+ staticClass: `q-slider__pin-text-container q-slider__pin-text-container${this.axis}`,
+ style: this.pinStyle.pinTextContainer
}, [
- this.computedLabel
+ h('span', {
+ staticClass: 'q-slider__pin-text',
+ class: this.pinTextClass
+ }, [
+ this.computedLabel
+ ])
])
- ])
- ]),
+ ]),
- h('div', {
- staticClass: `q-slider__arrow q-slider__arrow${this.axis}`,
- class: this.pinClass
- })
- )
- }
+ h('div', {
+ staticClass: `q-slider__arrow q-slider__arrow${this.axis}`,
+ class: this.pinClass
+ })
+ )
+ }
- if (this.name !== void 0 && this.disable !== true) {
- this.__injectFormInput(child, 'push')
+ return h('div', {
+ staticClass: `q-slider__thumb-container q-slider__thumb-container${this.axis} absolute non-selectable`,
+ class: this.thumbClass,
+ style: this.thumbStyle
+ }, child)
}
+ },
- const track = [
+ render (h) {
+ const child = [
h('div', {
- staticClass: `q-slider__track q-slider__track${this.axis} absolute`,
- style: this.trackStyle
- })
+ staticClass: `q-slider__track-container q-slider__track-container${this.axis} absolute`
+ }, this.__getTrack(h)),
+
+ this.__getThumb(h)
]
- this.markers !== false && track.push(
- h('div', {
- staticClass: `q-slider__track-markers q-slider__track-markers${this.axis} absolute-full fit`,
- style: this.markerStyle
- })
- )
+ if (this.name !== void 0 && this.disable !== true) {
+ this.__injectFormInput(child, 'push')
+ }
return h('div', {
- staticClass: this.value === null ? ' q-slider--no-value' : '',
+ staticClass: this.value === null
+ ? ' q-slider--no-value'
+ : void 0,
attrs: {
...this.attrs,
'aria-valuenow': this.value,
@@ -229,16 +248,6 @@ export default Vue.extend({
class: this.classes,
on: this.events,
directives: this.panDirectives
- }, [
- h('div', {
- staticClass: `q-slider__track-container q-slider__track-container${this.axis} absolute`
- }, track),
-
- h('div', {
- staticClass: `q-slider__thumb-container q-slider__thumb-container${this.axis} absolute non-selectable`,
- class: this.thumbClass,
- style: this.thumbStyle
- }, child)
- ])
+ }, child)
}
})
diff --git a/ui/src/components/slider/QSlider.json b/ui/src/components/slider/QSlider.json
index 423a5dc36c2..a87c2e2a982 100644
--- a/ui/src/components/slider/QSlider.json
+++ b/ui/src/components/slider/QSlider.json
@@ -15,7 +15,7 @@
"min": {
"type": "Number",
- "desc": "Minimum value of the model",
+ "desc": "Minimum value of the model; Set track's minimum value",
"default": 0,
"examples": [ ":min=\"0\"" ],
"category": "model"
@@ -23,7 +23,7 @@
"max": {
"type": "Number",
- "desc": "Maximum value of the model",
+ "desc": "Maximum value of the model; Set track's maximum value",
"default": 100,
"examples": [ ":max=\"100\"" ],
"category": "model"
@@ -37,6 +37,22 @@
"category": "model"
},
+ "inner-min": {
+ "type": "Number",
+ "desc": "Inner minimum value of the model; Use in case you need the model value to be inside of the track's min-max values; Needs to be higher or equal to 'min' prop; Defaults to 'min' prop",
+ "examples": [ ":inner-min=\"0\"" ],
+ "category": "content",
+ "addedIn": "v1.17"
+ },
+
+ "inner-max": {
+ "type": "Number",
+ "desc": "Inner maximum value of the model; Use in case you need the model value to be inside of the track's min-max values; Needs to be lower or equal to 'max' prop; Defaults to 'max' prop",
+ "examples": [ ":max-value=\"100\"" ],
+ "category": "content",
+ "addedIn": "v1.17"
+ },
+
"reverse": {
"type": "Boolean",
"desc": "Work in reverse (changes direction)",
@@ -152,5 +168,101 @@
},
"addedIn": "v1.14.0"
}
+ },
+
+ "scopedSlots": {
+ "markers": {
+ "desc": "Slot for adding markers to the QSlide selector",
+ "scope": {
+ "value": {
+ "type": "Number",
+ "desc": "The model",
+ "examples": [ 0 ]
+ },
+ "vertical": {
+ "type": "Boolean",
+ "desc": "Component is displayed in vertical direction"
+ },
+ "reverse": {
+ "type": "Boolean",
+ "desc": "Component works in reverse"
+ },
+ "editable": {
+ "type": "Boolean",
+ "desc": "Component is editable"
+ },
+ "min": {
+ "type": "Number",
+ "desc": "Minimum value on the track",
+ "examples": [ 0 ]
+ },
+ "max": {
+ "type": "Number",
+ "desc": "Maximum value on the track",
+ "examples": [ 100 ]
+ },
+ "minValue": {
+ "type": "Number",
+ "desc": "Minimum value of the model",
+ "examples": [ 10 ]
+ },
+ "maxValue": {
+ "type": "Number",
+ "desc": "Maximum value of the model",
+ "examples": [ 90 ]
+ },
+ "styleFn": {
+ "type": "Function",
+ "desc": "Calculates the style (position as %) for a value",
+ "params": {
+ "value": {
+ "type": "Number",
+ "desc": "Value for which to calculate the style",
+ "examples": [ 25 ]
+ }
+ },
+ "returns": {
+ "type": "Object",
+ "desc": "Object with prop (the name of the CSS property), value (the value of the CSS property), and style (the full style as text)",
+ "definition": {
+ "prop": {
+ "type": "String",
+ "desc": "The name of the CSS property",
+ "examples": [ "left", "right", "top", "bottom" ]
+ },
+ "value": {
+ "type": "String",
+ "desc": "The value of the CSS property",
+ "examples": [ "30%" ]
+ },
+ "style": {
+ "type": "String",
+ "desc": "The full style",
+ "examples": [ "left: 30%" ]
+ }
+ },
+ "examples": [ "{ prop: 'left', value: '30%', style: 'left: 30%' }" ]
+ }
+ },
+ "setFn": {
+ "type": "Function",
+ "desc": "Set the internal model (and emits the events if necessary)",
+ "params": {
+ "model": {
+ "type": "Number",
+ "desc": "The new value of the model",
+ "examples": [ 25 ]
+ }
+ },
+ "returns": null
+ },
+ "stopEvents": {
+ "type": "Object",
+ "desc": "Listeners on mousedown and click that can be applied on the markers to stop propagation to the QSlider; Can be used as `v-on=\"scope.stopEvents\"`",
+ "__exemption": [ "examples" ]
+ }
+ },
+ "addedIn": "v1.16.0"
+ }
}
}
diff --git a/ui/src/components/slider/QSlider.sass b/ui/src/components/slider/QSlider.sass
index 3d1f914c11f..e9639ca25ee 100644
--- a/ui/src/components/slider/QSlider.sass
+++ b/ui/src/components/slider/QSlider.sass
@@ -3,6 +3,7 @@
color: $primary
color: var(--q-color-primary)
outline: 0
+ isolation: isolate
&--h
width: 100%
@@ -12,7 +13,8 @@
height: 200px
&__track-container
- background: rgba(0,0,0,.26)
+ background: rgba(0,0,0,.06)
+ border-radius: $generic-border-radius
&--h
top: 50%
@@ -25,8 +27,21 @@
height: 100%
width: 2px
+ &__inner-track
+ background: rgba(0,0,0,.2)
+ border-radius: $generic-border-radius
+ z-index: -1
+
+ &--h
+ top: 0
+ bottom: 0
+ &--v
+ left: 0
+ right: 0
+
&__track
background: currentColor
+ border-radius: $generic-border-radius
&--h
will-change: width, left
@@ -208,7 +223,9 @@
&--dark
.q-slider__track-container
- background: rgba(#fff, .3)
+ background: rgba(#fff, .1)
+ .q-slider__inner-track
+ background: rgba(#fff, .2)
.q-slider__track-markers
color: #fff
diff --git a/ui/src/components/slider/QSlider.styl b/ui/src/components/slider/QSlider.styl
index 2301f1a741d..4a91c64c309 100644
--- a/ui/src/components/slider/QSlider.styl
+++ b/ui/src/components/slider/QSlider.styl
@@ -3,6 +3,7 @@
color: $primary
color: var(--q-color-primary)
outline: 0
+ isolation: isolate
&--h
width: 100%
@@ -12,7 +13,8 @@
height: 200px
&__track-container
- background: rgba(0,0,0,.26)
+ background: rgba(0,0,0,.06)
+ border-radius: $generic-border-radius
&--h
top: 50%
@@ -25,8 +27,21 @@
height: 100%
width: 2px
+ &__inner-track
+ background: rgba(0,0,0,.2)
+ border-radius: $generic-border-radius
+ z-index: -1
+
+ &--h
+ top: 0
+ bottom: 0
+ &--v
+ left: 0
+ right: 0
+
&__track
background: currentColor
+ border-radius: $generic-border-radius
&--h
will-change: width, left
@@ -208,7 +223,9 @@
&--dark
.q-slider__track-container
- background: rgba(#fff, .3)
+ background: rgba(#fff, .1)
+ .q-slider__inner-track
+ background: rgba(#fff, .2)
.q-slider__track-markers
color: #fff
diff --git a/ui/src/components/slider/slider-utils.js b/ui/src/components/slider/slider-utils.js
index 97b364e054d..ccd7017ef0e 100644
--- a/ui/src/components/slider/slider-utils.js
+++ b/ui/src/components/slider/slider-utils.js
@@ -1,5 +1,5 @@
import { between } from '../../utils/format.js'
-import { position } from '../../utils/event.js'
+import { position, stop } from '../../utils/event.js'
import { isNumber } from '../../utils/is.js'
import FormMixin from '../../mixins/form.js'
@@ -31,7 +31,7 @@ export function getModel (ratio, min, max, step, decimals) {
model = parseFloat(model.toFixed(decimals))
}
- return between(model, min, max)
+ return model
}
export const SliderMixin = {
@@ -56,6 +56,9 @@ export const SliderMixin = {
validator: v => v >= 0
},
+ innerMin: Number,
+ innerMax: Number,
+
color: String,
labelColor: String,
@@ -106,7 +109,7 @@ export const SliderMixin = {
},
editable () {
- return this.disable !== true && this.readonly !== true && this.min < this.max
+ return this.disable !== true && this.readonly !== true && this.minInnerValue <= this.maxInnerValue
},
decimals () {
@@ -121,10 +124,37 @@ export const SliderMixin = {
return this.max - this.min
},
+ minInnerValue () {
+ return isNaN(this.innerMin) === true || this.innerMin < this.min
+ ? this.min
+ : this.innerMin
+ },
+
+ maxInnerValue () {
+ return isNaN(this.innerMax) === true || this.innerMax > this.max
+ ? this.max
+ : this.innerMax
+ },
+
+ minInnerRatio () {
+ return this.__getModelRatio(this.minInnerValue)
+ },
+
+ maxInnerRatio () {
+ return this.__getModelRatio(this.maxInnerValue)
+ },
+
markerStep () {
return isNumber(this.markers) === true ? this.markers : this.computedStep
},
+ innerTrackStyle () {
+ return {
+ [ this.positionProp ]: `${100 * this.minInnerRatio}%`,
+ [ this.sizeProp ]: `${100 * (this.maxInnerRatio - this.minInnerRatio)}%`
+ }
+ },
+
markerStyle () {
if (this.minMaxDiff !== 0) {
const size = 100 * this.markerStep / this.minMaxDiff
@@ -137,6 +167,46 @@ export const SliderMixin = {
}
},
+ markersScopeBase () {
+ return {
+ vertical: this.vertical,
+ reverse: this.reverse,
+ editable: this.editable,
+
+ min: this.min,
+ max: this.max,
+ minValue: this.minInnerValue,
+ maxValue: this.maxInnerValue,
+
+ styleFn: val => {
+ const value = `${val / this.minMaxDiff * 100}%`
+
+ return {
+ prop: this.positionProp,
+ value,
+ style: `${this.positionProp}: ${value}`
+ }
+ },
+
+ setFn: val => {
+ this.model = val
+ this.__updateValue()
+ },
+
+ stopEvents: {
+ mousedown: stop,
+ click: stop
+ }
+ }
+ },
+
+ markersScope () {
+ return {
+ value: this.model,
+ ...this.markersScopeBase
+ }
+ },
+
computedTabindex () {
return this.editable === true ? this.tabindex || 0 : -1
},
@@ -165,8 +235,8 @@ export const SliderMixin = {
attrs () {
const attrs = {
role: 'slider',
- 'aria-valuemin': this.min,
- 'aria-valuemax': this.max,
+ 'aria-valuemin': this.minInnerValue,
+ 'aria-valuemax': this.maxInnerValue,
'aria-orientation': this.orientation,
'data-step': this.step
}
@@ -199,6 +269,10 @@ export const SliderMixin = {
},
methods: {
+ __getModelRatio (model) {
+ return this.minMaxDiff === 0 ? 0 : (model - this.min) / this.minMaxDiff
+ },
+
__getThumbSvg (h) {
return h('svg', {
staticClass: 'q-slider__thumb absolute',
@@ -218,6 +292,33 @@ export const SliderMixin = {
])
},
+ __getTrack (h) {
+ const track = [
+ h('div', {
+ staticClass: `q-slider__inner-track q-slider__inner-track${this.axis} absolute`,
+ style: this.innerTrackStyle
+ }),
+
+ h('div', {
+ staticClass: `q-slider__track q-slider__track${this.axis} absolute`,
+ style: this.trackStyle
+ })
+ ]
+
+ this.markers !== false && track.push(
+ h('div', {
+ staticClass: `q-slider__track-markers q-slider__track-markers${this.axis} absolute-full fit`,
+ style: this.markerStyle
+ })
+ )
+
+ this.$scopedSlots.markers !== void 0 && track.push(
+ ...this.$scopedSlots.markers(this.markersScope)
+ )
+
+ return track
+ },
+
__getPinStyle (percent, ratio) {
if (this.vertical === true) {
return {}