UNPKG

quasar

Version:

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

377 lines (314 loc) 10.5 kB
import { h, ref, computed, watch, Transition, onBeforeUnmount, getCurrentInstance } from 'vue' import useAnchor, { useAnchorProps } from '../../composables/private.use-anchor/use-anchor.js' import useScrollTarget from '../../composables/private.use-scroll-target/use-scroll-target.js' import useModelToggle, { useModelToggleProps, useModelToggleEmits } from '../../composables/private.use-model-toggle/use-model-toggle.js' import useDark, { useDarkProps } from '../../composables/private.use-dark/use-dark.js' import usePortal from '../../composables/private.use-portal/use-portal.js' import useTransition, { useTransitionProps } from '../../composables/private.use-transition/use-transition.js' import useTick from '../../composables/use-tick/use-tick.js' import useTimeout from '../../composables/use-timeout/use-timeout.js' import { createComponent } from '../../utils/private.create/create.js' import { closePortalMenus } from '../../utils/private.portal/portal.js' import { getScrollTarget, scrollTargetProp } from '../../utils/scroll/scroll.js' import { position, stopAndPrevent } from '../../utils/event/event.js' import { hSlot } from '../../utils/private.render/render.js' import { addEscapeKey, removeEscapeKey } from '../../utils/private.keyboard/escape-key.js' import { addFocusout, removeFocusout } from '../../utils/private.focus/focusout.js' import { childHasFocus } from '../../utils/dom/dom.js' import { addClickOutside, removeClickOutside } from '../../utils/private.click-outside/click-outside.js' import { addFocusFn } from '../../utils/private.focus/focus-manager.js' import { validatePosition, validateOffset, setPosition, parsePosition } from '../../utils/private.position-engine/position-engine.js' export default createComponent({ name: 'QMenu', inheritAttrs: false, props: { ...useAnchorProps, ...useModelToggleProps, ...useDarkProps, ...useTransitionProps, persistent: Boolean, autoClose: Boolean, separateClosePopup: Boolean, noRouteDismiss: Boolean, noRefocus: Boolean, noFocus: Boolean, fit: Boolean, cover: Boolean, square: Boolean, anchor: { type: String, validator: validatePosition }, self: { type: String, validator: validatePosition }, offset: { type: Array, validator: validateOffset }, scrollTarget: scrollTargetProp, touchPosition: Boolean, maxHeight: { type: String, default: null }, maxWidth: { type: String, default: null } }, emits: [ ...useModelToggleEmits, 'click', 'escapeKey' ], setup (props, { slots, emit, attrs }) { let refocusTarget = null, absoluteOffset, unwatchPosition, avoidAutoClose const vm = getCurrentInstance() const { proxy } = vm const { $q } = proxy const innerRef = ref(null) const showing = ref(false) const hideOnRouteChange = computed(() => props.persistent !== true && props.noRouteDismiss !== true ) const isDark = useDark(props, $q) const { registerTick, removeTick } = useTick() const { registerTimeout } = useTimeout() const { transitionProps, transitionStyle } = useTransition(props) const { localScrollTarget, changeScrollEvent, unconfigureScrollTarget } = useScrollTarget(props, configureScrollTarget) const { anchorEl, canShow } = useAnchor({ showing }) const { hide } = useModelToggle({ showing, canShow, handleShow, handleHide, hideOnRouteChange, processOnMount: true }) const { showPortal, hidePortal, renderPortal } = usePortal(vm, innerRef, renderPortalContent, 'menu') const clickOutsideProps = { anchorEl, innerRef, onClickOutside (e) { if (props.persistent !== true && showing.value === true) { hide(e) if ( // always prevent touch event e.type === 'touchstart' // prevent click if it's on a dialog backdrop || e.target.classList.contains('q-dialog__backdrop') ) { stopAndPrevent(e) } return true } } } const anchorOrigin = computed(() => parsePosition( props.anchor || ( props.cover === true ? 'center middle' : 'bottom start' ), $q.lang.rtl ) ) const selfOrigin = computed(() => ( props.cover === true ? anchorOrigin.value : parsePosition(props.self || 'top start', $q.lang.rtl) )) const menuClass = computed(() => (props.square === true ? ' q-menu--square' : '') + (isDark.value === true ? ' q-menu--dark q-dark' : '') ) const onEvents = computed(() => ( props.autoClose === true ? { onClick: onAutoClose } : {} )) const handlesFocus = computed(() => showing.value === true && props.persistent !== true ) watch(handlesFocus, val => { if (val === true) { addEscapeKey(onEscapeKey) addClickOutside(clickOutsideProps) } else { removeEscapeKey(onEscapeKey) removeClickOutside(clickOutsideProps) } }) function focus () { addFocusFn(() => { let node = innerRef.value if (node && node.contains(document.activeElement) !== true) { node = node.querySelector('[autofocus][tabindex], [data-autofocus][tabindex]') || node.querySelector('[autofocus] [tabindex], [data-autofocus] [tabindex]') || node.querySelector('[autofocus], [data-autofocus]') || node node.focus({ preventScroll: true }) } }) } function handleShow (evt) { refocusTarget = props.noRefocus === false ? document.activeElement : null addFocusout(onFocusout) showPortal() configureScrollTarget() absoluteOffset = void 0 if (evt !== void 0 && (props.touchPosition || props.contextMenu)) { const pos = position(evt) if (pos.left !== void 0) { const { top, left } = anchorEl.value.getBoundingClientRect() absoluteOffset = { left: pos.left - left, top: pos.top - top } } } if (unwatchPosition === void 0) { unwatchPosition = watch( () => $q.screen.width + '|' + $q.screen.height + '|' + props.self + '|' + props.anchor + '|' + $q.lang.rtl, updatePosition ) } if (props.noFocus !== true) { document.activeElement.blur() } // should removeTick() if this gets removed registerTick(() => { updatePosition() props.noFocus !== true && focus() }) // should removeTimeout() if this gets removed registerTimeout(() => { // required in order to avoid the "double-tap needed" issue if ($q.platform.is.ios === true) { // if auto-close, then this click should // not close the menu avoidAutoClose = props.autoClose innerRef.value.click() } updatePosition() showPortal(true) // done showing portal emit('show', evt) }, props.transitionDuration) } function handleHide (evt) { removeTick() hidePortal() anchorCleanup(true) if ( refocusTarget !== null && ( // menu was hidden from code or ESC plugin evt === void 0 // menu was not closed from a mouse or touch clickOutside || evt.qClickOutside !== true ) ) { ((evt && evt.type.indexOf('key') === 0 ? refocusTarget.closest('[tabindex]:not([tabindex^="-"])') : void 0 ) || refocusTarget).focus() refocusTarget = null } // should removeTimeout() if this gets removed registerTimeout(() => { hidePortal(true) // done hiding, now destroy emit('hide', evt) }, props.transitionDuration) } function anchorCleanup (hiding) { absoluteOffset = void 0 if (unwatchPosition !== void 0) { unwatchPosition() unwatchPosition = void 0 } if (hiding === true || showing.value === true) { removeFocusout(onFocusout) unconfigureScrollTarget() removeClickOutside(clickOutsideProps) removeEscapeKey(onEscapeKey) } if (hiding !== true) { refocusTarget = null } } function configureScrollTarget () { if (anchorEl.value !== null || props.scrollTarget !== void 0) { localScrollTarget.value = getScrollTarget(anchorEl.value, props.scrollTarget) changeScrollEvent(localScrollTarget.value, updatePosition) } } function onAutoClose (e) { // if auto-close, then the ios double-tap fix which // issues a click should not close the menu if (avoidAutoClose !== true) { closePortalMenus(proxy, e) emit('click', e) } else { avoidAutoClose = false } } function onFocusout (evt) { // the focus is not in a vue child component if ( handlesFocus.value === true && props.noFocus !== true && childHasFocus(innerRef.value, evt.target) !== true ) { focus() } } function onEscapeKey (evt) { emit('escapeKey') hide(evt) } function updatePosition () { setPosition({ targetEl: innerRef.value, offset: props.offset, anchorEl: anchorEl.value, anchorOrigin: anchorOrigin.value, selfOrigin: selfOrigin.value, absoluteOffset, fit: props.fit, cover: props.cover, maxHeight: props.maxHeight, maxWidth: props.maxWidth }) } function renderPortalContent () { return h( Transition, transitionProps.value, () => ( showing.value === true ? h('div', { role: 'menu', ...attrs, ref: innerRef, tabindex: -1, class: [ 'q-menu q-position-engine scroll' + menuClass.value, attrs.class ], style: [ attrs.style, transitionStyle.value ], ...onEvents.value }, hSlot(slots.default)) : null ) ) } onBeforeUnmount(anchorCleanup) // expose public methods Object.assign(proxy, { focus, updatePosition }) return renderPortal } })