mirror of
https://github.com/google/blockly.git
synced 2026-01-08 09:30:06 +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);
|
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 {
|
WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan {
|
||||||
return new Trashcan(workspace);
|
return new Trashcan(workspace);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import {IPaster} from '../interfaces/i_paster.js';
|
|||||||
import {ICopyData} from '../interfaces/i_copyable.js';
|
import {ICopyData} from '../interfaces/i_copyable.js';
|
||||||
import {Coordinate} from '../utils/coordinate.js';
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||||
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
|
|
||||||
import * as registry from './registry.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
|
export class WorkspaceCommentPaster
|
||||||
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
|
implements IPaster<WorkspaceCommentCopyData, RenderedWorkspaceComment>
|
||||||
{
|
{
|
||||||
static TYPE = 'workspace-comment';
|
static TYPE = 'workspace-comment';
|
||||||
|
|
||||||
@@ -20,26 +23,72 @@ export class WorkspaceCommentPaster
|
|||||||
copyData: WorkspaceCommentCopyData,
|
copyData: WorkspaceCommentCopyData,
|
||||||
workspace: WorkspaceSvg,
|
workspace: WorkspaceSvg,
|
||||||
coordinate?: Coordinate,
|
coordinate?: Coordinate,
|
||||||
): WorkspaceCommentSvg {
|
): RenderedWorkspaceComment | null {
|
||||||
const state = copyData.commentState;
|
const state = copyData.commentState;
|
||||||
|
|
||||||
if (coordinate) {
|
if (coordinate) {
|
||||||
state.setAttribute('x', `${coordinate.x}`);
|
state['x'] = coordinate.x;
|
||||||
state.setAttribute('y', `${coordinate.y}`);
|
state['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}`);
|
|
||||||
}
|
}
|
||||||
return WorkspaceCommentSvg.fromXmlRendered(
|
|
||||||
copyData.commentState,
|
eventUtils.disable();
|
||||||
workspace,
|
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 {
|
export interface WorkspaceCommentCopyData extends ICopyData {
|
||||||
commentState: Element;
|
commentState: commentSerialiation.State;
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());
|
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ import * as browserEvents from '../browser_events.js';
|
|||||||
import * as common from '../common.js';
|
import * as common from '../common.js';
|
||||||
import {ISelectable} from '../interfaces/i_selectable.js';
|
import {ISelectable} from '../interfaces/i_selectable.js';
|
||||||
import {IDeletable} from '../interfaces/i_deletable.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
|
export class RenderedWorkspaceComment
|
||||||
extends WorkspaceComment
|
extends WorkspaceComment
|
||||||
@@ -27,7 +33,8 @@ export class RenderedWorkspaceComment
|
|||||||
IRenderedElement,
|
IRenderedElement,
|
||||||
IDraggable,
|
IDraggable,
|
||||||
ISelectable,
|
ISelectable,
|
||||||
IDeletable
|
IDeletable,
|
||||||
|
ICopyable<WorkspaceCommentCopyData>
|
||||||
{
|
{
|
||||||
/** The class encompassing the svg elements making up the workspace comment. */
|
/** The class encompassing the svg elements making up the workspace comment. */
|
||||||
private view: CommentView;
|
private view: CommentView;
|
||||||
@@ -219,4 +226,17 @@ export class RenderedWorkspaceComment
|
|||||||
unselect(): void {
|
unselect(): void {
|
||||||
dom.removeClass(this.getSvgRoot(), 'blocklySelected');
|
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 {ISerializer} from '../interfaces/i_serializer.js';
|
||||||
import {Workspace} from '../workspace.js';
|
import {Workspace} from '../workspace.js';
|
||||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
|
||||||
import * as priorities from './priorities.js';
|
import * as priorities from './priorities.js';
|
||||||
import {WorkspaceComment} from '../comments/workspace_comment.js';
|
import type {WorkspaceComment} from '../comments/workspace_comment.js';
|
||||||
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
|
|
||||||
import * as eventUtils from '../events/utils.js';
|
import * as eventUtils from '../events/utils.js';
|
||||||
import {Coordinate} from '../utils/coordinate.js';
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
import * as serializationRegistry from './registry.js';
|
import * as serializationRegistry from './registry.js';
|
||||||
@@ -70,10 +68,7 @@ export function append(
|
|||||||
const prevRecordUndo = eventUtils.getRecordUndo();
|
const prevRecordUndo = eventUtils.getRecordUndo();
|
||||||
eventUtils.setRecordUndo(recordUndo);
|
eventUtils.setRecordUndo(recordUndo);
|
||||||
|
|
||||||
const comment =
|
const comment = workspace.newComment(state.id);
|
||||||
workspace instanceof WorkspaceSvg
|
|
||||||
? new RenderedWorkspaceComment(workspace, state.id)
|
|
||||||
: new WorkspaceComment(workspace, state.id);
|
|
||||||
|
|
||||||
if (state.text !== undefined) comment.setText(state.text);
|
if (state.text !== undefined) comment.setText(state.text);
|
||||||
if (state.x !== undefined || state.y !== undefined) {
|
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 type * as toolbox from './utils/toolbox.js';
|
||||||
import {VariableMap} from './variable_map.js';
|
import {VariableMap} from './variable_map.js';
|
||||||
import type {VariableModel} from './variable_model.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 {IProcedureMap} from './interfaces/i_procedure_map.js';
|
||||||
import {ObservableProcedureMap} from './observable_procedure_map.js';
|
import {ObservableProcedureMap} from './observable_procedure_map.js';
|
||||||
|
|
||||||
@@ -100,8 +101,8 @@ export class Workspace implements IASTNodeLocation {
|
|||||||
connectionChecker: IConnectionChecker;
|
connectionChecker: IConnectionChecker;
|
||||||
|
|
||||||
private readonly topBlocks: Block[] = [];
|
private readonly topBlocks: Block[] = [];
|
||||||
private readonly topComments: WorkspaceComment[] = [];
|
private readonly topComments: OldWorkspaceComment[] = [];
|
||||||
private readonly commentDB = new Map<string, WorkspaceComment>();
|
private readonly commentDB = new Map<string, OldWorkspaceComment>();
|
||||||
private readonly listeners: Function[] = [];
|
private readonly listeners: Function[] = [];
|
||||||
protected undoStack_: Abstract[] = [];
|
protected undoStack_: Abstract[] = [];
|
||||||
protected redoStack_: Abstract[] = [];
|
protected redoStack_: Abstract[] = [];
|
||||||
@@ -168,8 +169,8 @@ export class Workspace implements IASTNodeLocation {
|
|||||||
* a's index.
|
* a's index.
|
||||||
*/
|
*/
|
||||||
private sortObjects_(
|
private sortObjects_(
|
||||||
a: Block | WorkspaceComment,
|
a: Block | OldWorkspaceComment,
|
||||||
b: Block | WorkspaceComment,
|
b: Block | OldWorkspaceComment,
|
||||||
): number {
|
): number {
|
||||||
const offset =
|
const offset =
|
||||||
Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1);
|
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.
|
* @param comment comment to add.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
addTopComment(comment: WorkspaceComment) {
|
addTopComment(comment: OldWorkspaceComment) {
|
||||||
this.topComments.push(comment);
|
this.topComments.push(comment);
|
||||||
|
|
||||||
// Note: If the comment database starts to hold block comments, this may
|
// 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.
|
* @param comment comment to remove.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
removeTopComment(comment: WorkspaceComment) {
|
removeTopComment(comment: OldWorkspaceComment) {
|
||||||
if (!arrayUtils.removeElem(this.topComments, comment)) {
|
if (!arrayUtils.removeElem(this.topComments, comment)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
"Comment not present in workspace's list of top-most " + 'comments.',
|
"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.
|
* @returns The top-level comment objects.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
getTopComments(ordered = false): WorkspaceComment[] {
|
getTopComments(ordered = false): OldWorkspaceComment[] {
|
||||||
// Copy the topComments list.
|
// 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) {
|
if (ordered && comments.length > 1) {
|
||||||
comments.sort(this.sortObjects_.bind(this));
|
comments.sort(this.sortObjects_.bind(this));
|
||||||
}
|
}
|
||||||
@@ -515,6 +516,20 @@ export class Workspace implements IASTNodeLocation {
|
|||||||
'monkey-patched in by blockly.ts',
|
'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 */
|
/* eslint-enable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -736,7 +751,7 @@ export class Workspace implements IASTNodeLocation {
|
|||||||
* @returns The sought after comment, or null if not found.
|
* @returns The sought after comment, or null if not found.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
getCommentById(id: string): WorkspaceComment | null {
|
getCommentById(id: string): OldWorkspaceComment | null {
|
||||||
return this.commentDB.get(id) ?? 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 * as WidgetDiv from './widgetdiv.js';
|
||||||
import {Workspace} from './workspace.js';
|
import {Workspace} from './workspace.js';
|
||||||
import {WorkspaceAudio} from './workspace_audio.js';
|
import {WorkspaceAudio} from './workspace_audio.js';
|
||||||
import {WorkspaceComment} from './workspace_comment.js';
|
import {WorkspaceComment as OldWorkspaceComment} from './workspace_comment.js';
|
||||||
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
import {WorkspaceCommentSvg as OldWorkspaceCommentSvg} from './workspace_comment_svg.js';
|
||||||
|
import {WorkspaceComment} from './comments/workspace_comment.js';
|
||||||
import {ZoomControls} from './zoom_controls.js';
|
import {ZoomControls} from './zoom_controls.js';
|
||||||
import {ContextMenuOption} from './contextmenu_registry.js';
|
import {ContextMenuOption} from './contextmenu_registry.js';
|
||||||
import * as renderManagement from './render_management.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',
|
'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 */
|
/* eslint-enable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2128,8 +2143,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
|||||||
*
|
*
|
||||||
* @param comment comment to add.
|
* @param comment comment to add.
|
||||||
*/
|
*/
|
||||||
override addTopComment(comment: WorkspaceComment) {
|
override addTopComment(comment: OldWorkspaceComment) {
|
||||||
this.addTopBoundedElement(comment as WorkspaceCommentSvg);
|
this.addTopBoundedElement(comment as OldWorkspaceCommentSvg);
|
||||||
super.addTopComment(comment);
|
super.addTopComment(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2138,8 +2153,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
|||||||
*
|
*
|
||||||
* @param comment comment to remove.
|
* @param comment comment to remove.
|
||||||
*/
|
*/
|
||||||
override removeTopComment(comment: WorkspaceComment) {
|
override removeTopComment(comment: OldWorkspaceComment) {
|
||||||
this.removeTopBoundedElement(comment as WorkspaceCommentSvg);
|
this.removeTopBoundedElement(comment as OldWorkspaceCommentSvg);
|
||||||
super.removeTopComment(comment);
|
super.removeTopComment(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user