angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.22k stars 6.69k forks source link

[Drag and drop] problems when changing height after start #14703

Open JurajMlich opened 5 years ago

JurajMlich commented 5 years ago

What is the expected behavior?

Drag and drop working :)

What is the current behavior?

If I change the height of elements (that are draggable) for example by hiding some elements after I start dragging, the drag and drop mechanism thinks the items are still the original height (and therefore rendering placeholder at wrong place and behaving incorrectly).

What are the steps to reproduce?

https://stackblitz.com/edit/angular-material2-issue-ggdtyr?file=app%2Fapp.component.css

(moving between columns does not work for some unknown reason but that is not important)

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

newest ones

Is there anything else we should know?

If you point me in the right direction, I can provide a fix.

sergiocosus commented 5 years ago

@JurajMlich A temporal hack fix to that issue, on the cdkDragStarted event i put this, it is not a nice solution but meanwhile it is fixed.

  dragStarted(event: {source: CdkDrag}) {
    setTimeout(() => {
      event.source._dragRef.dropContainer['_cachePositions']();
    }, 200);
  }
vorachirag commented 4 years ago

@JurajMlich I tried to attempt the same thing. I just hide content inside of draggable elements when an element starts dragging.
.cdk-drop-list-dragging { mat-card { mat-card-content { display: none; } } }

@sergiocosus I am getting an error when I tried your solution.

Property 'dropContainer' does not exist on type 'DragRef<CdkDrag<any>>'. Did you mean '_dropContainer'?ts(2551)
drag-ref.d.ts(137, 13): '_dropContainer' is declared here.
thabalija commented 4 years ago

@vorachirag it worked before the update. Now this works:

dragStarted(event: {source: CdkDrag}) {
  setTimeout(() => {
    const dropContainer = event.source._dragRef['_dropContainer'];

    if (dropContainer) {
      dropContainer['_cacheOwnPosition']();
      dropContainer['_cacheItemPositions']();
    }
  });
}
vorachirag commented 4 years ago

@thabalija Thanks. It works.

crisbeto commented 3 years ago

Bumping to a P2 since it seems like an issue that people keep bumping into, based on the number of duplicate issues.

Achilles1515 commented 3 years ago

There are a variety of problems going on with this issue and those recently closed.

One of which is that connected drop list client rects are not being cached after an action occurs that could affect the rendered layout (e.g. transferring an item between lists with no fixed size).

This problem can be alleviated with something like the following:

// drop-list-ref.ts
  enter(item: DragRef, pointerX: number, pointerY: number, index?: number): void {
    this.start();

    // ...add item to this drop list's draggables 

    // Note that the positions were already cached when we called `start` above,
    // but we need to refresh them since the amount of items has changed and also parent rects.
    this._cacheItemPositions();
    this._cacheParentPositions();

    // NEW - remeasure connected drop list client rects
    this._siblings.forEach(sibling => {
        sibling._clientRect = sibling.element.getBoundingClientRect();
    });

    this.entered.next({item, container: this, currentIndex: this.getItemIndex(item)});
  }

(though obviously not using private variables directly and perhaps setting up a public method to re-cache the drop list client rect)

Example of problem (StackBlitz): GIF2

With above solution (uncomment lines 108-110): GIF3


The other main issue I see, which is going to require a good deal of refactoring to fix, is that this library's functionality completely starts to fall apart once you start adding layout changing CSS styles to the classes added by dynamic host bindings (e.g. .cdk-drop-list-dragging). These classes being added/removed require change detection to run, which is an awful dependency for a general purpose library like this, and change detection is generally running after rects have been cached, causing all these measurements to [potentially] be invalidated and never refreshed. I talked about this a little more here.

GitHubish commented 3 years ago

@crisbeto any news on how to manage this case simply? I'm trying to drag and drop an element on an area that is collapsed and uncollapsed on hovering but it's really tedious.

digaus commented 3 years ago

I have a similar issue with my elements. They have a dynamic height but it should not change while dragging. However the placeholder height seems to be wrong as it will overlap the remaining elements (it's correctly visible but height seems to be smaller than real height resulting in the overlap)

YSFKBDY commented 3 years ago

@crisbeto any news on how to manage this case simply? I'm trying to drag and drop an element on an area that is collapsed and uncollapsed on hovering but it's really tedious.

I have literally the same problem. Is there any fix or workaround for this?

Edit: I've tried @Achilles1515's solution, it will work, but I need to trigger it at a specific condition. How can I do that?

BenRacicot commented 2 years ago

Thank you @crisbeto for referring my issue to this one. For the record here is a Stackblitz repro.

Azbesciak commented 1 year ago

I have some glitches even when the height does not change, but when the element underneath need some rerendering (same height still). The list from that point just freezes, even sort preview does not work then.

oleksandr-kupenko commented 1 year ago

I had a task to increase the area of all containers when the move started. I had a task to increase the area of all containers when the move started. I had to override the start and enter methods for this in the prototype. I understand how bad this is, but a quick solution. Helped in this answer Achilles1515.

import {
  CdkDragDrop,
  DragDrop, DragRef,
  DropListRef,
  moveItemInArray,
  transferArrayItem
} from '@angular/cdk/drag-drop';

export class MyComponentWithStartCotainer  { 
  ...
}

DropListRef.prototype.start = function(force?: boolean) {
  const styles = coerceElement(this.element).style;
  this.beforeStarted.next();
  this._isDragging = true;
  // this._initialScrollSnap = styles.msScrollSnapType || (styles as any).scrollSnapType || '';
  (styles as any).scrollSnapType = styles.msScrollSnapType = 'none';
  this._cacheItems();
  this._siblings.forEach(sibling => sibling._startReceiving(this));
  // this._viewportScrollSubscription.unsubscribe();
  // this._listenToScrollEvents();

  if (!force) {
    setTimeout(() => {
      const item = this._draggables[0];
      this.exit(item);
      this.start(true);
    }, 0);
  }
}

  DropListRef.prototype.enter = function(item: DragRef, pointerX: number, pointerY: number, index?: number): void {
    (this.start as (force: boolean) => void)(true);

    let newIndex: number;

    if (index == null) {
      newIndex = this.sortingDisabled ? this._draggables.indexOf(item) : -1;

      if (newIndex === -1) {
        newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY);
      }
    } else {
      newIndex = index;
    }

    const activeDraggables = this._activeDraggables;
    const currentIndex = activeDraggables.indexOf(item);
    const placeholder = item.getPlaceholderElement();
    let newPositionReference: DragRef | undefined = activeDraggables[newIndex];
    if (newPositionReference === item) {
      newPositionReference = activeDraggables[newIndex + 1];
    }
    if (currentIndex > -1) {
      activeDraggables.splice(currentIndex, 1);
    }
    if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) {
      const element = newPositionReference.getRootElement();
      element.parentElement!.insertBefore(placeholder, element);
      activeDraggables.splice(newIndex, 0, item);
    } else if (this._shouldEnterAsFirstChild(pointerX, pointerY)) {
      const reference = activeDraggables[0].getRootElement();
      reference.parentNode!.insertBefore(placeholder, reference);
      activeDraggables.unshift(item);
    } else {
      this.element.appendChild(placeholder);
      activeDraggables.push(item);
    }
    placeholder.style.transform = '';
    this._cacheItemPositions();
    this._cacheParentPositions();

    this.entered.next({item, container: this, currentIndex: this.getItemIndex(item)});
  }

The contents of the start and enter methods may differ. I took for version 9 from the source on GitHub

The key here is the repeated call of methods for caching elements is setTimeout. And when we call the start method again (in the enter method), we no longer do repeated caching.

bezo97 commented 4 months ago

This has changed a bit in newer versions since the last answer but the issue remains. I've found that simply calling _cacheParentPositions() on DropListRef whenever the placeholders are resized/repositioned is a sufficient workaround.

AnnaKozlova commented 3 months ago

Hello!

Is there anyone who can help with a case: all my elements are expandable panels. I need collapse all elements before dragging and ensure that dragging element works correctly;

https://stackblitz.com/edit/angular-csru2j-f21c6s?file=src%2Fapp%2Fexpansion-overview-example.ts

raphael-bison commented 3 months ago

Hello

We currently are experiencing the following issue: When we add an element, in our case an drop zone on drag start and the height of the row changes, then we can not drop the item. Somehow the drop list will not be recognized unless you move the item over the row in a circle for a second or two. Yet its not always the same behavior sometimes it works like it should and sometimes it doesn't work at all.

Does anyone have an solution for this issue?

F.Y. I tried the ones in this thread already but none of them worked for us.

Aidan-Chey commented 2 months ago

I have a similar issue with a Angular 17 component. It's a set of columns that overflow off screen (horizontally). When drag starts I adjust their width style so flex makes them fit on the page. After doing these adjustments, the dragging element doesn't seem to recognise that i t has been dragged into a list (no drag entered event).

Weirdly once I drag over to the first column (the one that hasn't really changed position) the whole system works correctly. Some sort of public method to re-calculate drop list boundaries would be appreciated, I could call that after dragging starts.

I've tried a few things like calling _cacheParentPostions, or triggering a drag entered event on the first element, or dispatching a resize event to the window (AI suggested). Nothing really seems to help.

IBot18 commented 1 month ago

Hello,

I'm stuck at a similar problem (at least i think so)

I have 3 Lists with interchangable items. A List is only shown initialy if it has at least one item. Otherwise height and visiblility are changed so it's not visible:

.cdk-drop-list {
  margin: 0;
  //background: radial-gradient(85.63% 310.97% at 19.17% 24.48%, rgba(121, 128, 127, 0.9) 0.08%, rgba(84, 89, 89, 0.9) 100%);
  background: #00000033;
  border-radius: 8px;
  box-shadow: inset 0 0 4px 1px #00000033;
  height: auto;
  min-height: 45px;
}
//this is just my last try, similar stylings have the same result
.cdk-drop-list:not(.cdk-drop-list-receiving):not(.show) {
  visibility: hidden;
  min-height: unset;
}

Now if i start dragging ('show') is set true and as a result the list(-container) is shown. But I'm not able to drag any items into it. The interesting part!!: This is unless i scroll just a tiny bit. I tried to simulate this with code in typescript but it didnt work.

If dont use my styling with min-height, or any other attempt to change the height dynamically it works, but of course this is a no go from the visual/UX standpoint.

I wasnt able to get it to work even with the workarounds like suggested here by @thabalija

Are there any ideas what i could try to fix this?

Edit: I was able to reproduce our code and problem in stackblitz: https://stackblitz.com/edit/stackblitz-starters-iwvwyg?file=src%2Fapp%2Fapp-module.component.html

Some interesting observations: It is possible to drag into the last list, as long as there are items in the first list ('todo')! or i add more items to the second list, so i have to scroll into the last list.

Any advice would be highly appreciated :)

antur84 commented 1 month ago

I stumbled upon this issue when investigating this very bug in our app where the container sizes change as you drag large items around, making it very hard to drop items at desired locations. As a workaround I eventually had to resort to code violence =/

During dragging operation, I call _cacheParentPositions() on the DropListRef every 100 ms.

IBot18 commented 1 month ago

I stumbled upon this issue when investigating this very bug in our app where the container sizes change as you drag large items around, making it very hard to drop items at desired locations. As a workaround I eventually had to resort to code violence =/

During dragging operation, I call _cacheParentPositions() on the DropListRef every 100 ms.

Could you provide a full code snippet for me, maybe in my stackblitz reproduction?

Edit: I made it work in my example, have to try it in production code. But as you said "violance" is the right term. Wish there was a solution in the framework for this...

antur84 commented 1 month ago

I stumbled upon this issue when investigating this very bug in our app where the container sizes change as you drag large items around, making it very hard to drop items at desired locations. As a workaround I eventually had to resort to code violence =/ During dragging operation, I call _cacheParentPositions() on the DropListRef every 100 ms.

Could you provide a full code snippet for me, maybe in my stackblitz reproduction?

Edit: I made it work in my example, have to try it in production code. But as you said "violance" is the right term. Wish there was a solution in the framework for this...

Aha I missed your edit, yeah glad it's working for you. Yes the library code obvisouly had a performance problem they wanted to solve with caching, but now we have a problem with caching instead 🍰

IBot18 commented 1 month ago

Got it to work in production code as well. Had to use multiple surpresses for eslint. Whoever will do the review in my team, will scratch his head on this one for sure...

raphael-bison commented 1 month ago

What worked for me is changeDetectorRef.detectChanges(). Since the Table Height got changed, because of the drop-zone item which got displayed when *ngIf="this.isDragging".

dragStarted(event: CdkDragStart<Email>) { this.dragItem = event.source.data; this.isDragging = true; this.changeDetectorRef.detectChanges(); }

huy4429dev commented 3 weeks ago

collapse

image image

I have resolved the issue as follows:

  1. Update your state to collapse all items
  2. Update dragItems
  3. Call detectChanges
  4. Set display: none for collapsed content