Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 on lines +199 to +203
}

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) {
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;
}

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 on lines +44 to +46
Comment on lines +42 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 / 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


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;
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 { 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 on lines +35 to +36
*/
get selectedBlocks(): BlockNodeSerialized[] | null;
Comment on lines +35 to +38
}
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 on lines +8 to +21
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';
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 on lines 1 to 5
} 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));
});

return blocksHolder;
}

Expand Down
Loading