unovue / shadcn-vue

Vue port of shadcn-ui
https://www.shadcn-vue.com/
MIT License
4.59k stars 267 forks source link

[Feature]: Dialog can click header and drag #720

Closed han1548772930 closed 4 weeks ago

han1548772930 commented 1 month ago

Describe the feature

This is how I currently do it.

<script setup lang="ts">

import {
  Dialog,
  DialogContent, DialogDescription,
  DialogFooter,
  DialogHeader, DialogTitle,
  DialogTrigger
} from "@/components/ui/dialog";

import {nextTick, onBeforeUnmount, ref, watch, watchEffect} from "vue";
import {useDraggable} from "@vueuse/core";

defineProps<{
  dialogClass?: string
}>()
const open = defineModel({
  type: Boolean,
})
const dialogTitleRef = ref<HTMLElement | null>(null);
const dialogContentRef = ref<HTMLElement | null>(null);
const {x, y, isDragging} = useDraggable(dialogTitleRef);

const startX = ref<number>(0);
const startY = ref<number>(0);
const startedDrag = ref(false);
const transformX = ref(0);
const transformY = ref(0);
const preTransformX = ref(0);
const preTransformY = ref(0);
const dragRect = ref({left: 0, right: 0, top: 0, bottom: 0});

const w1 = watch([x, y], () => {
  if (!startedDrag.value && dialogTitleRef.value && dialogContentRef.value) {
    startX.value = x.value;
    startY.value = y.value;
    const bodyRect = document.body.getBoundingClientRect();
    const titleRect = dialogTitleRef.value.getBoundingClientRect();
    dragRect.value.left = 0;
    dragRect.value.top = 0;
    dragRect.value.right = bodyRect.width - titleRect.width;
    dragRect.value.bottom = bodyRect.height - titleRect.height;
    preTransformX.value = transformX.value;
    preTransformY.value = transformY.value;
  }
  if (isDragging.value) {
    startedDrag.value = true;
  }
});

const w2 = watch(isDragging, () => {
  if (!isDragging.value) {
    startedDrag.value = false;
  }
});

watchEffect(() => {
  if (startedDrag.value) {
    transformX.value =
        preTransformX.value +
        Math.min(Math.max(dragRect.value.left, x.value), dragRect.value.right) -
        startX.value;
    transformY.value =
        preTransformY.value +
        Math.min(Math.max(dragRect.value.top, y.value), dragRect.value.bottom) -
        startY.value;
    if (dialogContentRef.value) {
      if (transformX.value === 0 && transformY.value === 0) {
        return
      } else {
        dialogContentRef.value.style.transform = `translate(calc(-50% + ${transformX.value}px), calc(-50% + ${transformY.value}px))`;
      }
    }
  }
});

const bindingEl = () => {
  nextTick(() => {
    const dialogHeaders = document.querySelectorAll('#dialog-header');
    const dialogContents = document.querySelectorAll('.dialog-content');
    dialogTitleRef.value = dialogHeaders[dialogHeaders.length - 1] as HTMLElement;
    dialogContentRef.value = dialogContents[dialogContents.length - 1] as HTMLElement;
    // Reset initial positions
    startX.value = 0;
    startY.value = 0;
    preTransformX.value = 0;
    preTransformY.value = 0;
    transformX.value = 0;
    transformY.value = 0;
    if (dialogContentRef.value) {
      dialogContentRef.value.style.transform = 'translate(-50%, -50%)';
    }
  });
};

onBeforeUnmount(() => {
  w1();
  w2();
})
watchEffect(() => {
  if (open.value) {
    bindingEl()
  }
})
</script>

<template>
  <Dialog v-model:open="open">
    <DialogTrigger as-child>
      <slot name="trigger"></slot>
    </DialogTrigger>
    <DialogContent class="select-none transition-none translate-x-[-50%] translate-y-[-50%] dialog-content"
                   :class="dialogClass">
      <DialogHeader id="dialog-header" class="cursor-move">
        <DialogTitle>
          <slot name="header-title"></slot>
        </DialogTitle>
        <dialog-description></dialog-description>
      </DialogHeader>
      <slot name="default"></slot>
      <DialogFooter>
        <slot name="footer"></slot>
      </DialogFooter>
    </DialogContent>

  </Dialog>
</template>

<style scoped>

</style>

Additional information

hrynevychroman commented 1 month ago

@han1548772930 Do You want to add dialog that could be positioned by cursor dragging when handling the header?

han1548772930 commented 1 month ago

@han1548772930 Do You want to add dialog that could be positioned by cursor dragging when handling the header?

Yes

hrynevychroman commented 1 month ago

@sadeghbarati do you think we could add this, as it is not present in original shadcn library 🤔

sadeghbarati commented 1 month ago

@romanhrynevych

An example would be nice, but including in components, I'm not sure really

Saeid-Za commented 1 month ago

If you're open to the idea, we could utilize the vueuse composables, add some variations to the docs as examples.

han1548772930 commented 1 month ago

If you're open to the idea, we could utilize the vueuse composables, add some variations to the docs as examples.

This is also good