Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copy canvas when using enableGhostResize #129

Merged
merged 2 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});