fix!: refactor comment icon (#7128)

* fix: add basic comment icon

* fix: add using comment icon

* chore: delete old comment icon

* chore: add docs to the comment icon

* chore: move Comment to icons.CommentIcon

* chore: mode properties to module level

* chore: properly override and call super

* chore: remove .comment and .commentIcon_

* chore: cleanup test

* chore: deprecate getCommentIcon and getCommentText

* chore: change imports to import type

* chore: refactor code for paren peace

* chore: fix lint and make it error

* chore: remove change to block JS file

* chore: fix css

* chore: add renamings

* chore: format
This commit is contained in:
Beka Westberg
2023-06-02 09:53:05 -07:00
committed by GitHub
parent f4e378d096
commit 50d9474db5
19 changed files with 518 additions and 617 deletions

View File

@@ -85,7 +85,7 @@ function buildTSOverride({files, tsconfig}) {
// Use TS-specific rule. // Use TS-specific rule.
'no-unused-vars': ['off'], 'no-unused-vars': ['off'],
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'warn', 'error',
{ {
'argsIgnorePattern': '^_', 'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_', 'varsIgnorePattern': '^_',

View File

@@ -20,7 +20,6 @@ import './events/events_block_create.js';
import './events/events_block_delete.js'; import './events/events_block_delete.js';
import {Blocks} from './blocks.js'; import {Blocks} from './blocks.js';
import type {Comment} from './comment.js';
import * as common from './common.js'; import * as common from './common.js';
import {Connection} from './connection.js'; import {Connection} from './connection.js';
import {ConnectionType} from './connection_type.js'; import {ConnectionType} from './connection_type.js';
@@ -37,6 +36,7 @@ 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 type {Mutator} from './mutator.js'; import type {Mutator} from './mutator.js';
import {CommentIcon} from './icons/comment_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';
import {Coordinate} from './utils/coordinate.js'; import {Coordinate} from './utils/coordinate.js';
@@ -183,14 +183,6 @@ export class Block implements IASTNodeLocation, IDeletable {
*/ */
private disposing = false; private disposing = false;
/**
* A string representing the comment attached to this block.
*
* @deprecated August 2019. Use getCommentText instead.
*/
comment: string | Comment | null = null;
/** @internal */
commentModel: CommentModel;
private readonly xy_: Coordinate; private readonly xy_: Coordinate;
isInFlyout: boolean; isInFlyout: boolean;
isInMutator: boolean; isInMutator: boolean;
@@ -239,9 +231,6 @@ export class Block implements IASTNodeLocation, IDeletable {
opt_id && !workspace.getBlockById(opt_id) ? opt_id : idGenerator.genUid(); opt_id && !workspace.getBlockById(opt_id) ? opt_id : idGenerator.genUid();
workspace.setBlockById(this.id, this); workspace.setBlockById(this.id, this);
/** A model of the comment attached to this block. */
this.commentModel = {text: null, pinned: false, size: new Size(160, 80)};
/** /**
* The block's position in workspace units. (0, 0) is at the workspace's * The block's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value. * origin; scale does not change this value.
@@ -2169,7 +2158,8 @@ export class Block implements IASTNodeLocation, IDeletable {
* @returns Block's comment. * @returns Block's comment.
*/ */
getCommentText(): string | null { getCommentText(): string | null {
return this.commentModel.text; const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
return comment?.getText() ?? null;
} }
/** /**
@@ -2178,20 +2168,28 @@ 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) {
if (this.commentModel.text === text) { const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null;
return; const oldText = comment?.getText() ?? null;
} if (oldText === text) return;
eventUtils.fire( eventUtils.fire(
new (eventUtils.get(eventUtils.BLOCK_CHANGE))( new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this, this,
'comment', 'comment',
null, null,
this.commentModel.text, oldText,
text text
) )
); );
this.commentModel.text = text;
this.comment = text; // For backwards compatibility. if (text !== null) {
let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined;
if (!comment) {
comment = this.addIcon(new CommentIcon(this));
}
comment.setText(text);
} else {
this.removeIcon(CommentIcon.TYPE);
}
} }
/** /**

View File

@@ -18,7 +18,7 @@ import './events/events_selected.js';
import {Block} from './block.js'; import {Block} from './block.js';
import * as blockAnimations from './block_animations.js'; import * as blockAnimations from './block_animations.js';
import * as browserEvents from './browser_events.js'; import * as browserEvents from './browser_events.js';
import {Comment} from './comment.js'; import {CommentIcon} from './icons/comment_icon.js';
import * as common from './common.js'; import * as common from './common.js';
import {config} from './config.js'; import {config} from './config.js';
import type {Connection} from './connection.js'; import type {Connection} from './connection.js';
@@ -60,6 +60,7 @@ import {WarningIcon} from './icons/warning_icon.js';
import type {Workspace} from './workspace.js'; import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js';
import {queueRender} from './render_management.js'; import {queueRender} from './render_management.js';
import * as deprecation from './utils/deprecation.js';
/** /**
* Class for a block's SVG representation. * Class for a block's SVG representation.
@@ -109,9 +110,6 @@ export class BlockSvg
/** Block's mutator icon (if any). */ /** Block's mutator icon (if any). */
mutator: Mutator | null = null; mutator: Mutator | null = null;
/** Block's comment icon (if any). */
private commentIcon_: Comment | null = null;
/** /**
* Block's warning icon (if any). * Block's warning icon (if any).
* *
@@ -909,44 +907,11 @@ export class BlockSvg
* comment. * comment.
* *
* @returns The comment icon attached to this block, or null. * @returns The comment icon attached to this block, or null.
* @deprecated Use getIcon. To be remove in v11.
*/ */
getCommentIcon(): Comment | null { getCommentIcon(): CommentIcon | null {
return this.commentIcon_; deprecation.warn('getCommentIcon', 'v10', 'v11', 'getIcon');
} return (this.getIcon(CommentIcon.TYPE) ?? null) as CommentIcon | null;
/**
* Set this block's comment text.
*
* @param text The text, or null to delete.
*/
override setCommentText(text: string | null) {
if (this.commentModel.text === text) {
return;
}
super.setCommentText(text);
const shouldHaveComment = text !== null;
if (!!this.commentIcon_ === shouldHaveComment) {
// If the comment's state of existence is correct, but the text is new
// that means we're just updating a comment.
this.commentIcon_!.updateText();
return;
}
if (shouldHaveComment) {
this.commentIcon_ = new Comment(this);
this.comment = this.commentIcon_; // For backwards compatibility.
} else {
this.commentIcon_!.dispose();
this.commentIcon_ = null;
this.comment = null; // For backwards compatibility.
}
if (this.rendered) {
// Icons must force an immediate render so that bubbles can be opened
// immedately at the correct position.
this.render();
// Adding or removing a comment icon will cause the block to change shape.
this.bumpNeighbours();
}
} }
/** /**
@@ -1092,7 +1057,6 @@ export class BlockSvg
// resolved. // resolved.
override getIcons(): AnyDuringMigration[] { override getIcons(): AnyDuringMigration[] {
const icons: AnyDuringMigration = [...this.icons]; const icons: AnyDuringMigration = [...this.icons];
if (this.commentIcon_) icons.push(this.commentIcon_);
if (this.mutator) icons.push(this.mutator); if (this.mutator) icons.push(this.mutator);
return icons; return icons;
} }

View File

@@ -27,7 +27,6 @@ import {Bubble} from './bubble_old.js';
import {BubbleDragger} from './bubble_dragger.js'; import {BubbleDragger} from './bubble_dragger.js';
import * as bumpObjects from './bump_objects.js'; import * as bumpObjects from './bump_objects.js';
import * as clipboard from './clipboard.js'; import * as clipboard from './clipboard.js';
import {Comment} from './comment.js';
import * as common from './common.js'; import * as common from './common.js';
import {ComponentManager} from './component_manager.js'; import {ComponentManager} from './component_manager.js';
import {config} from './config.js'; import {config} from './config.js';
@@ -504,7 +503,6 @@ export {Blocks};
export {Bubble}; export {Bubble};
export {BubbleDragger}; export {BubbleDragger};
export {CollapsibleToolboxCategory}; export {CollapsibleToolboxCategory};
export {Comment};
export {ComponentManager}; export {ComponentManager};
export {Connection}; export {Connection};
export {ConnectionType}; export {ConnectionType};

View File

@@ -6,6 +6,7 @@
import {Bubble} from './bubble.js'; import {Bubble} from './bubble.js';
import {Coordinate} from '../utils/coordinate.js'; import {Coordinate} from '../utils/coordinate.js';
import * as Css from '../css.js';
import * as dom from '../utils/dom.js'; import * as dom from '../utils/dom.js';
import {Rect} from '../utils/rect.js'; import {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js'; import {Size} from '../utils/size.js';
@@ -39,6 +40,9 @@ export class TextInputBubble extends Bubble {
/** Functions listening for changes to the text of this bubble. */ /** Functions listening for changes to the text of this bubble. */
private textChangeListeners: (() => void)[] = []; private textChangeListeners: (() => void)[] = [];
/** Functions listening for changes to the size of this bubble. */
private sizeChangeListeners: (() => void)[] = [];
/** The text of this bubble. */ /** The text of this bubble. */
private text = ''; private text = '';
@@ -91,6 +95,11 @@ export class TextInputBubble extends Bubble {
this.textChangeListeners.push(listener); this.textChangeListeners.push(listener);
} }
/** Adds a change listener to be notified when this bubble's size changes. */
addSizeChangeListener(listener: () => void) {
this.sizeChangeListeners.push(listener);
}
/** Creates the editor UI for this bubble. */ /** Creates the editor UI for this bubble. */
private createEditor(container: SVGGElement): { private createEditor(container: SVGGElement): {
inputRoot: SVGForeignObjectElement; inputRoot: SVGForeignObjectElement;
@@ -224,6 +233,7 @@ export class TextInputBubble extends Bubble {
} }
super.setSize(size, relayout); super.setSize(size, relayout);
this.onSizeChange();
} }
/** @returns the size of this bubble. */ /** @returns the size of this bubble. */
@@ -285,6 +295,7 @@ export class TextInputBubble extends Bubble {
new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y), new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y),
false false
); );
this.onSizeChange();
} }
/** /**
@@ -305,4 +316,24 @@ export class TextInputBubble extends Bubble {
listener(); listener();
} }
} }
/** Handles a size change event for the text area. Calls event listeners. */
private onSizeChange() {
for (const listener of this.sizeChangeListeners) {
listener();
}
}
} }
Css.register(`
.blocklyCommentTextarea {
background-color: #fef49c;
border: 0;
display: block;
margin: 0;
outline: 0;
padding: 3px;
resize: none;
text-overflow: hidden;
}
`);

View File

@@ -1,419 +0,0 @@
/**
* @license
* Copyright 2011 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Object representing a code comment.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Comment');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_bubble_open.js';
import type {CommentModel} from './block.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import {Bubble} from './bubble_old.js';
import * as Css from './css.js';
import * as eventUtils from './events/utils.js';
import {Icon} from './icon_old.js';
import type {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import type {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
/**
* Class for a comment.
*/
export class Comment extends Icon {
private readonly model: CommentModel;
/**
* The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit.
*/
private cachedText: string | null = '';
/**
* Array holding info needed to unbind events.
* Used for disposing.
* Ex: [[node, name, func], [node, name, func]].
*/
private boundEvents: browserEvents.Data[] = [];
/**
* The SVG element that contains the text edit area, or null if not created.
*/
private foreignObject: SVGForeignObjectElement | null = null;
/** The editable text area, or null if not created. */
private textarea_: HTMLTextAreaElement | null = null;
/** The top-level node of the comment text, or null if not created. */
private paragraphElement_: SVGTextElement | null = null;
/** @param block The block associated with this comment. */
constructor(block: BlockSvg) {
super(block);
/** The model for this comment. */
this.model = block.commentModel;
// If someone creates the comment directly instead of calling
// block.setCommentText we want to make sure the text is non-null;
this.model.text = this.model.text ?? '';
this.createIcon();
}
/**
* Draw the comment icon.
*
* @param group The icon group.
*/
protected override drawIcon_(group: Element) {
// Circle.
dom.createSvgElement(
Svg.CIRCLE,
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
group
);
// Can't use a real '?' text character since different browsers and
// operating systems render it differently. Body of question mark.
dom.createSvgElement(
Svg.PATH,
{
'class': 'blocklyIconSymbol',
'd':
'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
'-1.201,0.998 -1.201,1.528 -1.204,2.19z',
},
group
);
// Dot of question mark.
dom.createSvgElement(
Svg.RECT,
{
'class': 'blocklyIconSymbol',
'x': '6.8',
'y': '10.78',
'height': '2',
'width': '2',
},
group
);
}
/**
* Create the editor for the comment's bubble.
*
* @returns The top-level node of the editor.
*/
private createEditor(): SVGElement {
/* Create the editor. Here's the markup that will be generated in
* editable mode:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml"
class="blocklyMinimalBody"> <textarea
xmlns="http://www.w3.org/1999/xhtml" class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* For non-editable mode see Warning.textToDom_.
*/
this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
'x': Bubble.BORDER_WIDTH,
'y': Bubble.BORDER_WIDTH,
});
const body = document.createElementNS(dom.HTML_NS, 'body');
body.setAttribute('xmlns', dom.HTML_NS);
body.className = 'blocklyMinimalBody';
this.textarea_ = document.createElementNS(
dom.HTML_NS,
'textarea'
) as HTMLTextAreaElement;
const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.getBlock().RTL ? 'RTL' : 'LTR');
textarea.value = this.model.text ?? '';
this.resizeTextarea();
body.appendChild(textarea);
this.foreignObject!.appendChild(body);
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'focus',
this,
this.startEdit,
true
)
);
// Don't zoom with mousewheel.
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'wheel',
this,
function (e: Event) {
e.stopPropagation();
}
)
);
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'change',
this,
/**
* @param _e Unused event parameter.
*/
function (this: Comment, _e: Event) {
if (this.cachedText !== this.model.text) {
eventUtils.fire(
new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.getBlock(),
'comment',
null,
this.cachedText,
this.model.text
)
);
}
}
)
);
this.boundEvents.push(
browserEvents.conditionalBind(
textarea,
'input',
this,
/**
* @param _e Unused event parameter.
*/
function (this: Comment, _e: Event) {
this.model.text = textarea.value;
}
)
);
setTimeout(textarea.focus.bind(textarea), 0);
return this.foreignObject;
}
/** Add or remove editability of the comment. */
override updateEditable() {
super.updateEditable();
if (this.isVisible()) {
// Recreate the bubble with the correct UI.
this.disposeBubble();
this.createBubble();
}
}
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
*/
private onBubbleResize() {
if (!this.isVisible() || !this.bubble_) {
return;
}
this.model.size = this.bubble_.getBubbleSize();
this.resizeTextarea();
}
/**
* Resizes the text area to match the size defined on the model (which is
* the size of the bubble).
*/
private resizeTextarea() {
if (!this.textarea_ || !this.foreignObject) {
return;
}
const size = this.model.size;
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
const widthMinusBorder = size.width - doubleBorderWidth;
const heightMinusBorder = size.height - doubleBorderWidth;
this.foreignObject.setAttribute('width', `${widthMinusBorder}`);
this.foreignObject.setAttribute('height', `${heightMinusBorder}`);
this.textarea_.style.width = widthMinusBorder - 4 + 'px';
this.textarea_.style.height = heightMinusBorder - 4 + 'px';
}
/**
* Show or hide the comment bubble.
*
* @param visible True if the bubble should be visible.
*/
override setVisible(visible: boolean) {
if (visible === this.isVisible()) {
return;
}
eventUtils.fire(
new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.getBlock(),
visible,
'comment'
)
);
this.model.pinned = visible;
if (visible) {
this.createBubble();
} else {
this.disposeBubble();
}
}
/** Show the bubble. Handles deciding if it should be editable or not. */
private createBubble() {
if (!this.getBlock().isEditable()) {
this.createNonEditableBubble();
} else {
this.createEditableBubble();
}
}
/** Show an editable bubble. */
private createEditableBubble() {
const block = this.getBlock();
this.bubble_ = new Bubble(
block.workspace,
this.createEditor(),
block.pathObject.svgPath,
this.iconXY_ as Coordinate,
this.model.size.width,
this.model.size.height
);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(block.id);
this.bubble_.registerResizeEvent(this.onBubbleResize.bind(this));
this.applyColour();
}
/**
* Show a non-editable bubble.
*/
private createNonEditableBubble() {
// TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.model.text ?? '');
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_,
this.getBlock(),
this.iconXY_ as Coordinate
);
this.applyColour();
}
/**
* Dispose of the bubble.
*/
private disposeBubble() {
for (const event of this.boundEvents) {
browserEvents.unbind(event);
}
this.boundEvents.length = 0;
if (this.bubble_) {
this.bubble_.dispose();
this.bubble_ = null;
}
this.textarea_ = null;
this.foreignObject = null;
this.paragraphElement_ = null;
}
/**
* Callback fired when an edit starts.
*
* Bring the comment to the top of the stack when clicked on. Also cache the
* current text so it can be used to fire a change event.
*
* @param _e Mouse up event.
*/
private startEdit(_e: PointerEvent) {
if (this.bubble_?.promote()) {
// Since the act of moving this node within the DOM causes a loss of
// focus, we need to reapply the focus.
this.textarea_!.focus();
}
this.cachedText = this.model.text;
}
/**
* Get the dimensions of this comment's bubble.
*
* @returns Object with width and height properties.
*/
getBubbleSize(): Size {
return this.model.size;
}
/**
* Size this comment's bubble.
*
* @param width Width of the bubble.
* @param height Height of the bubble.
*/
setBubbleSize(width: number, height: number) {
if (this.bubble_) {
this.bubble_.setBubbleSize(width, height);
} else {
this.model.size.width = width;
this.model.size.height = height;
}
}
/**
* Update the comment's view to match the model.
*
* @internal
*/
updateText() {
if (this.textarea_) {
this.textarea_.value = this.model.text ?? '';
} else if (this.paragraphElement_) {
// Non-Editable mode.
// TODO (#2917): If 2917 gets added this will probably need to be updated.
this.paragraphElement_.firstChild!.textContent = this.model.text;
}
}
/**
* Dispose of this comment.
*
* If you want to receive a comment "delete" event (newValue: null), then this
* should not be called directly. Instead call block.setCommentText(null);
*/
override dispose() {
this.getBlock().comment = null;
super.dispose();
}
}
/** CSS for block comment. See css.js for use. */
Css.register(`
.blocklyCommentTextarea {
background-color: #fef49c;
border: 0;
display: block;
margin: 0;
outline: 0;
padding: 3px;
resize: none;
text-overflow: hidden;
}
`);

View File

@@ -4,7 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import {CommentIcon} from './icons/comment_icon.js';
import * as exceptions from './icons/exceptions.js'; import * as exceptions from './icons/exceptions.js';
import * as registry from './icons/registry.js'; import * as registry from './icons/registry.js';
export {exceptions, registry}; export {CommentIcon, exceptions, registry};

328
core/icons/comment_icon.ts Normal file
View File

@@ -0,0 +1,328 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Comment');
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import {COMMENT_TYPE} from './icon_types.js';
import {Coordinate} from '../utils.js';
import * as dom from '../utils/dom.js';
import * as eventUtils from '../events/utils.js';
import {Icon} from './icon.js';
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
import type {ISerializable} from '../interfaces/i_serializable.js';
import {Rect} from '../utils/rect.js';
import * as registry from './registry.js';
import {Size} from '../utils/size.js';
import {Svg} from '../utils/svg.js';
import {TextBubble} from '../bubbles/text_bubble.js';
import {TextInputBubble} from '../bubbles/textinput_bubble.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
/** The size of the comment icon in workspace-scale units. */
const SIZE = 17;
/** The default width in workspace-scale units of the text input bubble. */
const DEFAULT_BUBBLE_WIDTH = 160;
/** The default height in workspace-scale units of the text input bubble. */
const DEFAULT_BUBBLE_HEIGHT = 80;
export class CommentIcon extends Icon implements IHasBubble, ISerializable {
/** The type string used to identify this icon. */
static readonly TYPE = COMMENT_TYPE;
/**
* The weight this icon has relative to other icons. Icons with more positive
* weight values are rendered farther toward the end of the block.
*/
static readonly WEIGHT = 3;
/** The bubble used to show editable text to the user. */
private textInputBubble: TextInputBubble | null = null;
/** The bubble used to show non-editable text to the user. */
private textBubble: TextBubble | null = null;
/** The text of this comment. */
private text = '';
/** The size of this comment (which is applied to the editable bubble). */
private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT);
/**
* The visibility of the bubble for this comment.
*
* This is used to track what the visibile state /should/ be, not necessarily
* what it currently /is/. E.g. sometimes this will be true, but the block
* hasn't been rendered yet, so the bubble will not currently be visible.
*/
private bubbleVisiblity = false;
constructor(protected readonly sourceBlock: Block) {
super(sourceBlock);
}
override getType(): string {
return CommentIcon.TYPE;
}
override initView(pointerdownListener: (e: PointerEvent) => void): void {
if (this.svgRoot) return; // Already initialized.
super.initView(pointerdownListener);
// Circle.
dom.createSvgElement(
Svg.CIRCLE,
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
this.svgRoot
);
// Can't use a real '?' text character since different browsers and
// operating systems render it differently. Body of question mark.
dom.createSvgElement(
Svg.PATH,
{
'class': 'blocklyIconSymbol',
'd':
'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
'-1.201,0.998 -1.201,1.528 -1.204,2.19z',
},
this.svgRoot
);
// Dot of question mark.
dom.createSvgElement(
Svg.RECT,
{
'class': 'blocklyIconSymbol',
'x': '6.8',
'y': '10.78',
'height': '2',
'width': '2',
},
this.svgRoot
);
}
override dispose() {
super.dispose();
this.textInputBubble?.dispose();
this.textBubble?.dispose();
}
override getWeight(): number {
return CommentIcon.WEIGHT;
}
override getSize(): Size {
return new Size(SIZE, SIZE);
}
override applyColour(): void {
super.applyColour();
const colour = (this.sourceBlock as BlockSvg).style.colourPrimary;
this.textInputBubble?.setColour(colour);
this.textBubble?.setColour(colour);
}
/**
* Updates the state of the bubble (editable / noneditable) to reflect the
* state of the bubble if the bubble is currently shown.
*/
override updateEditable(): void {
super.updateEditable();
if (this.bubbleIsVisible()) {
// Close and reopen the bubble to display the correct UI.
this.setBubbleVisible(false);
this.setBubbleVisible(true);
}
}
override onLocationChange(blockOrigin: Coordinate): void {
super.onLocationChange(blockOrigin);
const anchorLocation = this.getAnchorLocation();
this.textInputBubble?.setAnchorLocation(anchorLocation);
this.textBubble?.setAnchorLocation(anchorLocation);
}
/** Sets the text of this comment. Updates any bubbles if they are visible. */
setText(text: string) {
this.text = text;
this.textInputBubble?.setText(this.text);
this.textBubble?.setText(this.text);
}
/** Returns the text of this comment. */
getText(): string {
return this.text;
}
/**
* Sets the size of the editable bubble for this comment. Resizes the
* bubble if it is visible.
*/
setBubbleSize(size: Size) {
this.bubbleSize = size;
this.textInputBubble?.setSize(this.bubbleSize, true);
}
/** @returns the size of the editable bubble for this comment. */
getBubbleSize(): Size {
return this.bubbleSize;
}
/**
* @returns the state of the comment as a JSON serializable value if the
* comment has text. Otherwise returns null.
*/
saveState(): CommentState | null {
if (this.text) {
return {
'text': this.text,
'pinned': this.bubbleIsVisible(),
'height': this.bubbleSize.height,
'width': this.bubbleSize.width,
};
}
return null;
}
/** Applies the given state to this comment. */
loadState(state: CommentState) {
this.text = state['text'] ?? '';
this.bubbleSize = new Size(
state['width'] ?? DEFAULT_BUBBLE_WIDTH,
state['height'] ?? DEFAULT_BUBBLE_HEIGHT
);
this.bubbleVisiblity = state['pinned'] ?? false;
// Give the block a chance to be positioned and rendered before showing.
setTimeout(() => this.setBubbleVisible(this.bubbleVisiblity), 1);
}
override onClick(): void {
super.onClick();
this.setBubbleVisible(!this.bubbleIsVisible());
}
/**
* Updates the text of this comment in response to changes in the text of
* the input bubble.
*/
onTextChange(): void {
if (this.textInputBubble) {
this.text = this.textInputBubble.getText();
}
}
/**
* Updates the size of this icon in response to changes in the size of the
* input bubble.
*/
onSizeChange(): void {
if (this.textInputBubble) {
this.bubbleSize = this.textInputBubble.getSize();
}
}
bubbleIsVisible(): boolean {
return this.bubbleVisiblity;
}
setBubbleVisible(visible: boolean): void {
if (visible && (this.textBubble || this.textInputBubble)) return;
if (!visible && !(this.textBubble || this.textInputBubble)) return;
this.bubbleVisiblity = visible;
if (!this.sourceBlock.rendered || this.sourceBlock.isInFlyout) return;
if (visible) {
if (this.sourceBlock.isEditable()) {
this.showEditableBubble();
} else {
this.showNonEditableBubble();
}
this.applyColour();
} else {
this.hideBubble();
}
eventUtils.fire(
new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.sourceBlock,
visible,
'comment'
)
);
}
/**
* Shows the editable text bubble for this comment, and adds change listeners
* to update the state of this icon in response to changes in the bubble.
*/
private showEditableBubble() {
this.textInputBubble = new TextInputBubble(
this.sourceBlock.workspace as WorkspaceSvg,
this.getAnchorLocation(),
this.getBubbleOwnerRect()
);
this.textInputBubble.setText(this.getText());
this.textInputBubble.setSize(this.bubbleSize, true);
this.textInputBubble.addTextChangeListener(() => this.onTextChange());
this.textInputBubble.addSizeChangeListener(() => this.onSizeChange());
}
/** Shows the non editable text bubble for this comment. */
private showNonEditableBubble() {
this.textBubble = new TextBubble(
this.getText(),
this.sourceBlock.workspace as WorkspaceSvg,
this.getAnchorLocation(),
this.getBubbleOwnerRect()
);
}
/** Hides any open bubbles owned by this comment. */
private hideBubble() {
this.textInputBubble?.dispose();
this.textInputBubble = null;
this.textBubble?.dispose();
this.textBubble = null;
}
/**
* @returns the location the bubble should be anchored to.
* I.E. the middle of this icon.
*/
private getAnchorLocation(): Coordinate {
const midIcon = SIZE / 2;
return Coordinate.sum(
this.workspaceLocation,
new Coordinate(midIcon, midIcon)
);
}
/**
* @returns the rect the bubble should avoid overlapping.
* I.E. the block that owns this icon.
*/
private getBubbleOwnerRect(): Rect {
const bbox = (this.sourceBlock as BlockSvg).getSvgRoot().getBBox();
return new Rect(bbox.y, bbox.y + bbox.height, bbox.x, bbox.x + bbox.width);
}
}
export interface CommentState {
text?: string;
pinned?: boolean;
height?: number;
width?: number;
}
registry.register(CommentIcon.TYPE, CommentIcon);

View File

@@ -29,7 +29,7 @@ export abstract class Icon implements IIcon {
constructor(protected sourceBlock: Block) {} constructor(protected sourceBlock: Block) {}
getType(): string { getType(): string {
return 'abstract type'; throw new Error('Icons must implement getType');
} }
initView(pointerdownListener: (e: PointerEvent) => void): void { initView(pointerdownListener: (e: PointerEvent) => void): void {

14
core/icons/icon_types.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/** The type for a mutator icon. Used for registration and access. */
export const MUTATOR_TYPE = 'mutator';
/** The type for a warning icon. Used for registration and access. */
export const WARNING_TYPE = 'warning';
/** The type for a warning icon. Used for registration and access. */
export const COMMENT_TYPE = 'comment';

View File

@@ -9,7 +9,7 @@ export interface ISerializable {
* @param doFullSerialization If true, this signals that any backing data * @param doFullSerialization If true, this signals that any backing data
* structures used by this ISerializable should also be serialized. This * structures used by this ISerializable should also be serialized. This
* is used for copy-paste. * is used for copy-paste.
* @returns a JSON serializable value that records the icon's state. * @returns a JSON serializable value that records the ISerializable's state.
*/ */
saveState(doFullSerialization: boolean): any; saveState(doFullSerialization: boolean): any;

View File

@@ -16,7 +16,6 @@ import {isIcon} from '../interfaces/i_icon.js';
import {isSerializable} from '../interfaces/i_serializable.js'; import {isSerializable} from '../interfaces/i_serializable.js';
import type {ISerializer} from '../interfaces/i_serializer.js'; import type {ISerializer} from '../interfaces/i_serializer.js';
import * as registry from '../registry.js'; import * as registry from '../registry.js';
import {Size} from '../utils/size.js';
import * as utilsXml from '../utils/xml.js'; import * as utilsXml from '../utils/xml.js';
import type {Workspace} from '../workspace.js'; import type {Workspace} from '../workspace.js';
import * as Xml from '../xml.js'; import * as Xml from '../xml.js';
@@ -219,20 +218,11 @@ function saveIcons(block: Block, state: State, doFullSerialization: boolean) {
const icons = Object.create(null); const icons = Object.create(null);
for (const icon of block.getIcons()) { for (const icon of block.getIcons()) {
if (isSerializable(icon)) { if (isSerializable(icon)) {
icons[icon.getType()] = icon.saveState(doFullSerialization); const state = icon.saveState(doFullSerialization);
if (state) icons[icon.getType()] = state;
} }
} }
// TODO(#7038): Remove this logic and put it in the comment icon.
if (block.getCommentText()) {
icons['comment'] = {
'text': block.getCommentText(),
'pinned': block.commentModel.pinned,
'height': Math.round(block.commentModel.size.height),
'width': Math.round(block.commentModel.size.width),
};
}
if (Object.keys(icons).length) { if (Object.keys(icons).length) {
state['icons'] = icons; state['icons'] = icons;
} }
@@ -594,34 +584,20 @@ function loadIcons(block: Block, state: State) {
const iconTypes = Object.keys(state['icons']); const iconTypes = Object.keys(state['icons']);
for (const iconType of iconTypes) { for (const iconType of iconTypes) {
// TODO(#7038): Remove this special casing of comment..
if (iconType === 'comment') continue;
const iconState = state['icons'][iconType]; const iconState = state['icons'][iconType];
const constructor = registry.getClass(registry.Type.ICON, iconType, false); let icon = block.getIcon(iconType);
if (!constructor) throw new UnregisteredIcon(iconType, block, state); if (!icon) {
const icon = new constructor(); const constructor = registry.getClass(
block.addIcon(icon); registry.Type.ICON,
iconType,
false
);
if (!constructor) throw new UnregisteredIcon(iconType, block, state);
icon = new constructor(block);
block.addIcon(icon);
}
if (isSerializable(icon)) icon.loadState(iconState); if (isSerializable(icon)) icon.loadState(iconState);
} }
// TODO(#7038): Remove this logic and put it in the icon.
const comment = state['icons']['comment'];
if (comment) {
block.setCommentText(comment['text']);
// Load if saved. (Cleaned unnecessary attributes when in the trashcan.)
if ('pinned' in comment) {
block.commentModel.pinned = comment['pinned'];
}
if ('width' in comment && 'height' in comment) {
block.commentModel.size = new Size(comment['width'], comment['height']);
}
if (comment['pinned'] && block.rendered && !block.isInFlyout) {
// Give the block a chance to be positioned and rendered before showing.
const blockSvg = block as BlockSvg;
setTimeout(() => blockSvg.getCommentIcon()!.setVisible(true), 1);
}
}
} }
/** /**

View File

@@ -12,6 +12,8 @@ import type {BlockSvg} from './block_svg.js';
import type {Connection} from './connection.js'; import type {Connection} from './connection.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
import type {Field} from './field.js'; import type {Field} from './field.js';
import type {CommentIcon} from './icons/comment_icon.js';
import {COMMENT_TYPE} from './icons/icon_types.js';
import {inputTypes} from './inputs/input_types.js'; import {inputTypes} from './inputs/input_types.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
import {Size} from './utils/size.js'; import {Size} from './utils/size.js';
@@ -188,8 +190,9 @@ export function blockToDom(
const commentText = block.getCommentText(); const commentText = block.getCommentText();
if (commentText) { if (commentText) {
const size = block.commentModel.size; const comment = block.getIcon(COMMENT_TYPE) as CommentIcon;
const pinned = block.commentModel.pinned; const size = comment.getBubbleSize();
const pinned = comment.bubbleIsVisible();
const commentElement = utilsXml.createElement('comment'); const commentElement = utilsXml.createElement('comment');
commentElement.appendChild(utilsXml.createTextNode(commentText)); commentElement.appendChild(utilsXml.createTextNode(commentText));
@@ -720,17 +723,14 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) {
const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10); const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10);
block.setCommentText(text); block.setCommentText(text);
block.commentModel.pinned = pinned; const comment = block.getIcon(COMMENT_TYPE) as CommentIcon;
if (!isNaN(width) && !isNaN(height)) { if (!isNaN(width) && !isNaN(height)) {
block.commentModel.size = new Size(width, height); comment.setBubbleSize(new Size(width, height));
}
if (pinned && (block as BlockSvg).getCommentIcon && !block.isInFlyout) {
const blockSvg = block as BlockSvg;
setTimeout(function () {
blockSvg.getCommentIcon()!.setVisible(true);
}, 1);
} }
// Set the pinned state of the bubble.
comment.setBubbleVisible(pinned);
// Actually show the bubble after the block has been rendered.
setTimeout(() => comment.setBubbleVisible(pinned), 1);
} }
} }

View File

@@ -1457,5 +1457,15 @@
}, },
}, },
}, },
{
oldName: 'Blockly.Comment',
exports: {
Comment: {
newExport: 'CommentIcon',
oldPath: 'Blockly.Comment',
newPath: 'Blocky.icons.CommentIcon',
},
},
},
], ],
} }

View File

@@ -1235,9 +1235,21 @@ suite('Blocks', function () {
const calls = eventSpy.getCalls(); const calls = eventSpy.getCalls();
const event = calls[calls.length - 1].args[0]; const event = calls[calls.length - 1].args[0];
chai.assert.equal(event.type, eventUtils.BLOCK_CHANGE); chai.assert.equal(event.type, eventUtils.BLOCK_CHANGE);
chai.assert.equal(event.element, 'comment'); chai.assert.equal(
chai.assert.equal(event.oldValue, oldValue); event.element,
chai.assert.equal(event.newValue, newValue); 'comment',
'Expected the element to be a comment'
);
chai.assert.equal(
event.oldValue,
oldValue,
'Expected the old values to match'
);
chai.assert.equal(
event.newValue,
newValue,
'Expected the new values to match'
);
} }
function assertNoCommentEvent(eventSpy) { function assertNoCommentEvent(eventSpy) {
const calls = eventSpy.getCalls(); const calls = eventSpy.getCalls();
@@ -1318,37 +1330,23 @@ suite('Blocks', function () {
}); });
test('Set While Visible - Editable', function () { test('Set While Visible - Editable', function () {
this.block.setCommentText('test1'); this.block.setCommentText('test1');
const icon = this.block.getCommentIcon(); const icon = this.block.getIcon(Blockly.icons.CommentIcon.TYPE);
icon.setVisible(true); icon.setBubbleVisible(true);
this.block.setCommentText('test2'); this.block.setCommentText('test2');
chai.assert.equal(this.block.getCommentText(), 'test2'); chai.assert.equal(this.block.getCommentText(), 'test2');
assertCommentEvent(this.eventsFireSpy, 'test1', 'test2'); assertCommentEvent(this.eventsFireSpy, 'test1', 'test2');
chai.assert.equal(icon.textarea_.value, 'test2');
}); });
test('Set While Visible - NonEditable', function () { test('Set While Visible - NonEditable', function () {
this.block.setCommentText('test1'); this.block.setCommentText('test1');
// Restored up by call to sinon.restore() in sharedTestTeardown() // Restored up by call to sinon.restore() in sharedTestTeardown()
sinon.stub(this.block, 'isEditable').returns(false); sinon.stub(this.block, 'isEditable').returns(false);
const icon = this.block.getCommentIcon(); const icon = this.block.getIcon(Blockly.icons.CommentIcon.TYPE);
icon.setVisible(true); icon.setBubbleVisible(true);
this.block.setCommentText('test2'); this.block.setCommentText('test2');
chai.assert.equal(this.block.getCommentText(), 'test2'); chai.assert.equal(this.block.getCommentText(), 'test2');
assertCommentEvent(this.eventsFireSpy, 'test1', 'test2'); assertCommentEvent(this.eventsFireSpy, 'test1', 'test2');
chai.assert.equal(
icon.paragraphElement_.firstChild.textContent,
'test2'
);
});
test('Get Text While Editing', function () {
this.block.setCommentText('test1');
const icon = this.block.getCommentIcon();
icon.setVisible(true);
icon.textarea_.value = 'test2';
icon.textarea_.dispatchEvent(new Event('input'));
chai.assert.equal(this.block.getCommentText(), 'test2');
}); });
}); });
}); });
@@ -1687,7 +1685,7 @@ suite('Blocks', function () {
} }
const icons = block.getIcons(); const icons = block.getIcons();
for (let i = 0, icon; (icon = icons[i]); i++) { for (let i = 0, icon; (icon = icons[i]); i++) {
chai.assert.isFalse(icon.isVisible()); chai.assert.isFalse(icon.bubbleIsVisible());
} }
const input = block.getInput(Blockly.Block.COLLAPSED_INPUT_NAME); const input = block.getInput(Blockly.Block.COLLAPSED_INPUT_NAME);

View File

@@ -58,14 +58,14 @@ suite('Comment Deserialization', function () {
function assertComment(workspace, text) { function assertComment(workspace, text) {
// Show comment. // Show comment.
const block = workspace.getAllBlocks()[0]; const block = workspace.getAllBlocks()[0];
block.comment.setVisible(true); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
icon.setBubbleVisible(true);
// Check comment bubble size. // Check comment bubble size.
const comment = block.getCommentIcon(); const comment = block.getCommentIcon();
const bubbleSize = comment.getBubbleSize(); const bubbleSize = comment.getBubbleSize();
chai.assert.isNotNaN(bubbleSize.width); chai.assert.isNotNaN(bubbleSize.width);
chai.assert.isNotNaN(bubbleSize.height); chai.assert.isNotNaN(bubbleSize.height);
// Check comment text. chai.assert.equal(icon.getText(), text);
chai.assert.equal(comment.textarea_.value, text);
} }
test('Trashcan', function () { test('Trashcan', function () {
// Create block. // Create block.

View File

@@ -31,8 +31,7 @@ suite('Comments', function () {
Blockly.utils.xml.textToDom('<block type="empty_block"/>'), Blockly.utils.xml.textToDom('<block type="empty_block"/>'),
this.workspace this.workspace
); );
this.comment = new Blockly.Comment(this.block); this.comment = new Blockly.icons.CommentIcon(this.block);
this.comment.computeIconLocation();
}); });
teardown(function () { teardown(function () {
sharedTestTeardown.call(this); sharedTestTeardown.call(this);
@@ -43,21 +42,16 @@ suite('Comments', function () {
}); });
function assertEditable(comment) { function assertEditable(comment) {
chai.assert.isNotOk(comment.paragraphElement_); chai.assert.isNotOk(comment.textBubble);
chai.assert.isOk(comment.textarea_); chai.assert.isOk(comment.textInputBubble);
chai.assert.equal(comment.textarea_.value, 'test text');
} }
function assertNotEditable(comment) { function assertNotEditable(comment) {
chai.assert.isNotOk(comment.textarea_); chai.assert.isNotOk(comment.textInputBubble);
chai.assert.isOk(comment.paragraphElement_); chai.assert.isOk(comment.textBubble);
chai.assert.equal(
comment.paragraphElement_.firstChild.textContent,
'test text'
);
} }
test('Editable', function () { test('Editable', function () {
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
chai.assert.isTrue(this.comment.isVisible()); chai.assert.isTrue(this.comment.bubbleIsVisible());
assertEditable(this.comment); assertEditable(this.comment);
assertEventFired( assertEventFired(
this.eventsFireStub, this.eventsFireStub,
@@ -70,9 +64,9 @@ suite('Comments', function () {
test('Not Editable', function () { test('Not Editable', function () {
sinon.stub(this.block, 'isEditable').returns(false); sinon.stub(this.block, 'isEditable').returns(false);
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
chai.assert.isTrue(this.comment.isVisible()); chai.assert.isTrue(this.comment.bubbleIsVisible());
assertNotEditable(this.comment); assertNotEditable(this.comment);
assertEventFired( assertEventFired(
this.eventsFireStub, this.eventsFireStub,
@@ -83,12 +77,12 @@ suite('Comments', function () {
); );
}); });
test('Editable -> Not Editable', function () { test('Editable -> Not Editable', function () {
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
sinon.stub(this.block, 'isEditable').returns(false); sinon.stub(this.block, 'isEditable').returns(false);
this.comment.updateEditable(); this.comment.updateEditable();
chai.assert.isTrue(this.comment.isVisible()); chai.assert.isTrue(this.comment.bubbleIsVisible());
assertNotEditable(this.comment); assertNotEditable(this.comment);
assertEventFired( assertEventFired(
this.eventsFireStub, this.eventsFireStub,
@@ -101,12 +95,12 @@ suite('Comments', function () {
test('Not Editable -> Editable', function () { test('Not Editable -> Editable', function () {
const editableStub = sinon.stub(this.block, 'isEditable').returns(false); const editableStub = sinon.stub(this.block, 'isEditable').returns(false);
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
editableStub.returns(true); editableStub.returns(true);
this.comment.updateEditable(); this.comment.updateEditable();
chai.assert.isTrue(this.comment.isVisible()); chai.assert.isTrue(this.comment.bubbleIsVisible());
assertEditable(this.comment); assertEditable(this.comment);
assertEventFired( assertEventFired(
this.eventsFireStub, this.eventsFireStub,
@@ -130,23 +124,21 @@ suite('Comments', function () {
assertBubbleSize(comment, 80, 160); assertBubbleSize(comment, 80, 160);
} }
test('Set Size While Visible', function () { test('Set Size While Visible', function () {
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
const bubbleSizeSpy = sinon.spy(this.comment.bubble_, 'setBubbleSize');
assertBubbleSizeDefault(this.comment); assertBubbleSizeDefault(this.comment);
this.comment.setBubbleSize(100, 100); this.comment.setBubbleSize(new Blockly.utils.Size(100, 100));
assertBubbleSize(this.comment, 100, 100); assertBubbleSize(this.comment, 100, 100);
sinon.assert.calledOnce(bubbleSizeSpy);
this.comment.setVisible(false); this.comment.setBubbleVisible(false);
assertBubbleSize(this.comment, 100, 100); assertBubbleSize(this.comment, 100, 100);
}); });
test('Set Size While Invisible', function () { test('Set Size While Invisible', function () {
assertBubbleSizeDefault(this.comment); assertBubbleSizeDefault(this.comment);
this.comment.setBubbleSize(100, 100); this.comment.setBubbleSize(new Blockly.utils.Size(100, 100));
assertBubbleSize(this.comment, 100, 100); assertBubbleSize(this.comment, 100, 100);
this.comment.setVisible(true); this.comment.setBubbleVisible(true);
assertBubbleSize(this.comment, 100, 100); assertBubbleSize(this.comment, 100, 100);
}); });
}); });

View File

@@ -260,7 +260,7 @@ suite('JSO Serialization', function () {
test('Pinned', function () { test('Pinned', function () {
const block = this.workspace.newBlock('row_block'); const block = this.workspace.newBlock('row_block');
block.setCommentText('test'); block.setCommentText('test');
block.commentModel.pinned = true; block.getIcon(Blockly.icons.CommentIcon.TYPE).setBubbleVisible(true);
const jso = Blockly.serialization.blocks.save(block); const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'icons', { assertProperty(jso, 'icons', {
'comment': { 'comment': {
@@ -275,8 +275,9 @@ suite('JSO Serialization', function () {
test('Size', function () { test('Size', function () {
const block = this.workspace.newBlock('row_block'); const block = this.workspace.newBlock('row_block');
block.setCommentText('test'); block.setCommentText('test');
block.commentModel.size.height = 40; block
block.commentModel.size.width = 320; .getIcon(Blockly.icons.CommentIcon.TYPE)
.setBubbleSize(new Blockly.utils.Size(320, 40));
const jso = Blockly.serialization.blocks.save(block); const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'icons', { assertProperty(jso, 'icons', {
'comment': { 'comment': {

View File

@@ -441,7 +441,9 @@ suite('XML', function () {
}); });
test('Size', function () { test('Size', function () {
this.block.setCommentText('test text'); this.block.setCommentText('test text');
this.block.getCommentIcon().setBubbleSize(100, 200); this.block
.getCommentIcon()
.setBubbleSize(new Blockly.utils.Size(100, 200));
const xml = Blockly.Xml.blockToDom(this.block); const xml = Blockly.Xml.blockToDom(this.block);
const commentXml = xml.firstChild; const commentXml = xml.firstChild;
chai.assert.equal(commentXml.tagName, 'comment'); chai.assert.equal(commentXml.tagName, 'comment');
@@ -450,7 +452,7 @@ suite('XML', function () {
}); });
test('Pinned True', function () { test('Pinned True', function () {
this.block.setCommentText('test text'); this.block.setCommentText('test text');
this.block.getCommentIcon().setVisible(true); this.block.getCommentIcon().setBubbleVisible(true);
const xml = Blockly.Xml.blockToDom(this.block); const xml = Blockly.Xml.blockToDom(this.block);
const commentXml = xml.firstChild; const commentXml = xml.firstChild;
chai.assert.equal(commentXml.tagName, 'comment'); chai.assert.equal(commentXml.tagName, 'comment');
@@ -629,10 +631,13 @@ suite('XML', function () {
), ),
this.workspace this.workspace
); );
chai.assert.deepEqual(block.commentModel.size, { chai.assert.deepEqual(
width: 100, block.getIcon(Blockly.icons.CommentIcon.TYPE).getBubbleSize(),
height: 200, {
}); width: 100,
height: 200,
}
);
}); });
test('Pinned True', function () { test('Pinned True', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -643,7 +648,9 @@ suite('XML', function () {
), ),
this.workspace this.workspace
); );
chai.assert.isTrue(block.commentModel.pinned); chai.assert.isTrue(
block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible()
);
}); });
test('Pinned False', function () { test('Pinned False', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -654,7 +661,9 @@ suite('XML', function () {
), ),
this.workspace this.workspace
); );
chai.assert.isFalse(block.commentModel.pinned); chai.assert.isFalse(
block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible()
);
}); });
test('Pinned Undefined', function () { test('Pinned Undefined', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -665,7 +674,9 @@ suite('XML', function () {
), ),
this.workspace this.workspace
); );
chai.assert.isFalse(block.commentModel.pinned); chai.assert.isFalse(
block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible()
);
}); });
}); });
suite('Rendered', function () { suite('Rendered', function () {
@@ -686,7 +697,7 @@ suite('XML', function () {
this.workspace this.workspace
); );
chai.assert.equal(block.getCommentText(), 'test text'); chai.assert.equal(block.getCommentText(), 'test text');
chai.assert.isNotNull(block.getCommentIcon()); chai.assert.isOk(block.getCommentIcon());
}); });
test('No Text', function () { test('No Text', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -698,7 +709,7 @@ suite('XML', function () {
this.workspace this.workspace
); );
chai.assert.equal(block.getCommentText(), ''); chai.assert.equal(block.getCommentText(), '');
chai.assert.isNotNull(block.getCommentIcon()); chai.assert.isOk(block.getIcon(Blockly.icons.CommentIcon.TYPE));
}); });
test('Size', function () { test('Size', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -709,11 +720,7 @@ suite('XML', function () {
), ),
this.workspace this.workspace
); );
chai.assert.deepEqual(block.commentModel.size, { chai.assert.isOk(block.getIcon(Blockly.icons.CommentIcon.TYPE));
width: 100,
height: 200,
});
chai.assert.isNotNull(block.getCommentIcon());
chai.assert.deepEqual(block.getCommentIcon().getBubbleSize(), { chai.assert.deepEqual(block.getCommentIcon().getBubbleSize(), {
width: 100, width: 100,
height: 200, height: 200,
@@ -730,9 +737,9 @@ suite('XML', function () {
this.workspace this.workspace
); );
this.clock.runAll(); this.clock.runAll();
chai.assert.isTrue(block.commentModel.pinned); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
chai.assert.isNotNull(block.getCommentIcon()); chai.assert.isOk(icon);
chai.assert.isTrue(block.getCommentIcon().isVisible()); chai.assert.isTrue(icon.bubbleIsVisible());
}); });
test('Pinned False', function () { test('Pinned False', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -744,9 +751,9 @@ suite('XML', function () {
this.workspace this.workspace
); );
this.clock.runAll(); this.clock.runAll();
chai.assert.isFalse(block.commentModel.pinned); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
chai.assert.isNotNull(block.getCommentIcon()); chai.assert.isOk(icon);
chai.assert.isFalse(block.getCommentIcon().isVisible()); chai.assert.isFalse(icon.bubbleIsVisible());
}); });
test('Pinned Undefined', function () { test('Pinned Undefined', function () {
const block = Blockly.Xml.domToBlock( const block = Blockly.Xml.domToBlock(
@@ -758,9 +765,9 @@ suite('XML', function () {
this.workspace this.workspace
); );
this.clock.runAll(); this.clock.runAll();
chai.assert.isFalse(block.commentModel.pinned); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
chai.assert.isNotNull(block.getCommentIcon()); chai.assert.isOk(icon);
chai.assert.isFalse(block.getCommentIcon().isVisible()); chai.assert.isFalse(icon.bubbleIsVisible());
}); });
}); });
}); });
@@ -938,8 +945,9 @@ suite('XML', function () {
this.renderedWorkspace this.renderedWorkspace
); );
block.setCommentText('test text'); block.setCommentText('test text');
block.getCommentIcon().setBubbleSize(100, 100); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
block.getCommentIcon().setVisible(true); icon.setBubbleSize(new Blockly.utils.Size(100, 100));
icon.setBubbleVisible(true);
assertRoundTrip(this.renderedWorkspace, this.headlessWorkspace); assertRoundTrip(this.renderedWorkspace, this.headlessWorkspace);
}); });
}); });
@@ -950,8 +958,9 @@ suite('XML', function () {
this.headlessWorkspace this.headlessWorkspace
); );
block.setCommentText('test text'); block.setCommentText('test text');
block.commentModel.size = new Blockly.utils.Size(100, 100); const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE);
block.commentModel.pinned = true; icon.setBubbleSize(new Blockly.utils.Size(100, 100));
icon.setBubbleVisible(true);
this.clock.runAll(); this.clock.runAll();