Skip to content

Commit

Permalink
fix(drag-drop): not transferring input value when cloning element for…
Browse files Browse the repository at this point in the history
… preview (#20009)

We use `cloneNode` to create a preview for an element which clones all of the attributes of an element, but that may not pick up properties like its `value`. These changes add some logic where we clone the value with a similar approach that we use for cloning `canvas` elements. Also generalizes the approach a bit so we don't have to keep repeating the logic.

Fixes #19905.

(cherry picked from commit d53d5b6)
  • Loading branch information
crisbeto authored and wagnermaciel committed Jul 21, 2020
1 parent d383bba commit 9833eea
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 34 deletions.
63 changes: 63 additions & 0 deletions src/cdk/drag-drop/clone-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/** Creates a deep clone of an element. */
export function deepCloneNode(node: HTMLElement): HTMLElement {
const clone = node.cloneNode(true) as HTMLElement;
const descendantsWithId = clone.querySelectorAll('[id]');
const nodeName = node.nodeName.toLowerCase();

// Remove the `id` to avoid having multiple elements with the same id on the page.
clone.removeAttribute('id');

for (let i = 0; i < descendantsWithId.length; i++) {
descendantsWithId[i].removeAttribute('id');
}

if (nodeName === 'canvas') {
transferCanvasData(node as HTMLCanvasElement, clone as HTMLCanvasElement);
} else if (nodeName === 'input' || nodeName === 'select' || nodeName === 'textarea') {
transferInputData(node as HTMLInputElement, clone as HTMLInputElement);
}

transferData('canvas', node, clone, transferCanvasData);
transferData('input, textarea, select', node, clone, transferInputData);
return clone;
}

/** Matches elements between an element and its clone and allows for their data to be cloned. */
function transferData<T extends Element>(selector: string, node: HTMLElement, clone: HTMLElement,
callback: (source: T, clone: T) => void) {
const descendantElements = node.querySelectorAll<T>(selector);

if (descendantElements.length) {
const cloneElements = clone.querySelectorAll<T>(selector);

for (let i = 0; i < descendantElements.length; i++) {
callback(descendantElements[i], cloneElements[i]);
}
}
}

/** Transfers the data of one input element to another. */
function transferInputData(source: Element & {value: string}, clone: Element & {value: string}) {
clone.value = source.value;
}

/** Transfers the data of one canvas element to another. */
function transferCanvasData(source: HTMLCanvasElement, clone: HTMLCanvasElement) {
const context = clone.getContext('2d');

if (context) {
// In some cases `drawImage` can throw (e.g. if the canvas size is 0x0).
// We can't do much about it so just ignore the error.
try {
context.drawImage(source, 0, 0);
} catch {}
}
}
57 changes: 57 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,31 @@ describe('CdkDrag', () => {
expect(document.querySelector('.cdk-drag-preview canvas')).toBeTruthy();
}));

it('should clone the content of descendant input elements', fakeAsync(() => {
const fixture = createComponent(DraggableWithInputsInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const sourceInput = item.querySelector('input')!;
const sourceTextarea = item.querySelector('textarea')!;
const sourceSelect = item.querySelector('select')!;
const value = fixture.componentInstance.inputValue;

expect(sourceInput.value).toBe(value);
expect(sourceTextarea.value).toBe(value);
expect(sourceSelect.value).toBe(value);

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')!;
const previewInput = preview.querySelector('input')!;
const previewTextarea = preview.querySelector('textarea')!;
const previewSelect = preview.querySelector('select')!;

expect(previewInput.value).toBe(value);
expect(previewTextarea.value).toBe(value);
expect(previewSelect.value).toBe(value);
}));

it('should clear the ids from descendants of the preview', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down Expand Up @@ -6366,6 +6391,38 @@ class DraggableWithAlternateRootAndSelfHandle {
}


@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}}
<input [value]="inputValue"/>
<textarea [value]="inputValue"></textarea>
<select [value]="inputValue">
<option value="goodbye">Goodbye</option>
<option value="hello">Hello</option>
</select>
</div>
</div>
`
})
class DraggableWithInputsInDropZone extends DraggableInDropZone {
inputValue = 'hello';
}


/**
* Drags an element to a position on the page using the mouse.
* @param fixture Fixture on which to run change detection.
Expand Down
35 changes: 1 addition & 34 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
import {getTransformTransitionDurationInMs} from './transition-duration';
import {getMutableClientRect, adjustClientRect} from './client-rect';
import {ParentPositionTracker} from './parent-position-tracker';
import {deepCloneNode} from './clone-node';

/** Object that can be used to configure the behavior of DragRef. */
export interface DragRefConfig {
Expand Down Expand Up @@ -1312,40 +1313,6 @@ function getTransform(x: number, y: number): string {
return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
}

/** Creates a deep clone of an element. */
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');

for (let i = 0; i < descendantsWithId.length; i++) {
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) {
// In some cases `drawImage` can throw (e.g. if the canvas size is 0x0).
// We can't do much about it so just ignore the error.
try {
correspondingCloneContext.drawImage(descendantCanvases[i], 0, 0);
} catch {}
}
}
}

return clone;
}

/** Clamps a value between a minimum and a maximum. */
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
Expand Down

0 comments on commit 9833eea

Please sign in to comment.