mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
731 lines
20 KiB
TypeScript
731 lines
20 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2011 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Object representing a trash can icon.
|
|
*
|
|
* @class
|
|
*/
|
|
// Former goog.module ID: Blockly.Trashcan
|
|
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
import * as browserEvents from './browser_events.js';
|
|
import {ComponentManager} from './component_manager.js';
|
|
import {DeleteArea} from './delete_area.js';
|
|
import type {Abstract} from './events/events_abstract.js';
|
|
import './events/events_trashcan_open.js';
|
|
import {isBlockDelete} from './events/predicates.js';
|
|
import {EventType} from './events/type.js';
|
|
import * as eventUtils from './events/utils.js';
|
|
import type {IAutoHideable} from './interfaces/i_autohideable.js';
|
|
import type {IDraggable} from './interfaces/i_draggable.js';
|
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
|
import type {IPositionable} from './interfaces/i_positionable.js';
|
|
import type {UiMetrics} from './metrics_manager.js';
|
|
import * as uiPosition from './positionable_helpers.js';
|
|
import * as registry from './registry.js';
|
|
import type * as blocks from './serialization/blocks.js';
|
|
import {SPRITE} from './sprites.js';
|
|
import * as dom from './utils/dom.js';
|
|
import {Rect} from './utils/rect.js';
|
|
import {Size} from './utils/size.js';
|
|
import {Svg} from './utils/svg.js';
|
|
import * as toolbox from './utils/toolbox.js';
|
|
import {BlockInfo} from './utils/toolbox.js';
|
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
|
|
|
/**
|
|
* Class for a trash can.
|
|
*/
|
|
export class Trashcan
|
|
extends DeleteArea
|
|
implements IAutoHideable, IPositionable
|
|
{
|
|
/**
|
|
* The unique id for this component that is used to register with the
|
|
* ComponentManager.
|
|
*/
|
|
override id = 'trashcan';
|
|
|
|
/**
|
|
* A list of JSON (stored as strings) representing blocks in the trashcan.
|
|
*/
|
|
private readonly contents: string[] = [];
|
|
|
|
/**
|
|
* The trashcan flyout.
|
|
*
|
|
* @internal
|
|
*/
|
|
flyout: IFlyout | null = null;
|
|
|
|
/** Current open/close state of the lid. */
|
|
isLidOpen = false;
|
|
|
|
/**
|
|
* The minimum openness of the lid. Used to indicate if the trashcan
|
|
* contains blocks.
|
|
*/
|
|
private minOpenness = 0;
|
|
|
|
/** The SVG group containing the trash can. */
|
|
private svgGroup: SVGElement | null = null;
|
|
|
|
/** The SVG image element of the trash can lid. */
|
|
private svgLid: SVGElement | null = null;
|
|
|
|
/** Task ID of opening/closing animation. */
|
|
private lidTask: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
/** Current state of lid opening (0.0 = closed, 1.0 = open). */
|
|
private lidOpen = 0;
|
|
|
|
/** Left coordinate of the trash can. */
|
|
private left = 0;
|
|
|
|
/** Top coordinate of the trash can. */
|
|
private top = 0;
|
|
|
|
/** Whether this trash can has been initialized. */
|
|
private initialized = false;
|
|
|
|
/** @param workspace The workspace to sit in. */
|
|
constructor(private workspace: WorkspaceSvg) {
|
|
super();
|
|
|
|
if (this.workspace.options.maxTrashcanContents <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Create flyout options.
|
|
const flyoutWorkspaceOptions = this.workspace.copyOptionsForFlyout();
|
|
// Create vertical or horizontal flyout.
|
|
if (this.workspace.horizontalLayout) {
|
|
flyoutWorkspaceOptions.toolboxPosition =
|
|
this.workspace.toolboxPosition === toolbox.Position.TOP
|
|
? toolbox.Position.BOTTOM
|
|
: toolbox.Position.TOP;
|
|
const HorizontalFlyout = registry.getClassFromOptions(
|
|
registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX,
|
|
this.workspace.options,
|
|
true,
|
|
);
|
|
this.flyout = new HorizontalFlyout!(flyoutWorkspaceOptions);
|
|
} else {
|
|
flyoutWorkspaceOptions.toolboxPosition =
|
|
this.workspace.toolboxPosition === toolbox.Position.RIGHT
|
|
? toolbox.Position.LEFT
|
|
: toolbox.Position.RIGHT;
|
|
const VerticalFlyout = registry.getClassFromOptions(
|
|
registry.Type.FLYOUTS_VERTICAL_TOOLBOX,
|
|
this.workspace.options,
|
|
true,
|
|
);
|
|
this.flyout = new VerticalFlyout!(flyoutWorkspaceOptions);
|
|
}
|
|
this.workspace.addChangeListener(this.onDelete.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Create the trash can elements.
|
|
*
|
|
* @returns The trash can's SVG group.
|
|
*/
|
|
createDom(): SVGElement {
|
|
/* Here's the markup that will be generated:
|
|
<g class="blocklyTrash">
|
|
<clippath id="blocklyTrashBodyClipPath837493">
|
|
<rect width="47" height="45" y="15"></rect>
|
|
</clippath>
|
|
<image width="64" height="92" y="-32" xlink:href="media/sprites.png"
|
|
clip-path="url(#blocklyTrashBodyClipPath837493)"></image>
|
|
<clippath id="blocklyTrashLidClipPath837493">
|
|
<rect width="47" height="15"></rect>
|
|
</clippath>
|
|
<image width="84" height="92" y="-32" xlink:href="media/sprites.png"
|
|
clip-path="url(#blocklyTrashLidClipPath837493)"></image>
|
|
</g>
|
|
*/
|
|
this.svgGroup = dom.createSvgElement(Svg.G, {'class': 'blocklyTrash'});
|
|
let clip;
|
|
const rnd = String(Math.random()).substring(2);
|
|
clip = dom.createSvgElement(
|
|
Svg.CLIPPATH,
|
|
{'id': 'blocklyTrashBodyClipPath' + rnd},
|
|
this.svgGroup,
|
|
);
|
|
dom.createSvgElement(
|
|
Svg.RECT,
|
|
{'width': WIDTH, 'height': BODY_HEIGHT, 'y': LID_HEIGHT},
|
|
clip,
|
|
);
|
|
const body = dom.createSvgElement(
|
|
Svg.IMAGE,
|
|
{
|
|
'width': SPRITE.width,
|
|
'x': -SPRITE_LEFT,
|
|
'height': SPRITE.height,
|
|
'y': -SPRITE_TOP,
|
|
'clip-path': 'url(#blocklyTrashBodyClipPath' + rnd + ')',
|
|
},
|
|
this.svgGroup,
|
|
);
|
|
body.setAttributeNS(
|
|
dom.XLINK_NS,
|
|
'xlink:href',
|
|
this.workspace.options.pathToMedia + SPRITE.url,
|
|
);
|
|
|
|
clip = dom.createSvgElement(
|
|
Svg.CLIPPATH,
|
|
{'id': 'blocklyTrashLidClipPath' + rnd},
|
|
this.svgGroup,
|
|
);
|
|
dom.createSvgElement(
|
|
Svg.RECT,
|
|
{'width': WIDTH, 'height': LID_HEIGHT},
|
|
clip,
|
|
);
|
|
this.svgLid = dom.createSvgElement(
|
|
Svg.IMAGE,
|
|
{
|
|
'width': SPRITE.width,
|
|
'x': -SPRITE_LEFT,
|
|
'height': SPRITE.height,
|
|
'y': -SPRITE_TOP,
|
|
'clip-path': 'url(#blocklyTrashLidClipPath' + rnd + ')',
|
|
},
|
|
this.svgGroup,
|
|
);
|
|
this.svgLid.setAttributeNS(
|
|
dom.XLINK_NS,
|
|
'xlink:href',
|
|
this.workspace.options.pathToMedia + SPRITE.url,
|
|
);
|
|
|
|
// bindEventWithChecks_ quashes events too aggressively. See:
|
|
// https://groups.google.com/forum/#!topic/blockly/QF4yB9Wx00s
|
|
// Using bindEventWithChecks_ for blocking mousedown causes issue in mobile.
|
|
// See #4303
|
|
browserEvents.bind(
|
|
this.svgGroup,
|
|
'pointerdown',
|
|
this,
|
|
this.blockMouseDownWhenOpenable,
|
|
);
|
|
browserEvents.bind(this.svgGroup, 'pointerup', this, this.click);
|
|
// Bind to body instead of this.svgGroup so that we don't get lid jitters
|
|
browserEvents.bind(body, 'pointerover', this, this.mouseOver);
|
|
browserEvents.bind(body, 'pointerout', this, this.mouseOut);
|
|
this.animateLid();
|
|
return this.svgGroup;
|
|
}
|
|
|
|
/** Initializes the trash can. */
|
|
init() {
|
|
if (this.workspace.options.maxTrashcanContents > 0) {
|
|
const flyoutDom = this.flyout!.createDom(Svg.SVG)!;
|
|
dom.addClass(flyoutDom, 'blocklyTrashcanFlyout');
|
|
dom.insertAfter(flyoutDom, this.workspace.getParentSvg());
|
|
this.flyout!.init(this.workspace);
|
|
}
|
|
this.workspace.getComponentManager().addComponent({
|
|
component: this,
|
|
weight: ComponentManager.ComponentWeight.TRASHCAN_WEIGHT,
|
|
capabilities: [
|
|
ComponentManager.Capability.AUTOHIDEABLE,
|
|
ComponentManager.Capability.DELETE_AREA,
|
|
ComponentManager.Capability.DRAG_TARGET,
|
|
ComponentManager.Capability.POSITIONABLE,
|
|
],
|
|
});
|
|
this.initialized = true;
|
|
this.setLidOpen(false);
|
|
}
|
|
|
|
/**
|
|
* Dispose of this trash can.
|
|
* Unlink from all DOM elements to prevent memory leaks.
|
|
*/
|
|
dispose() {
|
|
this.workspace.getComponentManager().removeComponent('trashcan');
|
|
if (this.svgGroup) {
|
|
dom.removeNode(this.svgGroup);
|
|
}
|
|
if (this.lidTask) {
|
|
clearTimeout(this.lidTask);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the trashcan has contents.
|
|
*
|
|
* @returns True if the trashcan has contents.
|
|
*/
|
|
private hasContents(): boolean {
|
|
return !!this.contents.length;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the trashcan contents-flyout is currently open.
|
|
*
|
|
* @returns True if the trashcan contents-flyout is currently open.
|
|
*/
|
|
contentsIsOpen(): boolean {
|
|
return !!this.flyout && this.flyout.isVisible();
|
|
}
|
|
|
|
/** Opens the trashcan flyout. */
|
|
openFlyout() {
|
|
if (this.contentsIsOpen()) {
|
|
return;
|
|
}
|
|
const contents = this.contents.map(function (string) {
|
|
return JSON.parse(string);
|
|
});
|
|
// Trashcans with lots of blocks can take a second to render.
|
|
const blocklyStyle = this.workspace.getParentSvg().style;
|
|
blocklyStyle.cursor = 'wait';
|
|
setTimeout(() => {
|
|
this.flyout?.show(contents);
|
|
blocklyStyle.cursor = '';
|
|
this.workspace.scrollbar?.setVisible(false);
|
|
}, 10);
|
|
this.fireUiEvent(true);
|
|
}
|
|
|
|
/** Closes the trashcan flyout. */
|
|
closeFlyout() {
|
|
if (!this.contentsIsOpen()) {
|
|
return;
|
|
}
|
|
this.flyout?.hide();
|
|
this.workspace.scrollbar?.setVisible(true);
|
|
this.fireUiEvent(false);
|
|
this.workspace.recordDragTargets();
|
|
}
|
|
|
|
/**
|
|
* Hides the component. Called in WorkspaceSvg.hideChaff.
|
|
*
|
|
* @param onlyClosePopups Whether only popups should be closed.
|
|
* Flyouts should not be closed if this is true.
|
|
*/
|
|
autoHide(onlyClosePopups: boolean) {
|
|
// For now the trashcan flyout always autocloses because it overlays the
|
|
// trashcan UI (no trashcan to click to close it).
|
|
if (!onlyClosePopups && this.flyout) {
|
|
this.closeFlyout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Empties the trashcan's contents. If the contents-flyout is currently open
|
|
* it will be closed.
|
|
*/
|
|
emptyContents() {
|
|
if (!this.hasContents()) {
|
|
return;
|
|
}
|
|
this.contents.length = 0;
|
|
this.setMinOpenness(0);
|
|
this.closeFlyout();
|
|
}
|
|
|
|
/**
|
|
* Positions the trashcan.
|
|
* It is positioned in the opposite corner to the corner the
|
|
* categories/toolbox starts at.
|
|
*
|
|
* @param metrics The workspace metrics.
|
|
* @param savedPositions List of rectangles that are already on the workspace.
|
|
*/
|
|
position(metrics: UiMetrics, savedPositions: Rect[]) {
|
|
// Not yet initialized.
|
|
if (!this.initialized) {
|
|
return;
|
|
}
|
|
|
|
const cornerPosition = uiPosition.getCornerOppositeToolbox(
|
|
this.workspace,
|
|
metrics,
|
|
);
|
|
|
|
const height = BODY_HEIGHT + LID_HEIGHT;
|
|
const startRect = uiPosition.getStartPositionRect(
|
|
cornerPosition,
|
|
new Size(WIDTH, height),
|
|
MARGIN_HORIZONTAL,
|
|
MARGIN_VERTICAL,
|
|
metrics,
|
|
this.workspace,
|
|
);
|
|
|
|
const verticalPosition = cornerPosition.vertical;
|
|
const bumpDirection =
|
|
verticalPosition === uiPosition.verticalPosition.TOP
|
|
? uiPosition.bumpDirection.DOWN
|
|
: uiPosition.bumpDirection.UP;
|
|
const positionRect = uiPosition.bumpPositionRect(
|
|
startRect,
|
|
MARGIN_VERTICAL,
|
|
bumpDirection,
|
|
savedPositions,
|
|
);
|
|
|
|
this.top = positionRect.top;
|
|
this.left = positionRect.left;
|
|
this.svgGroup?.setAttribute(
|
|
'transform',
|
|
'translate(' + this.left + ',' + this.top + ')',
|
|
);
|
|
|
|
this.flyout?.position();
|
|
}
|
|
|
|
/**
|
|
* Returns the bounding rectangle of the UI element in pixel units relative to
|
|
* the Blockly injection div.
|
|
*
|
|
* @returns The UI elements's bounding box. Null if bounding box should be
|
|
* ignored by other UI elements.
|
|
*/
|
|
getBoundingRectangle(): Rect | null {
|
|
const bottom = this.top + BODY_HEIGHT + LID_HEIGHT;
|
|
const right = this.left + WIDTH;
|
|
return new Rect(this.top, bottom, this.left, right);
|
|
}
|
|
|
|
/**
|
|
* Returns the bounding rectangle of the drag target area in pixel units
|
|
* relative to viewport.
|
|
*
|
|
* @returns The component's bounding box. Null if drag target area should be
|
|
* ignored.
|
|
*/
|
|
override getClientRect(): Rect | null {
|
|
if (!this.svgGroup) {
|
|
return null;
|
|
}
|
|
|
|
const trashRect = this.svgGroup.getBoundingClientRect();
|
|
const top = trashRect.top + SPRITE_TOP - MARGIN_HOTSPOT;
|
|
const bottom = top + LID_HEIGHT + BODY_HEIGHT + 2 * MARGIN_HOTSPOT;
|
|
const left = trashRect.left + SPRITE_LEFT - MARGIN_HOTSPOT;
|
|
const right = left + WIDTH + 2 * MARGIN_HOTSPOT;
|
|
return new Rect(top, bottom, left, right);
|
|
}
|
|
|
|
/**
|
|
* Handles when a cursor with a block or bubble is dragged over this drag
|
|
* target.
|
|
*
|
|
* @param _dragElement The block or bubble currently being dragged.
|
|
*/
|
|
override onDragOver(_dragElement: IDraggable) {
|
|
this.setLidOpen(this.wouldDelete_);
|
|
}
|
|
|
|
/**
|
|
* Handles when a cursor with a block or bubble exits this drag target.
|
|
*
|
|
* @param _dragElement The block or bubble currently being dragged.
|
|
*/
|
|
override onDragExit(_dragElement: IDraggable) {
|
|
this.setLidOpen(false);
|
|
}
|
|
|
|
/**
|
|
* Handles when a block or bubble is dropped on this component.
|
|
* Should not handle delete here.
|
|
*
|
|
* @param _dragElement The block or bubble currently being dragged.
|
|
*/
|
|
override onDrop(_dragElement: IDraggable) {
|
|
setTimeout(this.setLidOpen.bind(this, false), 100);
|
|
}
|
|
|
|
/**
|
|
* Flip the lid open or shut.
|
|
*
|
|
* @param state True if open.
|
|
* @internal
|
|
*/
|
|
setLidOpen(state: boolean) {
|
|
if (this.isLidOpen === state) {
|
|
return;
|
|
}
|
|
if (this.lidTask) {
|
|
clearTimeout(this.lidTask);
|
|
}
|
|
this.isLidOpen = state;
|
|
this.animateLid();
|
|
}
|
|
|
|
/** Rotate the lid open or closed by one step. Then wait and recurse. */
|
|
private animateLid() {
|
|
const frames = ANIMATION_FRAMES;
|
|
|
|
const delta = 1 / (frames + 1);
|
|
this.lidOpen += this.isLidOpen ? delta : -delta;
|
|
this.lidOpen = Math.min(Math.max(this.lidOpen, this.minOpenness), 1);
|
|
|
|
this.setLidAngle(this.lidOpen * MAX_LID_ANGLE);
|
|
|
|
// Linear interpolation between min and max.
|
|
const opacity = OPACITY_MIN + this.lidOpen * (OPACITY_MAX - OPACITY_MIN);
|
|
if (this.svgGroup) {
|
|
this.svgGroup.style.opacity = `${opacity}`;
|
|
}
|
|
|
|
if (this.lidOpen > this.minOpenness && this.lidOpen < 1) {
|
|
this.lidTask = setTimeout(
|
|
this.animateLid.bind(this),
|
|
ANIMATION_LENGTH / frames,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the angle of the trashcan's lid.
|
|
*
|
|
* @param lidAngle The angle at which to set the lid.
|
|
*/
|
|
private setLidAngle(lidAngle: number) {
|
|
const openAtRight =
|
|
this.workspace.toolboxPosition === toolbox.Position.RIGHT ||
|
|
(this.workspace.horizontalLayout && this.workspace.RTL);
|
|
this.svgLid?.setAttribute(
|
|
'transform',
|
|
'rotate(' +
|
|
(openAtRight ? -lidAngle : lidAngle) +
|
|
',' +
|
|
(openAtRight ? 4 : WIDTH - 4) +
|
|
',' +
|
|
(LID_HEIGHT - 2) +
|
|
')',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets the minimum openness of the trashcan lid. If the lid is currently
|
|
* closed, this will update lid's position.
|
|
*
|
|
* @param newMin The new minimum openness of the lid. Should be between 0
|
|
* and 1.
|
|
*/
|
|
private setMinOpenness(newMin: number) {
|
|
this.minOpenness = newMin;
|
|
if (!this.isLidOpen) {
|
|
this.setLidAngle(newMin * MAX_LID_ANGLE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flip the lid shut.
|
|
* Called externally after a drag.
|
|
*/
|
|
closeLid() {
|
|
this.setLidOpen(false);
|
|
}
|
|
|
|
/** Inspect the contents of the trash. */
|
|
click() {
|
|
if (!this.hasContents() || this.workspace.isDragging()) {
|
|
return;
|
|
}
|
|
this.openFlyout();
|
|
}
|
|
|
|
/**
|
|
* Fires a UI event for trashcan flyout open or close.
|
|
*
|
|
* @param trashcanOpen Whether the flyout is opening.
|
|
*/
|
|
private fireUiEvent(trashcanOpen: boolean) {
|
|
const uiEvent = new (eventUtils.get(EventType.TRASHCAN_OPEN))(
|
|
trashcanOpen,
|
|
this.workspace.id,
|
|
);
|
|
eventUtils.fire(uiEvent);
|
|
}
|
|
|
|
/**
|
|
* Prevents a workspace scroll and click event if the trashcan has blocks.
|
|
*
|
|
* @param e A mouse down event.
|
|
*/
|
|
private blockMouseDownWhenOpenable(e: PointerEvent) {
|
|
if (!this.contentsIsOpen() && this.hasContents()) {
|
|
// Don't start a workspace scroll.
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicate that the trashcan can be clicked (by opening it) if it has blocks.
|
|
*/
|
|
private mouseOver() {
|
|
if (this.hasContents()) {
|
|
this.setLidOpen(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the lid of the trashcan if it was open (Vis. it was indicating it had
|
|
* blocks).
|
|
*/
|
|
private mouseOut() {
|
|
// No need to do a .hasBlocks check here because if it doesn't the trashcan
|
|
// won't be open in the first place, and setOpen won't run.
|
|
this.setLidOpen(false);
|
|
}
|
|
|
|
/**
|
|
* Handle a BLOCK_DELETE event. Adds deleted blocks oldXml to the content
|
|
* array.
|
|
*
|
|
* @param event Workspace event.
|
|
*/
|
|
private onDelete(event: Abstract) {
|
|
if (
|
|
this.workspace.options.maxTrashcanContents <= 0 ||
|
|
!isBlockDelete(event) ||
|
|
event.wasShadow
|
|
) {
|
|
return;
|
|
}
|
|
if (!event.oldJson) {
|
|
throw new Error('Encountered a delete event without proper oldJson');
|
|
}
|
|
const cleanedJson = JSON.stringify(this.cleanBlockJson(event.oldJson));
|
|
if (this.contents.includes(cleanedJson)) return;
|
|
this.contents.unshift(cleanedJson);
|
|
while (this.contents.length > this.workspace.options.maxTrashcanContents) {
|
|
this.contents.pop();
|
|
}
|
|
|
|
this.setMinOpenness(HAS_BLOCKS_LID_ANGLE);
|
|
}
|
|
|
|
/**
|
|
* Converts JSON representing a block into text that can be stored in the
|
|
* content array.
|
|
*
|
|
* @param json A JSON representation of a block's state.
|
|
* @returns A BlockInfo object corresponding to the JSON, cleaned of all
|
|
* unnecessary attributes.
|
|
*/
|
|
private cleanBlockJson(json: blocks.State): BlockInfo {
|
|
// Create a deep copy.
|
|
json = JSON.parse(JSON.stringify(json)) as blocks.State;
|
|
|
|
/**
|
|
* Reshape JSON into a nicer format.
|
|
*
|
|
* @param json The JSON to clean.
|
|
*/
|
|
function cleanRec(json: blocks.State) {
|
|
if (!json) {
|
|
return;
|
|
}
|
|
|
|
delete json['id'];
|
|
delete json['x'];
|
|
delete json['y'];
|
|
delete json['enabled'];
|
|
delete json['disabledReasons'];
|
|
|
|
if (json['icons'] && json['icons']['comment']) {
|
|
const comment = json['icons']['comment'];
|
|
delete comment['height'];
|
|
delete comment['width'];
|
|
delete comment['pinned'];
|
|
}
|
|
|
|
const inputs = json['inputs'];
|
|
for (const name in inputs) {
|
|
const input = inputs[name];
|
|
const block = input['block'];
|
|
const shadow = input['shadow'];
|
|
if (block) {
|
|
cleanRec(block);
|
|
}
|
|
if (shadow) {
|
|
cleanRec(shadow);
|
|
}
|
|
}
|
|
if (json['next']) {
|
|
const next = json['next'];
|
|
const block = next['block'];
|
|
const shadow = next['shadow'];
|
|
if (block) {
|
|
cleanRec(block);
|
|
}
|
|
if (shadow) {
|
|
cleanRec(shadow);
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanRec(json);
|
|
|
|
const blockInfo: BlockInfo = {
|
|
'kind': 'BLOCK',
|
|
...json,
|
|
};
|
|
return blockInfo;
|
|
}
|
|
}
|
|
|
|
/** Width of both the trash can and lid images. */
|
|
const WIDTH = 47;
|
|
|
|
/** Height of the trashcan image (minus lid). */
|
|
const BODY_HEIGHT = 44;
|
|
|
|
/** Height of the lid image. */
|
|
const LID_HEIGHT = 16;
|
|
|
|
/** Distance between trashcan and bottom or top edge of workspace. */
|
|
const MARGIN_VERTICAL = 20;
|
|
|
|
/** Distance between trashcan and right or left edge of workspace. */
|
|
const MARGIN_HORIZONTAL = 20;
|
|
|
|
/** Extent of hotspot on all sides beyond the size of the image. */
|
|
const MARGIN_HOTSPOT = 10;
|
|
|
|
/** Location of trashcan in sprite image. */
|
|
const SPRITE_LEFT = 0;
|
|
|
|
/** Location of trashcan in sprite image. */
|
|
const SPRITE_TOP = 32;
|
|
|
|
/**
|
|
* The openness of the lid when the trashcan contains blocks.
|
|
* (0.0 = closed, 1.0 = open)
|
|
*/
|
|
const HAS_BLOCKS_LID_ANGLE = 0.1;
|
|
|
|
/** The length of the lid open/close animation in milliseconds. */
|
|
const ANIMATION_LENGTH = 80;
|
|
|
|
/** The number of frames in the animation. */
|
|
const ANIMATION_FRAMES = 4;
|
|
|
|
/** The minimum (resting) opacity of the trashcan and lid. */
|
|
const OPACITY_MIN = 0.4;
|
|
|
|
/** The maximum (hovered) opacity of the trashcan and lid. */
|
|
const OPACITY_MAX = 0.8;
|
|
|
|
/**
|
|
* The maximum angle the trashcan lid can opens to. At the end of the open
|
|
* animation the lid will be open to this angle.
|
|
*/
|
|
const MAX_LID_ANGLE = 45;
|