quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
305 lines (248 loc) • 8.51 kB
JavaScript
import { h, ref, computed, watch, onBeforeUnmount, Transition, getCurrentInstance } from 'vue'
import useAnchor, { useAnchorProps } from '../../composables/private/use-anchor.js'
import useScrollTarget from '../../composables/private/use-scroll-target.js'
import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private/use-model-toggle.js'
import usePortal from '../../composables/private/use-portal.js'
import useTransition, { useTransitionProps } from '../../composables/private/use-transition.js'
import useTick from '../../composables/private/use-tick.js'
import useTimeout from '../../composables/private/use-timeout.js'
import { createComponent } from '../../utils/private/create.js'
import { getScrollTarget } from '../../utils/scroll.js'
import { stopAndPrevent, addEvt, cleanEvt } from '../../utils/event.js'
import { clearSelection } from '../../utils/private/selection.js'
import { hSlot } from '../../utils/private/render.js'
import { addClickOutside, removeClickOutside } from '../../utils/private/click-outside.js'
import {
validatePosition, validateOffset, setPosition, parsePosition
} from '../../utils/private/position-engine.js'
export default createComponent({
name: 'QTooltip',
inheritAttrs: false,
props: {
...useAnchorProps,
...useModelToggleProps,
...useTransitionProps,
maxHeight: {
type: String,
default: null
},
maxWidth: {
type: String,
default: null
},
transitionShow: {
default: 'jump-down'
},
transitionHide: {
default: 'jump-up'
},
anchor: {
type: String,
default: 'bottom middle',
validator: validatePosition
},
self: {
type: String,
default: 'top middle',
validator: validatePosition
},
offset: {
type: Array,
default: () => [ 14, 14 ],
validator: validateOffset
},
scrollTarget: {
default: void 0
},
delay: {
type: Number,
default: 0
},
hideDelay: {
type: Number,
default: 0
}
},
emits: [
...useModelToggleEmits
],
setup (props, { slots, emit, attrs }) {
let unwatchPosition, observer
const vm = getCurrentInstance()
const { proxy: { $q } } = vm
const innerRef = ref(null)
const showing = ref(false)
const anchorOrigin = computed(() => parsePosition(props.anchor, $q.lang.rtl))
const selfOrigin = computed(() => parsePosition(props.self, $q.lang.rtl))
const hideOnRouteChange = computed(() => props.persistent !== true)
const { registerTick, removeTick } = useTick()
const { registerTimeout } = useTimeout()
const { transitionProps, transitionStyle } = useTransition(props)
const { localScrollTarget, changeScrollEvent, unconfigureScrollTarget } = useScrollTarget(props, configureScrollTarget)
const { anchorEl, canShow, anchorEvents } = useAnchor({ showing, configureAnchorEl })
const { show, hide } = useModelToggle({
showing, canShow, handleShow, handleHide,
hideOnRouteChange,
processOnMount: true
})
Object.assign(anchorEvents, { delayShow, delayHide })
const { showPortal, hidePortal, renderPortal } = usePortal(vm, innerRef, renderPortalContent, 'tooltip')
// if we're on mobile, let's improve the experience
// by closing it when user taps outside of it
if ($q.platform.is.mobile === true) {
const clickOutsideProps = {
anchorEl,
innerRef,
onClickOutside (e) {
hide(e)
// prevent click if it's on a dialog backdrop
if (e.target.classList.contains('q-dialog__backdrop')) {
stopAndPrevent(e)
}
return true
}
}
const hasClickOutside = computed(() =>
// it doesn't has external model
// (null is the default value)
props.modelValue === null
// and it's not persistent
&& props.persistent !== true
&& showing.value === true
)
watch(hasClickOutside, val => {
const fn = val === true ? addClickOutside : removeClickOutside
fn(clickOutsideProps)
})
onBeforeUnmount(() => {
removeClickOutside(clickOutsideProps)
})
}
function handleShow (evt) {
showPortal()
// should removeTick() if this gets removed
registerTick(() => {
observer = new MutationObserver(() => updatePosition())
observer.observe(innerRef.value, { attributes: false, childList: true, characterData: true, subtree: true })
updatePosition()
configureScrollTarget()
})
if (unwatchPosition === void 0) {
unwatchPosition = watch(
() => $q.screen.width + '|' + $q.screen.height + '|' + props.self + '|' + props.anchor + '|' + $q.lang.rtl,
updatePosition
)
}
// should removeTimeout() if this gets removed
registerTimeout(() => {
showPortal(true) // done showing portal
emit('show', evt)
}, props.transitionDuration)
}
function handleHide (evt) {
removeTick()
hidePortal()
anchorCleanup()
// should removeTimeout() if this gets removed
registerTimeout(() => {
hidePortal(true) // done hiding, now destroy
emit('hide', evt)
}, props.transitionDuration)
}
function anchorCleanup () {
if (observer !== void 0) {
observer.disconnect()
observer = void 0
}
if (unwatchPosition !== void 0) {
unwatchPosition()
unwatchPosition = void 0
}
unconfigureScrollTarget()
cleanEvt(anchorEvents, 'tooltipTemp')
}
function updatePosition () {
const el = innerRef.value
if (anchorEl.value === null || !el) {
return
}
setPosition({
el,
offset: props.offset,
anchorEl: anchorEl.value,
anchorOrigin: anchorOrigin.value,
selfOrigin: selfOrigin.value,
maxHeight: props.maxHeight,
maxWidth: props.maxWidth
})
}
function delayShow (evt) {
if ($q.platform.is.mobile === true) {
clearSelection()
document.body.classList.add('non-selectable')
const target = anchorEl.value
const evts = [ 'touchmove', 'touchcancel', 'touchend', 'click' ]
.map(e => ([ target, e, 'delayHide', 'passiveCapture' ]))
addEvt(anchorEvents, 'tooltipTemp', evts)
}
registerTimeout(() => { show(evt) }, props.delay)
}
function delayHide (evt) {
if ($q.platform.is.mobile === true) {
cleanEvt(anchorEvents, 'tooltipTemp')
clearSelection()
// delay needed otherwise selection still occurs
setTimeout(() => {
document.body.classList.remove('non-selectable')
}, 10)
}
// should removeTimeout() if this gets removed
registerTimeout(() => { hide(evt) }, props.hideDelay)
}
function configureAnchorEl () {
if (props.noParentEvent === true || anchorEl.value === null) { return }
const evts = $q.platform.is.mobile === true
? [
[ anchorEl.value, 'touchstart', 'delayShow', 'passive' ]
]
: [
[ anchorEl.value, 'mouseenter', 'delayShow', 'passive' ],
[ anchorEl.value, 'mouseleave', 'delayHide', 'passive' ]
]
addEvt(anchorEvents, 'anchor', evts)
}
function configureScrollTarget () {
if (anchorEl.value !== null || props.scrollTarget !== void 0) {
localScrollTarget.value = getScrollTarget(anchorEl.value, props.scrollTarget)
const fn = props.noParentEvent === true
? updatePosition
: hide
changeScrollEvent(localScrollTarget.value, fn)
}
}
function getTooltipContent () {
return showing.value === true
? h('div', {
...attrs,
ref: innerRef,
class: [
'q-tooltip q-tooltip--style q-position-engine no-pointer-events',
attrs.class
],
style: [
attrs.style,
transitionStyle.value
],
role: 'tooltip'
}, hSlot(slots.default))
: null
}
function renderPortalContent () {
return h(Transition, transitionProps.value, getTooltipContent)
}
onBeforeUnmount(anchorCleanup)
// expose public methods
Object.assign(vm.proxy, { updatePosition })
return renderPortal
}
})