Skip to content

Commit ca665ea

Browse files
committed
feat: improve design and add styling
1 parent 3f1d3f6 commit ca665ea

4 files changed

Lines changed: 170 additions & 98 deletions

File tree

core/comments/comment_editor.ts

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,34 @@ import {getFocusManager} from '../focus_manager.js';
99
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
1010
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
1111
import * as dom from '../utils/dom.js';
12+
import {Size} from '../utils/size.js';
1213
import {Svg} from '../utils/svg.js';
1314
import {WorkspaceSvg} from '../workspace_svg.js';
1415

16+
/** The part of a comment that can be typed into. */
1517
export class CommentEditor implements IFocusableNode {
1618
id?: string;
1719
/** The foreignObject containing the HTML text area. */
18-
foreignObject: SVGForeignObjectElement;
20+
private foreignObject: SVGForeignObjectElement;
1921

2022
/** The text area where the user can type. */
21-
textArea: HTMLTextAreaElement;
23+
private textArea: HTMLTextAreaElement;
24+
25+
/** Listeners for changes to text. */
26+
private textChangeListeners: Array<
27+
(oldText: string, newText: string) => void
28+
> = [];
29+
30+
/** The current text of the comment. Updates on text area change. */
31+
private text: string = '';
2232

2333
constructor(
2434
public workspace: WorkspaceSvg,
25-
commentSvgRoot: SVGGElement,
2635
commentId?: string,
2736
) {
28-
this.foreignObject = dom.createSvgElement(
29-
Svg.FOREIGNOBJECT,
30-
{
31-
'class': 'blocklyCommentForeignObject',
32-
},
33-
commentSvgRoot,
34-
);
37+
this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
38+
'class': 'blocklyCommentForeignObject',
39+
});
3540
const body = document.createElementNS(dom.HTML_NS, 'body');
3641
body.setAttribute('xmlns', dom.HTML_NS);
3742
body.className = 'blocklyMinimalBody';
@@ -46,10 +51,19 @@ export class CommentEditor implements IFocusableNode {
4651
this.foreignObject.appendChild(body);
4752

4853
if (commentId) {
49-
this.id = commentId + '_textarea_';
54+
this.id = commentId + '_comment_textarea_';
5055
this.textArea.setAttribute('id', this.id);
5156
}
5257

58+
// Register browser event listeners for the user typing in the textarea.
59+
browserEvents.conditionalBind(
60+
this.textArea,
61+
'change',
62+
this,
63+
this.onTextChange,
64+
);
65+
66+
// Register listener for pointerdown to focus the textarea.
5367
browserEvents.conditionalBind(
5468
this.textArea,
5569
'pointerdown',
@@ -63,6 +77,74 @@ export class CommentEditor implements IFocusableNode {
6377
);
6478
}
6579

80+
/** Gets the dom structure for this comment editor. */
81+
getDom(): SVGForeignObjectElement {
82+
return this.foreignObject;
83+
}
84+
85+
/** Gets the current text of the comment. */
86+
getText(): string {
87+
return this.text;
88+
}
89+
/** Sets the current text of the comment and fires change listeners. */
90+
setText(text: string) {
91+
this.textArea.value = text;
92+
this.onTextChange();
93+
}
94+
95+
/**
96+
* Triggers listeners when the text of the comment changes, either
97+
* programmatically or manually by the user.
98+
*/
99+
private onTextChange() {
100+
const oldText = this.text;
101+
this.text = this.textArea.value;
102+
// Loop through listeners backwards in case they remove themselves.
103+
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
104+
this.textChangeListeners[i](oldText, this.text);
105+
}
106+
}
107+
108+
/** Registers a callback that listens for text changes. */
109+
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
110+
this.textChangeListeners.push(listener);
111+
}
112+
113+
/** Removes the given listener from the list of text change listeners. */
114+
removeTextChangeListener(listener: () => void) {
115+
this.textChangeListeners.splice(
116+
this.textChangeListeners.indexOf(listener),
117+
1,
118+
);
119+
}
120+
121+
/** Sets the placeholder text displayed for an empty comment. */
122+
setPlaceholderText(text: string) {
123+
this.textArea.placeholder = text;
124+
}
125+
126+
/** Sets whether the textarea is editable. If not, the textarea will be readonly. */
127+
setEditable(isEditable: boolean) {
128+
if (isEditable) {
129+
this.textArea.removeAttribute('readonly');
130+
} else {
131+
this.textArea.setAttribute('readonly', 'true');
132+
}
133+
}
134+
135+
/** Update the size of the comment editor element. */
136+
updateSize(size: Size, topBarSize: Size) {
137+
this.foreignObject.setAttribute(
138+
'height',
139+
`${size.height - topBarSize.height}`,
140+
);
141+
this.foreignObject.setAttribute('width', `${size.width}`);
142+
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
143+
if (this.workspace.RTL) {
144+
this.foreignObject.setAttribute('x', `${-size.width}`);
145+
}
146+
}
147+
66148
getFocusableElement(): HTMLElement | SVGElement {
67149
return this.textArea;
68150
}
@@ -72,6 +154,7 @@ export class CommentEditor implements IFocusableNode {
72154
onNodeFocus(): void {}
73155
onNodeBlur(): void {}
74156
canBeFocused(): boolean {
75-
return true;
157+
if (this.id) return true;
158+
return false;
76159
}
77160
}

core/comments/comment_view.ts

Lines changed: 39 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import * as browserEvents from '../browser_events.js';
88
import * as css from '../css.js';
9+
import type {IFocusableNode} from '../interfaces/i_focusable_node';
910
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
1011
import * as layers from '../layers.js';
1112
import * as touch from '../touch.js';
@@ -47,11 +48,8 @@ export class CommentView implements IRenderedElement {
4748
/** The resize handle element. */
4849
private resizeHandle: SVGImageElement;
4950

50-
/** The foreignObject containing the HTML text area. */
51-
private foreignObject: SVGForeignObjectElement;
52-
53-
/** The text area where the user can type. */
54-
private textArea: HTMLTextAreaElement;
51+
/** The part of the comment view that contains the textarea to edit the comment. */
52+
private commentEditor: CommentEditor;
5553

5654
/** The current size of the comment in workspace units. */
5755
private size: Size;
@@ -65,14 +63,6 @@ export class CommentView implements IRenderedElement {
6563
/** The current location of the comment in workspace coordinates. */
6664
private location: Coordinate = new Coordinate(0, 0);
6765

68-
/** The current text of the comment. Updates on text area change. */
69-
private text: string = '';
70-
71-
/** Listeners for changes to text. */
72-
private textChangeListeners: Array<
73-
(oldText: string, newText: string) => void
74-
> = [];
75-
7666
/** Listeners for changes to size. */
7767
private sizeChangeListeners: Array<(oldSize: Size, newSize: Size) => void> =
7868
[];
@@ -107,8 +97,6 @@ export class CommentView implements IRenderedElement {
10797
/** The default size of newly created comments. */
10898
static defaultCommentSize = new Size(120, 100);
10999

110-
public commentEditor: CommentEditor | undefined;
111-
112100
constructor(
113101
readonly workspace: WorkspaceSvg,
114102
private commentId?: string,
@@ -128,8 +116,7 @@ export class CommentView implements IRenderedElement {
128116
textPreviewNode: this.textPreviewNode,
129117
} = this.createTopBar(this.svgRoot, workspace));
130118

131-
({foreignObject: this.foreignObject, textArea: this.textArea} =
132-
this.createTextArea(this.svgRoot));
119+
this.commentEditor = this.createTextArea(this.svgRoot);
133120

134121
this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace);
135122

@@ -242,19 +229,26 @@ export class CommentView implements IRenderedElement {
242229
/**
243230
* Creates the text area where users can type. Registers event listeners.
244231
*/
245-
private createTextArea(svgRoot: SVGGElement): {
246-
foreignObject: SVGForeignObjectElement;
247-
textArea: HTMLTextAreaElement;
248-
} {
249-
this.commentEditor = new CommentEditor(
250-
this.workspace,
251-
svgRoot,
252-
this.commentId,
253-
);
254-
const {foreignObject, textArea} = this.commentEditor;
255-
browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange);
232+
private createTextArea(svgRoot: SVGGElement) {
233+
const commentEditor = new CommentEditor(this.workspace, this.commentId);
234+
235+
this.svgRoot.appendChild(commentEditor.getDom());
256236

257-
return {foreignObject, textArea};
237+
commentEditor.addTextChangeListener((oldText, newText) => {
238+
this.updateTextPreview(newText);
239+
// Update size in case our minimum size increased.
240+
this.setSize(this.size);
241+
});
242+
243+
return commentEditor;
244+
}
245+
246+
/**
247+
*
248+
* @returns The FocusableNode representing the editor portion of this comment.
249+
*/
250+
getEditorFocusableNode(): IFocusableNode {
251+
return this.commentEditor;
258252
}
259253

260254
/** Creates the DOM elements for the comment resize handle. */
@@ -316,7 +310,7 @@ export class CommentView implements IRenderedElement {
316310

317311
this.updateHighlightRect(size);
318312
this.updateTopBarSize(size);
319-
this.updateTextAreaSize(size, topBarSize);
313+
this.commentEditor.updateSize(size, topBarSize);
320314
this.updateDeleteIconPosition(size, topBarSize, deleteSize);
321315
this.updateFoldoutIconPosition(topBarSize, foldoutSize);
322316
this.updateTextPreviewSize(
@@ -352,7 +346,7 @@ export class CommentView implements IRenderedElement {
352346
foldoutSize: Size,
353347
deleteSize: Size,
354348
): Size {
355-
this.updateTextPreview(this.textArea.value ?? '');
349+
this.updateTextPreview(this.commentEditor.getText() ?? '');
356350
const textPreviewWidth = dom.getTextWidth(this.textPreview);
357351

358352
const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize);
@@ -400,19 +394,6 @@ export class CommentView implements IRenderedElement {
400394
this.topBarBackground.setAttribute('width', `${size.width}`);
401395
}
402396

403-
/** Updates the size of the text area elements to reflect the new size. */
404-
private updateTextAreaSize(size: Size, topBarSize: Size) {
405-
this.foreignObject.setAttribute(
406-
'height',
407-
`${size.height - topBarSize.height}`,
408-
);
409-
this.foreignObject.setAttribute('width', `${size.width}`);
410-
this.foreignObject.setAttribute('y', `${topBarSize.height}`);
411-
if (this.workspace.RTL) {
412-
this.foreignObject.setAttribute('x', `${-size.width}`);
413-
}
414-
}
415-
416397
/**
417398
* Updates the position of the delete icon elements to reflect the new size.
418399
*/
@@ -644,12 +625,11 @@ export class CommentView implements IRenderedElement {
644625
if (this.editable) {
645626
dom.addClass(this.svgRoot, 'blocklyEditable');
646627
dom.removeClass(this.svgRoot, 'blocklyReadonly');
647-
this.textArea.removeAttribute('readonly');
648628
} else {
649629
dom.removeClass(this.svgRoot, 'blocklyEditable');
650630
dom.addClass(this.svgRoot, 'blocklyReadonly');
651-
this.textArea.setAttribute('readonly', 'true');
652631
}
632+
this.commentEditor.setEditable(editable);
653633
}
654634

655635
/** Returns the current location of the comment in workspace coordinates. */
@@ -670,49 +650,29 @@ export class CommentView implements IRenderedElement {
670650
);
671651
}
672652

673-
/** Retursn the current text of the comment. */
653+
/** Returns the current text of the comment. */
674654
getText() {
675-
return this.text;
655+
return this.commentEditor.getText();
676656
}
677657

678658
/** Sets the current text of the comment. */
679659
setText(text: string) {
680-
this.textArea.value = text;
681-
this.onTextChange();
660+
this.commentEditor.setText(text);
682661
}
683662

684663
/** Sets the placeholder text displayed for an empty comment. */
685664
setPlaceholderText(text: string) {
686-
this.textArea.placeholder = text;
665+
this.commentEditor.setPlaceholderText(text);
687666
}
688667

689-
/** Registers a callback that listens for text changes. */
668+
/** Registers a callback that listens for text changes on the comment editor. */
690669
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
691-
this.textChangeListeners.push(listener);
670+
this.commentEditor.addTextChangeListener(listener);
692671
}
693672

694-
/** Removes the given listener from the list of text change listeners. */
673+
/** Removes the given listener from the comment editor. */
695674
removeTextChangeListener(listener: () => void) {
696-
this.textChangeListeners.splice(
697-
this.textChangeListeners.indexOf(listener),
698-
1,
699-
);
700-
}
701-
702-
/**
703-
* Triggers listeners when the text of the comment changes, either
704-
* programmatically or manually by the user.
705-
*/
706-
private onTextChange() {
707-
const oldText = this.text;
708-
this.text = this.textArea.value;
709-
this.updateTextPreview(this.text);
710-
// Update size in case our minimum size increased.
711-
this.setSize(this.size);
712-
// Loop through listeners backwards in case they remove themselves.
713-
for (let i = this.textChangeListeners.length - 1; i >= 0; i--) {
714-
this.textChangeListeners[i](oldText, this.text);
715-
}
675+
this.commentEditor.removeTextChangeListener(listener);
716676
}
717677

718678
/** Updates the preview text element to reflect the given text. */
@@ -876,6 +836,11 @@ css.register(`
876836
fill: none;
877837
}
878838
839+
.blocklyCommentText.blocklyActiveFocus {
840+
border-color: #fc3;
841+
border-width: 2px;
842+
}
843+
879844
.blocklySelected .blocklyCommentHighlight {
880845
stroke: #fc3;
881846
stroke-width: 3px;

core/comments/rendered_workspace_comment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ export class RenderedWorkspaceComment
333333
}
334334
}
335335

336+
/**
337+
* @returns The FocusableNode representing the editor portion of this comment.
338+
*/
339+
getEditorFocusableNode(): IFocusableNode {
340+
return this.view.getEditorFocusableNode();
341+
}
342+
336343
/** See IFocusableNode.getFocusableElement. */
337344
getFocusableElement(): HTMLElement | SVGElement {
338345
return this.getSvgRoot();

0 commit comments

Comments
 (0)