diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..d360ff4 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: 'en-US' +early_access: false +reviews: + profile: 'chill' + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: true + base_branches: ['.*'] +chat: + auto_reply: true diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml deleted file mode 100644 index 0069fad..0000000 --- a/.github/workflows/pull-request-comment.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Add Comment to PR - -on: - pull_request_target: - types: - - opened - -jobs: - comment: - runs-on: ubuntu-latest - - steps: - - name: Create or Update Comment - uses: peter-evans/create-or-update-comment@v4.0.0 - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - Hi @${{ github.event.pull_request.user.login }}! Thank you for the contribution. diff --git a/src/Allonsh.js b/src/Allonsh.js new file mode 100644 index 0000000..9240dbf --- /dev/null +++ b/src/Allonsh.js @@ -0,0 +1,171 @@ +import { DEFAULTS, CSS_CLASSES } from './constants.js'; +import { EventManager } from './EventManager.js'; +import { DragManager } from './DragManager.js'; +import { DropzoneManager } from './DropzoneManager.js'; +import { StyleManager } from './StyleManager.js'; + +export default class Allonsh { + constructor(options = {}) { + const { + draggableSelector, + dropzoneSelector = null, + playAreaSelector = null, + restrictToDropzones = false, + enableStacking = false, + stackDirection = DEFAULTS.STACK_DIRECTION, + stackSpacing = DEFAULTS.STACK_SPACING, + useGhostEffect = false, + } = options; + + if (!draggableSelector) { + throw new Error("Allonsh Error: 'draggableSelector' is required."); + } + + this.options = { draggableSelector, dropzoneSelector, playAreaSelector }; + this.restrictToDropzones = restrictToDropzones; + this.enableStacking = enableStacking; + this.stackDirection = stackDirection; + this.stackSpacing = stackSpacing; + this.useGhostEffect = useGhostEffect; + + this.playAreaElement = playAreaSelector + ? document.querySelector(`.${playAreaSelector}`) + : document.body; + if (!this.playAreaElement) { + throw new Error( + `Allonsh Error: Play area element with class '${playAreaSelector}' not found.` + ); + } + + this.draggableElements = this.playAreaElement.querySelectorAll( + `.${draggableSelector}` + ); + if (!this.draggableElements.length) { + console.warn( + `Allonsh Warning: No draggable elements found with selector '.${draggableSelector}'.` + ); + } + + this.styleManager = new StyleManager(); + this.dropzoneManager = new DropzoneManager( + this.playAreaElement, + enableStacking, + stackDirection, + stackSpacing, + this.styleManager + ); + this.dragManager = new DragManager( + this.playAreaElement, + this.dropzoneManager, + this.styleManager, + useGhostEffect, + restrictToDropzones + ); + this.eventManager = new EventManager( + this.dragManager, + this.dropzoneManager, + this.styleManager + ); + + this._initialize(); + } + + _initialize() { + this.styleManager.applyPlayAreaStyles(this.playAreaElement); + if (!this.draggableElements.length) return; + this.eventManager.bindEvents(this.draggableElements); + this.dropzoneManager.initializeDropzones(this.options.dropzoneSelector); + } + + update(newOptions = {}) { + const { + draggableSelector = this.options.draggableSelector, + dropzoneSelector = this.options.dropzoneSelector, + playAreaSelector = this.options.playAreaSelector, + restrictToDropzones = this.restrictToDropzones, + enableStacking = this.enableStacking, + stackDirection = this.stackDirection, + stackSpacing = this.stackSpacing, + useGhostEffect = this.useGhostEffect, + } = newOptions; + + this.options = { + ...this.options, + draggableSelector, + dropzoneSelector, + playAreaSelector, + }; + this.restrictToDropzones = restrictToDropzones; + this.enableStacking = enableStacking; + this.stackDirection = stackDirection; + this.stackSpacing = stackSpacing; + this.useGhostEffect = useGhostEffect; + + if (playAreaSelector !== this.options.playAreaSelector) { + const newPlayArea = document.querySelector(`.${playAreaSelector}`); + if (newPlayArea) { + this.playAreaElement = newPlayArea; + this.styleManager.applyPlayAreaStyles(this.playAreaElement); + this.dragManager.playAreaElement = this.playAreaElement; + this.dropzoneManager.playAreaElement = this.playAreaElement; + } else { + console.warn( + `Allonsh Warning: Play area element with class '${playAreaSelector}' not found.` + ); + } + } + + if (draggableSelector !== this.options.draggableSelector) { + this.eventManager.unbindEvents(this.draggableElements); + this.draggableElements = this.playAreaElement.querySelectorAll( + `.${draggableSelector}` + ); + this.eventManager.bindEvents(this.draggableElements); + } + + if (dropzoneSelector !== this.options.dropzoneSelector) { + this.dropzoneManager.setDropzones(dropzoneSelector); + } + + this.dropzoneManager.enableStacking = this.enableStacking; + this.dropzoneManager.stackDirection = this.stackDirection; + this.dropzoneManager.stackSpacing = this.stackSpacing; + this.dragManager.useGhostEffect = this.useGhostEffect; + this.dragManager.restrictToDropzones = this.restrictToDropzones; + + this.dropzoneManager.dropzoneElements.forEach((dropzone) => { + this.enableStacking + ? this.dropzoneManager.applyStackingStyles(dropzone) + : this.dropzoneManager.removeStackingStyles(dropzone); + }); + } + + resetAll() { + this.draggableElements.forEach((el) => { + if (this.playAreaElement.contains(el)) { + this.playAreaElement.appendChild(el); + this.styleManager.resetPosition(el); + } + }); + } + + addDraggable(element) { + this.eventManager.bindEvents([element]); + this.draggableElements = this.playAreaElement.querySelectorAll( + `.${CSS_CLASSES.DRAGGABLE}` + ); + } + + removeDraggable(element) { + this.eventManager.unbindEvents([element]); + element.classList.remove(CSS_CLASSES.DRAGGABLE); + this.draggableElements = this.playAreaElement.querySelectorAll( + `.${CSS_CLASSES.DRAGGABLE}` + ); + } + + setDropzones(selector) { + this.dropzoneManager.setDropzones(selector); + this.options.dropzoneSelector = selector; + } +} diff --git a/src/DragManager.js b/src/DragManager.js new file mode 100644 index 0000000..4dbf092 --- /dev/null +++ b/src/DragManager.js @@ -0,0 +1,174 @@ +import { + CSS_POSITIONS, + OPACITY, + Z_INDEX, + EVENTS, + POINTER_EVENTS, +} from './constants.js'; + +export class DragManager { + constructor( + playAreaElement, + dropzoneManager, + styleManager, + useGhostEffect, + restrictToDropzones + ) { + this.playAreaElement = playAreaElement; + this.dropzoneManager = dropzoneManager; + this.styleManager = styleManager; + this.useGhostEffect = useGhostEffect; + this.restrictToDropzones = restrictToDropzones; + this.currentDraggedElement = null; + this.ghostElement = null; + this.originalParent = null; + this.originalDropzone = null; + this.dragOffsetX = 0; + this.dragOffsetY = 0; + } + + startDrag(event, clientX, clientY) { + this.currentDraggedElement = event.currentTarget; + this.styleManager.applyDraggingStyles(this.currentDraggedElement); + this.originalParent = this.currentDraggedElement.parentElement; + this.originalDropzone = this.dropzoneManager.findClosestDropzone( + this.currentDraggedElement + ); + + const playAreaRect = this.playAreaElement.getBoundingClientRect(); + const rect = this.currentDraggedElement.getBoundingClientRect(); + + if (this.useGhostEffect && this.originalParent !== this.playAreaElement) { + this._createGhostElement(rect, playAreaRect); + } + + this.dragOffsetX = clientX - rect.left; + this.dragOffsetY = clientY - rect.top; + + this.currentDraggedElement.dispatchEvent( + new CustomEvent(EVENTS.DRAG_START, { detail: { originalEvent: event } }) + ); + + this.dropzoneManager.toggleHighlight(true); + } + + _createGhostElement(rect, playAreaRect) { + this._removeGhostElement(); + this.ghostElement = this.currentDraggedElement.cloneNode(true); + this.styleManager.applyGhostStyles(this.ghostElement); + this.playAreaElement.appendChild(this.ghostElement); + this.ghostElement.style.left = `${rect.left - playAreaRect.left}px`; + this.ghostElement.style.top = `${rect.top - playAreaRect.top}px`; + this.currentDraggedElement.style.opacity = OPACITY.GHOST; + } + + updateDragPosition(clientX, clientY) { + if (!this.currentDraggedElement) return; + + const playAreaRect = this.playAreaElement.getBoundingClientRect(); + const element = this.ghostElement || this.currentDraggedElement; + const rect = element.getBoundingClientRect(); + const maxLeft = playAreaRect.width - rect.width; + const maxTop = playAreaRect.height - rect.height; + + let newLeft = clientX - playAreaRect.left - this.dragOffsetX; + let newTop = clientY - playAreaRect.top - this.dragOffsetY; + + newLeft = Math.max(0, Math.min(newLeft, maxLeft)); + newTop = Math.max(0, Math.min(newTop, maxTop)); + + if (this.ghostElement) { + this.ghostElement.style.left = `${newLeft}px`; + this.ghostElement.style.top = `${newTop}px`; + } else { + this.styleManager.applyAbsolutePositioning(this.currentDraggedElement); + this.currentDraggedElement.style.left = `${newLeft}px`; + this.currentDraggedElement.style.top = `${newTop}px`; + } + } + + handleDrop(clientX, clientY, event) { + if (!this.currentDraggedElement) return; + + this.currentDraggedElement.style.opacity = OPACITY.FULL; + const playAreaRect = this.playAreaElement.getBoundingClientRect(); + const clampedX = Math.min( + Math.max(clientX, playAreaRect.left + 1), + playAreaRect.right - 1 + ); + const clampedY = Math.min( + Math.max(clientY, playAreaRect.top + 1), + playAreaRect.bottom - 1 + ); + + this.currentDraggedElement.style.pointerEvents = POINTER_EVENTS.NONE; + const elementBelow = document.elementFromPoint(clampedX, clampedY); + this.currentDraggedElement.style.pointerEvents = POINTER_EVENTS.AUTO; + + if (!elementBelow && this.restrictToDropzones) { + this._returnToOrigin(); + this._resetDraggedElementState(); + return; + } + + const dropzoneFound = + this.dropzoneManager.findClosestDropzone(elementBelow); + if (dropzoneFound) { + this.dropzoneManager.handleDropzoneDrop( + this.currentDraggedElement, + dropzoneFound, + event + ); + } else if (!this.restrictToDropzones) { + this._placeInPlayArea(clampedX, clampedY, playAreaRect); + } else { + this._returnToOrigin(); + } + + this._resetDraggedElementState(); + } + + _placeInPlayArea(clampedX, clampedY, playAreaRect) { + if (this.currentDraggedElement.parentElement !== this.playAreaElement) { + this.playAreaElement.appendChild(this.currentDraggedElement); + this.styleManager.applyAbsolutePositioning(this.currentDraggedElement); + const offsetX = clampedX - playAreaRect.left - this.dragOffsetX; + const offsetY = clampedY - playAreaRect.top - this.dragOffsetY; + this.currentDraggedElement.style.left = `${offsetX}px`; + this.currentDraggedElement.style.top = `${offsetY}px`; + this.currentDraggedElement.style.zIndex = Z_INDEX.DRAGGING; + } + } + + _returnToOrigin() { + if ( + this.originalDropzone && + !this.dropzoneManager.isDropzoneRestricted(this.originalDropzone) + ) { + this.dropzoneManager.applyStackingStyles(this.originalDropzone); + this.styleManager.resetPosition(this.currentDraggedElement); + this.originalDropzone.appendChild(this.currentDraggedElement); + } else { + this.styleManager.resetPosition(this.currentDraggedElement); + this.playAreaElement.appendChild(this.currentDraggedElement); + } + } + + _removeGhostElement() { + if (this.ghostElement && this.ghostElement.parentElement) { + this.ghostElement.parentElement.removeChild(this.ghostElement); + this.ghostElement = null; + } + } + + _resetDraggedElementState() { + if (this.currentDraggedElement) { + this.styleManager.resetDraggableStyles(this.currentDraggedElement); + this.dropzoneManager.toggleHighlight(false); + } + this._removeGhostElement(); + this.currentDraggedElement = null; + this.originalParent = null; + this.originalDropzone = null; + } +} diff --git a/src/DropzoneManager.js b/src/DropzoneManager.js new file mode 100644 index 0000000..73e5bff --- /dev/null +++ b/src/DropzoneManager.js @@ -0,0 +1,98 @@ +import { CSS_CLASSES, EVENTS } from './constants.js'; + +export class DropzoneManager { + constructor( + playAreaElement, + enableStacking, + stackDirection, + stackSpacing, + styleManager + ) { + this.playAreaElement = playAreaElement; + this.enableStacking = enableStacking; + this.stackDirection = stackDirection; + this.stackSpacing = stackSpacing; + this.styleManager = styleManager; + this.dropzoneElements = []; + this._dropzoneSet = new Set(); + } + + initializeDropzones(dropzoneSelector) { + if (dropzoneSelector) { + this.dropzoneElements = this.playAreaElement.querySelectorAll( + `.${dropzoneSelector}` + ); + this._dropzoneSet = new Set(this.dropzoneElements); + this.dropzoneElements.forEach((dropzone) => { + if (this.enableStacking) { + this.applyStackingStyles(dropzone); + } + }); + } + } + + setDropzones(selector) { + this.dropzoneElements = this.playAreaElement.querySelectorAll( + `.${selector}` + ); + this._dropzoneSet = new Set(this.dropzoneElements); + this.dropzoneElements.forEach((dropzone) => { + if (this.enableStacking) { + this.applyStackingStyles(dropzone); + } + }); + } + + applyStackingStyles(dropzone) { + this.styleManager.applyStackingStyles(dropzone, { + stackDirection: this.stackDirection, + stackSpacing: this.stackSpacing, + }); + } + + removeStackingStyles(dropzone) { + this.styleManager.removeStackingStyles(dropzone); + } + + handleDropzoneDrop(draggedElement, dropzone, event) { + if (this.enableStacking) { + this.applyStackingStyles(dropzone); + } + this.styleManager.resetPosition(draggedElement); + dropzone.appendChild(draggedElement); + + dropzone.dispatchEvent( + new CustomEvent(EVENTS.DROP, { + detail: { draggedElement, originalEvent: event }, + }) + ); + dropzone.dispatchEvent( + new CustomEvent(EVENTS.DRAG_ENTER, { detail: { draggedElement } }) + ); + dropzone.dispatchEvent( + new CustomEvent(EVENTS.DRAG_LEAVE, { detail: { draggedElement } }) + ); + dropzone.classList.remove(CSS_CLASSES.HIGHLIGHT); + } + + findClosestDropzone(element) { + let el = element; + while (el && el !== this.playAreaElement) { + if (this._dropzoneSet.has(el)) { + return el; + } + el = el.parentElement; + } + return null; + } + + isDropzoneRestricted(dropzone) { + return dropzone?.classList.contains(CSS_CLASSES.RESTRICTED); + } + + toggleHighlight(enable) { + this.dropzoneElements.forEach((dropzone) => { + dropzone.classList.toggle(CSS_CLASSES.HIGHLIGHT, enable); + }); + } +} diff --git a/src/EventManager.js b/src/EventManager.js new file mode 100644 index 0000000..805f44e --- /dev/null +++ b/src/EventManager.js @@ -0,0 +1,72 @@ +import { CSS_CURSORS } from './constants.js'; + +export class EventManager { + constructor(dragManager, dropzoneManager, styleManager) { + this.dragManager = dragManager; + this.dropzoneManager = dropzoneManager; + this.styleManager = styleManager; + + this._boundMouseMoveHandler = this._onMouseMove.bind(this); + this._boundMouseUpHandler = this._onMouseUp.bind(this); + this._boundTouchMoveHandler = this._onTouchMove.bind(this); + this._boundTouchEndHandler = this._onTouchEnd.bind(this); + this._boundMouseDown = this._onMouseDown.bind(this); + this._boundTouchStart = this._onTouchStart.bind(this); + } + + bindEvents(draggableElements) { + draggableElements.forEach((element) => { + this.styleManager.applyDraggableStyles(element); + element.addEventListener('mousedown', this._boundMouseDown); + element.addEventListener('touchstart', this._boundTouchStart, { + passive: false, + }); + }); + } + + unbindEvents(draggableElements) { + draggableElements.forEach((element) => { + element.removeEventListener('mousedown', this._boundMouseDown); + element.removeEventListener('touchstart', this._boundTouchStart); + }); + } + + _onMouseDown(event) { + this.dragManager.startDrag(event, event.clientX, event.clientY); + document.addEventListener('mousemove', this._boundMouseMoveHandler); + document.addEventListener('mouseup', this._boundMouseUpHandler); + } + + _onTouchStart(event) { + event.preventDefault(); + const touchPoint = event.touches[0]; + this.dragManager.startDrag(event, touchPoint.clientX, touchPoint.clientY); + document.addEventListener('touchmove', this._boundTouchMoveHandler, { + passive: false, + }); + document.addEventListener('touchend', this._boundTouchEndHandler); + } + + _onMouseMove(event) { + this.dragManager.updateDragPosition(event.clientX, event.clientY); + } + + _onTouchMove(event) { + event.preventDefault(); + const touchPoint = event.touches[0]; + this.dragManager.updateDragPosition(touchPoint.clientX, touchPoint.clientY); + } + + _onMouseUp(event) { + this.dragManager.handleDrop(event.clientX, event.clientY, event); + document.removeEventListener('mousemove', this._boundMouseMoveHandler); + document.removeEventListener('mouseup', this._boundMouseUpHandler); + } + + _onTouchEnd(event) { + const touchPoint = event.changedTouches[0]; + this.dragManager.handleDrop(touchPoint.clientX, touchPoint.clientY, event); + document.removeEventListener('touchmove', this._boundTouchMoveHandler); + document.removeEventListener('touchend', this._boundTouchEndHandler); + } +} diff --git a/src/StyleManager.js b/src/StyleManager.js new file mode 100644 index 0000000..7794115 --- /dev/null +++ b/src/StyleManager.js @@ -0,0 +1,68 @@ +import { + CSS_CLASSES, + CSS_POSITIONS, + CSS_CURSORS, + OPACITY, + Z_INDEX, + FLEX_DIRECTIONS, + FLEX_WRAP, + DISPLAY_MODES, + POINTER_EVENTS, + STACK_DIRECTION, +} from './constants.js'; + +export class StyleManager { + applyDraggableStyles(element) { + element.style.cursor = CSS_CURSORS.GRAB; + element.classList.add(CSS_CLASSES.DRAGGABLE); + } + + applyDraggingStyles(element) { + element.style.cursor = CSS_CURSORS.GRABBING; + element.style.zIndex = Z_INDEX.DRAGGING; + } + + applyGhostStyles(element) { + element.style.pointerEvents = POINTER_EVENTS.NONE; + element.style.position = CSS_POSITIONS.ABSOLUTE; + element.style.zIndex = Z_INDEX.GHOST; + element.style.opacity = OPACITY.FULL; + } + + applyAbsolutePositioning(element) { + element.style.position = CSS_POSITIONS.ABSOLUTE; + } + + applyStackingStyles(dropzone, { stackDirection, stackSpacing }) { + dropzone.style.display = DISPLAY_MODES.FLEX; + dropzone.style.flexDirection = + stackDirection === STACK_DIRECTION.VERTICAL + ? FLEX_DIRECTIONS.COLUMN + : FLEX_DIRECTIONS.ROW; + dropzone.style.gap = `${stackSpacing}px`; + dropzone.style.flexWrap = FLEX_WRAP; + } + + removeStackingStyles(dropzone) { + dropzone.style.display = ''; + dropzone.style.flexDirection = ''; + dropzone.style.gap = ''; + dropzone.style.flexWrap = ''; + } + + resetPosition(element) { + element.style.left = ''; + element.style.top = ''; + element.style.position = CSS_POSITIONS.RELATIVE; + } + + resetDraggableStyles(element) { + element.style.cursor = CSS_CURSORS.GRAB; + element.style.zIndex = ''; + element.style.opacity = OPACITY.FULL; + } + + applyPlayAreaStyles(playAreaElement) { + playAreaElement.style.position = CSS_POSITIONS.RELATIVE; + } +} diff --git a/src/allonsh.js b/src/allonsh.js deleted file mode 100644 index dcac8d4..0000000 --- a/src/allonsh.js +++ /dev/null @@ -1,726 +0,0 @@ -import { - Z_INDEX, - DEFAULTS, - CSS_CLASSES, - CSS_POSITIONS, - CSS_CURSORS, - OPACITY, - EVENTS, - FLEX_DIRECTIONS, - FLEX_WRAP, - DISPLAY_MODES, - POINTER_EVENTS, - STACK_DIRECTION, -} from './constants.js'; - -class Allonsh { - constructor(options = {}) { - const { - draggableSelector, - dropzoneSelector = null, - playAreaSelector = null, - restrictToDropzones = false, - enableStacking = false, - stackDirection = DEFAULTS.STACK_DIRECTION, - stackSpacing = DEFAULTS.STACK_SPACING, - useGhostEffect = false, - } = options; - - if (!draggableSelector) { - throw new Error( - "Allonsh Error: 'draggableSelector' is required in options." - ); - } - - this.restrictToDropzones = restrictToDropzones; - this.enableStacking = enableStacking; - this.stackDirection = stackDirection; - this.stackSpacing = stackSpacing; - this.useGhostEffect = useGhostEffect; - - if (playAreaSelector) { - this.playAreaElement = document.querySelector(`.${playAreaSelector}`); - if (!this.playAreaElement) { - throw new Error( - `Allonsh Error: Play area element with class '${playAreaSelector}' not found.` - ); - } - } else { - this.playAreaElement = document.body; - } - - this.draggableElements = this.playAreaElement.querySelectorAll( - `.${draggableSelector}` - ); - if (this.draggableElements.length === 0) { - console.warn( - `Allonsh Warning: No draggable elements found with selector '.${draggableSelector}'.` - ); - } else { - this.draggableElements.forEach((element) => { - if (!element.classList.contains(CSS_CLASSES.DRAGGABLE)) { - element.classList.add(CSS_CLASSES.DRAGGABLE); - } - }); - } - - if (dropzoneSelector) { - this.dropzoneElements = this.playAreaElement.querySelectorAll( - `.${dropzoneSelector}` - ); - if (this.dropzoneElements.length === 0) { - console.warn( - `Allonsh Warning: No dropzone elements found with selector '.${dropzoneSelector}'.` - ); - } - } else { - this.dropzoneElements = []; - } - - this._dropzoneSet = new Set(this.dropzoneElements); - - this.currentDraggedElement = null; - this.dragOffsetX = 0; - this.dragOffsetY = 0; - - this.ghostElement = null; - this.originalParent = null; - this.originalDropzone = null; - - this._boundMouseMoveHandler = this._onMouseMove.bind(this); - this._boundMouseUpHandler = this._onMouseUp.bind(this); - this._boundTouchMoveHandler = this._onTouchMove.bind(this); - this._boundTouchEndHandler = this._onTouchEnd.bind(this); - this._boundMouseDown = this._onMouseDown.bind(this); - this._boundTouchStart = this._onTouchStart.bind(this); - - try { - this._initialize(); - } catch (error) { - console.error('Allonsh Initialization Error:', error); - } - } - - update(newOptions = {}) { - const { - draggableSelector, - dropzoneSelector, - playAreaSelector, - restrictToDropzones, - enableStacking, - stackDirection, - stackSpacing, - useGhostEffect, - } = newOptions; - - if (restrictToDropzones !== undefined) - this.restrictToDropzones = restrictToDropzones; - if (enableStacking !== undefined) this.enableStacking = enableStacking; - if (stackDirection !== undefined) this.stackDirection = stackDirection; - if (stackSpacing !== undefined) this.stackSpacing = stackSpacing; - if (useGhostEffect !== undefined) this.useGhostEffect = useGhostEffect; - - if (playAreaSelector) { - const newPlayArea = document.querySelector(`.${playAreaSelector}`); - if (newPlayArea) { - this.playAreaElement = newPlayArea; - this.playAreaElement.style.position = CSS_POSITIONS.RELATIVE; - } else { - console.warn( - `Allonsh Warning: Play area element with class '${playAreaSelector}' not found.` - ); - } - } - - this.draggableElements = this.playAreaElement.querySelectorAll( - `.${draggableSelector}` - ); - - this.draggableElements.forEach((element) => { - this._unbindEventListeners(); - element.style.cursor = CSS_CURSORS.GRAB; - element.addEventListener('mousedown', this._boundMouseDown); - element.addEventListener('touchstart', this._boundTouchStart, { - passive: false, - }); - }); - - if (dropzoneSelector) { - this.dropzoneElements = this.playAreaElement.querySelectorAll( - `.${dropzoneSelector}` - ); - this._dropzoneSet = new Set(this.dropzoneElements); - } - - if (this.enableStacking) { - if (this.dropzoneElements) { - this.dropzoneElements.forEach((dropzone) => { - this._applyStackingStyles(dropzone); - }); - } - } else { - if (this.dropzoneElements) { - this.dropzoneElements.forEach((dropzone) => { - this._removeStackingStyles(dropzone); - }); - } - } - } - - _initialize() { - if (!this.playAreaElement) { - throw new Error( - 'Allonsh Initialization Error: Play area element is not defined.' - ); - } - this.playAreaElement.style.position = CSS_POSITIONS.RELATIVE; - - if (!this.draggableElements || this.draggableElements.length === 0) { - console.warn('Allonsh Warning: No draggable elements to initialize.'); - return; - } - - this.draggableElements.forEach((element) => { - try { - element.style.cursor = CSS_CURSORS.GRAB; - element.addEventListener('mousedown', this._onMouseDown.bind(this)); - element.addEventListener('touchstart', this._onTouchStart.bind(this), { - passive: false, - }); - } catch (err) { - console.error( - 'Allonsh Error attaching event listeners to draggable element:', - err - ); - } - }); - - if (this.dropzoneElements) { - this.dropzoneElements.forEach((dropzone) => { - try { - dropzone.classList.add(CSS_CLASSES.DROPZONE); - if (this.enableStacking) { - this._applyStackingStyles(dropzone); - } - } catch (err) { - console.error('Allonsh Error initializing dropzone element:', err); - } - }); - } - } - - _unbindEventListeners() { - if (!this.draggableElements) return; - - this.draggableElements.forEach((element) => { - element.removeEventListener('mousedown', this._boundMouseDown); - element.removeEventListener('touchstart', this._boundTouchStart); - }); - } - - _applyStackingStyles(dropzone) { - try { - dropzone.style.display = DISPLAY_MODES.FLEX; - dropzone.style.flexDirection = - this.stackDirection === STACK_DIRECTION.VERTICAL - ? FLEX_DIRECTIONS.COLUMN - : FLEX_DIRECTIONS.ROW; - dropzone.style.gap = `${this.stackSpacing}px`; - dropzone.style.flexWrap = FLEX_WRAP; - } catch (err) { - console.error('Allonsh Error applying stacking styles:', err); - } - } - - _removeStackingStyles(dropzone) { - try { - dropzone.style.display = ''; - dropzone.style.flexDirection = ''; - dropzone.style.gap = ''; - dropzone.style.flexWrap = ''; - } catch (err) { - console.error('Allonsh Error removing stacking styles:', err); - } - } - - _onMouseDown(event) { - try { - this._onPointerDown(event.clientX, event.clientY, event); - document.addEventListener('mousemove', this._boundMouseMoveHandler); - document.addEventListener('mouseup', this._boundMouseUpHandler); - } catch (err) { - console.error('Allonsh Error during mousedown event:', err); - } - } - - _onTouchStart(event) { - try { - event.preventDefault(); - const touchPoint = event.touches[0]; - this._onPointerDown(touchPoint.clientX, touchPoint.clientY, event); - document.addEventListener('touchmove', this._boundTouchMoveHandler, { - passive: false, - }); - document.addEventListener('touchend', this._boundTouchEndHandler); - } catch (err) { - console.error('Allonsh Error during touchstart event:', err); - } - } - - _onPointerDown(clientX, clientY, event) { - this._startDrag(event, clientX, clientY); - } - - _onMouseMove(event) { - try { - this._updateDragPosition(event.clientX, event.clientY); - } catch (err) { - console.error('Allonsh Error during mousemove event:', err); - } - } - - _onTouchMove(event) { - try { - event.preventDefault(); - const touchPoint = event.touches[0]; - this._updateDragPosition(touchPoint.clientX, touchPoint.clientY); - } catch (err) { - console.error('Allonsh Error during touchmove event:', err); - } - } - - _onMouseUp(event) { - try { - this._onPointerUp(event.clientX, event.clientY, event); - document.removeEventListener('mousemove', this._boundMouseMoveHandler); - document.removeEventListener('mouseup', this._boundMouseUpHandler); - } catch (err) { - console.error('Allonsh Error during mouseup event:', err); - } - } - - _onTouchEnd(event) { - try { - const touchPoint = event.changedTouches[0]; - this._onPointerUp(touchPoint.clientX, touchPoint.clientY, event); - document.removeEventListener('touchmove', this._boundTouchMoveHandler); - document.removeEventListener('touchend', this._boundTouchEndHandler); - } catch (err) { - console.error('Allonsh Error during touchend event:', err); - } - } - - _onPointerUp(clientX, clientY, event) { - this.currentDraggedElement.style.opacity = OPACITY.FULL; - this._handleDrop(clientX, clientY, event); - } - - _startDrag(event, clientX, clientY) { - try { - this.currentDraggedElement = event.currentTarget; - this.currentDraggedElement.style.cursor = CSS_CURSORS.GRABBING; - - this.originalParent = this.currentDraggedElement.parentElement; - this.originalDropzone = this._findClosestDropzone( - this.currentDraggedElement - ); - - if (!this.currentDraggedElement) { - throw new Error('Dragged element is null or undefined.'); - } - - const playAreaRect = this.playAreaElement.getBoundingClientRect(); - const rect = this.currentDraggedElement.getBoundingClientRect(); - - if (this.originalParent !== this.playAreaElement && this.useGhostEffect) { - if (this.ghostElement && this.ghostElement.parentElement) { - this.ghostElement.parentElement.removeChild(this.ghostElement); - this.ghostElement = null; - } - - this.ghostElement = this.currentDraggedElement.cloneNode(true); - this.ghostElement.style.pointerEvents = POINTER_EVENTS.NONE; - this.ghostElement.style.position = CSS_POSITIONS.ABSOLUTE; - - this.ghostElement.style.zIndex = Z_INDEX.GHOST; - - this.currentDraggedElement.style.opacity = OPACITY.GHOST; - this.playAreaElement.appendChild(this.ghostElement); - - this.ghostElement.style.left = `${rect.left - playAreaRect.left}px`; - this.ghostElement.style.top = `${rect.top - playAreaRect.top}px`; - } else { - this.ghostElement = null; - } - - this.dragOffsetX = clientX - rect.left; - this.dragOffsetY = clientY - rect.top; - - this.currentDraggedElement.style.cursor = CSS_CURSORS.GRABBING; - this.currentDraggedElement.style.zIndex = Z_INDEX.DRAGGING; - - this.currentDraggedElement.dispatchEvent( - new CustomEvent(EVENTS.DRAG_START, { - detail: { originalEvent: event }, - }) - ); - - this._toggleDropzoneHighlight(true); - } catch (err) { - console.error('Allonsh Error in _startDrag:', err); - } - } - - _handleElementDrag(element, isGhost = false) { - try { - if (!element) { - throw new Error('Element is null or undefined.'); - } - - const style = element.style; - - if (isGhost) { - style.pointerEvents = POINTER_EVENTS.NONE; - style.position = CSS_POSITIONS.ABSOLUTE; - style.zIndex = Z_INDEX.GHOST; - style.opacity = OPACITY.GHOST; - } else { - style.cursor = CSS_CURSORS.GRABBING; - style.zIndex = Z_INDEX.DRAGGING; - } - - if (isGhost && element !== this.ghostElement) { - this.ghostElement = element; - this.playAreaElement.appendChild(this.ghostElement); - } - } catch (err) { - console.error( - `Allonsh Error handling ${isGhost ? 'ghost' : 'dragged'} element:`, - err - ); - } - } - - _updateDragPosition(clientX, clientY) { - if (!this.currentDraggedElement) { - console.warn('Allonsh Warning: No element is currently being dragged.'); - return; - } - - try { - if (this.ghostElement) { - const playAreaRect = this.playAreaElement.getBoundingClientRect(); - - let newLeft = clientX - playAreaRect.left - this.dragOffsetX; - let newTop = clientY - playAreaRect.top - this.dragOffsetY; - - const ghostRect = this.ghostElement.getBoundingClientRect(); - - const maxLeft = playAreaRect.width - ghostRect.width; - const maxTop = playAreaRect.height - ghostRect.height; - - newLeft = Math.max(0, Math.min(newLeft, maxLeft)); - newTop = Math.max(0, Math.min(newTop, maxTop)); - - this.ghostElement.style.left = `${newLeft}px`; - this.ghostElement.style.top = `${newTop}px`; - } else { - this.currentDraggedElement.style.position = CSS_POSITIONS.ABSOLUTE; - - const playAreaRect = this.playAreaElement.getBoundingClientRect(); - const draggedRect = this.currentDraggedElement.getBoundingClientRect(); - - let newLeft = clientX - playAreaRect.left - this.dragOffsetX; - let newTop = clientY - playAreaRect.top - this.dragOffsetY; - - const maxLeft = this.playAreaElement.clientWidth - draggedRect.width; - const maxTop = this.playAreaElement.clientHeight - draggedRect.height; - - newLeft = Math.max(0, Math.min(newLeft, maxLeft)); - newTop = Math.max(0, Math.min(newTop, maxTop)); - - this.currentDraggedElement.style.left = `${newLeft}px`; - this.currentDraggedElement.style.top = `${newTop}px`; - } - } catch (err) { - console.error('Allonsh Error updating drag position:', err); - } - } - - _handleDrop(clientX, clientY, event) { - if (!this.currentDraggedElement) { - console.warn( - 'Allonsh Warning: Drop attempted without a dragged element.' - ); - return; - } - - try { - const playAreaRect = this.playAreaElement.getBoundingClientRect(); - - const clampedX = Math.min( - Math.max(clientX, playAreaRect.left + 1), - playAreaRect.right - 1 - ); - const clampedY = Math.min( - Math.max(clientY, playAreaRect.top + 1), - playAreaRect.bottom - 1 - ); - - this.currentDraggedElement.style.pointerEvents = POINTER_EVENTS.NONE; - let elementBelow = document.elementFromPoint(clampedX, clampedY); - this.currentDraggedElement.style.pointerEvents = POINTER_EVENTS.AUTO; - - if (!elementBelow) { - if (this.restrictToDropzones) { - this._returnToOrigin(); - } - this._resetDraggedElementState(); - return; - } - - let dropzoneFound = null; - let el = elementBelow; - - while (el && el !== this.playAreaElement) { - if (this._dropzoneSet.has(el)) { - dropzoneFound = el; - break; - } - el = el.parentElement; - } - - if (dropzoneFound) { - this.currentDropzone = dropzoneFound; - - if (this.enableStacking) { - this._applyStackingStyles(this.currentDropzone); - } - - this._resetPositionToRelative(this.currentDraggedElement); - - if (this.ghostElement && this.ghostElement.parentElement) { - this.ghostElement.parentElement.removeChild(this.ghostElement); - this.ghostElement = null; - } - - try { - this.currentDropzone.appendChild(this.currentDraggedElement); - } catch (e) { - console.warn( - 'Allonsh Warning: Could not append dragged element to dropzone.', - e - ); - } - - this.currentDropzone.dispatchEvent( - new CustomEvent(EVENTS.DROP, { - detail: { - draggedElement: this.currentDraggedElement, - originalEvent: event, - }, - }) - ); - - this.currentDropzone.dispatchEvent( - new CustomEvent(EVENTS.DRAG_ENTER, { - detail: { draggedElement: this.currentDraggedElement }, - }) - ); - this.currentDropzone.dispatchEvent( - new CustomEvent(EVENTS.DRAG_LEAVE, { - detail: { draggedElement: this.currentDraggedElement }, - }) - ); - - this.currentDropzone.classList.remove(CSS_CLASSES.HIGHLIGHT); - this.currentDropzone = null; - } else { - if (this.ghostElement && this.ghostElement.parentElement) { - try { - this.ghostElement.parentElement.removeChild(this.ghostElement); - this.ghostElement = null; - } catch (e) { - console.warn('Allonsh Warning: Failed to remove ghost element.', e); - } - } - - if (this.restrictToDropzones) { - this._returnToOrigin(); - } else { - if ( - this.currentDraggedElement.parentElement !== this.playAreaElement - ) { - try { - this.playAreaElement.appendChild(this.currentDraggedElement); - - const offsetX = clampedX - playAreaRect.left - this.dragOffsetX; - const offsetY = clampedY - playAreaRect.top - this.dragOffsetY; - - this.currentDraggedElement.style.position = - CSS_POSITIONS.ABSOLUTE; - this.currentDraggedElement.style.left = `${offsetX}px`; - this.currentDraggedElement.style.top = `${offsetY}px`; - this.currentDraggedElement.style.zIndex = Z_INDEX.DRAGGING; - } catch (e) { - console.warn( - 'Allonsh Warning: Could not append dragged element back to play area.', - e - ); - } - } - } - } - - this._resetDraggedElementState(); - } catch (err) { - console.error('Allonsh Error handling drop:', err); - if (this.restrictToDropzones) { - this._returnToOrigin(); - } - this._resetDraggedElementState(); - } - } - - _returnToOrigin() { - try { - if ( - this.originalDropzone && - !this._isDropzoneRestricted(this.originalDropzone) - ) { - if (this.enableStacking) { - this._applyStackingStyles(this.originalDropzone); - } - this._resetPositionToRelative(this.currentDraggedElement); - this.originalDropzone.appendChild(this.currentDraggedElement); - } else { - this._resetPositionToRelative(this.currentDraggedElement); - this.playAreaElement.appendChild(this.currentDraggedElement); - } - } catch (err) { - console.error('Allonsh Error returning element to origin:', err); - } - } - - _isDropzoneRestricted(dropzone) { - return dropzone && dropzone.classList.contains(CSS_CLASSES.RESTRICTED); - } - - _resetDraggedElementState() { - if (!this.currentDraggedElement) return; - - try { - this.currentDraggedElement.style.cursor = CSS_CURSORS.GRAB; - this.currentDraggedElement.style.zIndex = ''; - this._toggleDropzoneHighlight(false); - - if ( - this.useGhostEffect && - this.ghostElement && - this.ghostElement.parentElement - ) { - this.ghostElement.parentElement.removeChild(this.ghostElement); - this.ghostElement = null; - } - } catch (err) { - console.error('Allonsh Error resetting dragged element state:', err); - } finally { - this.currentDraggedElement = null; - this.originalParent = null; - this.originalDropzone = null; - } - } - - _findClosestDropzone(element) { - let el = element; - while (el && el !== this.playAreaElement) { - if (this._dropzoneSet.has(el)) { - return el; - } - el = el.parentElement; - } - return null; - } - - _resetPositionToRelative(element) { - try { - element.style.left = ''; - element.style.top = ''; - element.style.position = CSS_POSITIONS.RELATIVE; - } catch (err) { - console.error('Allonsh Error resetting element position:', err); - } - } - - _toggleDropzoneHighlight(enable) { - try { - this.dropzoneElements.forEach((dropzone) => { - dropzone.classList.toggle(CSS_CLASSES.HIGHLIGHT, enable); - }); - } catch (err) { - console.error('Allonsh Error toggling dropzone highlight:', err); - } - } - - resetAll() { - try { - this.draggableElements.forEach((el) => { - if (this.playAreaElement.contains(el)) { - this.playAreaElement.appendChild(el); - this._resetPositionToRelative(el); - } - }); - } catch (err) { - console.error('Allonsh Error resetting all draggables:', err); - } - } - - addDraggable(element) { - try { - if (!element.classList.contains(CSS_CLASSES.DRAGGABLE)) { - element.classList.add(CSS_CLASSES.DRAGGABLE); - } - element.style.cursor = CSS_CURSORS.GRAB; - element.addEventListener('mousedown', this._boundMouseDown); - element.addEventListener('touchstart', this._boundTouchStart, { - passive: false, - }); - - this.draggableElements = this.playAreaElement.querySelectorAll( - `.${CSS_CLASSES.DRAGGABLE}` - ); - } catch (err) { - console.error('Allonsh Error adding draggable element:', err); - } - } - - removeDraggable(element) { - try { - element.removeEventListener('mousedown', this._boundMouseDown); - element.removeEventListener('touchstart', this._boundTouchStart); - - if (element.classList.contains(CSS_CLASSES.DRAGGABLE)) { - element.classList.remove(CSS_CLASSES.DRAGGABLE); - } - - this.draggableElements = this.playAreaElement.querySelectorAll( - `.${CSS_CLASSES.DRAGGABLE}` - ); - } catch (err) { - console.error('Allonsh Error removing draggable element:', err); - } - } - - setDropzones(selector) { - try { - this.dropzoneElements = this.playAreaElement.querySelectorAll( - `.${selector}` - ); - this._dropzoneSet = new Set(this.dropzoneElements); - } catch (err) { - console.error('Allonsh Error setting dropzones:', err); - } - } -} - -export default Allonsh; diff --git a/src/constants.js b/src/constants.js index c1243c4..8ce6a97 100644 --- a/src/constants.js +++ b/src/constants.js @@ -31,7 +31,7 @@ export const CSS_CURSORS = { }; export const OPACITY = { - GHOST: '0.3', + GHOST: '0.5', FULL: '1', };