-
Notifications
You must be signed in to change notification settings - Fork 3.9k
feat: make comment editor separately focusable from comment itself #9154
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
Changes from 1 commit
3f1d3f6
ca665ea
6bc398b
4cdb8b1
ead37db
eeae364
7f5544c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2024 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as browserEvents from '../browser_events.js'; | ||
| import {getFocusManager} from '../focus_manager.js'; | ||
| import {IFocusableNode} from '../interfaces/i_focusable_node.js'; | ||
| import {IFocusableTree} from '../interfaces/i_focusable_tree.js'; | ||
| import * as dom from '../utils/dom.js'; | ||
| import {Svg} from '../utils/svg.js'; | ||
| import {WorkspaceSvg} from '../workspace_svg.js'; | ||
|
|
||
| export class CommentEditor implements IFocusableNode { | ||
| id?: string; | ||
| /** The foreignObject containing the HTML text area. */ | ||
| foreignObject: SVGForeignObjectElement; | ||
|
|
||
| /** The text area where the user can type. */ | ||
| textArea: HTMLTextAreaElement; | ||
|
|
||
| constructor( | ||
| public workspace: WorkspaceSvg, | ||
| commentSvgRoot: SVGGElement, | ||
| commentId?: string, | ||
| ) { | ||
| this.foreignObject = dom.createSvgElement( | ||
| Svg.FOREIGNOBJECT, | ||
| { | ||
| 'class': 'blocklyCommentForeignObject', | ||
| }, | ||
| commentSvgRoot, | ||
| ); | ||
| const body = document.createElementNS(dom.HTML_NS, 'body'); | ||
| body.setAttribute('xmlns', dom.HTML_NS); | ||
| body.className = 'blocklyMinimalBody'; | ||
| this.textArea = document.createElementNS( | ||
| dom.HTML_NS, | ||
| 'textarea', | ||
| ) as HTMLTextAreaElement; | ||
| dom.addClass(this.textArea, 'blocklyCommentText'); | ||
| dom.addClass(this.textArea, 'blocklyTextarea'); | ||
| dom.addClass(this.textArea, 'blocklyText'); | ||
| body.appendChild(this.textArea); | ||
| this.foreignObject.appendChild(body); | ||
|
|
||
| if (commentId) { | ||
| this.id = commentId + '_textarea_'; | ||
| this.textArea.setAttribute('id', this.id); | ||
| } | ||
|
|
||
| browserEvents.conditionalBind( | ||
| this.textArea, | ||
| 'pointerdown', | ||
| this, | ||
| (e: PointerEvent) => { | ||
| // don't allow this event to bubble up | ||
| // and steal focus away from the editor/comment. | ||
| e.stopPropagation(); | ||
| getFocusManager().focusNode(this); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| getFocusableElement(): HTMLElement | SVGElement { | ||
| return this.textArea; | ||
| } | ||
| getFocusableTree(): IFocusableTree { | ||
| return this.workspace; | ||
| } | ||
| onNodeFocus(): void {} | ||
| onNodeBlur(): void {} | ||
| canBeFocused(): boolean { | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ 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 {CommentEditor} from './comment_editor.js'; | ||
|
|
||
| export class CommentView implements IRenderedElement { | ||
| /** The root group element of the comment view. */ | ||
|
|
@@ -106,7 +107,12 @@ export class CommentView implements IRenderedElement { | |
| /** The default size of newly created comments. */ | ||
| static defaultCommentSize = new Size(120, 100); | ||
|
|
||
| constructor(readonly workspace: WorkspaceSvg) { | ||
| public commentEditor: CommentEditor | undefined; | ||
|
|
||
| constructor( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative approach might be to make
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I just realized the oddity here. I suspect there's some way to clean this whole thing up a bit more, but I disagree with my previous comment. It does seem like the new class is needed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CommentView is an effort to separate the view logic from the business logic for the workspace comments, as opposed to everywhere else in blockly where we just mix the two willy nilly. So I think it does make sense that |
||
| readonly workspace: WorkspaceSvg, | ||
| private commentId?: string, | ||
| ) { | ||
| this.svgRoot = dom.createSvgElement(Svg.G, { | ||
| 'class': 'blocklyComment blocklyEditable blocklyDraggable', | ||
| }); | ||
|
|
@@ -240,26 +246,12 @@ export class CommentView implements IRenderedElement { | |
| foreignObject: SVGForeignObjectElement; | ||
| textArea: HTMLTextAreaElement; | ||
| } { | ||
| const foreignObject = dom.createSvgElement( | ||
| Svg.FOREIGNOBJECT, | ||
| { | ||
| 'class': 'blocklyCommentForeignObject', | ||
| }, | ||
| this.commentEditor = new CommentEditor( | ||
| this.workspace, | ||
| svgRoot, | ||
| this.commentId, | ||
| ); | ||
| 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); | ||
|
|
||
| const {foreignObject, textArea} = this.commentEditor; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming this is temporary, but if not we probably don't want to destructure a class like this since the class could change.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't this the same as doing ? if the class properties change, either one is going to have to change.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. Yes you're correct. I thought that |
||
| browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); | ||
|
|
||
| return {foreignObject, textArea}; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2715,6 +2715,7 @@ export class WorkspaceSvg | |
|
|
||
| /** See IFocusableTree.lookUpFocusableNode. */ | ||
| lookUpFocusableNode(id: string): IFocusableNode | null { | ||
| if (!id) return null; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed when logging there's a bunch of times that this function is triggered with an empty string for the id, which will never match anything so there's just a bunch of wasted work getting all the blocks and comments and connections and whatnot.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only place that we call I suspect this is happening for all the cases We should patch it in the traverser, I think, rather than here. In fact, we can probably short-circuit that entire function and just return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, I filed #9155 to clean that up separately and I'll remove it from this PR. |
||
| // Check against flyout items if this workspace is part of a flyout. Note | ||
| // that blocks may match against this pass before reaching getBlockById() | ||
| // below (but only for a flyout workspace). | ||
|
|
@@ -2757,21 +2758,31 @@ export class WorkspaceSvg | |
| return null; | ||
| } | ||
|
|
||
| // Search for a specific workspace comment editor | ||
| // (only if id seems like it is one). | ||
| const commentEditorIndicator = id.indexOf('_textarea_'); | ||
| if (commentEditorIndicator !== -1) { | ||
| const commentId = id.substring(0, commentEditorIndicator); | ||
| const comment = this.getCommentById(commentId); | ||
| if ( | ||
| comment instanceof RenderedWorkspaceComment && | ||
| comment.canBeFocused() && | ||
| comment.view.commentEditor | ||
| ) { | ||
| return comment.view.commentEditor; | ||
| } | ||
| } | ||
|
|
||
| // Search for a specific block. | ||
| const block = this.getAllBlocks(false).find( | ||
| (block) => block.getFocusableElement().id === id, | ||
| ); | ||
| if (block) return block; | ||
|
|
||
| // Search for a workspace comment (semi-expensive). | ||
| for (const comment of this.getTopComments()) { | ||
| if ( | ||
| comment instanceof RenderedWorkspaceComment && | ||
| comment.canBeFocused() && | ||
| comment.getFocusableElement().id === id | ||
| ) { | ||
| return comment; | ||
| } | ||
| // Search for a workspace comment. | ||
| const comment = this.getCommentById(id); | ||
| if (comment instanceof RenderedWorkspaceComment && comment.canBeFocused()) { | ||
| return comment; | ||
| } | ||
|
|
||
| // Search for icons and bubbles (which requires an expensive getAllBlocks). | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels a bit like a dependency inversion to me. I'm wondering if we could follow the
createDompattern here and have the holding object for this new class create and add the editor as a child, rather than the editor self-registering itself.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, this doesn't need to be passed in, the parent can create it and append it to itself. This was my quick and dirty approach to make sure it works (it does!) and I'll definitely do another pass to clean this up and make sure it's thoroughly reviewed. The other thing I haven't given thought to yet is that we intend for
RenderedWorkspaceCommentto be subclassable so I need to make sure the solution is suitable for subclasses as well.