11import '../styles/EditableText.css' ;
2+ import { Trash } from 'react-bootstrap-icons' ;
3+ import { createRoot } from 'react-dom/client' ;
24
3- import { useState , useEffect , useCallback } from 'react' ;
5+ import { useState , useEffect , useCallback , useRef } from 'react' ;
46import FormattedText from './FormattedText' ;
57import DiscreeteDropdown from './DiscreeteDropdown' ;
68import PictureUploadAction from '../menu-items/PictureUploadAction' ;
@@ -10,6 +12,9 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
1012 const [ beingEdited , setBeingEdited ] = useState ( false ) ;
1113 const [ editedDocument , setEditedDocument ] = useState ( ) ;
1214 const [ editedText , setEditedText ] = useState ( ) ;
15+ const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
16+ const [ deleteTarget , setDeleteTarget ] = useState ( { src : '' , internal : false , name : '' } ) ;
17+ const containerRef = useRef ( null ) ;
1318 const PASSAGE = new RegExp ( `\\{${ rubric } } ?([^{]*)` ) ;
1419
1520 let parsePassage = ( rawText ) => ( rubric )
@@ -79,25 +84,113 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
7984 . catch ( console . error ) ;
8085 } ;
8186
87+ useEffect ( ( ) => {
88+ const attachTrash = ( ) => {
89+ const root = containerRef . current ;
90+ if ( ! root ) return ;
91+ root . querySelectorAll ( 'figure' ) . forEach ( fig => {
92+ if ( fig . querySelector ( '.trash-overlay' ) ) return ;
93+ const img = fig . querySelector ( 'img' ) ;
94+ if ( ! img ) return ;
95+ const trash = document . createElement ( 'div' ) ;
96+ trash . className = 'trash-overlay' ;
97+ trash . setAttribute ( 'data-img' , img . src ) ;
98+ trash . setAttribute ( 'tabindex' , 0 ) ;
99+ fig . style . position = 'relative' ;
100+ fig . appendChild ( trash ) ;
101+ createRoot ( trash ) . render ( < Trash /> ) ;
102+ } ) ;
103+ } ;
104+
105+ const obs = new MutationObserver ( attachTrash ) ;
106+ if ( containerRef . current ) {
107+ obs . observe ( containerRef . current , { childList : true , subtree : true } ) ;
108+ }
109+ attachTrash ( ) ;
110+ return ( ) => obs . disconnect ( ) ;
111+ } , [ backend , id , setLastUpdate , beingEdited ] ) ;
112+
113+ const confirmDelete = ( ) => {
114+ const { src, internal, name } = deleteTarget ;
115+ const esc = s => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
116+ const mdRx = new RegExp ( `!?\\[[^\\]]*\\]\\(${ esc ( src ) } \\)` , 'g' ) ;
117+ const clean = t => ( t || '' ) . replace ( mdRx , '' ) . replace ( / \n { 2 , } / g, '\n\n' ) . trim ( ) ;
118+
119+ if ( internal ) {
120+ backend . deleteAttachment ( id , name , res => {
121+ if ( ! res . ok ) return alert ( 'Error deleting attachment.' ) ;
122+ backend . getDocument ( id ) . then ( doc => {
123+ const cleaned = clean ( doc . text ) ;
124+ backend . putDocument ( { ...doc , text : cleaned } ) . then ( r => {
125+ setEditedText ( cleaned ) ;
126+ setEditedDocument ( { ...doc , text : cleaned , _rev : r . rev } ) ;
127+ setLastUpdate ( r . rev ) ;
128+ setShowDeleteModal ( false ) ;
129+ } ) ;
130+ } ) ;
131+ } ) ;
132+ } else {
133+ const cleaned = clean ( editedText ) ;
134+ setEditedText ( cleaned ) ;
135+ setEditedDocument ( p => ( { ...p , text : cleaned } ) ) ;
136+ setShowDeleteModal ( false ) ;
137+ }
138+ } ;
139+
82140 if ( ! beingEdited ) return (
83141 < div className = "editable content position-relative" title = "Edit content..." >
84- < div className = "formatted-text" onClick = { handleClick } >
142+ < div className = "formatted-text" onClick = { e => {
143+ if ( ! beingEdited ) {
144+ const trash = e . target . closest ( '.trash-overlay' ) ;
145+ if ( trash ) {
146+ const src = trash . getAttribute ( 'data-img' ) ;
147+ const internal = src . includes ( `/${ id } /` ) ;
148+ const name = internal
149+ ? decodeURIComponent ( src . split ( `${ id } /` ) [ 1 ] )
150+ : src ;
151+ setDeleteTarget ( { src, internal, name } ) ;
152+ setShowDeleteModal ( true ) ;
153+ return ;
154+ }
155+ handleClick ( ) ;
156+ }
157+ } }
158+ ref = { containerRef }
159+ >
85160 < FormattedText { ...{ setHighlightedText, setSelectedText} } >
86161 { text || ' ' }
87162 </ FormattedText >
88163 </ div >
89164 < DiscreeteDropdown >
90- < PictureUploadAction { ... { id, backend, handleImageUrl} } />
165+ < PictureUploadAction { ...{ id, backend, handleImageUrl} } />
91166 </ DiscreeteDropdown >
167+ { showDeleteModal && (
168+ < div className = "modal fade show d-block" tabIndex = "-1" role = "dialog" >
169+ < div className = "modal-dialog" role = "document" >
170+ < div className = "modal-content" >
171+ < div className = "modal-header" >
172+ < h5 className = "modal-title" > Confirm deletion</ h5 >
173+ < button type = "button" className = "btn-close" onClick = { ( ) => setShowDeleteModal ( false ) } />
174+ </ div >
175+ < div className = "modal-body" >
176+ < p > Delete image { deleteTarget . internal ? `"${ deleteTarget . name } "` : 'external' } ?</ p >
177+ </ div >
178+ < div className = "modal-footer" >
179+ < button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowDeleteModal ( false ) } > Cancel</ button >
180+ < button type = "button" className = "btn btn-danger" onClick = { confirmDelete } > Delete</ button >
181+ </ div >
182+ </ div >
183+ </ div >
184+ </ div >
185+ ) }
92186 </ div >
93187 ) ;
94188 return (
95189 < form >
96- < textarea className = "form-control" type = "text" rows = "5" autoFocus
97- value = { editedText } onChange = { handleChange } onBlur = { handleBlur }
98- />
190+ < textarea className = "form-control" rows = "5" autoFocus value = { editedText } onChange = { handleChange } onBlur = { handleBlur } />
99191 </ form >
100192 ) ;
101193}
102194
103195export default EditableText ;
196+
0 commit comments