Files
blockly/core/bubbles/mini_workspace_bubble.ts
Beka Westberg 91e8105e81 chore: add top-level inline docs to bubbles icons and inputs (#7190)
* chore: add top-level inline docs to bubbles icons and inputs

* chore: fixup for PR comments
2023-06-21 10:14:25 -07:00

283 lines
8.3 KiB
TypeScript

/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Abstract as AbstractEvent} from '../events/events_abstract.js';
import type {BlocklyOptions} from '../blockly_options.js';
import {Bubble} from './bubble.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import {Options} from '../options.js';
import {Svg} from '../utils/svg.js';
import type {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
/**
* A bubble that contains a mini-workspace which can hold arbitrary blocks.
* Used by the mutator icon.
*/
export class MiniWorkspaceBubble extends Bubble {
/**
* The minimum amount of change to the mini workspace view to trigger
* resizing the bubble.
*/
private static readonly MINIMUM_VIEW_CHANGE = 10;
/**
* An arbitrary margin of whitespace to put around the blocks in the
* workspace.
*/
private static readonly MARGIN = Bubble.DOUBLE_BORDER * 3;
/** The root svg element containing the workspace. */
private svgDialog: SVGElement;
/** The workspace that gets shown within this bubble. */
private miniWorkspace: WorkspaceSvg;
/**
* Should this bubble automatically reposition itself when it resizes?
* Becomes false after this bubble is first dragged.
*/
private autoLayout = true;
/** @internal */
constructor(
workspaceOptions: BlocklyOptions,
protected readonly workspace: WorkspaceSvg,
protected anchor: Coordinate,
protected ownerRect?: Rect
) {
super(workspace, anchor, ownerRect);
const options = new Options(workspaceOptions);
this.validateWorkspaceOptions(options);
this.svgDialog = dom.createSvgElement(
Svg.SVG,
{
'x': Bubble.BORDER_WIDTH,
'y': Bubble.BORDER_WIDTH,
},
this.contentContainer
);
workspaceOptions.parentWorkspace = this.workspace;
this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions));
const background = this.miniWorkspace.createDom('blocklyMutatorBackground');
this.svgDialog.appendChild(background);
if (options.languageTree) {
background.insertBefore(
this.miniWorkspace.addFlyout(Svg.G),
this.miniWorkspace.getCanvas()
);
const flyout = this.miniWorkspace.getFlyout();
flyout?.init(this.miniWorkspace);
flyout?.show(options.languageTree);
}
this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this));
this.miniWorkspace
.getFlyout()
?.getWorkspace()
?.addChangeListener(this.onWorkspaceChange.bind(this));
this.updateBubbleSize();
}
dispose() {
this.miniWorkspace.dispose();
super.dispose();
}
/** @internal */
getWorkspace(): WorkspaceSvg {
return this.miniWorkspace;
}
/** Adds a change listener to the mini workspace. */
addWorkspaceChangeListener(listener: (e: AbstractEvent) => void) {
this.miniWorkspace.addChangeListener(listener);
}
/**
* Validates the workspace options to make sure folks aren't trying to
* enable things the miniworkspace doesn't support.
*/
private validateWorkspaceOptions(options: Options) {
if (options.hasCategories) {
throw new Error(
'The miniworkspace bubble does not support toolboxes with categories'
);
}
if (options.hasTrashcan) {
throw new Error('The miniworkspace bubble does not support trashcans');
}
if (
options.zoomOptions.controls ||
options.zoomOptions.wheel ||
options.zoomOptions.pinch
) {
throw new Error('The miniworkspace bubble does not support zooming');
}
if (
options.moveOptions.scrollbars ||
options.moveOptions.wheel ||
options.moveOptions.drag
) {
throw new Error(
'The miniworkspace bubble does not scrolling/moving the workspace'
);
}
if (options.horizontalLayout) {
throw new Error(
'The miniworkspace bubble does not support horizontal layouts'
);
}
}
private onWorkspaceChange() {
this.bumpBlocksIntoBounds();
this.updateBubbleSize();
}
/**
* Bumps blocks that are above the top or outside the start-side of the
* workspace back within the workspace.
*
* Blocks that are below the bottom or outside the end-side of the workspace
* are dealt with by resizing the workspace to show them.
*/
private bumpBlocksIntoBounds() {
if (this.miniWorkspace.isDragging()) return;
const MARGIN = 20;
for (const block of this.miniWorkspace.getTopBlocks(false)) {
const blockXY = block.getRelativeToSurfaceXY();
// Bump any block that's above the top back inside.
if (blockXY.y < MARGIN) {
block.moveBy(0, MARGIN - blockXY.y);
}
// Bump any block overlapping the flyout back inside.
if (block.RTL) {
let right = -MARGIN;
const flyout = this.miniWorkspace.getFlyout();
if (flyout) {
right -= flyout.getWidth();
}
if (blockXY.x > right) {
block.moveBy(right - blockXY.x, 0);
}
} else if (blockXY.x < MARGIN) {
block.moveBy(MARGIN - blockXY.x, 0);
}
}
}
/**
* Updates the size of this bubble to account for the size of the
* mini workspace.
*/
private updateBubbleSize() {
if (this.miniWorkspace.isDragging()) return;
const currSize = this.getSize();
const newSize = this.calculateWorkspaceSize();
if (
Math.abs(currSize.width - newSize.width) <
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE &&
Math.abs(currSize.height - newSize.height) <
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE
) {
// Only resize if the size has noticeably changed.
return;
}
this.svgDialog.setAttribute('width', `${newSize.width}px`);
this.svgDialog.setAttribute('height', `${newSize.height}px`);
this.miniWorkspace.setCachedParentSvgSize(newSize.width, newSize.height);
if (this.miniWorkspace.RTL) {
// Scroll the workspace to always left-align.
this.miniWorkspace
.getCanvas()
.setAttribute('transform', `translate(${newSize.width}, 0)`);
}
this.setSize(
new Size(
newSize.width + Bubble.DOUBLE_BORDER,
newSize.height + Bubble.DOUBLE_BORDER
),
this.autoLayout
);
this.miniWorkspace.resize();
this.miniWorkspace.recordDragTargets();
}
/**
* Calculates the size of the mini workspace for use in resizing the bubble.
*/
private calculateWorkspaceSize(): Size {
// TODO (#7104): Clean this up to be more readable and unified for RTL
// vs LTR.
const canvas = this.miniWorkspace.getCanvas();
const workspaceSize = canvas.getBBox();
let width = workspaceSize.width + workspaceSize.x;
let height = workspaceSize.height + MiniWorkspaceBubble.MARGIN;
const flyout = this.miniWorkspace.getFlyout();
if (flyout) {
const flyoutScrollMetrics = flyout
.getWorkspace()
.getMetricsManager()
.getScrollMetrics();
height = Math.max(height, flyoutScrollMetrics.height + 20);
width += flyout.getWidth();
}
if (this.miniWorkspace.RTL) {
width = -workspaceSize.x;
}
width += MiniWorkspaceBubble.MARGIN;
return new Size(width, height);
}
/** Reapplies styles to all of the blocks in the mini workspace. */
updateBlockStyles() {
for (const block of this.miniWorkspace.getAllBlocks(false)) {
block.setStyle(block.getStyleName());
}
const flyoutWs = this.miniWorkspace.getFlyout()?.getWorkspace();
if (flyoutWs) {
for (const block of flyoutWs.getAllBlocks(false)) {
block.setStyle(block.getStyleName());
}
}
}
/**
* Move this bubble during a drag.
*
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate): void {
super.moveDuringDrag(newLoc);
this.autoLayout = false;
}
/** @internal */
moveTo(x: number, y: number): void {
super.moveTo(x, y);
this.miniWorkspace.recordDragTargets();
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newWorkspaceSvg(options: Options): WorkspaceSvg {
throw new Error(
'The implementation of newWorkspaceSvg should be ' +
'monkey-patched in by blockly.ts'
);
}
}