UNPKG

quasar

Version:

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

313 lines (263 loc) 7.75 kB
/* * Inspired by RouterLink from Vue Router * --> API should match! */ import { computed, getCurrentInstance } from 'vue' import { vmHasRouter } from '../../utils/private.vm/vm.js' // Get the original path value of a record by following its aliasOf function getOriginalPath(record) { return record ? (record.aliasOf ? record.aliasOf.path : record.path) : '' } function isSameRouteRecord(a, b) { // since the original record has an undefined value for aliasOf // but all aliases point to the original record, this will always compare // the original record return (a.aliasOf || a) === (b.aliasOf || b) } function includesParams(outer, inner) { for (const key in inner) { const innerValue = inner[key], outerValue = outer[key] if (typeof innerValue === 'string') { if (innerValue !== outerValue) { return false } } else if ( Array.isArray(outerValue) === false || outerValue.length !== innerValue.length || innerValue.some((value, i) => value !== outerValue[i]) ) { return false } } return true } function isEquivalentArray(a, b) { return Array.isArray(b) === true ? a.length === b.length && a.every((value, i) => value === b[i]) : a.length === 1 && a[0] === b } function isSameRouteLocationParamsValue(a, b) { return Array.isArray(a) === true ? isEquivalentArray(a, b) : Array.isArray(b) === true ? isEquivalentArray(b, a) : a === b } function isSameRouteLocationParams(a, b) { if (Object.keys(a).length !== Object.keys(b).length) { return false } for (const key in a) { if (isSameRouteLocationParamsValue(a[key], b[key]) === false) { return false } } return true } export const useRouterLinkNonMatchingProps = { // router-link to: [String, Object], replace: Boolean, // regular <a> link href: String, target: String, // state disable: Boolean } export const useRouterLinkProps = { ...useRouterLinkNonMatchingProps, // router-link exact: Boolean, activeClass: { type: String, default: 'q-router-link--active' }, exactActiveClass: { type: String, default: 'q-router-link--exact-active' } } // external props: type, tag export default function useRouterLink({ fallbackTag, useDisableForRouterLinkProps = true } = {}) { const vm = getCurrentInstance() const { props, proxy, emit } = vm const hasRouter = vmHasRouter(vm) const hasHrefLink = computed( () => props.disable !== true && props.href !== void 0 ) // for perf reasons, we use minimum amount of runtime work const hasRouterLinkProps = useDisableForRouterLinkProps === true ? computed( () => hasRouter === true && props.disable !== true && hasHrefLink.value !== true && props.to !== void 0 && props.to !== null && props.to !== '' ) : computed( () => hasRouter === true && hasHrefLink.value !== true && props.to !== void 0 && props.to !== null && props.to !== '' ) const resolvedLink = computed(() => hasRouterLinkProps.value === true ? getLink(props.to) : null ) const hasRouterLink = computed(() => resolvedLink.value !== null) const hasLink = computed( () => hasHrefLink.value === true || hasRouterLink.value === true ) const linkTag = computed(() => props.type === 'a' || hasLink.value === true ? 'a' : props.tag || fallbackTag || 'div' ) const linkAttrs = computed(() => hasHrefLink.value === true ? { href: props.href, target: props.target } : hasRouterLink.value === true ? { href: resolvedLink.value.href, target: props.target } : {} ) const linkActiveIndex = computed(() => { if (hasRouterLink.value === false) { return -1 } const { matched } = resolvedLink.value, { length } = matched, routeMatched = matched[length - 1] if (routeMatched === void 0) { return -1 } const currentMatched = proxy.$route.matched if (currentMatched.length === 0) { return -1 } const index = currentMatched.findIndex( isSameRouteRecord.bind(null, routeMatched) ) if (index !== -1) { return index } // possible parent record const parentRecordPath = getOriginalPath(matched[length - 2]) return ( // we are dealing with nested routes length > 1 && // if the parent and matched route have the same path, this link is // referring to the empty child. Or we currently are on a different // child of the same parent getOriginalPath(routeMatched) === parentRecordPath && // avoid comparing the child with its parent currentMatched[currentMatched.length - 1].path !== parentRecordPath ? currentMatched.findIndex( isSameRouteRecord.bind(null, matched[length - 2]) ) : index ) }) const linkIsActive = computed( () => hasRouterLink.value === true && linkActiveIndex.value !== -1 && includesParams(proxy.$route.params, resolvedLink.value.params) ) const linkIsExactActive = computed( () => linkIsActive.value === true && linkActiveIndex.value === proxy.$route.matched.length - 1 && isSameRouteLocationParams(proxy.$route.params, resolvedLink.value.params) ) const linkClass = computed(() => hasRouterLink.value === true ? linkIsExactActive.value === true ? ` ${props.exactActiveClass} ${props.activeClass}` : props.exact === true ? '' : linkIsActive.value === true ? ` ${props.activeClass}` : '' : '' ) function getLink(to) { try { return proxy.$router.resolve(to) } catch {} return null } /** * @returns Promise<RouterError | false | undefined> */ function navigateToRouterLink( e, { returnRouterError, to = props.to, replace = props.replace } = {} ) { if (props.disable === true) { // ensure native navigation is prevented in all cases, // like when useDisableForRouterLinkProps === false (QRouteTab) e.preventDefault() return Promise.resolve(false) } if ( // don't redirect with control keys; // should match RouterLink from Vue Router e.metaKey || e.altKey || e.ctrlKey || e.shiftKey || // don't redirect on right click (e.button !== void 0 && e.button !== 0) || // don't redirect if it should open in a new window props.target === '_blank' ) { return Promise.resolve(false) } // hinder the native navigation e.preventDefault() // then() can also return a "soft" router error (Vue Router behavior) const promise = proxy.$router[replace === true ? 'replace' : 'push'](to) return returnRouterError === true ? promise : // else catching hard errors and also "soft" ones - then(err => ...) promise.then(() => {}).catch(() => {}) } // warning! ensure that the component using it has 'click' included in its 'emits' definition prop function navigateOnClick(e) { if (hasRouterLink.value === true) { const go = opts => navigateToRouterLink(e, opts) emit('click', e, go) if (e.defaultPrevented !== true) go() } else { emit('click', e) } } return { hasRouterLink, hasHrefLink, hasLink, linkTag, resolvedLink, linkIsActive, linkIsExactActive, linkClass, linkAttrs, getLink, navigateToRouterLink, navigateOnClick } }