editCommercialProps, confirmationService, MessageService, Drag & Drop
This commit is contained in:
parent
e0dbebb61c
commit
7f67b81242
|
|
@ -137,7 +137,7 @@ function getFilenames(id: string): string[] {
|
||||||
let filePath = `./pictures_base/property/${id}`;
|
let filePath = `./pictures_base/property/${id}`;
|
||||||
return readdirSync(filePath);
|
return readdirSync(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getRandomDateWithinLastYear(): Date {
|
function getRandomDateWithinLastYear(): Date {
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,9 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@ng-select/ng-select": "^13.4.1",
|
"@ng-select/ng-select": "^13.4.1",
|
||||||
|
"@types/cropperjs": "^1.3.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"angular-cropperjs": "^14.0.1",
|
|
||||||
"angular-mixed-cdk-drag-drop": "^2.2.3",
|
|
||||||
"browser-bunyan": "^1.8.0",
|
"browser-bunyan": "^1.8.0",
|
||||||
"cropperjs": "^1.6.2",
|
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"flowbite": "^2.4.1",
|
"flowbite": "^2.4.1",
|
||||||
|
|
@ -43,6 +41,7 @@
|
||||||
"keycloak-js": "^25.0.1",
|
"keycloak-js": "^25.0.1",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"ngx-currency": "^18.0.0",
|
"ngx-currency": "^18.0.0",
|
||||||
|
"ngx-image-cropper": "^8.0.0",
|
||||||
"ngx-quill": "^26.0.5",
|
"ngx-quill": "^26.0.5",
|
||||||
"on-change": "^5.0.1",
|
"on-change": "^5.0.1",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
|
|
|
||||||
|
|
@ -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: `
|
||||||
|
<div *ngIf="confirmationService.modalVisible$ | async" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||||
|
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||||
|
<button
|
||||||
|
(click)="confirmationService.reject()"
|
||||||
|
type="button"
|
||||||
|
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Close modal</span>
|
||||||
|
</button>
|
||||||
|
<div class="p-4 md:p-5 text-center">
|
||||||
|
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmationService.message$ | async }}</h3>
|
||||||
|
<button
|
||||||
|
(click)="confirmationService.accept()"
|
||||||
|
type="button"
|
||||||
|
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2"
|
||||||
|
>
|
||||||
|
Yes, I'm sure
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="confirmationService.reject()"
|
||||||
|
type="button"
|
||||||
|
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
No, cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ConfirmationComponent {
|
||||||
|
constructor(public confirmationService: ConfirmationService) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ConfirmationService {
|
||||||
|
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
private messageSubject = new BehaviorSubject<string>('');
|
||||||
|
private resolvePromise!: (value: boolean) => void;
|
||||||
|
|
||||||
|
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
||||||
|
message$: Observable<string> = this.messageSubject.asObservable();
|
||||||
|
|
||||||
|
showConfirmation(message: string): Promise<boolean> {
|
||||||
|
this.messageSubject.next(message);
|
||||||
|
this.modalVisibleSubject.next(true);
|
||||||
|
return new Promise<boolean>(resolve => {
|
||||||
|
this.resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
accept(): void {
|
||||||
|
this.modalVisibleSubject.next(false);
|
||||||
|
this.resolvePromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(): void {
|
||||||
|
this.modalVisibleSubject.next(false);
|
||||||
|
this.resolvePromise(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<div #_container class="container">
|
||||||
|
<!-- <div
|
||||||
|
*ngFor="let item of items"
|
||||||
|
cdkDrag
|
||||||
|
(cdkDragEnded)="dragEnded($event)"
|
||||||
|
(cdkDragStarted)="dragStarted()"
|
||||||
|
(cdkDragMoved)="dragMoved($event)"
|
||||||
|
class="item"
|
||||||
|
[class.animation]="isAnimationActive"
|
||||||
|
[class.large]="item === 3"
|
||||||
|
>
|
||||||
|
Drag Item {{ item }}
|
||||||
|
</div> -->
|
||||||
|
<div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item">
|
||||||
|
<div class="image-box hover:cursor-pointer">
|
||||||
|
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||||
|
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" (click)="imageToDelete.emit(item)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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<HTMLDivElement>;
|
||||||
|
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
|
||||||
|
|
||||||
|
listing = input<CommercialPropertyListing>();
|
||||||
|
imageOrderChanged = output<string[]>();
|
||||||
|
imageToDelete = output<string>();
|
||||||
|
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<DragRef>[] = [];
|
||||||
|
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<DragRef>[], 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<DragRef> = {
|
||||||
|
...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<DragRef> = {
|
||||||
|
...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<DragRef>[], 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<DragRef>[], 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<DragRef> = {
|
||||||
|
...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<DragRef>[], 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<DragRef>[], 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<T> {
|
||||||
|
drag: T;
|
||||||
|
clientRect: DOMRect;
|
||||||
|
transform: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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: `
|
||||||
|
<div id="toast-success" class="flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
||||||
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Check icon</span>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3 text-sm font-normal">Item moved successfully.</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
data-dismiss-target="#toast-success"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class MessageComponent {
|
||||||
|
constructor(public messageService: MessageService) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class MessageService {
|
||||||
|
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
private messageSubject = new BehaviorSubject<string>('');
|
||||||
|
private resolvePromise!: (value: boolean) => void;
|
||||||
|
|
||||||
|
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
||||||
|
message$: Observable<string> = this.messageSubject.asObservable();
|
||||||
|
|
||||||
|
showMessage(message: string): Promise<boolean> {
|
||||||
|
this.messageSubject.next(message);
|
||||||
|
this.modalVisibleSubject.next(true);
|
||||||
|
return new Promise<boolean>(resolve => {
|
||||||
|
this.resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
accept(): void {
|
||||||
|
this.modalVisibleSubject.next(false);
|
||||||
|
this.resolvePromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(): void {
|
||||||
|
this.modalVisibleSubject.next(false);
|
||||||
|
this.resolvePromise(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<T = any> 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<MixedCdkDragSizeHelperDirective>();
|
||||||
|
|
||||||
|
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<HTMLElement>, @Self() private cdkDropListGroup: CdkDropListGroup<any>) {
|
||||||
|
this.observer = new ResizeObserver((entries: Array<ResizeObserverEntry>) => {
|
||||||
|
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<void>();
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
<fa-icon [icon]="faTimes" size="2x"></fa-icon>
|
<fa-icon [icon]="faTimes" size="2x"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
@if (listing && listing.imageOrder.length > 0) {
|
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) {
|
||||||
<div id="gallery" class="relative w-full" data-carousel="slide">
|
<div id="gallery" class="relative w-full" data-carousel="slide">
|
||||||
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
|
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
|
||||||
@for (image of listing.imageOrder; track $index) {
|
@for (image of listing.imageOrder; track $index) {
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 mt-6 lg:mt-0">
|
<div class="w-full lg:w-1/2 mt-6 lg:mt-0">
|
||||||
<div class="hidden lg:block">
|
<div class="hidden lg:block">
|
||||||
@if (listing && listing.imageOrder.length > 0) {
|
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) {
|
||||||
<div id="gallery" class="relative w-full" data-carousel="slide">
|
<div id="gallery" class="relative w-full" data-carousel="slide">
|
||||||
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
|
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
|
||||||
@for (image of listing.imageOrder; track $index) {
|
@for (image of listing.imageOrder; track $index) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
|
|
@ -19,7 +18,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-account',
|
selector: 'app-account',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, AngularCropperjsModule],
|
imports: [SharedModule],
|
||||||
providers: [],
|
providers: [],
|
||||||
templateUrl: './account.component.html',
|
templateUrl: './account.component.html',
|
||||||
styleUrl: './account.component.scss',
|
styleUrl: './account.component.scss',
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import { createDefaultBusinessListing, map2User, routeListingWithState } from '.
|
||||||
|
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
|
||||||
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { QuillModule } from 'ngx-quill';
|
import { QuillModule } from 'ngx-quill';
|
||||||
|
|
||||||
|
|
@ -27,7 +25,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'business-listing',
|
selector: 'business-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, ArrayToStringPipe, AngularCropperjsModule, DragDropModule, MixedCdkDragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
|
imports: [SharedModule, ArrayToStringPipe, DragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
|
||||||
providers: [],
|
providers: [],
|
||||||
templateUrl: './edit-business-listing.component.html',
|
templateUrl: './edit-business-listing.component.html',
|
||||||
styleUrl: './edit-business-listing.component.scss',
|
styleUrl: './edit-business-listing.component.scss',
|
||||||
|
|
|
||||||
|
|
@ -146,10 +146,11 @@
|
||||||
<div class="container mx-auto p-4">
|
<div class="container mx-auto p-4">
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
|
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
|
||||||
|
@if (listing){
|
||||||
<form #listingForm="ngForm">
|
<form #listingForm="ngForm">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
|
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
|
||||||
<select id="listingsCategory" [(ngModel)]="listing.listingsCategory" name="listingsCategory" class="w-full p-2 border border-gray-300 rounded-md">
|
<select id="listingsCategory" [ngModel]="listingsCategory" name="listingsCategory" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
<option value="business">Business</option>
|
<option value="business">Business</option>
|
||||||
<option value="commercialProperty">Commercial Property</option>
|
<option value="commercialProperty">Commercial Property</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -220,11 +221,10 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container mx-auto p-4">
|
<!-- <div class="container mx-auto p-4">
|
||||||
<div cdkDropList class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" (cdkDropListDropped)="drop($event)">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
@for (image of listing.imageOrder; track listing.imageOrder) {
|
@for (image of listing.imageOrder; track listing.imageOrder) {
|
||||||
<div cdkDrag class="relative aspect-video cursor-move">
|
<div class="relative aspect-video cursor-move">
|
||||||
<!-- <img [src]="image" class="w-full h-full object-cover rounded-lg shadow-md" /> -->
|
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="w-full h-full object-cover rounded-lg shadow-md" />
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md">
|
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||||
|
|
@ -234,12 +234,56 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<!-- <div class="grid-container"> -->
|
||||||
|
<!-- @for (image of listing.imageOrder; track image) {
|
||||||
|
<div cdkDrag class="grid-item">
|
||||||
|
<div class="image-box">
|
||||||
|
<img [src]="getImageUrl(image)" [alt]="image" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||||
|
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} -->
|
||||||
|
<app-drag-drop-mixed [listing]="listing" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">Property Pictures</h2>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">(Pictures can be uploaded once the listing is posted initially)</p>
|
||||||
|
<button
|
||||||
|
(click)="openFileDialog()"
|
||||||
|
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<svg class="mr-2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||||
|
</svg>
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
<input type="file" #fileInput style="display: none" (change)="fileChangeEvent($event)" accept="image/*" />
|
||||||
</div>
|
</div>
|
||||||
@if (mode==='create'){
|
@if (mode==='create'){
|
||||||
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Post Listing</button>
|
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 mt-3 rounded-md hover:bg-blue-600">Post Listing</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Update Listing</button>
|
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 mt-3 rounded-md hover:bg-blue-600">Update Listing</button>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Modal -->
|
||||||
|
<div *ngIf="showModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||||
|
<div class="bg-white p-5 rounded-lg shadow-xl" style="width: 90%; max-width: 600px">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Crop Image</h3>
|
||||||
|
<image-cropper [imageChangedEvent]="imageChangedEvent" [maintainAspectRatio]="true" [aspectRatio]="16 / 9" format="png" (imageCropped)="imageCropped($event)"></image-cropper>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<button (click)="closeModal()" class="mr-2 px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Cancel</button>
|
||||||
|
<button (click)="uploadImage()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-confirmation></app-confirmation>
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,52 @@
|
||||||
cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
|
cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
|
||||||
border-radius: 8px; /* Optional: Abrunden der linken unteren Ecke für ästhetische Zwecke */
|
border-radius: 8px; /* Optional: Abrunden der linken unteren Ecke für ästhetische Zwecke */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-placeholder {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container.cdk-drop-list-dragging .grid-item:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
import { Component } from '@angular/core';
|
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
|
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
|
||||||
|
|
||||||
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
|
import { ViewportRuler } from '@angular/cdk/scrolling';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
|
||||||
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||||
|
import { ImageCroppedEvent, ImageCropperComponent } from 'ngx-image-cropper';
|
||||||
import { QuillModule } from 'ngx-quill';
|
import { QuillModule } from 'ngx-quill';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||||
|
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||||
|
import { DragDropMixedComponent } from '../../../components/drag-drop-mixed/drag-drop-mixed.component';
|
||||||
|
import { MessageService } from '../../../components/message/message.service';
|
||||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||||
import { GeoService } from '../../../services/geo.service';
|
import { GeoService } from '../../../services/geo.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
|
|
@ -26,13 +30,15 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'commercial-property-listing',
|
selector: 'commercial-property-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, ArrayToStringPipe, AngularCropperjsModule, DragDropModule, MixedCdkDragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
|
imports: [SharedModule, ArrayToStringPipe, DragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule, ImageCropperComponent, ConfirmationComponent, DragDropMixedComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
templateUrl: './edit-commercial-property-listing.component.html',
|
templateUrl: './edit-commercial-property-listing.component.html',
|
||||||
styleUrl: './edit-commercial-property-listing.component.scss',
|
styleUrl: './edit-commercial-property-listing.component.scss',
|
||||||
})
|
})
|
||||||
export class EditCommercialPropertyListingComponent {
|
export class EditCommercialPropertyListingComponent {
|
||||||
// @ViewChild(FileUpload) public fileUpload: FileUpload;
|
// @ViewChild(FileUpload) public fileUpload: FileUpload;
|
||||||
|
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
listingsCategory = 'commercialProperty';
|
listingsCategory = 'commercialProperty';
|
||||||
category: string;
|
category: string;
|
||||||
location: string;
|
location: string;
|
||||||
|
|
@ -74,6 +80,10 @@ export class EditCommercialPropertyListingComponent {
|
||||||
quillModules = {
|
quillModules = {
|
||||||
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
|
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
|
||||||
};
|
};
|
||||||
|
showModal = false;
|
||||||
|
imageChangedEvent: any = '';
|
||||||
|
croppedImage: Blob | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
|
@ -87,6 +97,10 @@ export class EditCommercialPropertyListingComponent {
|
||||||
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private keycloakService: KeycloakService,
|
private keycloakService: KeycloakService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private confirmationService: ConfirmationService,
|
||||||
|
private messageService: MessageService,
|
||||||
|
private viewportRuler: ViewportRuler,
|
||||||
) {
|
) {
|
||||||
// Abonniere Router-Events, um den aktiven Link zu ermitteln
|
// Abonniere Router-Events, um den aktiven Link zu ermitteln
|
||||||
this.router.events.subscribe(event => {
|
this.router.events.subscribe(event => {
|
||||||
|
|
@ -130,7 +144,43 @@ export class EditCommercialPropertyListingComponent {
|
||||||
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
|
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
|
||||||
this.suggestions = result.map(r => r.city).slice(0, 5);
|
this.suggestions = result.map(r => r.city).slice(0, 5);
|
||||||
}
|
}
|
||||||
|
openFileDialog() {
|
||||||
|
this.fileInput.nativeElement.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
fileChangeEvent(event: any): void {
|
||||||
|
this.imageChangedEvent = event;
|
||||||
|
this.showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCropped(event: ImageCroppedEvent) {
|
||||||
|
this.croppedImage = event.blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showModal = false;
|
||||||
|
this.imageChangedEvent = '';
|
||||||
|
this.croppedImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImage() {
|
||||||
|
if (this.croppedImage) {
|
||||||
|
// Convert base64 to blob
|
||||||
|
// Replace with your actual API endpoint
|
||||||
|
this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
|
||||||
|
async response => {
|
||||||
|
console.log('Upload successful', response);
|
||||||
|
setTimeout(async () => {
|
||||||
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
|
});
|
||||||
|
this.closeModal();
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('Upload failed', error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// select(event: any) {
|
// select(event: any) {
|
||||||
// const imageUrl = URL.createObjectURL(event.files[0]);
|
// const imageUrl = URL.createObjectURL(event.files[0]);
|
||||||
// getImageDimensions(imageUrl).then(dimensions => {
|
// getImageDimensions(imageUrl).then(dimensions => {
|
||||||
|
|
@ -174,36 +224,22 @@ export class EditCommercialPropertyListingComponent {
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// deleteConfirm(imageName: string) {
|
async deleteConfirm(imageName: string) {
|
||||||
// this.confirmationService.confirm({
|
const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?');
|
||||||
// target: event.target as EventTarget,
|
if (confirmed) {
|
||||||
// message: `Do you want to delete this image ${imageName}?`,
|
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
||||||
// header: 'Delete Confirmation',
|
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
||||||
// icon: 'pi pi-info-circle',
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
// acceptButtonStyleClass: 'p-button-danger p-button-text',
|
this.messageService.showMessage('Image deleted');
|
||||||
// rejectButtonStyleClass: 'p-button-text p-button-text',
|
} else {
|
||||||
// acceptIcon: 'none',
|
console.log('deny');
|
||||||
// rejectIcon: 'none',
|
|
||||||
|
|
||||||
// accept: async () => {
|
|
||||||
// this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
|
||||||
// await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
|
||||||
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
|
||||||
// },
|
|
||||||
// reject: () => {
|
|
||||||
// // this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
|
|
||||||
// console.log('deny');
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
onDrop(event: { previousIndex: number; currentIndex: number }) {
|
|
||||||
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeListingCategory(value: 'business' | 'commercialProperty') {
|
changeListingCategory(value: 'business' | 'commercialProperty') {
|
||||||
routeListingWithState(this.router, value, this.listing);
|
routeListingWithState(this.router, value, this.listing);
|
||||||
}
|
}
|
||||||
drop(event: CdkDragDrop<string[]>) {
|
imageOrderChanged(imageOrder: string[]) {
|
||||||
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
|
this.listing.imageOrder = imageOrder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue