import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop'; import { _getShadowRoot } from '@angular/cdk/platform'; import { CommonModule } from '@angular/common'; import { Component, ElementRef, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model'; import { environment } from '../../../environments/environment'; @Component({ selector: 'app-drag-drop-mixed', standalone: true, imports: [CommonModule, DragDropModule], templateUrl: './drag-drop-mixed.component.html', styleUrl: './drag-drop-mixed.component.scss', }) export class DragDropMixedComponent { @ViewChild('_container') _container!: ElementRef; @ViewChildren(CdkDrag) _drags!: QueryList; listing = input(); imageOrderChanged = output(); imageToDelete = output(); env = environment; ts = new Date().getTime(); items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9]; private _cachedItems: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9]; private _itemPositions: CachedItemPosition[] = []; private _rootNode: DocumentOrShadowRoot | undefined; private _activeItems: DragRef[] = []; private _previousSwap = { drag: null as DragRef | null, deltaX: 0, deltaY: 0, overlaps: false, }; private _containerStyle: CSSStyleDeclaration | null = null; public isAnimationActive = false; ngOnChanges() { this.items = this.listing()?.imageOrder; this._cachedItems = this.items.slice(); } getImageUrl(image: string): string { return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`; } dragStarted() { this.start(); } dragMoved(event: CdkDragMove) { const item = event.source._dragRef; this.sort(item, event.pointerPosition.x, event.pointerPosition.y, event.delta); } dragEnded(event: CdkDragEnd) { this.imageOrderChanged.emit(this._cachedItems); this.reset(); } start() { const dragRefs: DragRef[] = []; this._drags.forEach(drag => { dragRefs.push(drag._dragRef); }); this._activeItems = dragRefs; this._cacheItemPosition(); this.isAnimationActive = true; } sort(item: DragRef, pointerX: number, pointerY: number, pointerDelta: { x: number; y: number }) { const siblings = this._itemPositions.slice(); const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); const previousSwap = this._previousSwap; if (newIndex === -1 || this._activeItems[newIndex] === item) { return; } const toSwapWith = this._activeItems[newIndex]; if (previousSwap.drag === toSwapWith && previousSwap.overlaps && previousSwap.deltaX === pointerDelta.x && previousSwap.deltaY === pointerDelta.y) { return; } const previousIndex = this.getItemIndex(item); const siblingAtNewPosition = siblings[newIndex]; const previousPosition = siblings[previousIndex].clientRect; const newPosition = siblingAtNewPosition.clientRect; const delta = this.getDelta(newPosition.top, previousPosition.top, pointerDelta); if (delta === 0) return; if (delta === 1 && previousIndex > newIndex) return; if (delta === -1 && previousIndex < newIndex) return; const startIndex = Math.min(previousIndex, newIndex); const endIndex = Math.max(previousIndex, newIndex); let itemPositions = this._itemPositions.slice(); if (delta === 1) { for (let i = startIndex; i < endIndex; i++) { itemPositions = this._updateItemPosition(i, itemPositions, delta); const newIndex = i + 1; moveItemInArray(itemPositions, i, newIndex); } } else if (delta === -1) { for (let i = endIndex; i > startIndex; i--) { itemPositions = this._updateItemPosition(i, itemPositions, delta); const newIndex = i - 1; moveItemInArray(itemPositions, i, newIndex); } } const threshold = getMutableClientRect(this._container.nativeElement).right; let currentTop = itemPositions[0].clientRect.top; for (let i = 0; i < itemPositions.length; i++) { const itemPosition = itemPositions[i]; if (Math.round(itemPosition.clientRect.right) > Math.round(threshold)) { const nextPosition = itemPositions[i + 1]; if (nextPosition) { currentTop = nextPosition.clientRect.top; } itemPositions = this._updateItemPositionToDown(itemPositions, i); } else if (itemPosition.clientRect.top !== currentTop) { currentTop = itemPosition.clientRect.top; itemPositions = this._updateItemPositionToUp(itemPositions, i); } } const oldOrder = this._itemPositions.slice(); this._itemPositions = itemPositions; moveItemInArray(this._activeItems, previousIndex, newIndex); moveItemInArray(this._cachedItems, previousIndex, newIndex); itemPositions.forEach((sibling, index) => { if (oldOrder[index] === sibling) { return; } const isDraggedItem = sibling.drag === item; if (isDraggedItem) return; const elementToOffset = sibling.drag.getRootElement(); elementToOffset.style.transform = `translate3d(${Math.round(sibling.transform.x)}px, ${Math.round(sibling.transform.y)}px, 0)`; }); previousSwap.deltaX = pointerDelta.x; previousSwap.deltaY = pointerDelta.y; previousSwap.drag = toSwapWith; previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY); } reset() { // ignore animation this.isAnimationActive = false; const previousSwap = this._previousSwap; this.items = this._cachedItems.slice(); this._activeItems.forEach(item => { item.reset(); }); this._itemPositions = []; this._activeItems = []; previousSwap.drag = null; previousSwap.deltaX = previousSwap.deltaY = 0; previousSwap.overlaps = false; } getItemIndex(item: DragRef): number { return this._activeItems.indexOf(item); } getDelta(newTop: number, previousTop: number, pointerDelta: { x: number; y: number }) { if (newTop === previousTop) { return pointerDelta.x; } return newTop > previousTop ? 1 : -1; } private _getRootNode(): DocumentOrShadowRoot { if (!this._rootNode) { this._rootNode = _getShadowRoot(this._container.nativeElement) || document; } return this._rootNode; } private _cacheItemPosition() { this._itemPositions = this._activeItems.map(drag => { const elementToMeasure = drag.getRootElement(); return { drag, clientRect: getMutableClientRect(elementToMeasure), transform: { x: 0, y: 0, }, }; }); this._containerStyle = getComputedStyle(this._container.nativeElement); } private _updateItemPosition(currentIndex: number, siblings: CachedItemPosition[], delta: number) { let siblingsUpdated = siblings.slice(); const offsetVertical = this._getOffset(currentIndex, siblingsUpdated, delta, false); const offsetHorizontal = this._getOffset(currentIndex, siblingsUpdated, delta, true); const immediateIndex = currentIndex + delta * 1; const currentItem = siblingsUpdated[currentIndex]; const immediateSibling = siblingsUpdated[immediateIndex]; const currentItemUpdated: CachedItemPosition = { ...currentItem, clientRect: { ...currentItem.clientRect, x: currentItem.clientRect.x + offsetHorizontal.itemOffset, left: currentItem.clientRect.left + offsetHorizontal.itemOffset, right: currentItem.clientRect.right + offsetHorizontal.itemOffset, y: currentItem.clientRect.y + offsetVertical.itemOffset, top: currentItem.clientRect.top + offsetVertical.itemOffset, bottom: currentItem.clientRect.bottom + offsetVertical.itemOffset, }, transform: { x: currentItem.transform.x + offsetHorizontal.itemOffset, y: currentItem.transform.y + offsetVertical.itemOffset, }, }; const immediateSiblingUpdated: CachedItemPosition = { ...immediateSibling, clientRect: { ...immediateSibling.clientRect, x: immediateSibling.clientRect.x + offsetHorizontal.siblingOffset, left: immediateSibling.clientRect.left + offsetHorizontal.siblingOffset, right: immediateSibling.clientRect.right + offsetHorizontal.siblingOffset, y: immediateSibling.clientRect.y + offsetVertical.siblingOffset, top: immediateSibling.clientRect.top + offsetVertical.siblingOffset, bottom: immediateSibling.clientRect.bottom + offsetVertical.siblingOffset, }, transform: { x: immediateSibling.transform.x + offsetHorizontal.siblingOffset, y: immediateSibling.transform.y + offsetVertical.siblingOffset, }, }; if (offsetVertical.itemOffset !== offsetVertical.siblingOffset) { const offset = (currentItemUpdated.clientRect.right - immediateSibling.clientRect.right) * delta; const top = delta === 1 ? immediateSibling.clientRect.top : currentItem.clientRect.top; const ignoreItem = delta === 1 ? immediateSibling.drag : currentItem.drag; siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, top, offset, ignoreItem); } siblingsUpdated[currentIndex] = currentItemUpdated; siblingsUpdated[immediateIndex] = immediateSiblingUpdated; return siblingsUpdated; } private _updateItemPositionToUp(siblings: CachedItemPosition[], currentIndex: number) { let siblingsUpdated = siblings.slice(); const immediateSibling = siblingsUpdated[currentIndex - 1]; const currentItem = siblingsUpdated[currentIndex]; const nextEmptySlotLeft = immediateSibling.clientRect.right + this.getContainerGapPixel(); const threshold = getMutableClientRect(this._container.nativeElement).right; if (nextEmptySlotLeft + currentItem.clientRect.right - currentItem.clientRect.left <= threshold) { const offsetLeft = nextEmptySlotLeft - currentItem.clientRect.left; const offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top; const nextSibling = siblingsUpdated[currentIndex + 1]; if (nextSibling) { const offset = currentItem.clientRect.left - nextSibling.clientRect.left; siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, currentItem.clientRect.top, offset, currentItem.drag); } siblingsUpdated[currentIndex] = { ...currentItem, clientRect: { ...currentItem.clientRect, x: nextEmptySlotLeft, left: nextEmptySlotLeft, right: currentItem.clientRect.right - currentItem.clientRect.left + nextEmptySlotLeft, y: immediateSibling.clientRect.y, top: immediateSibling.clientRect.top, bottom: currentItem.clientRect.bottom - currentItem.clientRect.top + immediateSibling.clientRect.top, }, transform: { x: currentItem.transform.x + offsetLeft, y: currentItem.transform.y + offsetTop, }, }; } return siblingsUpdated; } private _updateItemPositionToDown(siblings: CachedItemPosition[], currentIndex: number) { let siblingsUpdated = siblings.slice(); const currentItem = siblingsUpdated[currentIndex]; const immediateSibling = siblingsUpdated[currentIndex + 1]; let offsetLeft = 0; let offsetTop = 0; if (immediateSibling) { offsetLeft = immediateSibling.clientRect.left - currentItem.clientRect.left; offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top; } else { const firstSibling = siblings.find(item => item.clientRect.top === currentItem.clientRect.top); if (firstSibling) { offsetLeft = firstSibling.clientRect.left - currentItem.clientRect.left; } offsetTop = currentItem.clientRect.bottom - currentItem.clientRect.top + this.getContainerGapPixel(); } const currentItemUpdated: CachedItemPosition = { ...currentItem, clientRect: { ...currentItem.clientRect, x: currentItem.clientRect.x + offsetLeft, left: currentItem.clientRect.left + offsetLeft, right: currentItem.clientRect.right + offsetLeft, y: currentItem.clientRect.y + offsetTop, top: currentItem.clientRect.top + offsetTop, bottom: currentItem.clientRect.bottom + offsetTop, }, transform: { x: currentItem.transform.x + offsetLeft, y: currentItem.transform.y + offsetTop, }, }; if (immediateSibling) { const offset = currentItemUpdated.clientRect.right - immediateSibling.clientRect.left + this.getContainerGapPixel(); siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, immediateSibling.clientRect.top, offset); } siblingsUpdated[currentIndex] = currentItemUpdated; return siblingsUpdated; } private _updateItemPositionHorizontalOnRow(siblings: CachedItemPosition[], top: number, offset: number, ignoreItem?: DragRef) { const siblingsUpdated = siblings.slice(); siblingsUpdated .filter(item => (!ignoreItem || item.drag !== ignoreItem) && item.clientRect.top === top) .forEach(currentItem => { const index = siblingsUpdated.findIndex(item => item.drag === currentItem.drag); siblingsUpdated[index] = { ...siblingsUpdated[index], clientRect: { ...siblingsUpdated[index].clientRect, x: siblingsUpdated[index].clientRect.x + offset, left: siblingsUpdated[index].clientRect.left + offset, right: siblingsUpdated[index].clientRect.right + offset, }, transform: { ...siblingsUpdated[index].transform, x: siblingsUpdated[index].transform.x + offset, }, }; }); return siblingsUpdated; } private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number) { const elementAtPoints = this._getRootNode().elementsFromPoint(Math.floor(pointerX), Math.floor(pointerY)); const elementAtPoint = elementAtPoints.find(element => { // ignore element is transiting const animations = element.getAnimations(); const isTransitionRunning = animations.length > 0; return !isTransitionRunning && this._itemPositions.some(item => item.drag.getRootElement() === element) && element !== item.getRootElement(); }); const index = elementAtPoint ? this._itemPositions.findIndex(({ drag }) => { // Skip the item itself. if (drag === item) { return false; } const root = drag.getRootElement(); return elementAtPoint === root || root.contains(elementAtPoint); }) : -1; return index; } private _getOffset(currentIndex: number, siblings: CachedItemPosition[], delta: number, isHorizontal: boolean) { const currentPosition = siblings[currentIndex].clientRect; const immediateSibling = siblings[currentIndex + delta].clientRect; let itemOffset = 0; let siblingOffset = 0; if (immediateSibling) { const start = isHorizontal ? 'left' : 'top'; const end = isHorizontal ? 'right' : 'bottom'; if (delta === 1) { itemOffset = immediateSibling[end] - currentPosition[end]; siblingOffset = currentPosition[start] - immediateSibling[start]; if (isHorizontal && immediateSibling[end] < currentPosition[end]) { itemOffset = immediateSibling[start] - currentPosition[start]; } } else { itemOffset = immediateSibling[start] - currentPosition[start]; siblingOffset = currentPosition[end] - immediateSibling[end]; if (isHorizontal && immediateSibling[end] > currentPosition[end]) { siblingOffset = currentPosition[start] - immediateSibling[start]; } } } return { itemOffset, siblingOffset, }; } private getContainerGapPixel() { if (this._containerStyle && (this._containerStyle.display === 'flex' || this._containerStyle.display === 'grid')) { return this._containerStyle.gap ? +this._containerStyle.gap.split('px')[0] : 0; } return 0; } } const getMutableClientRect = (element: Element): DOMRect => { const rect = element.getBoundingClientRect(); return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, width: rect.width, height: rect.height, x: rect.x, y: rect.y, } as DOMRect; }; const isInsideClientRect = (clientRect: DOMRect, x: number, y: number) => { const { top, bottom, left, right } = clientRect; return y >= top && y <= bottom && x >= left && x <= right; }; interface CachedItemPosition { drag: T; clientRect: DOMRect; transform: { x: number; y: number; }; }