quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
657 lines (549 loc) • 17.7 kB
JavaScript
import { h, ref, computed, onBeforeUnmount, getCurrentInstance } from 'vue'
import TouchPan from '../../directives/TouchPan.js'
import useDark, { useDarkProps } from '../../composables/private/use-dark.js'
import { useFormProps, useFormInject } from '../../composables/private/use-form.js'
import { between } from '../../utils/format.js'
import { position } from '../../utils/event.js'
import { isNumber, isObject } from '../../utils/is.js'
import { hDir } from '../../utils/private/render.js'
const markerPrefixClass = 'q-slider__marker-labels'
const defaultMarkerConvertFn = v => ({ value: v })
const defaultMarkerLabelRenderFn = ({ marker }) => h('div', {
key: marker.value,
style: marker.style,
class: marker.classes
}, marker.label)
// PGDOWN, LEFT, DOWN, PGUP, RIGHT, UP
export const keyCodes = [ 34, 37, 40, 33, 39, 38 ]
export const useSliderProps = {
...useDarkProps,
...useFormProps,
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
innerMin: Number,
innerMax: Number,
step: {
type: Number,
default: 1,
validator: v => v >= 0
},
snap: Boolean,
vertical: Boolean,
reverse: Boolean,
hideSelection: Boolean,
color: String,
markerLabelsClass: String,
label: Boolean,
labelColor: String,
labelTextColor: String,
labelAlways: Boolean,
switchLabelSide: Boolean,
markers: [ Boolean, Number ],
markerLabels: [ Boolean, Array, Object, Function ],
switchMarkerLabelsSide: Boolean,
trackImg: String,
trackColor: String,
innerTrackImg: String,
innerTrackColor: String,
selectionColor: String,
selectionImg: String,
thumbSize: {
type: String,
default: '20px'
},
trackSize: {
type: String,
default: '4px'
},
disable: Boolean,
readonly: Boolean,
dense: Boolean,
tabindex: [ String, Number ],
thumbColor: String,
thumbPath: {
type: String,
default: 'M 4, 10 a 6,6 0 1,0 12,0 a 6,6 0 1,0 -12,0'
}
}
export const useSliderEmits = [ 'pan', 'update:modelValue', 'change' ]
export default function ({ updateValue, updatePosition, getDragging, formAttrs }) {
const { props, emit, slots, proxy: { $q } } = getCurrentInstance()
const isDark = useDark(props, $q)
const injectFormInput = useFormInject(formAttrs)
const active = ref(false)
const preventFocus = ref(false)
const focus = ref(false)
const dragging = ref(false)
const axis = computed(() => (props.vertical === true ? '--v' : '--h'))
const labelSide = computed(() => '-' + (props.switchLabelSide === true ? 'switched' : 'standard'))
const isReversed = computed(() => (
props.vertical === true
? props.reverse === true
: props.reverse !== ($q.lang.rtl === true)
))
const innerMin = computed(() => (
isNaN(props.innerMin) === true || props.innerMin < props.min
? props.min
: props.innerMin
))
const innerMax = computed(() => (
isNaN(props.innerMax) === true || props.innerMax > props.max
? props.max
: props.innerMax
))
const editable = computed(() => (
props.disable !== true && props.readonly !== true
&& innerMin.value < innerMax.value
))
const decimals = computed(() => (String(props.step).trim().split('.')[ 1 ] || '').length)
const step = computed(() => (props.step === 0 ? 1 : props.step))
const tabindex = computed(() => (editable.value === true ? props.tabindex || 0 : -1))
const trackLen = computed(() => props.max - props.min)
const innerBarLen = computed(() => innerMax.value - innerMin.value)
const innerMinRatio = computed(() => convertModelToRatio(innerMin.value))
const innerMaxRatio = computed(() => convertModelToRatio(innerMax.value))
const positionProp = computed(() => (
props.vertical === true
? (isReversed.value === true ? 'bottom' : 'top')
: (isReversed.value === true ? 'right' : 'left')
))
const sizeProp = computed(() => (props.vertical === true ? 'height' : 'width'))
const thicknessProp = computed(() => (props.vertical === true ? 'width' : 'height'))
const orientation = computed(() => (props.vertical === true ? 'vertical' : 'horizontal'))
const attributes = computed(() => {
const acc = {
role: 'slider',
'aria-valuemin': innerMin.value,
'aria-valuemax': innerMax.value,
'aria-orientation': orientation.value,
'data-step': props.step
}
if (props.disable === true) {
acc[ 'aria-disabled' ] = 'true'
}
else if (props.readonly === true) {
acc[ 'aria-readonly' ] = 'true'
}
return acc
})
const classes = computed(() =>
`q-slider q-slider${ axis.value } q-slider--${ active.value === true ? '' : 'in' }active inline no-wrap `
+ (props.vertical === true ? 'row' : 'column')
+ (props.disable === true ? ' disabled' : ' q-slider--enabled' + (editable.value === true ? ' q-slider--editable' : ''))
+ (focus.value === 'both' ? ' q-slider--focus' : '')
+ (props.label || props.labelAlways === true ? ' q-slider--label' : '')
+ (props.labelAlways === true ? ' q-slider--label-always' : '')
+ (isDark.value === true ? ' q-slider--dark' : '')
+ (props.dense === true ? ' q-slider--dense q-slider--dense' + axis.value : '')
)
function getPositionClass (name) {
const cls = 'q-slider__' + name
return `${ cls } ${ cls }${ axis.value } ${ cls }${ axis.value }${ labelSide.value }`
}
function getAxisClass (name) {
const cls = 'q-slider__' + name
return `${ cls } ${ cls }${ axis.value }`
}
const selectionBarClass = computed(() => {
const color = props.selectionColor || props.color
return 'q-slider__selection absolute'
+ (color !== void 0 ? ` text-${ color }` : '')
})
const markerClass = computed(() => getAxisClass('markers') + ' absolute overflow-hidden')
const trackContainerClass = computed(() => getAxisClass('track-container'))
const pinClass = computed(() => getPositionClass('pin'))
const labelClass = computed(() => getPositionClass('label'))
const textContainerClass = computed(() => getPositionClass('text-container'))
const markerLabelsContainerClass = computed(() =>
getPositionClass('marker-labels-container')
+ (props.markerLabelsClass !== void 0 ? ` ${ props.markerLabelsClass }` : '')
)
const trackClass = computed(() =>
'q-slider__track relative-position no-outline'
+ (props.trackColor !== void 0 ? ` bg-${ props.trackColor }` : '')
)
const trackStyle = computed(() => {
const acc = { [ thicknessProp.value ]: props.trackSize }
if (props.trackImg !== void 0) {
acc.backgroundImage = `url(${ props.trackImg }) !important`
}
return acc
})
const innerBarClass = computed(() =>
'q-slider__inner absolute'
+ (props.innerTrackColor !== void 0 ? ` bg-${ props.innerTrackColor }` : '')
)
const innerBarStyle = computed(() => {
const acc = {
[ positionProp.value ]: `${ 100 * innerMinRatio.value }%`,
[ sizeProp.value ]: `${ 100 * (innerMaxRatio.value - innerMinRatio.value) }%`
}
if (props.innerTrackImg !== void 0) {
acc.backgroundImage = `url(${ props.innerTrackImg }) !important`
}
return acc
})
function convertRatioToModel (ratio) {
const { min, max, step } = props
let model = min + ratio * (max - min)
if (step > 0) {
const modulo = (model - min) % step
model += (Math.abs(modulo) >= step / 2 ? (modulo < 0 ? -1 : 1) * step : 0) - modulo
}
if (decimals.value > 0) {
model = parseFloat(model.toFixed(decimals.value))
}
return between(model, innerMin.value, innerMax.value)
}
function convertModelToRatio (model) {
return trackLen.value === 0
? 0
: (model - props.min) / trackLen.value
}
function getDraggingRatio (evt, dragging) {
const
pos = position(evt),
val = props.vertical === true
? between((pos.top - dragging.top) / dragging.height, 0, 1)
: between((pos.left - dragging.left) / dragging.width, 0, 1)
return between(
isReversed.value === true ? 1.0 - val : val,
innerMinRatio.value,
innerMaxRatio.value
)
}
const markerStep = computed(() => (
isNumber(props.markers) === true ? props.markers : step.value)
)
const markerTicks = computed(() => {
const acc = []
const step = markerStep.value
const max = props.max
let value = props.min
do {
acc.push(value)
value += step
} while (value < max)
acc.push(max)
return acc
})
const markerLabelClass = computed(() => {
const prefix = ` ${ markerPrefixClass }${ axis.value }-`
return markerPrefixClass
+ `${ prefix }${ props.switchMarkerLabelsSide === true ? 'switched' : 'standard' }`
+ `${ prefix }${ isReversed.value === true ? 'rtl' : 'ltr' }`
})
const markerLabelsList = computed(() => {
if (props.markerLabels === false) { return null }
return getMarkerList(props.markerLabels).map((entry, index) => ({
index,
value: entry.value,
label: entry.label || entry.value,
classes: markerLabelClass.value
+ (entry.classes !== void 0 ? ' ' + entry.classes : ''),
style: {
...getMarkerLabelStyle(entry.value),
...(entry.style || {})
}
}))
})
const markerScope = computed(() => ({
markerList: markerLabelsList.value,
markerMap: markerLabelsMap.value,
classes: markerLabelClass.value, // TODO ts definition
getStyle: getMarkerLabelStyle
}))
const markerStyle = computed(() => {
if (innerBarLen.value !== 0) {
const size = 100 * markerStep.value / innerBarLen.value
return {
...innerBarStyle.value,
backgroundSize: props.vertical === true
? `2px ${ size }%`
: `${ size }% 2px`
}
}
return null
})
function getMarkerList (def) {
if (def === false) { return null }
if (def === true) {
return markerTicks.value.map(defaultMarkerConvertFn)
}
if (typeof def === 'function') {
return markerTicks.value.map(value => {
const item = def(value)
return isObject(item) === true ? { ...item, value } : { value, label: item }
})
}
const filterFn = ({ value }) => value >= props.min && value <= props.max
if (Array.isArray(def) === true) {
return def
.map(item => (isObject(item) === true ? item : { value: item }))
.filter(filterFn)
}
return Object.keys(def).map(key => {
const item = def[ key ]
const value = Number(key)
return isObject(item) === true ? { ...item, value } : { value, label: item }
}).filter(filterFn)
}
function getMarkerLabelStyle (val) {
return { [ positionProp.value ]: `${ 100 * (val - props.min) / trackLen.value }%` }
}
const markerLabelsMap = computed(() => {
if (props.markerLabels === false) { return null }
const acc = {}
markerLabelsList.value.forEach(entry => {
acc[ entry.value ] = entry
})
return acc
})
function getMarkerLabelsContent () {
if (slots[ 'marker-label-group' ] !== void 0) {
return slots[ 'marker-label-group' ](markerScope.value)
}
const fn = slots[ 'marker-label' ] || defaultMarkerLabelRenderFn
return markerLabelsList.value.map(marker => fn({
marker,
...markerScope.value
}))
}
const panDirective = computed(() => {
// if editable.value === true
return [ [
TouchPan,
onPan,
void 0,
{
[ orientation.value ]: true,
prevent: true,
stop: true,
mouse: true,
mouseAllDir: true
}
] ]
})
function onPan (event) {
if (event.isFinal === true) {
if (dragging.value !== void 0) {
updatePosition(event.evt)
// only if touch, because we also have mousedown/up:
event.touch === true && updateValue(true)
dragging.value = void 0
emit('pan', 'end')
}
active.value = false
focus.value = false
}
else if (event.isFirst === true) {
dragging.value = getDragging(event.evt)
updatePosition(event.evt)
updateValue()
active.value = true
emit('pan', 'start')
}
else {
updatePosition(event.evt)
updateValue()
}
}
function onBlur () {
focus.value = false
}
function onActivate (evt) {
updatePosition(evt, getDragging(evt))
updateValue()
preventFocus.value = true
active.value = true
document.addEventListener('mouseup', onDeactivate, true)
}
function onDeactivate () {
preventFocus.value = false
active.value = false
updateValue(true)
onBlur()
document.removeEventListener('mouseup', onDeactivate, true)
}
function onMobileClick (evt) {
updatePosition(evt, getDragging(evt))
updateValue(true)
}
function onKeyup (evt) {
if (keyCodes.includes(evt.keyCode)) {
updateValue(true)
}
}
function getTextContainerStyle (ratio) {
if (props.vertical === true) { return null }
const p = $q.lang.rtl !== props.reverse ? 1 - ratio : ratio
return {
transform: `translateX(calc(${ 2 * p - 1 } * ${ props.thumbSize } / 2 + ${ 50 - 100 * p }%))`
}
}
function getThumbRenderFn (thumb) {
const focusClass = computed(() => (
preventFocus.value === false && (focus.value === thumb.focusValue || focus.value === 'both')
? ' q-slider--focus'
: ''
))
const classes = computed(() =>
`q-slider__thumb q-slider__thumb${ axis.value } q-slider__thumb${ axis.value }-${ isReversed.value === true ? 'rtl' : 'ltr' } absolute non-selectable`
+ focusClass.value
+ (thumb.thumbColor.value !== void 0 ? ` text-${ thumb.thumbColor.value }` : '')
)
const style = computed(() => ({
width: props.thumbSize,
height: props.thumbSize,
[ positionProp.value ]: `${ 100 * thumb.ratio.value }%`,
zIndex: focus.value === thumb.focusValue ? 2 : void 0
}))
const pinColor = computed(() => (
thumb.labelColor.value !== void 0
? ` text-${ thumb.labelColor.value }`
: ''
))
const textContainerStyle = computed(() => getTextContainerStyle(thumb.ratio.value))
const textClass = computed(() => (
'q-slider__text'
+ (thumb.labelTextColor.value !== void 0 ? ` text-${ thumb.labelTextColor.value }` : '')
))
return () => {
const thumbContent = [
h('svg', {
class: 'q-slider__thumb-shape absolute-full',
viewBox: '0 0 20 20',
'aria-hidden': 'true'
}, [
h('path', { d: props.thumbPath })
]),
h('div', { class: 'q-slider__focus-ring fit' })
]
if (props.label === true || props.labelAlways === true) {
thumbContent.push(
h('div', {
class: pinClass.value + ' absolute fit no-pointer-events' + pinColor.value
}, [
h('div', {
class: labelClass.value,
style: { minWidth: props.thumbSize }
}, [
h('div', {
class: textContainerClass.value,
style: textContainerStyle.value
}, [
h('span', { class: textClass.value }, thumb.label.value)
])
])
])
)
if (props.name !== void 0 && props.disable !== true) {
injectFormInput(thumbContent, 'push')
}
}
return h('div', {
class: classes.value,
style: style.value,
...thumb.getNodeData()
}, thumbContent)
}
}
function getContent (selectionBarStyle, trackContainerTabindex, trackContainerEvents, injectThumb) {
const trackContent = []
props.innerTrackColor !== 'transparent' && trackContent.push(
h('div', {
key: 'inner',
class: innerBarClass.value,
style: innerBarStyle.value
})
)
props.selectionColor !== 'transparent' && trackContent.push(
h('div', {
key: 'selection',
class: selectionBarClass.value,
style: selectionBarStyle.value
})
)
props.markers !== false && trackContent.push(
h('div', {
key: 'marker',
class: markerClass.value,
style: markerStyle.value
})
)
injectThumb(trackContent)
const content = [
hDir(
'div',
{
key: 'trackC',
class: trackContainerClass.value,
tabindex: trackContainerTabindex.value,
...trackContainerEvents.value
},
[
h('div', {
class: trackClass.value,
style: trackStyle.value
}, trackContent)
],
'slide',
editable.value, () => panDirective.value
)
]
if (props.markerLabels !== false) {
const action = props.switchMarkerLabelsSide === true
? 'unshift'
: 'push'
content[ action ](
h('div', {
key: 'markerL',
class: markerLabelsContainerClass.value
}, getMarkerLabelsContent())
)
}
return content
}
onBeforeUnmount(() => {
document.removeEventListener('mouseup', onDeactivate, true)
})
return {
state: {
active,
focus,
preventFocus,
dragging,
editable,
classes,
tabindex,
attributes,
step,
decimals,
trackLen,
innerMin,
innerMinRatio,
innerMax,
innerMaxRatio,
positionProp,
sizeProp,
isReversed
},
methods: {
onActivate,
onMobileClick,
onBlur,
onKeyup,
getContent,
getThumbRenderFn,
convertRatioToModel,
convertModelToRatio,
getDraggingRatio
}
}
}