Skip to content

Commit 989c91f

Browse files
authored
feat!: Add support for preserving block comment locations. (#8231)
* feat: Add support for preserving block comment locations. * chore: format the tests.
1 parent b0b7d78 commit 989c91f

File tree

6 files changed

+158
-5
lines changed

6 files changed

+158
-5
lines changed

core/bubbles/textinput_bubble.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export class TextInputBubble extends Bubble {
4747
/** Functions listening for changes to the size of this bubble. */
4848
private sizeChangeListeners: (() => void)[] = [];
4949

50+
/** Functions listening for changes to the location of this bubble. */
51+
private locationChangeListeners: (() => void)[] = [];
52+
5053
/** The text of this bubble. */
5154
private text = '';
5255

@@ -105,6 +108,11 @@ export class TextInputBubble extends Bubble {
105108
this.sizeChangeListeners.push(listener);
106109
}
107110

111+
/** Adds a change listener to be notified when this bubble's location changes. */
112+
addLocationChangeListener(listener: () => void) {
113+
this.locationChangeListeners.push(listener);
114+
}
115+
108116
/** Creates the editor UI for this bubble. */
109117
private createEditor(container: SVGGElement): {
110118
inputRoot: SVGForeignObjectElement;
@@ -212,10 +220,25 @@ export class TextInputBubble extends Bubble {
212220

213221
/** @returns the size of this bubble. */
214222
getSize(): Size {
215-
// Overriden to be public.
223+
// Overridden to be public.
216224
return super.getSize();
217225
}
218226

227+
override moveDuringDrag(newLoc: Coordinate) {
228+
super.moveDuringDrag(newLoc);
229+
this.onLocationChange();
230+
}
231+
232+
override setPositionRelativeToAnchor(left: number, top: number) {
233+
super.setPositionRelativeToAnchor(left, top);
234+
this.onLocationChange();
235+
}
236+
237+
protected override positionByRect(rect = new Rect(0, 0, 0, 0)) {
238+
super.positionByRect(rect);
239+
this.onLocationChange();
240+
}
241+
219242
/** Handles mouse down events on the resize target. */
220243
private onResizePointerDown(e: PointerEvent) {
221244
this.bringToFront();
@@ -297,6 +320,13 @@ export class TextInputBubble extends Bubble {
297320
listener();
298321
}
299322
}
323+
324+
/** Handles a location change event for the text area. Calls event listeners. */
325+
private onLocationChange() {
326+
for (const listener of this.locationChangeListeners) {
327+
listener();
328+
}
329+
}
300330
}
301331

302332
Css.register(`

core/icons/comment_icon.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
5858
/** The size of this comment (which is applied to the editable bubble). */
5959
private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT);
6060

61+
/** The location of the comment bubble in workspace coordinates. */
62+
private bubbleLocation?: Coordinate;
63+
6164
/**
6265
* The visibility of the bubble for this comment.
6366
*
@@ -149,7 +152,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
149152
}
150153

151154
override onLocationChange(blockOrigin: Coordinate): void {
155+
const oldLocation = this.workspaceLocation;
152156
super.onLocationChange(blockOrigin);
157+
if (this.bubbleLocation) {
158+
const newLocation = this.workspaceLocation;
159+
const delta = Coordinate.difference(newLocation, oldLocation);
160+
this.bubbleLocation = Coordinate.sum(this.bubbleLocation, delta);
161+
}
153162
const anchorLocation = this.getAnchorLocation();
154163
this.textInputBubble?.setAnchorLocation(anchorLocation);
155164
this.textBubble?.setAnchorLocation(anchorLocation);
@@ -191,18 +200,43 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
191200
return this.bubbleSize;
192201
}
193202

203+
/**
204+
* Sets the location of the comment bubble in the workspace.
205+
*/
206+
setBubbleLocation(location: Coordinate) {
207+
this.bubbleLocation = location;
208+
this.textInputBubble?.moveDuringDrag(location);
209+
this.textBubble?.moveDuringDrag(location);
210+
}
211+
212+
/**
213+
* @returns the location of the comment bubble in the workspace.
214+
*/
215+
getBubbleLocation(): Coordinate | undefined {
216+
return this.bubbleLocation;
217+
}
218+
194219
/**
195220
* @returns the state of the comment as a JSON serializable value if the
196221
* comment has text. Otherwise returns null.
197222
*/
198223
saveState(): CommentState | null {
199224
if (this.text) {
200-
return {
225+
const state: CommentState = {
201226
'text': this.text,
202227
'pinned': this.bubbleIsVisible(),
203228
'height': this.bubbleSize.height,
204229
'width': this.bubbleSize.width,
205230
};
231+
const location = this.getBubbleLocation();
232+
if (location) {
233+
state['x'] = this.sourceBlock.workspace.RTL
234+
? this.sourceBlock.workspace.getWidth() -
235+
(location.x + this.bubbleSize.width)
236+
: location.x;
237+
state['y'] = location.y;
238+
}
239+
return state;
206240
}
207241
return null;
208242
}
@@ -216,6 +250,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
216250
);
217251
this.bubbleVisiblity = state['pinned'] ?? false;
218252
this.setBubbleVisible(this.bubbleVisiblity);
253+
let x = state['x'];
254+
const y = state['y'];
255+
renderManagement.finishQueuedRenders().then(() => {
256+
if (x && y) {
257+
x = this.sourceBlock.workspace.RTL
258+
? this.sourceBlock.workspace.getWidth() - (x + this.bubbleSize.width)
259+
: x;
260+
this.setBubbleLocation(new Coordinate(x, y));
261+
}
262+
});
219263
}
220264

221265
override onClick(): void {
@@ -259,6 +303,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
259303
}
260304
}
261305

306+
onBubbleLocationChange(): void {
307+
if (this.textInputBubble) {
308+
this.bubbleLocation = this.textInputBubble.getRelativeToSurfaceXY();
309+
}
310+
}
311+
262312
bubbleIsVisible(): boolean {
263313
return this.bubbleVisiblity;
264314
}
@@ -308,8 +358,14 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
308358
);
309359
this.textInputBubble.setText(this.getText());
310360
this.textInputBubble.setSize(this.bubbleSize, true);
361+
if (this.bubbleLocation) {
362+
this.textInputBubble.moveDuringDrag(this.bubbleLocation);
363+
}
311364
this.textInputBubble.addTextChangeListener(() => this.onTextChange());
312365
this.textInputBubble.addSizeChangeListener(() => this.onSizeChange());
366+
this.textInputBubble.addLocationChangeListener(() =>
367+
this.onBubbleLocationChange(),
368+
);
313369
}
314370

315371
/** Shows the non editable text bubble for this comment. */
@@ -320,6 +376,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
320376
this.getAnchorLocation(),
321377
this.getBubbleOwnerRect(),
322378
);
379+
if (this.bubbleLocation) {
380+
this.textBubble.moveDuringDrag(this.bubbleLocation);
381+
}
323382
}
324383

325384
/** Hides any open bubbles owned by this comment. */
@@ -365,6 +424,12 @@ export interface CommentState {
365424

366425
/** The width of the comment bubble. */
367426
width?: number;
427+
428+
/** The X coordinate of the comment bubble. */
429+
x?: number;
430+
431+
/** The Y coordinate of the comment bubble. */
432+
y?: number;
368433
}
369434

370435
registry.register(CommentIcon.TYPE, CommentIcon);

core/interfaces/i_comment_icon.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {IconType} from '../icons/icon_types.js';
88
import {CommentState} from '../icons/comment_icon.js';
99
import {IIcon, isIcon} from './i_icon.js';
1010
import {Size} from '../utils/size.js';
11+
import {Coordinate} from '../utils/coordinate.js';
1112
import {IHasBubble, hasBubble} from './i_has_bubble.js';
1213
import {ISerializable, isSerializable} from './i_serializable.js';
1314

@@ -20,6 +21,10 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable {
2021

2122
getBubbleSize(): Size;
2223

24+
setBubbleLocation(location: Coordinate): void;
25+
26+
getBubbleLocation(): Coordinate | undefined;
27+
2328
saveState(): CommentState;
2429

2530
loadState(state: CommentState): void;
@@ -35,6 +40,8 @@ export function isCommentIcon(obj: Object): obj is ICommentIcon {
3540
(obj as any)['getText'] !== undefined &&
3641
(obj as any)['setBubbleSize'] !== undefined &&
3742
(obj as any)['getBubbleSize'] !== undefined &&
43+
(obj as any)['setBubbleLocation'] !== undefined &&
44+
(obj as any)['getBubbleLocation'] !== undefined &&
3845
obj.getType() === IconType.COMMENT
3946
);
4047
}

core/xml.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,24 @@ export function blockToDom(
217217
const comment = block.getIcon(IconType.COMMENT)!;
218218
const size = comment.getBubbleSize();
219219
const pinned = comment.bubbleIsVisible();
220+
const location = comment.getBubbleLocation();
220221

221222
const commentElement = utilsXml.createElement('comment');
222223
commentElement.appendChild(utilsXml.createTextNode(commentText));
223224
commentElement.setAttribute('pinned', `${pinned}`);
224-
commentElement.setAttribute('h', String(size.height));
225-
commentElement.setAttribute('w', String(size.width));
225+
commentElement.setAttribute('h', `${size.height}`);
226+
commentElement.setAttribute('w', `${size.width}`);
227+
if (location) {
228+
commentElement.setAttribute(
229+
'x',
230+
`${
231+
block.workspace.RTL
232+
? block.workspace.getWidth() - (location.x + size.width)
233+
: location.x
234+
}`,
235+
);
236+
commentElement.setAttribute('y', `${location.y}`);
237+
}
226238

227239
element.appendChild(commentElement);
228240
}
@@ -795,6 +807,8 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) {
795807
const pinned = xmlChild.getAttribute('pinned') === 'true';
796808
const width = parseInt(xmlChild.getAttribute('w') ?? '50', 10);
797809
const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10);
810+
let x = parseInt(xmlChild.getAttribute('x') ?? '', 10);
811+
const y = parseInt(xmlChild.getAttribute('y') ?? '', 10);
798812

799813
block.setCommentText(text);
800814
const comment = block.getIcon(IconType.COMMENT)!;
@@ -803,8 +817,15 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) {
803817
}
804818
// Set the pinned state of the bubble.
805819
comment.setBubbleVisible(pinned);
820+
806821
// Actually show the bubble after the block has been rendered.
807-
setTimeout(() => comment.setBubbleVisible(pinned), 1);
822+
setTimeout(() => {
823+
if (!isNaN(x) && !isNaN(y)) {
824+
x = block.workspace.RTL ? block.workspace.getWidth() - (x + width) : x;
825+
comment.setBubbleLocation(new Coordinate(x, y));
826+
}
827+
comment.setBubbleVisible(pinned);
828+
}, 1);
808829
}
809830
}
810831

tests/mocha/block_test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,10 @@ suite('Blocks', function () {
13881388
return Blockly.utils.Size(0, 0);
13891389
}
13901390

1391+
setBubbleLocation() {}
1392+
1393+
getBubbleLocation() {}
1394+
13911395
bubbleIsVisible() {
13921396
return true;
13931397
}

tests/mocha/comment_test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,30 @@ suite('Comments', function () {
141141
assertBubbleSize(this.comment, 100, 100);
142142
});
143143
});
144+
suite('Set/Get Bubble Location', function () {
145+
teardown(function () {
146+
sinon.restore();
147+
});
148+
function assertBubbleLocation(comment, x, y) {
149+
const location = comment.getBubbleLocation();
150+
assert.equal(location.x, x);
151+
assert.equal(location.y, y);
152+
}
153+
test('Set Location While Visible', function () {
154+
this.comment.setBubbleVisible(true);
155+
156+
this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100));
157+
assertBubbleLocation(this.comment, 100, 100);
158+
159+
this.comment.setBubbleVisible(false);
160+
assertBubbleLocation(this.comment, 100, 100);
161+
});
162+
test('Set Location While Invisible', function () {
163+
this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100));
164+
assertBubbleLocation(this.comment, 100, 100);
165+
166+
this.comment.setBubbleVisible(true);
167+
assertBubbleLocation(this.comment, 100, 100);
168+
});
169+
});
144170
});

0 commit comments

Comments
 (0)