UNPKG

quasar

Version:

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

595 lines (509 loc) 15.6 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 } 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 }) ] ) } })