Skip to content

Commit

Permalink
feat: make ICopyable generic and update clipboard APIs (#7348)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
BeksOmega authored Aug 3, 2023
1 parent ce1678e commit 001d9ff
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 63 deletions.
6 changes: 5 additions & 1 deletion core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -592,7 +592,7 @@ export {IComponent};
export {IConnectionChecker};
export {IContextMenu};
export {icons};
export {ICopyable};
export {ICopyable, isCopyable};
export {IDeletable};
export {IDeleteArea};
export {IDragTarget};
Expand Down
142 changes: 101 additions & 41 deletions core/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
// 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!;
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);
}
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 = {
Expand Down
2 changes: 1 addition & 1 deletion core/clipboard/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
) {
Expand Down
8 changes: 4 additions & 4 deletions core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
9 changes: 7 additions & 2 deletions core/interfaces/i_copyable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
6 changes: 4 additions & 2 deletions core/interfaces/i_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion core/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

/**
Expand Down
11 changes: 5 additions & 6 deletions core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion core/workspace_comment_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit 001d9ff

Please sign in to comment.