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;
}