Skip to content

Commit

Permalink
feat: Add inflaters for flyout labels and buttons. (#8593)
Browse files Browse the repository at this point in the history
* feat: Add inflaters for flyout labels and buttons.

* chore: Temporarily re-add createDom().

* chore: fix JSDoc.

* chore: Add license.

* chore: Add TSDoc.
  • Loading branch information
gonfunko authored Sep 27, 2024
1 parent 6ec1bc5 commit 489aded
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 19 deletions.
63 changes: 63 additions & 0 deletions core/button_flyout_inflater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {FlyoutButton} from './flyout_button.js';
import {ButtonOrLabelInfo} from './utils/toolbox.js';
import * as registry from './registry.js';

/**
* Class responsible for creating buttons for flyouts.
*/
export class ButtonFlyoutInflater implements IFlyoutInflater {
/**
* Inflates a flyout button from the given state and adds it to the flyout.
*
* @param state A JSON representation of a flyout button.
* @param flyoutWorkspace The workspace to create the button on.
* @returns A newly created FlyoutButton.
*/
load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
const button = new FlyoutButton(
flyoutWorkspace,
flyoutWorkspace.targetWorkspace!,
state as ButtonOrLabelInfo,
false,
);
button.show();
return button;
}

/**
* Returns the amount of space that should follow this button.
*
* @param state A JSON representation of a flyout button.
* @param defaultGap The default spacing for flyout items.
* @returns The amount of space that should follow this button.
*/
gapForElement(state: Object, defaultGap: number): number {
return defaultGap;
}

/**
* Disposes of the given button.
*
* @param element The flyout button to dispose of.
*/
disposeElement(element: IBoundedElement): void {
if (element instanceof FlyoutButton) {
element.dispose();
}
}
}

registry.register(
registry.Type.FLYOUT_INFLATER,
'button',
ButtonFlyoutInflater,
);
85 changes: 66 additions & 19 deletions core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ import * as style from './utils/style.js';
import {Svg} from './utils/svg.js';
import type * as toolbox from './utils/toolbox.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import type {IASTNodeLocationSvg} from './blockly.js';
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
import {Rect} from './utils/rect.js';

/**
* Class for a button or label in the flyout.
*/
export class FlyoutButton implements IASTNodeLocationSvg {
export class FlyoutButton
implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement
{
/** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5;

Expand All @@ -41,7 +46,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {
private readonly cssClass: string | null;

/** Mouse up event data. */
private onMouseUpWrapper: browserEvents.Data | null = null;
private onMouseDownWrapper: browserEvents.Data;
private onMouseUpWrapper: browserEvents.Data;
info: toolbox.ButtonOrLabelInfo;

/** The width of the button's rect. */
Expand All @@ -51,7 +57,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
height = 0;

/** The root SVG group for the button or label. */
private svgGroup: SVGGElement | null = null;
private svgGroup: SVGGElement;

/** The SVG element with the text of the label or button. */
private svgText: SVGTextElement | null = null;
Expand Down Expand Up @@ -92,14 +98,6 @@ export class FlyoutButton implements IASTNodeLocationSvg {

/** The JSON specifying the label / button. */
this.info = json;
}

/**
* Create the button elements.
*
* @returns The button's SVG group.
*/
createDom(): SVGElement {
let cssClass = this.isFlyoutLabel
? 'blocklyFlyoutLabel'
: 'blocklyFlyoutButton';
Expand Down Expand Up @@ -198,15 +196,24 @@ export class FlyoutButton implements IASTNodeLocationSvg {

this.updateTransform();

// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'EventTarget'.
this.onMouseDownWrapper = browserEvents.conditionalBind(
this.svgGroup,
'pointerdown',
this,
this.onMouseDown,
);
this.onMouseUpWrapper = browserEvents.conditionalBind(
this.svgGroup as AnyDuringMigration,
this.svgGroup,
'pointerup',
this,
this.onMouseUp,
);
return this.svgGroup!;
}

createDom(): SVGElement {
// No-op, now handled in constructor. Will be removed in followup refactor
// PR that updates the flyout classes to use inflaters.
return this.svgGroup;
}

/** Correctly position the flyout button and make it visible. */
Expand Down Expand Up @@ -235,6 +242,17 @@ export class FlyoutButton implements IASTNodeLocationSvg {
this.updateTransform();
}

/**
* Move the element by a relative offset.
*
* @param dx Horizontal offset in workspace units.
* @param dy Vertical offset in workspace units.
* @param _reason Why is this move happening? 'user', 'bump', 'snap'...
*/
moveBy(dx: number, dy: number, _reason?: string[]) {
this.moveTo(this.position.x + dx, this.position.y + dy);
}

/** @returns Whether or not the button is a label. */
isLabel(): boolean {
return this.isFlyoutLabel;
Expand All @@ -250,6 +268,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
return this.position;
}

/**
* Returns the coordinates of a bounded element describing the dimensions of
* the element. Coordinate system: workspace coordinates.
*
* @returns Object with coordinates of the bounded element.
*/
getBoundingRectangle() {
return new Rect(
this.position.y,
this.position.y + this.height,
this.position.x,
this.position.x + this.width,
);
}

/** @returns Text of the button. */
getButtonText(): string {
return this.text;
Expand All @@ -275,9 +308,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {

/** Dispose of this button. */
dispose() {
if (this.onMouseUpWrapper) {
browserEvents.unbind(this.onMouseUpWrapper);
}
browserEvents.unbind(this.onMouseDownWrapper);
browserEvents.unbind(this.onMouseUpWrapper);
if (this.svgGroup) {
dom.removeNode(this.svgGroup);
}
Expand Down Expand Up @@ -342,6 +374,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
}
}
}

private onMouseDown(e: PointerEvent) {
const gesture = this.targetWorkspace.getGesture(e);
const flyout = this.targetWorkspace.getFlyout();
if (gesture && flyout) {
gesture.handleFlyoutStart(e, flyout);
}
}

/**
* @returns The root SVG element of this rendered element.
*/
getSvgRoot() {
return this.svgGroup;
}
}

/** CSS for buttons and labels. See css.js for use. */
Expand Down
59 changes: 59 additions & 0 deletions core/label_flyout_inflater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {FlyoutButton} from './flyout_button.js';
import {ButtonOrLabelInfo} from './utils/toolbox.js';
import * as registry from './registry.js';

/**
* Class responsible for creating labels for flyouts.
*/
export class LabelFlyoutInflater implements IFlyoutInflater {
/**
* Inflates a flyout label from the given state and adds it to the flyout.
*
* @param state A JSON representation of a flyout label.
* @param flyoutWorkspace The workspace to create the label on.
* @returns A FlyoutButton configured as a label.
*/
load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
const label = new FlyoutButton(
flyoutWorkspace,
flyoutWorkspace.targetWorkspace!,
state as ButtonOrLabelInfo,
true,
);
label.show();
return label;
}

/**
* Returns the amount of space that should follow this label.
*
* @param state A JSON representation of a flyout label.
* @param defaultGap The default spacing for flyout items.
* @returns The amount of space that should follow this label.
*/
gapForElement(state: Object, defaultGap: number): number {
return defaultGap;
}

/**
* Disposes of the given label.
*
* @param element The flyout label to dispose of.
*/
disposeElement(element: IBoundedElement): void {
if (element instanceof FlyoutButton) {
element.dispose();
}
}
}

registry.register(registry.Type.FLYOUT_INFLATER, 'label', LabelFlyoutInflater);

0 comments on commit 489aded

Please sign in to comment.