diff --git a/core/blockly.ts b/core/blockly.ts index 01490dbb694..640f102696b 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -103,7 +103,7 @@ import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {VerticalFlyout} from './flyout_vertical.js'; import {CodeGenerator} from './generator.js'; import {Gesture} from './gesture.js'; -import {Grid} from './grid.js'; +import {Grid, GridProvider} from './grid.js'; import * as icons from './icons.js'; import {inject} from './inject.js'; import * as inputs from './inputs.js'; @@ -132,6 +132,7 @@ import { } from './interfaces/i_draggable.js'; import {IDragger} from './interfaces/i_dragger.js'; import {IFlyout} from './interfaces/i_flyout.js'; +import {IGrid, IGridProvider} from './interfaces/i_grid.js'; import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js'; import {IIcon, isIcon} from './interfaces/i_icon.js'; import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; @@ -508,6 +509,7 @@ export { CodeGenerator as Generator, Gesture, Grid, + GridProvider, HorizontalFlyout, IASTNodeLocation, IASTNodeLocationSvg, @@ -529,6 +531,8 @@ export { IDraggable, IDragger, IFlyout, + IGrid, + IGridProvider, IHasBubble, IIcon, IKeyboardAccessible, diff --git a/core/grid.ts b/core/grid.ts index e2fc054a262..f925d4379a4 100644 --- a/core/grid.ts +++ b/core/grid.ts @@ -12,15 +12,89 @@ */ // Former goog.module ID: Blockly.Grid +import {BlocklyOptions} from './blockly_options.js'; +import {IGrid, IGridProvider} from './interfaces/i_grid.js'; import {GridOptions} from './options.js'; +import * as registry from './registry.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; +export class GridProvider implements IGridProvider { + /** + * Create the DOM for the grid described by options. + * + * @param rnd A random ID to append to the pattern's ID. + * @param gridOptions The object containing grid configuration. + * @param defs The root SVG element for this workspace's defs. + * @returns The SVG element for the grid pattern. + * @internal + */ + createDom( + rnd: string, + gridOptions: GridOptions, + defs: SVGElement, + ): SVGElement { + /* + + + + + */ + const gridPattern = dom.createSvgElement( + Svg.PATTERN, + {'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'}, + defs, + ); + // x1, y1, x1, x2 properties will be set later in update. + if ((gridOptions['length'] ?? 1) > 0 && (gridOptions['spacing'] ?? 0) > 0) { + dom.createSvgElement( + Svg.LINE, + {'stroke': gridOptions['colour']}, + gridPattern, + ); + if (gridOptions['length'] ?? 1 > 1) { + dom.createSvgElement( + Svg.LINE, + {'stroke': gridOptions['colour']}, + gridPattern, + ); + } + } else { + // Edge 16 doesn't handle empty patterns + dom.createSvgElement(Svg.LINE, {}, gridPattern); + } + return gridPattern; + } + + /** + * Parse the user-specified grid options, using reasonable defaults where + * behaviour is unspecified. See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + * + * @param options Dictionary of options. + * @returns Normalized grid options. + */ + parseGridOptions(options: BlocklyOptions): GridOptions { + const grid = options['grid'] || {}; + const gridOptions = {} as GridOptions; + gridOptions.spacing = Number(grid['spacing']) || 0; + gridOptions.colour = grid['colour'] || '#888'; + gridOptions.length = + grid['length'] === undefined ? 1 : Number(grid['length']); + gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; + return gridOptions; + } + + createGrid(pattern: SVGElement, options: GridOptions): IGrid { + return new Grid(pattern, options); + } +} + /** * Class for a workspace's grid. */ -export class Grid { +export class Grid implements IGrid { private spacing: number; private length: number; private scale: number = 1; @@ -203,50 +277,6 @@ export class Grid { } return new Coordinate(x, y); } - - /** - * Create the DOM for the grid described by options. - * - * @param rnd A random ID to append to the pattern's ID. - * @param gridOptions The object containing grid configuration. - * @param defs The root SVG element for this workspace's defs. - * @returns The SVG element for the grid pattern. - * @internal - */ - static createDom( - rnd: string, - gridOptions: GridOptions, - defs: SVGElement, - ): SVGElement { - /* - - - - - */ - const gridPattern = dom.createSvgElement( - Svg.PATTERN, - {'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'}, - defs, - ); - // x1, y1, x1, x2 properties will be set later in update. - if ((gridOptions['length'] ?? 1) > 0 && (gridOptions['spacing'] ?? 0) > 0) { - dom.createSvgElement( - Svg.LINE, - {'stroke': gridOptions['colour']}, - gridPattern, - ); - if (gridOptions['length'] ?? 1 > 1) { - dom.createSvgElement( - Svg.LINE, - {'stroke': gridOptions['colour']}, - gridPattern, - ); - } - } else { - // Edge 16 doesn't handle empty patterns - dom.createSvgElement(Svg.LINE, {}, gridPattern); - } - return gridPattern; - } } + +registry.register(registry.Type.GRID_PROVIDER, registry.DEFAULT, GridProvider); diff --git a/core/inject.ts b/core/inject.ts index 40016bc23f4..3d6bdb8973c 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -12,7 +12,6 @@ import * as bumpObjects from './bump_objects.js'; import * as common from './common.js'; import * as Css from './css.js'; import * as dropDownDiv from './dropdowndiv.js'; -import {Grid} from './grid.js'; import {Msg} from './msg.js'; import {Options} from './options.js'; import {ScrollbarPair} from './scrollbar_pair.js'; @@ -141,7 +140,12 @@ function createDom(container: Element, options: Options): SVGElement { // https://neil.fraser.name/news/2015/11/01/ const rnd = String(Math.random()).substring(2); - options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs); + options.gridPattern = options.gridProvider.createDom( + rnd, + options.gridOptions, + defs, + ); + return svg; } diff --git a/core/interfaces/i_grid.ts b/core/interfaces/i_grid.ts new file mode 100644 index 00000000000..d45d795bdaa --- /dev/null +++ b/core/interfaces/i_grid.ts @@ -0,0 +1,116 @@ +import {BlocklyOptions} from '../blockly_options.js'; +import {GridOptions} from '../options.js'; +import {Coordinate} from '../utils'; +import type {IRegistrable} from './i_registrable.js'; + +export interface IGrid { + /** + * Sets the spacing between the centers of the grid lines. + * + * This does not trigger snapping to the newly spaced grid. If you want to + * snap blocks to the grid programmatically that needs to be triggered + * on individual top-level blocks. The next time a block is dragged and + * dropped it will snap to the grid if snapping to the grid is enabled. + */ + setSpacing(spacing: number): void; + + /** + * Get the spacing of the grid points (in px). + * + * @returns The spacing of the grid points. + */ + getSpacing(): number; + + /** Sets the length of the grid lines. */ + setLength(length: number): void; + + /** Get the length of the grid lines (in px). */ + getLength(): number; + + /** + * Sets whether blocks should snap to the grid or not. + * + * Setting this to true does not trigger snapping. If you want to snap blocks + * to the grid programmatically that needs to be triggered on individual + * top-level blocks. The next time a block is dragged and dropped it will + * snap to the grid. + */ + setSnapToGrid(snap: boolean): void; + + /** + * Whether blocks should snap to the grid. + * + * @returns True if blocks should snap, false otherwise. + */ + shouldSnap(): boolean; + + /** + * Get the ID of the pattern element, which should be randomized to avoid + * conflicts with other Blockly instances on the page. + * + * @returns The pattern ID. + * @internal + */ + getPatternId(): string; + + /** + * Update the grid with a new scale. + * + * @param scale The new workspace scale. + * @internal + */ + update(scale: number): void; + + /** + * Move the grid to a new x and y position, and make sure that change is + * visible. + * + * @param x The new x position of the grid (in px). + * @param y The new y position of the grid (in px). + * @internal + */ + moveTo(x: number, y: number): void; + + /** + * Given a coordinate, return the nearest coordinate aligned to the grid. + * + * @param xy A workspace coordinate. + * @returns Workspace coordinate of nearest grid point. + * If there's no change, return the same coordinate object. + */ + alignXY(xy: Coordinate): Coordinate; +} + +export interface IGridProvider extends IRegistrable { + /** + * Create the DOM for the grid described by options. + * + * @param rnd A random ID to append to the pattern's ID. + * @param gridOptions The object containing grid configuration. + * @param defs The root SVG element for this workspace's defs. + * @returns The SVG element for the grid pattern. + */ + createDom( + rnd: string, + gridOptions: GridOptions, + defs: SVGElement, + ): SVGElement; + + /** + * Parse the user-specified grid options, using reasonable defaults where + * behaviour is unspecified. See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + * + * @param options Dictionary of options. + * @returns Normalized grid options. + */ + parseGridOptions(options: BlocklyOptions): GridOptions; + + /** + * @param pattern The grid's SVG pattern, created during injection. + * @param options A dictionary of normalized options for the grid. + * See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + */ + createGrid(pattern: SVGElement, options: GridOptions): IGrid; +} diff --git a/core/options.ts b/core/options.ts index 539fd3f6f92..4ba6b04a587 100644 --- a/core/options.ts +++ b/core/options.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.Options import type {BlocklyOptions} from './blockly_options.js'; +import {IGridProvider} from './interfaces/i_grid.js'; import * as registry from './registry.js'; import {Theme} from './theme.js'; import {Classic} from './theme/classic.js'; @@ -60,6 +61,8 @@ export class Options { parentWorkspace: WorkspaceSvg | null; plugins: {[key: string]: (new (...p1: any[]) => any) | string}; + gridProvider: IGridProvider; + /** * If set, sets the translation of the workspace to match the scrollbars. * A function that @@ -175,7 +178,6 @@ export class Options { this.hasCss = hasCss; this.horizontalLayout = horizontalLayout; this.languageTree = toolboxJsonDef; - this.gridOptions = Options.parseGridOptions(options); this.zoomOptions = Options.parseZoomOptions(options); this.toolboxPosition = toolboxPosition; this.theme = Options.parseThemeOptions(options); @@ -191,6 +193,16 @@ export class Options { /** Map of plugin type to name of registered plugin or plugin class. */ this.plugins = plugins; + + // This should be safe to call since this.plugins has been set + const GridProvider = registry.getClassFromOptions( + registry.Type.GRID_PROVIDER, + this, + true, + ); + + this.gridProvider = new GridProvider!(); + this.gridOptions = this.gridProvider.parseGridOptions(options); } /** @@ -301,25 +313,6 @@ export class Options { return zoomOptions; } - /** - * Parse the user-specified grid options, using reasonable defaults where - * behaviour is unspecified. See grid documentation: - * https://developers.google.com/blockly/guides/configure/web/grid - * - * @param options Dictionary of options. - * @returns Normalized grid options. - */ - private static parseGridOptions(options: BlocklyOptions): GridOptions { - const grid = options['grid'] || {}; - const gridOptions = {} as GridOptions; - gridOptions.spacing = Number(grid['spacing']) || 0; - gridOptions.colour = grid['colour'] || '#888'; - gridOptions.length = - grid['length'] === undefined ? 1 : Number(grid['length']); - gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; - return gridOptions; - } - /** * Parse the user-specified theme options, using the classic theme as a * default. https://developers.google.com/blockly/guides/configure/web/themes diff --git a/core/registry.ts b/core/registry.ts index 60e8049797c..ce90716717b 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -14,6 +14,7 @@ import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js' import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; import type {IDragger} from './interfaces/i_dragger.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import {IGridProvider} from './interfaces/i_grid.js'; import type {IIcon} from './interfaces/i_icon.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IPaster} from './interfaces/i_paster.js'; @@ -101,6 +102,8 @@ export class Type<_T> { */ static BLOCK_DRAGGER = new Type('blockDragger'); + static GRID_PROVIDER = new Type('gridProvider'); + /** @internal */ static SERIALIZER = new Type('serializer'); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 6acd31c9c7f..ecb1d375b2f 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -37,11 +37,11 @@ import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import type {FlyoutButton} from './flyout_button.js'; import {Gesture} from './gesture.js'; -import {Grid} from './grid.js'; import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import {IGrid} from './interfaces/i_grid.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; import type {Cursor} from './keyboard_nav/cursor.js'; @@ -275,7 +275,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { */ private readonly highlightedBlocks: BlockSvg[] = []; private audioManager: WorkspaceAudio; - private grid: Grid | null; + private grid: IGrid | null; private markerManager: MarkerManager; /** @@ -355,7 +355,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** This workspace's grid object or null. */ this.grid = this.options.gridPattern - ? new Grid(this.options.gridPattern, options.gridOptions) + ? options.gridProvider.createGrid( + this.options.gridPattern, + options.gridOptions, + ) : null; /** Manager in charge of markers and cursors. */ @@ -2376,7 +2379,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @returns The grid object for this workspace. */ - getGrid(): Grid | null { + getGrid(): IGrid | null { return this.grid; }