Skip to content

Commit

Permalink
fix: copy canvas when using enableGhostResize (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
JBeen committed Aug 30, 2021
1 parent 6b3defd commit d65c257
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 7 deletions.
29 changes: 27 additions & 2 deletions demo/demo.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* tslint:disable:max-inline-declarations */
import { Component } from '@angular/core';
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { ResizeEvent } from '../src';

@Component({
Expand All @@ -21,6 +21,11 @@ import { ResizeEvent } from '../src';
box-sizing: border-box; // required for the enableGhostResize option to work
}
canvas {
width: 150px;
height: 100px;
}
.resize-handle-top,
.resize-handle-bottom {
position: absolute;
Expand Down Expand Up @@ -66,6 +71,8 @@ import { ResizeEvent } from '../src';
[resizeSnapGrid]="{ left: 50, right: 50 }"
(resizeEnd)="onResizeEnd($event)"
>
<div>HTML text example</div>
<canvas #canvas></canvas>
<div
class="resize-handle-top"
mwlResizeHandle
Expand All @@ -90,7 +97,10 @@ import { ResizeEvent } from '../src';
</div>
`
})
export class DemoComponent {
export class DemoComponent implements AfterViewInit {
@ViewChild('canvas')
public canvas: ElementRef<HTMLCanvasElement>;

public style: object = {};

validate(event: ResizeEvent): boolean {
Expand All @@ -115,4 +125,19 @@ export class DemoComponent {
height: `${event.rectangle.height}px`
};
}

drawCanvas(): void {
const ctx = this.canvas.nativeElement.getContext('2d');
if (ctx) {
ctx.font = '28px serif';
ctx.fillText('Canvas text example', 50, 50);
ctx.strokeStyle = 'green';
ctx.lineWidth = 5;
ctx.strokeRect(30, 10, 260, 60);
}
}

ngAfterViewInit(): void {
this.drawCanvas();
}
}
82 changes: 82 additions & 0 deletions src/clone-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/** 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');

descendantsWithId.forEach(descendant => {
descendant.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]);
}
}
}

// Counter for unique cloned radio button names.
let cloneUniqueId = 0;

/** Transfers the data of one input element to another. */
function transferInputData(
source: Element & { value: string },
clone: Element & { value: string; name: string; type: string }
) {
// Browsers throw an error when assigning the value of a file input programmatically.
if (clone.type !== 'file') {
clone.value = source.value;
}

// Radio button `name` attributes must be unique for radio button groups
// otherwise original radio buttons can lose their checked state
// once the clone is inserted in the DOM.
if (clone.type === 'radio' && clone.name) {
clone.name = `mat-clone-${clone.name}-${cloneUniqueId++}`;
}
}

/** 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 {}
}
}
3 changes: 2 additions & 1 deletion src/resizable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Edges } from './interfaces/edges.interface';
import { BoundingRectangle } from './interfaces/bounding-rectangle.interface';
import { ResizeEvent } from './interfaces/resize-event.interface';
import { IS_TOUCH_DEVICE } from './is-touch-device';
import { deepCloneNode } from './clone-node';

interface PointerEventCoordinate {
clientX: number;
Expand Down Expand Up @@ -723,7 +724,7 @@ export class ResizableDirective implements OnInit, OnChanges, OnDestroy {
this.renderer.setStyle(document.body, 'cursor', cursor);
this.setElementClass(this.elm, RESIZE_ACTIVE_CLASS, true);
if (this.enableGhostResize) {
currentResize.clonedNode = this.elm.nativeElement.cloneNode(true);
currentResize.clonedNode = deepCloneNode(this.elm.nativeElement);
this.elm.nativeElement.parentElement.appendChild(
currentResize.clonedNode
);
Expand Down
40 changes: 36 additions & 4 deletions test/resizable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* tslint:disable:max-inline-declarations enforce-component-selector */
import { Component, ElementRef, ViewChild } from '@angular/core';
import { ResizableDirective } from '../src/resizable.directive';
import { Component, DebugElement, ElementRef, ViewChild } from '@angular/core';
import {
MOUSE_MOVE_THROTTLE_MS,
ResizableDirective
} from '../src/resizable.directive';
import { Edges } from '../src/interfaces/edges.interface';
import { ResizeEvent, ResizableModule, ResizeHandleDirective } from '../src';
import { MOUSE_MOVE_THROTTLE_MS } from '../src/resizable.directive';
import { ResizableModule, ResizeEvent } from '../src';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { expect } from 'chai';
import * as sinon from 'sinon';
Expand Down Expand Up @@ -1630,4 +1632,34 @@ describe('resizable directive', () => {
}
});
});

it('should add canvas data to the ghost element', () => {
const template: string = `
<div
class="rectangle"
[ngStyle]="style"
mwlResizable
[resizeEdges]="resizeEdges"
[enableGhostResize]="enableGhostResize"
>
<canvas id="canvas"></canvas>
</div>
`;
const fixture: ComponentFixture<TestComponent> = createComponent(template);
const div = fixture.debugElement.childNodes[0] as DebugElement;
const canvas = div.nativeElement.children[0];
const ctx = canvas.getContext('2d');
ctx.fillText('Canvas text example', 0, 0);
const canvasData = canvas.toDataURL();

const elm: any = fixture.componentInstance.resizable.elm.nativeElement;
triggerDomEvent('mousedown', elm, {
clientX: 100,
clientY: 200
});
const clonedDiv = elm.nextSibling as HTMLElement;
const clonedCanvas = clonedDiv.children[0] as HTMLCanvasElement;
const actualCanvasData = clonedCanvas.toDataURL();
expect(actualCanvasData).to.equal(canvasData);
});
});

0 comments on commit d65c257

Please sign in to comment.