mirror of
https://github.com/google/blockly.git
synced 2026-01-05 08:00:09 +01:00
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:
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user