quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
595 lines (509 loc) • 15.6 kB
JavaScript
import {
h,
ref,
computed,
watch,
onActivated,
onDeactivated,
onBeforeUnmount,
getCurrentInstance
} from 'vue'
import useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import ScrollAreaControls from './ScrollAreaControls.js'
import QResizeObserver from '../resize-observer/QResizeObserver.js'
import QScrollObserver from '../scroll-observer/QScrollObserver.js'
import TouchPan from '../../directives/touch-pan/TouchPan.js'
import { createComponent } from '../../utils/private.create/create.js'
import { between } from '../../utils/format/format.js'
import {
setVerticalScrollPosition,
setHorizontalScrollPosition
} from '../../utils/scroll/scroll.js'
import { hMergeSlot } from '../../utils/private.render/render.js'
import debounce from '../../utils/debounce/debounce.js'
const axisList = ['vertical', 'horizontal']
const dirProps = {
vertical: { offset: 'offsetY', scroll: 'scrollTop', dir: 'down', dist: 'y' },
horizontal: {
offset: 'offsetX',
scroll: 'scrollLeft',
dir: 'right',
dist: 'x'
}
}
const panOpts = {
prevent: true,
mouse: true,
mouseAllDir: true
}
const getMinThumbSize = size => (size >= 250 ? 50 : Math.ceil(size / 5))
export default createComponent({
name: 'QScrollArea',
props: {
...useDarkProps,
thumbStyle: Object,
verticalThumbStyle: Object,
horizontalThumbStyle: Object,
barStyle: [Array, String, Object],
verticalBarStyle: [Array, String, Object],
horizontalBarStyle: [Array, String, Object],
verticalOffset: {
type: Array,
default: [0, 0]
},
horizontalOffset: {
type: Array,
default: [0, 0]
},
contentStyle: [Array, String, Object],
contentActiveStyle: [Array, String, Object],
delay: {
type: [String, Number],
default: 1000
},
visible: {
type: Boolean,
default: null
},
tabindex: [String, Number],
onScroll: Function
},
setup(props, { slots, emit }) {
// state management
const tempShowing = ref(false)
const panning = ref(false)
const hover = ref(false)
// other...
const container = {
vertical: ref(0),
horizontal: ref(0)
}
const scroll = {
vertical: {
ref: ref(null),
position: ref(0),
size: ref(0)
},
horizontal: {
ref: ref(null),
position: ref(0),
size: ref(0)
}
}
const { proxy } = getCurrentInstance()
const isDark = useDark(props, proxy.$q)
let timer = null,
panRefPos
const targetRef = ref(null)
const classes = computed(
() =>
'q-scrollarea' + (isDark.value === true ? ' q-scrollarea--dark' : '')
)
Object.assign(container, {
verticalInner: computed(
() =>
container.vertical.value -
props.verticalOffset[0] -
props.verticalOffset[1]
),
horizontalInner: computed(
() =>
container.horizontal.value -
props.horizontalOffset[0] -
props.horizontalOffset[1]
)
})
scroll.vertical.percentage = computed(() => {
const diff = scroll.vertical.size.value - container.vertical.value
if (diff <= 0) {
return 0
}
const p = between(scroll.vertical.position.value / diff, 0, 1)
return Math.round(p * 10000) / 10000
})
scroll.vertical.thumbHidden = computed(
() =>
((props.visible === null ? hover.value : props.visible) !== true &&
tempShowing.value === false &&
panning.value === false) ||
scroll.vertical.size.value <= container.vertical.value + 1
)
scroll.vertical.thumbStart = computed(
() =>
props.verticalOffset[0] +
scroll.vertical.percentage.value *
(container.verticalInner.value - scroll.vertical.thumbSize.value)
)
scroll.vertical.thumbSize = computed(() =>
Math.round(
between(
(container.verticalInner.value * container.verticalInner.value) /
scroll.vertical.size.value,
getMinThumbSize(container.verticalInner.value),
container.verticalInner.value
)
)
)
scroll.vertical.style = computed(() => ({
...props.thumbStyle,
...props.verticalThumbStyle,
top: `${scroll.vertical.thumbStart.value}px`,
height: `${scroll.vertical.thumbSize.value}px`,
right: `${props.horizontalOffset[1]}px`
}))
scroll.vertical.thumbClass = computed(
() =>
'q-scrollarea__thumb q-scrollarea__thumb--v absolute-right' +
(scroll.vertical.thumbHidden.value === true
? ' q-scrollarea__thumb--invisible'
: '')
)
scroll.vertical.barClass = computed(
() =>
'q-scrollarea__bar q-scrollarea__bar--v absolute-right' +
(scroll.vertical.thumbHidden.value === true
? ' q-scrollarea__bar--invisible'
: '')
)
scroll.horizontal.percentage = computed(() => {
const diff = scroll.horizontal.size.value - container.horizontal.value
if (diff <= 0) {
return 0
}
const p = between(Math.abs(scroll.horizontal.position.value) / diff, 0, 1)
return Math.round(p * 10000) / 10000
})
scroll.horizontal.thumbHidden = computed(
() =>
((props.visible === null ? hover.value : props.visible) !== true &&
tempShowing.value === false &&
panning.value === false) ||
scroll.horizontal.size.value <= container.horizontal.value + 1
)
scroll.horizontal.thumbStart = computed(
() =>
props.horizontalOffset[0] +
scroll.horizontal.percentage.value *
(container.horizontalInner.value - scroll.horizontal.thumbSize.value)
)
scroll.horizontal.thumbSize = computed(() =>
Math.round(
between(
(container.horizontalInner.value * container.horizontalInner.value) /
scroll.horizontal.size.value,
getMinThumbSize(container.horizontalInner.value),
container.horizontalInner.value
)
)
)
scroll.horizontal.style = computed(() => ({
...props.thumbStyle,
...props.horizontalThumbStyle,
[proxy.$q.lang.rtl === true ? 'right' : 'left']:
`${scroll.horizontal.thumbStart.value}px`,
width: `${scroll.horizontal.thumbSize.value}px`,
bottom: `${props.verticalOffset[1]}px`
}))
scroll.horizontal.thumbClass = computed(
() =>
'q-scrollarea__thumb q-scrollarea__thumb--h absolute-bottom' +
(scroll.horizontal.thumbHidden.value === true
? ' q-scrollarea__thumb--invisible'
: '')
)
scroll.horizontal.barClass = computed(
() =>
'q-scrollarea__bar q-scrollarea__bar--h absolute-bottom' +
(scroll.horizontal.thumbHidden.value === true
? ' q-scrollarea__bar--invisible'
: '')
)
const mainStyle = computed(() =>
scroll.vertical.thumbHidden.value === true &&
scroll.horizontal.thumbHidden.value === true
? props.contentStyle
: props.contentActiveStyle
)
function getScroll() {
const info = {}
axisList.forEach(axis => {
const data = scroll[axis]
Object.assign(info, {
[axis + 'Position']: data.position.value,
[axis + 'Percentage']: data.percentage.value,
[axis + 'Size']: data.size.value,
[axis + 'ContainerSize']: container[axis].value,
[axis + 'ContainerInnerSize']: container[axis + 'Inner'].value
})
})
return info
}
// we have lots of listeners, so
// ensure we're not emitting same info
// multiple times
const emitScroll = debounce(() => {
const info = getScroll()
info.ref = proxy
emit('scroll', info)
}, 0)
function localSetScrollPosition(axis, offset, duration) {
if (axisList.includes(axis) === false) {
console.error(
'[QScrollArea]: wrong first param of setScrollPosition (vertical/horizontal)'
)
return
}
const fn =
axis === 'vertical'
? setVerticalScrollPosition
: setHorizontalScrollPosition
fn(targetRef.value, offset, duration)
}
function updateContainer({ height, width }) {
let change = false
if (container.vertical.value !== height) {
container.vertical.value = height
change = true
}
if (container.horizontal.value !== width) {
container.horizontal.value = width
change = true
}
if (change === true) startTimer()
}
function updateScroll({ position }) {
let change = false
if (scroll.vertical.position.value !== position.top) {
scroll.vertical.position.value = position.top
change = true
}
if (scroll.horizontal.position.value !== position.left) {
scroll.horizontal.position.value = position.left
change = true
}
if (change === true) startTimer()
}
function updateScrollSize({ height, width }) {
if (scroll.horizontal.size.value !== width) {
scroll.horizontal.size.value = width
startTimer()
}
if (scroll.vertical.size.value !== height) {
scroll.vertical.size.value = height
startTimer()
}
}
function onPanThumb(e, axis) {
const data = scroll[axis]
if (e.isFirst === true) {
if (data.thumbHidden.value === true) return
panRefPos = data.position.value
panning.value = true
} else if (panning.value !== true) {
return
}
if (e.isFinal === true) {
panning.value = false
}
const dProp = dirProps[axis]
const multiplier =
(data.size.value - container[axis].value) /
(container[axis + 'Inner'].value - data.thumbSize.value)
const distance = e.distance[dProp.dist]
const pos =
panRefPos + (e.direction === dProp.dir ? 1 : -1) * distance * multiplier
setScroll(pos, axis)
}
function onMousedown(evt, axis) {
const data = scroll[axis]
if (data.thumbHidden.value !== true) {
const startOffset =
axis === 'vertical'
? props.verticalOffset[0]
: props.horizontalOffset[0]
const offset = evt[dirProps[axis].offset] - startOffset
const thumbStart = data.thumbStart.value - startOffset
if (offset < thumbStart || offset > thumbStart + data.thumbSize.value) {
const targetThumbStart = offset - data.thumbSize.value / 2
const percentage = between(
targetThumbStart /
(container[axis + 'Inner'].value - data.thumbSize.value),
0,
1
)
setScroll(
percentage * Math.max(0, data.size.value - container[axis].value),
axis
)
}
// activate thumb pan
if (data.ref.value !== null) {
data.ref.value.dispatchEvent(new MouseEvent(evt.type, evt))
}
}
}
function startTimer() {
tempShowing.value = true
if (timer !== null) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
tempShowing.value = false
}, props.delay)
if (props.onScroll !== void 0) emitScroll()
}
function setScroll(offset, axis) {
targetRef.value[dirProps[axis].scroll] = offset
}
let mouseEventTimer = null
function onMouseenter() {
if (mouseEventTimer !== null) {
clearTimeout(mouseEventTimer)
}
// setTimeout needed for iOS; see ticket #16210
mouseEventTimer = setTimeout(
() => {
mouseEventTimer = null
hover.value = true
},
proxy.$q.platform.is.ios ? 50 : 0
)
}
function onMouseleave() {
if (mouseEventTimer !== null) {
clearTimeout(mouseEventTimer)
mouseEventTimer = null
}
hover.value = false
}
let scrollPosition = null
watch(
() => proxy.$q.lang.rtl,
rtl => {
if (targetRef.value !== null) {
setHorizontalScrollPosition(
targetRef.value,
Math.abs(scroll.horizontal.position.value) * (rtl === true ? -1 : 1)
)
}
}
)
onDeactivated(() => {
scrollPosition = {
top: scroll.vertical.position.value,
left: scroll.horizontal.position.value
}
})
onActivated(() => {
if (scrollPosition === null) return
const scrollTarget = targetRef.value
if (scrollTarget !== null) {
setHorizontalScrollPosition(scrollTarget, scrollPosition.left)
setVerticalScrollPosition(scrollTarget, scrollPosition.top)
}
})
onBeforeUnmount(emitScroll.cancel)
// expose public methods
Object.assign(proxy, {
getScrollTarget: () => targetRef.value,
getScroll,
getScrollPosition: () => ({
top: scroll.vertical.position.value,
left: scroll.horizontal.position.value
}),
getScrollPercentage: () => ({
top: scroll.vertical.percentage.value,
left: scroll.horizontal.percentage.value
}),
setScrollPosition: localSetScrollPosition,
setScrollPercentage(axis, percentage, duration) {
localSetScrollPosition(
axis,
percentage *
(scroll[axis].size.value - container[axis].value) *
(axis === 'horizontal' && proxy.$q.lang.rtl === true ? -1 : 1),
duration
)
}
})
const store = {
scroll,
thumbVertDir: [
[
TouchPan,
e => {
onPanThumb(e, 'vertical')
},
void 0,
{ vertical: true, ...panOpts }
]
],
thumbHorizDir: [
[
TouchPan,
e => {
onPanThumb(e, 'horizontal')
},
void 0,
{ horizontal: true, ...panOpts }
]
],
onVerticalMousedown(evt) {
onMousedown(evt, 'vertical')
},
onHorizontalMousedown(evt) {
onMousedown(evt, 'horizontal')
}
}
return () =>
h(
'div',
{
class: classes.value,
onMouseenter,
onMouseleave
},
[
h(
'div',
{
ref: targetRef,
class:
'q-scrollarea__container scroll relative-position fit hide-scrollbar',
tabindex: props.tabindex !== void 0 ? props.tabindex : void 0
},
[
h(
'div',
{
class: 'q-scrollarea__content absolute',
style: mainStyle.value
},
hMergeSlot(slots.default, [
h(QResizeObserver, {
debounce: 0,
onResize: updateScrollSize
})
])
),
h(QScrollObserver, {
axis: 'both',
onScroll: updateScroll
})
]
),
h(QResizeObserver, {
debounce: 0,
onResize: updateContainer
}),
h(ScrollAreaControls, {
store,
barStyle: props.barStyle,
verticalBarStyle: props.verticalBarStyle,
horizontalBarStyle: props.horizontalBarStyle
})
]
)
}
})