@@ -22,10 +22,12 @@ import {
2222 SUPERDOC_MEDIA_MIME ,
2323 SUPERDOC_SLICE_ATTR ,
2424 SUPERDOC_BODY_SECT_PR_ATTR ,
25+ SUPERDOC_MEDIA_ATTR ,
2526 embedSliceInHtml ,
2627 extractSliceFromHtml ,
2728 stripSliceFromHtml ,
2829 extractBodySectPrFromHtml ,
30+ extractMediaFromHtml ,
2931 bodySectPrShouldEmbed ,
3032 collectReferencedImageMediaForClipboard ,
3133 applySuperdocClipboardMedia ,
@@ -35,7 +37,11 @@ import { annotateFragmentDomWithClipboardData } from './helpers/clipboardFragmen
3537/** Heuristic: clipboard HTML from SuperDoc copy (slice attrs, list/section metadata). */
3638export function isSuperdocOriginClipboardHtml ( html ) {
3739 if ( ! html || typeof html !== 'string' ) return false ;
38- if ( html . includes ( SUPERDOC_SLICE_ATTR ) || html . includes ( SUPERDOC_BODY_SECT_PR_ATTR ) ) {
40+ if (
41+ html . includes ( SUPERDOC_SLICE_ATTR ) ||
42+ html . includes ( SUPERDOC_BODY_SECT_PR_ATTR ) ||
43+ html . includes ( SUPERDOC_MEDIA_ATTR )
44+ ) {
3945 return true ;
4046 }
4147 if ( / d a t a - s d - s e c t - p r \s * = / i. test ( html ) ) {
@@ -315,10 +321,11 @@ export const inputRulesPlugin = ({ editor, rules }) => {
315321 const rawHtml = clipboard . getData ( 'text/html' ) ;
316322 const isSuperdocHtml = isSuperdocOriginClipboardHtml ( rawHtml ) ;
317323 const embeddedBodySectPr = isSuperdocHtml ? extractBodySectPrFromHtml ( rawHtml ) : null ;
324+ const embeddedMedia = isSuperdocHtml ? extractMediaFromHtml ( rawHtml ) : '' ;
318325
319326 let superdocSliceData = clipboard . getData ( SUPERDOC_SLICE_MIME ) || extractSliceFromHtml ( rawHtml ) ;
320327 if ( isSuperdocHtml || superdocSliceData ) {
321- superdocSliceData = applySuperdocClipboardMedia ( editor , clipboard , superdocSliceData || null ) ;
328+ superdocSliceData = applySuperdocClipboardMedia ( editor , clipboard , superdocSliceData || null , embeddedMedia ) ;
322329 }
323330 if ( superdocSliceData ) {
324331 try {
@@ -634,10 +641,8 @@ export function sanitizeHtml(html, forbiddenTags = ['meta', 'svg', 'script', 'st
634641}
635642
636643/**
637- * Reusable paste-handling utility that replicates the logic formerly held only
638- * inside the `inputRulesPlugin` paste handler. This allows other components
639- * (e.g. context-menu items) to invoke the same paste logic without duplicating
640- * code.
644+ * Handles clipboard content that was read outside the native paste event, such
645+ * as the context-menu Paste action.
641646 *
642647 * @param {Object } params
643648 * @param {Editor } params.editor The SuperEditor instance.
@@ -647,13 +652,33 @@ export function sanitizeHtml(html, forbiddenTags = ['meta', 'svg', 'script', 'st
647652 * @returns {Boolean } Whether the paste was handled.
648653 */
649654export function handleClipboardPaste ( { editor, view } , html , plainText ) {
655+ const rawHtml = html || '' ;
656+ const isSuperdocHtml = isSuperdocOriginClipboardHtml ( rawHtml ) ;
657+ const embeddedBodySectPr = isSuperdocHtml ? extractBodySectPrFromHtml ( rawHtml ) : null ;
658+ const embeddedMedia = isSuperdocHtml ? extractMediaFromHtml ( rawHtml ) : '' ;
659+ let pasteHtml = rawHtml ;
660+
661+ let superdocSliceData = extractSliceFromHtml ( rawHtml ) ;
662+ if ( superdocSliceData ) {
663+ superdocSliceData = applySuperdocClipboardMedia ( editor , null , superdocSliceData , embeddedMedia ) ;
664+ try {
665+ if ( handleSuperdocSlicePaste ( superdocSliceData , editor , view , embeddedBodySectPr ) ) return true ;
666+ } catch ( err ) {
667+ console . warn ( 'Failed to paste SuperDoc slice, falling back to HTML:' , err ) ;
668+ }
669+ }
670+
671+ if ( isSuperdocHtml ) {
672+ pasteHtml = stripSliceFromHtml ( rawHtml ) ;
673+ }
674+
650675 let source ;
651676
652- if ( ! html ) {
677+ if ( ! pasteHtml ) {
653678 source = 'plain-text' ;
654- } else if ( isWordHtml ( html ) ) {
679+ } else if ( isWordHtml ( pasteHtml ) ) {
655680 source = 'word-html' ;
656- } else if ( isGoogleDocsHtml ( html ) ) {
681+ } else if ( isGoogleDocsHtml ( pasteHtml ) ) {
657682 source = 'google-docs' ;
658683 } else {
659684 source = 'browser-html' ;
@@ -667,15 +692,31 @@ export function handleClipboardPaste({ editor, view }, html, plainText) {
667692 return handlePlainTextUrlPaste ( editor , view , plainText , detected ) ;
668693 }
669694 case 'word-html' :
670- if ( editor . options . mode === 'docx' && ! isSuperdocOriginClipboardHtml ( html ) ) {
671- return handleDocxPaste ( html , editor , view ) ;
695+ if ( editor . options . mode === 'docx' && ! isSuperdocHtml ) {
696+ return handleDocxPaste ( pasteHtml , editor , view ) ;
672697 }
673- return handleHtmlPaste ( html , editor ) ;
674- case 'google-docs' :
675- return handleGoogleDocsHtml ( html , editor , view ) ;
698+ {
699+ const ok = handleHtmlPaste ( pasteHtml , editor ) ;
700+ if ( ok && embeddedBodySectPr ) {
701+ tryApplyEmbeddedBodySectPr ( editor , view , embeddedBodySectPr ) ;
702+ }
703+ return ok ;
704+ }
705+ case 'google-docs' : {
706+ const ok = handleGoogleDocsHtml ( pasteHtml , editor , view ) ;
707+ if ( ok && embeddedBodySectPr ) {
708+ tryApplyEmbeddedBodySectPr ( editor , view , embeddedBodySectPr ) ;
709+ }
710+ return ok ;
711+ }
676712 // falls through to browser-html handling when not in DOCX mode
677- case 'browser-html' :
678- return handleHtmlPaste ( html , editor ) ;
713+ case 'browser-html' : {
714+ const ok = handleHtmlPaste ( pasteHtml , editor ) ;
715+ if ( ok && embeddedBodySectPr ) {
716+ tryApplyEmbeddedBodySectPr ( editor , view , embeddedBodySectPr ) ;
717+ }
718+ return ok ;
719+ }
679720 }
680721
681722 return false ;
@@ -709,7 +750,7 @@ function handleCutEvent(view, event, editor) {
709750 const html = unflattenListsInHtml ( div . innerHTML ) ;
710751 const bodySectPr = view . state . doc . attrs ?. bodySectPr ;
711752 const bodySectPrJson = bodySectPr && bodySectPrShouldEmbed ( bodySectPr ) ? JSON . stringify ( bodySectPr ) : '' ;
712- clipboardData . setData ( 'text/html' , embedSliceInHtml ( html , sliceJson , bodySectPrJson ) ) ;
753+ clipboardData . setData ( 'text/html' , embedSliceInHtml ( html , sliceJson , bodySectPrJson , mediaJson ) ) ;
713754 clipboardData . setData ( 'text/plain' , fragment . textBetween ( 0 , fragment . size , '\n\n' ) ) ;
714755
715756 event . preventDefault ( ) ;
0 commit comments