Skip to content
9 changes: 8 additions & 1 deletion packages/core/src/api/SelectionAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { injectable } from 'inversify';

import { SelectionManager } from '../components/SelectionManager.js';
import { createInlineToolName } from '@editorjs/model';
import { createInlineToolName, BlockNodeSerialized } from '@editorjs/model';
import { InlineToolFormatData } from '@editorjs/sdk';
import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk';

Expand Down Expand Up @@ -32,4 +32,11 @@
public applyInlineToolForCurrentSelection(toolName: string, data?: InlineToolFormatData): void {
this.#selectionManager.applyInlineToolForCurrentSelection(createInlineToolName(toolName), data);
}

/**
*
*/
public get selectedBlocks(): BlockNodeSerialized[] | null {

Check warning on line 39 in packages/core/src/api/SelectionAPI.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
return this.#selectionManager.selectedBlocks();

Check warning on line 40 in packages/core/src/api/SelectionAPI.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}
30 changes: 29 additions & 1 deletion packages/core/src/components/SelectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
createInlineToolData,
FormattingAction,
InlineFragment,
InlineToolName
InlineToolName, BlockNodeSerialized
} from '@editorjs/model';
import { CaretManagerCaretUpdatedEvent, Index, EditorJSModel, createInlineToolName } from '@editorjs/model';
import { EventType } from '@editorjs/model';
Expand Down Expand Up @@ -177,4 +177,32 @@
}
}
};

/**
*
*/
public selectedBlocks(): BlockNodeSerialized[] | null {

Check warning on line 184 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
const userCaret = this.#model.getCaret(this.#config.userId);

Check warning on line 185 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const index = userCaret?.index ?? null;

Check warning on line 186 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 186 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 186 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (index === null) {
return null;

Check warning on line 189 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 190 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 190 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (index.isBlockIndex) {
const { blockIndex } = index;

Check warning on line 193 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

return [this.#model.serialized.blocks[blockIndex!]];

Check warning on line 195 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 196 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 196 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (index.compositeSegments !== undefined) {
return index.compositeSegments.map((segment) => {

Check warning on line 199 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
const { blockIndex } = segment;

Check warning on line 200 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

return this.#model.serialized.blocks[blockIndex!];

Check warning on line 202 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
});

Check warning on line 203 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
Comment on lines +199 to +203
}

Check warning on line 204 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 204 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

return null;

Check warning on line 206 in packages/core/src/components/SelectionManager.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { BlocksManager } from './components/BlockManager.js';
import { BlockRenderer } from './components/BlockRenderer.js';
import { SelectionManager } from './components/SelectionManager.js';
import { TOKENS } from './tokens.js';
import { ClipboardPlugin } from './plugins/ClipboardPlugin.js';

/**
* If no holder is provided via config, the editor will be appended to the element with this id
*/
Expand Down Expand Up @@ -121,6 +123,7 @@ export default class Core {
this.use(LinkInlineTool);
this.use(ShortcutsPlugin);
this.use(DOMAdapters);
this.use(ClipboardPlugin);
}

/**
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/plugins/ClipboardPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { CopyUIEvent, EditorAPI, EditorjsPlugin, EditorjsPluginParams } from '@editorjs/sdk';
import { CopyUIEventName } from '@editorjs/sdk';
import { PluginType } from '@editorjs/sdk';

/**
* @todo update doc
*/
export class ClipboardPlugin implements EditorjsPlugin {
public static readonly type = PluginType.Plugin;

readonly #api: EditorAPI;

/**
* @param params @todo update doc
*/
constructor(params: EditorjsPluginParams) {
Comment on lines +5 to +16
const { api, eventBus } = params;

this.#api = api;

eventBus.addEventListener(`ui:${CopyUIEventName}`, (e: CopyUIEvent) => {
const { nativeEvent } = e.detail;

const selectedBlocks = this.#api.selection.selectedBlocks;

/**
* Don't override native event if there are no blocks selected
*/
if (selectedBlocks === null || selectedBlocks.length === 0) {
return;
}

nativeEvent.preventDefault();

const currentDOMSelection = window.getSelection();

if (!currentDOMSelection) {
return;
}

const selectionAsPlainText = currentDOMSelection?.toString() ?? '';
const selectionAsHTML = this.#parseDOMSelectionToHTML(currentDOMSelection);

nativeEvent.clipboardData?.setData('text/plain', selectionAsPlainText);
nativeEvent.clipboardData?.setData('text/html', selectionAsHTML);
nativeEvent.clipboardData?.setData('application/x-editor-js', JSON.stringify(selectedBlocks));
Comment on lines +33 to +46
Comment on lines +44 to +46
});
}

/**
* @todo update doc
*/
public destroy(): void {
// do nothing
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could unsubscribe from the copy event

}

/**
*
* @param selection

Check failure on line 59 in packages/core/src/plugins/ClipboardPlugin.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "selection" description
*/
#parseDOMSelectionToHTML(selection: Selection): string {
if (selection.rangeCount === 0) {
return '';
}

const container = document.createElement('div');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Template might be better instead of div


for (let i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i);

container.appendChild(range.cloneContents());
}

return container.innerHTML;
}
}
8 changes: 8 additions & 0 deletions packages/model/src/entities/Index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,12 @@
/* Stryker disable next-line ConditionalExpression, LogicalOperator -- compound data-index predicate; .isDataIndex specs cover field combinations */
return this.blockIndex !== undefined && this.tuneName === undefined && this.dataKey !== undefined && this.textRange === undefined;
}

/**
*
*/
public get isCompositeIndex(): boolean {

Check warning on line 286 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
/* Stryker disable next-line ConditionalExpression, LogicalOperator -- compound data-index predicate; .isDataIndex specs cover field combinations */
Comment on lines +284 to +287
return this.compositeSegments !== undefined && this.compositeSegments.length > 0 && this.blockIndex === undefined && this.tuneName === undefined && this.dataKey === undefined && this.textRange === undefined;

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 288 in packages/model/src/entities/Index/index.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
Comment on lines +286 to +288
}
}
7 changes: 6 additions & 1 deletion packages/sdk/src/api/SelectionAPI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InlineToolName } from '@editorjs/model';
import type { BlockNodeSerialized, InlineToolName } from '@editorjs/model';

/**
* Selection API interface
Expand All @@ -11,4 +11,9 @@ export interface SelectionAPI {
* @param data - optional data for the inline tool
*/
applyInlineToolForCurrentSelection(tool: InlineToolName, data?: Record<string, unknown>): void;

/**
*
Comment on lines +15 to +16
*/
get selectedBlocks(): BlockNodeSerialized[] | null;
}
29 changes: 29 additions & 0 deletions packages/sdk/src/entities/EventBus/events/ui/CopyUIEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { UIEventBase } from './UIEventBase.js';

/**
* Name of the copy UI event (dispatched as `ui:copy`)
*/
export const CopyUIEventName = 'copy';

/**
* Payload @todo update doc
*/
export interface CopyUIEventPayload {
/**
* Native ClipboardEvent
* UI does not call .preventDefault() for this event
*/
nativeEvent: ClipboardEvent;
}

/**
* Delegated copy event from the editor @todo update doc
*/
export class CopyUIEvent extends UIEventBase<CopyUIEventPayload> {
Comment on lines +19 to +22
/**
* @param payload - carries the original DOM `ClipboardEvent` as `nativeEvent` for providing rich clipboard data
Comment on lines +9 to +24
*/
constructor(payload: CopyUIEventPayload) {
super(CopyUIEventName, payload);
}
}
1 change: 1 addition & 0 deletions packages/sdk/src/entities/EventBus/events/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './UIEventBase.js';
export * from './BeforeInputUIEvent.js';
export * from './KeydownUIEvent.js';
export * from './CopyUIEvent.js';
19 changes: 14 additions & 5 deletions packages/ui/src/Blocks/Blocks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { BlockAddedCoreEvent,
BlockRemovedCoreEvent,
EditorjsPlugin,
EditorjsPluginParams } from '@editorjs/sdk';
import { CopyUIEvent } from '@editorjs/sdk';
import {
CoreEventType,
UiComponentType,
BeforeInputUIEvent
} from '@editorjs/sdk';
import type { EventBus } from '@editorjs/sdk';
import type { EventBus,
BlockAddedCoreEvent,
BlockRemovedCoreEvent, CopyUIEventPayload,
EditorjsPlugin,
EditorjsPluginParams } from '@editorjs/sdk';
import Style from './Blocks.module.pcss';
import { isNativeInput, make } from '@editorjs/dom';
import { BlocksHolderRenderedUIEvent, BlockSelectedUIEvent } from './events/index.js';
Expand Down Expand Up @@ -130,6 +131,14 @@ export class BlocksUI implements EditorjsPlugin {
e.preventDefault();
});

blocksHolder.addEventListener('copy', (e) => {
const payload: CopyUIEventPayload = {
nativeEvent: e,
};

this.#eventBus.dispatchEvent(new CopyUIEvent(payload));
});

return blocksHolder;
}

Expand Down
Loading