Skip to content

Commit 16af833

Browse files
authored
Merge pull request #4815 from BookStackApp/comment_wysiwyg
Comment WYSIWYG Inputs
2 parents 24e6dc4 + 47f082c commit 16af833

File tree

19 files changed

+225
-175
lines changed

19 files changed

+225
-175
lines changed

app/Activity/CommentRepo.php

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use BookStack\Activity\Models\Comment;
66
use BookStack\Entities\Models\Entity;
77
use BookStack\Facades\Activity as ActivityService;
8-
use League\CommonMark\CommonMarkConverter;
8+
use BookStack\Util\HtmlDescriptionFilter;
99

1010
class CommentRepo
1111
{
@@ -20,13 +20,12 @@ public function getById(int $id): Comment
2020
/**
2121
* Create a new comment on an entity.
2222
*/
23-
public function create(Entity $entity, string $text, ?int $parent_id): Comment
23+
public function create(Entity $entity, string $html, ?int $parent_id): Comment
2424
{
2525
$userId = user()->id;
2626
$comment = new Comment();
2727

28-
$comment->text = $text;
29-
$comment->html = $this->commentToHtml($text);
28+
$comment->html = HtmlDescriptionFilter::filterFromString($html);
3029
$comment->created_by = $userId;
3130
$comment->updated_by = $userId;
3231
$comment->local_id = $this->getNextLocalId($entity);
@@ -42,11 +41,10 @@ public function create(Entity $entity, string $text, ?int $parent_id): Comment
4241
/**
4342
* Update an existing comment.
4443
*/
45-
public function update(Comment $comment, string $text): Comment
44+
public function update(Comment $comment, string $html): Comment
4645
{
4746
$comment->updated_by = user()->id;
48-
$comment->text = $text;
49-
$comment->html = $this->commentToHtml($text);
47+
$comment->html = HtmlDescriptionFilter::filterFromString($html);
5048
$comment->save();
5149

5250
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
@@ -64,20 +62,6 @@ public function delete(Comment $comment): void
6462
ActivityService::add(ActivityType::COMMENT_DELETE, $comment);
6563
}
6664

67-
/**
68-
* Convert the given comment Markdown to HTML.
69-
*/
70-
public function commentToHtml(string $commentText): string
71-
{
72-
$converter = new CommonMarkConverter([
73-
'html_input' => 'strip',
74-
'max_nesting_level' => 10,
75-
'allow_unsafe_links' => false,
76-
]);
77-
78-
return $converter->convert($commentText);
79-
}
80-
8165
/**
8266
* Get the next local ID relative to the linked entity.
8367
*/

app/Activity/Controllers/CommentController.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public function __construct(
2222
*/
2323
public function savePageComment(Request $request, int $pageId)
2424
{
25-
$this->validate($request, [
26-
'text' => ['required', 'string'],
25+
$input = $this->validate($request, [
26+
'html' => ['required', 'string'],
2727
'parent_id' => ['nullable', 'integer'],
2828
]);
2929

@@ -39,7 +39,7 @@ public function savePageComment(Request $request, int $pageId)
3939

4040
// Create a new comment.
4141
$this->checkPermission('comment-create-all');
42-
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
42+
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
4343

4444
return view('comments.comment-branch', [
4545
'readOnly' => false,
@@ -57,17 +57,20 @@ public function savePageComment(Request $request, int $pageId)
5757
*/
5858
public function update(Request $request, int $commentId)
5959
{
60-
$this->validate($request, [
61-
'text' => ['required', 'string'],
60+
$input = $this->validate($request, [
61+
'html' => ['required', 'string'],
6262
]);
6363

6464
$comment = $this->commentRepo->getById($commentId);
6565
$this->checkOwnablePermission('page-view', $comment->entity);
6666
$this->checkOwnablePermission('comment-update', $comment);
6767

68-
$comment = $this->commentRepo->update($comment, $request->get('text'));
68+
$comment = $this->commentRepo->update($comment, $input['html']);
6969

70-
return view('comments.comment', ['comment' => $comment, 'readOnly' => false]);
70+
return view('comments.comment', [
71+
'comment' => $comment,
72+
'readOnly' => false,
73+
]);
7174
}
7275

7376
/**

app/Activity/Models/Comment.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
use BookStack\App\Model;
66
use BookStack\Users\Models\HasCreatorAndUpdater;
7+
use BookStack\Util\HtmlContentFilter;
78
use Illuminate\Database\Eloquent\Factories\HasFactory;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
910
use Illuminate\Database\Eloquent\Relations\MorphTo;
1011

1112
/**
1213
* @property int $id
13-
* @property string $text
14+
* @property string $text - Deprecated & now unused (#4821)
1415
* @property string $html
1516
* @property int|null $parent_id - Relates to local_id, not id
1617
* @property int $local_id
@@ -24,7 +25,7 @@ class Comment extends Model implements Loggable
2425
use HasFactory;
2526
use HasCreatorAndUpdater;
2627

27-
protected $fillable = ['text', 'parent_id'];
28+
protected $fillable = ['parent_id'];
2829
protected $appends = ['created', 'updated'];
2930

3031
/**
@@ -73,4 +74,9 @@ public function logDescriptor(): string
7374
{
7475
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
7576
}
77+
78+
public function safeHtml(): string
79+
{
80+
return HtmlContentFilter::removeScriptsFromHtmlString($this->html ?? '');
81+
}
7682
}

app/Activity/Tools/CommentTree.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ public function get(): array
4141
return $this->tree;
4242
}
4343

44+
public function canUpdateAny(): bool
45+
{
46+
foreach ($this->comments as $comment) {
47+
if (userCan('comment-update', $comment)) {
48+
return true;
49+
}
50+
}
51+
52+
return false;
53+
}
54+
4455
/**
4556
* @param Comment[] $comments
4657
*/

app/Console/Commands/RegenerateCommentContentCommand.php

Lines changed: 0 additions & 49 deletions
This file was deleted.

database/factories/Activity/Models/CommentFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public function definition()
2525

2626
return [
2727
'html' => $html,
28-
'text' => $text,
2928
'parent_id' => null,
29+
'local_id' => 1,
3030
];
3131
}
3232
}

resources/js/components/page-comment.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Component} from './component';
22
import {getLoading, htmlToDom} from '../services/dom';
3+
import {buildForInput} from '../wysiwyg/config';
34

45
export class PageComment extends Component {
56

@@ -11,7 +12,12 @@ export class PageComment extends Component {
1112
this.deletedText = this.$opts.deletedText;
1213
this.updatedText = this.$opts.updatedText;
1314

14-
// Element References
15+
// Editor reference and text options
16+
this.wysiwygEditor = null;
17+
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
18+
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
19+
20+
// Element references
1521
this.container = this.$el;
1622
this.contentContainer = this.$refs.contentContainer;
1723
this.form = this.$refs.form;
@@ -50,8 +56,25 @@ export class PageComment extends Component {
5056

5157
startEdit() {
5258
this.toggleEditMode(true);
53-
const lineCount = this.$refs.input.value.split('\n').length;
54-
this.$refs.input.style.height = `${(lineCount * 20) + 40}px`;
59+
60+
if (this.wysiwygEditor) {
61+
this.wysiwygEditor.focus();
62+
return;
63+
}
64+
65+
const config = buildForInput({
66+
language: this.wysiwygLanguage,
67+
containerElement: this.input,
68+
darkMode: document.documentElement.classList.contains('dark-mode'),
69+
textDirection: this.wysiwygTextDirection,
70+
translations: {},
71+
translationMap: window.editor_translations,
72+
});
73+
74+
window.tinymce.init(config).then(editors => {
75+
this.wysiwygEditor = editors[0];
76+
setTimeout(() => this.wysiwygEditor.focus(), 50);
77+
});
5578
}
5679

5780
async update(event) {
@@ -60,7 +83,7 @@ export class PageComment extends Component {
6083
this.form.toggleAttribute('hidden', true);
6184

6285
const reqData = {
63-
text: this.input.value,
86+
html: this.wysiwygEditor.getContent(),
6487
parent_id: this.parentId || null,
6588
};
6689

resources/js/components/page-comments.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Component} from './component';
22
import {getLoading, htmlToDom} from '../services/dom';
3+
import {buildForInput} from '../wysiwyg/config';
34

45
export class PageComments extends Component {
56

@@ -21,6 +22,11 @@ export class PageComments extends Component {
2122
this.hideFormButton = this.$refs.hideFormButton;
2223
this.removeReplyToButton = this.$refs.removeReplyToButton;
2324

25+
// WYSIWYG options
26+
this.wysiwygLanguage = this.$opts.wysiwygLanguage;
27+
this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
28+
this.wysiwygEditor = null;
29+
2430
// Translations
2531
this.createdText = this.$opts.createdText;
2632
this.countText = this.$opts.countText;
@@ -59,9 +65,8 @@ export class PageComments extends Component {
5965
this.form.after(loading);
6066
this.form.toggleAttribute('hidden', true);
6167

62-
const text = this.formInput.value;
6368
const reqData = {
64-
text,
69+
html: this.wysiwygEditor.getContent(),
6570
parent_id: this.parentId || null,
6671
};
6772

@@ -86,19 +91,19 @@ export class PageComments extends Component {
8691
}
8792

8893
resetForm() {
94+
this.removeEditor();
8995
this.formInput.value = '';
9096
this.parentId = null;
9197
this.replyToRow.toggleAttribute('hidden', true);
9298
this.container.append(this.formContainer);
9399
}
94100

95101
showForm() {
102+
this.removeEditor();
96103
this.formContainer.toggleAttribute('hidden', false);
97104
this.addButtonContainer.toggleAttribute('hidden', true);
98105
this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
99-
setTimeout(() => {
100-
this.formInput.focus();
101-
}, 100);
106+
this.loadEditor();
102107
}
103108

104109
hideForm() {
@@ -112,6 +117,34 @@ export class PageComments extends Component {
112117
this.addButtonContainer.toggleAttribute('hidden', false);
113118
}
114119

120+
loadEditor() {
121+
if (this.wysiwygEditor) {
122+
this.wysiwygEditor.focus();
123+
return;
124+
}
125+
126+
const config = buildForInput({
127+
language: this.wysiwygLanguage,
128+
containerElement: this.formInput,
129+
darkMode: document.documentElement.classList.contains('dark-mode'),
130+
textDirection: this.wysiwygTextDirection,
131+
translations: {},
132+
translationMap: window.editor_translations,
133+
});
134+
135+
window.tinymce.init(config).then(editors => {
136+
this.wysiwygEditor = editors[0];
137+
setTimeout(() => this.wysiwygEditor.focus(), 50);
138+
});
139+
}
140+
141+
removeEditor() {
142+
if (this.wysiwygEditor) {
143+
this.wysiwygEditor.remove();
144+
this.wysiwygEditor = null;
145+
}
146+
}
147+
115148
getCommentCount() {
116149
return this.container.querySelectorAll('[component="page-comment"]').length;
117150
}

resources/js/components/wysiwyg-input.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ export class WysiwygInput extends Component {
1010
language: this.$opts.language,
1111
containerElement: this.elem,
1212
darkMode: document.documentElement.classList.contains('dark-mode'),
13-
textDirection: this.textDirection,
14-
translations: {
15-
imageUploadErrorText: this.$opts.imageUploadErrorText,
16-
serverUploadLimitText: this.$opts.serverUploadLimitText,
17-
},
13+
textDirection: this.$opts.textDirection,
14+
translations: {},
1815
translationMap: window.editor_translations,
1916
});
2017

resources/js/wysiwyg/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export function buildForInput(options) {
339339
toolbar: 'bold italic link bullist numlist',
340340
content_style: getContentStyle(options),
341341
file_picker_types: 'file',
342+
valid_elements: 'p,a[href|title],ol,ul,li,strong,em,br',
342343
file_picker_callback: filePickerCallback,
343344
init_instance_callback(editor) {
344345
addCustomHeadContent(editor.getDoc());

0 commit comments

Comments
 (0)