Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add comment view (for workspace comments, and block comments for partners) #7914

Merged
merged 25 commits into from
Mar 11, 2024
Merged
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d1e370d
feat: add basic comment view
BeksOmega Feb 28, 2024
b8995ee
feat: add icons to comment
BeksOmega Feb 29, 2024
a512a85
chore: add text area to comment view
BeksOmega Feb 29, 2024
01b0357
feat: add getting size
BeksOmega Feb 29, 2024
15638f9
feat: add collapsing comment view
BeksOmega Feb 29, 2024
8aabbd7
feat: add setting editability
BeksOmega Feb 29, 2024
0f7043d
feat: add location and text hooks.
BeksOmega Mar 4, 2024
f07c05d
feat: add changing the size
BeksOmega Mar 5, 2024
39e300a
feat: resizing
BeksOmega Mar 5, 2024
caaca00
feat: add collapsing
BeksOmega Mar 5, 2024
fcddcf2
feat: add disposing
BeksOmega Mar 5, 2024
17d90e3
feat: add cursors
BeksOmega Mar 5, 2024
478612a
feat: add moving to the front
BeksOmega Mar 5, 2024
e14f41a
chore: split construction into subprocedures
BeksOmega Mar 5, 2024
b766c57
chore: split resizing into subprocedures
BeksOmega Mar 5, 2024
e218875
feat: handle RTL
BeksOmega Mar 5, 2024
519c055
chore: add doc comments throughout file
BeksOmega Mar 7, 2024
0dfc691
chore: reduce css specificity where possible
BeksOmega Mar 7, 2024
ba9b7da
chore: format
BeksOmega Mar 7, 2024
95efc0f
feat: add remove change listener methods
BeksOmega Mar 8, 2024
ca72b0a
chore: add tests for listeners
BeksOmega Mar 9, 2024
763ddd2
feat: add disposing accessors
BeksOmega Mar 9, 2024
351ff85
chore: add coordinate system notes
BeksOmega Mar 9, 2024
3a875f4
chore: add issues to TODOs where possible
BeksOmega Mar 11, 2024
b1ed4f0
chore: remove suite.only
BeksOmega Mar 11, 2024
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
119 changes: 115 additions & 4 deletions core/comments/comment_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,73 @@ import * as touch from '../touch.js';
// want to require an excessively large min size.
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. */
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. */
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
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;

constructor(private readonly workspace: WorkspaceSvg) {
Expand Down Expand Up @@ -68,6 +113,10 @@ export class CommentView implements IRenderedElement {
this.setSize(this.size);
}

/**
* Creates the top bar and the elements visually within it.
* Registers event listeners.
*/
private createTopBar(
svgRoot: SVGGElement,
workspace: WorkspaceSvg,
Expand Down Expand Up @@ -121,7 +170,7 @@ export class CommentView implements IRenderedElement {
foldoutIcon,
'pointerdown',
this,
this.onFoldoutUp,
this.onFoldoutDown,
);
browserEvents.conditionalBind(
deleteIcon,
Expand All @@ -133,6 +182,9 @@ export class CommentView implements IRenderedElement {
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;
Expand Down Expand Up @@ -162,6 +214,7 @@ export class CommentView implements IRenderedElement {
return {foreignObject, textArea};
}

/** Creates the DOM elements for the comment resize handle. */
private createResizeHandle(
svgRoot: SVGGElement,
workspace: WorkspaceSvg,
Expand All @@ -185,14 +238,20 @@ export class CommentView implements IRenderedElement {
return resizeHandle;
}

getSvgRoot(): SVGElement {
/** 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),
Expand Down Expand Up @@ -229,6 +288,7 @@ export class CommentView implements IRenderedElement {
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',
Expand All @@ -241,6 +301,9 @@ export class CommentView implements IRenderedElement {
}
}

/**
* Updates the position of the delete icon elements to reflect the new size.
*/
private updateDeleteIconPosition(
size: Size,
topBarSize: Size,
Expand All @@ -254,12 +317,18 @@ export class CommentView implements IRenderedElement {
);
}

/**
* 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,
Expand Down Expand Up @@ -288,16 +357,25 @@ export class CommentView implements IRenderedElement {
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) {
for (const listener of this.sizeChangeListeners) {
listener(oldSize, newSize);
}
}

/** Registers a callback that listens for size changes. */
addSizeChangeListener(listener: (oldSize: Size, newSize: Size) => void) {
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
this.sizeChangeListeners.push(listener);
}

/**
* Handles starting an interaction with the resize handle to resize the
* comment.
*/
private onResizePointerDown(e: PointerEvent) {
this.bringToFront();
if (browserEvents.isRightButton(e)) {
Expand Down Expand Up @@ -333,6 +411,7 @@ export class CommentView implements IRenderedElement {
e.stopPropagation();
}

/** Ends an interaction with the resize handle. */
private onResizePointerUp(_e: PointerEvent) {
touch.clearTouchIdentifier();
if (this.resizePointerUpListener) {
Expand All @@ -345,15 +424,18 @@ export class CommentView implements IRenderedElement {
}
}

/** Resizes the comment in response to a drag on the resize handle. */
private onResizePointerMove(e: PointerEvent) {
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) {
Expand All @@ -366,17 +448,26 @@ export class CommentView implements IRenderedElement {
this.onCollapse();
}

/**
* Triggers listeners when the collapsed-ness of the comment changes, either
* progrmatically or manually by the user.
*/
private onCollapse() {
for (const listener of this.collapseChangeListeners) {
listener(this.collapsed);
}
}

/** Registers a callback that listens for collapsed-ness changes. */
addOnCollapseListener(listener: (newCollapse: boolean) => void) {
this.collapseChangeListeners.push(listener);
}

private onFoldoutUp(e: PointerEvent) {
/**
* 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();
Expand All @@ -390,10 +481,12 @@ export class CommentView implements IRenderedElement {
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) {
Expand All @@ -407,6 +500,7 @@ export class CommentView implements IRenderedElement {
}
}

/** Returns the current location of the comment in workspace coordinates. */
getRelativeToSurfaceXY(): Coordinate {
return this.location;
}
Expand All @@ -424,19 +518,26 @@ export class CommentView implements IRenderedElement {
);
}

/** 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);
}

/**
* 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;
Expand All @@ -446,11 +547,15 @@ export class CommentView implements IRenderedElement {
}
}

/** Truncates the text to fit within the top view. */
private truncateText(text: string): string {
// TODO: before merging ile an issue to calculate how much this should be
// truncated automatically.
return text.length >= 12 ? `${text.substring(0, 9)}...` : text;
}

bringToFront() {
/** 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.
Expand All @@ -459,6 +564,10 @@ export class CommentView implements IRenderedElement {
}
}

/**
* Handles disposing of the comment when we get a pointer down event on the
* delete icon.
*/
private onDeleteDown(e: PointerEvent) {
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
if (browserEvents.isRightButton(e)) {
e.stopPropagation();
Expand All @@ -469,13 +578,15 @@ export class CommentView implements IRenderedElement {
e.stopPropagation();
}

/** Disposes of this comment view. */
dispose() {
dom.removeNode(this.svgRoot);
for (const listener of this.disposeListeners) {
listener();
}
}

/** Registers a callback that listens for disposal of this view. */
addDisposeListener(listener: () => void) {
this.disposeListeners.push(listener);
}
Expand Down