11import '../styles/EditableText.css' ;
22
3- import { useState , useEffect , useCallback } from 'react' ;
3+ import { useState , useEffect , useCallback , useMemo , useRef } from 'react' ;
44import FormattedText from './FormattedText' ;
55import DiscreeteDropdown from './DiscreeteDropdown' ;
66import PictureUploadAction from '../menu-items/PictureUploadAction' ;
@@ -10,13 +10,22 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
1010 const [ beingEdited , setBeingEdited ] = useState ( false ) ;
1111 const [ editedDocument , setEditedDocument ] = useState ( ) ;
1212 const [ editedText , setEditedText ] = useState ( ) ;
13- const PASSAGE = new RegExp ( `\\{${ rubric } } ?([^{]*)` ) ;
13+ const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
14+ const [ deleteTarget , setDeleteTarget ] = useState ( { src : '' , internal : false , name : '' } ) ;
15+ const containerRef = useRef ( null ) ;
16+ const PASSAGE = useMemo ( ( ) => new RegExp ( `\\{${ rubric } } ?([^\\{]*)` ) , [ rubric ] ) ;
1417
15- let parsePassage = ( rawText ) => ( rubric )
16- ? rawText . match ( PASSAGE ) [ 1 ]
17- : rawText ;
18+ let parsePassage = ( rawText ) => {
19+ if ( ! rawText ) return '' ;
20+ if ( rubric ) {
21+ const m = rawText . match ( PASSAGE ) ;
22+ return m ? m [ 1 ] : '' ;
23+ }
24+ return rawText ;
25+ } ;
1826
1927 let parseFirstPassage = useCallback ( ( rawText ) => {
28+ if ( ! rawText ) return '' ;
2029 const FIRST_PASSAGE = new RegExp ( '\\{[^}]+} ?([^{]*)' ) ;
2130 let parsed = rawText . match ( FIRST_PASSAGE ) ;
2231 return ( parsed ) ? parsed [ 1 ] : rawText ;
@@ -79,24 +88,147 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
7988 . catch ( console . error ) ;
8089 } ;
8190
91+ useEffect ( ( ) => {
92+ const attachTrash = ( ) => {
93+ const root = containerRef . current ;
94+ if ( ! root ) return ;
95+ root . querySelectorAll ( 'figure' ) . forEach ( fig => {
96+ if ( fig . querySelector ( '.trash-overlay' ) ) return ;
97+ const img = fig . querySelector ( 'img' ) ;
98+ if ( ! img ) return ;
99+ const trash = document . createElement ( 'div' ) ;
100+ trash . className = 'trash-overlay' ;
101+ trash . innerHTML = `
102+ <svg viewBox="0 0 16 16">
103+ <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
104+ <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1z"/>
105+ </svg>` ;
106+ Object . assign ( trash . style , {
107+ position : 'absolute' ,
108+ bottom : '10px' ,
109+ right : '10px' ,
110+ width : '24px' ,
111+ height : '24px' ,
112+ background : 'rgba(0,0,0,0.6)' ,
113+ display : 'flex' ,
114+ alignItems : 'center' ,
115+ justifyContent : 'center' ,
116+ cursor : 'pointer' ,
117+ opacity : '0' ,
118+ transition : 'opacity 0.2s ease' ,
119+ zIndex : '10'
120+ } ) ;
121+ fig . style . position = 'relative' ;
122+ fig . appendChild ( trash ) ;
123+ fig . addEventListener ( 'mouseenter' , ( ) => ( trash . style . opacity = '1' ) ) ;
124+ fig . addEventListener ( 'mouseleave' , ( ) => ( trash . style . opacity = '0' ) ) ;
125+ trash . addEventListener ( 'click' , ( ) => {
126+ const src = img . src ;
127+ const internal = src . includes ( `/${ id } /` ) ;
128+ const name = internal
129+ ? decodeURIComponent ( src . split ( `${ id } /` ) [ 1 ] )
130+ : src ;
131+ setDeleteTarget ( { src, internal, name } ) ;
132+ setShowDeleteModal ( true ) ;
133+ } ) ;
134+ } ) ;
135+ } ;
136+
137+ const obs = new MutationObserver ( attachTrash ) ;
138+ if ( containerRef . current ) {
139+ obs . observe ( containerRef . current , { childList : true , subtree : true } ) ;
140+ }
141+ attachTrash ( ) ;
142+ return ( ) => obs . disconnect ( ) ;
143+ } , [ backend , id , setLastUpdate , beingEdited ] ) ;
144+
145+ const confirmDelete = ( ) => {
146+ const { src, internal, name } = deleteTarget ;
147+ const esc = s => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
148+ const pattern = esc ( src ) ;
149+ const mdRx = new RegExp ( `!?\\[[^\\]]*\\]\\(${ pattern } \\)` , 'g' ) ;
150+ const clean = t => ( t || '' ) . replace ( mdRx , '' ) . replace ( / \n { 2 , } / g, '\n\n' ) . trim ( ) ;
151+
152+ if ( internal ) {
153+ backend . deleteAttachment ( id , name , res => {
154+ if ( ! res . ok ) return alert ( 'Erreur lors de la suppression.' ) ;
155+ backend . getDocument ( id ) . then ( doc => {
156+ const cleaned = clean ( doc . text ) ;
157+ backend . putDocument ( { ...doc , text : cleaned } ) . then ( r => {
158+ setEditedText ( cleaned ) ;
159+ setEditedDocument ( { ...doc , text : cleaned , _rev : r . rev } ) ;
160+ setLastUpdate ( r . rev ) ;
161+ setShowDeleteModal ( false ) ;
162+ } ) ;
163+ } ) ;
164+ } ) ;
165+ } else {
166+ const cleaned = clean ( editedText ) ;
167+ setEditedText ( cleaned ) ;
168+ setEditedDocument ( p => ( { ...p , text : cleaned } ) ) ;
169+ setShowDeleteModal ( false ) ;
170+ }
171+ } ;
172+
82173 if ( ! beingEdited ) return (
83- < div className = "editable content position-relative" title = "Edit content..." >
84- < div className = "formatted-text" onClick = { handleClick } >
85- < FormattedText { ...{ setHighlightedText, setSelectedText} } >
86- { text || ' ' }
87- </ FormattedText >
174+ < >
175+ < div className = "editable content position-relative" title = "Edit content..." >
176+ < div className = "formatted-text" onClick = { handleClick } ref = { containerRef } >
177+ < FormattedText { ...{ setHighlightedText, setSelectedText} } >
178+ { text || ' ' }
179+ </ FormattedText >
180+ </ div >
181+ < DiscreeteDropdown >
182+ < PictureUploadAction { ...{ id, backend, handleImageUrl} } />
183+ </ DiscreeteDropdown >
88184 </ div >
89- < DiscreeteDropdown >
90- < PictureUploadAction { ... { id, backend, handleImageUrl} } />
91- </ DiscreeteDropdown >
92- </ div >
185+ { showDeleteModal && (
186+ < div className = "modal fade show d-block" tabIndex = "-1" role = "dialog" >
187+ < div className = "modal-dialog" role = "document" >
188+ < div className = "modal-content" >
189+ < div className = "modal-header" >
190+ < h5 className = "modal-title" > Confirmer la suppression</ h5 >
191+ < button type = "button" className = "btn-close" onClick = { ( ) => setShowDeleteModal ( false ) } />
192+ </ div >
193+ < div className = "modal-body" >
194+ < p > Supprimer l’image { deleteTarget . internal ? `"${ deleteTarget . name } "` : 'externe' } ?</ p >
195+ </ div >
196+ < div className = "modal-footer" >
197+ < button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowDeleteModal ( false ) } > Annuler</ button >
198+ < button type = "button" className = "btn btn-danger" onClick = { confirmDelete } > Supprimer</ button >
199+ </ div >
200+ </ div >
201+ </ div >
202+ </ div >
203+ ) }
204+ </ >
93205 ) ;
206+
94207 return (
95- < form >
96- < textarea className = "form-control" type = "text" rows = "5" autoFocus
97- value = { editedText } onChange = { handleChange } onBlur = { handleBlur }
98- />
99- </ form >
208+ < >
209+ < form >
210+ < textarea className = "form-control" rows = { 5 } autoFocus value = { editedText } onChange = { handleChange } onBlur = { handleBlur } />
211+ </ form >
212+ { showDeleteModal && (
213+ < div className = "modal fade show d-block" tabIndex = "-1" role = "dialog" >
214+ < div className = "modal-dialog" role = "document" >
215+ < div className = "modal-content" >
216+ < div className = "modal-header" >
217+ < h5 className = "modal-title" > Confirmer la suppression</ h5 >
218+ < button type = "button" className = "btn-close" onClick = { ( ) => setShowDeleteModal ( false ) } />
219+ </ div >
220+ < div className = "modal-body" >
221+ < p > Supprimer l’image { deleteTarget . internal ? `"${ deleteTarget . name } "` : 'externe' } ?</ p >
222+ </ div >
223+ < div className = "modal-footer" >
224+ < button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowDeleteModal ( false ) } > Annuler</ button >
225+ < button type = "button" className = "btn btn-danger" onClick = { confirmDelete } > Supprimer</ button >
226+ </ div >
227+ </ div >
228+ </ div >
229+ </ div >
230+ ) }
231+ </ >
100232 ) ;
101233}
102234
0 commit comments