diff --git a/core/button_flyout_inflater.ts b/core/button_flyout_inflater.ts new file mode 100644 index 0000000000..703dc60693 --- /dev/null +++ b/core/button_flyout_inflater.ts @@ -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, +); diff --git a/core/flyout_button.ts b/core/flyout_button.ts index dfc7b95074..41c3636fe8 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -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; @@ -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. */ @@ -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; @@ -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'; @@ -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. */ @@ -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; @@ -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; @@ -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); } @@ -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. */ diff --git a/core/label_flyout_inflater.ts b/core/label_flyout_inflater.ts new file mode 100644 index 0000000000..67b02857a4 --- /dev/null +++ b/core/label_flyout_inflater.ts @@ -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);