|
| 1 | +<script lang="ts"> |
| 2 | +import type { ButtonHTMLAttributes } from 'vue' |
| 3 | +import type { AppConfig } from '@nuxt/schema' |
| 4 | +import theme from '#build/ui/link' |
| 5 | +import type { ComponentConfig } from '../../types/utils' |
| 6 | +
|
| 7 | +type Link = ComponentConfig<typeof theme, AppConfig, 'link'> |
| 8 | +
|
| 9 | +interface NuxtLinkProps { |
| 10 | + /** |
| 11 | + * Route Location the link should navigate to when clicked on. |
| 12 | + */ |
| 13 | + to?: string |
| 14 | + /** |
| 15 | + * An alias for `to`. If used with `to`, `href` will be ignored |
| 16 | + */ |
| 17 | + href?: string |
| 18 | + /** |
| 19 | + * Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases |
| 20 | + */ |
| 21 | + external?: boolean |
| 22 | + /** |
| 23 | + * Where to display the linked URL, as the name for a browsing context. |
| 24 | + */ |
| 25 | + target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null |
| 26 | + /** |
| 27 | + * A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links. |
| 28 | + */ |
| 29 | + rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null |
| 30 | + /** |
| 31 | + * If set to true, no rel attribute will be added to the link |
| 32 | + */ |
| 33 | + noRel?: boolean |
| 34 | + /** |
| 35 | + * A class to apply to links that have been prefetched. |
| 36 | + */ |
| 37 | + prefetchedClass?: string |
| 38 | + /** |
| 39 | + * When enabled will prefetch middleware, layouts and payloads of links in the viewport. |
| 40 | + */ |
| 41 | + prefetch?: boolean |
| 42 | + /** |
| 43 | + * Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility. |
| 44 | + */ |
| 45 | + prefetchOn?: 'visibility' | 'interaction' | Partial<{ |
| 46 | + visibility: boolean |
| 47 | + interaction: boolean |
| 48 | + }> |
| 49 | + /** |
| 50 | + * Escape hatch to disable `prefetch` attribute. |
| 51 | + */ |
| 52 | + noPrefetch?: boolean |
| 53 | + /** |
| 54 | + * Allows passing additional attributes to the actual rendered link. |
| 55 | + */ |
| 56 | + linkAttrs?: Record<string, any> |
| 57 | + ariaCurrentValue?: string |
| 58 | +} |
| 59 | +
|
| 60 | +export interface LinkProps extends NuxtLinkProps { |
| 61 | + /** |
| 62 | + * The element or component this component should render as when not a link. |
| 63 | + * @defaultValue 'button' |
| 64 | + */ |
| 65 | + as?: any |
| 66 | + /** |
| 67 | + * The type of the button when not a link. |
| 68 | + * @defaultValue 'button' |
| 69 | + */ |
| 70 | + type?: ButtonHTMLAttributes['type'] |
| 71 | + disabled?: boolean |
| 72 | + /** Force the link to be active independent of the current route. */ |
| 73 | + active?: boolean |
| 74 | + /** Will only be active if the current route is an exact match. */ |
| 75 | + exact?: boolean |
| 76 | + /** Will only be active if the current route query is an exact match. */ |
| 77 | + exactQuery?: boolean |
| 78 | + /** Will only be active if the current route hash is an exact match. */ |
| 79 | + exactHash?: boolean |
| 80 | + /** The class to apply when the link is active. */ |
| 81 | + activeClass?: string |
| 82 | + /** The class to apply when the link is inactive. */ |
| 83 | + inactiveClass?: string |
| 84 | + custom?: boolean |
| 85 | + /** When `true`, only styles from `class`, `activeClass`, and `inactiveClass` will be applied. */ |
| 86 | + raw?: boolean |
| 87 | + class?: any |
| 88 | +} |
| 89 | +
|
| 90 | +export interface LinkSlots { |
| 91 | + default(props: { active: boolean }): any |
| 92 | +} |
| 93 | +</script> |
| 94 | + |
| 95 | +<script setup lang="ts"> |
| 96 | +import { computed } from 'vue' |
| 97 | +import { defu } from 'defu' |
| 98 | +import { hasProtocol } from 'ufo' |
| 99 | +import { useAppConfig } from '#imports' |
| 100 | +import { tv } from '../../utils/tv' |
| 101 | +import ULinkBase from '../../components/LinkBase.vue' |
| 102 | +
|
| 103 | +defineOptions({ inheritAttrs: false }) |
| 104 | +
|
| 105 | +const props = withDefaults(defineProps<LinkProps>(), { |
| 106 | + as: 'button', |
| 107 | + type: 'button', |
| 108 | + active: undefined, |
| 109 | + activeClass: '', |
| 110 | + inactiveClass: '' |
| 111 | +}) |
| 112 | +defineSlots<LinkSlots>() |
| 113 | +
|
| 114 | +const appConfig = useAppConfig() as Link['AppConfig'] |
| 115 | +
|
| 116 | +const ui = computed(() => tv({ |
| 117 | + extend: tv(theme), |
| 118 | + ...defu({ |
| 119 | + variants: { |
| 120 | + active: { |
| 121 | + true: props.activeClass, |
| 122 | + false: props.inactiveClass |
| 123 | + } |
| 124 | + } |
| 125 | + }, appConfig.ui?.link || {}) |
| 126 | +})) |
| 127 | +
|
| 128 | +const to = computed(() => props.to ?? props.href) |
| 129 | +
|
| 130 | +const isExternal = computed(() => { |
| 131 | + if (props.external) { |
| 132 | + return true |
| 133 | + } |
| 134 | +
|
| 135 | + if (!to.value) { |
| 136 | + return false |
| 137 | + } |
| 138 | +
|
| 139 | + return typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }) |
| 140 | +}) |
| 141 | +
|
| 142 | +const active = computed(() => { |
| 143 | + if (props.active !== undefined) { |
| 144 | + return props.active |
| 145 | + } |
| 146 | +
|
| 147 | + // Without router, we can't determine if link is active |
| 148 | + return false |
| 149 | +}) |
| 150 | +
|
| 151 | +const rel = computed(() => { |
| 152 | + if (props.noRel) { |
| 153 | + return undefined |
| 154 | + } |
| 155 | +
|
| 156 | + if (props.rel) { |
| 157 | + return props.rel |
| 158 | + } |
| 159 | +
|
| 160 | + if (isExternal.value) { |
| 161 | + return 'noopener noreferrer' |
| 162 | + } |
| 163 | +
|
| 164 | + return undefined |
| 165 | +}) |
| 166 | +
|
| 167 | +function resolveLinkClass() { |
| 168 | + if (props.raw) { |
| 169 | + return [props.class, active.value ? props.activeClass : props.inactiveClass] |
| 170 | + } |
| 171 | +
|
| 172 | + return ui.value({ |
| 173 | + active: active.value, |
| 174 | + disabled: !!props.disabled, |
| 175 | + class: [props.class] |
| 176 | + }) |
| 177 | +} |
| 178 | +</script> |
| 179 | + |
| 180 | +<template> |
| 181 | + <template v-if="custom"> |
| 182 | + <slot |
| 183 | + v-bind="{ |
| 184 | + ...$attrs, |
| 185 | + as, |
| 186 | + type, |
| 187 | + disabled, |
| 188 | + href: to, |
| 189 | + rel, |
| 190 | + target: isExternal ? '_blank' : props.target, |
| 191 | + isExternal, |
| 192 | + active |
| 193 | + }" |
| 194 | + /> |
| 195 | + </template> |
| 196 | + <ULinkBase |
| 197 | + v-else |
| 198 | + v-bind="{ |
| 199 | + ...$attrs, |
| 200 | + as: to && !isExternal && !disabled ? 'a' : as, |
| 201 | + type, |
| 202 | + disabled, |
| 203 | + href: to, |
| 204 | + rel, |
| 205 | + target: isExternal ? '_blank' : props.target, |
| 206 | + isExternal |
| 207 | + }" |
| 208 | + :class="resolveLinkClass()" |
| 209 | + > |
| 210 | + <slot :active="active" /> |
| 211 | + </ULinkBase> |
| 212 | +</template> |
0 commit comments