Skip to content

Commit 31e72a7

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): preview element not maintaining canvas data (#15808)
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.
1 parent 399f25e commit 31e72a7

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,26 @@ describe('CdkDrag', () => {
15131513
expect(preview.getAttribute('id')).toBeFalsy();
15141514
}));
15151515

1516+
it('should clone the content of descendant canvas elements', fakeAsync(() => {
1517+
const fixture = createComponent(DraggableWithCanvasInDropZone);
1518+
fixture.detectChanges();
1519+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
1520+
const sourceCanvas = item.querySelector('canvas') as HTMLCanvasElement;
1521+
1522+
// via https://stackoverflow.com/a/17386803/2204158
1523+
expect(sourceCanvas.getContext('2d')!
1524+
.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height)
1525+
.data.some(channel => channel !== 0)).toBe(true, 'Expected source canvas to have data.');
1526+
1527+
startDraggingViaMouse(fixture, item);
1528+
1529+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
1530+
const previewCanvas = preview.querySelector('canvas')!;
1531+
1532+
expect(previewCanvas.toDataURL()).toBe(sourceCanvas.toDataURL(),
1533+
'Expected cloned canvas to have the same content as the source.');
1534+
}));
1535+
15161536
it('should clear the ids from descendants of the preview', fakeAsync(() => {
15171537
const fixture = createComponent(DraggableInDropZone);
15181538
fixture.detectChanges();
@@ -3821,6 +3841,47 @@ class ConnectedWrappedDropZones {
38213841
done = ['Four', 'Five', 'Six'];
38223842
}
38233843

3844+
@Component({
3845+
template: `
3846+
<div
3847+
cdkDropList
3848+
style="width: 100px; background: pink;"
3849+
[id]="dropZoneId"
3850+
[cdkDropListData]="items"
3851+
(cdkDropListSorted)="sortedSpy($event)"
3852+
(cdkDropListDropped)="droppedSpy($event)">
3853+
<div
3854+
*ngFor="let item of items"
3855+
cdkDrag
3856+
[cdkDragData]="item"
3857+
[style.height.px]="item.height"
3858+
[style.margin-bottom.px]="item.margin"
3859+
style="width: 100%; background: red;">
3860+
{{item.value}}
3861+
<canvas width="100px" height="100px"></canvas>
3862+
</div>
3863+
</div>
3864+
`
3865+
})
3866+
class DraggableWithCanvasInDropZone extends DraggableInDropZone implements AfterViewInit {
3867+
constructor(private _elementRef: ElementRef<HTMLElement>) {
3868+
super();
3869+
}
3870+
3871+
ngAfterViewInit() {
3872+
const canvases = this._elementRef.nativeElement.querySelectorAll('canvas');
3873+
3874+
// Add a circle to all the canvases.
3875+
for (let i = 0; i < canvases.length; i++) {
3876+
const canvas = canvases[i];
3877+
const context = canvas.getContext('2d')!;
3878+
context.beginPath();
3879+
context.arc(50, 50, 40, 0, 2 * Math.PI);
3880+
context.stroke();
3881+
}
3882+
}
3883+
}
3884+
38243885
/**
38253886
* Component that passes through whatever content is projected into it.
38263887
* Used to test having drag elements being projected into a component.

src/cdk/drag-drop/drag-ref.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,7 @@ function getTransform(x: number, y: number): string {
10491049
function deepCloneNode(node: HTMLElement): HTMLElement {
10501050
const clone = node.cloneNode(true) as HTMLElement;
10511051
const descendantsWithId = clone.querySelectorAll('[id]');
1052+
const descendantCanvases = node.querySelectorAll('canvas');
10521053

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

1061+
// `cloneNode` won't transfer the content of `canvas` elements so we have to do it ourselves.
1062+
// We match up the cloned canvas to their sources using their index in the DOM.
1063+
if (descendantCanvases.length) {
1064+
const cloneCanvases = clone.querySelectorAll('canvas');
1065+
1066+
for (let i = 0; i < descendantCanvases.length; i++) {
1067+
const correspondingCloneContext = cloneCanvases[i].getContext('2d');
1068+
1069+
if (correspondingCloneContext) {
1070+
correspondingCloneContext.drawImage(descendantCanvases[i], 0, 0);
1071+
}
1072+
}
1073+
}
1074+
10601075
return clone;
10611076
}
10621077

0 commit comments

Comments
 (0)