From 50d9474db5beee286529cf2f30dfeb2868a52053 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 2 Jun 2023 09:53:05 -0700 Subject: [PATCH] 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 --- .eslintrc.js | 2 +- core/block.ts | 36 +- core/block_svg.ts | 48 +-- core/blockly.ts | 2 - core/bubbles/textinput_bubble.ts | 31 ++ core/comment.ts | 419 -------------------- core/icons.ts | 3 +- core/icons/comment_icon.ts | 328 +++++++++++++++ core/icons/icon.ts | 2 +- core/icons/icon_types.ts | 14 + core/interfaces/i_serializable.ts | 2 +- core/serialization/blocks.ts | 50 +-- core/xml.ts | 22 +- scripts/migration/renamings.json5 | 10 + tests/mocha/block_test.js | 42 +- tests/mocha/comment_deserialization_test.js | 6 +- tests/mocha/comment_test.js | 44 +- tests/mocha/jso_serialization_test.js | 7 +- tests/mocha/xml_test.js | 67 ++-- 19 files changed, 518 insertions(+), 617 deletions(-) delete mode 100644 core/comment.ts create mode 100644 core/icons/comment_icon.ts create mode 100644 core/icons/icon_types.ts diff --git a/.eslintrc.js b/.eslintrc.js index ebe6252f0..c090b6041 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -85,7 +85,7 @@ function buildTSOverride({files, tsconfig}) { // Use TS-specific rule. 'no-unused-vars': ['off'], '@typescript-eslint/no-unused-vars': [ - 'warn', + 'error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', diff --git a/core/block.ts b/core/block.ts index b14080c49..d725c3499 100644 --- a/core/block.ts +++ b/core/block.ts @@ -20,7 +20,6 @@ import './events/events_block_create.js'; import './events/events_block_delete.js'; import {Blocks} from './blocks.js'; -import type {Comment} from './comment.js'; import * as common from './common.js'; import {Connection} from './connection.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 {IIcon} from './interfaces/i_icon.js'; import type {Mutator} from './mutator.js'; +import {CommentIcon} from './icons/comment_icon.js'; import * as Tooltip from './tooltip.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; @@ -183,14 +183,6 @@ export class Block implements IASTNodeLocation, IDeletable { */ 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; isInFlyout: boolean; isInMutator: boolean; @@ -239,9 +231,6 @@ export class Block implements IASTNodeLocation, IDeletable { opt_id && !workspace.getBlockById(opt_id) ? opt_id : idGenerator.genUid(); 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 * origin; scale does not change this value. @@ -2169,7 +2158,8 @@ export class Block implements IASTNodeLocation, IDeletable { * @returns Block's comment. */ 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. */ setCommentText(text: string | null) { - if (this.commentModel.text === text) { - return; - } + const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null; + const oldText = comment?.getText() ?? null; + if (oldText === text) return; eventUtils.fire( new (eventUtils.get(eventUtils.BLOCK_CHANGE))( this, 'comment', null, - this.commentModel.text, + oldText, 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); + } } /** diff --git a/core/block_svg.ts b/core/block_svg.ts index 40fe25eb6..df3384550 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -18,7 +18,7 @@ import './events/events_selected.js'; import {Block} from './block.js'; import * as blockAnimations from './block_animations.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 {config} from './config.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 {WorkspaceSvg} from './workspace_svg.js'; import {queueRender} from './render_management.js'; +import * as deprecation from './utils/deprecation.js'; /** * Class for a block's SVG representation. @@ -109,9 +110,6 @@ export class BlockSvg /** Block's mutator icon (if any). */ mutator: Mutator | null = null; - /** Block's comment icon (if any). */ - private commentIcon_: Comment | null = null; - /** * Block's warning icon (if any). * @@ -909,44 +907,11 @@ export class BlockSvg * comment. * * @returns The comment icon attached to this block, or null. + * @deprecated Use getIcon. To be remove in v11. */ - getCommentIcon(): Comment | null { - return this.commentIcon_; - } - - /** - * 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(); - } + getCommentIcon(): CommentIcon | null { + deprecation.warn('getCommentIcon', 'v10', 'v11', 'getIcon'); + return (this.getIcon(CommentIcon.TYPE) ?? null) as CommentIcon | null; } /** @@ -1092,7 +1057,6 @@ export class BlockSvg // resolved. override getIcons(): AnyDuringMigration[] { const icons: AnyDuringMigration = [...this.icons]; - if (this.commentIcon_) icons.push(this.commentIcon_); if (this.mutator) icons.push(this.mutator); return icons; } diff --git a/core/blockly.ts b/core/blockly.ts index b09d9dfaa..4e98d483e 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -27,7 +27,6 @@ import {Bubble} from './bubble_old.js'; import {BubbleDragger} from './bubble_dragger.js'; import * as bumpObjects from './bump_objects.js'; import * as clipboard from './clipboard.js'; -import {Comment} from './comment.js'; import * as common from './common.js'; import {ComponentManager} from './component_manager.js'; import {config} from './config.js'; @@ -504,7 +503,6 @@ export {Blocks}; export {Bubble}; export {BubbleDragger}; export {CollapsibleToolboxCategory}; -export {Comment}; export {ComponentManager}; export {Connection}; export {ConnectionType}; diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index bab9d1773..64d5f542a 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -6,6 +6,7 @@ import {Bubble} from './bubble.js'; import {Coordinate} from '../utils/coordinate.js'; +import * as Css from '../css.js'; import * as dom from '../utils/dom.js'; import {Rect} from '../utils/rect.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. */ private textChangeListeners: (() => void)[] = []; + /** Functions listening for changes to the size of this bubble. */ + private sizeChangeListeners: (() => void)[] = []; + /** The text of this bubble. */ private text = ''; @@ -91,6 +95,11 @@ export class TextInputBubble extends Bubble { 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. */ private createEditor(container: SVGGElement): { inputRoot: SVGForeignObjectElement; @@ -224,6 +233,7 @@ export class TextInputBubble extends Bubble { } super.setSize(size, relayout); + this.onSizeChange(); } /** @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), false ); + this.onSizeChange(); } /** @@ -305,4 +316,24 @@ export class TextInputBubble extends Bubble { 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; +} +`); diff --git a/core/comment.ts b/core/comment.ts deleted file mode 100644 index 4cb22690f..000000000 --- a/core/comment.ts +++ /dev/null @@ -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: - - - - - * 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; -} -`); diff --git a/core/icons.ts b/core/icons.ts index cacb8a525..34b87e7e0 100644 --- a/core/icons.ts +++ b/core/icons.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {CommentIcon} from './icons/comment_icon.js'; import * as exceptions from './icons/exceptions.js'; import * as registry from './icons/registry.js'; -export {exceptions, registry}; +export {CommentIcon, exceptions, registry}; diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts new file mode 100644 index 000000000..616c54c77 --- /dev/null +++ b/core/icons/comment_icon.ts @@ -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); diff --git a/core/icons/icon.ts b/core/icons/icon.ts index b35ba286e..e1f8066e3 100644 --- a/core/icons/icon.ts +++ b/core/icons/icon.ts @@ -29,7 +29,7 @@ export abstract class Icon implements IIcon { constructor(protected sourceBlock: Block) {} getType(): string { - return 'abstract type'; + throw new Error('Icons must implement getType'); } initView(pointerdownListener: (e: PointerEvent) => void): void { diff --git a/core/icons/icon_types.ts b/core/icons/icon_types.ts new file mode 100644 index 000000000..b15af1150 --- /dev/null +++ b/core/icons/icon_types.ts @@ -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'; diff --git a/core/interfaces/i_serializable.ts b/core/interfaces/i_serializable.ts index 3a64ebec5..380a27709 100644 --- a/core/interfaces/i_serializable.ts +++ b/core/interfaces/i_serializable.ts @@ -9,7 +9,7 @@ export interface ISerializable { * @param doFullSerialization If true, this signals that any backing data * structures used by this ISerializable should also be serialized. This * 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; diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index 76d75549d..f92fc6bac 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -16,7 +16,6 @@ import {isIcon} from '../interfaces/i_icon.js'; import {isSerializable} from '../interfaces/i_serializable.js'; import type {ISerializer} from '../interfaces/i_serializer.js'; import * as registry from '../registry.js'; -import {Size} from '../utils/size.js'; import * as utilsXml from '../utils/xml.js'; import type {Workspace} from '../workspace.js'; import * as Xml from '../xml.js'; @@ -219,20 +218,11 @@ function saveIcons(block: Block, state: State, doFullSerialization: boolean) { const icons = Object.create(null); for (const icon of block.getIcons()) { 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) { state['icons'] = icons; } @@ -594,34 +584,20 @@ function loadIcons(block: Block, state: State) { const iconTypes = Object.keys(state['icons']); for (const iconType of iconTypes) { - // TODO(#7038): Remove this special casing of comment.. - if (iconType === 'comment') continue; - const iconState = state['icons'][iconType]; - const constructor = registry.getClass(registry.Type.ICON, iconType, false); - if (!constructor) throw new UnregisteredIcon(iconType, block, state); - const icon = new constructor(); - block.addIcon(icon); + let icon = block.getIcon(iconType); + if (!icon) { + const constructor = registry.getClass( + 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); } - - // 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); - } - } } /** diff --git a/core/xml.ts b/core/xml.ts index bd89f0b34..1a69027de 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -12,6 +12,8 @@ import type {BlockSvg} from './block_svg.js'; import type {Connection} from './connection.js'; import * as eventUtils from './events/utils.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 * as dom from './utils/dom.js'; import {Size} from './utils/size.js'; @@ -188,8 +190,9 @@ export function blockToDom( const commentText = block.getCommentText(); if (commentText) { - const size = block.commentModel.size; - const pinned = block.commentModel.pinned; + const comment = block.getIcon(COMMENT_TYPE) as CommentIcon; + const size = comment.getBubbleSize(); + const pinned = comment.bubbleIsVisible(); const commentElement = utilsXml.createElement('comment'); commentElement.appendChild(utilsXml.createTextNode(commentText)); @@ -720,17 +723,14 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) { const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10); block.setCommentText(text); - block.commentModel.pinned = pinned; + const comment = block.getIcon(COMMENT_TYPE) as CommentIcon; if (!isNaN(width) && !isNaN(height)) { - block.commentModel.size = new Size(width, height); - } - - if (pinned && (block as BlockSvg).getCommentIcon && !block.isInFlyout) { - const blockSvg = block as BlockSvg; - setTimeout(function () { - blockSvg.getCommentIcon()!.setVisible(true); - }, 1); + comment.setBubbleSize(new Size(width, height)); } + // 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); } } diff --git a/scripts/migration/renamings.json5 b/scripts/migration/renamings.json5 index 2ea7f3e9f..d9239fe49 100644 --- a/scripts/migration/renamings.json5 +++ b/scripts/migration/renamings.json5 @@ -1457,5 +1457,15 @@ }, }, }, + { + oldName: 'Blockly.Comment', + exports: { + Comment: { + newExport: 'CommentIcon', + oldPath: 'Blockly.Comment', + newPath: 'Blocky.icons.CommentIcon', + }, + }, + }, ], } diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 22fa3c07f..fef3c084b 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -1235,9 +1235,21 @@ suite('Blocks', function () { const calls = eventSpy.getCalls(); const event = calls[calls.length - 1].args[0]; chai.assert.equal(event.type, eventUtils.BLOCK_CHANGE); - chai.assert.equal(event.element, 'comment'); - chai.assert.equal(event.oldValue, oldValue); - chai.assert.equal(event.newValue, newValue); + chai.assert.equal( + event.element, + '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) { const calls = eventSpy.getCalls(); @@ -1318,37 +1330,23 @@ suite('Blocks', function () { }); test('Set While Visible - Editable', function () { this.block.setCommentText('test1'); - const icon = this.block.getCommentIcon(); - icon.setVisible(true); + const icon = this.block.getIcon(Blockly.icons.CommentIcon.TYPE); + icon.setBubbleVisible(true); this.block.setCommentText('test2'); chai.assert.equal(this.block.getCommentText(), 'test2'); assertCommentEvent(this.eventsFireSpy, 'test1', 'test2'); - chai.assert.equal(icon.textarea_.value, 'test2'); }); test('Set While Visible - NonEditable', function () { this.block.setCommentText('test1'); // Restored up by call to sinon.restore() in sharedTestTeardown() sinon.stub(this.block, 'isEditable').returns(false); - const icon = this.block.getCommentIcon(); - icon.setVisible(true); + const icon = this.block.getIcon(Blockly.icons.CommentIcon.TYPE); + icon.setBubbleVisible(true); this.block.setCommentText('test2'); chai.assert.equal(this.block.getCommentText(), '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(); 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); diff --git a/tests/mocha/comment_deserialization_test.js b/tests/mocha/comment_deserialization_test.js index da4c96669..42ae34954 100644 --- a/tests/mocha/comment_deserialization_test.js +++ b/tests/mocha/comment_deserialization_test.js @@ -58,14 +58,14 @@ suite('Comment Deserialization', function () { function assertComment(workspace, text) { // Show comment. const block = workspace.getAllBlocks()[0]; - block.comment.setVisible(true); + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + icon.setBubbleVisible(true); // Check comment bubble size. const comment = block.getCommentIcon(); const bubbleSize = comment.getBubbleSize(); chai.assert.isNotNaN(bubbleSize.width); chai.assert.isNotNaN(bubbleSize.height); - // Check comment text. - chai.assert.equal(comment.textarea_.value, text); + chai.assert.equal(icon.getText(), text); } test('Trashcan', function () { // Create block. diff --git a/tests/mocha/comment_test.js b/tests/mocha/comment_test.js index 41873c049..1aa05d92b 100644 --- a/tests/mocha/comment_test.js +++ b/tests/mocha/comment_test.js @@ -31,8 +31,7 @@ suite('Comments', function () { Blockly.utils.xml.textToDom(''), this.workspace ); - this.comment = new Blockly.Comment(this.block); - this.comment.computeIconLocation(); + this.comment = new Blockly.icons.CommentIcon(this.block); }); teardown(function () { sharedTestTeardown.call(this); @@ -43,21 +42,16 @@ suite('Comments', function () { }); function assertEditable(comment) { - chai.assert.isNotOk(comment.paragraphElement_); - chai.assert.isOk(comment.textarea_); - chai.assert.equal(comment.textarea_.value, 'test text'); + chai.assert.isNotOk(comment.textBubble); + chai.assert.isOk(comment.textInputBubble); } function assertNotEditable(comment) { - chai.assert.isNotOk(comment.textarea_); - chai.assert.isOk(comment.paragraphElement_); - chai.assert.equal( - comment.paragraphElement_.firstChild.textContent, - 'test text' - ); + chai.assert.isNotOk(comment.textInputBubble); + chai.assert.isOk(comment.textBubble); } test('Editable', function () { - this.comment.setVisible(true); - chai.assert.isTrue(this.comment.isVisible()); + this.comment.setBubbleVisible(true); + chai.assert.isTrue(this.comment.bubbleIsVisible()); assertEditable(this.comment); assertEventFired( this.eventsFireStub, @@ -70,9 +64,9 @@ suite('Comments', function () { test('Not Editable', function () { 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); assertEventFired( this.eventsFireStub, @@ -83,12 +77,12 @@ suite('Comments', function () { ); }); test('Editable -> Not Editable', function () { - this.comment.setVisible(true); + this.comment.setBubbleVisible(true); sinon.stub(this.block, 'isEditable').returns(false); this.comment.updateEditable(); - chai.assert.isTrue(this.comment.isVisible()); + chai.assert.isTrue(this.comment.bubbleIsVisible()); assertNotEditable(this.comment); assertEventFired( this.eventsFireStub, @@ -101,12 +95,12 @@ suite('Comments', function () { test('Not Editable -> Editable', function () { const editableStub = sinon.stub(this.block, 'isEditable').returns(false); - this.comment.setVisible(true); + this.comment.setBubbleVisible(true); editableStub.returns(true); this.comment.updateEditable(); - chai.assert.isTrue(this.comment.isVisible()); + chai.assert.isTrue(this.comment.bubbleIsVisible()); assertEditable(this.comment); assertEventFired( this.eventsFireStub, @@ -130,23 +124,21 @@ suite('Comments', function () { assertBubbleSize(comment, 80, 160); } test('Set Size While Visible', function () { - this.comment.setVisible(true); - const bubbleSizeSpy = sinon.spy(this.comment.bubble_, 'setBubbleSize'); + this.comment.setBubbleVisible(true); assertBubbleSizeDefault(this.comment); - this.comment.setBubbleSize(100, 100); + this.comment.setBubbleSize(new Blockly.utils.Size(100, 100)); assertBubbleSize(this.comment, 100, 100); - sinon.assert.calledOnce(bubbleSizeSpy); - this.comment.setVisible(false); + this.comment.setBubbleVisible(false); assertBubbleSize(this.comment, 100, 100); }); test('Set Size While Invisible', function () { assertBubbleSizeDefault(this.comment); - this.comment.setBubbleSize(100, 100); + this.comment.setBubbleSize(new Blockly.utils.Size(100, 100)); assertBubbleSize(this.comment, 100, 100); - this.comment.setVisible(true); + this.comment.setBubbleVisible(true); assertBubbleSize(this.comment, 100, 100); }); }); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 036b5380a..36583611f 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -260,7 +260,7 @@ suite('JSO Serialization', function () { test('Pinned', function () { const block = this.workspace.newBlock('row_block'); block.setCommentText('test'); - block.commentModel.pinned = true; + block.getIcon(Blockly.icons.CommentIcon.TYPE).setBubbleVisible(true); const jso = Blockly.serialization.blocks.save(block); assertProperty(jso, 'icons', { 'comment': { @@ -275,8 +275,9 @@ suite('JSO Serialization', function () { test('Size', function () { const block = this.workspace.newBlock('row_block'); block.setCommentText('test'); - block.commentModel.size.height = 40; - block.commentModel.size.width = 320; + block + .getIcon(Blockly.icons.CommentIcon.TYPE) + .setBubbleSize(new Blockly.utils.Size(320, 40)); const jso = Blockly.serialization.blocks.save(block); assertProperty(jso, 'icons', { 'comment': { diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js index f381235bd..085984ed7 100644 --- a/tests/mocha/xml_test.js +++ b/tests/mocha/xml_test.js @@ -441,7 +441,9 @@ suite('XML', function () { }); test('Size', function () { 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 commentXml = xml.firstChild; chai.assert.equal(commentXml.tagName, 'comment'); @@ -450,7 +452,7 @@ suite('XML', function () { }); test('Pinned True', function () { this.block.setCommentText('test text'); - this.block.getCommentIcon().setVisible(true); + this.block.getCommentIcon().setBubbleVisible(true); const xml = Blockly.Xml.blockToDom(this.block); const commentXml = xml.firstChild; chai.assert.equal(commentXml.tagName, 'comment'); @@ -629,10 +631,13 @@ suite('XML', function () { ), this.workspace ); - chai.assert.deepEqual(block.commentModel.size, { - width: 100, - height: 200, - }); + chai.assert.deepEqual( + block.getIcon(Blockly.icons.CommentIcon.TYPE).getBubbleSize(), + { + width: 100, + height: 200, + } + ); }); test('Pinned True', function () { const block = Blockly.Xml.domToBlock( @@ -643,7 +648,9 @@ suite('XML', function () { ), this.workspace ); - chai.assert.isTrue(block.commentModel.pinned); + chai.assert.isTrue( + block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible() + ); }); test('Pinned False', function () { const block = Blockly.Xml.domToBlock( @@ -654,7 +661,9 @@ suite('XML', function () { ), this.workspace ); - chai.assert.isFalse(block.commentModel.pinned); + chai.assert.isFalse( + block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible() + ); }); test('Pinned Undefined', function () { const block = Blockly.Xml.domToBlock( @@ -665,7 +674,9 @@ suite('XML', function () { ), this.workspace ); - chai.assert.isFalse(block.commentModel.pinned); + chai.assert.isFalse( + block.getIcon(Blockly.icons.CommentIcon.TYPE).bubbleIsVisible() + ); }); }); suite('Rendered', function () { @@ -686,7 +697,7 @@ suite('XML', function () { this.workspace ); chai.assert.equal(block.getCommentText(), 'test text'); - chai.assert.isNotNull(block.getCommentIcon()); + chai.assert.isOk(block.getCommentIcon()); }); test('No Text', function () { const block = Blockly.Xml.domToBlock( @@ -698,7 +709,7 @@ suite('XML', function () { this.workspace ); chai.assert.equal(block.getCommentText(), ''); - chai.assert.isNotNull(block.getCommentIcon()); + chai.assert.isOk(block.getIcon(Blockly.icons.CommentIcon.TYPE)); }); test('Size', function () { const block = Blockly.Xml.domToBlock( @@ -709,11 +720,7 @@ suite('XML', function () { ), this.workspace ); - chai.assert.deepEqual(block.commentModel.size, { - width: 100, - height: 200, - }); - chai.assert.isNotNull(block.getCommentIcon()); + chai.assert.isOk(block.getIcon(Blockly.icons.CommentIcon.TYPE)); chai.assert.deepEqual(block.getCommentIcon().getBubbleSize(), { width: 100, height: 200, @@ -730,9 +737,9 @@ suite('XML', function () { this.workspace ); this.clock.runAll(); - chai.assert.isTrue(block.commentModel.pinned); - chai.assert.isNotNull(block.getCommentIcon()); - chai.assert.isTrue(block.getCommentIcon().isVisible()); + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + chai.assert.isOk(icon); + chai.assert.isTrue(icon.bubbleIsVisible()); }); test('Pinned False', function () { const block = Blockly.Xml.domToBlock( @@ -744,9 +751,9 @@ suite('XML', function () { this.workspace ); this.clock.runAll(); - chai.assert.isFalse(block.commentModel.pinned); - chai.assert.isNotNull(block.getCommentIcon()); - chai.assert.isFalse(block.getCommentIcon().isVisible()); + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + chai.assert.isOk(icon); + chai.assert.isFalse(icon.bubbleIsVisible()); }); test('Pinned Undefined', function () { const block = Blockly.Xml.domToBlock( @@ -758,9 +765,9 @@ suite('XML', function () { this.workspace ); this.clock.runAll(); - chai.assert.isFalse(block.commentModel.pinned); - chai.assert.isNotNull(block.getCommentIcon()); - chai.assert.isFalse(block.getCommentIcon().isVisible()); + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + chai.assert.isOk(icon); + chai.assert.isFalse(icon.bubbleIsVisible()); }); }); }); @@ -938,8 +945,9 @@ suite('XML', function () { this.renderedWorkspace ); block.setCommentText('test text'); - block.getCommentIcon().setBubbleSize(100, 100); - block.getCommentIcon().setVisible(true); + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + icon.setBubbleSize(new Blockly.utils.Size(100, 100)); + icon.setBubbleVisible(true); assertRoundTrip(this.renderedWorkspace, this.headlessWorkspace); }); }); @@ -950,8 +958,9 @@ suite('XML', function () { this.headlessWorkspace ); block.setCommentText('test text'); - block.commentModel.size = new Blockly.utils.Size(100, 100); - block.commentModel.pinned = true; + const icon = block.getIcon(Blockly.icons.CommentIcon.TYPE); + icon.setBubbleSize(new Blockly.utils.Size(100, 100)); + icon.setBubbleVisible(true); this.clock.runAll();