332 lines
9.2 KiB
Vue
332 lines
9.2 KiB
Vue
<template>
|
|
<aside id="comments-container" aria-labelledby="comments-label">
|
|
<h2 id="comments-label" class="sr-only">Commentaires</h2>
|
|
<div
|
|
class="comments | flow"
|
|
:class="{ empty: !comments || comments.length === 0 }"
|
|
>
|
|
<template v-if="comments">
|
|
<template v-if="!openedComment">
|
|
<Comment
|
|
v-for="(comment, commentIndex) in sortedComments"
|
|
:comment="comment"
|
|
:commentIndex="comments.length - commentIndex"
|
|
:key="comment.id"
|
|
@click="openComment(comment)"
|
|
@update:file="changeFile"
|
|
@close:comment="closeComment"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<button
|
|
class="btn | justify-start w-full | bg-white-10 text-white | px-8"
|
|
data-icon="chevron-single-left"
|
|
@click="
|
|
openedComment = null;
|
|
isAddOpen = false;
|
|
"
|
|
>
|
|
<span>Retour à la liste</span>
|
|
</button>
|
|
<Comment
|
|
:comment="openedComment"
|
|
data-opened="true"
|
|
@update:file="changeFile"
|
|
@close:comment="closeComment"
|
|
/>
|
|
<div v-if="sortedReplies.length > 0" class="replies | flow">
|
|
<Comment
|
|
v-for="(reply, commentIndex) in sortedReplies"
|
|
:comment="reply"
|
|
:commentIndex="sortedReplies.length - commentIndex"
|
|
:key="reply.id"
|
|
@update:file="changeFile"
|
|
@close:comment="closeComment"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
<button
|
|
v-if="!openedComment && !isAddOpen"
|
|
id="create-comment"
|
|
class="btn btn--white-20 | w-full"
|
|
@click="toggleCommentPositionMode(true)"
|
|
>
|
|
Ajouter un commentaire
|
|
</button>
|
|
<button
|
|
v-else-if="openedComment && !isAddOpen"
|
|
id="reply-comment"
|
|
class="btn btn--white-20 | justify-start w-full | text-white-50"
|
|
@click="isAddOpen = true"
|
|
>
|
|
Répondre…
|
|
</button>
|
|
<!-- TODO: afficher #new-comment une fois le bouton Ajouter un commentaire cliqué -->
|
|
<div
|
|
v-if="waitingForCommentPosition"
|
|
id="new-comment"
|
|
class="bg-primary | text-sm text-white | rounded-lg | p-12"
|
|
>
|
|
<p class="flex justify-start | mb-12" data-icon="comment">
|
|
<strong>Nouveau commentaire</strong>
|
|
</p>
|
|
<p>
|
|
Dans la zone du contenu, cliquez où vous souhaitez positionner le
|
|
commentaire
|
|
</p>
|
|
</div>
|
|
<form
|
|
v-if="isAddOpen"
|
|
action=""
|
|
method="post"
|
|
class="flow | p-12 | rounded-xl"
|
|
@submit="handleSubmit"
|
|
>
|
|
<label class="sr-only" for="comment">Votre commentaire</label>
|
|
<textarea
|
|
name="comment"
|
|
id="comment"
|
|
placeholder="Ajouter un commentaire…"
|
|
rows="5"
|
|
class="text-sm | rounded-lg bg-black p-12"
|
|
v-model="draftComment.text"
|
|
></textarea>
|
|
<footer class="flex">
|
|
<input type="submit" class="btn btn--tranparent" />
|
|
<button class="btn btn--white-10" @click="isAddOpen = false">
|
|
Annuler
|
|
</button>
|
|
</footer>
|
|
</form>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup>
|
|
import dayjs from 'dayjs';
|
|
import 'dayjs/locale/fr';
|
|
import uniqid from 'uniqid';
|
|
import { watch, ref, computed } from 'vue';
|
|
import { useUserStore } from '../../stores/user';
|
|
import { usePageStore } from '../../stores/page';
|
|
import { useApiStore } from '../../stores/api';
|
|
import { useDialogStore } from '../../stores/dialog';
|
|
import Comment from './Comment.vue';
|
|
import { storeToRefs } from 'pinia';
|
|
import { useRoute } from 'vue-router';
|
|
|
|
dayjs.locale('fr');
|
|
|
|
const { user } = useUserStore();
|
|
const { page } = usePageStore();
|
|
const dialog = useDialogStore();
|
|
const { comments, openedFile, openedComment, draftComment, activeTracks } =
|
|
storeToRefs(useDialogStore());
|
|
const api = useApiStore();
|
|
|
|
const isAddOpen = ref(false);
|
|
const route = useRoute();
|
|
const waitingForCommentPosition = ref(false);
|
|
|
|
const sortedComments = computed(() => {
|
|
return [...comments.value].reverse();
|
|
});
|
|
const sortedReplies = ref(null);
|
|
|
|
watch(openedComment, (newVal) => {
|
|
sortedReplies.value = newVal ? newVal.replies.slice().reverse() : null;
|
|
|
|
if (!newVal) {
|
|
unhighlightAllComments();
|
|
}
|
|
});
|
|
|
|
watch(isAddOpen, (newVal) => {
|
|
if (newVal) {
|
|
setTimeout(() => {
|
|
document.querySelector('textarea#comment').focus();
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
const viewContainer =
|
|
openedFile.value?.type === 'document'
|
|
? document.querySelector('.vpv-pages-inner-container')
|
|
: document.querySelector('.track');
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
if (
|
|
isAddOpen.value &&
|
|
event.key === 'Enter' &&
|
|
(event.metaKey || event.ctrlKey)
|
|
) {
|
|
handleSubmit();
|
|
}
|
|
});
|
|
|
|
// Functions
|
|
function unhighlightAllComments() {
|
|
document.querySelectorAll('.comment-marker.big').forEach((commentNode) => {
|
|
commentNode.classList.remove('big');
|
|
commentNode.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
function handleSubmit(event = null) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
const date = dayjs().format();
|
|
const newComment = {
|
|
fileName: openedFile.value ? openedFile.value.name : false,
|
|
userUuid: user.uuid,
|
|
text: draftComment.value.text,
|
|
date,
|
|
position: {
|
|
pageIndex: openedComment.value
|
|
? openedComment.value.position.pageIndex
|
|
: draftComment.value.position.pageIndex,
|
|
x: openedComment.value ? false : draftComment.value.position.x,
|
|
y: openedComment.value ? false : draftComment.value.position.y,
|
|
},
|
|
id: uniqid(),
|
|
};
|
|
if (openedComment.value) {
|
|
replyComment(newComment);
|
|
} else {
|
|
addComment(newComment);
|
|
}
|
|
}
|
|
|
|
async function replyComment(newComment) {
|
|
newComment.parentId = openedComment.value.id;
|
|
const matchFileParentUri = openedFile.value.url.match(
|
|
/projects\/.*?(?=\/[^/]+\/[^/]+$)/
|
|
);
|
|
newComment.fileParentUri = matchFileParentUri ? matchFileParentUri[0] : null;
|
|
const newFile = await api.replyComment(newComment);
|
|
resetDraftComment();
|
|
isAddOpen.value = false;
|
|
dialog.updateFile(newFile);
|
|
openedComment.value = newFile.comments.find(
|
|
(item) => item.id === openedComment.value.id
|
|
);
|
|
}
|
|
|
|
async function addComment(newComment) {
|
|
const matchFileParentUri = openedFile.value.url.match(
|
|
/projects\/.*?(?=\/[^/]+\/[^/]+$)/
|
|
);
|
|
newComment.fileParentUri = matchFileParentUri ? matchFileParentUri[0] : null;
|
|
const newFile = await api.addComment(newComment);
|
|
resetDraftComment();
|
|
isAddOpen.value = false;
|
|
if (!newFile) return;
|
|
dialog.updateFile(newFile);
|
|
if (activeTracks.value) {
|
|
activeTracks.value = activeTracks.value.map((track) => {
|
|
if (track.files) {
|
|
track.files = track.files.map((file) =>
|
|
file.uuid === newFile.uuid ? newFile : file
|
|
);
|
|
}
|
|
return track;
|
|
});
|
|
}
|
|
}
|
|
|
|
function resetDraftComment() {
|
|
draftComment.value.text = '';
|
|
draftComment.value.position = null;
|
|
draftComment.value.padeIndex = null;
|
|
}
|
|
|
|
function closeComment() {
|
|
openedComment.value = null;
|
|
}
|
|
|
|
function toggleCommentPositionMode(enable) {
|
|
if (enable) {
|
|
waitingForCommentPosition.value = true;
|
|
viewContainer.classList.add('waiting-comment');
|
|
viewContainer.addEventListener('click', handleCommentPositionClick);
|
|
} else {
|
|
waitingForCommentPosition.value = false;
|
|
viewContainer.classList.remove('waiting-comment');
|
|
viewContainer.removeEventListener('click', handleCommentPositionClick);
|
|
}
|
|
}
|
|
|
|
function handleCommentPositionClick(event) {
|
|
if (openedFile.value.type === 'document') {
|
|
prepareDraftCommentInPdf(event);
|
|
} else {
|
|
prepareDraftCommentInImage(event);
|
|
}
|
|
isAddOpen.value = true;
|
|
toggleCommentPositionMode(false);
|
|
}
|
|
|
|
function prepareDraftCommentInPdf(event) {
|
|
const pageContainer = event.target.closest('.page-inner-container');
|
|
if (!pageContainer || !viewContainer) return;
|
|
|
|
const pageLabel = pageContainer
|
|
.closest('.vpv-page-inner-container')
|
|
.getAttribute('aria-label');
|
|
const pageIndex = pageLabel.charAt(pageLabel.length - 1);
|
|
|
|
const viewRect = viewContainer.getBoundingClientRect();
|
|
const pageRect = pageContainer.getBoundingClientRect();
|
|
const pageScroll = viewRect.top - pageRect.top;
|
|
|
|
const mouseTop = event.clientY;
|
|
const y = mouseTop - viewRect.top + pageScroll;
|
|
const x = event.clientX - pageRect.left;
|
|
|
|
const relativeX = (x / pageRect.width) * 100;
|
|
const relativeY = (y / pageRect.height) * 100;
|
|
|
|
console.log(pageIndex);
|
|
|
|
draftComment.value.position = {
|
|
x: relativeX,
|
|
y: relativeY,
|
|
pageIndex: parseInt(pageIndex),
|
|
};
|
|
}
|
|
|
|
function prepareDraftCommentInImage(event) {
|
|
if (!viewContainer) return;
|
|
|
|
const imageRect = viewContainer.getBoundingClientRect();
|
|
|
|
const mouseTop = event.clientY;
|
|
const mouseLeft = event.clientX;
|
|
|
|
const relativeX = ((mouseLeft - imageRect.left) / imageRect.width) * 100;
|
|
const relativeY = ((mouseTop - imageRect.top) / imageRect.height) * 100;
|
|
|
|
draftComment.value.position = {
|
|
x: relativeX,
|
|
y: relativeY,
|
|
};
|
|
}
|
|
|
|
function openComment(comment) {
|
|
if (comment.isEditMode) return;
|
|
|
|
openedComment.value = comment;
|
|
|
|
if (activeTracks.value?.length === 1) {
|
|
showCorrespondingView();
|
|
}
|
|
}
|
|
|
|
function showCorrespondingView() {
|
|
openedFile.value = activeTracks.value[0].files.find(
|
|
(file) => file.uuid === openedComment.value.location.file.uuid
|
|
);
|
|
}
|
|
</script>
|