import { ComponentType } from 'react' ; import { ApplySchemaAttributes , command , CommandFunction , DOMCompatibleAttributes , extension , ExtensionPriority , ExtensionTag , findUploadPlaceholderPayload , getTextSelection , Handler , keyBinding , KeyBindingProps , NodeExtension , NodeExtensionSpec , NodeSpecOverride , NodeWithPosition , omitExtraAttributes , PrimitiveSelection , ProsemirrorNode , Transaction , uploadFile , UploadFileHandler , UploadPlaceholderPayload , } from '@remirror/core' ; import { PasteRule } from '@remirror/pm/paste-rules' ; import { NodeViewComponentProps } from '@remirror/react' ; import { FileComponent , FileComponentProps } from './file-component' ; import { createDataUrlFileUploader } from './file-uploaders' ; export interface FileOptions { /**
     * A function returns a `FileUploader` which will handle the upload process.
     */ uploadFileHandler ? : UploadFileHandler < FileAttributes > ; render ? : ( props : FileComponentProps ) => React . ReactElement < HTMLElement > | null ; default pasteRuleRegexp ? : RegExp ; /**
     * Called after the `commands.deleteFile` has been called.
     */ onDeleteFile ? : Handler < ( props : { tr : Transaction ; pos : number ; node : ProsemirrorNode ; } ) => void > ; } /**
 * Adds a file node to the editor
 */ @ extension < FileOptions > ( { defaultOptions : { uploadFileHandler : createDataUrlFileUploader , render : FileComponent , pasteRuleRegexp : /^((?!image).)*$/i , } , handlerKeys : [ 'onDeleteFile' ] , } ) export class FileExtension extends NodeExtension < FileOptions > { get name ( ) { return 'file' as const ; } ReactComponent : ComponentType < NodeViewComponentProps > = ( props ) => { const payload = findUploadPlaceholderPayload ( props . view . state , props . node . attrs . id ) ; const context = payload ?. context ; const abort = ( ) => payload ?. fileUploader . abort ( ) ; return this . options . render ( { ... props , context , abort } ) ; } ; createTags ( ) { return [ ExtensionTag . Block ] ; } createNodeSpec ( extra , override ) { return { attrs : { ... extra . defaults ( ) , id : { default : null } , url : { default : '' } , fileName : { default : '' } , fileType : { default : '' } , fileSize : { default : 0 } , error : { default : null } , } , selectable : true , draggable : true , atom : true , content : '' , ... override , parseDOM : [ { tag : 'div[data-file]' , priority : ExtensionPriority . Low , getAttrs : ( dom ) => { const anchor = dom as HTMLAnchorElement ; const url = anchor . getAttribute ( 'data-url' ) ; const fileName = anchor . getAttribute ( 'data-filename' ) ; const fileType = anchor . getAttribute ( 'data-filetype' ) ; const fileSize = anchor . getAttribute ( 'data-filesize' ) ; return { ... extra . parse ( dom ) , url , fileName , fileType , fileSize , } ; } , } , ... ( override . parseDOM ?? [ ] ) , ] , toDOM : ( node ) => { const { url , error , ... rest } = omitExtraAttributes ( node . attrs , extra ) ; const attrs = { ... extra . dom ( node ) , ... rest , 'data-url' : url , 'data-file' : '' , 'data-filename' : node . attrs . fileName , 'data-filetype' : node . attrs . fileType , 'data-filesize' : node . attrs . fileSize , } ; if ( error ) { attrs [ 'data-error' ] = error ; } return [ 'div' , attrs ] ; } , } ; } createPasteRules ( ) { return [ { type : 'file' , regexp : this . options . pasteRuleRegexp , fileHandler : ( props ) => { let pos ; if ( props . type === 'drop' ) { pos = props . pos ; } for ( const file of props . files ) { this . uploadFile ( file , pos ) ; } return true ; } , } , ] ; } @ command ( ) uploadFiles ( files ) { return ( ) => { for ( const file of files ) { this . uploadFile ( file ) ; } return true ; } ; } @ command ( ) updateFile ( pos , attrs ) { return ( { tr , dispatch } ) => { dispatch ?. ( tr . setNodeMarkup ( pos , undefined , attrs ) ) ; return true ; } ; } @ command ( ) insertFile ( attributes , selection ? ) { return ( { tr , dispatch } ) => { const { from , to } = getTextSelection ( selection ?? tr . selection , tr . doc ) ; const node = this . type . create ( attributes ) ; dispatch ?. ( tr . replaceRangeWith ( from , to , node ) ) ; return true ; } ; } @ command ( ) deleteFile ( pos ) { return ( { tr , state , dispatch } ) => { const node = state . doc . nodeAt ( pos ) ; if ( node && node . type === this . type ) { tr . delete ( pos , pos + 1 ) . scrollIntoView ( ) ; this . options . onDeleteFile ( { tr , pos , node } ) ; dispatch ?. ( tr ) ; return true ; } return false ; } ; } @ command ( ) renameFile ( pos , fileName ) { return ( { tr , state , dispatch } ) => { const node = state . doc . nodeAt ( pos ) ; if ( node && node . type === this . type ) { dispatch ?. ( tr . setNodeMarkup ( pos , undefined , { ... node . attrs , fileName } ) ) ; return true ; } return false ; } ; } @ keyBinding ( { shortcut : [ 'Backspace' , 'Delete' ] } ) backspaceShortcut ( props ) { const { tr , state } = props ; const { from , to , empty } = tr . selection ; if ( ! this . hasHandlers ( 'onDeleteFile' ) || empty ) { return false ; } const onDeleteFileCallbacks = [ ] ; state . doc . nodesBetween ( from , to , ( node , pos ) => { if ( node . type === this . type ) { onDeleteFileCallbacks . push ( { node , pos } ) ; } return true ; } ) ; onDeleteFileCallbacks . forEach ( ( { node , pos } ) => { this . options . onDeleteFile ( { tr , node , pos } ) ; } ) ; return false ; } private uploadFile ( file , pos ? ) { return uploadFile ( { file , pos , view : this . store . view , fileType : this . type , uploadHandler : this . options . uploadFileHandler , } ) ; } } export interface FileAttributes { /**
     * Unique identifier for a file
     * During the file uploading process, this is a temporary unique ID
     */ id ? : unknown ; /**
     * URL where the file is stored
     */ url ? : string ; /**
     * Name of the file
     */ fileName ? : string ; /**
     * Mime type of the file, e.g. "image/jpeg"
     */ fileType ? : string ; /**
     * File size in bytes
     */ fileSize ? : number ; /**
     * Error state for the file, e.g. upload failed
     */ error ? : string | null ; } declare global { namespace Remirror { interface AllExtensions { file : FileExtension ; } } }