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