UNPKG

quasar

Version:

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

487 lines (406 loc) 13.9 kB
import { h, ref, computed, watch, withDirectives, onActivated, onDeactivated, onBeforeUnmount, getCurrentInstance } from 'vue' import useDark, { useDarkProps } from '../../composables/private/use-dark.js' import QResizeObserver from '../resize-observer/QResizeObserver.js' import QScrollObserver from '../scroll-observer/QScrollObserver.js' import TouchPan from '../../directives/TouchPan.js' import { createComponent } from '../../utils/private/create.js' import { between } from '../../utils/format.js' import { setVerticalScrollPosition, setHorizontalScrollPosition } from '../../utils/scroll.js' import { hMergeSlot } from '../../utils/private/render.js' import debounce from '../../utils/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 ], 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' : '') ) 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(() => scroll.vertical.percentage.value * (container.vertical.value - scroll.vertical.thumbSize.value) ) scroll.vertical.thumbSize = computed(() => Math.round( between( container.vertical.value * container.vertical.value / scroll.vertical.size.value, getMinThumbSize(container.vertical.value), container.vertical.value ) ) ) scroll.vertical.style = computed(() => { return { ...props.thumbStyle, ...props.verticalThumbStyle, top: `${ scroll.vertical.thumbStart.value }px`, height: `${ scroll.vertical.thumbSize.value }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(() => scroll.horizontal.percentage.value * (container.horizontal.value - scroll.horizontal.thumbSize.value) ) scroll.horizontal.thumbSize = computed(() => Math.round( between( container.horizontal.value * container.horizontal.value / scroll.horizontal.size.value, getMinThumbSize(container.horizontal.value), container.horizontal.value ) ) ) scroll.horizontal.style = computed(() => { return { ...props.thumbStyle, ...props.horizontalThumbStyle, [ proxy.$q.lang.rtl === true ? 'right' : 'left' ]: `${ scroll.horizontal.thumbStart.value }px`, width: `${ scroll.horizontal.thumbSize.value }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 )) const thumbVertDir = [ [ TouchPan, e => { onPanThumb(e, 'vertical') }, void 0, { vertical: true, ...panOpts } ] ] const thumbHorizDir = [ [ TouchPan, e => { onPanThumb(e, 'horizontal') }, void 0, { horizontal: true, ...panOpts } ] ] function getScroll () { const info = {} axisList.forEach(axis => { const data = scroll[ axis ] info[ axis + 'Position' ] = data.position.value info[ axis + 'Percentage' ] = data.percentage.value info[ axis + 'Size' ] = data.size.value info[ axis + 'ContainerSize' ] = container[ axis ].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 containerSize = container[ axis ].value const multiplier = (data.size.value - containerSize) / (containerSize - 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 offset = evt[ dirProps[ axis ].offset ] if (offset < data.thumbStart.value || offset > data.thumbStart.value + data.thumbSize.value) { const pos = offset - data.thumbSize.value / 2 setScroll(pos / container[ axis ].value * data.size.value, axis) } // activate thumb pan if (data.ref.value !== null) { data.ref.value.dispatchEvent(new MouseEvent(evt.type, evt)) } } } function onVerticalMousedown (evt) { onMousedown(evt, 'vertical') } function onHorizontalMousedown (evt) { onMousedown(evt, 'horizontal') } 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 } function onMouseenter () { hover.value = true } function onMouseleave () { 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 ) } }) 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('div', { class: scroll.vertical.barClass.value, style: [ props.barStyle, props.verticalBarStyle ], 'aria-hidden': 'true', onMousedown: onVerticalMousedown }), h('div', { class: scroll.horizontal.barClass.value, style: [ props.barStyle, props.horizontalBarStyle ], 'aria-hidden': 'true', onMousedown: onHorizontalMousedown }), withDirectives( h('div', { ref: scroll.vertical.ref, class: scroll.vertical.thumbClass.value, style: scroll.vertical.style.value, 'aria-hidden': 'true' }), thumbVertDir ), withDirectives( h('div', { ref: scroll.horizontal.ref, class: scroll.horizontal.thumbClass.value, style: scroll.horizontal.style.value, 'aria-hidden': 'true' }), thumbHorizDir ) ]) } } })