UNPKG

quasar

Version:

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

380 lines (306 loc) 10.6 kB
import { h, ref, computed, Transition, onBeforeUnmount, withDirectives, getCurrentInstance } from 'vue' import QIcon from '../icon/QIcon.js' import QSpinner from '../spinner/QSpinner.js' import Ripple from '../../directives/Ripple.js' import useBtn, { useBtnProps } from './use-btn.js' import { createComponent } from '../../utils/private/create.js' import { hMergeSlot } from '../../utils/private/render.js' import { stop, prevent, stopAndPrevent, listenOpts } from '../../utils/event.js' import { isKeyCode } from '../../utils/private/key-composition.js' const { passiveCapture } = listenOpts let touchTarget = null, keyboardTarget = null, mouseTarget = null export default createComponent({ name: 'QBtn', props: { ...useBtnProps, percentage: Number, darkPercentage: Boolean, onTouchstart: [ Function, Array ] }, emits: [ 'click', 'keydown', 'mousedown', 'keyup' ], setup (props, { slots, emit }) { const { proxy } = getCurrentInstance() const { classes, style, innerClasses, attributes, hasLink, linkTag, navigateOnClick, isActionable } = useBtn(props) const rootRef = ref(null) const blurTargetRef = ref(null) let localTouchTargetEl = null, avoidMouseRipple, mouseTimer = null const hasLabel = computed(() => props.label !== void 0 && props.label !== null && props.label !== '' ) const ripple = computed(() => ( props.disable === true || props.ripple === false ? false : { keyCodes: hasLink.value === true ? [ 13, 32 ] : [ 13 ], ...(props.ripple === true ? {} : props.ripple) } )) const rippleProps = computed(() => ({ center: props.round })) const percentageStyle = computed(() => { const val = Math.max(0, Math.min(100, props.percentage)) return val > 0 ? { transition: 'transform 0.6s', transform: `translateX(${ val - 100 }%)` } : {} }) const onEvents = computed(() => { if (props.loading === true) { return { onMousedown: onLoadingEvt, onTouchstart: onLoadingEvt, onClick: onLoadingEvt, onKeydown: onLoadingEvt, onKeyup: onLoadingEvt } } if (isActionable.value === true) { const acc = { onClick, onKeydown, onMousedown } if (proxy.$q.platform.has.touch === true) { const suffix = props.onTouchstart !== void 0 ? '' : 'Passive' acc[ `onTouchstart${ suffix }` ] = onTouchstart } return acc } return { // needed; especially for disabled <a> tags onClick: stopAndPrevent } }) const nodeProps = computed(() => ({ ref: rootRef, class: 'q-btn q-btn-item non-selectable no-outline ' + classes.value, style: style.value, ...attributes.value, ...onEvents.value })) function onClick (e) { // is it already destroyed? if (rootRef.value === null) { return } if (e !== void 0) { if (e.defaultPrevented === true) { return } const el = document.activeElement // focus button if it came from ENTER on form // prevent the new submit (already done) if ( props.type === 'submit' && el !== document.body && rootRef.value.contains(el) === false // required for iOS and desktop Safari && el.contains(rootRef.value) === false ) { rootRef.value.focus() const onClickCleanup = () => { document.removeEventListener('keydown', stopAndPrevent, true) document.removeEventListener('keyup', onClickCleanup, passiveCapture) rootRef.value !== null && rootRef.value.removeEventListener('blur', onClickCleanup, passiveCapture) } document.addEventListener('keydown', stopAndPrevent, true) document.addEventListener('keyup', onClickCleanup, passiveCapture) rootRef.value.addEventListener('blur', onClickCleanup, passiveCapture) } } navigateOnClick(e) } function onKeydown (e) { // is it already destroyed? if (rootRef.value === null) { return } emit('keydown', e) if (isKeyCode(e, [ 13, 32 ]) === true && keyboardTarget !== rootRef.value) { keyboardTarget !== null && cleanup() if (e.defaultPrevented !== true) { // focus external button if the focus helper was focused before rootRef.value.focus() keyboardTarget = rootRef.value rootRef.value.classList.add('q-btn--active') document.addEventListener('keyup', onPressEnd, true) rootRef.value.addEventListener('blur', onPressEnd, passiveCapture) } stopAndPrevent(e) } } function onTouchstart (e) { // is it already destroyed? if (rootRef.value === null) { return } emit('touchstart', e) if (e.defaultPrevented === true) { return } if (touchTarget !== rootRef.value) { touchTarget !== null && cleanup() touchTarget = rootRef.value localTouchTargetEl = e.target localTouchTargetEl.addEventListener('touchcancel', onPressEnd, passiveCapture) localTouchTargetEl.addEventListener('touchend', onPressEnd, passiveCapture) } // avoid duplicated mousedown event // triggering another early ripple avoidMouseRipple = true mouseTimer !== null && clearTimeout(mouseTimer) mouseTimer = setTimeout(() => { mouseTimer = null avoidMouseRipple = false }, 200) } function onMousedown (e) { // is it already destroyed? if (rootRef.value === null) { return } e.qSkipRipple = avoidMouseRipple === true emit('mousedown', e) if (e.defaultPrevented !== true && mouseTarget !== rootRef.value) { mouseTarget !== null && cleanup() mouseTarget = rootRef.value rootRef.value.classList.add('q-btn--active') document.addEventListener('mouseup', onPressEnd, passiveCapture) } } function onPressEnd (e) { // is it already destroyed? if (rootRef.value === null) { return } // needed for IE (because it emits blur when focusing button from focus helper) if (e !== void 0 && e.type === 'blur' && document.activeElement === rootRef.value) { return } if (e !== void 0 && e.type === 'keyup') { if (keyboardTarget === rootRef.value && isKeyCode(e, [ 13, 32 ]) === true) { // for click trigger const evt = new MouseEvent('click', e) evt.qKeyEvent = true e.defaultPrevented === true && prevent(evt) e.cancelBubble === true && stop(evt) rootRef.value.dispatchEvent(evt) stopAndPrevent(e) // for ripple e.qKeyEvent = true } emit('keyup', e) } cleanup() } function cleanup (destroying) { const blurTarget = blurTargetRef.value if ( destroying !== true && (touchTarget === rootRef.value || mouseTarget === rootRef.value) && blurTarget !== null && blurTarget !== document.activeElement ) { blurTarget.setAttribute('tabindex', -1) blurTarget.focus() } if (touchTarget === rootRef.value) { if (localTouchTargetEl !== null) { localTouchTargetEl.removeEventListener('touchcancel', onPressEnd, passiveCapture) localTouchTargetEl.removeEventListener('touchend', onPressEnd, passiveCapture) } touchTarget = localTouchTargetEl = null } if (mouseTarget === rootRef.value) { document.removeEventListener('mouseup', onPressEnd, passiveCapture) mouseTarget = null } if (keyboardTarget === rootRef.value) { document.removeEventListener('keyup', onPressEnd, true) rootRef.value !== null && rootRef.value.removeEventListener('blur', onPressEnd, passiveCapture) keyboardTarget = null } rootRef.value !== null && rootRef.value.classList.remove('q-btn--active') } function onLoadingEvt (evt) { stopAndPrevent(evt) evt.qSkipRipple = true } onBeforeUnmount(() => { cleanup(true) }) // expose public methods Object.assign(proxy, { click: onClick }) return () => { let inner = [] props.icon !== void 0 && inner.push( h(QIcon, { name: props.icon, left: props.stack === false && hasLabel.value === true, role: 'img', 'aria-hidden': 'true' }) ) hasLabel.value === true && inner.push( h('span', { class: 'block' }, [ props.label ]) ) inner = hMergeSlot(slots.default, inner) if (props.iconRight !== void 0 && props.round === false) { inner.push( h(QIcon, { name: props.iconRight, right: props.stack === false && hasLabel.value === true, role: 'img', 'aria-hidden': 'true' }) ) } const child = [ h('span', { class: 'q-focus-helper', ref: blurTargetRef }) ] if (props.loading === true && props.percentage !== void 0) { child.push( h('span', { class: 'q-btn__progress absolute-full overflow-hidden' + (props.darkPercentage === true ? ' q-btn__progress--dark' : '') }, [ h('span', { class: 'q-btn__progress-indicator fit block', style: percentageStyle.value }) ]) ) } child.push( h('span', { class: 'q-btn__content text-center col items-center q-anchor--skip ' + innerClasses.value }, inner) ) props.loading !== null && child.push( h(Transition, { name: 'q-transition--fade' }, () => ( props.loading === true ? [ h('span', { key: 'loading', class: 'absolute-full flex flex-center' }, slots.loading !== void 0 ? slots.loading() : [ h(QSpinner) ]) ] : null )) ) return withDirectives( h( linkTag.value, nodeProps.value, child ), [ [ Ripple, ripple.value, void 0, rippleProps.value ] ] ) } } })