CommonLink.vue 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, toRef } from 'vue'
  4. import { useLink } from 'vue-router'
  5. import { getLinkClasses } from '#shared/initializer/initializeLinkClasses.ts'
  6. import { useApplicationStore } from '#shared/stores/application.ts'
  7. import type { Link } from '#shared/types/router.ts'
  8. import stopEvent from '#shared/utils/events.ts'
  9. import type { Sizes } from './types.ts'
  10. export interface Props {
  11. link: Link
  12. external?: boolean
  13. internal?: boolean
  14. restApi?: boolean
  15. disabled?: boolean
  16. rel?: string
  17. target?: string
  18. openInNewTab?: boolean
  19. replace?: boolean
  20. activeClass?: string
  21. exactActiveClass?: string
  22. size?: Sizes
  23. }
  24. const props = withDefaults(defineProps<Props>(), {
  25. external: false,
  26. internal: false,
  27. replace: false,
  28. append: false,
  29. openInNewTab: false,
  30. disabled: false,
  31. activeClass: 'router-link-active',
  32. exactActiveClass: 'router-link-exact-active',
  33. size: 'large',
  34. })
  35. const emit = defineEmits<{
  36. click: [event: MouseEvent]
  37. }>()
  38. const target = computed(() => {
  39. if (props.target) return props.target
  40. if (props.openInNewTab) return '_blank'
  41. return undefined
  42. })
  43. const linkClass = computed(() => {
  44. const { base } = getLinkClasses()
  45. if (props.disabled) return `${base} pointer-events-none`
  46. return base
  47. })
  48. const fontSizeClassMap = {
  49. xs: 'text-[10px] leading-[10px]',
  50. small: 'text-xs leading-snug',
  51. medium: 'text-sm leading-snug',
  52. large: 'text-base leading-snug',
  53. xl: 'text-xl leading-snug',
  54. }
  55. const { href, route, navigate, isActive, isExactActive } = useLink({
  56. to: toRef(props, 'link'),
  57. replace: toRef(props, 'replace'),
  58. })
  59. const isInternalLink = computed(() => {
  60. if (props.external || props.restApi) return false
  61. if (props.internal) return true
  62. // zammad desktop urls
  63. if (route.value.fullPath.startsWith('/#')) return false
  64. return route.value.matched.length > 0 && route.value.name !== 'Error'
  65. })
  66. const app = useApplicationStore()
  67. const path = computed(() => {
  68. if (isInternalLink.value) {
  69. return href.value
  70. }
  71. if (props.restApi) {
  72. return `${app.config.api_path}${props.link}`
  73. }
  74. return props.link as string
  75. })
  76. const onClick = (event: MouseEvent) => {
  77. if (props.disabled) {
  78. stopEvent(event, { immediatePropagation: true })
  79. return
  80. }
  81. emit('click', event)
  82. if (isInternalLink.value) {
  83. navigate(event)
  84. }
  85. // Stop the scroll-to-top behavior or navigation on regular links when href is just '#'.
  86. if (!isInternalLink.value && props.link === '#') {
  87. stopEvent(event, { propagation: false })
  88. }
  89. }
  90. defineExpose({
  91. isActive,
  92. isExactActive,
  93. })
  94. </script>
  95. <template>
  96. <a
  97. data-test-id="common-link"
  98. :href="path"
  99. :target="target"
  100. :rel="rel"
  101. :class="[
  102. linkClass,
  103. fontSizeClassMap[props.size],
  104. {
  105. [activeClass]: isActive,
  106. [exactActiveClass]: isExactActive,
  107. },
  108. ]"
  109. @click="onClick"
  110. >
  111. <slot></slot>
  112. </a>
  113. </template>