1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
/**
* Draggable class that enables HTML elements to be moved within a parent container.
*
* This class creates a draggable container with a handle and attaches the specified
* element to it. The element can then be dragged around its parent container using
* the handle, providing an interactive UI element for repositioning content.
*
* Features:
* - Flexible positioning using top/bottom and left/right coordinates
* - Customizable handle size, color, and appearance
* - Maintains position relative to parent container edges on window resize
* - Touch-enabled with pointer events support for multi-device compatibility
* - Smooth drag animations with visual feedback during movement
* - Boundary constraints within the parent container
*
* @example
* // Create a draggable element at the bottom-right corner
* const element = document.getElementById('my-element');
* const parent = document.querySelector('.parent-container');
* const draggable = new Draggable(element, parent, {
* bottom: 20,
* right: 20,
* handleColor: 'rgba(100, 150, 200, 0.7)'
* });
*/
class Draggable {
/**
* Creates a new Draggable instance.
*
* @param {HTMLElement} element - The element to be made draggable
* @param {HTMLElement|string} parent - The parent element where the draggable container will be appended.
* Can be either an HTMLElement or a CSS selector string
* @param {Object} [options={}] - Configuration options for the draggable element
* @param {number|null} [options.top=null] - The initial top position in pixels. Mutually exclusive with bottom
* @param {number|null} [options.bottom=20] - The initial bottom position in pixels. Mutually exclusive with top
* @param {number|null} [options.left=null] - The initial left position in pixels. Mutually exclusive with right
* @param {number|null} [options.right=20] - The initial right position in pixels. Mutually exclusive with left
* @param {number} [options.handleSize=10] - The size of the drag handle in pixels
* @param {number} [options.handleGap=5] - The gap between the handle and the draggable content in pixels
* @param {number} [options.zindex=200] - The z-index of the draggable container
* @param {string} [options.handleColor='#f0f0f0b3'] - The background color of the handle (supports rgba)
* @param {number} [options.dragOpacity=0.6] - Opacity of the element while being dragged (between 0 and 1)
*/
constructor(element, parent, options = {}) {
// Set default options
this.options = {
top: null,
bottom: 20,
left: null,
right: 20,
handleSize: 10,
handleGap: 5,
zindex: 200,
handleColor: '#f0f0f0b3', // rgba(240, 240, 240, 0.7)
dragOpacity: 0.6
};
// Merge user options with defaults
Object.assign(this.options, options);
// Store element and parent references
this.element = element;
this.parent = typeof parent === 'string' ? document.querySelector(parent) : parent;
if (!this.element || !this.parent) {
throw new Error('Draggable requires valid element and parent');
}
// Handle positioning priority
if (this.options.left !== null) this.options.right = null;
if (this.options.top !== null) this.options.bottom = null;
// Disable context menu globally if not already disabled
this.setupContextMenu();
// Create container and handle
this.createElements();
// Setup event listeners for dragging
this.setupDragEvents();
// Append element to container
this.appendChild(this.element);
// Setup resize handling
this.setupResizeHandler();
}
/**
* Disables the context menu globally if not already disabled.
* @private
*/
setupContextMenu() {
if (!window.setCtxMenu) {
window.addEventListener("contextmenu", e => e.preventDefault());
window.setCtxMenu = true;
}
}
/**
* Creates the draggable container and handle elements.
* @private
*/
createElements() {
const { handleGap, zindex, handleColor, handleSize } = this.options;
// Create container element
this.container = document.createElement('div');
this.container.classList.add('openlime-draggable');
this.container.style.display = 'flex';
this.container.style.gap = `${handleGap}px`;
this.container.style.position = 'absolute';
this.container.style.zIndex = zindex;
this.container.style.touchAction = 'none';
this.container.style.visibility = 'visible';
// Create handle element
this.handle = document.createElement('div');
this.handle.style.borderRadius = '4px';
this.handle.style.backgroundColor = handleColor;
this.handle.style.padding = '0';
this.handle.style.width = `${handleSize}px`;
this.handle.style.height = `${handleSize}px`;
this.handle.style.zIndex = zindex + 5;
this.handle.style.cursor = 'grab';
// Assemble elements
this.container.appendChild(this.handle);
this.parent.appendChild(this.container);
}
/**
* Sets up event listeners for window resize.
* @private
*/
setupResizeHandler() {
// Use debounced resize handler to improve performance
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => this.updatePosition(), 100);
});
}
/**
* Sets up the drag event listeners for the handle.
* Manages pointer events for drag operations.
* @private
*/
setupDragEvents() {
let offsetX, offsetY;
let isDragging = false;
// Use bound methods to maintain this context
const dragStart = (e) => {
e.preventDefault();
// Set dragging state
isDragging = true;
this.container.style.opacity = this.options.dragOpacity;
this.handle.style.cursor = 'grabbing';
// Calculate offsets based on pointer position
offsetX = e.clientX - this.container.offsetLeft;
offsetY = e.clientY - this.container.offsetTop;
// Add move event listener
document.addEventListener("pointermove", drag);
};
const drag = (e) => {
if (!isDragging) return;
e.preventDefault();
// Calculate new position
const newLeft = Math.max(0, Math.min(
e.clientX - offsetX,
this.parent.offsetWidth - this.container.offsetWidth
));
const newTop = Math.max(0, Math.min(
e.clientY - offsetY,
this.parent.offsetHeight - this.container.offsetHeight
));
// Update position
this.container.style.left = `${newLeft}px`;
this.container.style.top = `${newTop}px`;
// Update the option values based on new position
this.options.left = newLeft;
this.options.right = null;
this.options.top = newTop;
this.options.bottom = null;
};
const dragEnd = () => {
if (!isDragging) return;
// Reset visual state
this.container.style.opacity = '1.0';
this.handle.style.cursor = 'grab';
// Clear dragging state
isDragging = false;
// Remove move event listener
document.removeEventListener("pointermove", drag);
};
// Attach event listeners
this.handle.addEventListener("pointerdown", dragStart);
document.addEventListener("pointerup", dragEnd);
document.addEventListener("pointercancel", dragEnd);
}
/**
* Appends an HTML element to the draggable container and updates its position.
* @param {HTMLElement} element - The element to append to the draggable container
* @returns {Draggable} This instance for method chaining
*/
appendChild(element) {
if (element) {
// Ensure the element has proper positioning
element.style.position = 'unset';
this.container.appendChild(element);
this.updatePosition();
}
return this;
}
/**
* Updates the position of the draggable container based on its current options and parent dimensions.
* This method is called automatically on window resize and when elements are appended.
* @returns {Draggable} This instance for method chaining
*/
updatePosition() {
const containerWidth = this.container.offsetWidth;
const containerHeight = this.container.offsetHeight;
const parentWidth = this.parent.offsetWidth;
const parentHeight = this.parent.offsetHeight;
let top = 0;
let left = 0;
// Calculate top/bottom position
if (this.options.top !== null) {
top = this.options.top;
} else if (this.options.bottom !== null) {
top = parentHeight - this.options.bottom - containerHeight;
}
// Calculate left/right position
if (this.options.left !== null) {
left = this.options.left;
} else if (this.options.right !== null) {
left = parentWidth - this.options.right - containerWidth;
}
// Ensure the element stays within parent bounds
top = Math.max(0, Math.min(top, parentHeight - containerHeight));
left = Math.max(0, Math.min(left, parentWidth - containerWidth));
// Apply position
this.container.style.top = `${top}px`;
this.container.style.left = `${left}px`;
return this;
}
/**
* Shows the draggable element if it's hidden.
* @returns {Draggable} This instance for method chaining
*/
show() {
this.container.style.visibility = 'visible';
return this;
}
/**
* Hides the draggable element.
* @returns {Draggable} This instance for method chaining
*/
hide() {
this.container.style.visibility = 'hidden';
return this;
}
/**
* Changes the handle color.
* @param {string} color - New color for the handle (hex, rgb, rgba)
* @returns {Draggable} This instance for method chaining
*/
setHandleColor(color) {
this.options.handleColor = color;
this.handle.style.backgroundColor = color;
return this;
}
}
export { Draggable }