]> BookStack Code Mirror - bookstack/blob - resources/js/components/page-comments.ts
Comments: Added back-end content reference handling
[bookstack] / resources / js / components / page-comments.ts
1 import {Component} from './component';
2 import {getLoading, htmlToDom} from '../services/dom.ts';
3 import {buildForInput} from '../wysiwyg-tinymce/config';
4
5 export interface CommentReplyEvent extends Event {
6     detail: {
7         id: string; // ID of comment being replied to
8         element: HTMLElement; // Container for comment replied to
9     }
10 }
11
12 export class PageComments extends Component {
13
14     private elem: HTMLElement;
15     private pageId: number;
16     private container: HTMLElement;
17     private commentCountBar: HTMLElement;
18     private commentsTitle: HTMLElement;
19     private addButtonContainer: HTMLElement;
20     private replyToRow: HTMLElement;
21     private formContainer: HTMLElement;
22     private form: HTMLFormElement;
23     private formInput: HTMLInputElement;
24     private formReplyLink: HTMLAnchorElement;
25     private addCommentButton: HTMLElement;
26     private hideFormButton: HTMLElement;
27     private removeReplyToButton: HTMLElement;
28     private wysiwygLanguage: string;
29     private wysiwygTextDirection: string;
30     private wysiwygEditor: any = null;
31     private createdText: string;
32     private countText: string;
33     private parentId: number | null = null;
34     private contentReference: string = '';
35     private formReplyText: string = '';
36
37     setup() {
38         this.elem = this.$el;
39         this.pageId = Number(this.$opts.pageId);
40
41         // Element references
42         this.container = this.$refs.commentContainer;
43         this.commentCountBar = this.$refs.commentCountBar;
44         this.commentsTitle = this.$refs.commentsTitle;
45         this.addButtonContainer = this.$refs.addButtonContainer;
46         this.replyToRow = this.$refs.replyToRow;
47         this.formContainer = this.$refs.formContainer;
48         this.form = this.$refs.form as HTMLFormElement;
49         this.formInput = this.$refs.formInput as HTMLInputElement;
50         this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement;
51         this.addCommentButton = this.$refs.addCommentButton;
52         this.hideFormButton = this.$refs.hideFormButton;
53         this.removeReplyToButton = this.$refs.removeReplyToButton;
54
55         // WYSIWYG options
56         this.wysiwygLanguage = this.$opts.wysiwygLanguage;
57         this.wysiwygTextDirection = this.$opts.wysiwygTextDirection;
58
59         // Translations
60         this.createdText = this.$opts.createdText;
61         this.countText = this.$opts.countText;
62
63         this.formReplyText = this.formReplyLink?.textContent || '';
64
65         this.setupListeners();
66     }
67
68     protected setupListeners(): void {
69         this.elem.addEventListener('page-comment-delete', () => {
70             setTimeout(() => this.updateCount(), 1);
71             this.hideForm();
72         });
73
74         this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => {
75             this.setReply(event.detail.id, event.detail.element);
76         });
77
78         if (this.form) {
79             this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
80             this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
81             this.addCommentButton.addEventListener('click', this.showForm.bind(this));
82             this.form.addEventListener('submit', this.saveComment.bind(this));
83         }
84     }
85
86     protected saveComment(event): void {
87         event.preventDefault();
88         event.stopPropagation();
89
90         const loading = getLoading();
91         loading.classList.add('px-l');
92         this.form.after(loading);
93         this.form.toggleAttribute('hidden', true);
94
95         const reqData = {
96             html: this.wysiwygEditor.getContent(),
97             parent_id: this.parentId || null,
98             content_ref: this.contentReference || '',
99         };
100
101         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
102             const newElem = htmlToDom(resp.data as string);
103
104             if (reqData.parent_id) {
105                 this.formContainer.after(newElem);
106             } else {
107                 this.container.append(newElem);
108             }
109
110             window.$events.success(this.createdText);
111             this.hideForm();
112             this.updateCount();
113         }).catch(err => {
114             this.form.toggleAttribute('hidden', false);
115             window.$events.showValidationErrors(err);
116         });
117
118         this.form.toggleAttribute('hidden', false);
119         loading.remove();
120     }
121
122     protected updateCount(): void {
123         const count = this.getCommentCount();
124         this.commentsTitle.textContent = window.$trans.choice(this.countText, count);
125     }
126
127     protected resetForm(): void {
128         this.removeEditor();
129         this.formInput.value = '';
130         this.parentId = null;
131         this.contentReference = '';
132         this.replyToRow.toggleAttribute('hidden', true);
133         this.container.append(this.formContainer);
134     }
135
136     protected showForm(): void {
137         this.removeEditor();
138         this.formContainer.toggleAttribute('hidden', false);
139         this.addButtonContainer.toggleAttribute('hidden', true);
140         this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
141         this.loadEditor();
142     }
143
144     protected hideForm(): void {
145         this.resetForm();
146         this.formContainer.toggleAttribute('hidden', true);
147         if (this.getCommentCount() > 0) {
148             this.elem.append(this.addButtonContainer);
149         } else {
150             this.commentCountBar.append(this.addButtonContainer);
151         }
152         this.addButtonContainer.toggleAttribute('hidden', false);
153     }
154
155     protected loadEditor(): void {
156         if (this.wysiwygEditor) {
157             this.wysiwygEditor.focus();
158             return;
159         }
160
161         const config = buildForInput({
162             language: this.wysiwygLanguage,
163             containerElement: this.formInput,
164             darkMode: document.documentElement.classList.contains('dark-mode'),
165             textDirection: this.wysiwygTextDirection,
166             drawioUrl: '',
167             pageId: 0,
168             translations: {},
169             translationMap: (window as Record<string, Object>).editor_translations,
170         });
171
172         (window as {tinymce: {init: (Object) => Promise<any>}}).tinymce.init(config).then(editors => {
173             this.wysiwygEditor = editors[0];
174             setTimeout(() => this.wysiwygEditor.focus(), 50);
175         });
176     }
177
178     protected removeEditor(): void {
179         if (this.wysiwygEditor) {
180             this.wysiwygEditor.remove();
181             this.wysiwygEditor = null;
182         }
183     }
184
185     protected getCommentCount(): number {
186         return this.container.querySelectorAll('[component="page-comment"]').length;
187     }
188
189     protected setReply(commentLocalId, commentElement): void {
190         const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children');
191         targetFormLocation.append(this.formContainer);
192         this.showForm();
193         this.parentId = commentLocalId;
194         this.replyToRow.toggleAttribute('hidden', false);
195         this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));
196         this.formReplyLink.href = `#comment${this.parentId}`;
197     }
198
199     protected removeReplyTo(): void {
200         this.parentId = null;
201         this.replyToRow.toggleAttribute('hidden', true);
202         this.container.append(this.formContainer);
203         this.showForm();
204     }
205
206     public startNewComment(contentReference: string): void {
207         this.removeReplyTo();
208         this.contentReference = contentReference;
209     }
210
211 }