mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-21 15:31:46 +01:00
feat: module improvements
This commit is contained in:
165
src/runtime/components/elements/Avatar.vue
Normal file
165
src/runtime/components/elements/Avatar.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<span class="relative inline-flex items-center justify-center" :class="avatarClass" @click="goto">
|
||||
<img v-if="url" :src="url" :alt="alt" :class="[sizeClass, roundedClass]">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-else-if="gradientPlaceholder" class="w-full h-full overflow-hidden" :class="roundedClass" v-html="gradientPlaceholder" />
|
||||
<span
|
||||
v-else-if="placeholder"
|
||||
class="font-medium leading-none u-text-gray-900 uppercase"
|
||||
>{{ placeholder }}</span>
|
||||
<span
|
||||
v-else-if="text"
|
||||
>{{ text }}</span>
|
||||
<svg
|
||||
v-else
|
||||
class="w-full h-full u-text-gray-300"
|
||||
:class="roundedClass"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
v-if="status"
|
||||
class="absolute top-0 right-0 block rounded-full ring-1 u-ring-white"
|
||||
:class="statusClass"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import avatar from 'gradient-avatar'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'].includes(value)
|
||||
}
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
gradient: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value) {
|
||||
return ['online', 'idle', 'invisible', 'donotdisturb', 'focus'].includes(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
url () {
|
||||
if (typeof this.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return this.src
|
||||
},
|
||||
placeholder () {
|
||||
if (!this.alt) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.alt.split(' ').map(word => word.charAt(0)).join('')
|
||||
},
|
||||
gradientPlaceholder () {
|
||||
if (!this.gradient) {
|
||||
return
|
||||
}
|
||||
|
||||
return avatar(this.alt || new Date().toString())
|
||||
},
|
||||
sizeClass () {
|
||||
return ({
|
||||
xxxs: 'h-4 w-4 text-xs',
|
||||
xxs: 'h-5 w-5 text-xs',
|
||||
xs: 'h-6 w-6 text-xs',
|
||||
sm: 'h-8 w-8 text-sm',
|
||||
md: 'h-10 w-10 text-md',
|
||||
lg: 'h-12 w-12 text-lg',
|
||||
xl: 'h-14 w-14 text-xl',
|
||||
'2xl': 'h-16 w-16 text-2xl',
|
||||
'3xl': 'h-20 w-20 text-3xl'
|
||||
})[this.size]
|
||||
},
|
||||
roundedClass () {
|
||||
return ({
|
||||
true: 'rounded-lg',
|
||||
false: 'rounded-full'
|
||||
})[this.rounded]
|
||||
},
|
||||
placeholderClass () {
|
||||
return ({
|
||||
true: 'u-bg-gray-100',
|
||||
false: 'u-bg-gray-100'
|
||||
})[!!this.alt]
|
||||
},
|
||||
avatarClass () {
|
||||
return [
|
||||
this.sizeClass,
|
||||
this.roundedClass,
|
||||
this.placeholderClass,
|
||||
this.to ? 'cursor-pointer' : ''
|
||||
].join(' ')
|
||||
},
|
||||
statusClass () {
|
||||
return [
|
||||
({
|
||||
online: 'bg-green-400',
|
||||
idle: 'bg-yellow-400',
|
||||
invisible: 'u-bg-gray-300',
|
||||
donotdisturb: 'bg-red-400',
|
||||
focus: 'bg-primary-500'
|
||||
})[this.status],
|
||||
({
|
||||
xxxs: 'h-1 w-1',
|
||||
xxs: 'h-1 w-1',
|
||||
xs: 'h-1.5 w-1.5',
|
||||
sm: 'h-2 w-2',
|
||||
md: 'h-2.5 w-2.5',
|
||||
lg: 'h-3 w-3',
|
||||
xl: 'h-3.5 w-3.5',
|
||||
'2xl': 'h-3.5 w-3.5',
|
||||
'3xl': 'h-4 w-4'
|
||||
})[this.size],
|
||||
({
|
||||
true: 'transform -translate-y-1/2 translate-x-1/2'
|
||||
})[this.rounded]
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goto (e) {
|
||||
if (!this.to || !this.$router) { return }
|
||||
e.preventDefault()
|
||||
this.$router.push(this.to)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
62
src/runtime/components/elements/AvatarGroup.vue
Normal file
62
src/runtime/components/elements/AvatarGroup.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<Avatar
|
||||
v-for="(avatar, index) of avatars"
|
||||
:key="index"
|
||||
:src="avatar.src"
|
||||
class="ring-2 u-ring-white -ml-1.5 first:ml-0"
|
||||
:size="size"
|
||||
:status="avatar.status"
|
||||
/>
|
||||
<Avatar
|
||||
v-if="remainingGroupSize > 0"
|
||||
class="ring-2 u-ring-white -ml-1.5 first:ml-0 text-[10px]"
|
||||
:size="size"
|
||||
:text="`+${remainingGroupSize}`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from './Avatar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatars () {
|
||||
return this.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
},
|
||||
displayedGroup () {
|
||||
if (!this.max) { return this.avatars }
|
||||
|
||||
return this.avatars.slice(0, this.max)
|
||||
},
|
||||
remainingGroupSize () {
|
||||
if (!this.max) { return 0 }
|
||||
|
||||
return this.avatars.length - this.max
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
72
src/runtime/components/elements/Badge.vue
Normal file
72
src/runtime/components/elements/Badge.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<span class="inline-flex items-center font-medium" :class="badgeClass">
|
||||
<slot>{{ label }}</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['sm', 'md', 'lg'].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value) {
|
||||
return ['primary', 'gray', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'indigo', 'purple', 'pink', 'gradient'].includes(value)
|
||||
}
|
||||
},
|
||||
pill: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeClass () {
|
||||
return ({
|
||||
sm: 'text-xs px-2 py-0.5',
|
||||
md: 'text-sm px-2.5 py-0.5 leading-4',
|
||||
lg: 'text-sm px-3 py-0.5 leading-5'
|
||||
})[this.size]
|
||||
},
|
||||
variantClass () {
|
||||
return ({
|
||||
primary: 'bg-primary-100 dark:bg-primary-700 text-primary-800 dark:text-primary-100',
|
||||
gray: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100',
|
||||
red: 'bg-red-100 dark:bg-red-700 text-red-800 dark:text-red-100',
|
||||
orange: 'bg-orange-100 dark:bg-orange-700 text-orange-800 dark:text-orange-100',
|
||||
yellow: 'bg-yellow-100 dark:bg-yellow-700 text-yellow-800 dark:text-yellow-100',
|
||||
green: 'bg-green-100 dark:bg-green-700 text-green-800 dark:text-green-100',
|
||||
teal: 'bg-teal-100 dark:bg-teal-700 text-teal-800 dark:text-teal-100',
|
||||
blue: 'bg-blue-100 dark:bg-blue-700 text-blue-800 dark:text-blue-100',
|
||||
indigo: 'bg-indigo-100 dark:bg-indigo-700 text-indigo-800 dark:text-indigo-100',
|
||||
purple: 'bg-purple-100 dark:bg-purple-700 text-purple-800 dark:text-purple-100',
|
||||
pink: 'bg-pink-100 dark:bg-pink-700 text-pink-800 dark:text-pink-100',
|
||||
gradient: 'bg-gradient-to-r from-indigo-600 to-blue-600 text-white'
|
||||
})[this.variant]
|
||||
},
|
||||
roundedClass () {
|
||||
return ({
|
||||
false: 'rounded',
|
||||
true: 'rounded-full'
|
||||
})[this.pill]
|
||||
},
|
||||
badgeClass () {
|
||||
return [
|
||||
this.sizeClass,
|
||||
this.variantClass,
|
||||
this.roundedClass
|
||||
].join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
185
src/runtime/components/elements/Button.vue
Normal file
185
src/runtime/components/elements/Button.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<component
|
||||
:is="buttonIs"
|
||||
ref="button"
|
||||
:class="buttonClass"
|
||||
:aria-label="ariaLabel"
|
||||
v-bind="buttonProps"
|
||||
>
|
||||
<Icon v-if="isLeading" :name="iconName" :class="iconClass" aria-hidden="true" />
|
||||
<slot><span :class="truncate ? 'text-left break-all line-clamp-1' : ''">{{ label }}</span></slot>
|
||||
<Icon v-if="isTrailing" :name="iconName" :class="iconClass" aria-hidden="true" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import Link from '../elements/Link'
|
||||
import Icon from '../elements/Icon'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
Link
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
const button = ref(null)
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return 'Link'
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
switch (buttonIs.value) {
|
||||
case 'Link': return { to: props.to, target: props.target }
|
||||
default: return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.button.size[props.size],
|
||||
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
$ui.button.variant[props.variant],
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
|
||||
props.rounded ? 'rounded-full' : 'rounded-md',
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.input.icon.size[props.size],
|
||||
isLeading.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
|
||||
isTrailing.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
button,
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
buttonClass,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
iconName,
|
||||
iconClass
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
161
src/runtime/components/elements/Dropdown.vue
Normal file
161
src/runtime/components/elements/Dropdown.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<Menu v-slot="{ open }" as="div" :class="wrapperClass">
|
||||
<MenuButton ref="trigger" as="div">
|
||||
<slot :open="open">
|
||||
<button>Open</button>
|
||||
</slot>
|
||||
</MenuButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="containerClass">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-out"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<MenuItems :class="itemsClass" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" class="py-1">
|
||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }">
|
||||
<Component v-bind="item" :is="(item.to && 'Link') || 'button'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)">
|
||||
<slot :name="item.slot" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" />
|
||||
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</Component>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</div>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem
|
||||
} from '@headlessui/vue'
|
||||
|
||||
import Icon from '../elements/Icon'
|
||||
import Link from '../elements/Link'
|
||||
import { classNames, usePopper } from '../../utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Link
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-end',
|
||||
validator: (value) => {
|
||||
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
|
||||
}
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
default: 'fixed',
|
||||
validator: (value) => {
|
||||
return ['absolute', 'fixed'].includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: 'relative inline-block text-left'
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: 'w-48 z-20'
|
||||
},
|
||||
itemsClass: {
|
||||
type: String,
|
||||
default: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 ring-black ring-opacity-5'
|
||||
},
|
||||
itemClass: {
|
||||
type: String,
|
||||
default: 'group flex items-center px-4 py-2 text-sm w-full'
|
||||
},
|
||||
itemActiveClass: {
|
||||
type: String,
|
||||
default: 'u-bg-gray-100 u-text-gray-900'
|
||||
},
|
||||
itemInactiveClass: {
|
||||
type: String,
|
||||
default: 'u-text-gray-700'
|
||||
},
|
||||
itemDisabledClass: {
|
||||
type: String,
|
||||
default: 'cursor-not-allowed opacity-50'
|
||||
},
|
||||
itemIconClass: {
|
||||
type: String,
|
||||
default: 'mr-3 h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500'
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const [trigger, container] = usePopper({
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
modifiers: [{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
adaptive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 8
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
function resolveItemClass ({ active, disabled }) {
|
||||
return classNames(
|
||||
props.itemClass,
|
||||
active ? props.itemActiveClass : props.itemInactiveClass,
|
||||
disabled && props.itemDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
function onItemClick (item) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.click) {
|
||||
item.click()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
trigger,
|
||||
container,
|
||||
onItemClick,
|
||||
resolveItemClass
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
14
src/runtime/components/elements/Icon.vue
Normal file
14
src/runtime/components/elements/Icon.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div :class="name" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
64
src/runtime/components/elements/Link.vue
Normal file
64
src/runtime/components/elements/Link.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<a v-if="isExternalLink" v-bind="$attrs" :class="isActive ? activeClass : inactiveClass" :href="to" :target="target">
|
||||
<slot v-bind="{ isActive }" />
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
v-slot="{ href, navigate }"
|
||||
v-bind="$props"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="isActive ? activeClass : inactiveClass"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive }" />
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useRoute } from '#imports'
|
||||
|
||||
export default {
|
||||
name: 'Link',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...RouterLink.props,
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const route = useRoute()
|
||||
const isActive = computed(() => {
|
||||
if (props.exact) {
|
||||
return [props.to, `${props.to}/`].includes(route.path)
|
||||
} else {
|
||||
return !!route.path.startsWith(props.to)
|
||||
}
|
||||
})
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
|
||||
return {
|
||||
isActive,
|
||||
isExternalLink
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user