mirror of
https://github.com/google/blockly.git
synced 2026-01-07 09:00:11 +01:00
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:
@@ -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': '^_',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|||||||
419
core/comment.ts
419
core/comment.ts
@@ -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;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
@@ -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
328
core/icons/comment_icon.ts
Normal 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);
|
||||||
@@ -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
14
core/icons/icon_types.ts
Normal 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';
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
22
core/xml.ts
22
core/xml.ts
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1457,5 +1457,15 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
oldName: 'Blockly.Comment',
|
||||||
|
exports: {
|
||||||
|
Comment: {
|
||||||
|
newExport: 'CommentIcon',
|
||||||
|
oldPath: 'Blockly.Comment',
|
||||||
|
newPath: 'Blocky.icons.CommentIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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': {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user