Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make ICopyable generic and update clipboard APIs #7348

Merged
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};
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
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>(
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would rather not add the deprecation warning until you've fixed the internal usages, but since this is going into a feature branch and not directly into the release i won't stop you

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a draft PR fixing all internal usages :D #7352

'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 {
BeksOmega marked this conversation as resolved.
Show resolved Hide resolved
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