mirror of
https://github.com/google/blockly.git
synced 2026-01-09 18:10:08 +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.
|
||||
'no-unused-vars': ['off'],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
'error',
|
||||
{
|
||||
'argsIgnorePattern': '^_',
|
||||
'varsIgnorePattern': '^_',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
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};
|
||||
|
||||
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) {}
|
||||
|
||||
getType(): string {
|
||||
return 'abstract type';
|
||||
throw new Error('Icons must implement getType');
|
||||
}
|
||||
|
||||
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
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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 * 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -31,8 +31,7 @@ suite('Comments', function () {
|
||||
Blockly.utils.xml.textToDom('<block type="empty_block"/>'),
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user