mirror of
https://github.com/google/blockly.git
synced 2026-01-07 09:00:11 +01:00
feat: mini workspace bubble (#7096)
* feat: add properly sizing mini workspace bubble * chore: add properly handling workspace options * fix: various sizing and option bugs * fix: code related to dragging * fix: remove adding flyout change listener * chore: add docs * fix: build * fix: PR comments' * chore: PR comments
This commit is contained in:
@@ -18,28 +18,31 @@ import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
export abstract class Bubble implements IBubble {
|
||||
/** The width of the border around the bubble. */
|
||||
static BORDER_WIDTH = 6;
|
||||
static readonly BORDER_WIDTH = 6;
|
||||
|
||||
/** Double the width of the border around the bubble. */
|
||||
static readonly DOUBLE_BORDER = this.BORDER_WIDTH * 2;
|
||||
|
||||
/** The minimum size the bubble can have. */
|
||||
static MIN_SIZE = this.BORDER_WIDTH * 2;
|
||||
static readonly MIN_SIZE = this.DOUBLE_BORDER;
|
||||
|
||||
/**
|
||||
* The thickness of the base of the tail in relation to the size of the
|
||||
* bubble. Higher numbers result in thinner tails.
|
||||
*/
|
||||
static TAIL_THICKNESS = 1;
|
||||
static readonly TAIL_THICKNESS = 1;
|
||||
|
||||
/** The number of degrees that the tail bends counter-clockwise. */
|
||||
static TAIL_ANGLE = 20;
|
||||
static readonly TAIL_ANGLE = 20;
|
||||
|
||||
/**
|
||||
* The sharpness of the tail's bend. Higher numbers result in smoother
|
||||
* tails.
|
||||
*/
|
||||
static TAIL_BEND = 4;
|
||||
static readonly TAIL_BEND = 4;
|
||||
|
||||
/** Distance between arrow point and anchor point. */
|
||||
static ANCHOR_RADIUS = 8;
|
||||
static readonly ANCHOR_RADIUS = 8;
|
||||
|
||||
/** The SVG group containing all parts of the bubble. */
|
||||
private svgRoot: SVGGElement;
|
||||
@@ -548,11 +551,8 @@ export abstract class Bubble implements IBubble {
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this bubble during a drag, taking into account whether or not there is
|
||||
* a drag surface.
|
||||
* Move this bubble during a drag.
|
||||
*
|
||||
* @param dragSurface The surface that carries rendered items during a drag,
|
||||
* or null if no drag surface is in use.
|
||||
* @param newLoc The location to translate to, in workspace coordinates.
|
||||
* @internal
|
||||
*/
|
||||
|
||||
224
core/bubbles/mini_workspace_bubble.ts
Normal file
224
core/bubbles/mini_workspace_bubble.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
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.updateBubbleSize.bind(this));
|
||||
this.miniWorkspace
|
||||
.getFlyout()
|
||||
?.getWorkspace()
|
||||
?.addChangeListener(this.updateBubbleSize.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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user