Free Drag with cdkDropList - javascript

I'm working on a project where I need to implement some sort of drop zone where you can drag element from a list, then drop them in a zone where they can be dragged freely. I also would like to use a cdkDropList for the zone, because it provides all the tools for connecting lists.
I used this example as a reference for my implementation, but I am not able to make it work right.
When I drop an item in the zone, it does not drop where the cursor was, it just goes to the top left of my zone, like it was in a list.
When I drag an item in the zone, it either drags correctly to where I want it to be dropped, gets dropped near the drop point, or just goes back to the top left.
Here is my cdkDrag element, it differs from the example linked above because I absolutely need it to be in it's own component (I would like to apply some logic to it in the future), but it is essentially the same concept (cdkDrag div in cdkDropList div). I managed to route all the needed events to the parent element (the zone) using Outputs.
<div class="element-box"
cdkDrag
cdkDragBoundary={{boundary_name}}
(cdkDragDropped)="dragDroppedEventToParent($event)"
(cdkDragStarted)="dragStartedEventToParent($event)"
(cdkDragMoved)="dragMovedEventToParent($event)">
{{object_name}}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
Here is the logic for the drag element:
export class ElementBoxComponent implements OnInit {
#Input () object_name: string;
#Input () boundary_name: string;
#Input () itemSelf: any;
#Output () dragMovedEvent = new EventEmitter<CdkDragMove>();
#Output () dragStartedEvent = new EventEmitter<CdkDragStart>();
#Output () dragDroppedEvent = new EventEmitter<any>();
constructor() {}
ngOnInit(): void {
}
dragMovedEventToParent(event: CdkDragMove) {
this.dragMovedEvent.emit(event);
}
dragStartedEventToParent(event: CdkDragStart){
this.dragStartedEvent.emit(event);
}
dragDroppedEventToParent(event: CdkDragEnd){
this.dragDroppedEvent.emit({event, "self": this.itemSelf});
}
}
Here is my drop zone element, where I render the drag elements (you can see that I routed the Outputs to methods in my logic for the zone):
<div class="drop-container"
#cdkBoard
cdkDropList
[id]="'cdkBoard'"
[cdkDropListData]="itemsInBoard"
[cdkDropListConnectedTo]="connectedTo"
cdkDropListSortingDisabled="true"
(cdkDropListDropped)="itemDropped($event)">
<app-element-box *ngFor="let item of itemsInBoard; let i=index"
object_name="{{item.name}}"
boundary_name=".drop-container"
itemSelf="{{item}}"
style="position:absolute; z-index:i"
[style.top]="item.top"
[style.left]="item.left"
(dragMovedEvent)="elementIsMoving($event)"
(dragStartedEvent)="startedDragging($event)"
(dragDroppedEvent)="stoppedDragging($event)"></app-element-box>
<svg-container class="bin-container" containerId="bin-image-container" *ngIf="_binVisible" height=40>
<svg-image class="bin-icon" [imageUrl]="BIN_ICON_URL" height=40 width=40></svg-image>
</svg-container>
</div>
And here are the relevant methods in the TS file for my drop zone:
itemDropped(event: CdkDragDrop<any[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(this.itemsInBoard, event.previousIndex, event.currentIndex);
} else {
copyArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
)
}
}
changePosition(event: CdkDragDrop<any>, field) {
const rectZone = this.dropZone.nativeElement.getBoundingClientRect();
const rectElement = event.item.element.nativeElement.getBoundingClientRect();
const top = rectElement.top + event.distance.y - rectZone.top;
const left = rectElement.left + event.distance.x - rectZone.left;
const out = (top < 0) || (left < 0) || (top > (rectZone.height - rectElement.height)) || (left > (rectZone.width - rectElement.width));
if (!out) {
event.item.element.nativeElement.style.top = top + 'px';
event.item.element.nativeElement.style.left = left + 'px';
} else {
this.itemsInBoard = this.itemsInBoard.filter((x) => x != event.item);
}
}
Again, the only differences are that my elements are encapsulated in their own components, and that the way I access the top and left style elements of the components are different (the code in the example did not work).
I know that the problem is the way I calculate the top and left variable, but I've been stuck on this for a week and cannot seem to find out what's wrong with it.
Here is a short demonstrative video if you want to better visualize what I am talking about.
Does anyone know what could be wrong with this ? I am open to any suggestions, thank you :)

It's difficut without a stackblitz know what is wrong
In this stackblitz I made two ckd-list
One cdkDropList (todoList) it's a typical "list", the other one is a dropZone (doneList). My elements are in the way
{label:string,x:number,y:number,'z-index':number}
The cdkDrag in the dropZone is in the way
<div cdkDrag class="item-box"
[style.top.px]="item.y"
[style.left.px]="item.x"
[style.z-index]="item['z-index']"
>
I choose that the todoList is connected to the dropZone, but the dropZone is not connected to anything. When I mover the elements of the dropZone if it's move away this one, simply add to the list
We need get the doneList as ElementRef
#ViewChild('doneList',{read:ElementRef,static:true}) dropZone:ElementRef;
And the neccesary functions
drop(event: CdkDragDrop<any[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex,
);
event.item.data.y=(this._pointerPosition.y-this.dropZone.nativeElement.getBoundingClientRect().top)
event.item.data.x=(this._pointerPosition.x-this.dropZone.nativeElement.getBoundingClientRect().left)
this.changeZIndex(event.item.data)
}
this.posInside={source:null,x:0,y:0}
}
moved(event: CdkDragMove) {
this._pointerPosition=event.pointerPosition;
}
changeZIndex(item:any)
{
this.done.forEach(x=>x['z-index']=(x==item?1:0))
}
changePosition(event:CdkDragDrop<any>,field)
{
const rectZone=this.dropZone.nativeElement.getBoundingClientRect()
const rectElement=event.item.element.nativeElement.getBoundingClientRect()
let y=+field.y+event.distance.y
let x=+field.x+event.distance.x
const out=y<0 || x<0 || (y>(rectZone.height-rectElement.height)) || (x>(rectZone.width-rectElement.width))
if (!out)
{
field.y=y
field.x=x
}
else{
this.todo.push(field)
this.done=this.done.filter(x=>x!=field)
}
}
The .html like
<div class="wrapper">
<div
cdkDropList
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList]"
class="example-list"
(cdkDropListDropped)="drop($event)"
>
<div
class="example-box"
*ngFor="let item of todo"
cdkDrag
[cdkDragData]="item"
(cdkDragMoved)="moved($event)"
>
{{ item.label }}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
</div>
<div
cdkDropList
#doneList="cdkDropList"
[cdkDropListData]="done"
class="drag-zone"
cdkDropListSortingDisabled="true"
(cdkDropListDropped)="drop($event)"
>
<ng-container *ngFor="let item of done">
<div
cdkDrag
class="item-box"
[style.top.px]="item.y"
[style.left.px]="item.x"
[style.z-index]="item['z-index']"
(cdkDragStarted)="changeZIndex(item)"
(cdkDragDropped)="changePosition($event, item)"
>
{{ item.label }}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
</ng-container>
</div>
</div>

Related

Drag and drop list with hierarchy/nested behavior

I only want to do this with vanilla javascript. But if there is some lightweight library I might reconsider. Anyway, I basically want to emulate a file system with a list of elements.
<div class="container" >
<div class="item draggable" draggable> First </div>
<div class="children-nodes">
<div class="container" >
<div class="item draggable" draggable> First </div>
<div class="children-nodes">
</div>
</div>
</div>
</div>
As you can see in my HTML structure. Each container can be dragged and placed inside another .children-nodes, however the items themselves can also be dragged and moved around in the hierarchy. There are multiple of these containers with items nested inside eachother. I have written several algorithms to try to make this work I just can't seem to make it happen. I get errors like "new child contains parent" and etc.
Here's the latest logic i tried:
const draggables = document.querySelectorAll('.draggable');
draggables.forEach(draggable => {
draggable.addEventListener('dragstart', () => {
draggable.classList.add('is-dragging');
})
draggable.addEventListener('dragend', () => {
draggable.classList.remove('is-dragging');
})
draggable.addEventListener('dragover', (event) => {
event.stopPropagation()
event.preventDefault();
if(!draggable.classList.contains('.is-dragging')){
const item = document.querySelector('.is-dragging');
const containerOfItem = document.querySelector('.container:has(.is-dragging)');
const childrenContainer = containerOfItem.querySelector('.children-nodes');
const children = childrenContainer.querySelectorAll('div');
if(children.length == 0) {
const childrenDropContainer = draggable.parentNode.querySelector('.children-nodes');
childrenDropContainer.appendChild(containerOfItem);
} else {
}
}
})
})
And like I said. I've written it multiple different ways. I changed the markup too and searched around for some information. The kind of help that I would want would be what to consider, in which direction should I move, how do I solve this problem. Any help is gladly received.

*ngIf not detecting changes on variable change on swiper.js (slideChange) event but detects on swiperRef.slideTo(index ) call

iam passing an array of object containing photo url to swiper container,
for unverified photo iam showing a a text,
on each slide change i have to check photos verification status
the variable is changing to true or false but dom is not updating even when i inspect it shows
ng-reflect-ng-if :'true;
but i have to scroll to a specific slide on user clicking on specific thumbnail
for that iam using
this.photoPopupSlider.swiperRef.slideTo(RowIndex, sliderSpeed);
at this time onSlidChange event triggers updates the dom correctly
my component.html 👇
<div style="padding: 0">
<swiper
[config]="sliderConfig"
class="sliderBox"
[pagination]="false"
(slideChange)="onSlideChange($event)"
#photoPopupSlider
>
<ng-template swiperSlide *ngFor="let pic of myPhotos">
<div
class="slide_wrap"
style="height: 68vh; background: #f3f4f9"
>
<img
[src]="pic.image"
class="slide_img"
style="height: auto; width: auto; max-width: 100%"
/>
</div>
</ng-template>
</swiper>
<span
id="photo_under_verification"
*ngIf="underVerification"
class="ph_uv"
>Photo under verification</span
>
</div>
my component.ts file 👇
underVerification = false;
onSlideChange([swiper]: any) {
const index = swiper.activeIndex;
this._ps.sliderIndex = index;
this.sliderIndex = index;
console.log("sliderChanged", this.sliderIndex);
const photoShowingInSlider = this.myPhotos[index];
const isPhotoUnderVerification =
photoShowingInSlider?.photostatus == "0" ? true : false;
this.underVerification = isPhotoUnderVerification;
console.log("under", isPhotoUnderVerification);
// const photUnderVerifyMsg = document.getElementById(
// "photo_under_verification"
// ).style;
// if (isPhotoUnderVerification) photUnderVerifyMsg.display = "inline";
// else photUnderVerifyMsg.display = "none";
}
actually i was able to implement the logic by using document.getElemntById
those commented lines of code ( i thing it is a bad way in angular to access dom that way )
how can implement this login in angular way ( means not accessing dom directly ) ?
Well, you have to tell Angular to run change detection as the code under onSlideChange (and other events) runs outside of Zone
Note that Swiper Angular component all events emits outside of NgZone
for better perfomance. Dont forget to use ngzone.run or
ChangeDetector if you need to change view (e.g slides) in event
handlers (e.g slideChange).
import the ChangeDetectorRef in the constructor and call the detectChange method in onSlideChange method
this.cd.detectChanges()
Here is the working code. I just take a random stackblitz link so adapt to your swiper version: https://stackblitz.com/edit/swiper-angular-example-rmgv2b

Angular: unable to scroll down to bottom in element

I've been postponing fixing this error that I have been having for a while now. I have the below chatwindow:
The window where I display the messages is a separate component (chat-window.component.ts). I want to scroll to the bottom with ngOnChanges.
When we receive the conversation with the messages from the parent component, where it is received from the server via an asynchronous request, we want to scroll to the bottom of the window element. We do this by calling the this.scrollToBottom() method of the class in the ngOnChanges lifecycle hook.
This.scrollToBottom does get called, but it doesn't scroll to the bottom of the element. Can someone see why?
chat-window.component.ts: in ngOnchanges we do some synchronous stuff before we call this.scrollToBottom()
export class ChatboxWindowComponent implements OnChanges, OnInit, AfterViewChecked {
#Input('conversation') conversation;
#ViewChild('window') window;
constructor() { }
ngOnChanges() {
// If the date separators have already been added once, we avoid doing it a second time
const existingDateObj = this.conversation.messages.findIndex((item, i) => item.dateObj);
if (existingDateObj === -1) {
this.conversation.messages.forEach( (item, index, array) => {
if (index !== 0) {
const date1 = new Date(array[index - 1].date);
const date2 = new Date(item.date);
if (date2.getDate() !== date1.getDate() || date2.getMonth() !== date1.getMonth()) {
this.conversation.messages.splice(index, 0, {date: date2, dateObj: true});
console.log(this.conversation.messages.length);
}
}
});
}
this.scrollToBottom();
}
ngOnInit() {
}
ngAfterViewChecked() {
}
isItMyMsg(msg) {
return msg.from._id === this.conversation.otherUser.userId;
}
scrollToBottom() {
try {
console.log('scrollToBottom called');
this.window.nativeElement.top = this.window.nativeElement.scrollHeight;
} catch (err) {}
}
}
chat-window.component.html
<div #window class="window">
<ng-container *ngFor="let message of conversation.messages">
<div class="date-container" *ngIf="!message.msg; else windowMsg">
<p class="date">{{message.date | amDateFormat:'LL'}}</p>
</div>
<ng-template #windowMsg>
<p
class="window__message"
[ngClass]="{
'window__message--left': isItMyMsg(message),
'window__message--right': !isItMyMsg(message)
}"
>
{{message.msg}}
</p>
</ng-template>
</ng-container>
</div>
The scroll doesn't work because the list of messages is not rendered yet when you call scrollToBottom. In order to scroll once the messages have been displayed, set a template reference variable (e.g. #messageContainer) on the message containers:
<ng-container #messageContainer *ngFor="let message of conversation.messages">
...
</ng-container>
In the code, you can then access these elements with ViewChildren and scroll the window when the QueryList.changes event is triggered:
#ViewChildren("messageContainer") messageContainers: QueryList<ElementRef>;
ngAfterViewInit() {
this.scrollToBottom(); // For messsages already present
this.messageContainers.changes.subscribe((list: QueryList<ElementRef>) => {
this.scrollToBottom(); // For messages added later
});
}
You can add the following code into your HTML element.
#window [scrollTop]="window.scrollHeight" *ngIf="messages.length > 0"
Full code according to your code sample as follows,
<div #window [scrollTop]="window.scrollHeight" *ngIf="messages.length > 0" class="window">
<ng-container *ngFor="let message of conversation.messages">
<div class="date-container" *ngIf="!message.msg; else windowMsg">
<p class="date">{{message.date | amDateFormat:'LL'}}</p>
</div>
<ng-template #windowMsg>
<p
class="window__message"
[ngClass]="{
'window__message--left': isItMyMsg(message),
'window__message--right': !isItMyMsg(message)
}"
>
{{message.msg}}
</p>
</ng-template>
</ng-container>
</div>
This is work for me. (Currently, I'm using Angular 11) 😊👍

Displaying Go To Top button when page becomes scrollable

Just wondering How I can do this in Angular 2/4 : This might be easy but I just can't figure out.
Here is my code:
Let me explain it, I have a component which scrolls me to the top of the page, when I am at the bottom. But the floating div i.e, little red arrow always stays visible even when page need not scroll.
In Html:
Each button is dynamically linked to div. So div displays when button is clicked
<div *ngFor="let sampledata of SAMPLEDATA; trackBy: trackId">
<button (click)="transmitInfo(sampledata ,0)" > </button>
<div *ngFor="let data of sampledata .data; trackBy: trackId" >
<button (click)="transmitInfo(data,1)" > </button>
</div>
<!-- This keeps on going -->
</div>
<div>
<div *ngIf="renderdata === 0"> {{Object Data}}</div>
<div *ngIf="renderdata === 1">{{Object Data}}</div>
<div *ngIf="renderdata === 2">{{Object Data}}</div>
</div>
<div id="scroolUpRight">
<img src="../../../content/images/scrollup.png" width="50px" height="50px" (click)="scrollToTop()">
</div>
Let's assume when a user clicks on button 2 or 3, 2nd or 3rd div is displayed based on button clicked, this div's are a huge data. Page automatically becomes scrollable when these are activated.
In Css:
#scroolUpRight {
position: fixed;
bottom: 4%;
right: 2%;
}
#scroolUpRight :hover {
cursor: pointer;
}
In my Component I have this to take me to the top of the page:
ngOnInit() {
this.renderdata = 0;
}
transmitInfo(data, type): void {
if (type === 1) { this.sampleData = data; this.renderdata = 1; }
if (type === 2) { this.dataData = data; this. renderdata = 2; }
}
scrollToTop() {
return window.scrollTo(0, 0);
}
Now I don't know if this works but I did this:
toogleScroolButton(): void {
if (window.screenY > 300 ) {
console.log('window length is 300 +');
}
}
But this is a function. How can I make a function or component that auto detects when page becomes scrollable and display this div, hide it when not scrollable.
Expected Result : Is to make this div visible once person starts to scroll.
Previous Knowledge:
I used Javascript and Jquery before to do the same. But how do I use
angular2,4 or higher for this? Reason I need this is to animate this div when
person starts to scroll.
I do accept recommendations to optimize the above code. Please do let me know if any.. ;)
This Worked. I need to get HostListener to get windows scroll even to see if I can scroll the page.
window.scrollY gives me the scroll page size which helps me in finding out if I am scrolling my page. If scrollY reaches to certain count I can say I am scrolling down i.e, I can trigger an *ngIf to true if I am scrolling bottom else I can make it false. Code Below :)
Add
import { HostListener } from '#angular/core';
export class BlaBlaBla {
//And this did the trick
activateGoTop : boolean;
OnNgInit :: activateGoTop = false /* added Silly Reference please put this in ngOnInit() { --- }*/
#HostListener('window:scroll',[])
onWindowScroll() {
if ( window.scrollY > 100 ) {
this.activateGoTop = true;
} else {
this.activateGoTop = false;
}
}
}
in Html:
//Gets activated when screenY is scrolled for more than 100px
<div id="scroolUpRight" *ngIf="activateGoTop">
<img src="../../../content/images/scrollup.png" width="50px" height="50px" (click)="scrollToTop()">
</div>
Hope this helps someOne .. ;)
You can use a simple *ngIf binding with your method:
<div *ngIf="scrollButton()">
Top <button>up button</button>
</div>
with scrollButton() method simple as that:
public scrollButton():boolean {
return window.screenY > 300;
}
The div will only get rendered if scrollButton() method returns true, this allows you to customize your top button render conditions easily, because you only need to return a boolean from it.

how to mix a decision of with an observer and synced actions? (Angular)

I have six square views in my project, I have to options to click on them:
click on one and and it get selected with some css fade color on top of it to show its selected. this already works.
select couple of them and having the same css fade color to show the squares are selected. this works to some degree but messed with something else.
so to recognize which mode im at I have radio buttom that call multipleSelectionMode.
so this is the code:
this is my square component html file.
<div *ngIf="oneChoosed" layout="row" layout-align="center center" class="one-chose">
</div>
<div *ngIf="multipleChoosed" layout="row" layout-align="center center" class="omult-chose">
</div>
in this component class I have 2 inputs I get from the main screen component:
#Input() oneChoosed: boolean;
#Input() multipleChoosed: boolean;
this is have I update this inputs from the main screen component html file:
[oneChoosed]="isOneChoosed(squares)"
[multipleChoosed]="isMultipleChoosed(squares)"
and in the main screen component class I get the click events and the id's of the squares, it looks like this:
export class mainScreenComp implements OnInit, OnDestroy {
multipleSelectionMode: Observable<boolean>;
oneChoosenIds: string[];
multipleChosenIds: string[];
_oneChoseSquareIdsSubscription: Subscription;
_multipleChoseSquaresIdsSubscription: Subscription;
ngOnInit() {
this.multipleSelectionMode = this.brainComp.curSelectionTyp.map(selecType => selecType === SelectionType.multiple);
this._oneChoseSquareIdsSubscription = this.brainComp.choosingOption.subscribe(
(chos: ChoosingOption) => {
switch (chos.option) {
case ChoosingOption.oneChose:
this.oneChoosenIds = chos.chosedIds.squares;
break;
case ChoosingOption.finishedOneSel:
this.oneChoosenIds = undefined;
break;
}
}
);
this._multipleChoseSquaresIdsSubscription = this.brainComp.choosingOption.subscribe(
(chos: Chosed) => {
this.multipleChosenIds = chos.squares;
}
);
}
isOneChoosed(square: SquaresIQ): boolean {
return (this.oneChoosenIds ? this.oneChoosenIds.includes(square.squareId) : false);
}
isMultipleChoosed(square: SquaresIQ): boolean {
return (this.multipleChosenIds ? this.multipleChosenIds.includes(square.squareId) : false);
}
so basically I add squares id's to the arrays oneChoosenIds and multipleChosenIds and check if they empty or not to decide what to return,
this works, BUT, when im in oneChoosing state where I can
only choose one, when I click on it I see both css square covers that shows that the square is selected...cause they have different design I can notice it.
so it means this *ngIf="oneChoosed" and this *ngIf="multipleChoosed" both returned true...so I thought to put extra condition with my observer multipleSelectionMode in the _multipleChoseSquaresIdsSubscription subscription, but this is async cause its an observer and the other actions are sync so its not working properly. do you have a suggestion for me to solve it?

Categories

Resources