drawer.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. <script lang="ts" setup>
  2. import type { DrawerProps, ExtendedDrawerApi } from './drawer';
  3. import { useIsMobile, usePriorityValue } from '@vben-core/composables';
  4. import { Info, X } from '@vben-core/icons';
  5. import {
  6. Sheet,
  7. SheetClose,
  8. SheetContent,
  9. SheetDescription,
  10. SheetFooter,
  11. SheetHeader,
  12. SheetTitle,
  13. VbenButton,
  14. VbenIconButton,
  15. VbenLoading,
  16. VbenTooltip,
  17. VisuallyHidden,
  18. } from '@vben-core/shadcn-ui';
  19. import { cn } from '@vben-core/shared';
  20. interface Props extends DrawerProps {
  21. class?: string;
  22. contentClass?: string;
  23. drawerApi?: ExtendedDrawerApi;
  24. }
  25. const props = withDefaults(defineProps<Props>(), {
  26. class: '',
  27. contentClass: '',
  28. drawerApi: undefined,
  29. });
  30. const { isMobile } = useIsMobile();
  31. const state = props.drawerApi?.useStore?.();
  32. const title = usePriorityValue('title', props, state);
  33. const description = usePriorityValue('description', props, state);
  34. const titleTooltip = usePriorityValue('titleTooltip', props, state);
  35. const showFooter = usePriorityValue('footer', props, state);
  36. const showLoading = usePriorityValue('loading', props, state);
  37. const closable = usePriorityValue('closable', props, state);
  38. const modal = usePriorityValue('modal', props, state);
  39. const confirmLoading = usePriorityValue('confirmLoading', props, state);
  40. const cancelText = usePriorityValue('cancelText', props, state);
  41. const confirmText = usePriorityValue('confirmText', props, state);
  42. const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
  43. const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
  44. function interactOutside(e: Event) {
  45. if (!closeOnClickModal.value) {
  46. e.preventDefault();
  47. }
  48. }
  49. function escapeKeyDown(e: KeyboardEvent) {
  50. if (!closeOnPressEscape.value) {
  51. e.preventDefault();
  52. }
  53. }
  54. // pointer-down-outside
  55. function pointerDownOutside(e: Event) {
  56. const target = e.target as HTMLElement;
  57. const isDismissableModal = !!target?.dataset.dismissableModal;
  58. if (!closeOnClickModal.value || !isDismissableModal) {
  59. e.preventDefault();
  60. }
  61. }
  62. </script>
  63. <template>
  64. <Sheet
  65. :modal="modal"
  66. :open="state?.isOpen"
  67. @update:open="() => drawerApi?.close()"
  68. >
  69. <SheetContent
  70. :class="
  71. cn('flex w-[520px] flex-col', props.class, {
  72. '!w-full': isMobile,
  73. })
  74. "
  75. @escape-key-down="escapeKeyDown"
  76. @interact-outside="interactOutside"
  77. @pointer-down-outside="pointerDownOutside"
  78. >
  79. <SheetHeader
  80. :class="
  81. cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
  82. 'px-4 py-3': closable,
  83. })
  84. "
  85. >
  86. <div>
  87. <SheetTitle v-if="title" class="text-left">
  88. <slot name="title">
  89. {{ title }}
  90. <VbenTooltip v-if="titleTooltip" side="right">
  91. <template #trigger>
  92. <Info class="inline-flex size-5 cursor-pointer pb-1" />
  93. </template>
  94. {{ titleTooltip }}
  95. </VbenTooltip>
  96. </slot>
  97. </SheetTitle>
  98. <SheetDescription v-if="description" class="mt-1 text-xs">
  99. <slot name="description">
  100. {{ description }}
  101. </slot>
  102. </SheetDescription>
  103. </div>
  104. <VisuallyHidden v-if="!title || !description">
  105. <SheetTitle v-if="!title" />
  106. <SheetDescription v-if="!description" />
  107. </VisuallyHidden>
  108. <div class="flex-center">
  109. <slot name="extra"></slot>
  110. <SheetClose
  111. v-if="closable"
  112. as-child
  113. class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
  114. >
  115. <VbenIconButton>
  116. <X class="size-4" />
  117. </VbenIconButton>
  118. </SheetClose>
  119. </div>
  120. </SheetHeader>
  121. <div
  122. :class="
  123. cn('relative flex-1 p-3', contentClass, {
  124. 'overflow-y-auto': !showLoading,
  125. })
  126. "
  127. >
  128. <VbenLoading v-if="showLoading" class="size-full" spinning />
  129. <slot></slot>
  130. </div>
  131. <SheetFooter
  132. v-if="showFooter"
  133. class="w-full flex-row items-center justify-end border-t p-2 px-3"
  134. >
  135. <slot name="prepend-footer"></slot>
  136. <slot name="footer">
  137. <VbenButton variant="ghost" @click="() => drawerApi?.onCancel()">
  138. <slot name="cancelText">
  139. {{ cancelText }}
  140. </slot>
  141. </VbenButton>
  142. <VbenButton
  143. :loading="confirmLoading"
  144. @click="() => drawerApi?.onConfirm()"
  145. >
  146. <slot name="confirmText">
  147. {{ confirmText }}
  148. </slot>
  149. </VbenButton>
  150. </slot>
  151. <slot name="append-footer"></slot>
  152. </SheetFooter>
  153. </SheetContent>
  154. </Sheet>
  155. </template>