mirror of
https://github.com/google/blockly.git
synced 2026-01-06 08:30:13 +01:00
feat: merge copy paste work into develop
Merge pull request #7379 from google/operation-copy-that
This commit is contained in:
@@ -37,7 +37,7 @@ import {FieldLabel} from './field_label.js';
|
|||||||
import type {Input} from './inputs/input.js';
|
import type {Input} from './inputs/input.js';
|
||||||
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyable} from './interfaces/i_copyable.js';
|
||||||
import type {IDraggable} from './interfaces/i_draggable.js';
|
import type {IDraggable} from './interfaces/i_draggable.js';
|
||||||
import {IIcon} from './interfaces/i_icon.js';
|
import {IIcon} from './interfaces/i_icon.js';
|
||||||
import * as internalConstants from './internal_constants.js';
|
import * as internalConstants from './internal_constants.js';
|
||||||
@@ -62,6 +62,7 @@ import type {WorkspaceSvg} from './workspace_svg.js';
|
|||||||
import * as renderManagement from './render_management.js';
|
import * as renderManagement from './render_management.js';
|
||||||
import * as deprecation from './utils/deprecation.js';
|
import * as deprecation from './utils/deprecation.js';
|
||||||
import {IconType} from './icons/icon_types.js';
|
import {IconType} from './icons/icon_types.js';
|
||||||
|
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for a block's SVG representation.
|
* Class for a block's SVG representation.
|
||||||
@@ -69,7 +70,11 @@ import {IconType} from './icons/icon_types.js';
|
|||||||
*/
|
*/
|
||||||
export class BlockSvg
|
export class BlockSvg
|
||||||
extends Block
|
extends Block
|
||||||
implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
|
implements
|
||||||
|
IASTNodeLocationSvg,
|
||||||
|
IBoundedElement,
|
||||||
|
ICopyable<BlockCopyData>,
|
||||||
|
IDraggable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Constant for identifying rows that are to be rendered inline.
|
* Constant for identifying rows that are to be rendered inline.
|
||||||
@@ -823,18 +828,17 @@ export class BlockSvg
|
|||||||
* Encode a block for copying.
|
* Encode a block for copying.
|
||||||
*
|
*
|
||||||
* @returns Copy metadata, or null if the block is an insertion marker.
|
* @returns Copy metadata, or null if the block is an insertion marker.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
toCopyData(): CopyData | null {
|
toCopyData(): BlockCopyData | null {
|
||||||
if (this.isInsertionMarker_) {
|
if (this.isInsertionMarker_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
saveInfo: blocks.save(this, {
|
paster: BlockPaster.TYPE,
|
||||||
|
blockState: blocks.save(this, {
|
||||||
addCoordinates: true,
|
addCoordinates: true,
|
||||||
addNextBlocks: false,
|
addNextBlocks: false,
|
||||||
}) as blocks.State,
|
}) as blocks.State,
|
||||||
source: this.workspace,
|
|
||||||
typeCounts: common.getBlockTypeCounts(this, true),
|
typeCounts: common.getBlockTypeCounts(this, true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.j
|
|||||||
import {IComponent} from './interfaces/i_component.js';
|
import {IComponent} from './interfaces/i_component.js';
|
||||||
import {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
import {IConnectionChecker} from './interfaces/i_connection_checker.js';
|
||||||
import {IContextMenu} from './interfaces/i_contextmenu.js';
|
import {IContextMenu} from './interfaces/i_contextmenu.js';
|
||||||
import {ICopyable} from './interfaces/i_copyable.js';
|
import {ICopyable, isCopyable} from './interfaces/i_copyable.js';
|
||||||
import {IDeletable} from './interfaces/i_deletable.js';
|
import {IDeletable} from './interfaces/i_deletable.js';
|
||||||
import {IDeleteArea} from './interfaces/i_delete_area.js';
|
import {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||||
import {IDragTarget} from './interfaces/i_drag_target.js';
|
import {IDragTarget} from './interfaces/i_drag_target.js';
|
||||||
@@ -152,6 +152,7 @@ import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
|||||||
import {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
import {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||||
import {IMovable} from './interfaces/i_movable.js';
|
import {IMovable} from './interfaces/i_movable.js';
|
||||||
import {IObservable, isObservable} from './interfaces/i_observable.js';
|
import {IObservable, isObservable} from './interfaces/i_observable.js';
|
||||||
|
import {IPaster, isPaster} from './interfaces/i_paster.js';
|
||||||
import {IPositionable} from './interfaces/i_positionable.js';
|
import {IPositionable} from './interfaces/i_positionable.js';
|
||||||
import {IRegistrable} from './interfaces/i_registrable.js';
|
import {IRegistrable} from './interfaces/i_registrable.js';
|
||||||
import {ISelectable} from './interfaces/i_selectable.js';
|
import {ISelectable} from './interfaces/i_selectable.js';
|
||||||
@@ -591,7 +592,7 @@ export {IComponent};
|
|||||||
export {IConnectionChecker};
|
export {IConnectionChecker};
|
||||||
export {IContextMenu};
|
export {IContextMenu};
|
||||||
export {icons};
|
export {icons};
|
||||||
export {ICopyable};
|
export {ICopyable, isCopyable};
|
||||||
export {IDeletable};
|
export {IDeletable};
|
||||||
export {IDeleteArea};
|
export {IDeleteArea};
|
||||||
export {IDragTarget};
|
export {IDragTarget};
|
||||||
@@ -606,6 +607,7 @@ export {Input};
|
|||||||
export {inputs};
|
export {inputs};
|
||||||
export {InsertionMarkerManager};
|
export {InsertionMarkerManager};
|
||||||
export {IObservable, isObservable};
|
export {IObservable, isObservable};
|
||||||
|
export {IPaster, isPaster};
|
||||||
export {IPositionable};
|
export {IPositionable};
|
||||||
export {IRegistrable};
|
export {IRegistrable};
|
||||||
export {ISelectable};
|
export {ISelectable};
|
||||||
|
|||||||
@@ -7,78 +7,149 @@
|
|||||||
import * as goog from '../closure/goog/goog.js';
|
import * as goog from '../closure/goog/goog.js';
|
||||||
goog.declareModuleId('Blockly.clipboard');
|
goog.declareModuleId('Blockly.clipboard');
|
||||||
|
|
||||||
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||||
|
import {BlockPaster} from './clipboard/block_paster.js';
|
||||||
|
import * as globalRegistry from './registry.js';
|
||||||
|
import {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
import * as registry from './clipboard/registry.js';
|
||||||
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
|
import * as deprecation from './utils/deprecation.js';
|
||||||
|
|
||||||
/** Metadata about the object that is currently on the clipboard. */
|
/** Metadata about the object that is currently on the clipboard. */
|
||||||
let copyData: CopyData | null = null;
|
let stashedCopyData: ICopyData | null = null;
|
||||||
|
|
||||||
|
let stashedWorkspace: WorkspaceSvg | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy a block or workspace comment onto the local clipboard.
|
* Copy a copyable element onto the local clipboard.
|
||||||
*
|
*
|
||||||
* @param toCopy Block or Workspace Comment to be copied.
|
* @param toCopy The copyable element to be copied.
|
||||||
|
* @deprecated v11. Use `myCopyable.toCopyData()` instead. To be removed v12.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function copy(toCopy: ICopyable) {
|
export function copy<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
|
||||||
TEST_ONLY.copyInternal(toCopy);
|
deprecation.warn(
|
||||||
|
'Blockly.clipboard.copy',
|
||||||
|
'v11',
|
||||||
|
'v12',
|
||||||
|
'myCopyable.toCopyData()',
|
||||||
|
);
|
||||||
|
return TEST_ONLY.copyInternal(toCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private version of copy for stubbing in tests.
|
* Private version of copy for stubbing in tests.
|
||||||
*/
|
*/
|
||||||
function copyInternal(toCopy: ICopyable) {
|
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
|
||||||
copyData = toCopy.toCopyData();
|
const data = toCopy.toCopyData();
|
||||||
|
stashedCopyData = data;
|
||||||
|
stashedWorkspace = (toCopy as any).workspace ?? null;
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paste a block or workspace comment on to the main workspace.
|
* Paste a pasteable element into the workspace.
|
||||||
*
|
*
|
||||||
|
* @param copyData The data to paste into the workspace.
|
||||||
|
* @param workspace The workspace to paste the data into.
|
||||||
|
* @param coordinate The location to paste the thing at.
|
||||||
* @returns The pasted thing if the paste was successful, null otherwise.
|
* @returns The pasted thing if the paste was successful, null otherwise.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
export function paste(): ICopyable | null {
|
export function paste<T extends ICopyData>(
|
||||||
if (!copyData) {
|
copyData: T,
|
||||||
return null;
|
workspace: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): ICopyable<T> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pastes the last copied ICopyable into the workspace.
|
||||||
|
*
|
||||||
|
* @returns the pasted thing if the paste was successful, null otherwise.
|
||||||
|
*/
|
||||||
|
export function paste(): ICopyable<ICopyData> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pastes the given data into the workspace, or the last copied ICopyable if
|
||||||
|
* no data is passed.
|
||||||
|
*
|
||||||
|
* @param copyData The data to paste into the workspace.
|
||||||
|
* @param workspace The workspace to paste the data into.
|
||||||
|
* @param coordinate The location to paste the thing at.
|
||||||
|
* @returns The pasted thing if the paste was successful, null otherwise.
|
||||||
|
*/
|
||||||
|
export function paste<T extends ICopyData>(
|
||||||
|
copyData?: T,
|
||||||
|
workspace?: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): ICopyable<ICopyData> | null {
|
||||||
|
if (!copyData || !workspace) {
|
||||||
|
if (!stashedCopyData || !stashedWorkspace) return null;
|
||||||
|
return pasteFromData(stashedCopyData, stashedWorkspace);
|
||||||
}
|
}
|
||||||
// Pasting always pastes to the main workspace, even if the copy
|
return pasteFromData(copyData, workspace, coordinate);
|
||||||
// started in a flyout workspace.
|
|
||||||
let workspace = copyData.source;
|
|
||||||
if (workspace.isFlyout) {
|
|
||||||
workspace = workspace.targetWorkspace!;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
copyData.typeCounts &&
|
|
||||||
workspace.isCapacityAvailable(copyData.typeCounts)
|
|
||||||
) {
|
|
||||||
return workspace.paste(copyData.saveInfo);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate this block and its children, or a workspace comment.
|
* Paste a pasteable element into the workspace.
|
||||||
*
|
*
|
||||||
* @param toDuplicate Block or Workspace Comment to be duplicated.
|
* @param copyData The data to paste into the workspace.
|
||||||
* @returns The block or workspace comment that was duplicated, or null if the
|
* @param workspace The workspace to paste the data into.
|
||||||
* duplication failed.
|
* @param coordinate The location to paste the thing at.
|
||||||
|
* @returns The pasted thing if the paste was successful, null otherwise.
|
||||||
|
*/
|
||||||
|
function pasteFromData<T extends ICopyData>(
|
||||||
|
copyData: T,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): ICopyable<T> | null {
|
||||||
|
workspace = workspace.getRootWorkspace() ?? workspace;
|
||||||
|
return (globalRegistry
|
||||||
|
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
|
||||||
|
?.paste(copyData, workspace, coordinate) ?? null) as ICopyable<T> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate this copy-paste-able element.
|
||||||
|
*
|
||||||
|
* @param toDuplicate The element to be duplicated.
|
||||||
|
* @returns The element that was duplicated, or null if the duplication failed.
|
||||||
|
* @deprecated v11. Use
|
||||||
|
* `Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)` instead.
|
||||||
|
* To be removed v12.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function duplicate(toDuplicate: ICopyable): ICopyable | null {
|
export function duplicate<
|
||||||
|
U extends ICopyData,
|
||||||
|
T extends ICopyable<U> & IHasWorkspace,
|
||||||
|
>(toDuplicate: T): T | null {
|
||||||
|
deprecation.warn(
|
||||||
|
'Blockly.clipboard.duplicate',
|
||||||
|
'v11',
|
||||||
|
'v12',
|
||||||
|
'Blockly.clipboard.paste(myCopyable.toCopyData(), myWorkspace)',
|
||||||
|
);
|
||||||
return TEST_ONLY.duplicateInternal(toDuplicate);
|
return TEST_ONLY.duplicateInternal(toDuplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private version of duplicate for stubbing in tests.
|
* Private version of duplicate for stubbing in tests.
|
||||||
*/
|
*/
|
||||||
function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
|
function duplicateInternal<
|
||||||
const oldCopyData = copyData;
|
U extends ICopyData,
|
||||||
copy(toDuplicate);
|
T extends ICopyable<U> & IHasWorkspace,
|
||||||
const pastedThing =
|
>(toDuplicate: T): T | null {
|
||||||
toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;
|
const data = toDuplicate.toCopyData();
|
||||||
copyData = oldCopyData;
|
if (!data) return null;
|
||||||
return pastedThing;
|
return paste(data, toDuplicate.workspace) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IHasWorkspace {
|
||||||
|
workspace: WorkspaceSvg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TEST_ONLY = {
|
export const TEST_ONLY = {
|
||||||
duplicateInternal,
|
duplicateInternal,
|
||||||
copyInternal,
|
copyInternal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export {BlockPaster, registry};
|
||||||
|
|||||||
120
core/clipboard/block_paster.ts
Normal file
120
core/clipboard/block_paster.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {BlockSvg} from '../block_svg.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
|
import {ICopyData} from '../interfaces/i_copyable.js';
|
||||||
|
import {IPaster} from '../interfaces/i_paster.js';
|
||||||
|
import {State, append} from '../serialization/blocks.js';
|
||||||
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||||
|
import * as eventUtils from '../events/utils.js';
|
||||||
|
import {config} from '../config.js';
|
||||||
|
|
||||||
|
export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
|
||||||
|
static TYPE = 'block';
|
||||||
|
|
||||||
|
paste(
|
||||||
|
copyData: BlockCopyData,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): BlockSvg | null {
|
||||||
|
if (!workspace.isCapacityAvailable(copyData.typeCounts!)) return null;
|
||||||
|
|
||||||
|
if (coordinate) {
|
||||||
|
copyData.blockState['x'] = coordinate.x;
|
||||||
|
copyData.blockState['y'] = coordinate.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventUtils.disable();
|
||||||
|
let block;
|
||||||
|
try {
|
||||||
|
block = append(copyData.blockState, workspace) as BlockSvg;
|
||||||
|
moveBlockToNotConflict(block);
|
||||||
|
} finally {
|
||||||
|
eventUtils.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!block) return block;
|
||||||
|
|
||||||
|
if (eventUtils.isEnabled() && !block.isShadow()) {
|
||||||
|
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
|
||||||
|
}
|
||||||
|
block.select();
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the given block to a location where it does not: (1) overlap exactly
|
||||||
|
* with any other blocks, or (2) look like it is connected to any other blocks.
|
||||||
|
*
|
||||||
|
* Exported for testing.
|
||||||
|
*
|
||||||
|
* @param block The block to move to an unambiguous location.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function moveBlockToNotConflict(block: BlockSvg) {
|
||||||
|
const workspace = block.workspace;
|
||||||
|
const snapRadius = config.snapRadius;
|
||||||
|
const coord = block.getRelativeToSurfaceXY();
|
||||||
|
const offset = new Coordinate(0, 0);
|
||||||
|
// getRelativeToSurfaceXY is really expensive, so we want to cache this.
|
||||||
|
const otherCoords = workspace
|
||||||
|
.getAllBlocks(false)
|
||||||
|
.filter((otherBlock) => otherBlock.id != block.id)
|
||||||
|
.map((b) => b.getRelativeToSurfaceXY());
|
||||||
|
|
||||||
|
while (
|
||||||
|
blockOverlapsOtherExactly(Coordinate.sum(coord, offset), otherCoords) ||
|
||||||
|
blockIsInSnapRadius(block, offset, snapRadius)
|
||||||
|
) {
|
||||||
|
if (workspace.RTL) {
|
||||||
|
offset.translate(-snapRadius, snapRadius * 2);
|
||||||
|
} else {
|
||||||
|
offset.translate(snapRadius, snapRadius * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block!.moveTo(Coordinate.sum(coord, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the given block coordinates are less than a delta of 1 from
|
||||||
|
* any of the other coordinates.
|
||||||
|
*/
|
||||||
|
function blockOverlapsOtherExactly(
|
||||||
|
coord: Coordinate,
|
||||||
|
otherCoords: Coordinate[],
|
||||||
|
): boolean {
|
||||||
|
return otherCoords.some(
|
||||||
|
(otherCoord) =>
|
||||||
|
Math.abs(otherCoord.x - coord.x) <= 1 &&
|
||||||
|
Math.abs(otherCoord.y - coord.y) <= 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the given block (when offset by the given amount) is close
|
||||||
|
* enough to any other connections (within the snap radius) that it looks
|
||||||
|
* like they could connect.
|
||||||
|
*/
|
||||||
|
function blockIsInSnapRadius(
|
||||||
|
block: BlockSvg,
|
||||||
|
offset: Coordinate,
|
||||||
|
snapRadius: number,
|
||||||
|
): boolean {
|
||||||
|
return block
|
||||||
|
.getConnections_(false)
|
||||||
|
.some((connection) => !!connection.closest(snapRadius, offset).connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockCopyData extends ICopyData {
|
||||||
|
blockState: State;
|
||||||
|
typeCounts: {[key: string]: number};
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(BlockPaster.TYPE, new BlockPaster());
|
||||||
31
core/clipboard/registry.ts
Normal file
31
core/clipboard/registry.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {ICopyable, ICopyData} from '../interfaces/i_copyable.js';
|
||||||
|
import type {IPaster} from '../interfaces/i_paster.js';
|
||||||
|
import * as registry from '../registry.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the given paster so that it cna be used for pasting.
|
||||||
|
*
|
||||||
|
* @param type The type of the paster to register, e.g. 'block', 'comment', etc.
|
||||||
|
* @param paster The paster to register.
|
||||||
|
*/
|
||||||
|
export function register<U extends ICopyData, T extends ICopyable<U>>(
|
||||||
|
type: string,
|
||||||
|
paster: IPaster<U, T>,
|
||||||
|
) {
|
||||||
|
registry.register(registry.Type.PASTER, type, paster);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the paster associated with the given type.
|
||||||
|
*
|
||||||
|
* @param type The type of the paster to unregister.
|
||||||
|
*/
|
||||||
|
export function unregister(type: string) {
|
||||||
|
registry.unregister(registry.Type.PASTER, type);
|
||||||
|
}
|
||||||
45
core/clipboard/workspace_comment_paster.ts
Normal file
45
core/clipboard/workspace_comment_paster.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {IPaster} from '../interfaces/i_paster.js';
|
||||||
|
import {ICopyData} from '../interfaces/i_copyable.js';
|
||||||
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||||
|
import {WorkspaceCommentSvg} from '../workspace_comment_svg.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
|
|
||||||
|
export class WorkspaceCommentPaster
|
||||||
|
implements IPaster<WorkspaceCommentCopyData, WorkspaceCommentSvg>
|
||||||
|
{
|
||||||
|
static TYPE = 'workspace-comment';
|
||||||
|
|
||||||
|
paste(
|
||||||
|
copyData: WorkspaceCommentCopyData,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): WorkspaceCommentSvg {
|
||||||
|
const state = copyData.commentState;
|
||||||
|
if (coordinate) {
|
||||||
|
state.setAttribute('x', `${coordinate.x}`);
|
||||||
|
state.setAttribute('y', `${coordinate.y}`);
|
||||||
|
} else {
|
||||||
|
const x = parseInt(state.getAttribute('x') ?? '0') + 50;
|
||||||
|
const y = parseInt(state.getAttribute('y') ?? '0') + 50;
|
||||||
|
state.setAttribute('x', `${x}`);
|
||||||
|
state.setAttribute('y', `${y}`);
|
||||||
|
}
|
||||||
|
return WorkspaceCommentSvg.fromXmlRendered(
|
||||||
|
copyData.commentState,
|
||||||
|
workspace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceCommentCopyData extends ICopyData {
|
||||||
|
commentState: Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(WorkspaceCommentPaster.TYPE, new WorkspaceCommentPaster());
|
||||||
@@ -9,9 +9,9 @@ goog.declareModuleId('Blockly.common');
|
|||||||
|
|
||||||
/* eslint-disable-next-line no-unused-vars */
|
/* eslint-disable-next-line no-unused-vars */
|
||||||
import type {Block} from './block.js';
|
import type {Block} from './block.js';
|
||||||
|
import {ISelectable} from './blockly.js';
|
||||||
import {BlockDefinition, Blocks} from './blocks.js';
|
import {BlockDefinition, Blocks} from './blocks.js';
|
||||||
import type {Connection} from './connection.js';
|
import type {Connection} from './connection.js';
|
||||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
|
||||||
@@ -88,12 +88,12 @@ export function setMainWorkspace(workspace: Workspace) {
|
|||||||
/**
|
/**
|
||||||
* Currently selected copyable object.
|
* Currently selected copyable object.
|
||||||
*/
|
*/
|
||||||
let selected: ICopyable | null = null;
|
let selected: ISelectable | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the currently selected copyable object.
|
* Returns the currently selected copyable object.
|
||||||
*/
|
*/
|
||||||
export function getSelected(): ICopyable | null {
|
export function getSelected(): ISelectable | null {
|
||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export function getSelected(): ICopyable | null {
|
|||||||
* @param newSelection The newly selected block.
|
* @param newSelection The newly selected block.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function setSelected(newSelection: ICopyable | null) {
|
export function setSelected(newSelection: ISelectable | null) {
|
||||||
selected = newSelection;
|
selected = newSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,9 @@ export function commentDuplicateOption(
|
|||||||
text: Msg['DUPLICATE_COMMENT'],
|
text: Msg['DUPLICATE_COMMENT'],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
callback: function () {
|
callback: function () {
|
||||||
clipboard.duplicate(comment);
|
const data = comment.toCopyData();
|
||||||
|
if (!data) return;
|
||||||
|
clipboard.paste(data, comment.workspace);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return duplicateOption;
|
return duplicateOption;
|
||||||
|
|||||||
@@ -331,9 +331,10 @@ export function registerDuplicate() {
|
|||||||
return 'hidden';
|
return 'hidden';
|
||||||
},
|
},
|
||||||
callback(scope: Scope) {
|
callback(scope: Scope) {
|
||||||
if (scope.block) {
|
if (!scope.block) return;
|
||||||
clipboard.duplicate(scope.block);
|
const data = scope.block.toCopyData();
|
||||||
}
|
if (!data) return;
|
||||||
|
clipboard.paste(data, scope.block.workspace);
|
||||||
},
|
},
|
||||||
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
|
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
|
||||||
id: 'blockDuplicate',
|
id: 'blockDuplicate',
|
||||||
|
|||||||
@@ -7,25 +7,26 @@
|
|||||||
import * as goog from '../../closure/goog/goog.js';
|
import * as goog from '../../closure/goog/goog.js';
|
||||||
goog.declareModuleId('Blockly.ICopyable');
|
goog.declareModuleId('Blockly.ICopyable');
|
||||||
|
|
||||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
|
||||||
import type {ISelectable} from './i_selectable.js';
|
import type {ISelectable} from './i_selectable.js';
|
||||||
|
|
||||||
export interface ICopyable extends ISelectable {
|
export interface ICopyable<T extends ICopyData> extends ISelectable {
|
||||||
/**
|
/**
|
||||||
* Encode for copying.
|
* Encode for copying.
|
||||||
*
|
*
|
||||||
* @returns Copy metadata.
|
* @returns Copy metadata.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
toCopyData(): CopyData | null;
|
toCopyData(): T | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace ICopyable {
|
export namespace ICopyable {
|
||||||
export interface CopyData {
|
export interface ICopyData {
|
||||||
saveInfo: Object | Element;
|
paster: string;
|
||||||
source: WorkspaceSvg;
|
|
||||||
typeCounts: {[key: string]: number} | null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CopyData = ICopyable.CopyData;
|
export type ICopyData = ICopyable.ICopyData;
|
||||||
|
|
||||||
|
/** @returns true if the given object is copyable. */
|
||||||
|
export function isCopyable(obj: any): obj is ICopyable<ICopyData> {
|
||||||
|
return obj.toCopyData !== undefined;
|
||||||
|
}
|
||||||
|
|||||||
25
core/interfaces/i_paster.ts
Normal file
25
core/interfaces/i_paster.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||||
|
import {ICopyable, ICopyData} from './i_copyable.js';
|
||||||
|
|
||||||
|
/** An object that can paste data into a workspace. */
|
||||||
|
export interface IPaster<U extends ICopyData, T extends ICopyable<U>> {
|
||||||
|
paste(
|
||||||
|
copyData: U,
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
coordinate?: Coordinate,
|
||||||
|
): T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns True if the given object is a paster. */
|
||||||
|
export function isPaster(
|
||||||
|
obj: any,
|
||||||
|
): obj is IPaster<ICopyData, ICopyable<ICopyData>> {
|
||||||
|
return obj.paste !== undefined;
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ import type {Options} from './options.js';
|
|||||||
import type {Renderer} from './renderers/common/renderer.js';
|
import type {Renderer} from './renderers/common/renderer.js';
|
||||||
import type {Theme} from './theme.js';
|
import type {Theme} from './theme.js';
|
||||||
import type {ToolboxItem} from './toolbox/toolbox_item.js';
|
import type {ToolboxItem} from './toolbox/toolbox_item.js';
|
||||||
|
import {IPaster} from './interfaces/i_paster.js';
|
||||||
|
import {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of maps. With the keys being the type and name of the class we are
|
* A map of maps. With the keys being the type and name of the class we are
|
||||||
@@ -96,6 +98,9 @@ export class Type<_T> {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
static ICON = new Type<IIcon>('icon');
|
static ICON = new Type<IIcon>('icon');
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -396,7 +396,9 @@ export function appendInternal(
|
|||||||
const block = appendPrivate(state, workspace, {parentConnection, isShadow});
|
const block = appendPrivate(state, workspace, {parentConnection, isShadow});
|
||||||
|
|
||||||
eventUtils.enable();
|
eventUtils.enable();
|
||||||
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
|
if (eventUtils.isEnabled()) {
|
||||||
|
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
|
||||||
|
}
|
||||||
eventUtils.setGroup(existingGroup);
|
eventUtils.setGroup(existingGroup);
|
||||||
eventUtils.setRecordUndo(prevRecordUndo);
|
eventUtils.setRecordUndo(prevRecordUndo);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {BlockSvg} from './block_svg.js';
|
|||||||
import * as clipboard from './clipboard.js';
|
import * as clipboard from './clipboard.js';
|
||||||
import * as common from './common.js';
|
import * as common from './common.js';
|
||||||
import {Gesture} from './gesture.js';
|
import {Gesture} from './gesture.js';
|
||||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
import {ICopyData, isCopyable} from './interfaces/i_copyable.js';
|
||||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||||
import {KeyCodes} from './utils/keycodes.js';
|
import {KeyCodes} from './utils/keycodes.js';
|
||||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||||
@@ -81,6 +81,9 @@ export function registerDelete() {
|
|||||||
ShortcutRegistry.registry.register(deleteShortcut);
|
ShortcutRegistry.registry.register(deleteShortcut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let copyData: ICopyData | null = null;
|
||||||
|
let copyWorkspace: WorkspaceSvg | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c.
|
* Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c.
|
||||||
*/
|
*/
|
||||||
@@ -104,18 +107,20 @@ export function registerCopy() {
|
|||||||
!Gesture.inProgress() &&
|
!Gesture.inProgress() &&
|
||||||
selected != null &&
|
selected != null &&
|
||||||
selected.isDeletable() &&
|
selected.isDeletable() &&
|
||||||
selected.isMovable()
|
selected.isMovable() &&
|
||||||
|
isCopyable(selected)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
callback(workspace, e) {
|
callback(workspace, e) {
|
||||||
// Prevent the default copy behavior, which may beep or otherwise indicate
|
// Prevent the default copy behavior, which may beep or otherwise indicate
|
||||||
// an error due to the lack of a selection.
|
// an error due to the lack of a selection.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// AnyDuringMigration because: Property 'hideChaff' does not exist on
|
workspace.hideChaff();
|
||||||
// type 'Workspace'.
|
const selected = common.getSelected();
|
||||||
(workspace as AnyDuringMigration).hideChaff();
|
if (!selected || !isCopyable(selected)) return false;
|
||||||
clipboard.copy(common.getSelected() as ICopyable);
|
copyData = selected.toCopyData();
|
||||||
return true;
|
copyWorkspace = workspace;
|
||||||
|
return !!copyData;
|
||||||
},
|
},
|
||||||
keyCodes: [ctrlC, altC, metaC],
|
keyCodes: [ctrlC, altC, metaC],
|
||||||
};
|
};
|
||||||
@@ -150,13 +155,11 @@ export function registerCut() {
|
|||||||
!selected.workspace!.isFlyout
|
!selected.workspace!.isFlyout
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
callback() {
|
callback(workspace) {
|
||||||
const selected = common.getSelected();
|
const selected = common.getSelected();
|
||||||
if (!selected) {
|
if (!selected || !isCopyable(selected)) return false;
|
||||||
// Shouldn't happen but appeases the type system
|
copyData = selected.toCopyData();
|
||||||
return false;
|
copyWorkspace = workspace;
|
||||||
}
|
|
||||||
clipboard.copy(selected);
|
|
||||||
(selected as BlockSvg).checkAndDelete();
|
(selected as BlockSvg).checkAndDelete();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -186,7 +189,8 @@ export function registerPaste() {
|
|||||||
return !workspace.options.readOnly && !Gesture.inProgress();
|
return !workspace.options.readOnly && !Gesture.inProgress();
|
||||||
},
|
},
|
||||||
callback() {
|
callback() {
|
||||||
return !!clipboard.paste();
|
if (!copyData || !copyWorkspace) return false;
|
||||||
|
return !!clipboard.paste(copyData, copyWorkspace);
|
||||||
},
|
},
|
||||||
keyCodes: [ctrlV, altV, metaV],
|
keyCodes: [ctrlV, altV, metaV],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ goog.declareModuleId('Blockly.ShortcutRegistry');
|
|||||||
|
|
||||||
import {KeyCodes} from './utils/keycodes.js';
|
import {KeyCodes} from './utils/keycodes.js';
|
||||||
import * as object from './utils/object.js';
|
import * as object from './utils/object.js';
|
||||||
import type {Workspace} from './workspace.js';
|
import {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for the registry of keyboard shortcuts. This is intended to be a
|
* Class for the registry of keyboard shortcuts. This is intended to be a
|
||||||
@@ -224,7 +224,7 @@ export class ShortcutRegistry {
|
|||||||
* @param e The key down event.
|
* @param e The key down event.
|
||||||
* @returns True if the event was handled, false otherwise.
|
* @returns True if the event was handled, false otherwise.
|
||||||
*/
|
*/
|
||||||
onKeyDown(workspace: Workspace, e: KeyboardEvent): boolean {
|
onKeyDown(workspace: WorkspaceSvg, e: KeyboardEvent): boolean {
|
||||||
const key = this.serializeKeyEvent_(e);
|
const key = this.serializeKeyEvent_(e);
|
||||||
const shortcutNames = this.getShortcutNamesByKeyCode(key);
|
const shortcutNames = this.getShortcutNamesByKeyCode(key);
|
||||||
if (!shortcutNames) {
|
if (!shortcutNames) {
|
||||||
@@ -346,9 +346,9 @@ export class ShortcutRegistry {
|
|||||||
|
|
||||||
export namespace ShortcutRegistry {
|
export namespace ShortcutRegistry {
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
callback?: (p1: Workspace, p2: Event, p3: KeyboardShortcut) => boolean;
|
callback?: (p1: WorkspaceSvg, p2: Event, p3: KeyboardShortcut) => boolean;
|
||||||
name: string;
|
name: string;
|
||||||
preconditionFn?: (p1: Workspace) => boolean;
|
preconditionFn?: (p1: WorkspaceSvg) => boolean;
|
||||||
metadata?: object;
|
metadata?: object;
|
||||||
keyCodes?: (number | string)[];
|
keyCodes?: (number | string)[];
|
||||||
allowCollision?: boolean;
|
allowCollision?: boolean;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type {CommentMove} from './events/events_comment_move.js';
|
|||||||
import * as eventUtils from './events/utils.js';
|
import * as eventUtils from './events/utils.js';
|
||||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
import type {IBubble} from './interfaces/i_bubble.js';
|
import type {IBubble} from './interfaces/i_bubble.js';
|
||||||
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyable} from './interfaces/i_copyable.js';
|
||||||
import * as Touch from './touch.js';
|
import * as Touch from './touch.js';
|
||||||
import {Coordinate} from './utils/coordinate.js';
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
import * as dom from './utils/dom.js';
|
import * as dom from './utils/dom.js';
|
||||||
@@ -32,6 +32,10 @@ import {Svg} from './utils/svg.js';
|
|||||||
import * as svgMath from './utils/svg_math.js';
|
import * as svgMath from './utils/svg_math.js';
|
||||||
import {WorkspaceComment} from './workspace_comment.js';
|
import {WorkspaceComment} from './workspace_comment.js';
|
||||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
import {
|
||||||
|
WorkspaceCommentCopyData,
|
||||||
|
WorkspaceCommentPaster,
|
||||||
|
} from './clipboard/workspace_comment_paster.js';
|
||||||
|
|
||||||
/** Size of the resize icon. */
|
/** Size of the resize icon. */
|
||||||
const RESIZE_SIZE = 8;
|
const RESIZE_SIZE = 8;
|
||||||
@@ -47,7 +51,7 @@ const TEXTAREA_OFFSET = 2;
|
|||||||
*/
|
*/
|
||||||
export class WorkspaceCommentSvg
|
export class WorkspaceCommentSvg
|
||||||
extends WorkspaceComment
|
extends WorkspaceComment
|
||||||
implements IBoundedElement, IBubble, ICopyable
|
implements IBoundedElement, IBubble, ICopyable<WorkspaceCommentCopyData>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The width and height to use to size a workspace comment when it is first
|
* The width and height to use to size a workspace comment when it is first
|
||||||
@@ -566,13 +570,11 @@ export class WorkspaceCommentSvg
|
|||||||
* Encode a comment for copying.
|
* Encode a comment for copying.
|
||||||
*
|
*
|
||||||
* @returns Copy metadata.
|
* @returns Copy metadata.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
toCopyData(): CopyData {
|
toCopyData(): WorkspaceCommentCopyData {
|
||||||
return {
|
return {
|
||||||
saveInfo: this.toXmlWithXY(),
|
paster: WorkspaceCommentPaster.TYPE,
|
||||||
source: this.workspace,
|
commentState: this.toXmlWithXY(),
|
||||||
typeCounts: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {Gesture} from './gesture.js';
|
|||||||
import {Grid} from './grid.js';
|
import {Grid} from './grid.js';
|
||||||
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
||||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||||
@@ -1294,7 +1294,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
|||||||
*/
|
*/
|
||||||
paste(
|
paste(
|
||||||
state: AnyDuringMigration | Element | DocumentFragment,
|
state: AnyDuringMigration | Element | DocumentFragment,
|
||||||
): ICopyable | null {
|
): ICopyable<ICopyData> | null {
|
||||||
if (!this.rendered || (!state['type'] && !state['tagName'])) {
|
if (!this.rendered || (!state['type'] && !state['tagName'])) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1382,6 +1382,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
|||||||
for (let i = 0, connection; (connection = connections[i]); i++) {
|
for (let i = 0, connection; (connection = connections[i]); i++) {
|
||||||
const neighbour = connection.closest(
|
const neighbour = connection.closest(
|
||||||
config.snapRadius,
|
config.snapRadius,
|
||||||
|
// TODO: This code doesn't work because it's passing an absolute
|
||||||
|
// coordinate instead of a relative coordinate. Need to
|
||||||
|
// figure out if I'm deprecating this function or if I
|
||||||
|
// need to fix this.
|
||||||
new Coordinate(blockX, blockY),
|
new Coordinate(blockX, blockY),
|
||||||
);
|
);
|
||||||
if (neighbour.connection) {
|
if (neighbour.connection) {
|
||||||
@@ -1435,6 +1439,9 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
|||||||
// with any blocks.
|
// with any blocks.
|
||||||
commentX += 50;
|
commentX += 50;
|
||||||
commentY += 50;
|
commentY += 50;
|
||||||
|
// TODO: This code doesn't work because it's using absolute coords
|
||||||
|
// where relative coords are expected. Need to figure out what I'm
|
||||||
|
// doing with this function and if I need to fix it.
|
||||||
comment.moveBy(commentX, commentY);
|
comment.moveBy(commentX, commentY);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
136
tests/mocha/clipboard_test.js
Normal file
136
tests/mocha/clipboard_test.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
goog.declareModuleId('Blockly.test.clipboard');
|
||||||
|
|
||||||
|
import {
|
||||||
|
sharedTestSetup,
|
||||||
|
sharedTestTeardown,
|
||||||
|
} from './test_helpers/setup_teardown.js';
|
||||||
|
import {
|
||||||
|
assertEventFired,
|
||||||
|
createChangeListenerSpy,
|
||||||
|
} from './test_helpers/events.js';
|
||||||
|
|
||||||
|
suite('Clipboard', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock;
|
||||||
|
this.workspace = Blockly.inject('blocklyDiv');
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
sharedTestTeardown.call(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a paster registered with a given type is called when pasting that type', function () {
|
||||||
|
const paster = {
|
||||||
|
paste: sinon.stub().returns(null),
|
||||||
|
};
|
||||||
|
Blockly.clipboard.registry.register('test-paster', paster);
|
||||||
|
|
||||||
|
Blockly.clipboard.paste({paster: 'test-paster'}, this.workspace);
|
||||||
|
chai.assert.isTrue(paster.paste.calledOnce);
|
||||||
|
|
||||||
|
Blockly.clipboard.registry.unregister('test-paster');
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('pasting blocks', function () {
|
||||||
|
test('pasting blocks fires a create event', function () {
|
||||||
|
const eventSpy = createChangeListenerSpy(this.workspace);
|
||||||
|
const block = Blockly.serialization.blocks.append(
|
||||||
|
{
|
||||||
|
'type': 'controls_if',
|
||||||
|
'id': 'blockId',
|
||||||
|
},
|
||||||
|
this.workspace,
|
||||||
|
);
|
||||||
|
const data = block.toCopyData();
|
||||||
|
this.clock.runAll();
|
||||||
|
eventSpy.resetHistory();
|
||||||
|
|
||||||
|
Blockly.clipboard.paste(data, this.workspace);
|
||||||
|
this.clock.runAll();
|
||||||
|
|
||||||
|
assertEventFired(
|
||||||
|
eventSpy,
|
||||||
|
Blockly.Events.BlockCreate,
|
||||||
|
{'recordUndo': true, 'type': Blockly.Events.BLOCK_CREATE},
|
||||||
|
this.workspace.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('pasted blocks are placed in unambiguous locations', function () {
|
||||||
|
test('pasted blocks are bumped to not overlap', function () {
|
||||||
|
const block = Blockly.serialization.blocks.append(
|
||||||
|
{
|
||||||
|
'type': 'controls_if',
|
||||||
|
'x': 38,
|
||||||
|
'y': 13,
|
||||||
|
},
|
||||||
|
this.workspace,
|
||||||
|
);
|
||||||
|
const data = block.toCopyData();
|
||||||
|
|
||||||
|
const newBlock = Blockly.clipboard.paste(data, this.workspace);
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
newBlock.getRelativeToSurfaceXY(),
|
||||||
|
new Blockly.utils.Coordinate(66, 69),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pasted blocks are bumped to be outside the connection snap radius', function () {
|
||||||
|
Blockly.serialization.workspaces.load(
|
||||||
|
{
|
||||||
|
'blocks': {
|
||||||
|
'languageVersion': 0,
|
||||||
|
'blocks': [
|
||||||
|
{
|
||||||
|
'type': 'controls_if',
|
||||||
|
'id': 'sourceBlockId',
|
||||||
|
'x': 38,
|
||||||
|
'y': 13,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'logic_compare',
|
||||||
|
'x': 113,
|
||||||
|
'y': 63,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.workspace,
|
||||||
|
);
|
||||||
|
this.clock.runAll(); // Update the connection DB.
|
||||||
|
const data = this.workspace.getBlockById('sourceBlockId').toCopyData();
|
||||||
|
|
||||||
|
const newBlock = Blockly.clipboard.paste(data, this.workspace);
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
newBlock.getRelativeToSurfaceXY(),
|
||||||
|
new Blockly.utils.Coordinate(94, 125),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('pasting comments', function () {
|
||||||
|
test('pasted comments are bumped to not overlap', function () {
|
||||||
|
Blockly.Xml.domToWorkspace(
|
||||||
|
Blockly.utils.xml.textToDom(
|
||||||
|
'<xml><comment id="test" x=10 y=10/></xml>',
|
||||||
|
),
|
||||||
|
this.workspace,
|
||||||
|
);
|
||||||
|
const comment = this.workspace.getTopComments(false)[0];
|
||||||
|
const data = comment.toCopyData();
|
||||||
|
|
||||||
|
const newComment = Blockly.clipboard.paste(data, this.workspace);
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
newComment.getRelativeToSurfaceXY(),
|
||||||
|
new Blockly.utils.Coordinate(60, 60),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -419,13 +419,13 @@ suite('Context Menu Items', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Calls duplicate', function () {
|
test('the block is duplicated', function () {
|
||||||
const spy = sinon.spy(Blockly.clipboard.TEST_ONLY, 'duplicateInternal');
|
|
||||||
|
|
||||||
this.duplicateOption.callback(this.scope);
|
this.duplicateOption.callback(this.scope);
|
||||||
|
chai.assert.equal(
|
||||||
sinon.assert.calledOnce(spy);
|
this.workspace.getTopBlocks(false).length,
|
||||||
sinon.assert.calledWith(spy, this.block);
|
2,
|
||||||
|
'Expected a second block',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Has correct label', function () {
|
test('Has correct label', function () {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
'Blockly.test.astNode',
|
'Blockly.test.astNode',
|
||||||
'Blockly.test.blockJson',
|
'Blockly.test.blockJson',
|
||||||
'Blockly.test.blocks',
|
'Blockly.test.blocks',
|
||||||
|
'Blockly.test.clipboard',
|
||||||
'Blockly.test.comments',
|
'Blockly.test.comments',
|
||||||
'Blockly.test.commentDeserialization',
|
'Blockly.test.commentDeserialization',
|
||||||
'Blockly.test.connectionChecker',
|
'Blockly.test.connectionChecker',
|
||||||
|
|||||||
@@ -26,10 +26,13 @@ suite('Key Down', function () {
|
|||||||
/**
|
/**
|
||||||
* Creates a block and sets it as Blockly.selected.
|
* Creates a block and sets it as Blockly.selected.
|
||||||
* @param {Blockly.Workspace} workspace The workspace to create a new block on.
|
* @param {Blockly.Workspace} workspace The workspace to create a new block on.
|
||||||
|
* @return {Blockly.Block} The block being selected.
|
||||||
*/
|
*/
|
||||||
function setSelectedBlock(workspace) {
|
function setSelectedBlock(workspace) {
|
||||||
defineStackBlock();
|
defineStackBlock();
|
||||||
Blockly.common.setSelected(workspace.newBlock('stack_block'));
|
const block = workspace.newBlock('stack_block');
|
||||||
|
Blockly.common.setSelected(block);
|
||||||
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,8 +112,8 @@ suite('Key Down', function () {
|
|||||||
|
|
||||||
suite('Copy', function () {
|
suite('Copy', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
setSelectedBlock(this.workspace);
|
this.block = setSelectedBlock(this.workspace);
|
||||||
this.copySpy = sinon.spy(Blockly.clipboard.TEST_ONLY, 'copyInternal');
|
this.copySpy = sinon.spy(this.block, 'toCopyData');
|
||||||
this.hideChaffSpy = sinon.spy(
|
this.hideChaffSpy = sinon.spy(
|
||||||
Blockly.WorkspaceSvg.prototype,
|
Blockly.WorkspaceSvg.prototype,
|
||||||
'hideChaff',
|
'hideChaff',
|
||||||
|
|||||||
@@ -149,13 +149,10 @@ export function assertEventFired(
|
|||||||
expectedWorkspaceId,
|
expectedWorkspaceId,
|
||||||
expectedBlockId,
|
expectedBlockId,
|
||||||
) {
|
) {
|
||||||
expectedProperties = Object.assign(
|
const baseProps = {};
|
||||||
{
|
if (expectedWorkspaceId) baseProps.workspaceId = expectedWorkspaceId;
|
||||||
workspaceId: expectedWorkspaceId,
|
if (expectedBlockId) baseProps.blockId = expectedBlockId;
|
||||||
blockId: expectedBlockId,
|
expectedProperties = Object.assign(baseProps, expectedProperties);
|
||||||
},
|
|
||||||
expectedProperties,
|
|
||||||
);
|
|
||||||
const expectedEvent = sinon.match
|
const expectedEvent = sinon.match
|
||||||
.instanceOf(instanceType)
|
.instanceOf(instanceType)
|
||||||
.and(sinon.match(expectedProperties));
|
.and(sinon.match(expectedProperties));
|
||||||
|
|||||||
Reference in New Issue
Block a user