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