From 7f67b812428de376fb9d05413b961d079822cd87 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 9 Jul 2024 14:32:20 +0200 Subject: [PATCH] editCommercialProps, confirmationService, MessageService, Drag & Drop --- bizmatch-server/src/drizzle/import.ts | 2 +- bizmatch/package.json | 5 +- .../confirmation/confirmation.component.ts | 50 ++ .../confirmation/confirmation.service.ts | 32 ++ .../drag-drop-mixed.component.html | 24 + .../drag-drop-mixed.component.scss | 41 ++ .../drag-drop-mixed.component.ts | 476 ++++++++++++++++++ .../components/message/message.component.ts | 35 ++ .../app/components/message/message.service.ts | 32 ++ .../mixed-cdk-drag-drop.directive.ts | 197 ++++++++ ...commercial-property-listing.component.html | 4 +- .../subscription/account/account.component.ts | 3 +- .../edit-business-listing.component.ts | 4 +- ...commercial-property-listing.component.html | 58 ++- ...commercial-property-listing.component.scss | 49 ++ ...t-commercial-property-listing.component.ts | 100 ++-- 16 files changed, 1062 insertions(+), 50 deletions(-) create mode 100644 bizmatch/src/app/components/confirmation/confirmation.component.ts create mode 100644 bizmatch/src/app/components/confirmation/confirmation.service.ts create mode 100644 bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.html create mode 100644 bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.scss create mode 100644 bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.ts create mode 100644 bizmatch/src/app/components/message/message.component.ts create mode 100644 bizmatch/src/app/components/message/message.service.ts create mode 100644 bizmatch/src/app/directives/mixed-cdk-drag-drop.directive.ts diff --git a/bizmatch-server/src/drizzle/import.ts b/bizmatch-server/src/drizzle/import.ts index 1b123e2..093163d 100644 --- a/bizmatch-server/src/drizzle/import.ts +++ b/bizmatch-server/src/drizzle/import.ts @@ -137,7 +137,7 @@ function getFilenames(id: string): string[] { let filePath = `./pictures_base/property/${id}`; return readdirSync(filePath); } catch (e) { - return null; + return []; } } function getRandomDateWithinLastYear(): Date { diff --git a/bizmatch/package.json b/bizmatch/package.json index 6cba5a2..a494ce5 100644 --- a/bizmatch/package.json +++ b/bizmatch/package.json @@ -30,11 +30,9 @@ "@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@ng-select/ng-select": "^13.4.1", + "@types/cropperjs": "^1.3.0", "@types/uuid": "^10.0.0", - "angular-cropperjs": "^14.0.1", - "angular-mixed-cdk-drag-drop": "^2.2.3", "browser-bunyan": "^1.8.0", - "cropperjs": "^1.6.2", "dayjs": "^1.11.11", "express": "^4.18.2", "flowbite": "^2.4.1", @@ -43,6 +41,7 @@ "keycloak-js": "^25.0.1", "memoize-one": "^6.0.0", "ngx-currency": "^18.0.0", + "ngx-image-cropper": "^8.0.0", "ngx-quill": "^26.0.5", "on-change": "^5.0.1", "rxjs": "~7.8.1", diff --git a/bizmatch/src/app/components/confirmation/confirmation.component.ts b/bizmatch/src/app/components/confirmation/confirmation.component.ts new file mode 100644 index 0000000..5f31681 --- /dev/null +++ b/bizmatch/src/app/components/confirmation/confirmation.component.ts @@ -0,0 +1,50 @@ +import { AsyncPipe, NgIf } from '@angular/common'; +import { Component } from '@angular/core'; +import { ConfirmationService } from './confirmation.service'; + +@Component({ + selector: 'app-confirmation', + standalone: true, + imports: [AsyncPipe, NgIf], + template: ` +
+
+
+ +
+ +

{{ confirmationService.message$ | async }}

+ + +
+
+
+
+ `, +}) +export class ConfirmationComponent { + constructor(public confirmationService: ConfirmationService) {} +} diff --git a/bizmatch/src/app/components/confirmation/confirmation.service.ts b/bizmatch/src/app/components/confirmation/confirmation.service.ts new file mode 100644 index 0000000..f394d3e --- /dev/null +++ b/bizmatch/src/app/components/confirmation/confirmation.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfirmationService { + private modalVisibleSubject = new BehaviorSubject(false); + private messageSubject = new BehaviorSubject(''); + private resolvePromise!: (value: boolean) => void; + + modalVisible$: Observable = this.modalVisibleSubject.asObservable(); + message$: Observable = this.messageSubject.asObservable(); + + showConfirmation(message: string): Promise { + this.messageSubject.next(message); + this.modalVisibleSubject.next(true); + return new Promise(resolve => { + this.resolvePromise = resolve; + }); + } + + accept(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(true); + } + + reject(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(false); + } +} diff --git a/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.html b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.html new file mode 100644 index 0000000..79e5968 --- /dev/null +++ b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.html @@ -0,0 +1,24 @@ +
+ +
+
+ +
+ + + +
+
+
+
diff --git a/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.scss b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.scss new file mode 100644 index 0000000..e9cdf4c --- /dev/null +++ b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.scss @@ -0,0 +1,41 @@ +.container { + display: flex; + flex-wrap: wrap; + gap: 8px; + width: 100%; +} + +.item { + // max-width: 200px; + max-height: 150px; + // background-color: blanchedalmond; +} + +.animation.cdk-drag:not(.cdk-drag-dragging) { + transition: transform 200ms cubic-bezier(0, 0, 0.2, 1); +} + +.large { + width: 150px; +} +// -------------------- +.image-box { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} +.grid-item { + display: flex; + justify-content: center; + align-items: center; + aspect-ratio: 16 / 9; + background-color: #f0f0f0; + border-radius: 8px; + transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12); +} + +.grid-item:active { + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} diff --git a/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.ts b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.ts new file mode 100644 index 0000000..4eae827 --- /dev/null +++ b/bizmatch/src/app/components/drag-drop-mixed/drag-drop-mixed.component.ts @@ -0,0 +1,476 @@ +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; + }; +} diff --git a/bizmatch/src/app/components/message/message.component.ts b/bizmatch/src/app/components/message/message.component.ts new file mode 100644 index 0000000..13285a1 --- /dev/null +++ b/bizmatch/src/app/components/message/message.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +import { AsyncPipe, NgIf } from '@angular/common'; +import { MessageService } from './message.service'; + +@Component({ + selector: 'app-message', + standalone: true, + imports: [AsyncPipe, NgIf], + template: ` + + `, +}) +export class MessageComponent { + constructor(public messageService: MessageService) {} +} diff --git a/bizmatch/src/app/components/message/message.service.ts b/bizmatch/src/app/components/message/message.service.ts new file mode 100644 index 0000000..ee57e11 --- /dev/null +++ b/bizmatch/src/app/components/message/message.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class MessageService { + private modalVisibleSubject = new BehaviorSubject(false); + private messageSubject = new BehaviorSubject(''); + private resolvePromise!: (value: boolean) => void; + + modalVisible$: Observable = this.modalVisibleSubject.asObservable(); + message$: Observable = this.messageSubject.asObservable(); + + showMessage(message: string): Promise { + this.messageSubject.next(message); + this.modalVisibleSubject.next(true); + return new Promise(resolve => { + this.resolvePromise = resolve; + }); + } + + accept(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(true); + } + + reject(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(false); + } +} diff --git a/bizmatch/src/app/directives/mixed-cdk-drag-drop.directive.ts b/bizmatch/src/app/directives/mixed-cdk-drag-drop.directive.ts new file mode 100644 index 0000000..47f48c0 --- /dev/null +++ b/bizmatch/src/app/directives/mixed-cdk-drag-drop.directive.ts @@ -0,0 +1,197 @@ +import { CdkDrag, CdkDragEnter, CdkDragSortEvent, CdkDropList, CdkDropListGroup, DropListOrientation, moveItemInArray } from '@angular/cdk/drag-drop'; +import { AfterViewInit, Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, Self, SimpleChanges, SkipSelf } from '@angular/core'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +export const autoScrollStep = 6; +@Directive({ + selector: '[cdkDropListGroup][mixedCdkDragDrop]', // eslint-disable-line +}) +export class MixedCdkDragDropDirective implements OnChanges, AfterViewInit, OnDestroy { + /** @param {EventEmitter} dropped: emit previousIndex and currentIndex when dropList dropped. Valid when itemList is not being provided. **/ + @Output() readonly dropped = new EventEmitter<{ previousIndex: number; currentIndex: number }>(); + + @Input() itemList: T[] | undefined; + @Input() orientation: DropListOrientation = 'horizontal'; + @Input() containerSelector = ''; + + private readonly _resizeDragItem = new Set(); + + private targetIndex = -1; + private sourceIndex = -1; + private source: CdkDropList | undefined; + private observer: ResizeObserver | undefined; + private currentContentRect: DOMRectReadOnly | undefined; + private animationFrame: number | undefined; + + constructor(public element: ElementRef, @Self() private cdkDropListGroup: CdkDropListGroup) { + this.observer = new ResizeObserver((entries: Array) => { + this.animationFrame = window.requestAnimationFrame(() => { + if (entries.length) { + const element = this.containerSelector ? entries[0] : entries.find((e: ResizeObserverEntry) => e.target === this.element.nativeElement); + if (element) { + this.currentContentRect = element.contentRect; + for (let item of this._resizeDragItem) { + item.onSizeChangeEmit(element.contentRect); + } + } + } + }); + }); + } + + ngAfterViewInit() { + this.observeAll(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['orientation']) { + this.cdkDropListGroup._items.forEach((i: CdkDropList) => { + i.orientation = this.orientation; + i.element.nativeElement.style.flexDirection = this.orientation === 'horizontal' ? 'row' : 'column'; + }); + } + if (changes['containerSelector']) { + this.observer?.disconnect(); + this.observeAll(); + } + } + + addResizeDragItem(item: MixedCdkDragSizeHelperDirective) { + this._resizeDragItem.add(item); + if (this.currentContentRect) { + item.onSizeChangeEmit(this.currentContentRect); + } + } + + deleteResizeDragItem(item: MixedCdkDragSizeHelperDirective) { + this._resizeDragItem.delete(item); + } + + onDropListDropped() { + if (this.sourceIndex < 0 || this.targetIndex < 0) { + return; + } + // if sourceIndex is before targetIndex then the real target should minus one, to remove the source placeholder which being counted. + const target = this.targetIndex + (this.sourceIndex < this.targetIndex ? -1 : 0); + if (this.sourceIndex !== this.targetIndex && target >= 0) { + if (this.itemList) { + moveItemInArray(this.itemList, this.sourceIndex, target); + } else { + this.dropped.emit({ + previousIndex: this.sourceIndex, + currentIndex: target, + }); + } + this.sourceIndex = -1; + this.targetIndex = -1; + } + // reset + this.source = undefined; + } + + onDropListEntered({ item, container, currentIndex }: CdkDragEnter | CdkDragSortEvent) { + // dropList which the cdkDrag currently entered. + const dropElement = container.element.nativeElement; + // get all the dropList nodes in dropListGroup + const dropListNodes = Array.from(dropElement.parentElement?.children ?? []); + // dropList which the cdkDrag originally belonged. + const sourceElement = item.dropContainer.element.nativeElement; + + // might enter multiple dropList after drag start, should only keep the index from the first time + if (!this.source || this.sourceIndex === -1) { + this.sourceIndex = dropListNodes.indexOf(sourceElement); + this.source = item.dropContainer; + } + // target index should consider the currentIndex, which indicate drop before/after dropIndex (index of dropList which currently entered). + this.targetIndex = dropListNodes.indexOf(dropElement) + currentIndex; + } + + private observeAll() { + if (this.containerSelector) { + const el = document.querySelector(this.containerSelector); + if (el) { + this.observer?.observe(el); + } + } else { + this.observer?.observe(this.element.nativeElement); + } + } + + ngOnDestroy() { + this.observer?.disconnect(); + this.observer = undefined; + this.currentContentRect = undefined; + this._resizeDragItem.clear(); + if (this.animationFrame) { + window.cancelAnimationFrame(this.animationFrame); + } + } +} + +@Directive({ + selector: '[cdkDropList][mixedCdkDropList]', // eslint-disable-line +}) +export class MixedCdkDropListDirective implements OnInit, OnDestroy { + private lifecycleEmitter = new Subject(); + + constructor(@Self() private cdkDropList: CdkDropList, @SkipSelf() private mixedDragDrop: MixedCdkDragDropDirective) {} + + ngOnInit() { + this.cdkDropList.autoScrollStep = autoScrollStep; + this.cdkDropList.orientation = this.mixedDragDrop.orientation; + this.cdkDropList.element.nativeElement.style.flexDirection = this.mixedDragDrop.orientation === 'horizontal' ? 'row' : 'column'; + this.cdkDropList.element.nativeElement.style.display = 'flex'; + this.cdkDropList.element.nativeElement.style.flexWrap = 'nowrap'; + this.cdkDropList.element.nativeElement.style.width = 'fit-content'; + this.cdkDropList.element.nativeElement.style.height = 'fit-content'; + this.cdkDropList.sorted.pipe(takeUntil(this.lifecycleEmitter)).subscribe(event => this.mixedDragDrop.onDropListEntered(event)); + this.cdkDropList.entered.pipe(takeUntil(this.lifecycleEmitter)).subscribe(event => this.mixedDragDrop.onDropListEntered(event)); + this.cdkDropList.dropped.pipe(takeUntil(this.lifecycleEmitter)).subscribe(() => this.mixedDragDrop.onDropListDropped()); + } + + ngOnDestroy() { + this.lifecycleEmitter.next(); + this.lifecycleEmitter.unsubscribe(); + } +} + +@Directive({ + selector: '[cdkDrag][mixedCdkDragSizeHelper]', // eslint-disable-line +}) +export class MixedCdkDragSizeHelperDirective implements AfterViewInit, OnDestroy { + @Output() contentBoxSize = new EventEmitter<{ + drag: CdkDrag; + containerSize: DOMRectReadOnly; + }>(); + + constructor(@Self() private cdkDrag: CdkDrag, @SkipSelf() private mixedContainer: MixedCdkDragDropDirective) {} + + ngAfterViewInit() { + this.mixedContainer.addResizeDragItem(this); + } + + ngOnDestroy() { + this.mixedContainer.deleteResizeDragItem(this); + } + + onSizeChangeEmit(rect: DOMRectReadOnly) { + this.contentBoxSize?.emit({ drag: this.cdkDrag, containerSize: rect }); + } + + /** @param {drag: CdkDrag, containerSize: DOMRectReadOnly} event: contentSize observer event. + * @param {number} percentWidth: set width to the percentage based on the dropListGroup Container width, valid from 0 to 100. + * @param {number} percentHeight: set width to the percentage based on the dropListGroup Container width, valid from 0 to 100. **/ + static defaultEmitter(event: { drag: CdkDrag; containerSize: DOMRectReadOnly }, percentWidth: number, percentHeight: number) { + if (percentWidth) { + event.drag.element.nativeElement.style.width = `${(percentWidth * event.containerSize.width) / 100}px`; + } else { + event.drag.element.nativeElement.style.width = ''; + } + if (percentHeight) { + event.drag.element.nativeElement.style.height = `${(percentHeight * event.containerSize.height) / 100}px`; + } else { + event.drag.element.nativeElement.style.height = ''; + } + } +} diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html index 4b06490..c019fd4 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html @@ -105,7 +105,7 @@
- @if (listing && listing.imageOrder.length > 0) { + @if (listing && listing.imageOrder && listing.imageOrder.length > 0) {