]> BookStack Code Mirror - bookstack/blob - resources/js/components/page-editor.js
Merge pull request #3512 from BookStackApp/code_manager_updates
[bookstack] / resources / js / components / page-editor.js
1 import * as Dates from "../services/dates";
2 import {onSelect} from "../services/dom";
3
4 /**
5  * Page Editor
6  * @extends {Component}
7  */
8 class PageEditor {
9     setup() {
10         // Options
11         this.draftsEnabled = this.$opts.draftsEnabled === 'true';
12         this.editorType = this.$opts.editorType;
13         this.pageId = Number(this.$opts.pageId);
14         this.isNewDraft = this.$opts.pageNewDraft === 'true';
15         this.hasDefaultTitle = this.$opts.hasDefaultTitle || false;
16
17         // Elements
18         this.container = this.$el;
19         this.titleElem = this.$refs.titleContainer.querySelector('input');
20         this.saveDraftButton = this.$refs.saveDraft;
21         this.discardDraftButton = this.$refs.discardDraft;
22         this.discardDraftWrap = this.$refs.discardDraftWrap;
23         this.draftDisplay = this.$refs.draftDisplay;
24         this.draftDisplayIcon = this.$refs.draftDisplayIcon;
25         this.changelogInput = this.$refs.changelogInput;
26         this.changelogDisplay = this.$refs.changelogDisplay;
27         this.changeEditorButtons = this.$manyRefs.changeEditor;
28         this.switchDialogContainer = this.$refs.switchDialog;
29
30         // Translations
31         this.draftText = this.$opts.draftText;
32         this.autosaveFailText = this.$opts.autosaveFailText;
33         this.editingPageText = this.$opts.editingPageText;
34         this.draftDiscardedText = this.$opts.draftDiscardedText;
35         this.setChangelogText = this.$opts.setChangelogText;
36
37         // State data
38         this.editorHTML = '';
39         this.editorMarkdown = '';
40         this.autoSave = {
41             interval: null,
42             frequency: 30000,
43             last: 0,
44         };
45         this.shownWarningsCache = new Set();
46
47         if (this.pageId !== 0 && this.draftsEnabled) {
48             window.setTimeout(() => {
49                 this.startAutoSave();
50             }, 1000);
51         }
52         this.draftDisplay.innerHTML = this.draftText;
53
54         this.setupListeners();
55         this.setInitialFocus();
56     }
57
58     setupListeners() {
59         // Listen to save events from editor
60         window.$events.listen('editor-save-draft', this.saveDraft.bind(this));
61         window.$events.listen('editor-save-page', this.savePage.bind(this));
62
63         // Listen to content changes from the editor
64         window.$events.listen('editor-html-change', html => {
65             this.editorHTML = html;
66         });
67         window.$events.listen('editor-markdown-change', markdown => {
68             this.editorMarkdown = markdown;
69         });
70
71         // Changelog controls
72         this.changelogInput.addEventListener('change', this.updateChangelogDisplay.bind(this));
73
74         // Draft Controls
75         onSelect(this.saveDraftButton, this.saveDraft.bind(this));
76         onSelect(this.discardDraftButton, this.discardDraft.bind(this));
77
78         // Change editor controls
79         onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
80     }
81
82     setInitialFocus() {
83         if (this.hasDefaultTitle) {
84             return this.titleElem.select();
85         }
86
87         window.setTimeout(() => {
88             window.$events.emit('editor::focus', '');
89         }, 500);
90     }
91
92     startAutoSave() {
93         let lastContent = this.titleElem.value.trim() + '::' + this.editorHTML;
94         this.autoSaveInterval = window.setInterval(() => {
95             // Stop if manually saved recently to prevent bombarding the server
96             let savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
97             if (savedRecently) return;
98             const newContent = this.titleElem.value.trim() + '::' + this.editorHTML;
99             if (newContent !== lastContent) {
100                 lastContent = newContent;
101                 this.saveDraft();
102             }
103
104         }, this.autoSave.frequency);
105     }
106
107     savePage() {
108         this.container.closest('form').submit();
109     }
110
111     async saveDraft() {
112         const data = {
113             name: this.titleElem.value.trim(),
114             html: this.editorHTML,
115         };
116
117         if (this.editorType === 'markdown') {
118             data.markdown = this.editorMarkdown;
119         }
120
121         let didSave = false;
122         try {
123             const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
124             if (!this.isNewDraft) {
125                 this.toggleDiscardDraftVisibility(true);
126             }
127
128             this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
129             this.autoSave.last = Date.now();
130             if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
131                 window.$events.emit('warning', resp.data.warning);
132                 this.shownWarningsCache.add(resp.data.warning);
133             }
134
135             didSave = true;
136         } catch (err) {
137             // Save the editor content in LocalStorage as a last resort, just in case.
138             try {
139                 const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
140                 window.localStorage.setItem(saveKey, JSON.stringify(data));
141             } catch (err) {}
142
143             window.$events.emit('error', this.autosaveFailText);
144         }
145
146         return didSave;
147     }
148
149     draftNotifyChange(text) {
150         this.draftDisplay.innerText = text;
151         this.draftDisplayIcon.classList.add('visible');
152         window.setTimeout(() => {
153             this.draftDisplayIcon.classList.remove('visible');
154         }, 2000);
155     }
156
157     async discardDraft() {
158         let response;
159         try {
160             response = await window.$http.get(`/ajax/page/${this.pageId}`);
161         } catch (e) {
162             return console.error(e);
163         }
164
165         if (this.autoSave.interval) {
166             window.clearInterval(this.autoSave.interval);
167         }
168
169         this.draftDisplay.innerText = this.editingPageText;
170         this.toggleDiscardDraftVisibility(false);
171         window.$events.emit('editor::replace', {
172             html: response.data.html,
173             markdown: response.data.markdown,
174         });
175
176         this.titleElem.value = response.data.name;
177         window.setTimeout(() => {
178             this.startAutoSave();
179         }, 1000);
180         window.$events.emit('success', this.draftDiscardedText);
181
182     }
183
184     updateChangelogDisplay() {
185         let summary = this.changelogInput.value.trim();
186         if (summary.length === 0) {
187             summary = this.setChangelogText;
188         } else if (summary.length > 16) {
189             summary = summary.slice(0, 16) + '...';
190         }
191         this.changelogDisplay.innerText = summary;
192     }
193
194     toggleDiscardDraftVisibility(show) {
195         this.discardDraftWrap.classList.toggle('hidden', !show);
196     }
197
198     async changeEditor(event) {
199         event.preventDefault();
200
201         const link = event.target.closest('a').href;
202         const dialog = this.switchDialogContainer.components['confirm-dialog'];
203         const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
204
205         if (saved && confirmed) {
206             window.location = link;
207         }
208     }
209
210 }
211
212 export default PageEditor;