import { Logger } from '../core/utils.js';
import { Position } from '../services/position.js';
export class DraggableManager {
#element;
#options;
#isDragging = false;
#startX = 0;
#startY = 0;
#initialLeft = 0;
#initialTop = 0;
#handlers = {};
constructor(element, options = {}) {
this.#element = element;
this.#options = {
storageKey: null,
handle: null,
onDragStart: null,
onDragmove: null,
onDragEnd: null,
saveOffset: false,
...options
};
this.#init();
}
#init() {
const handle = this.#options.handle
? this.#element.querySelector(this.#options.handle)
: this.#element;
if (!handle) {
Logger.warn('Draggable', 'Handle element not found');
return;
}
this.#handlers.mousedown = this.#onDragStart.bind(this);
this.#handlers.mousemove = this.#onDragmove.bind(this);
this.#handlers.mouseup = this.#onDragEnd.bind(this);
this.#handlers.touchstart = this.#onDragStart.bind(this);
this.#handlers.touchmove = this.#onDragmove.bind(this);
this.#handlers.touchend = this.#onDragEnd.bind(this);
handle.addEventListener('mousedown', this.#handlers.mousedown);
handle.addEventListener('touchstart', this.#handlers.touchstart, { passive: false });
}
#onDragStart(e) {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') {
return;
}
this.#isDragging = true;
const { pageX, pageY } = e.type === 'touchstart' ? e.touches[0] : e;
this.#startX = pageX;
this.#startY = pageY;
this.#initialLeft = parseFloat(this.#element.style.left) || 0;
this.#initialTop = parseFloat(this.#element.style.top) || 0;
this.#element.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', this.#handlers.mousemove);
document.addEventListener('mouseup', this.#handlers.mouseup);
document.addEventListener('touchmove', this.#handlers.touchmove, { passive: false });
document.addEventListener('touchend', this.#handlers.touchend);
e.preventDefault();
if (this.#options.onDragStart) {
this.#options.onDragStart(e);
}
}
#onDragmove(e) {
if (!this.#isDragging) return;
const { pageX, pageY } = e.type === 'touchmove' ? e.touches[0] : e;
const dx = pageX - this.#startX;
const dy = pageY - this.#startY;
let newLeft = this.#initialLeft + dx;
let newTop = this.#initialTop + dy;
({ left: newLeft, top: newTop } = Position.constrainToViewport(
newLeft,
newTop,
this.#element.offsetWidth,
this.#element.offsetHeight
));
this.#element.style.left = `${newLeft}px`;
this.#element.style.top = `${newTop}px`;
if (this.#options.onDragmove) {
this.#options.onDragmove(dx, dy);
}
}
#onDragEnd() {
if (!this.#isDragging) return;
this.#isDragging = false;
this.#element.style.cursor = ''; document.body.style.userSelect = '';
document.removeEventListener('mousemove', this.#handlers.mousemove);
document.removeEventListener('mouseup', this.#handlers.mouseup);
document.removeEventListener('touchmove', this.#handlers.touchmove);
document.removeEventListener('touchend', this.#handlers.touchend);
if (this.#options.saveOffset && this.#options.storageKey) {
this.#saveOffset();
}
if (this.#options.onDragEnd) {
const finalLeft = parseFloat(this.#element.style.left) || 0;
const finalTop = parseFloat(this.#element.style.top) || 0;
this.#options.onDragEnd(finalLeft, finalTop);
}
}
#saveOffset() {
const finalLeft = parseFloat(this.#element.style.left) || 0;
const finalTop = parseFloat(this.#element.style.top) || 0;
const originalLeft = parseFloat(this.#element.dataset.originalLeft);
const originalTop = parseFloat(this.#element.dataset.originalTop);
if (!isNaN(originalLeft) && !isNaN(originalTop)) {
const offset = {
dx: finalLeft - originalLeft,
dy: finalTop - originalTop,
};
localStorage.setItem(this.#options.storageKey, JSON.stringify(offset));
Logger.log('Draggable', `Saved offset to ${this.#options.storageKey}:`, offset);
}
}
destroy() {
const handle = this.#options.handle
? this.#element.querySelector(this.#options.handle)
: this.#element;
if (handle) {
handle.removeEventListener('mousedown', this.#handlers.mousedown);
handle.removeEventListener('touchstart', this.#handlers.touchstart);
}
if (this.#isDragging) {
document.removeEventListener('mousemove', this.#handlers.mousemove);
document.removeEventListener('mouseup', this.#handlers.mouseup);
document.removeEventListener('touchmove', this.#handlers.touchmove);
document.removeEventListener('touchend', this.#handlers.touchend);
}
this.#element.style.cursor = '';
Logger.log('Draggable', 'Destroyed');
}
}
export function makeDraggable(element, options = {}) {
return new DraggableManager(element, options);
}