Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9dd6f8e
feat(eventbus): create CopyUIEvent entity
ilyamore88 Apr 17, 2026
b53c482
feat(ui-blocks): subscribe for copy event and pass native event objec…
ilyamore88 Apr 17, 2026
0be2ca4
feat(core): init clipboard plugin
ilyamore88 Apr 17, 2026
10c1c22
feat(core): use ClipboardPlugin
ilyamore88 Apr 17, 2026
ad5b441
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Apr 17, 2026
e1359c8
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Apr 23, 2026
500178d
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 May 24, 2026
bc3657c
feat: implement clipboard plugin
ilyamore88 May 24, 2026
00d10ac
fix(clipboard-plugin): prevent native event only after checking selec…
ilyamore88 Jun 9, 2026
d426ae4
chore: fix stryker exception description
ilyamore88 Jun 9, 2026
31a5758
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Jun 9, 2026
9f44277
feat(clipboard-plugin): update docs, add destroy method implementatio…
ilyamore88 Jul 7, 2026
f63d6dd
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Jul 7, 2026
55b62c8
chore: use currentSelection in selection manager
ilyamore88 Jul 7, 2026
2681e5b
feat(clipboard-plugin): describe clipboard object
ilyamore88 Jul 8, 2026
2b288dc
refactor(selection-api): return empty array instead of null
ilyamore88 Jul 8, 2026
f2a6067
chore: add docs
ilyamore88 Jul 8, 2026
264f721
chore: update docs
ilyamore88 Jul 8, 2026
f4601a6
fix: make removeEventListener js-private
ilyamore88 Jul 8, 2026
ec4aca5
chore(clipboard-plugin): add unit-tests
ilyamore88 Jul 9, 2026
c144d28
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Jul 15, 2026
b7b617f
fix(clipboard-plugin-tests): proper mock of esm module with mockModule
ilyamore88 Jul 18, 2026
40a9b2b
refactor(clipboard-plugin): create separate package for plugin
ilyamore88 Jul 19, 2026
9de3ad6
ci: add permissions to ci of clipboard plugin
ilyamore88 Jul 19, 2026
158abf6
fix(clipboard-plugin): don't prevent native event if clipboardData is…
ilyamore88 Jul 19, 2026
c163812
chore(index): remove unused isCompositeIndex from scope
ilyamore88 Jul 19, 2026
2cf7dfe
chore: add more unit-tests to improve mutation testing score
ilyamore88 Jul 19, 2026
a4365c3
chore: address review commits
ilyamore88 Jul 20, 2026
4dff6fa
Merge branch 'main' of github.com:editor-js/document-model into feat/…
ilyamore88 Jul 20, 2026
9d0f896
chore: add spec for clipboard plugin
ilyamore88 Jul 20, 2026
18963e9
chore: address review comments
ilyamore88 Jul 20, 2026
c322b46
test(clipboard-plugin): cover multi-line copy event
ilyamore88 Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 'reflect-metadata';
import { inject, injectable } from 'inversify';

import { SelectionManager } from '../components/SelectionManager.js';
import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType } from '@editorjs/model';
import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType, BlockNodeSerialized } from '@editorjs/model';
import { CoreConfigValidated } from '@editorjs/sdk';
import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk';
import { TOKENS } from '../tokens.js';
Expand Down Expand Up @@ -70,4 +70,11 @@ export class SelectionAPI implements SelectionApiInterface {
public getCaret(userId = this.#config.userId): Caret | undefined {
return this.#model.getCaret(userId);
}

/**
*
*/
public get selectedBlocks(): BlockNodeSerialized[] | null {
return this.#selectionManager.selectedBlocks();
}
}
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 @@ import {
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 @@ export class SelectionManager {
}
}
};

/**
*
*/
public selectedBlocks(): BlockNodeSerialized[] | null {
const userCaret = this.#model.getCaret(this.#config.userId);
const index = userCaret?.index ?? null;

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

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

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

if (index.compositeSegments !== undefined) {
return index.compositeSegments.map((segment) => {
const { blockIndex } = segment;

return this.#model.serialized.blocks[blockIndex!];
});
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
}

return null;
}
}
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { BlockRenderer } from './components/BlockRenderer.js';
import { SelectionManager } from './components/SelectionManager.js';
import { TOKENS } from './tokens.js';
import { UndoRedoManager } from './components/UndoRedoManager.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 @@ -107,6 +109,7 @@ export default class Core {
this.use(ShortcutsPlugin);
this.use(CollaborationManager);
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) {
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;
}

const currentDOMSelection = window.getSelection();

if (!currentDOMSelection) {
return;
}

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

nativeEvent.preventDefault();

nativeEvent.clipboardData?.setData('text/plain', selectionAsPlainText);
nativeEvent.clipboardData?.setData('text/html', selectionAsHTML);
nativeEvent.clipboardData?.setData('application/x-editor-js', JSON.stringify(selectedBlocks));
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
});
}

/**
* @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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved

}

/**
*
* @param selection

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

View workflow job for this annotation

GitHub Actions / package-check / 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved


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 @@ export class Index {
/* 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;
}

/**
* Returns true if index points to a composite index
*/
public get isCompositeIndex(): boolean {
/* Stryker disable next-line ConditionalExpression, LogicalOperator -- compound composite-index predicate; .isCompositeIndex specs cover field combinations */
return this.compositeSegments !== undefined && this.compositeSegments.length > 0 && this.blockIndex === undefined && this.tuneName === undefined && this.dataKey === undefined && this.textRange === undefined;
}
}
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 { Caret, CaretManagerEvents } from '@editorjs/model';
import type { Caret, CaretManagerEvents, BlockNodeSerialized } from '@editorjs/model';

/**
* Selection API interface
Expand Down Expand Up @@ -31,4 +31,9 @@ export interface SelectionAPI {
* @param userId - user id. If not provided, returns for current user
*/
getCaret(userId?: string | number): Caret | undefined;

/**
*
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
*/
get selectedBlocks(): BlockNodeSerialized[] | null;
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated

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.

Maybe [] instead of null

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved

}
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
*/
Comment thread
ilyamore88 marked this conversation as resolved.
export class CopyUIEvent extends UIEventBase<CopyUIEventPayload> {
Comment thread
ilyamore88 marked this conversation as resolved.
/**
* @param payload - carries the original DOM `ClipboardEvent` as `nativeEvent` for providing rich clipboard data
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
*/
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';
16 changes: 14 additions & 2 deletions packages/ui/src/Blocks/Blocks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {
BlockAddedCoreEvent,
BlockRemovedCoreEvent, EditorAPI,
BlockRemovedCoreEvent, CopyUIEvent, EditorAPI,
EditorjsPlugin,
EditorjsPluginParams
Comment thread
ilyamore88 marked this conversation as resolved.
Outdated
} from '@editorjs/sdk';
Expand All @@ -9,7 +9,11 @@ import {
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 @@ -138,6 +142,14 @@ export class BlocksUI implements EditorjsPlugin {
e.preventDefault();
});

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

this.#eventBus.dispatchEvent(new CopyUIEvent(payload));
});
Comment on lines +140 to +146

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Tracked in #170


return blocksHolder;
}

Expand Down
Loading