UNPKG

@blockly/zoom-to-fit

Version:

A Blockly plugin that adds a zoom-to-fit control to the workspace.

269 lines (243 loc) 6.97 kB
/** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as Blockly from 'blockly/core'; /** * Class for zoom to fit control. */ export class ZoomToFitControl implements Blockly.IPositionable { /** * The unique id for this component. */ id = 'zoomToFit'; /** * The SVG group containing the zoom-to-fit control. */ private svgGroup: SVGElement | null = null; /** * Left coordinate of the zoom-to-fit control. */ private left = 0; /** * Top coordinate of the zoom-to-fit control. */ private top = 0; /** * Width of the zoom-to-fit control. */ private readonly width = 32; /** * Height of the zoom-to-fit control. */ private readonly height = 32; /** * Distance between zoom-to-fit control and bottom or top edge of workspace. */ private readonly marginVertical = 20; /** * Distance between zoom-to-fit control and right or left edge of workspace. */ private readonly marginHorizontal = 20; /** * Whether this has been initialized. */ private initialized = false; private onZoomToFitWrapper: Blockly.browserEvents.Data | null = null; /** * Constructor for zoom-to-fit control. * * @param workspace The workspace that the zoom-to-fit * control will be added to. */ constructor(protected workspace: Blockly.WorkspaceSvg) {} /** * Initializes the zoom reset control. */ init() { this.workspace.getComponentManager().addComponent({ component: this, weight: 2, capabilities: [Blockly.ComponentManager.Capability.POSITIONABLE], }); this.createDom(); this.initialized = true; this.workspace.resize(); } /** * Disposes of workspace search. * Unlink from all DOM elements and remove all event listeners * to prevent memory leaks. */ dispose() { if (this.svgGroup) { Blockly.utils.dom.removeNode(this.svgGroup); } if (this.onZoomToFitWrapper) { Blockly.browserEvents.unbind(this.onZoomToFitWrapper); this.onZoomToFitWrapper = null; } } /** * Creates DOM for ui element. */ private createDom() { this.svgGroup = Blockly.utils.dom.createSvgElement( Blockly.utils.Svg.IMAGE, { height: `${this.height}px`, width: `${this.width}px`, class: 'zoomToFit', }, ); this.svgGroup.setAttributeNS( Blockly.utils.dom.XLINK_NS, 'xlink:href', zoomToFitSvgDataUri, ); Blockly.utils.dom.insertAfter( this.svgGroup, this.workspace.getBubbleCanvas(), ); // Attach listener. this.onZoomToFitWrapper = Blockly.browserEvents.conditionalBind( this.svgGroup, 'pointerdown', null, this.onClick.bind(this), ); } /** * Handle click event. * * @param e A pointer down event. */ private onClick(e: PointerEvent) { this.workspace.zoomToFit(); const uiEvent = new (Blockly.Events.get(Blockly.Events.CLICK))( null, this.workspace.id, 'zoom_reset_control', ); Blockly.Events.fire(uiEvent); e.stopPropagation(); // avoid to also fire workspace click event e.preventDefault(); } /** * Returns the bounding rectangle of the UI element in pixel units relative to * the Blockly injection div. * * @returns The component’s bounding box. */ getBoundingRectangle(): Blockly.utils.Rect { return new Blockly.utils.Rect( this.top, this.top + this.height, this.left, this.left + this.width, ); } /** * Positions the zoom-to-fit control. * 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: Blockly.MetricsManager.UiMetrics, savedPositions: Blockly.utils.Rect[], ) { if (!this.initialized) { return; } const hasVerticalScrollbars = this.workspace.scrollbar && this.workspace.scrollbar.canScrollHorizontally(); const hasHorizontalScrollbars = this.workspace.scrollbar && this.workspace.scrollbar.canScrollVertically(); if ( metrics.toolboxMetrics.position === Blockly.TOOLBOX_AT_LEFT || (this.workspace.horizontalLayout && !this.workspace.RTL) ) { // Right corner placement. this.left = metrics.absoluteMetrics.left + metrics.viewMetrics.width - this.width - this.marginHorizontal; if (hasVerticalScrollbars && !this.workspace.RTL) { this.left -= Blockly.Scrollbar.scrollbarThickness; } } else { // Left corner placement. this.left = this.marginHorizontal; if (hasVerticalScrollbars && this.workspace.RTL) { this.left += Blockly.Scrollbar.scrollbarThickness; } } const startAtBottom = metrics.toolboxMetrics.position !== Blockly.TOOLBOX_AT_BOTTOM; if (startAtBottom) { // Bottom corner placement this.top = metrics.absoluteMetrics.top + metrics.viewMetrics.height - this.height - this.marginVertical; if (hasHorizontalScrollbars) { // The horizontal scrollbars are always positioned on the bottom. this.top -= Blockly.Scrollbar.scrollbarThickness; } } else { // Upper corner placement this.top = metrics.absoluteMetrics.top + this.marginVertical; } // Check for collision and bump if needed. let boundingRect = this.getBoundingRectangle(); for (let i = 0, otherEl; (otherEl = savedPositions[i]); i++) { if (boundingRect.intersects(otherEl)) { if (startAtBottom) { // Bump up. this.top = otherEl.top - this.height - this.marginVertical; } else { // Bump down. this.top = otherEl.bottom + this.marginVertical; } // Recheck other savedPositions boundingRect = this.getBoundingRectangle(); i = -1; } } this.svgGroup?.setAttribute( 'transform', `translate(${this.left}, ${this.top})`, ); } } /** * Base64 encoded data uri for zoom to fit icon. */ const zoomToFitSvgDataUri = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC' + '9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZm' + 'lsbD0iIzU0NkU3QSI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PH' + 'BhdGggZD0iTTUgNi40Mkw4LjA5IDkuNSA5LjUgOC4wOSA2LjQxIDVIOVYzSDN2Nmgyem0xMC' + '0zLjQxdjJoMi41N0wxNC41IDguMDlsMS40MSAxLjQxTDE5IDYuNDFWOWgyVjMuMDF6bTQgMT' + 'QuNTdsLTMuMDktMy4wOC0xLjQxIDEuNDFMMTcuNTkgMTlIMTV2Mmg2di02aC0yek04LjA5ID' + 'E0LjVMNSAxNy41OVYxNUgzdjZoNnYtMkg2LjQybDMuMDgtMy4wOXoiLz48L3N2Zz4='; Blockly.Css.register(` .zoomToFit { opacity: 0.4; } .zoomToFit:hover { opacity: 0.6; } .zoomToFit:active { opacity: 0.8; } `);