Newer
Older
import {
AfterViewInit,
Component, ElementRef, Input, OnDestroy, OnInit, Pipe, PipeTransform, ViewChild
} from '@angular/core';
import { DropListElement } from 'common/models/elements/input-elements/drop-list';
import { DragNDropValueObject } from 'common/models/elements/element';
import { FormElementComponent } from '../../directives/form-element-component.directive';
selector: 'aspect-drop-list',
<div class="list" [id]="elementModel.id"
[fxLayout]="elementModel.orientation | droplistLayout"
[fxLayoutAlign]="elementModel.orientation | droplistLayoutAlign"
[ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical',
'horizontal-orientation' : elementModel.orientation === 'horizontal',
'clozeContext': clozeContext}"
[style.min-height.px]="elementModel.position?.useMinHeight ? elementModel.height : undefined"
[style.color]="elementModel.styling.fontColor"
[style.font-family]="elementModel.styling.font"
[style.font-size.px]="elementModel.styling.fontSize"
[style.font-weight]="elementModel.styling.bold ? 'bold' : ''"
[style.font-style]="elementModel.styling.italic ? 'italic' : ''"
[style.text-decoration]="elementModel.styling.underline ? 'underline' : ''"
[style.backgroundColor]="elementModel.styling.backgroundColor"
[class.errors]="elementFormControl.errors && elementFormControl.touched"
[style.outline-color]="elementModel.highlightReceivingDropListColor"
[class.highlight-valid-drop]="highlightValidDrop"
[class.highlight-as-receiver]="highlightAsReceiver"
tabindex="0"
(focusout)="elementFormControl.markAsTouched()"
(drop)="drop($event)" (dragenter)="dragEnterList($event)" (dragleave)="dragLeaveList($event)"
(dragover)="$event.preventDefault()">
<ng-container *ngFor="let dropListValueElement of viewModel let index = index;">
<div *ngIf="!dropListValueElement.imgSrc"
class="list-item"
draggable="true"
(dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)"
(dragenter)="dragEnterItem($event)"
[class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index"
[class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index"
[style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''"
[style.background-color]="elementModel.styling.itemBackgroundColor">
<span>{{dropListValueElement.text}}</span>
</div>
<img *ngIf="dropListValueElement.imgSrc"
class="list-item"
[src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder"
[id]="dropListValueElement.id"
draggable="true"
(dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)"
(dragenter)="dragEnterItem($event)"
[class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index"
[class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index"
[style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''">
</ng-container>
</div>
<mat-error *ngIf="elementFormControl.errors && elementFormControl.touched"
class="error-message">
{{elementFormControl.errors | errorTransform: elementModel}}
</mat-error>
'.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); padding: 3px;}',
':not(.clozeContext) .list-item {border-radius: 5px; padding: 10px;}',
'img.list-item {align-self: start;}',
'.vertical-orientation .list-item:not(:last-child) {margin-bottom: 5px;}',
'.horizontal-orientation .list-item:not(:last-child) {margin-right: 5px;}',
'.errors {border: 2px solid #f44336;}',
'.error-message {font-size: 75%; margin-top: 10px; margin-left: 3px;}',
'.error-message {position: absolute; bottom: 3px; pointer-events: none;}',
'.list-item {cursor: grab;}',
'.list-item:active {cursor: grabbing}',
'.show-as-placeholder {opacity: 0.5 !important; pointer-events: none;}',
'.highlight-valid-drop {background-color: lightblue !important;}',
'.highlight-as-receiver {border: 2px solid;}',
export class DropListComponent extends FormElementComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() elementModel!: DropListElement;
@Input() clozeContext: boolean = false;
@ViewChild('placeholder') placeholder!: ElementRef<HTMLElement>;
static dragAndDropComponents: { [id: string]: DropListComponent } = {};
viewModel: DragNDropValueObject[] = [];
placeHolderIndex?: number;
highlightAsReceiver = false;
dragging = false;
showAsPlaceholder = false;
hidePlaceholder = false;
highlightValidDrop = false;
static draggedElement?: DragNDropValueObject;
static sourceList?: DropListComponent;
ngOnInit() {
super.ngOnInit();
this.viewModel = [...this.elementFormControl.value];
ngAfterViewInit() {
DropListComponent.dragAndDropComponents[this.elementModel.id] = this;
dragStart(dragEvent: DragEvent,
dropListValueElement: DragNDropValueObject,
sourceListIndex: number) {
if (dragEvent.dataTransfer) {
dragEvent.dataTransfer.effectAllowed = 'copyMove';
dragEvent.dataTransfer.setDragImage(
this.createDragImage(dragEvent.target as HTMLElement, dropListValueElement.id), 0, 0);
// Sadly timeout is necessary for Chrome, which does not allow DOM manipulation on dragstart
setTimeout(() => {
DropListComponent.draggedElement = dropListValueElement;
DropListComponent.sourceList = this;
this.placeHolderIndex = sourceListIndex;
if (this.elementModel.isSortList) {
this.showAsPlaceholder = true;
} else {
this.hidePlaceholder = true;
this.highlightValidDrop = true;
}
Object.entries(DropListComponent.dragAndDropComponents)
.forEach(([, value]) => {
value.dragging = true;
});
if (this.elementModel.highlightReceivingDropList) {
this.highlightAsReceiver = true;
this.elementModel.connectedTo.forEach(connectedDropListID => {
DropListComponent.dragAndDropComponents[connectedDropListID].highlightAsReceiver = true;
});
}
});
createDragImage(baseElement: HTMLElement, baseID: string): HTMLElement {
const dragImage: HTMLElement = baseElement.cloneNode(true) as HTMLElement;
dragImage.id = `${baseID}-dragimage`;
dragImage.style.display = 'block';
dragImage.style.width = `${(baseElement as HTMLElement).offsetWidth}px`;
dragImage.style.fontSize = `${this.elementModel.styling.fontSize}px`;
dragImage.style.borderRadius = '5px';
dragImage.style.padding = '10px';
document.body.appendChild(dragImage);
return dragImage;
}
dragEnterItem(event: DragEvent) {
event.preventDefault();
if (this.elementModel.isSortList && DropListComponent.sourceList === this) {
const sourceIndex: number = this.placeHolderIndex as number;
const targetIndex: number = Array.from((event.target as any).parentNode.children).indexOf(event.target);
const removedElement = this.viewModel.splice(sourceIndex, 1)[0];
this.viewModel.splice(targetIndex, 0, removedElement);
this.placeHolderIndex = targetIndex;
}
}
dragEnterList(event: DragEvent) {
event.preventDefault();
if (!this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) return;
if (!this.elementModel.isSortList) {
this.highlightValidDrop = true;
} else if (DropListComponent.sourceList !== this) {
this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject);
const sourceList = DropListComponent.sourceList as DropListComponent;
sourceList.viewModel.splice(sourceList.placeHolderIndex as number, 1);
sourceList.elementFormControl.setValue(sourceList.viewModel);
sourceList.placeHolderIndex = undefined;
DropListComponent.sourceList = this;
this.placeHolderIndex = this.viewModel.length > 0 ? this.viewModel.length - 1 : 0;
}
}
dragLeaveList(event: DragEvent) {
event.preventDefault();
this.highlightValidDrop = false;
}
drop(event: DragEvent) {
event.preventDefault();
// SortList viewModel already gets manipulated while dragging. Just set the value.
if (DropListComponent.sourceList === this && this.elementModel.isSortList) {
this.elementFormControl.setValue(this.viewModel);
} else if (this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) {
const valueIDs = this.elementFormControl.value.map((valueValue: DragNDropValueObject) => valueValue.id);
if (!valueIDs.includes(DropListComponent.draggedElement?.id)) {
this.addDraggedElementToList();
if (!DropListComponent.sourceList?.elementModel.copyOnDrop) {
DropListComponent.removeElementFromSourceList();
} else if (this.elementModel.deleteDroppedItemWithSameID) {
DropListComponent.removeElementFromSourceList();
// else {
// console.log('Not an allowed target list');
// }
addDraggedElementToList(): void {
this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject);
this.elementFormControl.setValue(this.viewModel);
}
static removeElementFromSourceList(): void {
DropListComponent.sourceList?.viewModel.splice(DropListComponent.sourceList.placeHolderIndex as number, 1);
DropListComponent.sourceList?.elementFormControl.setValue(DropListComponent.sourceList.viewModel);
}
isDropAllowed(connectedDropLists: string[]): boolean {
return (DropListComponent.sourceList === this) ||
((connectedDropLists as string[]).includes(this.elementModel.id) &&
!(this.elementModel.onlyOneItem && this.elementModel.value.length > 0));
// TODO presentValueIDs?
}
dragEnd(event?: DragEvent) {
event?.preventDefault();
Object.entries(DropListComponent.dragAndDropComponents)
.forEach(([, value]) => {
value.highlightAsReceiver = false;
value.dragging = false;
value.highlightValidDrop = false;
});
if (DropListComponent.sourceList) DropListComponent.sourceList.placeHolderIndex = undefined;
this.placeHolderIndex = undefined;
document.getElementById(`${DropListComponent.draggedElement?.id}-dragimage`)?.remove();
ngOnDestroy(): void {
delete DropListComponent.dragAndDropComponents[this.elementModel.id];
@Pipe({
name: 'droplistLayout'
})
export class DropListLayoutPipe implements PipeTransform {
transform(orientation: string): string {
switch (orientation) {
case 'horizontal':
return 'row';
case 'vertical':
return 'column';
case 'flex':
return 'row wrap';
default:
throw Error(`droplist orientation invalid: ${orientation}`);
@Pipe({
name: 'droplistLayoutAlign'
})
export class DropListLayoutAlignPipe implements PipeTransform {
transform(orientation: string): string {
switch (orientation) {
case 'horizontal':
return 'start start';
case 'vertical':
return 'start stretch';
case 'flex':
return 'space-around center';
default:
throw Error(`droplist orientation invalid: ${orientation}`);
}
}