diff --git a/core/blockly.ts b/core/blockly.ts index 879672de4a2..77a0ef1b94d 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -379,6 +379,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); }; diff --git a/core/clipboard/workspace_comment_paster.ts b/core/clipboard/workspace_comment_paster.ts index aeedbfb2b77..c7e5eed68b0 100644 --- a/core/clipboard/workspace_comment_paster.ts +++ b/core/clipboard/workspace_comment_paster.ts @@ -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 + implements IPaster { 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; + } + + eventUtils.disable(); + let comment; + try { + comment = commentSerialiation.append( + state, + workspace, + ) as RenderedWorkspaceComment; + moveCommentToNotConflict(comment); + } finally { + eventUtils.enable(); } - return WorkspaceCommentSvg.fromXmlRendered( - copyData.commentState, - workspace, + + 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()); diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 5a143666287..9d9ccc57288 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -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 { /** 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, + }), + }; + } } diff --git a/core/serialization/workspace_comments.ts b/core/serialization/workspace_comments.ts index a2891395c34..525274e58e4 100644 --- a/core/serialization/workspace_comments.ts +++ b/core/serialization/workspace_comments.ts @@ -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) { diff --git a/core/workspace.ts b/core/workspace.ts index dbbcb7743f1..424d9efb7e4 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -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(); + private readonly topComments: OldWorkspaceComment[] = []; + private readonly commentDB = new Map(); 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().concat(this.topComments); + const comments = new Array().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; } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 126b87c6e6d..611a6a7179c 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -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); }