mirror of
https://github.com/google/blockly.git
synced 2026-01-05 08:00:09 +01:00
feat: Add support for keyboard navigation in/to workspace comments. (#9182)
* feat: Enhance the Rect API. * feat: Add support for sorting IBoundedElements in general. * fix: Improve typings of getTopElement/Comment methods. * feat: Add classes to represent comment icons. * refactor: Use comment icons in comment view. * feat: Update navigation policies to support workspace comments. * feat: Make the navigator and workspace handle workspace comments. * feat: Visit workspace comments when navigating with the up/down arrows. * chore: Make the linter happy. * chore: Rename comment icons to bar buttons. * refactor: Rename CommentIcons to CommentBarButtons. * chore: Improve docstrings. * chore: Clarify unit type. * refactor: Remove workspace argument from `navigateStacks()`. * fix: Fix errant find and replace in CSS. * fix: Fix issue that could cause delete button to become misaligned.
This commit is contained in:
@@ -4,7 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export {CollapseCommentBarButton} from './comments/collapse_comment_bar_button.js';
|
||||
export {CommentBarButton} from './comments/comment_bar_button.js';
|
||||
export {CommentEditor} from './comments/comment_editor.js';
|
||||
export {CommentView} from './comments/comment_view.js';
|
||||
export {DeleteCommentBarButton} from './comments/delete_comment_bar_button.js';
|
||||
export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
export {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
|
||||
101
core/comments/collapse_comment_bar_button.ts
Normal file
101
core/comments/collapse_comment_bar_button.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import * as touch from '../touch.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentBarButton} from './comment_bar_button.js';
|
||||
|
||||
/**
|
||||
* Magic string appended to the comment ID to create a unique ID for this button.
|
||||
*/
|
||||
export const COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER =
|
||||
'_collapse_bar_button';
|
||||
|
||||
/**
|
||||
* Button that toggles the collapsed state of a comment.
|
||||
*/
|
||||
export class CollapseCommentBarButton extends CommentBarButton {
|
||||
/**
|
||||
* Opaque ID used to unbind event handlers during disposal.
|
||||
*/
|
||||
private readonly bindId: browserEvents.Data;
|
||||
|
||||
/**
|
||||
* SVG image displayed on this button.
|
||||
*/
|
||||
protected override readonly icon: SVGImageElement;
|
||||
|
||||
/**
|
||||
* Creates a new CollapseCommentBarButton instance.
|
||||
*
|
||||
* @param id The ID of this button's parent comment.
|
||||
* @param workspace The workspace this button's parent comment is displayed on.
|
||||
* @param container An SVG group that this button should be a child of.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
) {
|
||||
super(id, workspace, container);
|
||||
|
||||
this.icon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyFoldoutIcon',
|
||||
'href': `${this.workspace.options.pathToMedia}foldout-icon.svg`,
|
||||
'id': `${this.id}${COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER}`,
|
||||
},
|
||||
this.container,
|
||||
);
|
||||
this.bindId = browserEvents.conditionalBind(
|
||||
this.icon,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.performAction.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of this button.
|
||||
*/
|
||||
dispose() {
|
||||
browserEvents.unbind(this.bindId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the positioning of this button within its container.
|
||||
*/
|
||||
override reposition() {
|
||||
const margin = this.getMargin();
|
||||
this.icon.setAttribute('y', `${margin}`);
|
||||
this.icon.setAttribute('x', `${margin}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the collapsed state of the parent comment.
|
||||
*
|
||||
* @param e The event that triggered this action.
|
||||
*/
|
||||
override performAction(e?: Event) {
|
||||
touch.clearTouchIdentifier();
|
||||
|
||||
const comment = this.getParentComment();
|
||||
comment.view.bringToFront();
|
||||
if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
comment.setCollapsed(!comment.isCollapsed());
|
||||
this.workspace.hideChaff();
|
||||
|
||||
e?.stopPropagation();
|
||||
}
|
||||
}
|
||||
105
core/comments/comment_bar_button.ts
Normal file
105
core/comments/comment_bar_button.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {Rect} from '../utils/rect.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js';
|
||||
|
||||
/**
|
||||
* Button displayed on a comment's top bar.
|
||||
*/
|
||||
export abstract class CommentBarButton implements IFocusableNode {
|
||||
/**
|
||||
* SVG image displayed on this button.
|
||||
*/
|
||||
protected abstract readonly icon: SVGImageElement;
|
||||
|
||||
/**
|
||||
* Creates a new CommentBarButton instance.
|
||||
*
|
||||
* @param id The ID of this button's parent comment.
|
||||
* @param workspace The workspace this button's parent comment is on.
|
||||
* @param container An SVG group that this button should be a child of.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns whether or not this button is currently visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return this.icon.checkVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent comment of this comment bar button.
|
||||
*/
|
||||
getParentComment(): RenderedWorkspaceComment {
|
||||
const comment = this.workspace.getCommentById(this.id);
|
||||
if (!comment) {
|
||||
throw new Error(
|
||||
`Comment bar button ${this.id} has no corresponding comment`,
|
||||
);
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
/** Adjusts the position of this button within its parent container. */
|
||||
abstract reposition(): void;
|
||||
|
||||
/** Perform the action this button should take when it is acted on. */
|
||||
abstract performAction(e?: Event): void;
|
||||
|
||||
/**
|
||||
* Returns the dimensions of this button in workspace coordinates.
|
||||
*
|
||||
* @param includeMargin True to include the margin when calculating the size.
|
||||
* @returns The size of this button.
|
||||
*/
|
||||
getSize(includeMargin = false): Rect {
|
||||
const bounds = this.icon.getBBox();
|
||||
const rect = Rect.from(bounds);
|
||||
if (includeMargin) {
|
||||
const margin = this.getMargin();
|
||||
rect.left -= margin;
|
||||
rect.top -= margin;
|
||||
rect.bottom += margin;
|
||||
rect.right += margin;
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
/** Returns the margin in workspace coordinates surrounding this button. */
|
||||
getMargin(): number {
|
||||
return (this.container.getBBox().height - this.icon.getBBox().height) / 2;
|
||||
}
|
||||
|
||||
/** Returns a DOM element representing this button that can receive focus. */
|
||||
getFocusableElement() {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
/** Returns the workspace this button is a child of. */
|
||||
getFocusableTree() {
|
||||
return this.workspace;
|
||||
}
|
||||
|
||||
/** Called when this button's focusable DOM element gains focus. */
|
||||
onNodeFocus() {}
|
||||
|
||||
/** Called when this button's focusable DOM element loses focus. */
|
||||
onNodeBlur() {}
|
||||
|
||||
/** Returns whether this button can be focused. True if it is visible. */
|
||||
canBeFocused() {
|
||||
return this.isVisible();
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,17 @@ import * as drag from '../utils/drag.js';
|
||||
import {Size} from '../utils/size.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CollapseCommentBarButton} from './collapse_comment_bar_button.js';
|
||||
import {CommentBarButton} from './comment_bar_button.js';
|
||||
import {CommentEditor} from './comment_editor.js';
|
||||
import {DeleteCommentBarButton} from './delete_comment_bar_button.js';
|
||||
|
||||
export class CommentView implements IRenderedElement {
|
||||
/** The root group element of the comment view. */
|
||||
private svgRoot: SVGGElement;
|
||||
|
||||
/**
|
||||
* The svg rect element that we use to create a hightlight around the comment.
|
||||
* The SVG rect element that we use to create a highlight around the comment.
|
||||
*/
|
||||
private highlightRect: SVGRectElement;
|
||||
|
||||
@@ -33,11 +36,11 @@ export class CommentView implements IRenderedElement {
|
||||
/** The rect background for the top bar. */
|
||||
private topBarBackground: SVGRectElement;
|
||||
|
||||
/** The delete icon that goes in the top bar. */
|
||||
private deleteIcon: SVGImageElement;
|
||||
/** The delete button that goes in the top bar. */
|
||||
private deleteButton: DeleteCommentBarButton;
|
||||
|
||||
/** The foldout icon that goes in the top bar. */
|
||||
private foldoutIcon: SVGImageElement;
|
||||
/** The foldout button that goes in the top bar. */
|
||||
private foldoutButton: CollapseCommentBarButton;
|
||||
|
||||
/** The text element that goes in the top bar. */
|
||||
private textPreview: SVGTextElement;
|
||||
@@ -99,7 +102,7 @@ export class CommentView implements IRenderedElement {
|
||||
|
||||
constructor(
|
||||
readonly workspace: WorkspaceSvg,
|
||||
private commentId?: string,
|
||||
private commentId: string,
|
||||
) {
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyComment blocklyEditable blocklyDraggable',
|
||||
@@ -110,11 +113,11 @@ export class CommentView implements IRenderedElement {
|
||||
({
|
||||
topBarGroup: this.topBarGroup,
|
||||
topBarBackground: this.topBarBackground,
|
||||
deleteIcon: this.deleteIcon,
|
||||
foldoutIcon: this.foldoutIcon,
|
||||
deleteButton: this.deleteButton,
|
||||
foldoutButton: this.foldoutButton,
|
||||
textPreview: this.textPreview,
|
||||
textPreviewNode: this.textPreviewNode,
|
||||
} = this.createTopBar(this.svgRoot, workspace));
|
||||
} = this.createTopBar(this.svgRoot));
|
||||
|
||||
this.commentEditor = this.createTextArea();
|
||||
|
||||
@@ -147,14 +150,11 @@ export class CommentView implements IRenderedElement {
|
||||
* Creates the top bar and the elements visually within it.
|
||||
* Registers event listeners.
|
||||
*/
|
||||
private createTopBar(
|
||||
svgRoot: SVGGElement,
|
||||
workspace: WorkspaceSvg,
|
||||
): {
|
||||
private createTopBar(svgRoot: SVGGElement): {
|
||||
topBarGroup: SVGGElement;
|
||||
topBarBackground: SVGRectElement;
|
||||
deleteIcon: SVGImageElement;
|
||||
foldoutIcon: SVGImageElement;
|
||||
deleteButton: DeleteCommentBarButton;
|
||||
foldoutButton: CollapseCommentBarButton;
|
||||
textPreview: SVGTextElement;
|
||||
textPreviewNode: Text;
|
||||
} {
|
||||
@@ -172,22 +172,14 @@ export class CommentView implements IRenderedElement {
|
||||
},
|
||||
topBarGroup,
|
||||
);
|
||||
// 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`,
|
||||
},
|
||||
const deleteButton = new DeleteCommentBarButton(
|
||||
this.commentId,
|
||||
this.workspace,
|
||||
topBarGroup,
|
||||
);
|
||||
const foldoutIcon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyFoldoutIcon',
|
||||
'href': `${workspace.options.pathToMedia}foldout-icon.svg`,
|
||||
},
|
||||
const foldoutButton = new CollapseCommentBarButton(
|
||||
this.commentId,
|
||||
this.workspace,
|
||||
topBarGroup,
|
||||
);
|
||||
const textPreview = dom.createSvgElement(
|
||||
@@ -200,27 +192,11 @@ export class CommentView implements IRenderedElement {
|
||||
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 {
|
||||
topBarGroup,
|
||||
topBarBackground,
|
||||
deleteIcon,
|
||||
foldoutIcon,
|
||||
deleteButton,
|
||||
foldoutButton,
|
||||
textPreview,
|
||||
textPreviewNode,
|
||||
};
|
||||
@@ -300,15 +276,10 @@ export class CommentView implements IRenderedElement {
|
||||
*/
|
||||
setSizeWithoutFiringEvents(size: Size) {
|
||||
const topBarSize = this.topBarBackground.getBBox();
|
||||
const deleteSize = this.deleteIcon.getBBox();
|
||||
const foldoutSize = this.foldoutIcon.getBBox();
|
||||
const textPreviewSize = this.textPreview.getBBox();
|
||||
const resizeSize = this.resizeHandle.getBBox();
|
||||
|
||||
size = Size.max(
|
||||
size,
|
||||
this.calcMinSize(topBarSize, foldoutSize, deleteSize),
|
||||
);
|
||||
size = Size.max(size, this.calcMinSize(topBarSize));
|
||||
this.size = size;
|
||||
|
||||
this.svgRoot.setAttribute('height', `${size.height}`);
|
||||
@@ -317,15 +288,9 @@ export class CommentView implements IRenderedElement {
|
||||
this.updateHighlightRect(size);
|
||||
this.updateTopBarSize(size);
|
||||
this.commentEditor.updateSize(size, topBarSize);
|
||||
this.updateDeleteIconPosition(size, topBarSize, deleteSize);
|
||||
this.updateFoldoutIconPosition(topBarSize, foldoutSize);
|
||||
this.updateTextPreviewSize(
|
||||
size,
|
||||
topBarSize,
|
||||
textPreviewSize,
|
||||
deleteSize,
|
||||
resizeSize,
|
||||
);
|
||||
this.deleteButton.reposition();
|
||||
this.foldoutButton.reposition();
|
||||
this.updateTextPreviewSize(size, topBarSize, textPreviewSize);
|
||||
this.updateResizeHandlePosition(size, resizeSize);
|
||||
}
|
||||
|
||||
@@ -347,25 +312,18 @@ export class CommentView implements IRenderedElement {
|
||||
*
|
||||
* The minimum height is based on the height of the top bar.
|
||||
*/
|
||||
private calcMinSize(
|
||||
topBarSize: Size,
|
||||
foldoutSize: Size,
|
||||
deleteSize: Size,
|
||||
): Size {
|
||||
private calcMinSize(topBarSize: Size): Size {
|
||||
this.updateTextPreview(this.commentEditor.getText() ?? '');
|
||||
const textPreviewWidth = dom.getTextWidth(this.textPreview);
|
||||
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
|
||||
let width = textPreviewWidth;
|
||||
if (this.foldoutIcon.checkVisibility()) {
|
||||
width += foldoutSize.width + foldoutMargin * 2;
|
||||
if (this.foldoutButton.isVisible()) {
|
||||
width += this.foldoutButton.getSize(true).getWidth();
|
||||
} else if (textPreviewWidth) {
|
||||
width += 4; // Arbitrary margin before text.
|
||||
}
|
||||
if (this.deleteIcon.checkVisibility()) {
|
||||
width += deleteSize.width + deleteMargin * 2;
|
||||
if (this.deleteButton.isVisible()) {
|
||||
width += this.deleteButton.getSize(true).getWidth();
|
||||
} else if (textPreviewWidth) {
|
||||
width += 4; // Arbitrary margin after text.
|
||||
}
|
||||
@@ -376,16 +334,6 @@ export class CommentView implements IRenderedElement {
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
/** Calculates the margin that should exist around the delete icon. */
|
||||
private calcDeleteMargin(topBarSize: Size, deleteSize: Size) {
|
||||
return (topBarSize.height - deleteSize.height) / 2;
|
||||
}
|
||||
|
||||
/** Calculates the margin that should exist around the foldout icon. */
|
||||
private calcFoldoutMargin(topBarSize: Size, foldoutSize: Size) {
|
||||
return (topBarSize.height - foldoutSize.height) / 2;
|
||||
}
|
||||
|
||||
/** Updates the size of the highlight rect to reflect the new size. */
|
||||
private updateHighlightRect(size: Size) {
|
||||
this.highlightRect.setAttribute('height', `${size.height}`);
|
||||
@@ -400,31 +348,6 @@ export class CommentView implements IRenderedElement {
|
||||
this.topBarBackground.setAttribute('width', `${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 = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
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 = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
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.
|
||||
*/
|
||||
@@ -432,25 +355,14 @@ export class CommentView implements IRenderedElement {
|
||||
size: Size,
|
||||
topBarSize: Size,
|
||||
textPreviewSize: Size,
|
||||
deleteSize: Size,
|
||||
foldoutSize: Size,
|
||||
) {
|
||||
const textPreviewMargin = (topBarSize.height - textPreviewSize.height) / 2;
|
||||
const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize);
|
||||
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
|
||||
const foldoutSize = this.foldoutButton.getSize(true);
|
||||
const deleteSize = this.deleteButton.getSize(true);
|
||||
|
||||
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)
|
||||
}`,
|
||||
);
|
||||
size.width - foldoutSize.getWidth() - deleteSize.getWidth();
|
||||
this.textPreview.setAttribute('x', `${foldoutSize.getWidth()}`);
|
||||
this.textPreview.setAttribute(
|
||||
'y',
|
||||
`${textPreviewMargin + textPreviewSize.height / 2}`,
|
||||
@@ -601,25 +513,6 @@ export class CommentView implements IRenderedElement {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the collapsedness of the block when we receive a pointer down
|
||||
* event on the foldout icon.
|
||||
*/
|
||||
private onFoldoutDown(e: PointerEvent) {
|
||||
touch.clearTouchIdentifier();
|
||||
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;
|
||||
@@ -692,7 +585,7 @@ export class CommentView implements IRenderedElement {
|
||||
}
|
||||
|
||||
/** Brings the workspace comment to the front of its layer. */
|
||||
private bringToFront() {
|
||||
bringToFront() {
|
||||
const parent = this.svgRoot.parentNode;
|
||||
const childNodes = parent!.childNodes;
|
||||
// Avoid moving the comment if it's already at the bottom.
|
||||
@@ -719,6 +612,8 @@ export class CommentView implements IRenderedElement {
|
||||
/** Disposes of this comment view. */
|
||||
dispose() {
|
||||
this.disposing = true;
|
||||
this.foldoutButton.dispose();
|
||||
this.deleteButton.dispose();
|
||||
dom.removeNode(this.svgRoot);
|
||||
// Loop through listeners backwards in case they remove themselves.
|
||||
for (let i = this.disposeListeners.length - 1; i >= 0; i--) {
|
||||
@@ -749,6 +644,13 @@ export class CommentView implements IRenderedElement {
|
||||
removeDisposeListener(listener: () => void) {
|
||||
this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getCommentBarButtons(): CommentBarButton[] {
|
||||
return [this.foldoutButton, this.deleteButton];
|
||||
}
|
||||
}
|
||||
|
||||
css.register(`
|
||||
|
||||
102
core/comments/delete_comment_bar_button.ts
Normal file
102
core/comments/delete_comment_bar_button.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as browserEvents from '../browser_events.js';
|
||||
import * as touch from '../touch.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import {Svg} from '../utils/svg.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {CommentBarButton} from './comment_bar_button.js';
|
||||
|
||||
/**
|
||||
* Magic string appended to the comment ID to create a unique ID for this button.
|
||||
*/
|
||||
export const COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER = '_delete_bar_button';
|
||||
|
||||
/**
|
||||
* Button that deletes a comment.
|
||||
*/
|
||||
export class DeleteCommentBarButton extends CommentBarButton {
|
||||
/**
|
||||
* Opaque ID used to unbind event handlers during disposal.
|
||||
*/
|
||||
private readonly bindId: browserEvents.Data;
|
||||
|
||||
/**
|
||||
* SVG image displayed on this button.
|
||||
*/
|
||||
protected override readonly icon: SVGImageElement;
|
||||
|
||||
/**
|
||||
* Creates a new DeleteCommentBarButton instance.
|
||||
*
|
||||
* @param id The ID of this button's parent comment.
|
||||
* @param workspace The workspace this button's parent comment is shown on.
|
||||
* @param container An SVG group that this button should be a child of.
|
||||
*/
|
||||
constructor(
|
||||
protected readonly id: string,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
protected readonly container: SVGGElement,
|
||||
) {
|
||||
super(id, workspace, container);
|
||||
|
||||
this.icon = dom.createSvgElement(
|
||||
Svg.IMAGE,
|
||||
{
|
||||
'class': 'blocklyDeleteIcon',
|
||||
'href': `${this.workspace.options.pathToMedia}delete-icon.svg`,
|
||||
'id': `${this.id}${COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER}`,
|
||||
},
|
||||
container,
|
||||
);
|
||||
this.bindId = browserEvents.conditionalBind(
|
||||
this.icon,
|
||||
'pointerdown',
|
||||
this,
|
||||
this.performAction.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of this button.
|
||||
*/
|
||||
dispose() {
|
||||
browserEvents.unbind(this.bindId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the positioning of this button within its container.
|
||||
*/
|
||||
override reposition() {
|
||||
const margin = this.getMargin();
|
||||
// Reset to 0 so that our position doesn't force the parent container to
|
||||
// grow.
|
||||
this.icon.setAttribute('x', `0`);
|
||||
const containerSize = this.container.getBBox();
|
||||
this.icon.setAttribute('y', `${margin}`);
|
||||
this.icon.setAttribute(
|
||||
'x',
|
||||
`${containerSize.width - this.getSize(true).getWidth()}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes parent comment.
|
||||
*
|
||||
* @param e The event that triggered this action.
|
||||
*/
|
||||
override performAction(e?: Event) {
|
||||
touch.clearTouchIdentifier();
|
||||
if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
this.getParentComment().dispose();
|
||||
e?.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,11 @@ import {BlockSvg} from '../block_svg.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import type {Field} from '../field.js';
|
||||
import type {Icon} from '../icons/icon.js';
|
||||
import type {IBoundedElement} from '../interfaces/i_bounded_element.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import type {ISelectable} from '../interfaces/i_selectable.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
@@ -143,21 +146,25 @@ function getBlockNavigationCandidates(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next/previous stack relative to the given block's stack.
|
||||
* Returns the next/previous stack relative to the given element's stack.
|
||||
*
|
||||
* @param current The block whose stack will be navigated relative to.
|
||||
* @param current The element whose stack will be navigated relative to.
|
||||
* @param delta The difference in index to navigate; positive values navigate
|
||||
* to the nth next stack, while negative values navigate to the nth previous
|
||||
* stack.
|
||||
* @returns The first block in the stack offset by `delta` relative to the
|
||||
* current block's stack, or the last block in the stack offset by `delta`
|
||||
* relative to the current block's stack when navigating backwards.
|
||||
* @returns The first element in the stack offset by `delta` relative to the
|
||||
* current element's stack, or the last element in the stack offset by
|
||||
* `delta` relative to the current element's stack when navigating backwards.
|
||||
*/
|
||||
export function navigateStacks(current: BlockSvg, delta: number) {
|
||||
const stacks = current.workspace.getTopBlocks(true);
|
||||
const currentIndex = stacks.indexOf(current.getRootBlock());
|
||||
export function navigateStacks(current: ISelectable, delta: number) {
|
||||
const stacks: IFocusableNode[] = (current.workspace as WorkspaceSvg)
|
||||
.getTopBoundedElements(true)
|
||||
.filter((element: IBoundedElement) => isFocusableNode(element));
|
||||
const currentIndex = stacks.indexOf(
|
||||
current instanceof BlockSvg ? current.getRootBlock() : current,
|
||||
);
|
||||
const targetIndex = currentIndex + delta;
|
||||
let result: BlockSvg | null = null;
|
||||
let result: IFocusableNode | null = null;
|
||||
if (targetIndex >= 0 && targetIndex < stacks.length) {
|
||||
result = stacks[targetIndex];
|
||||
} else if (targetIndex < 0) {
|
||||
@@ -166,9 +173,9 @@ export function navigateStacks(current: BlockSvg, delta: number) {
|
||||
result = stacks[0];
|
||||
}
|
||||
|
||||
// When navigating to a previous stack, our previous sibling is the last
|
||||
// When navigating to a previous block stack, our previous sibling is the last
|
||||
// block in it.
|
||||
if (delta < 0 && result) {
|
||||
if (delta < 0 && result instanceof BlockSvg) {
|
||||
return result.lastConnectionInStack(false)?.getSourceBlock() ?? result;
|
||||
}
|
||||
|
||||
|
||||
86
core/keyboard_nav/comment_bar_button_navigation_policy.ts
Normal file
86
core/keyboard_nav/comment_bar_button_navigation_policy.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {CommentBarButton} from '../comments/comment_bar_button.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a CommentBarButton.
|
||||
*/
|
||||
export class CommentBarButtonNavigationPolicy
|
||||
implements INavigationPolicy<CommentBarButton>
|
||||
{
|
||||
/**
|
||||
* Returns the first child of the given CommentBarButton.
|
||||
*
|
||||
* @param _current The CommentBarButton to return the first child of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getFirstChild(_current: CommentBarButton): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given CommentBarButton.
|
||||
*
|
||||
* @param current The CommentBarButton to return the parent of.
|
||||
* @returns The parent comment of the given CommentBarButton.
|
||||
*/
|
||||
getParent(current: CommentBarButton): IFocusableNode | null {
|
||||
return current.getParentComment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next peer node of the given CommentBarButton.
|
||||
*
|
||||
* @param current The CommentBarButton to find the following element of.
|
||||
* @returns The next CommentBarButton, if any.
|
||||
*/
|
||||
getNextSibling(current: CommentBarButton): IFocusableNode | null {
|
||||
const children = current.getParentComment().view.getCommentBarButtons();
|
||||
const currentIndex = children.indexOf(current);
|
||||
if (currentIndex >= 0 && currentIndex + 1 < children.length) {
|
||||
return children[currentIndex + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous peer node of the given CommentBarButton.
|
||||
*
|
||||
* @param current The CommentBarButton to find the preceding element of.
|
||||
* @returns The CommentBarButton's previous CommentBarButton, if any.
|
||||
*/
|
||||
getPreviousSibling(current: CommentBarButton): IFocusableNode | null {
|
||||
const children = current.getParentComment().view.getCommentBarButtons();
|
||||
const currentIndex = children.indexOf(current);
|
||||
if (currentIndex > 0) {
|
||||
return children[currentIndex - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the given CommentBarButton can be navigated to.
|
||||
*
|
||||
* @param current The instance to check for navigability.
|
||||
* @returns True if the given CommentBarButton can be focused.
|
||||
*/
|
||||
isNavigable(current: CommentBarButton): boolean {
|
||||
return current.canBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given object can be navigated from by this policy.
|
||||
*
|
||||
* @param current The object to check if this policy applies to.
|
||||
* @returns True if the object is an CommentBarButton.
|
||||
*/
|
||||
isApplicable(current: any): current is CommentBarButton {
|
||||
return current instanceof CommentBarButton;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
|
||||
import {Field} from '../field.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
@@ -38,11 +39,11 @@ export class LineCursor extends Marker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor to the next previous connection, next connection or block
|
||||
* in the pre order traversal. Finds the next node in the pre order traversal.
|
||||
* Moves the cursor to the next block or workspace comment in the pre-order
|
||||
* traversal.
|
||||
*
|
||||
* @returns The next node, or null if the current node is
|
||||
* not set or there is no next value.
|
||||
* @returns The next node, or null if the current node is not set or there is
|
||||
* no next value.
|
||||
*/
|
||||
next(): IFocusableNode | null {
|
||||
const curNode = this.getCurNode();
|
||||
@@ -53,8 +54,9 @@ export class LineCursor extends Marker {
|
||||
curNode,
|
||||
(candidate: IFocusableNode | null) => {
|
||||
return (
|
||||
candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()
|
||||
(candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()) ||
|
||||
candidate instanceof RenderedWorkspaceComment
|
||||
);
|
||||
},
|
||||
true,
|
||||
@@ -87,11 +89,11 @@ export class LineCursor extends Marker {
|
||||
return newNode;
|
||||
}
|
||||
/**
|
||||
* Moves the cursor to the previous next connection or previous connection in
|
||||
* the pre order traversal.
|
||||
* Moves the cursor to the previous block or workspace comment in the
|
||||
* pre-order traversal.
|
||||
*
|
||||
* @returns The previous node, or null if the current node
|
||||
* is not set or there is no previous value.
|
||||
* @returns The previous node, or null if the current node is not set or there
|
||||
* is no previous value.
|
||||
*/
|
||||
prev(): IFocusableNode | null {
|
||||
const curNode = this.getCurNode();
|
||||
@@ -102,8 +104,9 @@ export class LineCursor extends Marker {
|
||||
curNode,
|
||||
(candidate: IFocusableNode | null) => {
|
||||
return (
|
||||
candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()
|
||||
(candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()) ||
|
||||
candidate instanceof RenderedWorkspaceComment
|
||||
);
|
||||
},
|
||||
true,
|
||||
|
||||
77
core/keyboard_nav/workspace_comment_navigation_policy.ts
Normal file
77
core/keyboard_nav/workspace_comment_navigation_policy.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {navigateStacks} from './block_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from an RenderedWorkspaceComment.
|
||||
*/
|
||||
export class WorkspaceCommentNavigationPolicy
|
||||
implements INavigationPolicy<RenderedWorkspaceComment>
|
||||
{
|
||||
/**
|
||||
* Returns the first child of the given workspace comment.
|
||||
*
|
||||
* @param current The workspace comment to return the first child of.
|
||||
* @returns The first child button of the given comment.
|
||||
*/
|
||||
getFirstChild(current: RenderedWorkspaceComment): IFocusableNode | null {
|
||||
return current.view.getCommentBarButtons()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given workspace comment.
|
||||
*
|
||||
* @param current The workspace comment to return the parent of.
|
||||
* @returns The parent workspace of the given comment.
|
||||
*/
|
||||
getParent(current: RenderedWorkspaceComment): IFocusableNode | null {
|
||||
return current.workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next peer node of the given workspace comment.
|
||||
*
|
||||
* @param current The workspace comment to find the following element of.
|
||||
* @returns The next workspace comment or block stack, if any.
|
||||
*/
|
||||
getNextSibling(current: RenderedWorkspaceComment): IFocusableNode | null {
|
||||
return navigateStacks(current, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous peer node of the given workspace comment.
|
||||
*
|
||||
* @param current The workspace comment to find the preceding element of.
|
||||
* @returns The previous workspace comment or block stack, if any.
|
||||
*/
|
||||
getPreviousSibling(current: RenderedWorkspaceComment): IFocusableNode | null {
|
||||
return navigateStacks(current, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the given workspace comment can be navigated to.
|
||||
*
|
||||
* @param current The instance to check for navigability.
|
||||
* @returns True if the given workspace comment can be focused.
|
||||
*/
|
||||
isNavigable(current: RenderedWorkspaceComment): boolean {
|
||||
return current.canBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given object can be navigated from by this policy.
|
||||
*
|
||||
* @param current The object to check if this policy applies to.
|
||||
* @returns True if the object is an RenderedWorkspaceComment.
|
||||
*/
|
||||
isApplicable(current: any): current is RenderedWorkspaceComment {
|
||||
return current instanceof RenderedWorkspaceComment;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
|
||||
import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js';
|
||||
import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js';
|
||||
import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
|
||||
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
|
||||
import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js';
|
||||
import {WorkspaceCommentNavigationPolicy} from './keyboard_nav/workspace_comment_navigation_policy.js';
|
||||
import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js';
|
||||
|
||||
type RuleList<T> = INavigationPolicy<T>[];
|
||||
@@ -29,6 +31,8 @@ export class Navigator {
|
||||
new ConnectionNavigationPolicy(),
|
||||
new WorkspaceNavigationPolicy(),
|
||||
new IconNavigationPolicy(),
|
||||
new WorkspaceCommentNavigationPolicy(),
|
||||
new CommentBarButtonNavigationPolicy(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,16 @@ export class Rect {
|
||||
public right: number,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts a DOM or SVG Rect to a Blockly Rect.
|
||||
*
|
||||
* @param rect The rectangle to convert.
|
||||
* @returns A representation of the same rectangle as a Blockly Rect.
|
||||
*/
|
||||
static from(rect: DOMRect | SVGRect): Rect {
|
||||
return new Rect(rect.y, rect.y + rect.height, rect.x, rect.x + rect.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new copy of this rectangle.
|
||||
*
|
||||
@@ -51,6 +61,11 @@ export class Rect {
|
||||
return this.right - this.left;
|
||||
}
|
||||
|
||||
/** Returns the top left coordinate of this rectangle. */
|
||||
getOrigin(): Coordinate {
|
||||
return new Coordinate(this.left, this.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether this rectangle contains a x/y coordinate.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as common from './common.js';
|
||||
import type {ConnectionDB} from './connection_db.js';
|
||||
import type {Abstract} from './events/events_abstract.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||
import {IProcedureMap} from './interfaces/i_procedure_map.js';
|
||||
import type {IVariableMap} from './interfaces/i_variable_map.js';
|
||||
@@ -35,6 +36,7 @@ import * as arrayUtils from './utils/array.js';
|
||||
import * as deprecation from './utils/deprecation.js';
|
||||
import * as idGenerator from './utils/idgenerator.js';
|
||||
import * as math from './utils/math.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
import type * as toolbox from './utils/toolbox.js';
|
||||
import {deleteVariable, getVariableUsesById} from './variables.js';
|
||||
|
||||
@@ -181,10 +183,31 @@ export class Workspace {
|
||||
a: Block | WorkspaceComment,
|
||||
b: Block | WorkspaceComment,
|
||||
): number {
|
||||
const wrap = (element: Block | WorkspaceComment) => {
|
||||
return {
|
||||
getBoundingRectangle: () => {
|
||||
const xy = element.getRelativeToSurfaceXY();
|
||||
return new Rect(xy.y, xy.y, xy.x, xy.x);
|
||||
},
|
||||
moveBy: () => {},
|
||||
};
|
||||
};
|
||||
return this.sortByOrigin(wrap(a), wrap(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts bounded elements on the workspace by their relative position, top to
|
||||
* bottom (with slight LTR or RTL bias).
|
||||
*
|
||||
* @param a The first element to sort.
|
||||
* @param b The second elment to sort.
|
||||
* @returns -1, 0 or 1 depending on the sort order.
|
||||
*/
|
||||
protected sortByOrigin(a: IBoundedElement, b: IBoundedElement): number {
|
||||
const offset =
|
||||
Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1);
|
||||
const aXY = a.getRelativeToSurfaceXY();
|
||||
const bXY = b.getRelativeToSurfaceXY();
|
||||
const aXY = a.getBoundingRectangle().getOrigin();
|
||||
const bXY = b.getBoundingRectangle().getOrigin();
|
||||
return aXY.y + offset * aXY.x - (bXY.y + offset * bXY.x);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ import type {Block} from './block.js';
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import type {BlocklyOptions} from './blockly_options.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js';
|
||||
import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js';
|
||||
import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js';
|
||||
import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js';
|
||||
import {WorkspaceComment} from './comments/workspace_comment.js';
|
||||
import * as common from './common.js';
|
||||
@@ -2266,8 +2268,8 @@ export class WorkspaceSvg
|
||||
*
|
||||
* @param comment comment to add.
|
||||
*/
|
||||
override addTopComment(comment: WorkspaceComment) {
|
||||
this.addTopBoundedElement(comment as RenderedWorkspaceComment);
|
||||
override addTopComment(comment: RenderedWorkspaceComment) {
|
||||
this.addTopBoundedElement(comment);
|
||||
super.addTopComment(comment);
|
||||
}
|
||||
|
||||
@@ -2276,11 +2278,31 @@ export class WorkspaceSvg
|
||||
*
|
||||
* @param comment comment to remove.
|
||||
*/
|
||||
override removeTopComment(comment: WorkspaceComment) {
|
||||
this.removeTopBoundedElement(comment as RenderedWorkspaceComment);
|
||||
override removeTopComment(comment: RenderedWorkspaceComment) {
|
||||
this.removeTopBoundedElement(comment);
|
||||
super.removeTopComment(comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of comments on this workspace.
|
||||
*
|
||||
* @param ordered If true, sorts the comments based on their position.
|
||||
* @returns A list of workspace comments.
|
||||
*/
|
||||
override getTopComments(ordered = false): RenderedWorkspaceComment[] {
|
||||
return super.getTopComments(ordered) as RenderedWorkspaceComment[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workspace comment with the given ID, if any.
|
||||
*
|
||||
* @param id The ID of the comment to retrieve.
|
||||
* @returns The workspace comment with the given ID, or null.
|
||||
*/
|
||||
override getCommentById(id: string): RenderedWorkspaceComment | null {
|
||||
return super.getCommentById(id) as RenderedWorkspaceComment | null;
|
||||
}
|
||||
|
||||
override getRootWorkspace(): WorkspaceSvg | null {
|
||||
return super.getRootWorkspace() as WorkspaceSvg | null;
|
||||
}
|
||||
@@ -2308,8 +2330,15 @@ export class WorkspaceSvg
|
||||
*
|
||||
* @returns The top-level bounded elements.
|
||||
*/
|
||||
getTopBoundedElements(): IBoundedElement[] {
|
||||
return new Array<IBoundedElement>().concat(this.topBoundedElements);
|
||||
getTopBoundedElements(ordered = false): IBoundedElement[] {
|
||||
const elements = new Array<IBoundedElement>().concat(
|
||||
this.topBoundedElements,
|
||||
);
|
||||
if (ordered) {
|
||||
elements.sort(this.sortByOrigin.bind(this));
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2794,19 +2823,32 @@ export class WorkspaceSvg
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for a specific workspace comment editor
|
||||
// (only if id seems like it is one).
|
||||
const commentEditorIndicator = id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER);
|
||||
if (commentEditorIndicator !== -1) {
|
||||
const commentId = id.substring(0, commentEditorIndicator);
|
||||
// Search for a specific workspace comment or comment icon if the ID
|
||||
// indicates the presence of one.
|
||||
const commentIdSeparatorIndex = Math.max(
|
||||
id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER),
|
||||
id.indexOf(COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER),
|
||||
id.indexOf(COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER),
|
||||
);
|
||||
if (commentIdSeparatorIndex !== -1) {
|
||||
const commentId = id.substring(0, commentIdSeparatorIndex);
|
||||
const comment = this.searchForWorkspaceComment(commentId);
|
||||
if (comment) {
|
||||
return comment.getEditorFocusableNode();
|
||||
if (id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER) > -1) {
|
||||
return comment.getEditorFocusableNode();
|
||||
} else {
|
||||
return (
|
||||
comment.view
|
||||
.getCommentBarButtons()
|
||||
.find((button) => button.getFocusableElement().id.includes(id)) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a specific block.
|
||||
// Don't use `getBlockById` because the block ID is not guaranteeed
|
||||
// Don't use `getBlockById` because the block ID is not guaranteed
|
||||
// to be globally unique, but the ID on the focusable element is.
|
||||
const block = this.getAllBlocks(false).find(
|
||||
(block) => block.getFocusableElement().id === id,
|
||||
|
||||
Reference in New Issue
Block a user