11import '../styles/EditableText.css' ;
2+ import { Trash } from 'react-bootstrap-icons' ;
23
34import { useState , useEffect , useCallback } from 'react' ;
45import FormattedText from './FormattedText' ;
@@ -10,6 +11,8 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
1011 const [ beingEdited , setBeingEdited ] = useState ( false ) ;
1112 const [ editedDocument , setEditedDocument ] = useState ( ) ;
1213 const [ editedText , setEditedText ] = useState ( ) ;
14+ const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
15+ const [ deleteTarget , setDeleteTarget ] = useState ( { src : '' , alt : '' , internal : false , name : '' } ) ;
1316 const PASSAGE = new RegExp ( `\\{${ rubric } } ?([^{]*)` ) ;
1417
1518 let parsePassage = ( rawText ) => ( rubric )
@@ -79,18 +82,103 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
7982 . catch ( console . error ) ;
8083 } ;
8184
85+ const imageRegex = / ! \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) ] + ) \) / g;
86+ let images = [ ] ;
87+ let Text = text || '' ;
88+ let match ;
89+ while ( ( match = imageRegex . exec ( Text ) ) !== null ) {
90+ images . push ( { alt : match [ 1 ] , src : match [ 2 ] } ) ;
91+ }
92+ Text = Text . replace ( imageRegex , '' ) ;
93+
94+ const confirmDelete = ( ) => {
95+ const { src, alt, internal, name } = deleteTarget ;
96+ const esc = s => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
97+ const mdRx = new RegExp ( `!\\[${ esc ( alt ) } \\]\\(${ esc ( src ) } \\)` , 'g' ) ;
98+ const clean = t => ( t || '' ) . replace ( mdRx , '' ) . replace ( / \n { 2 , } / g, '\n\n' ) . trim ( ) ;
99+ if ( internal ) {
100+ backend . deleteAttachment ( id , name , res => {
101+ if ( ! res . ok ) return alert ( 'Error deleting attachment.' ) ;
102+ backend . getDocument ( id ) . then ( doc => {
103+ const cleaned = clean ( doc . text ) ;
104+ backend . putDocument ( { ...doc , text : cleaned } ) . then ( r => {
105+ setEditedText ( cleaned ) ;
106+ setEditedDocument ( { ...doc , text : cleaned , _rev : r . rev } ) ;
107+ setLastUpdate ( r . rev ) ;
108+ setShowDeleteModal ( false ) ;
109+ } ) ;
110+ } ) ;
111+ } ) ;
112+ } else {
113+ const cleaned = clean ( editedText ) ;
114+ setEditedText ( cleaned ) ;
115+ setEditedDocument ( p => ( { ...p , text : cleaned } ) ) ;
116+ setShowDeleteModal ( false ) ;
117+ }
118+ } ;
119+
82120 if ( ! beingEdited ) return (
83121 < div className = "editable content position-relative" title = "Edit content..." >
84122 < div className = "formatted-text" onClick = { handleClick } >
85- < FormattedText { ...{ setHighlightedText, setSelectedText} } >
86- { text || ' ' }
123+ < FormattedText { ...{ setHighlightedText, setSelectedText } } >
124+ { Text || ' ' }
87125 </ FormattedText >
126+ { images . map ( ( { src, alt } ) => (
127+ < figure
128+ key = { src + alt }
129+ className = "has-trash-overlay"
130+ style = { { position : 'relative' , display : 'inline-block' , margin : 0 } }
131+ >
132+ < img
133+ src = { src }
134+ alt = { alt }
135+ className = "img-fluid rounded editable-image"
136+ />
137+ < button
138+ className = "trash-overlay"
139+ type = "button"
140+ aria-label = { `Delete image ${ alt || src } ` }
141+ title = { `Delete image ${ alt || src } ` }
142+ onClick = { e => {
143+ e . stopPropagation ( ) ;
144+ const internal = src . includes ( `/${ id } /` ) ;
145+ const name = internal
146+ ? decodeURIComponent ( src . split ( `${ id } /` ) [ 1 ] )
147+ : src ;
148+ setDeleteTarget ( { src, alt, internal, name } ) ;
149+ setShowDeleteModal ( true ) ;
150+ } }
151+ >
152+ < Trash />
153+ </ button >
154+ </ figure >
155+ ) ) }
88156 </ div >
89157 < DiscreeteDropdown >
90- < PictureUploadAction { ... { id, backend, handleImageUrl} } />
158+ < PictureUploadAction { ...{ id, backend, handleImageUrl} } />
91159 </ DiscreeteDropdown >
160+ { showDeleteModal && (
161+ < div className = "modal fade show d-block" tabIndex = "-1" role = "dialog" >
162+ < div className = "modal-dialog" role = "document" >
163+ < div className = "modal-content" >
164+ < div className = "modal-header" >
165+ < h5 className = "modal-title" > Confirm deletion</ h5 >
166+ < button type = "button" className = "btn-close" onClick = { ( ) => setShowDeleteModal ( false ) } />
167+ </ div >
168+ < div className = "modal-body" >
169+ < p > Delete image { deleteTarget . internal ? `"${ deleteTarget . name } "` : 'external' } ?</ p >
170+ </ div >
171+ < div className = "modal-footer" >
172+ < button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowDeleteModal ( false ) } > Cancel</ button >
173+ < button type = "button" className = "btn btn-danger" onClick = { confirmDelete } > Delete</ button >
174+ </ div >
175+ </ div >
176+ </ div >
177+ </ div >
178+ ) }
92179 </ div >
93180 ) ;
181+
94182 return (
95183 < form >
96184 < textarea className = "form-control" type = "text" rows = "5" autoFocus
0 commit comments