quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
769 lines (661 loc) • 18.7 kB
JavaScript
import { h, ref, computed, onBeforeUnmount, getCurrentInstance } from 'vue'
import TouchPan from '../../directives/touch-pan/TouchPan.js'
import useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import {
useFormProps,
useFormInject
} from '../../composables/use-form/private.use-form.js'
import { between } from '../../utils/format/format.js'
import { position } from '../../utils/event/event.js'
import { isNumber, isObject } from '../../utils/is/is.js'
import { hDir } from '../../utils/private.render/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,
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 useSlider({
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 roundValueFn = computed(() => {
if (props.step === 0) {
return v => v
}
const decimals = (String(props.step).trim().split('.')[1] || '').length
return v => parseFloat(v.toFixed(decimals))
})
const keyStep = 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 innerDiff = innerMaxRatio.value - innerMinRatio.value
const acc = {
[positionProp.value]: `${100 * innerMinRatio.value}%`,
[sizeProp.value]: innerDiff === 0 ? '2px' : `${100 * innerDiff}%`
}
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 - innerMin.value) % step
model +=
(Math.abs(modulo) >= step / 2 ? (modulo < 0 ? -1 : 1) * step : 0) -
modulo
}
model = roundValueFn.value(model)
return between(model, innerMin.value, innerMax.value)
}
function convertModelToRatio(model) {
return trackLen.value === 0 ? 0 : (model - props.min) / trackLen.value
}
function getDraggingRatio(evt, draggingInfo) {
const pos = position(evt),
val =
props.vertical === true
? between((pos.top - draggingInfo.top) / draggingInfo.height, 0, 1)
: between((pos.left - draggingInfo.left) / draggingInfo.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 : keyStep.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(() => {
const size =
innerBarLen.value === 0
? '2px'
: (100 * markerStep.value) / innerBarLen.value
return {
...innerBarStyle.value,
backgroundSize: props.vertical === true ? `2px ${size}%` : `${size}% 2px`
}
})
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(() => [
[
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:
if (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 thumbClasses = 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: thumbClasses.value,
style: style.value,
...thumb.getNodeData()
},
thumbContent
)
}
}
function getContent(
selectionBarStyle,
trackContainerTabindex,
trackContainerEvents,
injectThumb
) {
const trackContent = []
if (props.innerTrackColor !== 'transparent') {
trackContent.push(
h('div', {
key: 'inner',
class: innerBarClass.value,
style: innerBarStyle.value
})
)
}
if (props.selectionColor !== 'transparent') {
trackContent.push(
h('div', {
key: 'selection',
class: selectionBarClass.value,
style: selectionBarStyle.value
})
)
}
if (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,
roundValueFn,
keyStep,
trackLen,
innerMin,
innerMinRatio,
innerMax,
innerMaxRatio,
positionProp,
sizeProp,
isReversed
},
methods: {
onActivate,
onMobileClick,
onBlur,
onKeyup,
getContent,
getThumbRenderFn,
convertRatioToModel,
convertModelToRatio,
getDraggingRatio
}
}
}