feat!: add ability to copy and paste workspace comments (#8024)

* chore: add support for copying and pasting workspace comments

* chore: fix build

* fix: PR comments
This commit is contained in:
Beka Westberg
2024-04-15 21:43:58 +00:00
committed by GitHub
parent b6753a250e
commit dabb11f4cb
6 changed files with 145 additions and 39 deletions

View File

@@ -383,6 +383,18 @@ WorkspaceSvg.prototype.newBlock = function (
return new BlockSvg(this, prototypeName, opt_id);
};
Workspace.prototype.newComment = function (
id?: string,
): comments.WorkspaceComment {
return new comments.WorkspaceComment(this, id);
};
WorkspaceSvg.prototype.newComment = function (
id?: string,
): comments.RenderedWorkspaceComment {
return new comments.RenderedWorkspaceComment(this, id);
};
WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
return new Trashcan(workspace);
};

View File

@@ -8,11 +8,14 @@ import {IPaster} from '../interfaces/i_paster.js';
import {ICopyData} from '../interfaces/i_copyable.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
import * as registry from './registry.js';
import * as commentSerialiation from '../serialization/workspace_comments.js';
import * as eventUtils from '../events/utils.js';
import * as common from '../common.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
export class WorkspaceCommentPaster
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
implements IPaster<WorkspaceCommentCopyData, RenderedWorkspaceComment>
{
static TYPE = 'workspace-comment';
@@ -20,26 +23,72 @@ export class WorkspaceCommentPaster
copyData: WorkspaceCommentCopyData,
workspace: WorkspaceSvg,
coordinate?: Coordinate,
): WorkspaceCommentSvg {
): RenderedWorkspaceComment | null {
const state = copyData.commentState;
if (coordinate) {
state.setAttribute('x', `${coordinate.x}`);
state.setAttribute('y', `${coordinate.y}`);
} else {
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
state.setAttribute('x', `${x}`);
state.setAttribute('y', `${y}`);
state['x'] = coordinate.x;
state['y'] = coordinate.y;
}
return WorkspaceCommentSvg.fromXmlRendered(
copyData.commentState,
workspace,
);
eventUtils.disable();
let comment;
try {
comment = commentSerialiation.append(
state,
workspace,
) as RenderedWorkspaceComment;
moveCommentToNotConflict(comment);
} finally {
eventUtils.enable();
}
if (!comment) return null;
if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment));
}
common.setSelected(comment);
return comment;
}
}
function moveCommentToNotConflict(comment: RenderedWorkspaceComment) {
const workspace = comment.workspace;
const translateDistance = 30;
const coord = comment.getRelativeToSurfaceXY();
const offset = new Coordinate(0, 0);
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
const otherCoords = workspace
.getTopComments(false)
.filter((otherComment) => otherComment.id !== comment.id)
.map((c) => c.getRelativeToSurfaceXY());
while (
commentOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords)
) {
offset.translate(
workspace.RTL ? -translateDistance : translateDistance,
translateDistance,
);
}
comment.moveTo(Coordinate.sum(coord, offset));
}
function commentOverlapsOtherExactly(
coord: Coordinate,
otherCoords: Coordinate[],
): boolean {
return otherCoords.some(
(otherCoord) =>
Math.abs(otherCoord.x - coord.x) <= 1 &&
Math.abs(otherCoord.y - coord.y) <= 1,
);
}
export interface WorkspaceCommentCopyData extends ICopyData {
commentState: Element;
commentState: commentSerialiation.State;
}
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());

View File

@@ -19,6 +19,12 @@ import * as browserEvents from '../browser_events.js';
import * as common from '../common.js';
import {ISelectable} from '../interfaces/i_selectable.js';
import {IDeletable} from '../interfaces/i_deletable.js';
import {ICopyable} from '../interfaces/i_copyable.js';
import * as commentSerialization from '../serialization/workspace_comments.js';
import {
WorkspaceCommentPaster,
WorkspaceCommentCopyData,
} from '../clipboard/workspace_comment_paster.js';
export class RenderedWorkspaceComment
extends WorkspaceComment
@@ -27,7 +33,8 @@ export class RenderedWorkspaceComment
IRenderedElement,
IDraggable,
ISelectable,
IDeletable
IDeletable,
ICopyable<WorkspaceCommentCopyData>
{
/** The class encompassing the svg elements making up the workspace comment. */
private view: CommentView;
@@ -219,4 +226,17 @@ export class RenderedWorkspaceComment
unselect(): void {
dom.removeClass(this.getSvgRoot(), 'blocklySelected');
}
/**
* Returns a JSON serializable representation of this comment's state that
* can be used for pasting.
*/
toCopyData(): WorkspaceCommentCopyData | null {
return {
paster: WorkspaceCommentPaster.TYPE,
commentState: commentSerialization.save(this, {
addCoordinates: true,
}),
};
}
}

View File

@@ -6,10 +6,8 @@
import {ISerializer} from '../interfaces/i_serializer.js';
import {Workspace} from '../workspace.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as priorities from './priorities.js';
import {WorkspaceComment} from '../comments/workspace_comment.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
import type {WorkspaceComment} from '../comments/workspace_comment.js';
import * as eventUtils from '../events/utils.js';
import {Coordinate} from '../utils/coordinate.js';
import * as serializationRegistry from './registry.js';
@@ -70,10 +68,7 @@ export function append(
const prevRecordUndo = eventUtils.getRecordUndo();
eventUtils.setRecordUndo(recordUndo);
const comment =
workspace instanceof WorkspaceSvg
? new RenderedWorkspaceComment(workspace, state.id)
: new WorkspaceComment(workspace, state.id);
const comment = workspace.newComment(state.id);
if (state.text !== undefined) comment.setText(state.text);
if (state.x !== undefined || state.y !== undefined) {

View File

@@ -30,7 +30,8 @@ import * as math from './utils/math.js';
import type * as toolbox from './utils/toolbox.js';
import {VariableMap} from './variable_map.js';
import type {VariableModel} from './variable_model.js';
import type {WorkspaceComment} from './workspace_comment.js';
import type {WorkspaceComment as OldWorkspaceComment} from './workspace_comment.js';
import {WorkspaceComment} from './comments/workspace_comment.js';
import {IProcedureMap} from './interfaces/i_procedure_map.js';
import {ObservableProcedureMap} from './observable_procedure_map.js';
@@ -100,8 +101,8 @@ export class Workspace implements IASTNodeLocation {
connectionChecker: IConnectionChecker;
private readonly topBlocks: Block[] = [];
private readonly topComments: WorkspaceComment[] = [];
private readonly commentDB = new Map<string, WorkspaceComment>();
private readonly topComments: OldWorkspaceComment[] = [];
private readonly commentDB = new Map<string, OldWorkspaceComment>();
private readonly listeners: Function[] = [];
protected undoStack_: Abstract[] = [];
protected redoStack_: Abstract[] = [];
@@ -168,8 +169,8 @@ export class Workspace implements IASTNodeLocation {
* a's index.
*/
private sortObjects_(
a: Block | WorkspaceComment,
b: Block | WorkspaceComment,
a: Block | OldWorkspaceComment,
b: Block | OldWorkspaceComment,
): number {
const offset =
Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1);
@@ -266,7 +267,7 @@ export class Workspace implements IASTNodeLocation {
* @param comment comment to add.
* @internal
*/
addTopComment(comment: WorkspaceComment) {
addTopComment(comment: OldWorkspaceComment) {
this.topComments.push(comment);
// Note: If the comment database starts to hold block comments, this may
@@ -287,7 +288,7 @@ export class Workspace implements IASTNodeLocation {
* @param comment comment to remove.
* @internal
*/
removeTopComment(comment: WorkspaceComment) {
removeTopComment(comment: OldWorkspaceComment) {
if (!arrayUtils.removeElem(this.topComments, comment)) {
throw Error(
"Comment not present in workspace's list of top-most " + 'comments.',
@@ -306,9 +307,9 @@ export class Workspace implements IASTNodeLocation {
* @returns The top-level comment objects.
* @internal
*/
getTopComments(ordered = false): WorkspaceComment[] {
getTopComments(ordered = false): OldWorkspaceComment[] {
// Copy the topComments list.
const comments = new Array<WorkspaceComment>().concat(this.topComments);
const comments = new Array<OldWorkspaceComment>().concat(this.topComments);
if (ordered && comments.length > 1) {
comments.sort(this.sortObjects_.bind(this));
}
@@ -515,6 +516,20 @@ export class Workspace implements IASTNodeLocation {
'monkey-patched in by blockly.ts',
);
}
/**
* Obtain a newly created comment.
*
* @param id Optional ID. Use this ID if provided, otherwise create a new
* ID.
* @returns The created comment.
*/
newComment(id?: string): WorkspaceComment {
throw new Error(
'The implementation of newComment should be ' +
'monkey-patched in by blockly.ts',
);
}
/* eslint-enable */
/**
@@ -736,7 +751,7 @@ export class Workspace implements IASTNodeLocation {
* @returns The sought after comment, or null if not found.
* @internal
*/
getCommentById(id: string): WorkspaceComment | null {
getCommentById(id: string): OldWorkspaceComment | null {
return this.commentDB.get(id) ?? null;
}

View File

@@ -68,8 +68,9 @@ import * as VariablesDynamic from './variables_dynamic.js';
import * as WidgetDiv from './widgetdiv.js';
import {Workspace} from './workspace.js';
import {WorkspaceAudio} from './workspace_audio.js';
import {WorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceComment as OldWorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg as OldWorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceComment} from './comments/workspace_comment.js';
import {ZoomControls} from './zoom_controls.js';
import {ContextMenuOption} from './contextmenu_registry.js';
import * as renderManagement from './render_management.js';
@@ -1395,6 +1396,20 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
'monkey-patched in by blockly.ts',
);
}
/**
* Obtain a newly created comment.
*
* @param id Optional ID. Use this ID if provided, otherwise create a new
* ID.
* @returns The created comment.
*/
newComment(id?: string): WorkspaceComment {
throw new Error(
'The implementation of newComment should be ' +
'monkey-patched in by blockly.ts',
);
}
/* eslint-enable */
/**
@@ -2128,8 +2143,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @param comment comment to add.
*/
override addTopComment(comment: WorkspaceComment) {
this.addTopBoundedElement(comment as WorkspaceCommentSvg);
override addTopComment(comment: OldWorkspaceComment) {
this.addTopBoundedElement(comment as OldWorkspaceCommentSvg);
super.addTopComment(comment);
}
@@ -2138,8 +2153,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*
* @param comment comment to remove.
*/
override removeTopComment(comment: WorkspaceComment) {
this.removeTopBoundedElement(comment as WorkspaceCommentSvg);
override removeTopComment(comment: OldWorkspaceComment) {
this.removeTopBoundedElement(comment as OldWorkspaceCommentSvg);
super.removeTopComment(comment);
}