Skip to content

Commit ff54fb7

Browse files
committed
FEATURE: Delete a picture from a document (closes #187).
1 parent 62e6f04 commit ff54fb7

1 file changed

Lines changed: 52 additions & 57 deletions

File tree

frontend/src/components/EditableText.jsx

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
import '../styles/EditableText.css';
22
import { 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';
65
import FormattedText from './FormattedText';
76
import DiscreeteDropdown from './DiscreeteDropdown';
87
import PictureUploadAction from '../menu-items/PictureUploadAction';
98
import {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 || '&nbsp;'}
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

195191
export default EditableText;
196-

0 commit comments

Comments
 (0)