UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

510 lines (424 loc) 14.8 kB
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 } 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 } 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 timer !== null && clearTimeout(timer) timer = setTimeout(() => { timer = null tempShowing.value = false }, props.delay) 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 () => { 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 }) ]) } } })