11import '../styles/EditableText.css' ;
22import { Trash } from 'react-bootstrap-icons' ;
3- import { createRoot } from 'react-dom/client' ;
43
5- import { useState , useEffect , useCallback , useRef } from 'react' ;
4+ import { useState , useEffect , useCallback } from 'react' ;
65import FormattedText from './FormattedText' ;
76import DiscreeteDropdown from './DiscreeteDropdown' ;
87import PictureUploadAction from '../menu-items/PictureUploadAction' ;
98import { v4 as uuid } from 'uuid' ;
109
11- function EditableText ( { id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, backend, setLastUpdate} ) {
10+ function EditableText ( { id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, backend, setLastUpdate } ) {
1211 const [ beingEdited , setBeingEdited ] = useState ( false ) ;
1312 const [ editedDocument , setEditedDocument ] = useState ( ) ;
1413 const [ editedText , setEditedText ] = useState ( ) ;
1514 const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
16- const [ deleteTarget , setDeleteTarget ] = useState ( { src : '' , internal : false , name : '' } ) ;
17- const containerRef = useRef ( null ) ;
15+ const [ deleteTarget , setDeleteTarget ] = useState ( { src : '' , alt : '' , internal : false , name : '' } ) ;
1816 const PASSAGE = new RegExp ( `\\{${ rubric } } ?([^{]*)` ) ;
1917
2018 let parsePassage = ( rawText ) => ( rubric )
@@ -30,7 +28,7 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
3028 let updateEditedDocument = useCallback ( ( ) => backend . getDocument ( id )
3129 . then ( ( x ) => {
3230 x = x . error
33- ? { _id : uuid ( ) , text : `{${ rubric } }` , isPartOf, links}
31+ ? { _id : uuid ( ) , text : `{${ rubric } }` , isPartOf, links }
3432 : x ;
3533 setEditedDocument ( x ) ;
3634 return x ;
@@ -84,38 +82,20 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
8482 . catch ( console . error ) ;
8583 } ;
8684
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 ] ) ;
85+ const imageRegex = / ! \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) ] + ) \) / g;
86+ let images = [ ] ;
87+ let textSansImages = text || '' ;
88+ let match ;
89+ while ( ( match = imageRegex . exec ( textSansImages ) ) !== null ) {
90+ images . push ( { alt : match [ 1 ] , src : match [ 2 ] } ) ;
91+ }
92+ textSansImages = textSansImages . replace ( imageRegex , '' ) ;
11293
11394 const confirmDelete = ( ) => {
114- const { src, internal, name } = deleteTarget ;
95+ const { src, alt , internal, name } = deleteTarget ;
11596 const esc = s => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
116- const mdRx = new RegExp ( `!? \\[[^\\]]* \\]\\(${ esc ( src ) } \\)` , 'g' ) ;
97+ const mdRx = new RegExp ( `!\\[${ esc ( alt ) } \\]\\(${ esc ( src ) } \\)` , 'g' ) ;
11798 const clean = t => ( t || '' ) . replace ( mdRx , '' ) . replace ( / \n { 2 , } / g, '\n\n' ) . trim ( ) ;
118-
11999 if ( internal ) {
120100 backend . deleteAttachment ( id , name , res => {
121101 if ( ! res . ok ) return alert ( 'Error deleting attachment.' ) ;
@@ -139,30 +119,43 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
139119
140120 if ( ! beingEdited ) return (
141121 < div className = "editable content position-relative" title = "Edit content..." >
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- >
160- < FormattedText { ...{ setHighlightedText, setSelectedText} } >
161- { text || ' ' }
122+ < div className = "formatted-text" onClick = { handleClick } >
123+ < FormattedText { ...{ setHighlightedText, setSelectedText } } >
124+ { textSansImages || '\u00A0' }
162125 </ 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+ ) ) }
163156 </ div >
164157 < DiscreeteDropdown >
165- < PictureUploadAction { ...{ id, backend, handleImageUrl} } />
158+ < PictureUploadAction { ...{ id, backend, handleImageUrl } } />
166159 </ DiscreeteDropdown >
167160 { showDeleteModal && (
168161 < div className = "modal fade show d-block" tabIndex = "-1" role = "dialog" >
@@ -185,12 +178,14 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
185178 ) }
186179 </ div >
187180 ) ;
181+
188182 return (
189183 < form >
190- < textarea className = "form-control" rows = "5" autoFocus value = { editedText } onChange = { handleChange } onBlur = { handleBlur } />
184+ < textarea className = "form-control" type = "text" rows = "5" autoFocus
185+ value = { editedText } onChange = { handleChange } onBlur = { handleBlur }
186+ />
191187 </ form >
192188 ) ;
193189}
194190
195191export default EditableText ;
196-
0 commit comments