Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 101 additions & 0 deletions core/comments/collapse_comment_bar_button.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 105 additions & 0 deletions core/comments/comment_bar_button.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading