CommonLink.vue 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. <!-- Copyright (C) 2012-2023 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 stopEvent from '@shared/utils/events'
  6. import type { Link } from '@shared/types/router'
  7. import { useApplicationStore } from '@shared/stores/application'
  8. export interface Props {
  9. link: Link
  10. external?: boolean
  11. internal?: boolean
  12. restApi?: boolean
  13. disabled?: boolean
  14. rel?: string
  15. target?: string
  16. openInNewTab?: boolean
  17. replace?: boolean
  18. activeClass?: string
  19. exactActiveClass?: string
  20. }
  21. const props = withDefaults(defineProps<Props>(), {
  22. external: false,
  23. internal: false,
  24. replace: false,
  25. append: false,
  26. openInNewTab: false,
  27. disabled: false,
  28. activeClass: 'router-link-active',
  29. exactActiveClass: 'router-link-exact-active',
  30. })
  31. const emit = defineEmits<{
  32. (e: 'click', event: MouseEvent): void
  33. }>()
  34. const target = computed(() => {
  35. if (props.target) return props.target
  36. if (props.openInNewTab) return '_blank'
  37. return null
  38. })
  39. // TODO: Correct styling is currently missing.
  40. const linkClass = computed(() => {
  41. if (props.disabled) {
  42. return 'pointer-events-none'
  43. }
  44. return ''
  45. })
  46. const { href, route, navigate, isActive, isExactActive } = useLink({
  47. to: toRef(props, 'link'),
  48. replace: toRef(props, 'replace'),
  49. })
  50. const isInternalLink = computed(() => {
  51. if (props.external || props.restApi) return false
  52. if (props.internal) return true
  53. // zammad desktop urls
  54. if (route.value.fullPath.startsWith('/#')) return false
  55. return route.value.matched.length > 0 && route.value.name !== 'Error'
  56. })
  57. const app = useApplicationStore()
  58. const path = computed(() => {
  59. if (isInternalLink.value) {
  60. return href.value
  61. }
  62. if (props.restApi) {
  63. return `${String(app.config.api_path)}${props.link}`
  64. }
  65. return props.link as string
  66. })
  67. const onClick = (event: MouseEvent) => {
  68. if (props.disabled) {
  69. stopEvent(event, { immediatePropagation: true })
  70. return
  71. }
  72. emit('click', event)
  73. if (isInternalLink.value) {
  74. navigate(event)
  75. }
  76. // Stop the scroll-to-top behavior or navigation on regular links when href is just '#'.
  77. if (!isInternalLink.value && props.link === '#') {
  78. stopEvent(event, { propagation: false })
  79. }
  80. }
  81. </script>
  82. <template>
  83. <a
  84. data-test-id="common-link"
  85. :href="path"
  86. :target="target"
  87. :rel="rel"
  88. :class="[
  89. linkClass,
  90. {
  91. [activeClass]: isActive,
  92. [exactActiveClass]: isExactActive,
  93. },
  94. ]"
  95. @click="onClick"
  96. >
  97. <slot></slot>
  98. </a>
  99. </template>