@lexical/clipboard
Version:
This package provides the copy/paste functionality for Lexical.
237 lines (236 loc) • 9.5 kB
TypeScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { BaseSelection } from 'lexical';
import { LexicalClipboardData } from './clipboard';
/**
* A middleware function in a per-MIME-type clipboard-import stack. Mirrors
* the shape of {@link ExportMimeTypeFunction} on the export side.
*
* - `data` is the non-empty string returned by `DataTransfer.getData(mime)`
* for this MIME type.
* - `selection` is the current editor selection at the insertion point.
* - `$next` defers to the next-lower handler in the stack (i.e. the handler
* that was registered earlier). Returns `true` if that handler claimed
* the data; `false` if no handler accepted it.
* - `dataTransfer` is the full {@link DataTransfer} the paste/drop came
* from, so a handler can inspect companion MIME types or attached
* files in addition to the slot it was invoked for (e.g. peek at
* `'application/x-vscode-source'` while handling `'text/html'`). When
* threading through the new pipeline, pass this into
* `$generateNodesFromDOMViaExtension(dom, {
* context: [contextValue(ImportSourceDataTransfer, dataTransfer)],
* })` so rules and preprocessors can read it via
* `ctx.get(ImportSourceDataTransfer)`.
*
* The function should return `true` if it consumed the data (the caller
* stops trying further handlers for this MIME type and does not move on to
* the next MIME type). Return `$next()` to delegate. Return `false` if the
* function decided not to handle the data after inspecting it (e.g. the
* JSON namespace didn't match) so a lower-priority handler — or the next
* MIME type — gets a chance.
*
* @experimental
*/
export type ImportMimeTypeFunction = (data: string, selection: BaseSelection, $next: () => boolean, dataTransfer: DataTransfer) => boolean;
/**
* A mapping from MIME type to a stack of {@link ImportMimeTypeFunction}.
*
* Each entry is an ordered array; the function at the highest index runs
* first and may call `next()` to fall through to the function below it.
* The default config provides one handler each for
* `'application/x-lexical-editor'`, `'text/html'`, and `'text/plain'` that
* matches the legacy {@link $insertDataTransferForRichText} behavior.
*
* When {@link ClipboardImportExtension} merges a partial config, new
* functions are appended to the existing array for each MIME type, so
* later-registered handlers run before earlier ones (including the
* defaults) and may delegate to them via `next()`.
*
* @experimental
*/
export type ImportMimeTypeConfig = {
[key in keyof LexicalClipboardData | (string & {})]?: ImportMimeTypeFunction[] | undefined;
};
/**
* Per-MIME-type ordering weights. Lower numbers run first.
*
* Composable across extensions: each extension contributes weights for
* its MIME types without needing to coordinate. A partial config that
* sets `{'application/vnd.myapp+json': 5}` slots its type between the
* built-in `application/x-lexical-editor` (0) and `text/html` (10) — no
* need to enumerate the full ordering. mergeConfig spreads pairs (later
* keys override earlier ones for the same MIME type, so an extension
* can also re-rank a built-in by repeating its key with a new weight).
*
* Iteration: every MIME type that has a handler stack and is present in
* the dataTransfer (regardless of whether it has an explicit weight) is
* tried; MIME types with no explicit weight sort to the end, behind all
* weighted ones, in lexical order.
*
* @experimental
*/
export type ImportMimeTypePriority = {
readonly [key in keyof LexicalClipboardData | (string & {})]?: number | undefined;
};
/**
* Configuration for {@link ClipboardImportExtension}.
*
* @experimental
*/
export interface ClipboardImportConfig {
/**
* The per-MIME-type deserializer stacks used by
* {@link $insertDataTransferForRichText} when handling a paste or drop
* event.
*
* Merged with `[...prev, ...override]` per MIME type, matching the
* behavior of {@link GetClipboardDataExtension.$exportMimeType}.
*
* Apps add a stack under a brand-new key to register a brand-new MIME
* type. Set a {@link priority} weight to control where in the
* iteration it sits relative to the built-ins.
*/
$importMimeType: ImportMimeTypeConfig;
/**
* See {@link ImportMimeTypePriority}. Spread-merged across configs —
* extensions contribute weights without coordinating with each other.
*/
priority: ImportMimeTypePriority;
}
/**
* Default per-MIME-type weights reproducing the legacy
* `$insertDataTransferForRichText` ordering:
*
* `application/x-lexical-editor` (0) → `text/html` (10) →
* `text/plain` (20) → `text/uri-list` (30).
*
* Gaps between weights let third-party MIME types slot in (e.g. weight
* 5 to run between lexical and html). Apps can also override built-in
* weights to demote them.
*
* @experimental
*/
export declare const DEFAULT_IMPORT_MIME_TYPE_PRIORITY: ImportMimeTypePriority;
/**
* The default per-MIME-type handler stacks reproducing the legacy
* {@link $insertDataTransferForRichText} behavior exactly. Stacked
* extensions append on top of these.
*
* @experimental
*/
export declare const DEFAULT_IMPORT_MIME_TYPE: ImportMimeTypeConfig;
/**
* Output of {@link ClipboardImportExtension}: the merged configuration
* plus a self-contained {@link $insertDataTransfer} function that owns
* the entire paste-side iteration over the priority list. Apps look this
* up via peer-dependency and call it directly; {@link
* $insertDataTransferForRichText} delegates to it.
*
* @experimental
*/
export interface ClipboardImportOutput extends ClipboardImportConfig {
/**
* Try every MIME type in `priority` order against the `DataTransfer`,
* invoking the configured stack for the first one that has a non-empty
* payload. Returns `true` if any stack claimed the data.
*/
$insertDataTransfer(dataTransfer: DataTransfer, selection: BaseSelection): boolean;
}
/**
* @internal
*
* Look up the {@link ClipboardImportOutput} on the active editor. Returns
* a static default-backed output when no {@link ClipboardImportExtension}
* is configured, so callers can always invoke `output.$insertDataTransfer`
* regardless of whether the editor opted in.
*/
export declare function $getImportOutput(): ClipboardImportOutput;
/**
* @experimental
*
* Mirror of {@link GetClipboardDataExtension} for the import direction.
* Holds a per-MIME-type stack of {@link ImportMimeTypeFunction}s.
*
* @example
* Route `text/html` pastes through {@link DOMImportExtension}, leaving the
* defaults for other MIME types untouched:
* ```ts
* import {configExtension, defineExtension, $getEditor} from 'lexical';
* import {
* ClipboardImportExtension,
* $insertGeneratedNodes,
* } from '@lexical/clipboard';
* import {
* contextValue,
* DOMImportExtension,
* ImportSource,
* ImportSourceDataTransfer,
* $generateNodesFromDOMViaExtension,
* } from '@lexical/html';
*
* defineExtension({
* name: 'app',
* dependencies: [
* DOMImportExtension,
* configExtension(ClipboardImportExtension, {
* $importMimeType: {
* 'text/html': [
* (html, selection, _$next, dataTransfer) => {
* const parser = new DOMParser();
* const dom = parser.parseFromString(html, 'text/html');
* const nodes = $generateNodesFromDOMViaExtension(dom, {
* context: [
* contextValue(ImportSource, 'paste'),
* contextValue(ImportSourceDataTransfer, dataTransfer),
* ],
* });
* $insertGeneratedNodes($getEditor(), nodes, selection);
* return true;
* },
* ],
* },
* }),
* ],
* });
* ```
*/
export declare const ClipboardImportExtension: import("lexical").LexicalExtension<ClipboardImportConfig, "@lexical/clipboard/Import", ClipboardImportOutput, unknown>;
/**
* @experimental
*
* Drop-in extension that routes `text/html` clipboard pastes and drops
* through the {@link DOMImportExtension} pipeline (rules, schemas,
* preprocessors, overlays) instead of the legacy
* {@link $generateNodesFromDOM}. Add to your extension dependencies along
* with the per-package import extensions you want active
* ({@link CoreImportExtension}, {@link RichTextImportExtension}, etc.).
*
* The original {@link DataTransfer} and `'paste'` source kind are forwarded
* into the import context so rules and preprocessors can read them via
* `ctx.get(ImportSourceDataTransfer)` / `ctx.get(ImportSource)`.
*
* Equivalent to stacking this `text/html` handler manually via
* `configExtension(ClipboardImportExtension, {...})`.
*
* @example
* ```ts
* import {defineExtension} from 'lexical';
* import {ClipboardDOMImportExtension} from '@lexical/clipboard';
* import {CoreImportExtension, RichTextImportExtension} from '@lexical/html';
*
* defineExtension({
* name: 'app',
* dependencies: [
* CoreImportExtension,
* RichTextImportExtension,
* ClipboardDOMImportExtension,
* ],
* });
* ```
*/
export declare const ClipboardDOMImportExtension: import("lexical").LexicalExtension<import("lexical").ExtensionConfigBase, "@lexical/clipboard/DOMImport", unknown, unknown>;