/** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Utilities for bumping objects back into worksapce bounds. */ 'use strict'; /** * Utilities for bumping objects back into worksapce bounds. * @namespace Blockly.bumpObjects */ goog.module('Blockly.bumpObjects'); const eventUtils = goog.require('Blockly.Events.utils'); const mathUtils = goog.require('Blockly.utils.math'); /* eslint-disable-next-line no-unused-vars */ const {Abstract} = goog.requireType('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {BlockSvg} = goog.requireType('Blockly.BlockSvg'); /* eslint-disable-next-line no-unused-vars */ const {IBoundedElement} = goog.requireType('Blockly.IBoundedElement'); /* eslint-disable-next-line no-unused-vars */ const {MetricsManager} = goog.requireType('Blockly.MetricsManager'); /* eslint-disable-next-line no-unused-vars */ const {ViewportChange} = goog.requireType('Blockly.Events.ViewportChange'); /* eslint-disable-next-line no-unused-vars */ const {WorkspaceCommentSvg} = goog.requireType('Blockly.WorkspaceCommentSvg'); /* eslint-disable-next-line no-unused-vars */ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); /** * Bumps the given object that has passed out of bounds. * @param {!WorkspaceSvg} workspace The workspace containing the object. * @param {!MetricsManager.ContainerRegion} scrollMetrics Scroll metrics * in workspace coordinates. * @param {!IBoundedElement} object The object to bump. * @return {boolean} True if block was bumped. * @alias Blockly.bumpObjects.bumpIntoBounds */ const bumpObjectIntoBounds = function(workspace, scrollMetrics, object) { // Compute new top/left position for object. const objectMetrics = object.getBoundingRectangle(); const height = objectMetrics.bottom - objectMetrics.top; const width = objectMetrics.right - objectMetrics.left; const topClamp = scrollMetrics.top; const scrollMetricsBottom = scrollMetrics.top + scrollMetrics.height; const bottomClamp = scrollMetricsBottom - height; // If the object is taller than the workspace we want to // top-align the block const newYPosition = mathUtils.clamp(topClamp, objectMetrics.top, bottomClamp); const deltaY = newYPosition - objectMetrics.top; // Note: Even in RTL mode the "anchor" of the object is the // top-left corner of the object. let leftClamp = scrollMetrics.left; const scrollMetricsRight = scrollMetrics.left + scrollMetrics.width; let rightClamp = scrollMetricsRight - width; if (workspace.RTL) { // If the object is wider than the workspace and we're in RTL // mode we want to right-align the block, which means setting // the left clamp to match. leftClamp = Math.min(rightClamp, leftClamp); } else { // If the object is wider than the workspace and we're in LTR // mode we want to left-align the block, which means setting // the right clamp to match. rightClamp = Math.max(leftClamp, rightClamp); } const newXPosition = mathUtils.clamp(leftClamp, objectMetrics.left, rightClamp); const deltaX = newXPosition - objectMetrics.left; if (deltaX || deltaY) { object.moveBy(deltaX, deltaY); return true; } return false; }; exports.bumpIntoBounds = bumpObjectIntoBounds; /** * Creates a handler for bumping objects when they cross fixed bounds. * @param {!WorkspaceSvg} workspace The workspace to handle. * @return {function(Abstract)} The event handler. * @alias Blockly.bumpObjects.bumpIntoBoundsHandler */ const bumpIntoBoundsHandler = function(workspace) { return function(e) { const metricsManager = workspace.getMetricsManager(); if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { return; } if (eventUtils.BUMP_EVENTS.indexOf(e.type) !== -1) { const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); // Triggered by move/create event const object = extractObjectFromEvent(workspace, e); if (!object) { return; } // Handle undo. const oldGroup = eventUtils.getGroup(); eventUtils.setGroup(e.group); const wasBumped = bumpObjectIntoBounds( workspace, scrollMetricsInWsCoords, /** @type {!IBoundedElement} */ (object)); if (wasBumped && !e.group) { console.warn( 'Moved object in bounds but there was no' + ' event group. This may break undo.'); } if (oldGroup !== null) { eventUtils.setGroup(oldGroup); } } else if (e.type === eventUtils.VIEWPORT_CHANGE) { const viewportEvent = /** @type {!ViewportChange} */ (e); if (viewportEvent.scale > viewportEvent.oldScale) { bumpTopObjectsIntoBounds(workspace); } } }; }; exports.bumpIntoBoundsHandler = bumpIntoBoundsHandler; /** * Extracts the object from the given event. * @param {!WorkspaceSvg} workspace The workspace the event originated * from. * @param {!eventUtils.BumpEvent} e An event containing an object. * @return {?BlockSvg|?WorkspaceCommentSvg} The extracted * object. */ const extractObjectFromEvent = function(workspace, e) { let object = null; switch (e.type) { case eventUtils.BLOCK_CREATE: case eventUtils.BLOCK_MOVE: object = workspace.getBlockById(e.blockId); if (object) { object = object.getRootBlock(); } break; case eventUtils.COMMENT_CREATE: case eventUtils.COMMENT_MOVE: object = ( /** @type {?WorkspaceCommentSvg} */ (workspace.getCommentById(e.commentId))); break; } return object; }; /** * Bumps the top objects in the given workspace into bounds. * @param {!WorkspaceSvg} workspace The workspace. * @alias Blockly.bumpObjects.bumpTopObjectsIntoBounds */ const bumpTopObjectsIntoBounds = function(workspace) { const metricsManager = workspace.getMetricsManager(); if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { return; } const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); const topBlocks = workspace.getTopBoundedElements(); for (let i = 0, block; (block = topBlocks[i]); i++) { bumpObjectIntoBounds(workspace, scrollMetricsInWsCoords, block); } }; exports.bumpTopObjectsIntoBounds = bumpTopObjectsIntoBounds;