Skip to content

Basic Draggable Component Code ​

Dependencies

This component requires:

  • Vue 3 with Composition API
  • SCSS support

Full Component Code ​

vue
<script setup>
// vue imports
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';

// constants
// store

// libraries

// components

// Props
const props = defineProps({
    positionX: {
        required: false,
        type: Number,
        default: 200,
    },
    positionY: {
        required: false,
        type: Number,
        default: 200,
    },
    dragAlongWithCursor: {
        type: Boolean,
        default: false,
        required: false,
    },
    placement: {
        type: String,
        default: 'bottom',
        required: false,
    },
});

// Rem to pixel conversion
const remToPixel = ref(16); // Default to 16px (1rem = 16px typically)

const calculateRemToPixel = () => {
  // Get the root font size
  if (typeof document !== 'undefined') {
    const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
    remToPixel.value = rootFontSize || 16;
  }
};

const emit = defineEmits(['setPosition', 'onStartDrag', 'onDragging', 'onStopDrag']);

let draggableComponent = ref(null);
let isDragging = ref(false);
let posX = ref(0);
let posY = ref(0);
let mouseX = ref(0);
let mouseY = ref(0);

const position = computed(() => {
    let x;
    let y;
    let positionXValue = props.positionX;
    let positionYValue = props.positionY;

    // setPosXY();
    mouseX.value = 0;
    mouseY.value = 0;

    const remSizeInPX = remToPixel.value;
    const bodyElement = document.querySelector('body');
    const bodyHeight = bodyElement.offsetHeight;
    const bodyWidth = bodyElement.offsetWidth;

    if (positionXValue === 'center') {
        x = bodyWidth / 2 + 4 * remSizeInPX;
    } else {
        x = positionXValue;
    }

    if (positionYValue === 'center') {
        y = bodyHeight / 2;
    } else {
        y = positionYValue;
    }

    console.log('position', x, y);

    return {
        left: x,
        top: y,
    };
});

// Methods
const startDrag = (event) => {
    isDragging.value = true;
    mouseX.value = event.clientX;
    mouseY.value = event.clientY;
    document.addEventListener('mousemove', dragging);
    document.addEventListener('mouseup', stopDrag);
    emit('onStartDrag');
};

const stopDrag = () => {
    isDragging.value = false;
    document.removeEventListener('mousemove', dragging);
    document.removeEventListener('mouseup', stopDrag);
    emit('setPosition', {
        positionX: position.value.left + posX.value,
        positionY: position.value.top + posY.value,
        placement: props.placement,
    });
    emit('onStopDrag');
    posX.value = 0;
    posY.value = 0;
};

const dragging = (event) => {
    console.log('dragging');
    const remSizeInPX = remToPixel.value;
    const bodyElement = document.querySelector('body');
    const bodyHeight = bodyElement.offsetHeight;
    const bodyWidth = bodyElement.offsetWidth;

    let promptElementHeight = 2.5 * remSizeInPX;
    // foyrAi prompt input element width
    let promptElementWidth = 2 * remSizeInPX;

    if (isDragging.value) {
        let clientX = event.clientX;
        let clientY = event.clientY;

        if (clientX < 1 * remSizeInPX) {
            clientX = 1 * remSizeInPX;
        }
        if (clientX > bodyWidth - promptElementWidth - 4 * remSizeInPX) {
            clientX = bodyWidth - promptElementWidth - 4 * remSizeInPX;
        }
        if (clientY < 1 * remSizeInPX) {
            clientY = 1 * remSizeInPX;
        }
        if (clientY > bodyHeight - promptElementHeight) {
            clientY = bodyHeight - promptElementHeight;
        }

        const deltaX = clientX - mouseX.value;
        const deltaY = clientY - mouseY.value;
        posX.value += deltaX;
        posY.value += deltaY;
        mouseX.value = clientX;
        mouseY.value = clientY;
        let lastEmitTime = 0;
        const throttleTime = 16;
        const now = Date.now();
        if (now - lastEmitTime >= throttleTime) {
            emit('onDragging');
            lastEmitTime = now;
        }
    }
};

const dragSmartBarAlong = (event) => {
    const remSizeInPX = remToPixel.value;
    if (props.dragAlongWithCursor) {
        posX.value = event.clientX < 4 * remSizeInPX ? 5 * remSizeInPX : event.clientX + remSizeInPX;
        posY.value = event.clientY < 5.375 * remSizeInPX ? 5.375 * remSizeInPX : event.clientY;
    }
};

// Lifecycle Hooks
onMounted(() => {
    calculateRemToPixel();
    // Recalculate on resize
    window.addEventListener('resize', calculateRemToPixel);
    document.addEventListener('mousemove', dragSmartBarAlong);
});

onBeforeUnmount(() => {
    window.removeEventListener('resize', calculateRemToPixel);
    document.removeEventListener('mousemove', dragSmartBarAlong);
});
</script>
<template>
    <div ref="draggableComponent" class="basic-draggable-floating-component-wrapper" :class="placement" :style="{
        left: position.left + posX + 'px',
        top: position.top + posY + 'px',
    }">
        <!-- @mousedown="startDrag"  -->
        <slot name="main" v-bind:startDrag="startDrag"></slot>
    </div>
</template>
<style lang="scss" scoped>
.basic-draggable-floating-component-wrapper {
    position: fixed;
    z-index: 999;

    &.left {
        transform: translate(-100%, -50%);
    }

    &.right {
        transform: translate(0%, -50%);
    }

    &.top {
        transform: translate(-50%, -100%);
    }

    &.bottom {
        transform: translate(-50%, 0%);
    }
}
</style>