]> BookStack Code Mirror - bookstack/blob - resources/js/components/page-comments.ts
Comments: Fixed pointer display, Fixed translation test
[bookstack] / resources / js / components / page-comments.ts
1 import {Component} from './component';
2 import {getLoading, htmlToDom} from '../services/dom';
3 import {buildForInput} from '../wysiwyg-tinymce/config';
4 import {Tabs} from "./tabs";
5 import {PageCommentReference} from "./page-comment-reference";
6 import {scrollAndHighlightElement} from "../services/util";
7 import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
8
9 export class PageComments extends Component {
10
11     private elem!: HTMLElement;
12     private pageId!: number;
13     private container!: HTMLElement;
14     private commentCountBar!: HTMLElement;
15     private activeTab!: HTMLElement;
16     private archivedTab!: HTMLElement;
17     private addButtonContainer!: HTMLElement;
18     private archiveContainer!: HTMLElement;
19     private replyToRow!: HTMLElement;
20     private referenceRow!: HTMLElement;
21     private formContainer!: HTMLElement;
22     private form!: HTMLFormElement;
23     private formInput!: HTMLInputElement;
24     private formReplyLink!: HTMLAnchorElement;
25     private formReferenceLink!: HTMLAnchorElement;
26     private addCommentButton!: HTMLElement;
27     private hideFormButton!: HTMLElement;
28     private removeReplyToButton!: HTMLElement;
29     private removeReferenceButton!: HTMLElement;
30     private wysiwygLanguage!: string;
31     private wysiwygTextDirection!: string;
32     private wysiwygEditor: any = null;
33     private createdText!: string;
34     private countText!: string;
35     private archivedCountText!: string;
36     private parentId: number | null = null;
37     private contentReference: string = '';
38     private formReplyText: string = '';
39
40     setup() {
41         this.elem = this.$el;
42         this.pageId = Number(this.$opts.pageId);
43
44         // Element references
45         this.container = this.$refs.commentContainer;
46         this.commentCountBar = this.$refs.commentCountBar;
47         this.activeTab = this.$refs.activeTab;
48         this.archivedTab = this.$refs.archivedTab;
49         this.addButtonContainer = this.$refs.addButtonContainer;
50         this.archiveContainer = this.$refs.archiveContainer;
51         this.replyToRow = this.$refs.replyToRow;
52         this.referenceRow = this.$refs.referenceRow;
53         this.formContainer = this.$refs.formContainer;
54         this.form = this.$refs.form as HTMLFormElement;
55         this.formInput = this.$refs.formInput as HTMLInputElement;
56         this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement;
57         this.formReferenceLink = this.$refs.formReferenceLink as HTMLAnchorElement;
58         this.addCommentButton = this.$refs.addCommentButton;
59         this.hideFormButton = this.$refs.hideFormButton;
60         this.removeReplyToButton = this.$refs.removeReplyToButton;
61         this.removeReferenceButton = this.$refs.removeReferenceButton;
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         this.archivedCountText = this.$opts.archivedCountText;
71
72         this.formReplyText = this.formReplyLink?.textContent || '';
73
74         this.setupListeners();
75     }
76
77     protected setupListeners(): void {
78         this.elem.addEventListener('page-comment-delete', () => {
79             setTimeout(() => this.updateCount(), 1);
80             this.hideForm();
81         });
82
83         this.elem.addEventListener('page-comment-reply', ((event: CustomEvent<PageCommentReplyEventData>) => {
84             this.setReply(event.detail.id, event.detail.element);
85         }) as EventListener);
86
87         this.elem.addEventListener('page-comment-archive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
88             this.archiveContainer.append(event.detail.new_thread_dom);
89             setTimeout(() => this.updateCount(), 1);
90         }) as EventListener);
91
92         this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent<PageCommentArchiveEventData>) => {
93             this.container.append(event.detail.new_thread_dom);
94             setTimeout(() => this.updateCount(), 1);
95         }) as EventListener);
96
97         if (this.form) {
98             this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this));
99             this.removeReferenceButton.addEventListener('click', () => this.setContentReference(''));
100             this.hideFormButton.addEventListener('click', this.hideForm.bind(this));
101             this.addCommentButton.addEventListener('click', this.showForm.bind(this));
102             this.form.addEventListener('submit', this.saveComment.bind(this));
103         }
104     }
105
106     protected saveComment(event: SubmitEvent): void {
107         event.preventDefault();
108         event.stopPropagation();
109
110         const loading = getLoading();
111         loading.classList.add('px-l');
112         this.form.after(loading);
113         this.form.toggleAttribute('hidden', true);
114
115         const reqData = {
116             html: this.wysiwygEditor.getContent(),
117             parent_id: this.parentId || null,
118             content_ref: this.contentReference,
119         };
120
121         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
122             const newElem = htmlToDom(resp.data as string);
123
124             if (reqData.parent_id) {
125                 this.formContainer.after(newElem);
126             } else {
127                 this.container.append(newElem);
128             }
129
130             const refs = window.$components.allWithinElement<PageCommentReference>(newElem, 'page-comment-reference');
131             for (const ref of refs) {
132                 ref.showForDisplay();
133             }
134
135             window.$events.success(this.createdText);
136             this.hideForm();
137             this.updateCount();
138         }).catch(err => {
139             this.form.toggleAttribute('hidden', false);
140             window.$events.showValidationErrors(err);
141         });
142
143         this.form.toggleAttribute('hidden', false);
144         loading.remove();
145     }
146
147     protected updateCount(): void {
148         const activeCount = this.getActiveThreadCount();
149         this.activeTab.textContent = window.$trans.choice(this.countText, activeCount);
150         const archivedCount = this.getArchivedThreadCount();
151         this.archivedTab.textContent = window.$trans.choice(this.archivedCountText, archivedCount);
152     }
153
154     protected resetForm(): void {
155         this.removeEditor();
156         this.formInput.value = '';
157         this.setContentReference('');
158         this.removeReplyTo();
159     }
160
161     protected showForm(): void {
162         this.removeEditor();
163         this.formContainer.toggleAttribute('hidden', false);
164         this.addButtonContainer.toggleAttribute('hidden', true);
165         this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'});
166         this.loadEditor();
167
168         // Ensure the active comments tab is displaying
169         const tabs = window.$components.firstOnElement(this.elem, 'tabs');
170         if (tabs instanceof Tabs) {
171             tabs.show('comment-tab-panel-active');
172         }
173     }
174
175     protected hideForm(): void {
176         this.resetForm();
177         this.formContainer.toggleAttribute('hidden', true);
178         if (this.getActiveThreadCount() > 0) {
179             this.elem.append(this.addButtonContainer);
180         } else {
181             this.commentCountBar.append(this.addButtonContainer);
182         }
183         this.addButtonContainer.toggleAttribute('hidden', false);
184     }
185
186     protected loadEditor(): void {
187         if (this.wysiwygEditor) {
188             this.wysiwygEditor.focus();
189             return;
190         }
191
192         const config = buildForInput({
193             language: this.wysiwygLanguage,
194             containerElement: this.formInput,
195             darkMode: document.documentElement.classList.contains('dark-mode'),
196             textDirection: this.wysiwygTextDirection,
197             drawioUrl: '',
198             pageId: 0,
199             translations: {},
200             translationMap: (window as unknown as Record<string, Object>).editor_translations,
201         });
202
203         (window as unknown as {tinymce: {init: (arg0: Object) => Promise<any>}}).tinymce.init(config).then(editors => {
204             this.wysiwygEditor = editors[0];
205             setTimeout(() => this.wysiwygEditor.focus(), 50);
206         });
207     }
208
209     protected removeEditor(): void {
210         if (this.wysiwygEditor) {
211             this.wysiwygEditor.remove();
212             this.wysiwygEditor = null;
213         }
214     }
215
216     protected getActiveThreadCount(): number {
217         return this.container.querySelectorAll(':scope > .comment-branch:not([hidden])').length;
218     }
219
220     protected getArchivedThreadCount(): number {
221         return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length;
222     }
223
224     protected setReply(commentLocalId: string, commentElement: HTMLElement): void {
225         const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement;
226         targetFormLocation.append(this.formContainer);
227         this.showForm();
228         this.parentId = Number(commentLocalId);
229         this.replyToRow.toggleAttribute('hidden', false);
230         this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId));
231         this.formReplyLink.href = `#comment${this.parentId}`;
232     }
233
234     protected removeReplyTo(): void {
235         this.parentId = null;
236         this.replyToRow.toggleAttribute('hidden', true);
237         this.container.append(this.formContainer);
238         this.showForm();
239     }
240
241     public startNewComment(contentReference: string): void {
242         this.removeReplyTo();
243         this.setContentReference(contentReference);
244     }
245
246     protected setContentReference(reference: string): void {
247         this.contentReference = reference;
248         this.referenceRow.toggleAttribute('hidden', !Boolean(reference));
249         const [id] = reference.split(':');
250         this.formReferenceLink.href = `#${id}`;
251         this.formReferenceLink.onclick = function(event) {
252             event.preventDefault();
253             const el = document.getElementById(id);
254             if (el) {
255                 scrollAndHighlightElement(el);
256             }
257         };
258     }
259
260 }