Skip to content

Commit 2c55c78

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 2c55c78

File tree

3 files changed

+132
-3
lines changed

3 files changed

+132
-3
lines changed

frontend/src/components/EditableText.jsx

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import '../styles/EditableText.css';
2+
import { Trash } from 'react-bootstrap-icons';
23

34
import { useState, useEffect, useCallback } from 'react';
45
import FormattedText from './FormattedText';
@@ -10,6 +11,8 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
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,18 +82,103 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
7982
.catch(console.error);
8083
};
8184

85+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
86+
let images = [];
87+
let Text = text || '';
88+
let match;
89+
while ((match = imageRegex.exec(Text)) !== null) {
90+
images.push({ alt: match[1], src: match[2] });
91+
}
92+
Text = Text.replace(imageRegex, '');
93+
94+
const confirmDelete = () => {
95+
const { src, alt, internal, name } = deleteTarget;
96+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97+
const mdRx = new RegExp(`!\\[${esc(alt)}\\]\\(${esc(src)}\\)`, 'g');
98+
const clean = t => (t || '').replace(mdRx, '').replace(/\n{2,}/g, '\n\n').trim();
99+
if (internal) {
100+
backend.deleteAttachment(id, name, res => {
101+
if (!res.ok) return alert('Error deleting attachment.');
102+
backend.getDocument(id).then(doc => {
103+
const cleaned = clean(doc.text);
104+
backend.putDocument({ ...doc, text: cleaned }).then(r => {
105+
setEditedText(cleaned);
106+
setEditedDocument({ ...doc, text: cleaned, _rev: r.rev });
107+
setLastUpdate(r.rev);
108+
setShowDeleteModal(false);
109+
});
110+
});
111+
});
112+
} else {
113+
const cleaned = clean(editedText);
114+
setEditedText(cleaned);
115+
setEditedDocument(p => ({ ...p, text: cleaned }));
116+
setShowDeleteModal(false);
117+
}
118+
};
119+
82120
if (!beingEdited) return (
83121
<div className="editable content position-relative" title="Edit content...">
84122
<div className="formatted-text" onClick={handleClick}>
85-
<FormattedText {...{setHighlightedText, setSelectedText}}>
86-
{text || '&nbsp;'}
123+
<FormattedText {...{ setHighlightedText, setSelectedText }}>
124+
{Text || '&nbsp;'}
87125
</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+
))}
88156
</div>
89157
<DiscreeteDropdown>
90-
<PictureUploadAction {... {id, backend, handleImageUrl}}/>
158+
<PictureUploadAction {...{id, backend, handleImageUrl}} />
91159
</DiscreeteDropdown>
160+
{showDeleteModal && (
161+
<div className="modal fade show d-block" tabIndex="-1" role="dialog">
162+
<div className="modal-dialog" role="document">
163+
<div className="modal-content">
164+
<div className="modal-header">
165+
<h5 className="modal-title">Confirm deletion</h5>
166+
<button type="button" className="btn-close" onClick={() => setShowDeleteModal(false)} />
167+
</div>
168+
<div className="modal-body">
169+
<p>Delete image {deleteTarget.internal ? `"${deleteTarget.name}"` : 'external'}?</p>
170+
</div>
171+
<div className="modal-footer">
172+
<button type="button" className="btn btn-secondary" onClick={() => setShowDeleteModal(false)}>Cancel</button>
173+
<button type="button" className="btn btn-danger" onClick={confirmDelete}>Delete</button>
174+
</div>
175+
</div>
176+
</div>
177+
</div>
178+
)}
92179
</div>
93180
);
181+
94182
return (
95183
<form>
96184
<textarea className="form-control" type="text" rows="5" autoFocus

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)