mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
feat: allow overriding comment icons (#7937)
* feat: add comment icon interface * feat: have blocks construct comment icons from registry * chore: add tests for setCommentText * fix: typeguard
This commit is contained in:
@@ -34,8 +34,8 @@ import {Input} from './inputs/input.js';
|
|||||||
import {Align} from './inputs/align.js';
|
import {Align} from './inputs/align.js';
|
||||||
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||||
import type {IDeletable} from './interfaces/i_deletable.js';
|
import type {IDeletable} from './interfaces/i_deletable.js';
|
||||||
import type {IIcon} from './interfaces/i_icon.js';
|
import {type IIcon} from './interfaces/i_icon.js';
|
||||||
import {CommentIcon} from './icons/comment_icon.js';
|
import {isCommentIcon} from './interfaces/i_comment_icon.js';
|
||||||
import type {MutatorIcon} from './icons/mutator_icon.js';
|
import type {MutatorIcon} from './icons/mutator_icon.js';
|
||||||
import * as Tooltip from './tooltip.js';
|
import * as Tooltip from './tooltip.js';
|
||||||
import * as arrayUtils from './utils/array.js';
|
import * as arrayUtils from './utils/array.js';
|
||||||
@@ -2212,7 +2212,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
* @returns Block's comment.
|
* @returns Block's comment.
|
||||||
*/
|
*/
|
||||||
getCommentText(): string | null {
|
getCommentText(): string | null {
|
||||||
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
|
const comment = this.getIcon(IconType.COMMENT);
|
||||||
return comment?.getText() ?? null;
|
return comment?.getText() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2222,19 +2222,36 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
* @param text The text, or null to delete.
|
* @param text The text, or null to delete.
|
||||||
*/
|
*/
|
||||||
setCommentText(text: string | null) {
|
setCommentText(text: string | null) {
|
||||||
const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
|
const comment = this.getIcon(IconType.COMMENT);
|
||||||
const oldText = comment?.getText() ?? null;
|
const oldText = comment?.getText() ?? null;
|
||||||
if (oldText === text) return;
|
if (oldText === text) return;
|
||||||
if (text !== null) {
|
if (text !== null) {
|
||||||
let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined;
|
let comment = this.getIcon(IconType.COMMENT);
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
comment = this.addIcon(new CommentIcon(this));
|
const commentConstructor = registry.getClass(
|
||||||
|
registry.Type.ICON,
|
||||||
|
IconType.COMMENT.toString(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (!commentConstructor) {
|
||||||
|
throw new Error(
|
||||||
|
'No comment icon class is registered, so a comment cannot be set',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const icon = new commentConstructor(this);
|
||||||
|
if (!isCommentIcon(icon)) {
|
||||||
|
throw new Error(
|
||||||
|
'The class registered as a comment icon does not conform to the ' +
|
||||||
|
'ICommentIcon interface',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
comment = this.addIcon(icon);
|
||||||
}
|
}
|
||||||
eventUtils.disable();
|
eventUtils.disable();
|
||||||
comment.setText(text);
|
comment.setText(text);
|
||||||
eventUtils.enable();
|
eventUtils.enable();
|
||||||
} else {
|
} else {
|
||||||
this.removeIcon(CommentIcon.TYPE);
|
this.removeIcon(IconType.COMMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventUtils.fire(
|
eventUtils.fire(
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ICommentIcon} from '../interfaces/i_comment_icon.js';
|
||||||
import {IIcon} from '../interfaces/i_icon.js';
|
import {IIcon} from '../interfaces/i_icon.js';
|
||||||
import {CommentIcon} from './comment_icon.js';
|
|
||||||
import {MutatorIcon} from './mutator_icon.js';
|
import {MutatorIcon} from './mutator_icon.js';
|
||||||
import {WarningIcon} from './warning_icon.js';
|
import {WarningIcon} from './warning_icon.js';
|
||||||
|
|
||||||
@@ -28,5 +28,5 @@ export class IconType<_T extends IIcon> {
|
|||||||
|
|
||||||
static MUTATOR = new IconType<MutatorIcon>('mutator');
|
static MUTATOR = new IconType<MutatorIcon>('mutator');
|
||||||
static WARNING = new IconType<WarningIcon>('warning');
|
static WARNING = new IconType<WarningIcon>('warning');
|
||||||
static COMMENT = new IconType<CommentIcon>('comment');
|
static COMMENT = new IconType<ICommentIcon>('comment');
|
||||||
}
|
}
|
||||||
|
|||||||
33
core/interfaces/i_comment_icon.ts
Normal file
33
core/interfaces/i_comment_icon.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {IconType} from '../icons.js';
|
||||||
|
import {IIcon, isIcon} from './i_icon.js';
|
||||||
|
import {Size} from '../utils/size.js';
|
||||||
|
import {IHasBubble, hasBubble} from './i_has_bubble.js';
|
||||||
|
|
||||||
|
export interface ICommentIcon extends IIcon, IHasBubble {
|
||||||
|
setText(text: string): void;
|
||||||
|
|
||||||
|
getText(): string;
|
||||||
|
|
||||||
|
setBubbleSize(size: Size): void;
|
||||||
|
|
||||||
|
getBubbleSize(): Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether the given object is an ICommentIcon. */
|
||||||
|
export function isCommentIcon(obj: Object): obj is ICommentIcon {
|
||||||
|
return (
|
||||||
|
isIcon(obj) &&
|
||||||
|
hasBubble(obj) &&
|
||||||
|
(obj as any)['setText'] !== undefined &&
|
||||||
|
(obj as any)['getText'] !== undefined &&
|
||||||
|
(obj as any)['setBubbleSize'] !== undefined &&
|
||||||
|
(obj as any)['getBubbleSize'] !== undefined &&
|
||||||
|
obj.getType() === IconType.COMMENT
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
createMockEvent,
|
createMockEvent,
|
||||||
} from './test_helpers/events.js';
|
} from './test_helpers/events.js';
|
||||||
import {MockIcon, MockBubbleIcon} from './test_helpers/icon_mocks.js';
|
import {MockIcon, MockBubbleIcon} from './test_helpers/icon_mocks.js';
|
||||||
|
import {IconType} from '../../build/src/core/icons/icon_types.js';
|
||||||
|
|
||||||
suite('Blocks', function () {
|
suite('Blocks', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
@@ -1367,6 +1368,93 @@ suite('Blocks', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('Constructing registered comment classes', function () {
|
||||||
|
class MockComment extends MockIcon {
|
||||||
|
getType() {
|
||||||
|
return Blockly.icons.IconType.COMMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
setText() {}
|
||||||
|
|
||||||
|
getText() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setBubbleSize() {}
|
||||||
|
|
||||||
|
getBubbleSize() {
|
||||||
|
return Blockly.utils.Size(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bubbleIsVisible() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBubbleVisible() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(function () {
|
||||||
|
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||||
|
|
||||||
|
this.block = this.workspace.newBlock('stack_block');
|
||||||
|
this.block.initSvg();
|
||||||
|
this.block.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
workspaceTeardown.call(this, this.workspace);
|
||||||
|
|
||||||
|
Blockly.icons.registry.unregister(
|
||||||
|
Blockly.icons.IconType.COMMENT.toString(),
|
||||||
|
);
|
||||||
|
Blockly.icons.registry.register(
|
||||||
|
Blockly.icons.IconType.COMMENT,
|
||||||
|
Blockly.icons.CommentIcon,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setCommentText constructs the registered comment icon', function () {
|
||||||
|
Blockly.icons.registry.unregister(
|
||||||
|
Blockly.icons.IconType.COMMENT.toString(),
|
||||||
|
);
|
||||||
|
Blockly.icons.registry.register(
|
||||||
|
Blockly.icons.IconType.COMMENT,
|
||||||
|
MockComment,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.block.setCommentText('test text');
|
||||||
|
|
||||||
|
chai.assert.instanceOf(
|
||||||
|
this.block.getIcon(Blockly.icons.IconType.COMMENT),
|
||||||
|
MockComment,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setCommentText throws if no icon is registered', function () {
|
||||||
|
Blockly.icons.registry.unregister(
|
||||||
|
Blockly.icons.IconType.COMMENT.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
chai.assert.throws(() => {
|
||||||
|
this.block.setCommentText('test text');
|
||||||
|
}, 'No comment icon class is registered, so a comment cannot be set');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setCommentText throws if the icon is not an ICommentIcon', function () {
|
||||||
|
Blockly.icons.registry.unregister(
|
||||||
|
Blockly.icons.IconType.COMMENT.toString(),
|
||||||
|
);
|
||||||
|
Blockly.icons.registry.register(
|
||||||
|
Blockly.icons.IconType.COMMENT,
|
||||||
|
MockIcon,
|
||||||
|
);
|
||||||
|
|
||||||
|
chai.assert.throws(() => {
|
||||||
|
this.block.setCommentText('test text');
|
||||||
|
}, 'The class registered as a comment icon does not conform to the ICommentIcon interface');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('Getting/Setting Field (Values)', function () {
|
suite('Getting/Setting Field (Values)', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user