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_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated', 'comment_updated_success' => 'Comment updated',
'comment_view' => 'View comment', 'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?', 'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId', '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.', '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 {el} from "../wysiwyg/utils/dom";
import commentIcon from "@icons/comment.svg" 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 { export class PageComment extends Component {
@ -13,6 +20,8 @@ export class PageComment extends Component {
protected deletedText: string; protected deletedText: string;
protected updatedText: string; protected updatedText: string;
protected viewCommentText: string; protected viewCommentText: string;
protected jumpToThreadText: string;
protected closeText: string;
protected wysiwygEditor: any = null; protected wysiwygEditor: any = null;
protected wysiwygLanguage: string; protected wysiwygLanguage: string;
@ -35,6 +44,8 @@ export class PageComment extends Component {
this.deletedText = this.$opts.deletedText; this.deletedText = this.$opts.deletedText;
this.updatedText = this.$opts.updatedText; this.updatedText = this.$opts.updatedText;
this.viewCommentText = this.$opts.viewCommentText; this.viewCommentText = this.$opts.viewCommentText;
this.jumpToThreadText = this.$opts.jumpToThreadText;
this.closeText = this.$opts.closeText;
// Editor reference and text options // Editor reference and text options
this.wysiwygLanguage = this.$opts.wysiwygLanguage; this.wysiwygLanguage = this.$opts.wysiwygLanguage;
@ -130,7 +141,7 @@ export class PageComment extends Component {
await window.$http.delete(`/comment/${this.commentId}`); await window.$http.delete(`/comment/${this.commentId}`);
this.$emit('delete'); this.$emit('delete');
this.container.closest('.comment-branch').remove(); this.container.closest('.comment-branch')?.remove();
window.$events.success(this.deletedText); window.$events.success(this.deletedText);
} }
@ -196,16 +207,22 @@ export class PageComment extends Component {
} }
protected showCommentAtMarker(marker: HTMLElement): void { protected showCommentAtMarker(marker: HTMLElement): void {
// Hide marker and close existing marker windows
if (openMarkerClose) {
openMarkerClose();
}
marker.hidden = true; 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'); const toRemove = readClone.querySelectorAll('.actions, form');
for (const el of toRemove) { for (const el of toRemove) {
el.remove(); el.remove();
} }
const close = el('button', {type: 'button'}, ['x']); const close = el('button', {type: 'button', title: this.closeText});
const jump = el('button', {type: 'button'}, ['Jump to thread']); close.innerHTML = (closeIcon as string);
const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);
const commentWindow = el('div', { const commentWindow = el('div', {
class: 'content-comment-window' class: 'content-comment-window'
@ -214,19 +231,29 @@ export class PageComment extends Component {
class: 'content-comment-window-actions', class: 'content-comment-window-actions',
}, [jump, close]), }, [jump, close]),
el('div', { el('div', {
class: 'content-comment-window-content', class: 'content-comment-window-content comment-container-compact comment-container-super-compact',
}, [readClone]), }, [readClone]),
]); ]);
marker.parentElement.append(commentWindow); marker.parentElement?.append(commentWindow);
// Handle interaction within window
const closeAction = () => { const closeAction = () => {
commentWindow.remove(); commentWindow.remove();
marker.hidden = false; 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', () => { jump.addEventListener('click', () => {
closeAction(); closeAction();
this.container.scrollIntoView({behavior: 'smooth'}); this.container.scrollIntoView({behavior: 'smooth'});
@ -235,7 +262,12 @@ export class PageComment extends Component {
highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
}); });
// TODO - Position wrapper sensibly // Position window within bounds
// TODO - Movement control? 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); height: calc(100% - vars.$m);
} }
.comment-branch .comment-box {
margin-bottom: vars.$m;
}
.comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator { .comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator {
display: none; display: none;
} }
@ -761,6 +765,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
} }
.comment-container-compact .comment-box { .comment-container-compact .comment-box {
margin-bottom: vars.$xs;
.meta { .meta {
font-size: 0.8rem; font-size: 0.8rem;
} }
@ -778,6 +783,28 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
width: vars.$m; 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 { #tag-manager .drag-card {
max-width: 500px; max-width: 500px;
} }

View File

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

View File

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

View File

@ -8,6 +8,8 @@
option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}"
option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
option:page-comment:view-comment-text="{{ trans('entities.comment_view') }}" 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-language="{{ $locale->htmlLang() }}"
option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
id="comment{{$comment->local_id}}" id="comment{{$comment->local_id}}"