UNPKG

@blockly/continuous-toolbox

Version:

A Blockly plugin that adds a continous-scrolling style toolbox and flyout

196 lines (176 loc) 5.84 kB
/** * @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(); } }