<template> <div class="comment-list" :class="stickyComment && 'comment-list--' + stickyComment" :style="{width: constants.gutterWidth + 'px'}"> <comment v-for="(comment, discussionId) in currentFileDiscussionLastComments" :key="discussionId" v-if="comment.discussionId !== currentDiscussionId" :comment="comment" class="comment--last" :class="'comment--discussion-' + discussionId" :style="{top: tops[discussionId] + 'px'}" @click.native="setCurrentDiscussionId(discussionId)"></comment> <div class="comment-list__current-discussion" :style="{top: tops.current + 'px'}"> <comment v-for="(comment, id) in currentDiscussionComments" :key="id" :comment="comment" :class="'comment--' + id"></comment> <new-comment v-if="isCommenting"></new-comment> </div> </div> </template> <script> import { mapState, mapGetters, mapMutations } from 'vuex'; import Comment from './Comment'; import NewComment from './NewComment'; import editorSvc from '../../services/editorSvc'; import utils from '../../services/utils'; export default { components: { Comment, NewComment, }, data: () => ({ tops: {}, }), computed: { ...mapGetters('layout', [ 'constants', 'styles', ]), ...mapState('discussion', [ 'currentDiscussionId', 'isCommenting', 'newCommentText', 'stickyComment', ]), ...mapGetters('discussion', [ 'newDiscussion', 'currentDiscussion', 'currentFileDiscussions', 'currentFileDiscussionLastComments', 'currentDiscussionComments', 'currentDiscussionLastCommentId', ]), updateTopsTrigger() { return utils.serializeObject([ this.styles, this.currentFileDiscussionLastComments, this.currentDiscussionComments, this.currentDiscussionId, this.isCommenting, ]); }, updateStickyTrigger() { return utils.serializeObject([ this.updateTopsTrigger, this.newCommentText, ]); }, }, methods: { ...mapMutations('discussion', [ 'setCurrentDiscussionId', ]), updateTops() { const layoutSettings = this.$store.getters['data/layoutSettings']; const minTop = -2; let minCommentTop = minTop; const getTop = (discussion, commentElt1, commentElt2, isCurrent) => { const firstElt = commentElt1 || commentElt2; const secondElt = commentElt1 && commentElt2; const coordinates = layoutSettings.showEditor ? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end) : editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end)); let commentTop = minTop; if (coordinates) { commentTop = (coordinates.top + coordinates.height) - 80; } let top = commentTop; if (isCurrent) { top -= firstElt.offsetTop + 2; // 2 for top border } if (top < minTop) { commentTop += minTop - top; top = minTop; } if (commentTop < minCommentTop) { top += minCommentTop - commentTop; commentTop = minCommentTop; } minCommentTop = commentTop + firstElt.offsetHeight + 60; if (secondElt) { minCommentTop += secondElt.offsetHeight; } return top; }; // Get the discussion top coordinates const tops = {}; const discussions = this.currentFileDiscussions; Object.entries(discussions) .sort(([, discussion1], [, discussion2]) => discussion1.end - discussion2.end) .forEach(([discussionId, discussion]) => { if (discussion === this.currentDiscussion || discussion === this.newDiscussion) { tops.current = getTop( discussion, this.currentDiscussionLastCommentId && this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`), this.$el.querySelector('.comment--new'), true); } else { tops[discussionId] = getTop(discussion, this.$el.querySelector(`.comment--discussion-${discussionId}`)); } }); this.tops = tops; }, }, mounted() { this.$watch( () => this.updateTopsTrigger, () => this.updateTops(), { immediate: true }); const layoutSettings = this.$store.getters['data/layoutSettings']; this.scrollerElt = layoutSettings.showEditor ? editorSvc.editorElt.parentNode : editorSvc.previewElt.parentNode; this.updateSticky = () => { const commitIfDifferent = (value) => { if (this.$store.state.discussion.stickyComment !== value) { this.$store.commit('discussion/setStickyComment', value); } }; let height = 0; let offsetTop = this.tops.current; const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`); if (lastCommentElt) { height += lastCommentElt.clientHeight; offsetTop += lastCommentElt.offsetTop; } const newCommentElt = this.$el.querySelector('.comment--new'); if (newCommentElt) { height += newCommentElt.clientHeight; } const currentDiscussionElt = document.querySelector('.current-discussion__inner'); const minOffsetTop = this.scrollerElt.scrollTop + 10; const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height - currentDiscussionElt.clientHeight - 10; if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) { commitIfDifferent('bottom'); } else if (offsetTop < minOffsetTop) { commitIfDifferent('top'); } else { commitIfDifferent(null); } }; this.scrollerElt.addEventListener('scroll', this.updateSticky); this.$watch( () => this.updateStickyTrigger, () => this.updateSticky(), { immediate: true }); // Move preview discussions once previewCtxWithDiffs has been calculated if (!editorSvc.previewCtxWithDiffs) { editorSvc.$once('previewCtxWithDiffs', () => { this.updateTops(); this.updateSticky(); }); } }, destroyed() { this.scrollerElt.removeEventListener('scroll', this.updateSticky); }, }; </script> <style lang="scss"> @import '../common/variables.scss'; .comment-list { position: absolute; right: 0; font-size: 15px; } .comment--last, .comment-list__current-discussion { position: absolute; width: 100%; padding-top: 10px; } .comment-list__current-discussion { border-top: 2px solid; border-bottom: 2px solid; .comment-list--top & { border-bottom-color: transparent; } .comment-list--bottom & { border-top-color: transparent; } } /* use div selector to avoid collision with Prism */ div.comment { padding: 5px 10px 10px; } .comment--last { opacity: 0.33; cursor: pointer; * { pointer-events: none; } &:hover, &.comment--hover { opacity: 0.5; } } .comment__header { font-size: 0.75em; padding-bottom: 0.25em; } .comment__user-image { height: 20px; width: 20px; border-radius: $border-radius-base; overflow: hidden; margin-right: 5px; .comment:hover & { display: none; .sticky-comment & { display: block; } } .comment--new:hover &, .comment--last:hover & { display: block; } } .comment__remove-button { height: 20px; width: 20px; padding: 1px; color: rgba(0, 0, 0, 0.33); margin-right: 5px; display: none; &:active, &:focus, &:hover { color: rgba(0, 0, 0, 0.5); } .comment:hover & { display: block; .sticky-comment & { display: none; } } .comment--last:hover & { display: none; } } .comment__created { opacity: 0.5; } .comment__buttons { padding: 10px 5px 0; } .comment__button { padding: 0 8px; line-height: 28px; height: 28px; } .comment__text { position: relative; &::before { content: ''; position: absolute; bottom: -8px; right: 0; border-top: 8px solid $editor-background-light; border-left: 8px solid transparent; .app--dark & { border-top-color: $editor-background-dark; } } h1, h2, h3, h4, h5, h6 { font-size: inherit; } h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol, dl { margin: 0.25em 0; } pre { font-variant-ligatures: no-common-ligatures; white-space: pre-wrap; word-break: break-word; word-wrap: break-word; caret-color: #000; } img { max-width: 100%; } .table-wrapper { max-width: 100%; overflow: auto; } } .comment__text-inner { min-height: 37px; max-height: 200px; overflow: auto; padding: 1px 8px; background-color: $editor-background-light; border: 1px solid transparent; border-radius: $border-radius-base; border-bottom-right-radius: 0; .app--dark & { background-color: $editor-background-dark; } .markdown-highlighting { padding: 5px 0; margin: 0; } } </style>