Skip to content
Closed
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
5 changes: 5 additions & 0 deletions core/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {Input} from './inputs/input.js';
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as blocks from './serialization/blocks.js';
import {idGenerator} from './utils.js';
import * as Xml from './xml.js';

/**
Expand Down Expand Up @@ -55,6 +56,9 @@ export class Connection implements IASTNodeLocationWithBlock {
/** DOM representation of a shadow block, or null if none. */
private shadowDom: Element | null = null;

/** The unique ID of this connection. */
id: string;

/**
* Horizontal location of this connection.
*
Expand All @@ -80,6 +84,7 @@ export class Connection implements IASTNodeLocationWithBlock {
public type: number,
) {
this.sourceBlock_ = source;
this.id = `${source.id}_connection_${idGenerator.getNextUniqueId()}`;
}

/**
Expand Down
57 changes: 54 additions & 3 deletions core/rendered_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@ import * as ContextMenu from './contextmenu.js';
import {ContextMenuRegistry} from './contextmenu_registry.js';
import * as eventUtils from './events/utils.js';
import {IContextMenu} from './interfaces/i_contextmenu.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {hasBubble} from './interfaces/i_has_bubble.js';
import * as internalConstants from './internal_constants.js';
import {Coordinate} from './utils/coordinate.js';
import * as svgMath from './utils/svg_math.js';
import {WorkspaceSvg} from './workspace_svg.js';

/** Maximum randomness in workspace units for bumping a block. */
const BUMP_RANDOMNESS = 10;

/**
* Class for a connection between blocks that may be rendered on screen.
*/
export class RenderedConnection extends Connection implements IContextMenu {
export class RenderedConnection
extends Connection
implements IContextMenu, IFocusableNode
{
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
sourceBlock_!: BlockSvg;
private readonly db: ConnectionDB;
Expand Down Expand Up @@ -320,13 +326,28 @@ export class RenderedConnection extends Connection implements IContextMenu {
/** Add highlighting around this connection. */
highlight() {
this.highlighted = true;
this.getSourceBlock().queueRender();

// Note that this needs to be done synchronously (vs. queuing a render pass)
// since only a displayed element can be focused, and this focusable node is
// implemented to make itself visible immediately prior to receiving DOM
// focus. It's expected that the connection's position should already be
// correct by this point (otherwise it will be corrected in a subsequent
// draw pass).
const highlightSvg = this.findHighlightSvg();
if (highlightSvg) {
highlightSvg.style.display = '';
}
}

/** Remove the highlighting around this connection. */
unhighlight() {
this.highlighted = false;
this.getSourceBlock().queueRender();

// Note that this is done synchronously for parity with highlight().
const highlightSvg = this.findHighlightSvg();
if (highlightSvg) {
highlightSvg.style.display = 'none';
}
}

/** Returns true if this connection is highlighted, false otherwise. */
Expand Down Expand Up @@ -626,6 +647,36 @@ export class RenderedConnection extends Connection implements IContextMenu {

ContextMenu.show(e, menuOptions, block.RTL, workspace, location);
}

/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
const highlightSvg = this.findHighlightSvg();
if (highlightSvg) return highlightSvg;
throw new Error('No highlight SVG found corresponding to this connection.');
}

/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
return this.getSourceBlock().workspace as WorkspaceSvg;
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {
this.highlight();
}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {
this.unhighlight();
}

private findHighlightSvg(): SVGElement | null {
// This cast is valid as TypeScript's definition is wrong. See:
// https://github.com/microsoft/TypeScript/issues/60996.
return document.getElementById(this.id) as
| unknown
| null as SVGElement | null;
}
}

export namespace RenderedConnection {
Expand Down
13 changes: 5 additions & 8 deletions core/renderers/common/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,19 +435,16 @@ export class Drawer {
for (const elem of row.elements) {
if (!(elem instanceof Connection)) continue;

if (elem.highlighted) {
this.drawConnectionHighlightPath(elem);
} else {
this.block_.pathObject.removeConnectionHighlight?.(
elem.connectionModel,
);
const highlightSvg = this.drawConnectionHighlightPath(elem);
if (highlightSvg) {
highlightSvg.style.display = elem.highlighted ? '' : 'none';
}
}
}
}

/** Returns a path to highlight the given connection. */
drawConnectionHighlightPath(measurable: Connection) {
drawConnectionHighlightPath(measurable: Connection): SVGElement | undefined {
const conn = measurable.connectionModel;
let path = '';
if (
Expand All @@ -459,7 +456,7 @@ export class Drawer {
path = this.getStatementConnectionHighlightPath(measurable);
}
const block = conn.getSourceBlock();
block.pathObject.addConnectionHighlight?.(
return block.pathObject.addConnectionHighlight?.(
conn,
path,
conn.getOffsetInBlock(),
Expand Down
2 changes: 1 addition & 1 deletion core/renderers/common/i_path_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export interface IPathObject {
connectionPath: string,
offset: Coordinate,
rtl: boolean,
): void;
): SVGElement;

/**
* Apply the stored colours to the block's path, taking into account whether
Expand Down
36 changes: 16 additions & 20 deletions core/renderers/common/path_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,37 +268,33 @@ export class PathObject implements IPathObject {
connectionPath: string,
offset: Coordinate,
rtl: boolean,
) {
if (this.connectionHighlights.has(connection)) {
if (this.currentHighlightMatchesNew(connection, connectionPath, offset)) {
return;
}
this.removeConnectionHighlight(connection);
): SVGElement {
const transformation =
`translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : '');

const previousHighlight = this.connectionHighlights.get(connection);
if (previousHighlight) {
// Since a connection already exists, make sure that its path and
// transform are correct.
previousHighlight.setAttribute('d', connectionPath);
previousHighlight.setAttribute('transform', transformation);
return previousHighlight;
}

const highlight = dom.createSvgElement(
Svg.PATH,
{
'id': connection.id,
'class': 'blocklyHighlightedConnectionPath',
'style': 'display: none;',
'tabindex': '-1',
'd': connectionPath,
'transform':
`translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''),
'transform': transformation,
},
this.svgRoot,
);
this.connectionHighlights.set(connection, highlight);
}

private currentHighlightMatchesNew(
connection: RenderedConnection,
newPath: string,
newOffset: Coordinate,
): boolean {
const currPath = this.connectionHighlights
.get(connection)
?.getAttribute('d');
const currOffset = this.highlightOffsets.get(connection);
return currPath === newPath && Coordinate.equals(currOffset, newOffset);
return highlight;
}

/**
Expand Down
9 changes: 5 additions & 4 deletions core/renderers/zelos/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,16 @@ export class Drawer extends BaseDrawer {
}

/** Returns a path to highlight the given connection. */
drawConnectionHighlightPath(measurable: Connection) {
override drawConnectionHighlightPath(
measurable: Connection,
): SVGElement | undefined {
const conn = measurable.connectionModel;
if (
conn.type === ConnectionType.NEXT_STATEMENT ||
conn.type === ConnectionType.PREVIOUS_STATEMENT ||
(conn.type === ConnectionType.OUTPUT_VALUE && !measurable.isDynamicShape)
) {
super.drawConnectionHighlightPath(measurable);
return;
return super.drawConnectionHighlightPath(measurable);
}

let path = '';
Expand All @@ -261,7 +262,7 @@ export class Drawer extends BaseDrawer {
(output.shape as DynamicShape).pathDown(output.height);
}
const block = conn.getSourceBlock();
block.pathObject.addConnectionHighlight?.(
return block.pathObject.addConnectionHighlight?.(
conn,
path,
conn.getOffsetInBlock(),
Expand Down
10 changes: 10 additions & 0 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,7 @@ export class WorkspaceSvg
}

const fieldIndicatorIndex = id.indexOf('_field_');
const connectionIndicatorIndex = id.indexOf('_connection_');
if (fieldIndicatorIndex !== -1) {
const blockId = id.substring(0, fieldIndicatorIndex);
const block = this.getBlockById(blockId);
Expand All @@ -2719,6 +2720,15 @@ export class WorkspaceSvg
}
}
return null;
} else if (connectionIndicatorIndex !== -1) {
const blockId = id.substring(0, connectionIndicatorIndex);
const block = this.getBlockById(blockId);
if (block) {
for (const connection of block.getConnections_(true)) {
if (connection.id === id) return connection;
}
}
return null;
}

return this.getBlockById(id) as IFocusableNode;
Expand Down