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

Add ShapeClipboardManager #633

Merged
merged 3 commits into from
Dec 18, 2024
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
13 changes: 13 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/serialization/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,16 @@ export class SerializableLineConnector {
return result;
}
}

/* eslint-disable @typescript-eslint/no-explicit-any */
export const SerializableLineConnectorArraySerializer = {
serialize(value: SerializableLineConnector[]): any[] {
// @ts-expect-error toJson is attached by Jsonizable
return value.map(connector => connector.toJson());
},
deserialize(value: any[]): SerializableLineConnector[] {
// @ts-expect-error fromJson is attached by Jsonizable
return value.map(json => SerializableLineConnector.fromJson(json));
},
};
/* eslint-enable @typescript-eslint/no-explicit-any */
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ export function Jsonizable(constructor: Function) {
// 1st: Check if the field has a serializer
// 2nd: Check if the field has a fromJson method
// 3rd: Use the value directly
console.log(field, key, value);
if (serializers[key]) {
instance[key] = serializers[key].deserialize(value);
} else if (field && field.constructor && field.constructor.fromJson) {
Expand Down
16 changes: 8 additions & 8 deletions monosketch-svelte/src/lib/mono/shape/serialization/shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ export class SerializableLine extends AbstractSerializableShape {
return result;
}
}
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
const ShapeArraySerializer = {
/* eslint-disable @typescript-eslint/no-explicit-any */
export const ShapeArraySerializer = {
serialize: (value: AbstractSerializableShape[]): any[] => {
// @ts-ignore
// @ts-expect-error toJson is attached by Jsonizable
return value.map((shape) => shape.toJson());
},

Expand All @@ -233,24 +233,24 @@ const ShapeArraySerializer = {
const type = json["type"];
switch (type) {
case "R":
// @ts-ignore
// @ts-expect-error fromJson is attached by Jsonizable
return SerializableRectangle.fromJson(json);
case "T":
// @ts-ignore
// @ts-expect-error fromJson is attached by Jsonizable
return SerializableText.fromJson(json);
case "L":
// @ts-ignore
// @ts-expect-error fromJson is attached by Jsonizable
return SerializableLine.fromJson(json);
case "G":
// @ts-ignore
// @ts-expect-error fromJson is attached by Jsonizable
return SerializableGroup.fromJson(json);
default:
throw new Error(`Unrecognizable type ${type}`);
}
});
},
};
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
/* eslint-enable @typescript-eslint/no-explicit-any */

/**
* A serializable class for a group shape.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, tuanchauict
*/
import { Point, PointF } from "$libs/graphics-geo/point";
import { Rect } from "$libs/graphics-geo/rect";
import { SerializableLineConnector } from "$mono/shape/serialization/connector";
import { SerializableRectExtra } from "$mono/shape/serialization/extras";
import { SerializableRectangle } from "$mono/shape/serialization/shapes";
import { ClipboardObject } from "$mono/shape/shape-clipboard-manager";
import { LineAnchor } from "$mono/shape/shape/linehelper";
import { describe, expect, test } from "vitest";

describe('ClipboardObjectTest', () => {
test('testJsonize', () => {
const shape = SerializableRectangle.create(
{
id: '1',
isIdTemporary: false,
versionCode: 0,
bound: Rect.ZERO,
extra: SerializableRectExtra.EMPTY,
},
);
const connector = SerializableLineConnector.create(
{
lineId: '1',
anchor: LineAnchor.START,
targetId: '2',
ratio: PointF.create({ left: 0, top: 0 }),
offset: Point.ZERO,
},
);

const clipboardObject = ClipboardObject.create([shape], [connector]);
// @ts-expect-error toJson is attached by Jsonizable
const json = clipboardObject.toJson();
console.log(json);
expect(json).toEqual({
// @ts-expect-error toJson is attached by Jsonizable
shapes: [shape.toJson()],
// @ts-expect-error toJson is attached by Jsonizable
connectors: [connector.toJson()],
});

// @ts-expect-error fromJson is attached by Jsonizable
const deserializedClipboardObject = ClipboardObject.fromJson(json);
expect(deserializedClipboardObject).toStrictEqual(clipboardObject);
});
});
123 changes: 123 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/shape-clipboard-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { Flow } from "$libs/flow";
import { Rect } from "$libs/graphics-geo/rect";
import { TextExtra } from "$mono/shape/extra/shape-extra";
import {
type SerializableLineConnector,
SerializableLineConnectorArraySerializer,
} from "$mono/shape/serialization/connector";
import { Jsonizable, Serializer, SerialName } from "$mono/shape/serialization/serializable";
import {
type AbstractSerializableShape,
SerializableText,
ShapeArraySerializer,
} from "$mono/shape/serialization/shapes";
import { AbstractShape } from "./shape/abstract-shape";

/**
* A clipboard manager specializing for shapes.
* This class handles storing shapes to clipboard and getting shapes in clipboard from paste action.
*/
export class ShapeClipboardManager {
private clipboardShapeMutableFlow: Flow<ClipboardObject> = new Flow(ClipboardObject.create([], []));
public clipboardShapeFlow: Flow<ClipboardObject> = this.clipboardShapeMutableFlow.immutable();

constructor() {
document.onpaste = (event) => {
event.preventDefault();
event.stopPropagation();
this.onPasteText(event.clipboardData?.getData('text/plain') || '');
};
}

private onPasteText(text: string) {
if (text.trim() === '') {
return;
}
const json = this.parseJsonFromString(text);
// @ts-expect-error fromJson is attached by Jsonizable
const clipboardObject = json ? ClipboardObject.fromJson(json) : null;
if (clipboardObject && clipboardObject.shapes.length > 0) {
this.clipboardShapeMutableFlow.value = clipboardObject;
}

this.clipboardShapeMutableFlow.value = ClipboardObject.create([this.createTextShapeFromText(text)], []);
}

private parseJsonFromString(text: string) {
try {
const json = JSON.parse(text);
// Backward compatibility with old clipboard format
return Array.isArray(json) ? { shapes: json, connectors: [] } : json;
} catch (e) {
console.error('Failed to parse JSON from clipboard text', e);
return null;
}

}

private createTextShapeFromText(text: string): SerializableText {
const lines = text.split('\n').flatMap(line => line.match(/.{1,400}/g) || []);
const width = Math.max(...lines.map(line => line.length));
const height = lines.length;

const toBeUsedText = text.replace(/ /g, '\u00a0');
return SerializableText.create({
id: null,
isIdTemporary: false,
versionCode: AbstractShape.nextVersionCode(),
bound: Rect.byLTWH(0, 0, width, height),
text: toBeUsedText,
extra: TextExtra.NO_BOUND.toSerializableExtra(),
isTextEditable: false,
});
}

public setClipboard(clipboardObject: ClipboardObject) {
// @ts-expect-error toJson is attached by Jsonizable
const text = JSON.stringify(clipboardObject.toJson());
this.setClipboardText(text);
}

public setClipboardText(text: string) {
const textArea = document.createElement('textarea');
textArea.style.position = 'absolute';
textArea.style.left = '-10000px';
textArea.style.top = '-10000px';
textArea.style.width = '1px';
textArea.style.height = '1px';
textArea.style.opacity = '0';
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
// TODO: Address this deprecated method
document.execCommand('copy');
document.body.removeChild(textArea);
}
}

/**
* A data class to store shapes and connectors in clipboard when serializing to JSON.
*/
@Jsonizable
export class ClipboardObject {
@SerialName("shapes")
@Serializer(ShapeArraySerializer)
public shapes: AbstractSerializableShape[] = [];
@SerialName("connectors")
@Serializer(SerializableLineConnectorArraySerializer)
public connectors: SerializableLineConnector[] = [];

private constructor() {
}

static create(shapes: AbstractSerializableShape[], connectors: SerializableLineConnector[]): ClipboardObject {
const result = new ClipboardObject();
result.shapes = shapes;
result.connectors = connectors;
return result;
}
}
Loading