Comments: Styled content comments & improved interaction

This commit is contained in:
Dan Brown 2025-04-24 13:21:23 +01:00
parent 5bfba281fc
commit f656a82fe7
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 98 additions and 15 deletions

View File

@ -403,6 +403,7 @@ return [
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',

View File

@ -4,6 +4,13 @@ import {buildForInput} from '../wysiwyg-tinymce/config';
import {el} from "../wysiwyg/utils/dom";
import commentIcon from "@icons/comment.svg"
import closeIcon from "@icons/close.svg"
/**
* Track the close function for the current open marker so it can be closed
* when another is opened so we only show one marker comment thread at one time.
*/
let openMarkerClose: Function|null = null;
export class PageComment extends Component {
@ -13,6 +20,8 @@ export class PageComment extends Component {
protected deletedText: string;
protected updatedText: string;
protected viewCommentText: string;
protected jumpToThreadText: string;
protected closeText: string;
protected wysiwygEditor: any = null;
protected wysiwygLanguage: string;
@ -35,6 +44,8 @@ export class PageComment extends Component {
this.deletedText = this.$opts.deletedText;
this.updatedText = this.$opts.updatedText;
this.viewCommentText = this.$opts.viewCommentText;
this.jumpToThreadText = this.$opts.jumpToThreadText;
this.closeText = this.$opts.closeText;
// Editor reference and text options
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
@ -130,7 +141,7 @@ export class PageComment extends Component {
await window.$http.delete(`/comment/${this.commentId}`);
this.$emit('delete');
this.container.closest('.comment-branch').remove();
this.container.closest('.comment-branch')?.remove();
window.$events.success(this.deletedText);
}
@ -196,16 +207,22 @@ export class PageComment extends Component {
}
protected showCommentAtMarker(marker: HTMLElement): void {
// Hide marker and close existing marker windows
if (openMarkerClose) {
openMarkerClose();
}
marker.hidden = true;
const readClone = this.container.closest('.comment-branch').cloneNode(true) as HTMLElement;
// Build comment window
const readClone = (this.container.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement;
const toRemove = readClone.querySelectorAll('.actions, form');
for (const el of toRemove) {
el.remove();
}
const close = el('button', {type: 'button'}, ['x']);
const jump = el('button', {type: 'button'}, ['Jump to thread']);
const close = el('button', {type: 'button', title: this.closeText});
close.innerHTML = (closeIcon as string);
const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);
const commentWindow = el('div', {
class: 'content-comment-window'
@ -214,19 +231,29 @@ export class PageComment extends Component {
class: 'content-comment-window-actions',
}, [jump, close]),
el('div', {
class: 'content-comment-window-content',
class: 'content-comment-window-content comment-container-compact comment-container-super-compact',
}, [readClone]),
]);
marker.parentElement.append(commentWindow);
marker.parentElement?.append(commentWindow);
// Handle interaction within window
const closeAction = () => {
commentWindow.remove();
marker.hidden = false;
window.removeEventListener('click', windowCloseAction);
openMarkerClose = null;
};
close.addEventListener('click', closeAction.bind(this));
const windowCloseAction = (event: MouseEvent) => {
if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) {
closeAction();
}
};
window.addEventListener('click', windowCloseAction);
openMarkerClose = closeAction;
close.addEventListener('click', closeAction.bind(this));
jump.addEventListener('click', () => {
closeAction();
this.container.scrollIntoView({behavior: 'smooth'});
@ -235,7 +262,12 @@ export class PageComment extends Component {
highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
});
// TODO - Position wrapper sensibly
// TODO - Movement control?
// Position window within bounds
const commentWindowBounds = commentWindow.getBoundingClientRect();
const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect();
if (contentBounds && commentWindowBounds.right > contentBounds.right) {
const diff = commentWindowBounds.right - contentBounds.right;
commentWindow.style.left = `-${diff}px`;
}
}
}

View File

@ -746,6 +746,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: calc(100% - vars.$m);
}
.comment-branch .comment-box {
margin-bottom: vars.$m;
}
.comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {
display: none;
}
@ -761,6 +765,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
.comment-container-compact .comment-box {
margin-bottom: vars.$xs;
.meta {
font-size: 0.8rem;
}
@ -778,6 +783,28 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
width: vars.$m;
}
.comment-container-super-compact .comment-box {
.meta {
font-size: 12px;
}
.avatar {
width: 18px;
margin-inline-end: 2px !important;
}
.content {
padding: vars.$xxs vars.$s;
line-height: 1.2;
}
.content p {
font-size: 12px;
}
}
.comment-container-super-compact .comment-thread-indicator {
width: (vars.$xs + 3px);
margin-inline-start: 3px;
}
#tag-manager .drag-card {
max-width: 500px;
}

View File

@ -242,12 +242,13 @@ body.tox-fullscreen, body.markdown-fullscreen {
.content-comment-window {
font-size: vars.$fs-m;
line-height: 1.4;
position: relative;
z-index: 90;
position: absolute;
top: calc(100% + 3px);
left: 0;
z-index: 92;
pointer-events: all;
min-width: min(340px, 80vw);
background-color: #FFF;
//border: 1px solid var(--color-primary);
box-shadow: vars.$bs-hover;
border-radius: 4px;
overflow: hidden;
@ -258,9 +259,24 @@ body.tox-fullscreen, body.markdown-fullscreen {
display: flex;
align-items: center;
justify-content: end;
gap: vars.$xs;
button {
color: #FFF;
font-size: 12px;
padding: vars.$xs;
line-height: 1;
cursor: pointer;
}
button[data-action="jump"] {
text-decoration: underline;
}
svg {
fill: currentColor;
width: 12px;
}
}
.content-comment-window-content {
padding: vars.$xs;
padding: vars.$xs vars.$s vars.$xs vars.$xs;
max-height: 200px;
overflow-y: scroll;
}
@ -280,11 +296,16 @@ body.tox-fullscreen, body.markdown-fullscreen {
color: #FFF;
cursor: pointer;
z-index: 90;
transform: scale(1);
transition: transform ease-in-out 120ms;
svg {
fill: #FFF;
width: 80%;
}
}
.page-content [id^="bkmrk-"]:hover .content-comment-marker {
transform: scale(1.15);
}
// Page editor sidebar toolbox
.floating-toolbox {

View File

@ -1,5 +1,5 @@
<div class="comment-branch">
<div class="mb-m">
<div>
@include('comments.comment', ['comment' => $branch['comment']])
</div>
<div class="flex-container-row">

View File

@ -8,6 +8,8 @@
option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}"
option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
option:page-comment:view-comment-text="{{ trans('entities.comment_view') }}"
option:page-comment:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}"
option:page-comment:close-text="{{ trans('common.close') }}"
option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}"
option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
id="comment{{$comment->local_id}}"