From 597670f27dca514e9c4f5f4f5d4288c3e53e5a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Weis?= Date: Fri, 7 Jun 2024 17:58:53 +0200 Subject: [PATCH] FEATURE: Upload a picture into a document (see #153). Co-Authored-By: Martin Gandon <105852593+nitram35@users.noreply.github.com> --- frontend/src/components/EditableText.js | 29 +++++++++-- frontend/src/components/PassageMarginMenu.js | 51 ++++++++++++++++++++ frontend/src/hyperglosae.js | 26 ++++++++++ frontend/src/routes/Lectern.js | 2 +- frontend/src/styles/EditableText.css | 10 +++- frontend/src/styles/PassageMarginMenu.css | 18 +++++++ 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/PassageMarginMenu.js create mode 100644 frontend/src/styles/PassageMarginMenu.css diff --git a/frontend/src/components/EditableText.js b/frontend/src/components/EditableText.js index 5cbb473f..b6916d3d 100644 --- a/frontend/src/components/EditableText.js +++ b/frontend/src/components/EditableText.js @@ -2,6 +2,7 @@ import '../styles/EditableText.css'; import { useState, useEffect } from 'react'; import FormattedText from './FormattedText'; +import PassageMarginMenu from './PassageMarginMenu'; import {v4 as uuid} from 'uuid'; function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, backend, setLastUpdate}) { @@ -42,7 +43,6 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, }, [fragment]); let handleClick = () => { - setHighlightedText(''); setBeingEdited(true); updateEditedDocument() .then((x) => { @@ -50,6 +50,22 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, }); }; + let handleImageUrl = (imageTag) => { + backend.getDocument(id).then((editedDocument) => { + let parsedText = parsePassage(editedDocument.text) + imageTag; + let text = (rubric) + ? editedDocument.text.replace(PASSAGE, `{${rubric}} ${parsedText}`) + : parsedText; + backend.putDocument({ ...editedDocument, text }) + .then(x => { + setEditedText(parsedText); + return x; + }) + .then(x => setLastUpdate(x.rev)) + .catch(console.error); + }); + }; + let handleChange = (event) => { setEditedText(event.target.value); }; @@ -70,10 +86,13 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, }; if (!beingEdited) return ( -
- - {editedText || text} - +
+
+ + {editedText || text} + +
+
); return ( diff --git a/frontend/src/components/PassageMarginMenu.js b/frontend/src/components/PassageMarginMenu.js new file mode 100644 index 00000000..d86ab47c --- /dev/null +++ b/frontend/src/components/PassageMarginMenu.js @@ -0,0 +1,51 @@ +import '../styles/PassageMarginMenu.css'; + +import { Dropdown } from 'react-bootstrap'; +import {forwardRef, useRef} from 'react'; +import { ThreeDotsVertical } from 'react-bootstrap-icons'; + +function PassageMarginMenu ({ id, backend, handleImageUrl }) { + const fileInputRef = useRef(null); + + const handleClick = () => { + fileInputRef.current.click(); + }; + + const handleFileChange = (event) => { + const file = event.target.files[0]; + if (file) backend.putAttachment(id, file, (response) => { + handleImageUrl(`![](${response.url})`); + }); + }; + + return ( + <> + + + + Add an image + + + + + ); +} + +const BlockMenuButton = forwardRef(({ children, onClick }, ref) => ( + { + e.preventDefault(); + onClick(e); + }} + ref={ref} class="editable-button"> + {children} + +)); + +export default PassageMarginMenu; diff --git a/frontend/src/hyperglosae.js b/frontend/src/hyperglosae.js index 27839c43..529a8cbe 100644 --- a/frontend/src/hyperglosae.js +++ b/frontend/src/hyperglosae.js @@ -46,6 +46,32 @@ function Hyperglosae(logger) { return x; }); + this.getDocumentMetadata = (id) => + fetch(`${service}/${id}`, { + method: 'HEAD', + headers: basicAuthentication({ force: false }) + }); + + this.putAttachment = (id, attachment, callback) => + this.getDocumentMetadata(id).then(x => { + const reader = new FileReader(); + reader.readAsArrayBuffer(attachment); + reader.onload = () => { + const arrayBuffer = reader.result; + + fetch(`${service}/${id}/${attachment.name}`, { + method: 'PUT', + headers: { + ...basicAuthentication({ force: false }), + // ETag is the header that carries the current rev. + 'If-Match': x.headers.get('ETag'), + 'Content-Type': attachment.type + }, + body: arrayBuffer + }).then(response => callback(response)); + }; + }); + this.authenticate = ({name, password}) => { this.credentials = {name, password}; return fetch(`${service}`, { diff --git a/frontend/src/routes/Lectern.js b/frontend/src/routes/Lectern.js index b21208bd..4c14e84c 100644 --- a/frontend/src/routes/Lectern.js +++ b/frontend/src/routes/Lectern.js @@ -74,7 +74,7 @@ function Lectern({backend}) { if (isPartOf === id) { part.source.push(text); } else { - part.scholia = [...part.scholia || [], {id: x.id, text, isPartOf, rubric: x.key[1]}]; + part.scholia = [...part.scholia || [], {id: x.id, rev: x.rev, text, isPartOf, rubric: x.key[1]}]; } if (i === length - 1) { return [...whole, part]; diff --git a/frontend/src/styles/EditableText.css b/frontend/src/styles/EditableText.css index 384a8ea8..e6e90a01 100644 --- a/frontend/src/styles/EditableText.css +++ b/frontend/src/styles/EditableText.css @@ -1,4 +1,12 @@ -.editable:hover { +.content { + display: flex; +} + +.formatted-text { + flex: 1; +} + +.formatted-text:hover { background: lightgray; border-color: black; } diff --git a/frontend/src/styles/PassageMarginMenu.css b/frontend/src/styles/PassageMarginMenu.css new file mode 100644 index 00000000..86306691 --- /dev/null +++ b/frontend/src/styles/PassageMarginMenu.css @@ -0,0 +1,18 @@ +.editable-button { + visibility: hidden; + margin-top: 0.25rem; + color: crimson; +} + +.editable:hover .editable-button { + visibility: visible; + cursor: pointer; +} + +#block-actions .dropdown-item:hover { + background-color: lightgray; +} + +#block-actions .dropdown-item:active { + color: black; +}