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