From 5c92b72fdd419ccb6f77bfdf0a1cb1358c51a9d8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 30 Jan 2024 14:27:09 +0000 Subject: [PATCH 1/6] Comments: Added input wysiwyg for creating/updating comments Not supporting old content, existing HTML or updating yet. --- app/Activity/Tools/CommentTree.php | 11 ++++++++ resources/js/components/page-comment.js | 27 ++++++++++++++++--- resources/js/components/page-comments.js | 30 ++++++++++++++++++--- resources/js/components/wysiwyg-input.js | 7 ++--- resources/sass/_tinymce.scss | 7 +++++ resources/views/comments/comment.blade.php | 2 ++ resources/views/comments/comments.blade.php | 10 ++++++- resources/views/layouts/base.blade.php | 3 +++ 8 files changed, 85 insertions(+), 12 deletions(-) diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php index 3303add39..16f6804ea 100644 --- a/app/Activity/Tools/CommentTree.php +++ b/app/Activity/Tools/CommentTree.php @@ -41,6 +41,17 @@ class CommentTree return $this->tree; } + public function canUpdateAny(): bool + { + foreach ($this->comments as $comment) { + if (userCan('comment-update', $comment)) { + return true; + } + } + + return false; + } + /** * @param Comment[] $comments */ diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index 8284d7f20..dc6ca8264 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -1,5 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; +import {buildForInput} from "../wysiwyg/config"; export class PageComment extends Component { @@ -11,7 +12,12 @@ export class PageComment extends Component { this.deletedText = this.$opts.deletedText; this.updatedText = this.$opts.updatedText; - // Element References + // Editor reference and text options + this.wysiwygEditor = null; + this.wysiwygLanguage = this.$opts.wysiwygLanguage; + this.wysiwygTextDirection = this.$opts.wysiwygTextDirection; + + // Element references this.container = this.$el; this.contentContainer = this.$refs.contentContainer; this.form = this.$refs.form; @@ -50,8 +56,23 @@ export class PageComment extends Component { startEdit() { this.toggleEditMode(true); - const lineCount = this.$refs.input.value.split('\n').length; - this.$refs.input.style.height = `${(lineCount * 20) + 40}px`; + + if (this.wysiwygEditor) { + return; + } + + const config = buildForInput({ + language: this.wysiwygLanguage, + containerElement: this.input, + darkMode: document.documentElement.classList.contains('dark-mode'), + textDirection: this.wysiwygTextDirection, + translations: {}, + translationMap: window.editor_translations, + }); + + window.tinymce.init(config).then(editors => { + this.wysiwygEditor = editors[0]; + }); } async update(event) { diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index e2911afc6..ebcc95f07 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -1,5 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; +import {buildForInput} from "../wysiwyg/config"; export class PageComments extends Component { @@ -21,6 +22,11 @@ export class PageComments extends Component { this.hideFormButton = this.$refs.hideFormButton; this.removeReplyToButton = this.$refs.removeReplyToButton; + // WYSIWYG options + this.wysiwygLanguage = this.$opts.wysiwygLanguage; + this.wysiwygTextDirection = this.$opts.wysiwygTextDirection; + this.wysiwygEditor = null; + // Translations this.createdText = this.$opts.createdText; this.countText = this.$opts.countText; @@ -96,9 +102,7 @@ export class PageComments extends Component { this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); - setTimeout(() => { - this.formInput.focus(); - }, 100); + this.loadEditor(); } hideForm() { @@ -112,6 +116,26 @@ export class PageComments extends Component { this.addButtonContainer.toggleAttribute('hidden', false); } + loadEditor() { + if (this.wysiwygEditor) { + return; + } + + const config = buildForInput({ + language: this.wysiwygLanguage, + containerElement: this.formInput, + darkMode: document.documentElement.classList.contains('dark-mode'), + textDirection: this.wysiwygTextDirection, + translations: {}, + translationMap: window.editor_translations, + }); + + window.tinymce.init(config).then(editors => { + this.wysiwygEditor = editors[0]; + this.wysiwygEditor.focus(); + }); + } + getCommentCount() { return this.container.querySelectorAll('[component="page-comment"]').length; } diff --git a/resources/js/components/wysiwyg-input.js b/resources/js/components/wysiwyg-input.js index 88c06a334..ad964aed2 100644 --- a/resources/js/components/wysiwyg-input.js +++ b/resources/js/components/wysiwyg-input.js @@ -10,11 +10,8 @@ export class WysiwygInput extends Component { language: this.$opts.language, containerElement: this.elem, darkMode: document.documentElement.classList.contains('dark-mode'), - textDirection: this.textDirection, - translations: { - imageUploadErrorText: this.$opts.imageUploadErrorText, - serverUploadLimitText: this.$opts.serverUploadLimitText, - }, + textDirection: this.$opts.textDirection, + translations: {}, translationMap: window.editor_translations, }); diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss index c4336da7c..fb5ea7e6f 100644 --- a/resources/sass/_tinymce.scss +++ b/resources/sass/_tinymce.scss @@ -30,6 +30,13 @@ display: block; } +.wysiwyg-input.mce-content-body:before { + padding: 1rem; + top: 4px; + font-style: italic; + color: rgba(34,47,62,.5) +} + // Default styles for our custom root nodes .page-content.mce-content-body doc-root { display: block; diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 1cb709160..4340cfdf5 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -4,6 +4,8 @@ option:page-comment:comment-parent-id="{{ $comment->parent_id }}" option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" + option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}" + option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" id="comment{{$comment->local_id}}" class="comment-box">
diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 26d286290..2c314864b 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -2,6 +2,8 @@ option:page-comments:page-id="{{ $page->id }}" option:page-comments:created-text="{{ trans('entities.comment_created_success') }}" option:page-comments:count-text="{{ trans('entities.comment_count') }}" + option:page-comments:wysiwyg-language="{{ $locale->htmlLang() }}" + option:page-comments:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" class="comments-list" aria-label="{{ trans('entities.comments') }}"> @@ -24,7 +26,6 @@ @if(userCan('comment-create-all')) @include('comments.create') - @if (!$commentTree->empty())
@yield('bottom') + + @if($cspNonce ?? false) @endif @yield('scripts') + @stack('post-app-scripts') @include('layouts.parts.base-body-end') From adf0baebb9ffc61cc944c0572ec6dbb12a5b41a0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 30 Jan 2024 15:16:58 +0000 Subject: [PATCH 2/6] Comments: Added back-end HTML support, fixed editor focus Also fixed handling of editors when moved in DOM, to properly remove then re-init before & after move to avoid issues. --- app/Activity/CommentRepo.php | 26 ++++--------------- .../Controllers/CommentController.php | 17 +++++++----- resources/js/components/page-comment.js | 6 +++-- resources/js/components/page-comments.js | 17 +++++++++--- resources/views/comments/comment.blade.php | 2 +- resources/views/comments/create.blade.php | 2 +- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index ce2950e4d..3336e17e9 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -5,7 +5,7 @@ namespace BookStack\Activity; use BookStack\Activity\Models\Comment; use BookStack\Entities\Models\Entity; use BookStack\Facades\Activity as ActivityService; -use League\CommonMark\CommonMarkConverter; +use BookStack\Util\HtmlDescriptionFilter; class CommentRepo { @@ -20,13 +20,12 @@ class CommentRepo /** * Create a new comment on an entity. */ - public function create(Entity $entity, string $text, ?int $parent_id): Comment + public function create(Entity $entity, string $html, ?int $parent_id): Comment { $userId = user()->id; $comment = new Comment(); - $comment->text = $text; - $comment->html = $this->commentToHtml($text); + $comment->html = HtmlDescriptionFilter::filterFromString($html); $comment->created_by = $userId; $comment->updated_by = $userId; $comment->local_id = $this->getNextLocalId($entity); @@ -42,11 +41,10 @@ class CommentRepo /** * Update an existing comment. */ - public function update(Comment $comment, string $text): Comment + public function update(Comment $comment, string $html): Comment { $comment->updated_by = user()->id; - $comment->text = $text; - $comment->html = $this->commentToHtml($text); + $comment->html = HtmlDescriptionFilter::filterFromString($html); $comment->save(); ActivityService::add(ActivityType::COMMENT_UPDATE, $comment); @@ -64,20 +62,6 @@ class CommentRepo ActivityService::add(ActivityType::COMMENT_DELETE, $comment); } - /** - * Convert the given comment Markdown to HTML. - */ - public function commentToHtml(string $commentText): string - { - $converter = new CommonMarkConverter([ - 'html_input' => 'strip', - 'max_nesting_level' => 10, - 'allow_unsafe_links' => false, - ]); - - return $converter->convert($commentText); - } - /** * Get the next local ID relative to the linked entity. */ diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 516bcac75..340524cd0 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -22,8 +22,8 @@ class CommentController extends Controller */ public function savePageComment(Request $request, int $pageId) { - $this->validate($request, [ - 'text' => ['required', 'string'], + $input = $this->validate($request, [ + 'html' => ['required', 'string'], 'parent_id' => ['nullable', 'integer'], ]); @@ -39,7 +39,7 @@ class CommentController extends Controller // Create a new comment. $this->checkPermission('comment-create-all'); - $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id')); + $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null); return view('comments.comment-branch', [ 'readOnly' => false, @@ -57,17 +57,20 @@ class CommentController extends Controller */ public function update(Request $request, int $commentId) { - $this->validate($request, [ - 'text' => ['required', 'string'], + $input = $this->validate($request, [ + 'html' => ['required', 'string'], ]); $comment = $this->commentRepo->getById($commentId); $this->checkOwnablePermission('page-view', $comment->entity); $this->checkOwnablePermission('comment-update', $comment); - $comment = $this->commentRepo->update($comment, $request->get('text')); + $comment = $this->commentRepo->update($comment, $input['html']); - return view('comments.comment', ['comment' => $comment, 'readOnly' => false]); + return view('comments.comment', [ + 'comment' => $comment, + 'readOnly' => false, + ]); } /** diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index dc6ca8264..79c9d3c2c 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -1,6 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; -import {buildForInput} from "../wysiwyg/config"; +import {buildForInput} from '../wysiwyg/config'; export class PageComment extends Component { @@ -58,6 +58,7 @@ export class PageComment extends Component { this.toggleEditMode(true); if (this.wysiwygEditor) { + this.wysiwygEditor.focus(); return; } @@ -72,6 +73,7 @@ export class PageComment extends Component { window.tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; + setTimeout(() => this.wysiwygEditor.focus(), 50); }); } @@ -81,7 +83,7 @@ export class PageComment extends Component { this.form.toggleAttribute('hidden', true); const reqData = { - text: this.input.value, + html: this.wysiwygEditor.getContent(), parent_id: this.parentId || null, }; diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index ebcc95f07..cfb0634a9 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -1,6 +1,6 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; -import {buildForInput} from "../wysiwyg/config"; +import {buildForInput} from '../wysiwyg/config'; export class PageComments extends Component { @@ -65,9 +65,8 @@ export class PageComments extends Component { this.form.after(loading); this.form.toggleAttribute('hidden', true); - const text = this.formInput.value; const reqData = { - text, + html: this.wysiwygEditor.getContent(), parent_id: this.parentId || null, }; @@ -92,6 +91,7 @@ export class PageComments extends Component { } resetForm() { + this.removeEditor(); this.formInput.value = ''; this.parentId = null; this.replyToRow.toggleAttribute('hidden', true); @@ -99,6 +99,7 @@ export class PageComments extends Component { } showForm() { + this.removeEditor(); this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); @@ -118,6 +119,7 @@ export class PageComments extends Component { loadEditor() { if (this.wysiwygEditor) { + this.wysiwygEditor.focus(); return; } @@ -132,10 +134,17 @@ export class PageComments extends Component { window.tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; - this.wysiwygEditor.focus(); + setTimeout(() => this.wysiwygEditor.focus(), 50); }); } + removeEditor() { + if (this.wysiwygEditor) { + this.wysiwygEditor.remove(); + this.wysiwygEditor = null; + } + } + getCommentCount() { return this.container.querySelectorAll('[component="page-comment"]').length; } diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 4340cfdf5..e00307f0f 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -77,7 +77,7 @@ @if(!$readOnly && userCan('comment-update', $comment))