@@ -12,6 +12,7 @@ import {objectKlassEquals} from '@lexical/utils';
1212import {
1313 $cloneWithProperties ,
1414 $createTabNode ,
15+ $getEditor ,
1516 $getRoot ,
1617 $getSelection ,
1718 $isElementNode ,
@@ -34,18 +35,26 @@ import invariant from 'shared/invariant';
3435const getDOMSelection = ( targetWindow : Window | null ) : Selection | null =>
3536 CAN_USE_DOM ? ( targetWindow || window ) . getSelection ( ) : null ;
3637
38+ export interface LexicalClipboardData {
39+ 'text/html' ?: string | undefined ;
40+ 'application/x-lexical-editor' ?: string | undefined ;
41+ 'text/plain' : string ;
42+ }
43+
3744/**
3845 * Returns the *currently selected* Lexical content as an HTML string, relying on the
3946 * logic defined in the exportDOM methods on the LexicalNode classes. Note that
4047 * this will not return the HTML content of the entire editor (unless all the content is included
4148 * in the current selection).
4249 *
4350 * @param editor - LexicalEditor instance to get HTML content from
51+ * @param selection - The selection to use (default is $getSelection())
4452 * @returns a string of HTML content
4553 */
46- export function $getHtmlContent ( editor : LexicalEditor ) : string {
47- const selection = $getSelection ( ) ;
48-
54+ export function $getHtmlContent (
55+ editor : LexicalEditor ,
56+ selection = $getSelection ( ) ,
57+ ) : string {
4958 if ( selection == null ) {
5059 invariant ( false , 'Expected valid LexicalSelection' ) ;
5160 }
@@ -68,11 +77,13 @@ export function $getHtmlContent(editor: LexicalEditor): string {
6877 * in the current selection).
6978 *
7079 * @param editor - LexicalEditor instance to get the JSON content from
80+ * @param selection - The selection to use (default is $getSelection())
7181 * @returns
7282 */
73- export function $getLexicalContent ( editor : LexicalEditor ) : null | string {
74- const selection = $getSelection ( ) ;
75-
83+ export function $getLexicalContent (
84+ editor : LexicalEditor ,
85+ selection = $getSelection ( ) ,
86+ ) : null | string {
7687 if ( selection == null ) {
7788 invariant ( false , 'Expected valid LexicalSelection' ) ;
7889 }
@@ -383,6 +394,7 @@ let clipboardEventTimeout: null | number = null;
383394export async function copyToClipboard (
384395 editor : LexicalEditor ,
385396 event : null | ClipboardEvent ,
397+ data ?: LexicalClipboardData ,
386398) : Promise < boolean > {
387399 if ( clipboardEventTimeout !== null ) {
388400 // Prevent weird race conditions that can happen when this function is run multiple times
@@ -392,7 +404,7 @@ export async function copyToClipboard(
392404 if ( event !== null ) {
393405 return new Promise ( ( resolve , reject ) => {
394406 editor . update ( ( ) => {
395- resolve ( $copyToClipboardEvent ( editor , event ) ) ;
407+ resolve ( $copyToClipboardEvent ( editor , event , data ) ) ;
396408 } ) ;
397409 } ) ;
398410 }
@@ -423,7 +435,9 @@ export async function copyToClipboard(
423435 window . clearTimeout ( clipboardEventTimeout ) ;
424436 clipboardEventTimeout = null ;
425437 }
426- resolve ( $copyToClipboardEvent ( editor , secondEvent as ClipboardEvent ) ) ;
438+ resolve (
439+ $copyToClipboardEvent ( editor , secondEvent as ClipboardEvent , data ) ,
440+ ) ;
427441 }
428442 // Block the entire copy flow while we wait for the next ClipboardEvent
429443 return true ;
@@ -446,38 +460,83 @@ export async function copyToClipboard(
446460function $copyToClipboardEvent (
447461 editor : LexicalEditor ,
448462 event : ClipboardEvent ,
463+ data ?: LexicalClipboardData ,
449464) : boolean {
450- const domSelection = getDOMSelection ( editor . _window ) ;
451- if ( ! domSelection ) {
452- return false ;
453- }
454- const anchorDOM = domSelection . anchorNode ;
455- const focusDOM = domSelection . focusNode ;
456- if (
457- anchorDOM !== null &&
458- focusDOM !== null &&
459- ! isSelectionWithinEditor ( editor , anchorDOM , focusDOM )
460- ) {
461- return false ;
465+ if ( data === undefined ) {
466+ const domSelection = getDOMSelection ( editor . _window ) ;
467+ if ( ! domSelection ) {
468+ return false ;
469+ }
470+ const anchorDOM = domSelection . anchorNode ;
471+ const focusDOM = domSelection . focusNode ;
472+ if (
473+ anchorDOM !== null &&
474+ focusDOM !== null &&
475+ ! isSelectionWithinEditor ( editor , anchorDOM , focusDOM )
476+ ) {
477+ return false ;
478+ }
479+ const selection = $getSelection ( ) ;
480+ if ( selection === null ) {
481+ return false ;
482+ }
483+ data = $getClipboardDataFromSelection ( selection ) ;
462484 }
463485 event . preventDefault ( ) ;
464486 const clipboardData = event . clipboardData ;
465- const selection = $getSelection ( ) ;
466- if ( clipboardData === null || selection === null ) {
487+ if ( clipboardData === null ) {
467488 return false ;
468489 }
469- const htmlString = $getHtmlContent ( editor ) ;
470- const lexicalString = $getLexicalContent ( editor ) ;
471- let plainString = '' ;
472- if ( selection !== null ) {
473- plainString = selection . getTextContent ( ) ;
474- }
475- if ( htmlString !== null ) {
476- clipboardData . setData ( 'text/html' , htmlString ) ;
490+ setLexicalClipboardDataTransfer ( clipboardData , data ) ;
491+ return true ;
492+ }
493+
494+ const clipboardDataFunctions = [
495+ [ 'text/html' , $getHtmlContent ] ,
496+ [ 'application/x-lexical-editor' , $getLexicalContent ] ,
497+ ] as const ;
498+
499+ /**
500+ * Serialize the content of the current selection to strings in
501+ * text/plain, text/html, and application/x-lexical-editor (Lexical JSON)
502+ * formats (as available).
503+ *
504+ * @param selection the selection to serialize (defaults to $getSelection())
505+ * @returns LexicalClipboardData
506+ */
507+ export function $getClipboardDataFromSelection (
508+ selection : BaseSelection | null = $getSelection ( ) ,
509+ ) : LexicalClipboardData {
510+ const clipboardData : LexicalClipboardData = {
511+ 'text/plain' : selection ? selection . getTextContent ( ) : '' ,
512+ } ;
513+ if ( selection ) {
514+ const editor = $getEditor ( ) ;
515+ for ( const [ mimeType , $editorFn ] of clipboardDataFunctions ) {
516+ const v = $editorFn ( editor , selection ) ;
517+ if ( v !== null ) {
518+ clipboardData [ mimeType ] = v ;
519+ }
520+ }
477521 }
478- if ( lexicalString !== null ) {
479- clipboardData . setData ( 'application/x-lexical-editor' , lexicalString ) ;
522+ return clipboardData ;
523+ }
524+
525+ /**
526+ * Call setData on the given clipboardData for each MIME type present
527+ * in the given data (from {@link $getClipboardDataFromSelection})
528+ *
529+ * @param clipboardData the event.clipboardData to populate from data
530+ * @param data The lexical data
531+ */
532+ export function setLexicalClipboardDataTransfer (
533+ clipboardData : DataTransfer ,
534+ data : LexicalClipboardData ,
535+ ) {
536+ for ( const k in data ) {
537+ const v = data [ k as keyof LexicalClipboardData ] ;
538+ if ( v !== undefined ) {
539+ clipboardData . setData ( k , v ) ;
540+ }
480541 }
481- clipboardData . setData ( 'text/plain' , plainString ) ;
482- return true ;
483542}
0 commit comments