From 13d9918f6a3caa2e9a39a4b7b8ede78a53e395aa Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 13 Apr 2019 12:20:14 +0200 Subject: [PATCH] fix(drag-drop): preview element not maintaining canvas data 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. --- src/cdk/drag-drop/directives/drag.spec.ts | 61 +++++++++++++++++++++++ src/cdk/drag-drop/drag-ref.ts | 15 ++++++ 2 files changed, 76 insertions(+) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 30b4eca62e33..ab215629b7b1 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -1487,6 +1487,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(); @@ -3731,6 +3751,47 @@ class ConnectedWrappedDropZones { done = ['Four', 'Five', 'Six']; } +@Component({ + template: ` +
+
+ {{item.value}} + +
+
+ ` +}) +class DraggableWithCanvasInDropZone extends DraggableInDropZone implements AfterViewInit { + constructor(private _elementRef: ElementRef) { + 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. diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 341a1c27b784..7b9403ba5b48 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -1045,6 +1045,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'); @@ -1053,6 +1054,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; }