diff --git a/core/blockly.js b/core/blockly.js index 94f485c99..808c035b0 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -17,6 +17,7 @@ goog.provide('Blockly'); goog.require('Blockly.browserEvents'); +goog.require('Blockly.clipboard'); goog.require('Blockly.ComponentManager'); goog.require('Blockly.connectionTypes'); goog.require('Blockly.constants'); @@ -96,21 +97,21 @@ Blockly.draggingConnections = []; * @type {Element} * @private */ -Blockly.clipboardXml_ = null; +Blockly.clipboardXml_ = Blockly.clipboard.xml; /** * Source of the local clipboard. * @type {Blockly.WorkspaceSvg} * @private */ -Blockly.clipboardSource_ = null; +Blockly.clipboardSource_ = Blockly.clipboard.source; /** * Map of types to type counts for the clipboard object and descendants. * @type {Object} * @private */ -Blockly.clipboardTypeCounts_ = null; +Blockly.clipboardTypeCounts_ = Blockly.clipboard.typeCounts; /** * Cached value for whether 3D is supported. @@ -234,39 +235,14 @@ Blockly.deleteBlock = function(selected) { * @param {!Blockly.ICopyable} toCopy Block or Workspace Comment to be copied. * @package */ -Blockly.copy = function(toCopy) { - var data = toCopy.toCopyData(); - if (data) { - Blockly.clipboardXml_ = data.xml; - Blockly.clipboardSource_ = data.source; - Blockly.clipboardTypeCounts_ = data.typeCounts; - } -}; +Blockly.copy = Blockly.clipboard.copy; /** * Paste a block or workspace comment on to the main workspace. * @return {boolean} True if the paste was successful, false otherwise. * @package */ -Blockly.paste = function() { - if (!Blockly.clipboardXml_) { - return false; - } - // Pasting always pastes to the main workspace, even if the copy - // started in a flyout workspace. - var workspace = Blockly.clipboardSource_; - if (workspace.isFlyout) { - workspace = workspace.targetWorkspace; - } - if (Blockly.clipboardTypeCounts_ && - workspace.isCapacityAvailable(Blockly.clipboardTypeCounts_)) { - Blockly.Events.setGroup(true); - workspace.paste(Blockly.clipboardXml_); - Blockly.Events.setGroup(false); - return true; - } - return false; -}; +Blockly.paste = Blockly.clipboard.paste; /** * Duplicate this block and its children, or a workspace comment. @@ -274,19 +250,7 @@ Blockly.paste = function() { * copied. * @package */ -Blockly.duplicate = function(toDuplicate) { - // Save the clipboard. - var clipboardXml = Blockly.clipboardXml_; - var clipboardSource = Blockly.clipboardSource_; - - // Create a duplicate via a copy/paste operation. - Blockly.copy(toDuplicate); - toDuplicate.workspace.paste(Blockly.clipboardXml_); - - // Restore the clipboard. - Blockly.clipboardXml_ = clipboardXml; - Blockly.clipboardSource_ = clipboardSource; -}; +Blockly.duplicate = Blockly.clipboard.duplicate; /** * Cancel the native context menu, unless the focus is on an HTML input widget. diff --git a/core/clipboard.js b/core/clipboard.js new file mode 100644 index 000000000..887346a58 --- /dev/null +++ b/core/clipboard.js @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Blockly's internal clipboard for managing copy-paste. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.module('Blockly.clipboard'); +goog.module.declareLegacyNamespace(); + +const Events = goog.require('Blockly.Events'); +/* eslint-disable-next-line no-unused-vars */ +const ICopyable = goog.requireType('Blockly.ICopyable'); +/* eslint-disable-next-line no-unused-vars */ +const WorkspaceSvg = goog.requireType('Blockly.WorkspaceSvg'); + + +/** + * Contents of the local clipboard. + * @type {Element} + * @private + */ +let xml = null; +exports.xml = xml; + +/** + * Source of the local clipboard. + * @type {WorkspaceSvg} + * @private + */ +let source = null; +exports.source = source; + +/** + * Map of types to type counts for the clipboard object and descendants. + * @type {Object} + * @private + */ +let typeCounts = null; +exports.typeCounts = typeCounts; + +/** + * Copy a block or workspace comment onto the local clipboard. + * @param {!ICopyable} toCopy Block or Workspace Comment to be copied. + * @package + */ +const copy = function(toCopy) { + var data = toCopy.toCopyData(); + if (data) { + xml = data.xml; + source = data.source; + typeCounts = data.typeCounts; + } +}; +exports.copy = copy; + +/** + * Paste a block or workspace comment on to the main workspace. + * @return {boolean} True if the paste was successful, false otherwise. + * @package + */ +const paste = function() { + if (!xml) { + return false; + } + // Pasting always pastes to the main workspace, even if the copy + // started in a flyout workspace. + var workspace = source; + if (workspace.isFlyout) { + workspace = workspace.targetWorkspace; + } + if (typeCounts && workspace.isCapacityAvailable(typeCounts)) { + Events.setGroup(true); + workspace.paste(xml); + Events.setGroup(false); + return true; + } + return false; +}; +exports.paste = paste; + +/** + * Duplicate this block and its children, or a workspace comment. + * @param {!ICopyable} toDuplicate Block or Workspace Comment to be + * copied. + * @package + */ +const duplicate = function(toDuplicate) { + // Save the clipboard. + const oldXml = xml; + const oldSource = source; + + // Create a duplicate via a copy/paste operation. + copy(toDuplicate); + toDuplicate.workspace.paste(xml); + + // Restore the clipboard. + xml = oldXml; + source = oldSource; +}; +exports.duplicate = duplicate; diff --git a/core/contextmenu_items.js b/core/contextmenu_items.js index dffd4a83d..e985a8e5c 100644 --- a/core/contextmenu_items.js +++ b/core/contextmenu_items.js @@ -15,7 +15,7 @@ * @namespace */ goog.provide('Blockly.ContextMenuItems'); - +goog.require('Blockly.clipboard'); /** @suppress {extraRequire} */ goog.require('Blockly.constants'); goog.require('Blockly.ContextMenuRegistry'); @@ -316,7 +316,7 @@ Blockly.ContextMenuItems.registerDuplicate = function() { }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.block) { - Blockly.duplicate(scope.block); + Blockly.clipboard.duplicate(scope.block); } }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, diff --git a/core/shortcut_items.js b/core/shortcut_items.js index ef1cb07fc..494b64bfe 100644 --- a/core/shortcut_items.js +++ b/core/shortcut_items.js @@ -16,6 +16,7 @@ */ goog.provide('Blockly.ShortcutItems'); +goog.require('Blockly.clipboard'); goog.require('Blockly.Gesture'); goog.require('Blockly.ShortcutRegistry'); goog.require('Blockly.utils.KeyCodes'); @@ -104,7 +105,7 @@ Blockly.ShortcutItems.registerCopy = function() { // an error due to the lack of a selection. e.preventDefault(); Blockly.hideChaff(); - Blockly.copy(/** @type {!Blockly.ICopyable} */ (Blockly.selected)); + Blockly.clipboard.copy(/** @type {!Blockly.ICopyable} */ (Blockly.selected)); return true; } }; @@ -137,7 +138,7 @@ Blockly.ShortcutItems.registerCut = function() { !Blockly.selected.workspace.isFlyout; }, callback: function() { - Blockly.copy(/** @type {!Blockly.ICopyable} */ (Blockly.selected)); + Blockly.clipboard.copy(/** @type {!Blockly.ICopyable} */ (Blockly.selected)); Blockly.deleteBlock(/** @type {!Blockly.BlockSvg} */ (Blockly.selected)); return true; } @@ -167,7 +168,7 @@ Blockly.ShortcutItems.registerPaste = function() { return !workspace.options.readOnly && !Blockly.Gesture.inProgress(); }, callback: function() { - return Blockly.paste(); + return Blockly.clipboard.paste(); } }; diff --git a/tests/deps.js b/tests/deps.js index b28f86da2..44b852995 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -12,11 +12,12 @@ goog.addDependency('../../core/block_animations.js', ['Blockly.blockAnimations'] goog.addDependency('../../core/block_drag_surface.js', ['Blockly.BlockDragSurfaceSvg'], ['Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/block_dragger.js', ['Blockly.BlockDragger'], ['Blockly.Events', 'Blockly.Events.BlockDrag', 'Blockly.Events.BlockMove', 'Blockly.IBlockDragger', 'Blockly.InsertionMarkerManager', 'Blockly.blockAnimations', 'Blockly.registry', 'Blockly.utils.Coordinate', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/block_svg.js', ['Blockly.BlockSvg'], ['Blockly.ASTNode', 'Blockly.Block', 'Blockly.ContextMenu', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.Events.Selected', 'Blockly.IASTNodeLocationSvg', 'Blockly.IBoundedElement', 'Blockly.ICopyable', 'Blockly.IDraggable', 'Blockly.Msg', 'Blockly.RenderedConnection', 'Blockly.TabNavigateCursor', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Xml', 'Blockly.blockAnimations', 'Blockly.blockRendering.IPathObject', 'Blockly.browserEvents', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.deprecation', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent']); -goog.addDependency('../../core/blockly.js', ['Blockly'], ['Blockly.ComponentManager', 'Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.FinishedLoading', 'Blockly.Events.Ui', 'Blockly.Events.UiBase', 'Blockly.Events.VarCreate', 'Blockly.Procedures', 'Blockly.ShortcutRegistry', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WidgetDiv', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.inject', 'Blockly.inputTypes', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.colour', 'Blockly.utils.deprecation', 'Blockly.utils.toolbox']); +goog.addDependency('../../core/blockly.js', ['Blockly'], ['Blockly.ComponentManager', 'Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.FinishedLoading', 'Blockly.Events.Ui', 'Blockly.Events.UiBase', 'Blockly.Events.VarCreate', 'Blockly.Procedures', 'Blockly.ShortcutRegistry', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WidgetDiv', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.clipboard', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.inject', 'Blockly.inputTypes', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.colour', 'Blockly.utils.deprecation', 'Blockly.utils.toolbox']); goog.addDependency('../../core/blocks.js', ['Blockly.Blocks'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/browser_events.js', ['Blockly.browserEvents'], ['Blockly.Touch', 'Blockly.utils.global']); goog.addDependency('../../core/bubble.js', ['Blockly.Bubble'], ['Blockly.IBubble', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.Workspace', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.userAgent']); goog.addDependency('../../core/bubble_dragger.js', ['Blockly.BubbleDragger'], ['Blockly.Bubble', 'Blockly.ComponentManager', 'Blockly.Events', 'Blockly.Events.CommentMove', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Coordinate'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/clipboard.js', ['Blockly.clipboard'], ['Blockly.Events'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.browserEvents', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/component_manager.js', ['Blockly.ComponentManager'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.Xml', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.utils.deprecation'], {'lang': 'es6', 'module': 'goog'}); @@ -25,7 +26,7 @@ goog.addDependency('../../core/connection_db.js', ['Blockly.ConnectionDB'], ['Bl goog.addDependency('../../core/connection_types.js', ['Blockly.connectionTypes'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/constants.js', ['Blockly.constants'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/contextmenu.js', ['Blockly.ContextMenu'], ['Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.userAgent']); -goog.addDependency('../../core/contextmenu_items.js', ['Blockly.ContextMenuItems'], ['Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.constants', 'Blockly.inputTypes'], {'lang': 'es5'}); +goog.addDependency('../../core/contextmenu_items.js', ['Blockly.ContextMenuItems'], ['Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.clipboard', 'Blockly.constants', 'Blockly.inputTypes'], {'lang': 'es5'}); goog.addDependency('../../core/contextmenu_registry.js', ['Blockly.ContextMenuRegistry'], [], {'lang': 'es5'}); goog.addDependency('../../core/css.js', ['Blockly.Css'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/delete_area.js', ['Blockly.DeleteArea'], ['Blockly.BlockSvg', 'Blockly.DragTarget', 'Blockly.IDeleteArea', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); @@ -159,7 +160,7 @@ goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos. goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo']); goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', 'Blockly.Comment', 'Blockly.ContextMenuItems', 'Blockly.FieldAngle', 'Blockly.FieldCheckbox', 'Blockly.FieldColour', 'Blockly.FieldDropdown', 'Blockly.FieldImage', 'Blockly.FieldLabelSerializable', 'Blockly.FieldMultilineInput', 'Blockly.FieldNumber', 'Blockly.FieldTextInput', 'Blockly.FieldVariable', 'Blockly.FlyoutButton', 'Blockly.Generator', 'Blockly.HorizontalFlyout', 'Blockly.Mutator', 'Blockly.ShortcutItems', 'Blockly.Themes.Classic', 'Blockly.Toolbox', 'Blockly.Trashcan', 'Blockly.VariablesDynamic', 'Blockly.VerticalFlyout', 'Blockly.Warning', 'Blockly.ZoomControls', 'Blockly.geras.Renderer', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Svg', 'Blockly.utils.dom']); -goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.utils.KeyCodes']); +goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.clipboard', 'Blockly.utils.KeyCodes']); goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils', 'Blockly.utils.object']); goog.addDependency('../../core/theme/classic.js', ['Blockly.Themes.Classic'], ['Blockly.Theme']); diff --git a/tests/mocha/contextmenu_items_test.js b/tests/mocha/contextmenu_items_test.js index 62dd6f9ea..0d69fa77e 100644 --- a/tests/mocha/contextmenu_items_test.js +++ b/tests/mocha/contextmenu_items_test.js @@ -17,11 +17,11 @@ suite('Context Menu Items', function() { Blockly.ContextMenuItems.registerDefaultOptions(); this.registry = Blockly.ContextMenuRegistry.registry; }); - + teardown(function() { sharedTestTeardown.call(this); }); - + suite('Workspace Items', function() { setup(function() { this.scope = {workspace: this.workspace}; @@ -328,12 +328,12 @@ suite('Context Menu Items', function() { }); test('Calls duplicate', function() { - var stub = sinon.stub(Blockly, 'duplicate'); + var spy = sinon.spy(Blockly.clipboard, 'duplicate'); this.duplicateOption.callback(this.scope); - sinon.assert.calledOnce(stub); - sinon.assert.calledWith(stub, this.block); + sinon.assert.calledOnce(spy); + sinon.assert.calledWith(spy, this.block); }); test('Has correct label', function() { diff --git a/tests/mocha/keydown_test.js b/tests/mocha/keydown_test.js index aec646111..22fdbf96e 100644 --- a/tests/mocha/keydown_test.js +++ b/tests/mocha/keydown_test.js @@ -93,7 +93,7 @@ suite('Key Down', function() { suite('Copy', function() { setup(function() { setSelectedBlock(this.workspace); - this.copySpy = sinon.spy(Blockly, 'copy'); + this.copySpy = sinon.spy(Blockly.clipboard, 'copy'); this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); }); var testCases = [