fix: Drag and Resize events for workspace comments (#8217)

* feat: Added a comment_drag event.

* Add workspace comment resize events.

* Addressing PR feedback.

* Fixed chai imports in new test files.

* Addressing more PR feedback.
This commit is contained in:
John Nesky
2024-06-26 12:16:56 -07:00
committed by GitHub
parent be268e3bb4
commit 9a0619aa2a
14 changed files with 469 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ import type {BlockCreate} from './events/events_block_create.js';
import type {BlockMove} from './events/events_block_move.js';
import type {CommentCreate} from './events/events_comment_create.js';
import type {CommentMove} from './events/events_comment_move.js';
import type {CommentResize} from './events/events_comment_resize.js';
import type {ViewportChange} from './events/events_viewport.js';
import * as eventUtils from './events/utils.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
@@ -163,8 +164,9 @@ function extractObjectFromEvent(
break;
case eventUtils.COMMENT_CREATE:
case eventUtils.COMMENT_MOVE:
case eventUtils.COMMENT_RESIZE:
object = workspace.getCommentById(
(e as CommentCreate | CommentMove).commentId!,
(e as CommentCreate | CommentMove | CommentResize).commentId!,
) as RenderedWorkspaceComment;
break;
}

View File

@@ -125,7 +125,7 @@ export class CommentView implements IRenderedElement {
workspace.getLayerManager()?.append(this, layers.BLOCK);
// Set size to the default size.
this.setSize(this.size);
this.setSizeWithoutFiringEvents(this.size);
// Set default transform (including inverted scale for RTL).
this.moveTo(new Coordinate(0, 0));
@@ -298,7 +298,7 @@ export class CommentView implements IRenderedElement {
* Sets the size of the comment in workspace units, and updates the view
* elements to reflect the new size.
*/
setSize(size: Size) {
setSizeWithoutFiringEvents(size: Size) {
const topBarSize = this.topBarBackground.getBBox();
const deleteSize = this.deleteIcon.getBBox();
const foldoutSize = this.foldoutIcon.getBBox();
@@ -309,7 +309,6 @@ export class CommentView implements IRenderedElement {
size,
this.calcMinSize(topBarSize, foldoutSize, deleteSize),
);
const oldSize = this.size;
this.size = size;
this.svgRoot.setAttribute('height', `${size.height}`);
@@ -328,7 +327,15 @@ export class CommentView implements IRenderedElement {
resizeSize,
);
this.updateResizeHandlePosition(size, resizeSize);
}
/**
* Sets the size of the comment in workspace units, updates the view
* elements to reflect the new size, and triggers size change listeners.
*/
setSize(size: Size) {
const oldSize = this.size;
this.setSizeWithoutFiringEvents(size);
this.onSizeChange(oldSize, this.size);
}
@@ -472,7 +479,7 @@ export class CommentView implements IRenderedElement {
/**
* Triggers listeners when the size of the comment changes, either
* progrmatically or manually by the user.
* programmatically or manually by the user.
*/
private onSizeChange(oldSize: Size, newSize: Size) {
// Loop through listeners backwards in case they remove themselves.
@@ -550,13 +557,17 @@ export class CommentView implements IRenderedElement {
browserEvents.unbind(this.resizePointerMoveListener);
this.resizePointerMoveListener = null;
}
// When ending a resize drag, notify size change listeners to fire an event.
this.setSize(this.size);
}
/** Resizes the comment in response to a drag on the resize handle. */
private onResizePointerMove(e: PointerEvent) {
// TODO(#7926): Move this into a utils file.
const delta = this.workspace.moveDrag(e);
this.setSize(new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y));
const size = this.workspace.moveDrag(e);
this.setSizeWithoutFiringEvents(
new Size(this.workspace.RTL ? -size.x : size.x, size.y),
);
}
/** Returns true if the comment is currently collapsed. */
@@ -573,7 +584,7 @@ export class CommentView implements IRenderedElement {
dom.removeClass(this.svgRoot, 'blocklyCollapsed');
}
// Repositions resize handle and such.
this.setSize(this.size);
this.setSizeWithoutFiringEvents(this.size);
this.onCollapse();
}
@@ -682,7 +693,7 @@ export class CommentView implements IRenderedElement {
/**
* Triggers listeners when the text of the comment changes, either
* progrmatically or manually by the user.
* programmatically or manually by the user.
*/
private onTextChange() {
const oldText = this.text;

View File

@@ -10,6 +10,7 @@ import {Coordinate} from '../utils/coordinate.js';
import * as idGenerator from '../utils/idgenerator.js';
import * as eventUtils from '../events/utils.js';
import {CommentMove} from '../events/events_comment_move.js';
import {CommentResize} from '../events/events_comment_resize.js';
export class WorkspaceComment {
/** The unique identifier for this comment. */
@@ -104,7 +105,14 @@ export class WorkspaceComment {
/** Sets the comment's size in workspace units. */
setSize(size: Size) {
const event = new (eventUtils.get(eventUtils.COMMENT_RESIZE))(
this,
) as CommentResize;
this.size = size;
event.recordCurrentSizeAsNewSize();
eventUtils.fire(event);
}
/** Returns the comment's size in workspace units. */
@@ -196,7 +204,7 @@ export class WorkspaceComment {
this.location = location;
event.recordNew();
if (eventUtils.isEnabled()) eventUtils.fire(event);
eventUtils.fire(event);
}
/** Returns the position of the comment in workspace coordinates. */

View File

@@ -29,6 +29,7 @@ export class CommentDragStrategy implements IDragStrategy {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
this.fireDragStartEvent();
this.startLoc = this.comment.getRelativeToSurfaceXY();
this.workspace.setResizesEnabled(false);
this.workspace.getLayerManager()?.moveToDragLayer(this.comment);
@@ -40,6 +41,7 @@ export class CommentDragStrategy implements IDragStrategy {
}
endDrag(): void {
this.fireDragEndEvent();
this.fireMoveEvent();
this.workspace
@@ -53,6 +55,25 @@ export class CommentDragStrategy implements IDragStrategy {
eventUtils.setGroup(false);
}
/** Fire a UI event at the start of a comment drag. */
private fireDragStartEvent() {
const event = new (eventUtils.get(eventUtils.COMMENT_DRAG))(
this.comment,
true,
);
eventUtils.fire(event);
}
/** Fire a UI event at the end of a comment drag. */
private fireDragEndEvent() {
const event = new (eventUtils.get(eventUtils.COMMENT_DRAG))(
this.comment,
false,
);
eventUtils.fire(event);
}
/** Fire a move event at the end of a comment drag. */
private fireMoveEvent() {
if (this.comment.isDeadOrDying()) return;
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(

View File

@@ -24,6 +24,8 @@ import {CommentChange, CommentChangeJson} from './events_comment_change.js';
import {CommentCreate, CommentCreateJson} from './events_comment_create.js';
import {CommentDelete} from './events_comment_delete.js';
import {CommentMove, CommentMoveJson} from './events_comment_move.js';
import {CommentResize, CommentResizeJson} from './events_comment_resize.js';
import {CommentDrag, CommentDragJson} from './events_comment_drag.js';
import {
CommentCollapse,
CommentCollapseJson,
@@ -77,6 +79,10 @@ export {CommentCreateJson};
export {CommentDelete};
export {CommentMove};
export {CommentMoveJson};
export {CommentResize};
export {CommentResizeJson};
export {CommentDrag};
export {CommentDragJson};
export {CommentCollapse};
export {CommentCollapseJson};
export {FinishedLoading};
@@ -119,6 +125,8 @@ export const COMMENT_CHANGE = eventUtils.COMMENT_CHANGE;
export const COMMENT_CREATE = eventUtils.COMMENT_CREATE;
export const COMMENT_DELETE = eventUtils.COMMENT_DELETE;
export const COMMENT_MOVE = eventUtils.COMMENT_MOVE;
export const COMMENT_RESIZE = eventUtils.COMMENT_RESIZE;
export const COMMENT_DRAG = eventUtils.COMMENT_DRAG;
export const CREATE = eventUtils.CREATE;
export const DELETE = eventUtils.DELETE;
export const FINISHED_LOADING = eventUtils.FINISHED_LOADING;

View File

@@ -0,0 +1,99 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Events fired when a workspace comment is dragged.
*/
import type {WorkspaceComment} from '../comments/workspace_comment.js';
import * as registry from '../registry.js';
import {AbstractEventJson} from './events_abstract.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
import {Workspace} from '../workspace.js';
/**
* Notifies listeners when a comment is being manually dragged/dropped.
*/
export class CommentDrag extends UiBase {
/** The ID of the top-level comment being dragged. */
commentId?: string;
/** True if this is the start of a drag, false if this is the end of one. */
isStart?: boolean;
override type = eventUtils.COMMENT_DRAG;
/**
* @param opt_comment The comment that is being dragged.
* Undefined for a blank event.
* @param opt_isStart Whether this is the start of a comment drag.
* Undefined for a blank event.
*/
constructor(opt_comment?: WorkspaceComment, opt_isStart?: boolean) {
const workspaceId = opt_comment ? opt_comment.workspace.id : undefined;
super(workspaceId);
if (!opt_comment) return;
this.commentId = opt_comment.id;
this.isStart = opt_isStart;
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
override toJson(): CommentDragJson {
const json = super.toJson() as CommentDragJson;
if (this.isStart === undefined) {
throw new Error(
'Whether this event is the start of a drag is undefined. ' +
'Either pass the value to the constructor, or call fromJson',
);
}
if (this.commentId === undefined) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson',
);
}
json['isStart'] = this.isStart;
json['commentId'] = this.commentId;
return json;
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentDrag, but we can't specify that due to the fact that parameters
* to static methods in subclasses must be supertypes of parameters to
* static methods in superclasses.
* @internal
*/
static fromJson(
json: CommentDragJson,
workspace: Workspace,
event?: any,
): CommentDrag {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentDrag(),
) as CommentDrag;
newEvent.isStart = json['isStart'];
newEvent.commentId = json['commentId'];
return newEvent;
}
}
export interface CommentDragJson extends AbstractEventJson {
isStart: boolean;
commentId: string;
}
registry.register(registry.Type.EVENT, eventUtils.COMMENT_DRAG, CommentDrag);

View File

@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Class for comment resize event.
*/
import * as registry from '../registry.js';
import {Size} from '../utils/size.js';
import type {WorkspaceComment} from '../comments/workspace_comment.js';
import {CommentBase, CommentBaseJson} from './events_comment_base.js';
import * as eventUtils from './utils.js';
import type {Workspace} from '../workspace.js';
/**
* Notifies listeners that a workspace comment has resized.
*/
export class CommentResize extends CommentBase {
override type = eventUtils.COMMENT_RESIZE;
/** The size of the comment before the resize. */
oldSize?: Size;
/** The size of the comment after the resize. */
newSize?: Size;
/**
* @param opt_comment The comment that is being resized. Undefined for a blank
* event.
*/
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
}
this.oldSize = opt_comment.getSize();
}
/**
* Record the comment's new size. Called after the resize. Can only be
* called once.
*/
recordCurrentSizeAsNewSize() {
if (this.newSize) {
throw Error(
'Tried to record the new size of a comment on the ' +
'same event twice.',
);
}
const workspace = this.getEventWorkspace_();
if (!this.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson',
);
}
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
throw new Error(
'The comment associated with the comment resize event ' +
'could not be found',
);
}
this.newSize = comment.getSize();
}
/**
* Encode the event as JSON.
*
* @returns JSON representation.
*/
override toJson(): CommentResizeJson {
const json = super.toJson() as CommentResizeJson;
if (!this.oldSize) {
throw new Error(
'The old comment size is undefined. Either pass a comment to ' +
'the constructor, or call fromJson',
);
}
if (!this.newSize) {
throw new Error(
'The new comment size is undefined. Either call ' +
'recordCurrentSizeAsNewSize, or call fromJson',
);
}
json['oldWidth'] = Math.round(this.oldSize.width);
json['oldHeight'] = Math.round(this.oldSize.height);
json['newWidth'] = Math.round(this.newSize.width);
json['newHeight'] = Math.round(this.newSize.height);
return json;
}
/**
* Deserializes the JSON event.
*
* @param event The event to append new properties to. Should be a subclass
* of CommentResize, but we can't specify that due to the fact that
* parameters to static methods in subclasses must be supertypes of
* parameters to static methods in superclasses.
* @internal
*/
static fromJson(
json: CommentResizeJson,
workspace: Workspace,
event?: any,
): CommentResize {
const newEvent = super.fromJson(
json,
workspace,
event ?? new CommentResize(),
) as CommentResize;
newEvent.oldSize = new Size(json['oldWidth'], json['oldHeight']);
newEvent.newSize = new Size(json['newWidth'], json['newHeight']);
return newEvent;
}
/**
* Does this event record any change of state?
*
* @returns False if something changed.
*/
override isNull(): boolean {
return Size.equals(this.oldSize, this.newSize);
}
/**
* Run a resize event.
*
* @param forward True if run forward, false if run backward (undo).
*/
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (!this.commentId) {
throw new Error(
'The comment ID is undefined. Either pass a comment to ' +
'the constructor, or call fromJson',
);
}
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn("Can't resize non-existent comment: " + this.commentId);
return;
}
const size = forward ? this.newSize : this.oldSize;
if (!size) {
throw new Error(
'Either oldSize or newSize is undefined. ' +
'Either pass a comment to the constructor and call ' +
'recordCurrentSizeAsNewSize, or call fromJson',
);
}
comment.setSize(size);
}
}
export interface CommentResizeJson extends CommentBaseJson {
oldWidth: number;
oldHeight: number;
newWidth: number;
newHeight: number;
}
registry.register(
registry.Type.EVENT,
eventUtils.COMMENT_RESIZE,
CommentResize,
);

View File

@@ -19,6 +19,7 @@ import type {BlockCreate} from './events_block_create.js';
import type {BlockMove} from './events_block_move.js';
import type {CommentCreate} from './events_comment_create.js';
import type {CommentMove} from './events_comment_move.js';
import type {CommentResize} from './events_comment_resize.js';
import type {ViewportChange} from './events_viewport.js';
/** Group ID for new events. Grouped events are indivisible. */
@@ -116,7 +117,7 @@ export const VAR_RENAME = 'var_rename';
export const UI = 'ui';
/**
* Name of event that record a block drags a block.
* Name of event that drags a block.
*/
export const BLOCK_DRAG = 'drag';
@@ -180,7 +181,13 @@ export const COMMENT_CHANGE = 'comment_change';
*/
export const COMMENT_MOVE = 'comment_move';
/** Type of event that moves a comment. */
/** Name of event that resizes a comment. */
export const COMMENT_RESIZE = 'comment_resize';
/** Name of event that drags a comment. */
export const COMMENT_DRAG = 'comment_drag';
/** Type of event that collapses a comment. */
export const COMMENT_COLLAPSE = 'comment_collapse';
/**
@@ -201,7 +208,12 @@ const ORPHANED_BLOCK_DISABLED_REASON = 'ORPHANED_BLOCK';
* Not to be confused with bumping so that disconnected connections do not
* appear connected.
*/
export type BumpEvent = BlockCreate | BlockMove | CommentCreate | CommentMove;
export type BumpEvent =
| BlockCreate
| BlockMove
| CommentCreate
| CommentMove
| CommentResize;
/**
* List of events that cause objects to be bumped back into the visible

View File

@@ -34,7 +34,7 @@ export class Size {
* @returns True iff the sizes have equal widths and equal heights, or if both
* are null.
*/
static equals(a: Size | null, b: Size | null): boolean {
static equals(a?: Size | null, b?: Size | null): boolean {
if (a === b) {
return true;
}

View File

@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '../../node_modules/chai/chai.js';
import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
suite('Comment Drag Event', function () {
setup(function () {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
});
teardown(function () {
sharedTestTeardown.call(this);
});
suite('Serialization', function () {
test('events round-trip through JSON', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setText('test text');
const origEvent = new Blockly.Events.CommentDrag(comment, true);
const json = origEvent.toJson();
const newEvent = new Blockly.Events.fromJson(json, this.workspace);
assert.deepEqual(newEvent, origEvent);
});
});
});

View File

@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '../../node_modules/chai/chai.js';
import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
suite('Comment Resize Event', function () {
setup(function () {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
});
teardown(function () {
sharedTestTeardown.call(this);
});
suite('Serialization', function () {
test('events round-trip through JSON', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setText('test text');
comment.setSize(new Blockly.utils.Size(100, 100));
const origEvent = new Blockly.Events.CommentResize(comment);
comment.setSize(new Blockly.utils.Size(200, 200));
origEvent.recordCurrentSizeAsNewSize();
const json = origEvent.toJson();
const newEvent = new Blockly.Events.fromJson(json, this.workspace);
assert.deepEqual(newEvent, origEvent);
});
});
});

View File

@@ -862,7 +862,30 @@ suite('Events', function () {
},
}),
},
{
title: 'Comment drag start',
class: Blockly.Events.CommentDrag,
getArgs: (thisObj) => [thisObj.comment, true],
getExpectedJson: (thisObj) => ({
type: 'comment_drag',
group: '',
isStart: true,
commentId: thisObj.comment.id,
}),
},
{
title: 'Comment drag end',
class: Blockly.Events.CommentDrag,
getArgs: (thisObj) => [thisObj.comment, false],
getExpectedJson: (thisObj) => ({
type: 'comment_drag',
group: '',
isStart: false,
commentId: thisObj.comment.id,
}),
},
// TODO(#4577) Test serialization of move event coordinate properties.
// TODO(#4577) Test serialization of comment resize event properties.
];
const testSuites = [
{

View File

@@ -66,6 +66,8 @@
import './event_comment_create_test.js';
import './event_comment_delete_test.js';
import './event_comment_move_test.js';
import './event_comment_drag_test.js';
import './event_comment_resize_test.js';
import './event_marker_move_test.js';
import './event_selected_test.js';
import './event_theme_change_test.js';

View File

@@ -81,6 +81,28 @@ suite('Workspace comment', function () {
);
});
test('resize events are fired when a comment is resized', function () {
this.renderedComment = new Blockly.comments.RenderedWorkspaceComment(
this.workspace,
);
const spy = createChangeListenerSpy(this.workspace);
this.renderedComment.setSize(new Blockly.utils.Size(300, 200));
this.clock.runAll();
assertEventFired(
spy,
Blockly.Events.CommentResize,
{
commentId: this.renderedComment.id,
oldSize: {width: 120, height: 100},
newSize: {width: 300, height: 200},
},
this.workspace.id,
);
});
test('change events are fired when a comments text is edited', function () {
this.renderedComment = new Blockly.comments.RenderedWorkspaceComment(
this.workspace,