mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
feat: make ICopyable generic and update clipboard APIs (#7348)
* chore: rename module-local variables to not conflict * feat: make ICopyable generic and update clipboard APIs * chore: switch over more things to use generic ICopyables * chore: fix shortcut items using copy paste * chore: add test for interface between clipboard and pasters * chore: export isCopyable * chore: format * chore: fixup PR comments * chore: add deprecation tags
This commit is contained in:
@@ -70,7 +70,11 @@ import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
|
||||
*/
|
||||
export class BlockSvg
|
||||
extends Block
|
||||
implements IASTNodeLocationSvg, IBoundedElement, ICopyable, IDraggable
|
||||
implements
|
||||
IASTNodeLocationSvg,
|
||||
IBoundedElement,
|
||||
ICopyable<BlockCopyData>,
|
||||
IDraggable
|
||||
{
|
||||
/**
|
||||
* Constant for identifying rows that are to be rendered inline.
|
||||
|
||||
@@ -140,7 +140,7 @@ import {ICollapsibleToolboxItem} from './interfaces/i_collapsible_toolbox_item.j
|
||||
import {IComponent} from './interfaces/i_component.js';
|
||||
import {IConnectionChecker} from './interfaces/i_connection_checker.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 {IDeleteArea} from './interfaces/i_delete_area.js';
|
||||
import {IDragTarget} from './interfaces/i_drag_target.js';
|
||||
@@ -592,7 +592,7 @@ export {IComponent};
|
||||
export {IConnectionChecker};
|
||||
export {IContextMenu};
|
||||
export {icons};
|
||||
export {ICopyable};
|
||||
export {ICopyable, isCopyable};
|
||||
export {IDeletable};
|
||||
export {IDeleteArea};
|
||||
export {IDragTarget};
|
||||
|
||||
@@ -12,79 +12,139 @@ 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. */
|
||||
let copyData: ICopyData | null = null;
|
||||
let stashedCopyData: ICopyData | null = null;
|
||||
|
||||
let source: WorkspaceSvg | 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
|
||||
*/
|
||||
export function copy(toCopy: ICopyable) {
|
||||
TEST_ONLY.copyInternal(toCopy);
|
||||
export function copy<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
|
||||
deprecation.warn(
|
||||
'Blockly.clipboard.copy',
|
||||
'v11',
|
||||
'v12',
|
||||
'myCopyable.toCopyData()',
|
||||
);
|
||||
return TEST_ONLY.copyInternal(toCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of copy for stubbing in tests.
|
||||
*/
|
||||
function copyInternal(toCopy: ICopyable) {
|
||||
copyData = toCopy.toCopyData();
|
||||
source = (toCopy as any).workspace ?? null;
|
||||
function copyInternal<T extends ICopyData>(toCopy: ICopyable<T>): T | null {
|
||||
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.
|
||||
* @internal
|
||||
*/
|
||||
export function paste(): ICopyable | null {
|
||||
if (!copyData) {
|
||||
return null;
|
||||
export function paste<T extends ICopyData>(
|
||||
copyData: T,
|
||||
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
|
||||
// started in a flyout workspace.
|
||||
let workspace = source;
|
||||
if (workspace?.isFlyout) {
|
||||
workspace = workspace.targetWorkspace!;
|
||||
}
|
||||
if (!workspace) return null;
|
||||
return (
|
||||
globalRegistry
|
||||
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
|
||||
?.paste(copyData, workspace) ?? null
|
||||
);
|
||||
return pasteFromData(copyData, workspace, coordinate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns The block or workspace comment that was duplicated, or null if the
|
||||
* duplication failed.
|
||||
* @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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private version of duplicate for stubbing in tests.
|
||||
*/
|
||||
function duplicateInternal(toDuplicate: ICopyable): ICopyable | null {
|
||||
const oldCopyData = copyData;
|
||||
copy(toDuplicate);
|
||||
if (!copyData || !source) return null;
|
||||
const pastedThing =
|
||||
globalRegistry
|
||||
.getObject(globalRegistry.Type.PASTER, copyData.paster, false)
|
||||
?.paste(copyData, source) ?? null;
|
||||
copyData = oldCopyData;
|
||||
return pastedThing;
|
||||
function duplicateInternal<
|
||||
U extends ICopyData,
|
||||
T extends ICopyable<U> & IHasWorkspace,
|
||||
>(toDuplicate: T): T | null {
|
||||
const data = toDuplicate.toCopyData();
|
||||
if (!data) return null;
|
||||
return paste(data, toDuplicate.workspace) as T;
|
||||
}
|
||||
|
||||
interface IHasWorkspace {
|
||||
workspace: WorkspaceSvg;
|
||||
}
|
||||
|
||||
export const TEST_ONLY = {
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as registry from '../registry.js';
|
||||
* @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>(
|
||||
export function register<U extends ICopyData, T extends ICopyable<U>>(
|
||||
type: string,
|
||||
paster: IPaster<U, T>,
|
||||
) {
|
||||
|
||||
@@ -9,9 +9,9 @@ goog.declareModuleId('Blockly.common');
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
import type {Block} from './block.js';
|
||||
import {ISelectable} from './blockly.js';
|
||||
import {BlockDefinition, Blocks} from './blocks.js';
|
||||
import type {Connection} from './connection.js';
|
||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
@@ -88,12 +88,12 @@ export function setMainWorkspace(workspace: Workspace) {
|
||||
/**
|
||||
* Currently selected copyable object.
|
||||
*/
|
||||
let selected: ICopyable | null = null;
|
||||
let selected: ISelectable | null = null;
|
||||
|
||||
/**
|
||||
* Returns the currently selected copyable object.
|
||||
*/
|
||||
export function getSelected(): ICopyable | null {
|
||||
export function getSelected(): ISelectable | null {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function getSelected(): ICopyable | null {
|
||||
* @param newSelection The newly selected block.
|
||||
* @internal
|
||||
*/
|
||||
export function setSelected(newSelection: ICopyable | null) {
|
||||
export function setSelected(newSelection: ISelectable | null) {
|
||||
selected = newSelection;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ goog.declareModuleId('Blockly.ICopyable');
|
||||
|
||||
import type {ISelectable} from './i_selectable.js';
|
||||
|
||||
export interface ICopyable extends ISelectable {
|
||||
export interface ICopyable<T extends ICopyData> extends ISelectable {
|
||||
/**
|
||||
* Encode for copying.
|
||||
*
|
||||
* @returns Copy metadata.
|
||||
*/
|
||||
toCopyData(): ICopyData | null;
|
||||
toCopyData(): T | null;
|
||||
}
|
||||
|
||||
export namespace ICopyable {
|
||||
@@ -25,3 +25,8 @@ export namespace ICopyable {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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> {
|
||||
export interface IPaster<U extends ICopyData, T extends ICopyable<U>> {
|
||||
paste(
|
||||
copyData: U,
|
||||
workspace: WorkspaceSvg,
|
||||
@@ -18,6 +18,8 @@ export interface IPaster<U extends ICopyData, T extends ICopyable> {
|
||||
}
|
||||
|
||||
/** @returns True if the given object is a paster. */
|
||||
export function isPaster(obj: any): obj is IPaster<ICopyData, ICopyable> {
|
||||
export function isPaster(
|
||||
obj: any,
|
||||
): obj is IPaster<ICopyData, ICopyable<ICopyData>> {
|
||||
return obj.paste !== undefined;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class Type<_T> {
|
||||
static ICON = new Type<IIcon>('icon');
|
||||
|
||||
/** @internal */
|
||||
static PASTER = new Type<IPaster<ICopyData, ICopyable>>('paster');
|
||||
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import * as common from './common.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import type {ICopyable} from './interfaces/i_copyable.js';
|
||||
import {isCopyable} from './interfaces/i_copyable.js';
|
||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
@@ -114,7 +114,9 @@ export function registerCopy() {
|
||||
// AnyDuringMigration because: Property 'hideChaff' does not exist on
|
||||
// type 'Workspace'.
|
||||
(workspace as AnyDuringMigration).hideChaff();
|
||||
clipboard.copy(common.getSelected() as ICopyable);
|
||||
const selected = common.getSelected();
|
||||
if (!selected || !isCopyable(selected)) return false;
|
||||
clipboard.copy(selected);
|
||||
return true;
|
||||
},
|
||||
keyCodes: [ctrlC, altC, metaC],
|
||||
@@ -152,10 +154,7 @@ export function registerCut() {
|
||||
},
|
||||
callback() {
|
||||
const selected = common.getSelected();
|
||||
if (!selected) {
|
||||
// Shouldn't happen but appeases the type system
|
||||
return false;
|
||||
}
|
||||
if (!selected || !isCopyable(selected)) return false;
|
||||
clipboard.copy(selected);
|
||||
(selected as BlockSvg).checkAndDelete();
|
||||
return true;
|
||||
|
||||
@@ -51,7 +51,7 @@ const TEXTAREA_OFFSET = 2;
|
||||
*/
|
||||
export class WorkspaceCommentSvg
|
||||
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
|
||||
|
||||
@@ -36,7 +36,7 @@ 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 {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 {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
@@ -1300,7 +1300,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
|
||||
*/
|
||||
paste(
|
||||
state: AnyDuringMigration | Element | DocumentFragment,
|
||||
): ICopyable | null {
|
||||
): ICopyable<ICopyData> | null {
|
||||
if (!this.rendered || (!state['type'] && !state['tagName'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
35
tests/mocha/clipboard_test.js
Normal file
35
tests/mocha/clipboard_test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
goog.declareModuleId('Blockly.test.clipboard');
|
||||
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('Clipboard', function () {
|
||||
setup(function () {
|
||||
this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock;
|
||||
this.workspace = new Blockly.WorkspaceSvg(new Blockly.Options({}));
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,7 @@
|
||||
'Blockly.test.astNode',
|
||||
'Blockly.test.blockJson',
|
||||
'Blockly.test.blocks',
|
||||
'Blockly.test.clipboard',
|
||||
'Blockly.test.comments',
|
||||
'Blockly.test.commentDeserialization',
|
||||
'Blockly.test.connectionChecker',
|
||||
|
||||
Reference in New Issue
Block a user