@blockly/continuous-toolbox
Version:
A Blockly plugin that adds a continous-scrolling style toolbox and flyout
196 lines (176 loc) • 5.84 kB
text/typescript
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Toolbox that uses a continuous scrolling flyout.
*/
import * as Blockly from 'blockly/core';
import {ContinuousFlyout} from './ContinuousFlyout';
/**
* Class for continuous toolbox.
*/
export class ContinuousToolbox extends Blockly.Toolbox {
/**
* Timeout ID used to prevent refreshing the flyout during extensive block
* changes.
*/
private refreshDebouncer?: ReturnType<typeof setTimeout>;
/**
* Initializes the continuous toolbox.
*/
override init() {
super.init();
// Populate the flyout with all blocks and show it immediately.
const flyout = this.getFlyout();
flyout.show(this.getInitialFlyoutContents());
this.getWorkspace().addChangeListener((e: Blockly.Events.Abstract) => {
if (
e.type === Blockly.Events.BLOCK_CREATE ||
e.type === Blockly.Events.BLOCK_DELETE ||
e.type === Blockly.Events.BLOCK_CHANGE
) {
this.refreshSelection();
}
});
}
/**
* Returns the continuous toolbox's flyout.
*
* @returns The toolbox's flyout.
*/
override getFlyout(): ContinuousFlyout {
return super.getFlyout() as ContinuousFlyout;
}
/**
* Gets the contents that should be shown in the flyout immediately.
* This includes all blocks and labels for each category of block.
*
* @returns Flyout contents.
*/
private getInitialFlyoutContents(): Blockly.utils.toolbox.FlyoutItemInfoArray {
return this.getToolboxItems().flatMap(this.convertToolboxItemToFlyoutItems);
}
/**
* Converts a given toolbox item to an array of flyout items, generally a
* label followed by the category's blocks.
*
* @param toolboxItem The toolbox item/category to convert.
* @returns An array of flyout items contained in the given toolbox item.
*/
protected convertToolboxItemToFlyoutItems(
toolboxItem: Blockly.IToolboxItem,
): Blockly.utils.toolbox.FlyoutItemInfoArray {
let contents: Blockly.utils.toolbox.FlyoutItemInfoArray = [];
if (toolboxItem instanceof Blockly.ToolboxCategory) {
// Create a label node to go at the top of the category
contents.push({kind: 'LABEL', text: toolboxItem.getName()});
let itemContents = toolboxItem.getContents();
// Handle custom categories (e.g. variables and functions)
if (typeof itemContents === 'string') {
itemContents = [{custom: itemContents, kind: 'CATEGORY'}];
}
contents = contents.concat(itemContents);
}
return contents;
}
/**
* Updates the flyout's contents if it is visible.
*/
override refreshSelection() {
if (this.getFlyout().isVisible()) {
if (this.refreshDebouncer) {
clearTimeout(this.refreshDebouncer);
}
this.refreshDebouncer = setTimeout(() => {
this.getFlyout().show(this.getInitialFlyoutContents());
}, 100);
}
}
/**
* Scrolls the flyout to display the newly selected category's contents.
*
* @param oldItem The previously selected toolbox category.
* @param newItem The newly selected toolbox category.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
override updateFlyout_(
oldItem: Blockly.ISelectableToolboxItem | null,
newItem: Blockly.ISelectableToolboxItem | null,
) {
if (newItem) {
this.getFlyout().scrollToCategory(newItem);
if (!this.getFlyout().isVisible()) {
this.getFlyout().show(this.getInitialFlyoutContents());
}
} else if (this.getFlyout().autoClose) {
this.getFlyout().hide();
}
}
/**
* Returns whether or not the toolbox should deselect the old category.
*
* @param oldItem The previously selected toolbox category.
* @param newItem The newly selected toolbox category.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
override shouldDeselectItem_(
oldItem: Blockly.ISelectableToolboxItem | null,
newItem: Blockly.ISelectableToolboxItem | null,
): boolean {
// Should not deselect if the same category is clicked again.
return !!(oldItem && oldItem !== newItem);
}
/**
* Gets a category by name.
*
* @param name Name of category to get.
* @returns Category, or null if not found.
* @internal
*/
getCategoryByName(name: string): Blockly.ISelectableToolboxItem | null {
const category = this.getToolboxItems().find(
(item) =>
item instanceof Blockly.ToolboxCategory &&
item.isSelectable() &&
name === item.getName(),
);
if (!category) return null;
return category as Blockly.ISelectableToolboxItem;
}
/**
* Selects the category with the given name.
* Similar to setSelectedItem, but importantly, does not call updateFlyout
* because this is called while the flyout is being scrolled.
*
* @param name Name of category to select.
* @internal
*/
selectCategoryByName(name: string) {
const newItem = this.getCategoryByName(name);
if (!newItem) return;
const oldItem = this.selectedItem_;
if (oldItem && this.shouldDeselectItem_(oldItem, newItem)) {
this.deselectItem_(oldItem);
}
if (this.shouldSelectItem_(oldItem, newItem)) {
this.selectItem_(oldItem, newItem);
}
}
/**
* Returns the bounding rectangle of the drag target/deletion area in pixels
* relative to the viewport.
*
* @returns The toolbox's bounding box. Null if drag target area should be
* ignored.
*/
override getClientRect(): Blockly.utils.Rect | null {
// If the flyout never closes, it should be the deletable area.
const flyout = this.getFlyout();
if (flyout && !flyout.autoClose) {
return flyout.getClientRect();
}
return super.getClientRect();
}
}