mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
feat: add comment view (for workspace comments, and block comments for partners) (#7914)
* feat: add basic comment view * feat: add icons to comment * chore: add text area to comment view * feat: add getting size * feat: add collapsing comment view * feat: add setting editability * feat: add location and text hooks. * feat: add changing the size * feat: resizing * feat: add collapsing * feat: add disposing * feat: add cursors * feat: add moving to the front * chore: split construction into subprocedures * chore: split resizing into subprocedures * feat: handle RTL * chore: add doc comments throughout file * chore: reduce css specificity where possible * chore: format * feat: add remove change listener methods * chore: add tests for listeners * feat: add disposing accessors * chore: add coordinate system notes * chore: add issues to TODOs where possible * chore: remove suite.only
This commit is contained in:
@@ -36,6 +36,7 @@ import {ConnectionType} from './connection_type.js';
|
|||||||
import * as ContextMenu from './contextmenu.js';
|
import * as ContextMenu from './contextmenu.js';
|
||||||
import * as ContextMenuItems from './contextmenu_items.js';
|
import * as ContextMenuItems from './contextmenu_items.js';
|
||||||
import {ContextMenuRegistry} from './contextmenu_registry.js';
|
import {ContextMenuRegistry} from './contextmenu_registry.js';
|
||||||
|
import * as comments from './comments.js';
|
||||||
import * as Css from './css.js';
|
import * as Css from './css.js';
|
||||||
import {DeleteArea} from './delete_area.js';
|
import {DeleteArea} from './delete_area.js';
|
||||||
import * as dialog from './dialog.js';
|
import * as dialog from './dialog.js';
|
||||||
@@ -480,6 +481,7 @@ export {ConnectionType};
|
|||||||
export {ConnectionChecker};
|
export {ConnectionChecker};
|
||||||
export {ConnectionDB};
|
export {ConnectionDB};
|
||||||
export {ContextMenuRegistry};
|
export {ContextMenuRegistry};
|
||||||
|
export {comments};
|
||||||
export {Cursor};
|
export {Cursor};
|
||||||
export {DeleteArea};
|
export {DeleteArea};
|
||||||
export {DragTarget};
|
export {DragTarget};
|
||||||
|
|||||||
7
core/comments.ts
Normal file
7
core/comments.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {CommentView} from './comments/comment_view.js';
|
||||||
741
core/comments/comment_view.ts
Normal file
741
core/comments/comment_view.ts
Normal file
@@ -0,0 +1,741 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
|
||||||
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||||
|
import * as dom from '../utils/dom.js';
|
||||||
|
import {Svg} from '../utils/svg.js';
|
||||||
|
import * as layers from '../layers.js';
|
||||||
|
import * as css from '../css.js';
|
||||||
|
import {Coordinate, Size, browserEvents} from '../utils.js';
|
||||||
|
import * as touch from '../touch.js';
|
||||||
|
|
||||||
|
const MIN_SIZE = new Size(100, 60);
|
||||||
|
export class CommentView implements IRenderedElement {
|
||||||
|
/** The root group element of the comment view. */
|
||||||
|
private svgRoot: SVGGElement;
|
||||||
|
|
||||||
|
/** The rect background for the top bar. */
|
||||||
|
private topBar: SVGRectElement;
|
||||||
|
|
||||||
|
/** The delete icon that goes in the top bar. */
|
||||||
|
private deleteIcon: SVGImageElement;
|
||||||
|
|
||||||
|
/** The foldout icon that goes in the top bar. */
|
||||||
|
private foldoutIcon: SVGImageElement;
|
||||||
|
|
||||||
|
/** The text element that goes in the top bar. */
|
||||||
|
private textPreview: SVGTextElement;
|
||||||
|
|
||||||
|
/** The actual text node in the text preview. */
|
||||||
|
private textPreviewNode: Text;
|
||||||
|
|
||||||
|
/** The resize handle element. */
|
||||||
|
private resizeHandle: SVGImageElement;
|
||||||
|
|
||||||
|
/** The foreignObject containing the HTML text area. */
|
||||||
|
private foreignObject: SVGForeignObjectElement;
|
||||||
|
|
||||||
|
/** The text area where the user can type. */
|
||||||
|
private textArea: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
/** The current size of the comment in workspace units. */
|
||||||
|
private size: Size = new Size(120, 100);
|
||||||
|
|
||||||
|
/** Whether the comment is collapsed or not. */
|
||||||
|
private collapsed: boolean = false;
|
||||||
|
|
||||||
|
/** Whether the comment is editable or not. */
|
||||||
|
private editable: boolean = true;
|
||||||
|
|
||||||
|
/** The current location of the comment in workspace coordinates. */
|
||||||
|
private location: Coordinate = new Coordinate(0, 0);
|
||||||
|
|
||||||
|
/** The current text of the comment. Updates on text area change. */
|
||||||
|
private text: string = '';
|
||||||
|
|
||||||
|
/** Listeners for changes to text. */
|
||||||
|
private textChangeListeners: Array<
|
||||||
|
(oldText: string, newText: string) => void
|
||||||
|
> = [];
|
||||||
|
|
||||||
|
/** Listeners for changes to size. */
|
||||||
|
private sizeChangeListeners: Array<(oldSize: Size, newSize: Size) => void> =
|
||||||
|
[];
|
||||||
|
|
||||||
|
/** Listeners for disposal. */
|
||||||
|
private disposeListeners: Array<() => void> = [];
|
||||||
|
|
||||||
|
/** Listeners for collapsing. */
|
||||||
|
private collapseChangeListeners: Array<(newCollapse: boolean) => void> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event data for the pointer up event on the resize handle. Used to
|
||||||
|
* unregister the listener.
|
||||||
|
*/
|
||||||
|
private resizePointerUpListener: browserEvents.Data | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event data for the pointer move event on the resize handle. Used to
|
||||||
|
* unregister the listener.
|
||||||
|
*/
|
||||||
|
private resizePointerMoveListener: browserEvents.Data | null = null;
|
||||||
|
|
||||||
|
/** Whether this comment view is currently being disposed or not. */
|
||||||
|
private disposing = false;
|
||||||
|
|
||||||
|
/** Whether this comment view has been disposed or not. */
|
||||||
|
private disposed = false;
|
||||||
|
|
||||||
|
constructor(private readonly workspace: WorkspaceSvg) {
|
||||||
|
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||||
|
'class': 'blocklyComment blocklyEditable',
|
||||||
|
});
|
||||||
|
|
||||||
|
({
|
||||||
|
topBar: this.topBar,
|
||||||
|
deleteIcon: this.deleteIcon,
|
||||||
|
foldoutIcon: this.foldoutIcon,
|
||||||
|
textPreview: this.textPreview,
|
||||||
|
textPreviewNode: this.textPreviewNode,
|
||||||
|
} = this.createTopBar(this.svgRoot, workspace));
|
||||||
|
|
||||||
|
({foreignObject: this.foreignObject, textArea: this.textArea} =
|
||||||
|
this.createTextArea(this.svgRoot));
|
||||||
|
|
||||||
|
this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace);
|
||||||
|
|
||||||
|
// TODO: Remove this comment before merging.
|
||||||
|
// I think we want comments to exist on the same layer as blocks.
|
||||||
|
workspace.getLayerManager()?.append(this, layers.BLOCK);
|
||||||
|
|
||||||
|
// Set size to the default size.
|
||||||
|
this.setSize(this.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the top bar and the elements visually within it.
|
||||||
|
* Registers event listeners.
|
||||||
|
*/
|
||||||
|
private createTopBar(
|
||||||
|
svgRoot: SVGGElement,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
): {
|
||||||
|
topBar: SVGRectElement;
|
||||||
|
deleteIcon: SVGImageElement;
|
||||||
|
foldoutIcon: SVGImageElement;
|
||||||
|
textPreview: SVGTextElement;
|
||||||
|
textPreviewNode: Text;
|
||||||
|
} {
|
||||||
|
const topBar = dom.createSvgElement(
|
||||||
|
Svg.RECT,
|
||||||
|
{
|
||||||
|
'class': 'blocklyCommentTopbar',
|
||||||
|
'x': 0,
|
||||||
|
'y': 0,
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
// TODO: Before merging, does this mean to override an individual image,
|
||||||
|
// folks need to replace the whole media folder?
|
||||||
|
const deleteIcon = dom.createSvgElement(
|
||||||
|
Svg.IMAGE,
|
||||||
|
{
|
||||||
|
'class': 'blocklyDeleteIcon',
|
||||||
|
'href': `${workspace.options.pathToMedia}delete-icon.svg`,
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
const foldoutIcon = dom.createSvgElement(
|
||||||
|
Svg.IMAGE,
|
||||||
|
{
|
||||||
|
'class': 'blocklyFoldoutIcon',
|
||||||
|
'href': `${workspace.options.pathToMedia}arrow-dropdown.svg`,
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
const textPreview = dom.createSvgElement(
|
||||||
|
Svg.TEXT,
|
||||||
|
{
|
||||||
|
'class': 'blocklyCommentPreview blocklyCommentText blocklyText',
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
const textPreviewNode = document.createTextNode('');
|
||||||
|
textPreview.appendChild(textPreviewNode);
|
||||||
|
|
||||||
|
// TODO(toychest): Triggering this on pointerdown means that we can't start
|
||||||
|
// drags on the foldout icon. We need to open up the gesture system
|
||||||
|
// to fix this.
|
||||||
|
browserEvents.conditionalBind(
|
||||||
|
foldoutIcon,
|
||||||
|
'pointerdown',
|
||||||
|
this,
|
||||||
|
this.onFoldoutDown,
|
||||||
|
);
|
||||||
|
browserEvents.conditionalBind(
|
||||||
|
deleteIcon,
|
||||||
|
'pointerdown',
|
||||||
|
this,
|
||||||
|
this.onDeleteDown,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {topBar, deleteIcon, foldoutIcon, textPreview, textPreviewNode};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the text area where users can type. Registers event listeners.
|
||||||
|
*/
|
||||||
|
private createTextArea(svgRoot: SVGGElement): {
|
||||||
|
foreignObject: SVGForeignObjectElement;
|
||||||
|
textArea: HTMLTextAreaElement;
|
||||||
|
} {
|
||||||
|
const foreignObject = dom.createSvgElement(
|
||||||
|
Svg.FOREIGNOBJECT,
|
||||||
|
{
|
||||||
|
'class': 'blocklyCommentForeignObject',
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
const body = document.createElementNS(dom.HTML_NS, 'body');
|
||||||
|
body.setAttribute('xmlns', dom.HTML_NS);
|
||||||
|
body.className = 'blocklyMinimalBody';
|
||||||
|
const textArea = document.createElementNS(
|
||||||
|
dom.HTML_NS,
|
||||||
|
'textarea',
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
dom.addClass(textArea, 'blocklyCommentText');
|
||||||
|
dom.addClass(textArea, 'blocklyTextarea');
|
||||||
|
dom.addClass(textArea, 'blocklyText');
|
||||||
|
body.appendChild(textArea);
|
||||||
|
foreignObject.appendChild(body);
|
||||||
|
|
||||||
|
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
|
||||||
|
|
||||||
|
return {foreignObject, textArea};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates the DOM elements for the comment resize handle. */
|
||||||
|
private createResizeHandle(
|
||||||
|
svgRoot: SVGGElement,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
): SVGImageElement {
|
||||||
|
const resizeHandle = dom.createSvgElement(
|
||||||
|
Svg.IMAGE,
|
||||||
|
{
|
||||||
|
'class': 'blocklyResizeHandle',
|
||||||
|
'href': `${workspace.options.pathToMedia}resize-handle.svg`,
|
||||||
|
},
|
||||||
|
svgRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
browserEvents.conditionalBind(
|
||||||
|
resizeHandle,
|
||||||
|
'pointerdown',
|
||||||
|
this,
|
||||||
|
this.onResizePointerDown,
|
||||||
|
);
|
||||||
|
|
||||||
|
return resizeHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the root SVG group element of the comment view. */
|
||||||
|
getSvgRoot(): SVGGElement {
|
||||||
|
return this.svgRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current size of the comment in workspace units. */
|
||||||
|
getSize(): Size {
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the size of the comment in workspace units, and updates the view
|
||||||
|
* elements to reflect the new size.
|
||||||
|
*/
|
||||||
|
setSize(size: Size) {
|
||||||
|
size = new Size(
|
||||||
|
Math.max(size.width, MIN_SIZE.width),
|
||||||
|
Math.max(size.height, MIN_SIZE.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldSize = this.size;
|
||||||
|
this.size = size;
|
||||||
|
const topBarSize = this.topBar.getBBox();
|
||||||
|
const deleteSize = this.deleteIcon.getBBox();
|
||||||
|
const foldoutSize = this.foldoutIcon.getBBox();
|
||||||
|
const textPreviewSize = this.textPreview.getBBox();
|
||||||
|
const resizeSize = this.resizeHandle.getBBox();
|
||||||
|
|
||||||
|
this.svgRoot.setAttribute('height', `${size.height}`);
|
||||||
|
this.svgRoot.setAttribute('width', `${size.width}`);
|
||||||
|
|
||||||
|
this.topBar.setAttribute('width', `${size.width}`);
|
||||||
|
|
||||||
|
this.updateTextAreaSize(size, topBarSize);
|
||||||
|
this.updateDeleteIconPosition(size, topBarSize, deleteSize);
|
||||||
|
this.updateFoldoutIconPosition(topBarSize, foldoutSize);
|
||||||
|
this.updateTextPreviewSize(
|
||||||
|
size,
|
||||||
|
topBarSize,
|
||||||
|
textPreviewSize,
|
||||||
|
deleteSize,
|
||||||
|
resizeSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.resizeHandle.setAttribute('x', `${size.width - resizeSize.width}`);
|
||||||
|
this.resizeHandle.setAttribute('y', `${size.height - resizeSize.height}`);
|
||||||
|
|
||||||
|
this.onSizeChange(oldSize, this.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the size of the text area elements to reflect the new size. */
|
||||||
|
private updateTextAreaSize(size: Size, topBarSize: Size) {
|
||||||
|
this.foreignObject.setAttribute(
|
||||||
|
'height',
|
||||||
|
`${size.height - topBarSize.height}`,
|
||||||
|
);
|
||||||
|
this.foreignObject.setAttribute('width', `${size.width}`);
|
||||||
|
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
|
||||||
|
if (this.workspace.RTL) {
|
||||||
|
this.foreignObject.setAttribute('x', `${-size.width}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the position of the delete icon elements to reflect the new size.
|
||||||
|
*/
|
||||||
|
private updateDeleteIconPosition(
|
||||||
|
size: Size,
|
||||||
|
topBarSize: Size,
|
||||||
|
deleteSize: Size,
|
||||||
|
) {
|
||||||
|
const deleteMargin = (topBarSize.height - deleteSize.height) / 2;
|
||||||
|
this.deleteIcon.setAttribute('y', `${deleteMargin}`);
|
||||||
|
this.deleteIcon.setAttribute(
|
||||||
|
'x',
|
||||||
|
`${size.width - deleteSize.width - deleteMargin}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the position of the foldout icon elements to reflect the new size.
|
||||||
|
*/
|
||||||
|
private updateFoldoutIconPosition(topBarSize: Size, foldoutSize: Size) {
|
||||||
|
const foldoutMargin = (topBarSize.height - foldoutSize.height) / 2;
|
||||||
|
this.foldoutIcon.setAttribute('y', `${foldoutMargin}`);
|
||||||
|
this.foldoutIcon.setAttribute('x', `${foldoutMargin}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the size and position of the text preview elements to reflect the new size.
|
||||||
|
*/
|
||||||
|
private updateTextPreviewSize(
|
||||||
|
size: Size,
|
||||||
|
topBarSize: Size,
|
||||||
|
textPreviewSize: Size,
|
||||||
|
deleteSize: Size,
|
||||||
|
foldoutSize: Size,
|
||||||
|
) {
|
||||||
|
const textPreviewMargin = (topBarSize.height - textPreviewSize.height) / 2;
|
||||||
|
const deleteMargin = (topBarSize.height - deleteSize.height) / 2;
|
||||||
|
const foldoutMargin = (topBarSize.height - foldoutSize.height) / 2;
|
||||||
|
|
||||||
|
const textPreviewWidth =
|
||||||
|
size.width -
|
||||||
|
foldoutSize.width -
|
||||||
|
foldoutMargin * 2 -
|
||||||
|
deleteSize.width -
|
||||||
|
deleteMargin * 2;
|
||||||
|
this.textPreview.setAttribute(
|
||||||
|
'x',
|
||||||
|
`${
|
||||||
|
foldoutSize.width + foldoutMargin * 2 * (this.workspace.RTL ? -1 : 1)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
this.textPreview.setAttribute(
|
||||||
|
'y',
|
||||||
|
`${textPreviewMargin + textPreviewSize.height / 2}`,
|
||||||
|
);
|
||||||
|
this.textPreview.setAttribute('width', `${textPreviewWidth}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers listeners when the size of the comment changes, either
|
||||||
|
* progrmatically or manually by the user.
|
||||||
|
*/
|
||||||
|
private onSizeChange(oldSize: Size, newSize: Size) {
|
||||||
|
// Loop through listeners backwards in case they remove themselves.
|
||||||
|
for (let i = this.sizeChangeListeners.length - 1; i >= 0; i--) {
|
||||||
|
this.sizeChangeListeners[i](oldSize, newSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback that listens for size changes.
|
||||||
|
*
|
||||||
|
* @param listener Receives callbacks when the size of the comment changes.
|
||||||
|
* The new and old size are in workspace units.
|
||||||
|
*/
|
||||||
|
addSizeChangeListener(listener: (oldSize: Size, newSize: Size) => void) {
|
||||||
|
this.sizeChangeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the given listener from the list of size change listeners. */
|
||||||
|
removeSizeChangeListener(listener: () => void) {
|
||||||
|
this.sizeChangeListeners.splice(
|
||||||
|
this.sizeChangeListeners.indexOf(listener),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles starting an interaction with the resize handle to resize the
|
||||||
|
* comment.
|
||||||
|
*/
|
||||||
|
private onResizePointerDown(e: PointerEvent) {
|
||||||
|
this.bringToFront();
|
||||||
|
if (browserEvents.isRightButton(e)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(#7926): Move this into a utils file.
|
||||||
|
this.workspace.startDrag(
|
||||||
|
e,
|
||||||
|
new Coordinate(
|
||||||
|
this.workspace.RTL ? -this.getSize().width : this.getSize().width,
|
||||||
|
this.getSize().height,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.resizePointerUpListener = browserEvents.conditionalBind(
|
||||||
|
document,
|
||||||
|
'pointerup',
|
||||||
|
this,
|
||||||
|
this.onResizePointerUp,
|
||||||
|
);
|
||||||
|
this.resizePointerMoveListener = browserEvents.conditionalBind(
|
||||||
|
document,
|
||||||
|
'pointermove',
|
||||||
|
this,
|
||||||
|
this.onResizePointerMove,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.workspace.hideChaff();
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ends an interaction with the resize handle. */
|
||||||
|
private onResizePointerUp(_e: PointerEvent) {
|
||||||
|
touch.clearTouchIdentifier();
|
||||||
|
if (this.resizePointerUpListener) {
|
||||||
|
browserEvents.unbind(this.resizePointerUpListener);
|
||||||
|
this.resizePointerUpListener = null;
|
||||||
|
}
|
||||||
|
if (this.resizePointerMoveListener) {
|
||||||
|
browserEvents.unbind(this.resizePointerMoveListener);
|
||||||
|
this.resizePointerMoveListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resizes the comment in response to a drag on the resize handle. */
|
||||||
|
private onResizePointerMove(e: PointerEvent) {
|
||||||
|
// TODO(#7926): Move this into a utils file.
|
||||||
|
const delta = this.workspace.moveDrag(e);
|
||||||
|
this.setSize(new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the comment is currently collapsed. */
|
||||||
|
isCollapsed(): boolean {
|
||||||
|
return this.collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets whether the comment is currently collapsed or not. */
|
||||||
|
setCollapsed(collapsed: boolean) {
|
||||||
|
this.collapsed = collapsed;
|
||||||
|
if (collapsed) {
|
||||||
|
dom.addClass(this.svgRoot, 'blocklyCollapsed');
|
||||||
|
} else {
|
||||||
|
dom.removeClass(this.svgRoot, 'blocklyCollapsed');
|
||||||
|
}
|
||||||
|
// Repositions resize handle and such.
|
||||||
|
this.setSize(this.size);
|
||||||
|
this.onCollapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers listeners when the collapsed-ness of the comment changes, either
|
||||||
|
* progrmatically or manually by the user.
|
||||||
|
*/
|
||||||
|
private onCollapse() {
|
||||||
|
// Loop through listeners backwards in case they remove themselves.
|
||||||
|
for (let i = this.collapseChangeListeners.length - 1; i >= 0; i--) {
|
||||||
|
this.collapseChangeListeners[i](this.collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers a callback that listens for collapsed-ness changes. */
|
||||||
|
addOnCollapseListener(listener: (newCollapse: boolean) => void) {
|
||||||
|
this.collapseChangeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the given listener from the list of on collapse listeners. */
|
||||||
|
removeOnCollapseListener(listener: () => void) {
|
||||||
|
this.collapseChangeListeners.splice(
|
||||||
|
this.collapseChangeListeners.indexOf(listener),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the collapsedness of the block when we receive a pointer down
|
||||||
|
* event on the foldout icon.
|
||||||
|
*/
|
||||||
|
private onFoldoutDown(e: PointerEvent) {
|
||||||
|
this.bringToFront();
|
||||||
|
if (browserEvents.isRightButton(e)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCollapsed(!this.collapsed);
|
||||||
|
|
||||||
|
this.workspace.hideChaff();
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the comment is currently editable. */
|
||||||
|
isEditable(): boolean {
|
||||||
|
return this.editable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the editability of the comment. */
|
||||||
|
setEditable(editable: boolean) {
|
||||||
|
this.editable = editable;
|
||||||
|
if (this.editable) {
|
||||||
|
dom.addClass(this.svgRoot, 'blocklyEditable');
|
||||||
|
dom.removeClass(this.svgRoot, 'blocklyReadonly');
|
||||||
|
this.textArea.removeAttribute('readonly');
|
||||||
|
} else {
|
||||||
|
dom.removeClass(this.svgRoot, 'blocklyEditable');
|
||||||
|
dom.addClass(this.svgRoot, 'blocklyReadonly');
|
||||||
|
this.textArea.setAttribute('readonly', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current location of the comment in workspace coordinates. */
|
||||||
|
getRelativeToSurfaceXY(): Coordinate {
|
||||||
|
return this.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the comment view to the given location.
|
||||||
|
*
|
||||||
|
* @param location The location to move to in workspace coordinates.
|
||||||
|
*/
|
||||||
|
moveTo(location: Coordinate) {
|
||||||
|
this.location = location;
|
||||||
|
this.svgRoot.setAttribute(
|
||||||
|
'transform',
|
||||||
|
`translate(${location.x}, ${location.y})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retursn the current text of the comment. */
|
||||||
|
getText() {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the current text of the comment. */
|
||||||
|
setText(text: string) {
|
||||||
|
this.textArea.value = text;
|
||||||
|
this.onTextChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers a callback that listens for text changes. */
|
||||||
|
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
||||||
|
this.textChangeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the given listener from the list of text change listeners. */
|
||||||
|
removeTextChangeListener(listener: () => void) {
|
||||||
|
this.textChangeListeners.splice(
|
||||||
|
this.textChangeListeners.indexOf(listener),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers listeners when the text of the comment changes, either
|
||||||
|
* progrmatically or manually by the user.
|
||||||
|
*/
|
||||||
|
private onTextChange() {
|
||||||
|
const oldText = this.text;
|
||||||
|
this.text = this.textArea.value;
|
||||||
|
this.textPreviewNode.textContent = this.truncateText(this.text);
|
||||||
|
// Loop through listeners backwards in case they remove themselves.
|
||||||
|
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
|
||||||
|
this.textChangeListeners[i](oldText, this.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Truncates the text to fit within the top view. */
|
||||||
|
private truncateText(text: string): string {
|
||||||
|
return text.length >= 12 ? `${text.substring(0, 9)}...` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Brings the workspace comment to the front of its layer. */
|
||||||
|
private bringToFront() {
|
||||||
|
const parent = this.svgRoot.parentNode;
|
||||||
|
const childNodes = parent!.childNodes;
|
||||||
|
// Avoid moving the comment if it's already at the bottom.
|
||||||
|
if (childNodes[childNodes.length - 1] !== this.svgRoot) {
|
||||||
|
parent!.appendChild(this.svgRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles disposing of the comment when we get a pointer down event on the
|
||||||
|
* delete icon.
|
||||||
|
*/
|
||||||
|
private onDeleteDown(e: PointerEvent) {
|
||||||
|
if (browserEvents.isRightButton(e)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispose();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disposes of this comment view. */
|
||||||
|
dispose() {
|
||||||
|
this.disposing = true;
|
||||||
|
dom.removeNode(this.svgRoot);
|
||||||
|
// Loop through listeners backwards in case they remove themselves.
|
||||||
|
for (let i = this.disposeListeners.length - 1; i >= 0; i--) {
|
||||||
|
this.disposeListeners[i]();
|
||||||
|
}
|
||||||
|
this.disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether this comment view has been disposed or not. */
|
||||||
|
isDisposed(): boolean {
|
||||||
|
return this.disposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this comment view is currently being disposed or has
|
||||||
|
* already been disposed.
|
||||||
|
*/
|
||||||
|
isDeadOrDying(): boolean {
|
||||||
|
return this.disposing || this.disposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers a callback that listens for disposal of this view. */
|
||||||
|
addDisposeListener(listener: () => void) {
|
||||||
|
this.disposeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the given listener from the list of disposal listeners. */
|
||||||
|
removeDisposeListener(listener: () => void) {
|
||||||
|
this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
css.register(`
|
||||||
|
.blocklyWorkspace {
|
||||||
|
--commentFillColour: #FFFCC7;
|
||||||
|
--commentBorderColour: #F2E49B;
|
||||||
|
--commentIconColour: #1A1A1A
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyComment .blocklyTextarea {
|
||||||
|
background-color: var(--commentFillColour);
|
||||||
|
border: 1px solid var(--commentBorderColour);
|
||||||
|
outline: 0;
|
||||||
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyReadonly.blocklyComment .blocklyTextarea {
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyDeleteIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: none;
|
||||||
|
fill: var(--commentIconColour);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyFoldoutIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: var(--commentIconColour);
|
||||||
|
transform-origin: 12px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.blocklyResizeHandle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
stroke: var(--commentIconColour);
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyCommentTopbar {
|
||||||
|
fill: var(--commentBorderColour);
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyComment .blocklyCommentPreview.blocklyText {
|
||||||
|
fill: var(--commentIconColour);
|
||||||
|
dominant-baseline: middle;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyCollapsed.blocklyComment .blocklyCommentPreview {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyCollapsed.blocklyComment .blocklyCommentForeignObject,
|
||||||
|
.blocklyCollapsed.blocklyComment .blocklyResizeHandle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyCollapsed.blocklyComment .blocklyFoldoutIcon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyRTL .blocklyComment {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyRTL .blocklyCommentForeignObject {
|
||||||
|
/* Revert the scale and control RTL using direction instead. */
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyRTL .blocklyCommentPreview {
|
||||||
|
/* Revert the scale and control RTL using direction instead. */
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyRTL .blocklyResizeHandle {
|
||||||
|
cursor: sw-resize;
|
||||||
|
}
|
||||||
|
`);
|
||||||
@@ -53,7 +53,10 @@ export function inject(
|
|||||||
}
|
}
|
||||||
const options = new Options(opt_options || ({} as BlocklyOptions));
|
const options = new Options(opt_options || ({} as BlocklyOptions));
|
||||||
const subContainer = document.createElement('div');
|
const subContainer = document.createElement('div');
|
||||||
subContainer.className = 'injectionDiv';
|
dom.addClass(subContainer, 'injectionDiv');
|
||||||
|
if (opt_options?.rtl) {
|
||||||
|
dom.addClass(subContainer, 'blocklyRTL');
|
||||||
|
}
|
||||||
subContainer.tabIndex = 0;
|
subContainer.tabIndex = 0;
|
||||||
aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']);
|
aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']);
|
||||||
|
|
||||||
|
|||||||
1
media/arrow-dropdown.svg
Normal file
1
media/arrow-dropdown.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#041E49"><path d="M480-360 280-560h400L480-360Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 150 B |
3
media/resize-handle.svg
Normal file
3
media/resize-handle.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="8" viewBox="0 0 8 8" width="8" fill="#041E49" stroke="#000">
|
||||||
|
<g><line x1="2.6666666666666665" y1="7" x2="7" y2="2.6666666666666665"></line><line x1="5.333333333333333" y1="7" x2="7" y2="5.333333333333333"></line></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 272 B |
@@ -106,6 +106,8 @@
|
|||||||
import './metrics_test.js';
|
import './metrics_test.js';
|
||||||
import './mutator_test.js';
|
import './mutator_test.js';
|
||||||
import './names_test.js';
|
import './names_test.js';
|
||||||
|
// TODO: Remove these tests.
|
||||||
|
import './old_workspace_comment_test.js';
|
||||||
import './procedure_map_test.js';
|
import './procedure_map_test.js';
|
||||||
import './blocks/procedures_test.js';
|
import './blocks/procedures_test.js';
|
||||||
import './registry_test.js';
|
import './registry_test.js';
|
||||||
|
|||||||
267
tests/mocha/old_workspace_comment_test.js
Normal file
267
tests/mocha/old_workspace_comment_test.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
sharedTestSetup,
|
||||||
|
sharedTestTeardown,
|
||||||
|
} from './test_helpers/setup_teardown.js';
|
||||||
|
|
||||||
|
suite('Workspace comment', function () {
|
||||||
|
setup(function () {
|
||||||
|
sharedTestSetup.call(this);
|
||||||
|
this.workspace = new Blockly.Workspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
sharedTestTeardown.call(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getTopComments(ordered=true)', function () {
|
||||||
|
test('No comments', function () {
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('One comment', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(true).length, 1);
|
||||||
|
chai.assert.equal(this.workspace.commentDB.get('comment id'), comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After clear empty workspace', function () {
|
||||||
|
this.workspace.clear();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After clear non-empty workspace', function () {
|
||||||
|
new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
this.workspace.clear();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
||||||
|
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After dispose', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
comment.dispose();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
||||||
|
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getTopComments(ordered=false)', function () {
|
||||||
|
test('No comments', function () {
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('One comment', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(false).length, 1);
|
||||||
|
chai.assert.equal(this.workspace.commentDB.get('comment id'), comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After clear empty workspace', function () {
|
||||||
|
this.workspace.clear();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After clear non-empty workspace', function () {
|
||||||
|
new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
this.workspace.clear();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
||||||
|
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After dispose', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
comment.dispose();
|
||||||
|
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
||||||
|
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getCommentById', function () {
|
||||||
|
test('Trivial', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
chai.assert.equal(this.workspace.getCommentById(comment.id), comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Null id', function () {
|
||||||
|
chai.assert.isNull(this.workspace.getCommentById(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Non-existent id', function () {
|
||||||
|
chai.assert.isNull(this.workspace.getCommentById('badId'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After dispose', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
comment.dispose();
|
||||||
|
chai.assert.isNull(this.workspace.getCommentById(comment.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('dispose', function () {
|
||||||
|
test('Called twice', function () {
|
||||||
|
const comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
comment.dispose();
|
||||||
|
// Nothing should go wrong the second time dispose is called.
|
||||||
|
comment.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Width and height', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial values', function () {
|
||||||
|
chai.assert.equal(this.comment.getWidth(), 20, 'Width');
|
||||||
|
chai.assert.equal(this.comment.getHeight(), 10, 'Height');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setWidth does not affect height', function () {
|
||||||
|
this.comment.setWidth(30);
|
||||||
|
chai.assert.equal(this.comment.getWidth(), 30, 'Width');
|
||||||
|
chai.assert.equal(this.comment.getHeight(), 10, 'Height');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setHeight does not affect width', function () {
|
||||||
|
this.comment.setHeight(30);
|
||||||
|
chai.assert.equal(this.comment.getWidth(), 20, 'Width');
|
||||||
|
chai.assert.equal(this.comment.getHeight(), 30, 'Height');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('XY position', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial position', function () {
|
||||||
|
const xy = this.comment.getRelativeToSurfaceXY();
|
||||||
|
chai.assert.equal(xy.x, 0, 'Initial X position');
|
||||||
|
chai.assert.equal(xy.y, 0, 'Initial Y position');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moveBy', function () {
|
||||||
|
this.comment.moveBy(10, 100);
|
||||||
|
const xy = this.comment.getRelativeToSurfaceXY();
|
||||||
|
chai.assert.equal(xy.x, 10, 'New X position');
|
||||||
|
chai.assert.equal(xy.y, 100, 'New Y position');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Content', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.comment = new Blockly.WorkspaceComment(
|
||||||
|
this.workspace,
|
||||||
|
'comment text',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'comment id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('After creation', function () {
|
||||||
|
chai.assert.equal(this.comment.getContent(), 'comment text');
|
||||||
|
chai.assert.equal(
|
||||||
|
this.workspace.undoStack_.length,
|
||||||
|
1,
|
||||||
|
'Workspace undo stack',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Set to same value', function () {
|
||||||
|
this.comment.setContent('comment text');
|
||||||
|
chai.assert.equal(this.comment.getContent(), 'comment text');
|
||||||
|
// Setting the text to the old value does not fire an event.
|
||||||
|
chai.assert.equal(
|
||||||
|
this.workspace.undoStack_.length,
|
||||||
|
1,
|
||||||
|
'Workspace undo stack',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Set to different value', function () {
|
||||||
|
this.comment.setContent('new comment text');
|
||||||
|
chai.assert.equal(this.comment.getContent(), 'new comment text');
|
||||||
|
chai.assert.equal(
|
||||||
|
this.workspace.undoStack_.length,
|
||||||
|
2,
|
||||||
|
'Workspace undo stack',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2020 Google LLC
|
* Copyright 2024 Google LLC
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -12,256 +12,188 @@ import {
|
|||||||
suite('Workspace comment', function () {
|
suite('Workspace comment', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
sharedTestSetup.call(this);
|
sharedTestSetup.call(this);
|
||||||
this.workspace = new Blockly.Workspace();
|
this.workspace = new Blockly.inject('blocklyDiv', {});
|
||||||
|
this.commentView = new Blockly.comments.CommentView(this.workspace);
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(function () {
|
teardown(function () {
|
||||||
sharedTestTeardown.call(this);
|
sharedTestTeardown.call(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('getTopComments(ordered=true)', function () {
|
suite('Listeners', function () {
|
||||||
test('No comments', function () {
|
suite('Text change listeners', function () {
|
||||||
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
test('text change listeners are called when text is changed', function () {
|
||||||
|
const spy = sinon.spy();
|
||||||
|
this.commentView.addTextChangeListener(spy);
|
||||||
|
|
||||||
|
this.commentView.setText('test');
|
||||||
|
|
||||||
|
chai.assert.isTrue(
|
||||||
|
spy.calledOnce,
|
||||||
|
'Expected the spy to be called once',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
spy.calledWith('', 'test'),
|
||||||
|
'Expected the spy to be called with the given args',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('text change listeners can remove themselves without skipping others', function () {
|
||||||
|
const fake1 = sinon.fake();
|
||||||
|
const fake2 = sinon.fake(() =>
|
||||||
|
this.commentView.removeTextChangeListener(fake2),
|
||||||
|
);
|
||||||
|
const fake3 = sinon.fake();
|
||||||
|
this.commentView.addTextChangeListener(fake1);
|
||||||
|
this.commentView.addTextChangeListener(fake2);
|
||||||
|
this.commentView.addTextChangeListener(fake3);
|
||||||
|
|
||||||
|
this.commentView.setText('test');
|
||||||
|
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake1.calledOnce,
|
||||||
|
'Expected the first listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake2.calledOnce,
|
||||||
|
'Expected the second listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake3.calledOnce,
|
||||||
|
'Expected the third listener to be called',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('One comment', function () {
|
suite('Size change listeners', function () {
|
||||||
const comment = new Blockly.WorkspaceComment(
|
test('size change listeners are called when text is changed', function () {
|
||||||
this.workspace,
|
const spy = sinon.spy();
|
||||||
'comment text',
|
this.commentView.addSizeChangeListener(spy);
|
||||||
0,
|
const originalSize = this.commentView.getSize();
|
||||||
0,
|
const newSize = new Blockly.utils.Size(1337, 1337);
|
||||||
'comment id',
|
|
||||||
);
|
this.commentView.setSize(newSize);
|
||||||
chai.assert.equal(this.workspace.getTopComments(true).length, 1);
|
|
||||||
chai.assert.equal(this.workspace.commentDB.get('comment id'), comment);
|
chai.assert.isTrue(
|
||||||
|
spy.calledOnce,
|
||||||
|
'Expected the spy to be called once',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
spy.calledWith(originalSize, newSize),
|
||||||
|
'Expected the spy to be called with the given args',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('size change listeners can remove themselves without skipping others', function () {
|
||||||
|
const fake1 = sinon.fake();
|
||||||
|
const fake2 = sinon.fake(() =>
|
||||||
|
this.commentView.removeSizeChangeListener(fake2),
|
||||||
|
);
|
||||||
|
const fake3 = sinon.fake();
|
||||||
|
this.commentView.addSizeChangeListener(fake1);
|
||||||
|
this.commentView.addSizeChangeListener(fake2);
|
||||||
|
this.commentView.addSizeChangeListener(fake3);
|
||||||
|
const newSize = new Blockly.utils.Size(1337, 1337);
|
||||||
|
|
||||||
|
this.commentView.setSize(newSize);
|
||||||
|
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake1.calledOnce,
|
||||||
|
'Expected the first listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake2.calledOnce,
|
||||||
|
'Expected the second listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake3.calledOnce,
|
||||||
|
'Expected the third listener to be called',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('After clear empty workspace', function () {
|
suite('Collapse change listeners', function () {
|
||||||
this.workspace.clear();
|
test('collapse change listeners are called when text is changed', function () {
|
||||||
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
const spy = sinon.spy();
|
||||||
|
this.commentView.addOnCollapseListener(spy);
|
||||||
|
|
||||||
|
this.commentView.setCollapsed(true);
|
||||||
|
|
||||||
|
chai.assert.isTrue(
|
||||||
|
spy.calledOnce,
|
||||||
|
'Expected the spy to be called once',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
spy.calledWith(true),
|
||||||
|
'Expected the spy to be called with the given args',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collapse change listeners can remove themselves without skipping others', function () {
|
||||||
|
const fake1 = sinon.fake();
|
||||||
|
const fake2 = sinon.fake(() =>
|
||||||
|
this.commentView.removeOnCollapseListener(fake2),
|
||||||
|
);
|
||||||
|
const fake3 = sinon.fake();
|
||||||
|
this.commentView.addOnCollapseListener(fake1);
|
||||||
|
this.commentView.addOnCollapseListener(fake2);
|
||||||
|
this.commentView.addOnCollapseListener(fake3);
|
||||||
|
|
||||||
|
this.commentView.setCollapsed(true);
|
||||||
|
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake1.calledOnce,
|
||||||
|
'Expected the first listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake2.calledOnce,
|
||||||
|
'Expected the second listener to be called',
|
||||||
|
);
|
||||||
|
chai.assert.isTrue(
|
||||||
|
fake3.calledOnce,
|
||||||
|
'Expected the third listener to be called',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('After clear non-empty workspace', function () {
|
suite('Dispose change listeners', function () {
|
||||||
new Blockly.WorkspaceComment(
|
test('dispose listeners are called when text is changed', function () {
|
||||||
this.workspace,
|
const spy = sinon.spy();
|
||||||
'comment text',
|
this.commentView.addDisposeListener(spy);
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
this.workspace.clear();
|
|
||||||
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
|
||||||
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('After dispose', function () {
|
this.commentView.dispose();
|
||||||
const comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
comment.dispose();
|
|
||||||
chai.assert.equal(this.workspace.getTopComments(true).length, 0);
|
|
||||||
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('getTopComments(ordered=false)', function () {
|
chai.assert.isTrue(
|
||||||
test('No comments', function () {
|
spy.calledOnce,
|
||||||
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
'Expected the spy to be called once',
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('One comment', function () {
|
test('dispose listeners can remove themselves without skipping others', function () {
|
||||||
const comment = new Blockly.WorkspaceComment(
|
const fake1 = sinon.fake();
|
||||||
this.workspace,
|
const fake2 = sinon.fake(() =>
|
||||||
'comment text',
|
this.commentView.removeDisposeListener(fake2),
|
||||||
0,
|
);
|
||||||
0,
|
const fake3 = sinon.fake();
|
||||||
'comment id',
|
this.commentView.addDisposeListener(fake1);
|
||||||
);
|
this.commentView.addDisposeListener(fake2);
|
||||||
chai.assert.equal(this.workspace.getTopComments(false).length, 1);
|
this.commentView.addDisposeListener(fake3);
|
||||||
chai.assert.equal(this.workspace.commentDB.get('comment id'), comment);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('After clear empty workspace', function () {
|
this.commentView.dispose();
|
||||||
this.workspace.clear();
|
|
||||||
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('After clear non-empty workspace', function () {
|
chai.assert.isTrue(
|
||||||
new Blockly.WorkspaceComment(
|
fake1.calledOnce,
|
||||||
this.workspace,
|
'Expected the first listener to be called',
|
||||||
'comment text',
|
);
|
||||||
0,
|
chai.assert.isTrue(
|
||||||
0,
|
fake2.calledOnce,
|
||||||
'comment id',
|
'Expected the second listener to be called',
|
||||||
);
|
);
|
||||||
this.workspace.clear();
|
chai.assert.isTrue(
|
||||||
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
fake3.calledOnce,
|
||||||
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
'Expected the third listener to be called',
|
||||||
});
|
);
|
||||||
|
});
|
||||||
test('After dispose', function () {
|
|
||||||
const comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
comment.dispose();
|
|
||||||
chai.assert.equal(this.workspace.getTopComments(false).length, 0);
|
|
||||||
chai.assert.isFalse(this.workspace.commentDB.has('comment id'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('getCommentById', function () {
|
|
||||||
test('Trivial', function () {
|
|
||||||
const comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
chai.assert.equal(this.workspace.getCommentById(comment.id), comment);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Null id', function () {
|
|
||||||
chai.assert.isNull(this.workspace.getCommentById(null));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Non-existent id', function () {
|
|
||||||
chai.assert.isNull(this.workspace.getCommentById('badId'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('After dispose', function () {
|
|
||||||
const comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
comment.dispose();
|
|
||||||
chai.assert.isNull(this.workspace.getCommentById(comment.id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('dispose', function () {
|
|
||||||
test('Called twice', function () {
|
|
||||||
const comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
comment.dispose();
|
|
||||||
// Nothing should go wrong the second time dispose is called.
|
|
||||||
comment.dispose();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('Width and height', function () {
|
|
||||||
setup(function () {
|
|
||||||
this.comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Initial values', function () {
|
|
||||||
chai.assert.equal(this.comment.getWidth(), 20, 'Width');
|
|
||||||
chai.assert.equal(this.comment.getHeight(), 10, 'Height');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setWidth does not affect height', function () {
|
|
||||||
this.comment.setWidth(30);
|
|
||||||
chai.assert.equal(this.comment.getWidth(), 30, 'Width');
|
|
||||||
chai.assert.equal(this.comment.getHeight(), 10, 'Height');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setHeight does not affect width', function () {
|
|
||||||
this.comment.setHeight(30);
|
|
||||||
chai.assert.equal(this.comment.getWidth(), 20, 'Width');
|
|
||||||
chai.assert.equal(this.comment.getHeight(), 30, 'Height');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('XY position', function () {
|
|
||||||
setup(function () {
|
|
||||||
this.comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Initial position', function () {
|
|
||||||
const xy = this.comment.getRelativeToSurfaceXY();
|
|
||||||
chai.assert.equal(xy.x, 0, 'Initial X position');
|
|
||||||
chai.assert.equal(xy.y, 0, 'Initial Y position');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('moveBy', function () {
|
|
||||||
this.comment.moveBy(10, 100);
|
|
||||||
const xy = this.comment.getRelativeToSurfaceXY();
|
|
||||||
chai.assert.equal(xy.x, 10, 'New X position');
|
|
||||||
chai.assert.equal(xy.y, 100, 'New Y position');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('Content', function () {
|
|
||||||
setup(function () {
|
|
||||||
this.comment = new Blockly.WorkspaceComment(
|
|
||||||
this.workspace,
|
|
||||||
'comment text',
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
'comment id',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
teardown(function () {
|
|
||||||
sinon.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('After creation', function () {
|
|
||||||
chai.assert.equal(this.comment.getContent(), 'comment text');
|
|
||||||
chai.assert.equal(
|
|
||||||
this.workspace.undoStack_.length,
|
|
||||||
1,
|
|
||||||
'Workspace undo stack',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Set to same value', function () {
|
|
||||||
this.comment.setContent('comment text');
|
|
||||||
chai.assert.equal(this.comment.getContent(), 'comment text');
|
|
||||||
// Setting the text to the old value does not fire an event.
|
|
||||||
chai.assert.equal(
|
|
||||||
this.workspace.undoStack_.length,
|
|
||||||
1,
|
|
||||||
'Workspace undo stack',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Set to different value', function () {
|
|
||||||
this.comment.setContent('new comment text');
|
|
||||||
chai.assert.equal(this.comment.getContent(), 'new comment text');
|
|
||||||
chai.assert.equal(
|
|
||||||
this.workspace.undoStack_.length,
|
|
||||||
2,
|
|
||||||
'Workspace undo stack',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user