mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
* chore: add top-level inline docs to bubbles icons and inputs * chore: fixup for PR comments
283 lines
8.3 KiB
TypeScript
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'
|
|
);
|
|
}
|
|
}
|