Skip to content

Commit

Permalink
fix(drag-drop): preview element not maintaining canvas data (#15808)
Browse files Browse the repository at this point in the history
By default we generate the preview and placeholder for a `cdkDrag` using `cloneNode`, however it won't clone the content of `canvas` elements. These changes add some extra logic to transfer the canvas content over into the clones.

Fixes #15685.
  • Loading branch information
crisbeto authored and jelbourn committed May 13, 2019
1 parent 399f25e commit 31e72a7
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 0 deletions.
61 changes: 61 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,26 @@ describe('CdkDrag', () => {
expect(preview.getAttribute('id')).toBeFalsy();
}));

it('should clone the content of descendant canvas elements', fakeAsync(() => {
const fixture = createComponent(DraggableWithCanvasInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const sourceCanvas = item.querySelector('canvas') as HTMLCanvasElement;

// via https://stackoverflow.com/a/17386803/2204158
expect(sourceCanvas.getContext('2d')!
.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height)
.data.some(channel => channel !== 0)).toBe(true, 'Expected source canvas to have data.');

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const previewCanvas = preview.querySelector('canvas')!;

expect(previewCanvas.toDataURL()).toBe(sourceCanvas.toDataURL(),
'Expected cloned canvas to have the same content as the source.');
}));

it('should clear the ids from descendants of the preview', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down Expand Up @@ -3821,6 +3841,47 @@ class ConnectedWrappedDropZones {
done = ['Four', 'Five', 'Six'];
}

@Component({
template: `
<div
cdkDropList
style="width: 100px; background: pink;"
[id]="dropZoneId"
[cdkDropListData]="items"
(cdkDropListSorted)="sortedSpy($event)"
(cdkDropListDropped)="droppedSpy($event)">
<div
*ngFor="let item of items"
cdkDrag
[cdkDragData]="item"
[style.height.px]="item.height"
[style.margin-bottom.px]="item.margin"
style="width: 100%; background: red;">
{{item.value}}
<canvas width="100px" height="100px"></canvas>
</div>
</div>
`
})
class DraggableWithCanvasInDropZone extends DraggableInDropZone implements AfterViewInit {
constructor(private _elementRef: ElementRef<HTMLElement>) {
super();
}

ngAfterViewInit() {
const canvases = this._elementRef.nativeElement.querySelectorAll('canvas');

// Add a circle to all the canvases.
for (let i = 0; i < canvases.length; i++) {
const canvas = canvases[i];
const context = canvas.getContext('2d')!;
context.beginPath();
context.arc(50, 50, 40, 0, 2 * Math.PI);
context.stroke();
}
}
}

/**
* Component that passes through whatever content is projected into it.
* Used to test having drag elements being projected into a component.
Expand Down
15 changes: 15 additions & 0 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,7 @@ function getTransform(x: number, y: number): string {
function deepCloneNode(node: HTMLElement): HTMLElement {
const clone = node.cloneNode(true) as HTMLElement;
const descendantsWithId = clone.querySelectorAll('[id]');
const descendantCanvases = node.querySelectorAll('canvas');

// Remove the `id` to avoid having multiple elements with the same id on the page.
clone.removeAttribute('id');
Expand All @@ -1057,6 +1058,20 @@ function deepCloneNode(node: HTMLElement): HTMLElement {
descendantsWithId[i].removeAttribute('id');
}

// `cloneNode` won't transfer the content of `canvas` elements so we have to do it ourselves.
// We match up the cloned canvas to their sources using their index in the DOM.
if (descendantCanvases.length) {
const cloneCanvases = clone.querySelectorAll('canvas');

for (let i = 0; i < descendantCanvases.length; i++) {
const correspondingCloneContext = cloneCanvases[i].getContext('2d');

if (correspondingCloneContext) {
correspondingCloneContext.drawImage(descendantCanvases[i], 0, 0);
}
}
}

return clone;
}

Expand Down

0 comments on commit 31e72a7

Please sign in to comment.