Skip to content

Commit 00ed1b0

Browse files
IlanPincnxsolutions
andcommitted
FEATURE: Delete a picture from a document (closes #187).
Co-authored-by: Nasschml <nassim.chemlal@utt.fr>
1 parent 2eea2cf commit 00ed1b0

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

frontend/src/components/EditableText.jsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import FormattedText from './FormattedText';
55
import DiscreeteDropdown from './DiscreeteDropdown';
66
import PictureUploadAction from '../menu-items/PictureUploadAction';
77
import {v4 as uuid} from 'uuid';
8+
import ImagesWithDelete from './ImageDisplay';
89

910
function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, backend, setLastUpdate}) {
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,16 +82,85 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
7982
.catch(console.error);
8083
};
8184

85+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
86+
let images = [];
87+
let textWithoutInternalImages = text || '';
88+
let match;
89+
90+
while ((match = imageRegex.exec(text || '')) !== null) {
91+
const alt = match[1];
92+
const src = match[2];
93+
const isInternal = src.includes(`/${id}/`);
94+
if (isInternal) {
95+
images.push({ alt, src });
96+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97+
const mdRx = new RegExp(`!\\[${esc(alt)}\\]\\(${esc(src)}\\)`, 'g');
98+
textWithoutInternalImages = textWithoutInternalImages.replace(mdRx, '');
99+
}
100+
}
101+
textWithoutInternalImages = textWithoutInternalImages.replace(/\n{2,}/g, '\n\n').trim();
102+
103+
const confirmDelete = () => {
104+
const { src, alt, internal, name } = deleteTarget;
105+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106+
const mdRx = new RegExp(`!\\[${esc(alt)}\\]\\(${esc(src)}\\)`, 'g');
107+
const clean = t => (t || '').replace(mdRx, '').replace(/\n{2,}/g, '\n\n').trim();
108+
if (internal) {
109+
backend.deleteAttachment(id, name, res => {
110+
if (!res.ok) return;
111+
backend.getDocument(id).then(doc => {
112+
const cleaned = clean(doc.text);
113+
backend.putDocument({ ...doc, text: cleaned }).then(r => {
114+
setEditedText(cleaned);
115+
setEditedDocument({ ...doc, text: cleaned, _rev: r.rev });
116+
setLastUpdate(r.rev);
117+
setShowDeleteModal(false);
118+
});
119+
});
120+
});
121+
} else {
122+
const cleaned = clean(editedText);
123+
setEditedText(cleaned);
124+
setEditedDocument(p => ({ ...p, text: cleaned }));
125+
setShowDeleteModal(false);
126+
}
127+
};
128+
82129
if (!beingEdited) return (
83130
<div className="editable content position-relative" title="Edit content...">
84131
<div className="formatted-text" onClick={handleClick}>
85132
<FormattedText {...{setHighlightedText, setSelectedText}}>
86-
{text || '&nbsp;'}
133+
{textWithoutInternalImages || '&nbsp;'}
87134
</FormattedText>
135+
<ImagesWithDelete
136+
id={id}
137+
images={images}
138+
setDeleteTarget={setDeleteTarget}
139+
setShowDeleteModal={setShowDeleteModal}
140+
/>
88141
</div>
89142
<DiscreeteDropdown>
90143
<PictureUploadAction {... {id, backend, handleImageUrl}}/>
91144
</DiscreeteDropdown>
145+
{showDeleteModal && (
146+
<div className="modal fade show d-block" tabIndex="-1" role="dialog">
147+
<div className="modal-dialog" role="document">
148+
<div className="modal-content">
149+
<div className="modal-header">
150+
<h5 className="modal-title">Confirm deletion</h5>
151+
<button type="button" className="btn-close" onClick={() => setShowDeleteModal(false)} />
152+
</div>
153+
<div className="modal-body">
154+
<p>Delete image {deleteTarget.internal ? `"${deleteTarget.name}"` : 'external'}?</p>
155+
</div>
156+
<div className="modal-footer">
157+
<button type="button" className="btn btn-secondary" onClick={() => setShowDeleteModal(false)}>Cancel</button>
158+
<button type="button" className="btn btn-danger" onClick={confirmDelete}>Delete</button>
159+
</div>
160+
</div>
161+
</div>
162+
</div>
163+
)}
92164
</div>
93165
);
94166
return (
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Trash } from 'react-bootstrap-icons';
2+
3+
function ImagesWithDelete({ id, images, setDeleteTarget, setShowDeleteModal }) {
4+
return (
5+
<>
6+
{images.map(({ src, alt }) => (
7+
<figure
8+
key={src + alt}
9+
className="has-trash-overlay"
10+
style={{ position: 'relative', display: 'inline-block' }}
11+
>
12+
<img
13+
src={src}
14+
alt={alt}
15+
className="img-fluid rounded editable-image"
16+
/>
17+
<button
18+
className="trash-overlay"
19+
type="button"
20+
aria-label={`Delete image ${alt || src}`}
21+
title={`Delete image ${alt || src}`}
22+
onClick={e => {
23+
e.stopPropagation();
24+
const internal = src.includes(`/${id}/`);
25+
const name = internal
26+
? decodeURIComponent(src.split(`${id}/`)[1])
27+
: src;
28+
setDeleteTarget({ src, alt, internal, name });
29+
setShowDeleteModal(true);
30+
}}
31+
>
32+
<Trash />
33+
</button>
34+
</figure>
35+
))}
36+
</>
37+
);
38+
}
39+
40+
export default ImagesWithDelete;

frontend/src/hyperglosae.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ function Hyperglosae(logger) {
7070
};
7171
});
7272

73+
this.deleteAttachment = (id, attachmentName, callback) =>
74+
this.getDocumentMetadata(id).then(headRes => {
75+
fetch(`${service}/${id}/${encodeURIComponent(attachmentName)}`, {
76+
method: 'DELETE',
77+
headers: {
78+
'If-Match': headRes.headers.get('ETag'),
79+
'Accept': 'application/json'
80+
}
81+
}).then(response => callback(response));
82+
});
83+
7384
this.getSession = () =>
7485
fetch(`${service}/_session`)
7586
.then(x => x.json())

frontend/src/styles/EditableText.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,34 @@
1212
border-color: black;
1313
}
1414

15+
.trash-overlay {
16+
position: absolute;
17+
bottom: 10px;
18+
right: 10px;
19+
background-color: rgba(0, 0, 0, 0.6);
20+
width: 24px;
21+
height: 24px;
22+
display: flex;
23+
align-items: center;
24+
justify-content: center;
25+
cursor: pointer;
26+
opacity: 0;
27+
transition: opacity 0.2s ease;
28+
z-index: 10;
29+
}
30+
31+
figure.has-trash-overlay:hover .trash-overlay {
32+
opacity: 1;
33+
}
1534

35+
.trash-overlay .bi-trash {
36+
font-size: 22px;
37+
color: white;
38+
display: block;
39+
}
40+
41+
42+
figure:hover .trash-overlay,
43+
.trash-overlay:focus {
44+
opacity: 1;
45+
}

0 commit comments

Comments
 (0)