]> BookStack Code Mirror - bookstack/blob - resources/js/components/page-editor.js
Merge pull request #1 from BookStackApp/master
[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.isDefaultTitle || 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
28         // Translations
29         this.draftText = this.$opts.draftText;
30         this.autosaveFailText = this.$opts.autosaveFailText;
31         this.editingPageText = this.$opts.editingPageText;
32         this.draftDiscardedText = this.$opts.draftDiscardedText;
33         this.setChangelogText = this.$opts.setChangelogText;
34
35         // State data
36         this.editorHTML = '';
37         this.editorMarkdown = '';
38         this.autoSave = {
39             interval: null,
40             frequency: 30000,
41             last: 0,
42         };
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
76     setInitialFocus() {
77         if (this.hasDefaultTitle) {
78             return this.titleElem.select();
79         }
80
81         window.setTimeout(() => {
82             window.$events.emit('editor::focus', '');
83         }, 500);
84     }
85
86     startAutoSave() {
87         let lastContent = this.titleElem.value.trim() + '::' + this.editorHTML;
88         this.autoSaveInterval = window.setInterval(() => {
89             // Stop if manually saved recently to prevent bombarding the server
90             let savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
91             if (savedRecently) return;
92             const newContent = this.titleElem.value.trim() + '::' + this.editorHTML;
93             if (newContent !== lastContent) {
94                 lastContent = newContent;
95                 this.saveDraft();
96             }
97
98         }, this.autoSave.frequency);
99     }
100
101     savePage() {
102         this.container.closest('form').submit();
103     }
104
105     async saveDraft() {
106         const data = {
107             name: this.titleElem.value.trim(),
108             html: this.editorHTML,
109         };
110
111         if (this.editorType === 'markdown') {
112             data.markdown = this.editorMarkdown;
113         }
114
115         try {
116             const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
117             if (!this.isNewDraft) {
118                 this.toggleDiscardDraftVisibility(true);
119             }
120             this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
121             this.autoSave.last = Date.now();
122         } catch (err) {
123             // Save the editor content in LocalStorage as a last resort, just in case.
124             try {
125                 const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
126                 window.localStorage.setItem(saveKey, JSON.stringify(data));
127             } catch (err) {}
128
129             window.$events.emit('error', this.autosaveFailText);
130         }
131
132     }
133
134     draftNotifyChange(text) {
135         this.draftDisplay.innerText = text;
136         this.draftDisplayIcon.classList.add('visible');
137         window.setTimeout(() => {
138             this.draftDisplayIcon.classList.remove('visible');
139         }, 2000);
140     }
141
142     async discardDraft() {
143         let response;
144         try {
145             response = await window.$http.get(`/ajax/page/${this.pageId}`);
146         } catch (e) {
147             return console.error(e);
148         }
149
150         if (this.autoSave.interval) {
151             window.clearInterval(this.autoSave.interval);
152         }
153
154         this.draftDisplay.innerText = this.editingPageText;
155         this.toggleDiscardDraftVisibility(false);
156         window.$events.emit('editor-html-update', response.data.html || '');
157         window.$events.emit('editor-markdown-update', response.data.markdown || response.data.html);
158
159         this.titleElem.value = response.data.name;
160         window.setTimeout(() => {
161             this.startAutoSave();
162         }, 1000);
163         window.$events.emit('success', this.draftDiscardedText);
164
165     }
166
167     updateChangelogDisplay() {
168         let summary = this.changelogInput.value.trim();
169         if (summary.length === 0) {
170             summary = this.setChangelogText;
171         } else if (summary.length > 16) {
172             summary = summary.slice(0, 16) + '...';
173         }
174         this.changelogDisplay.innerText = summary;
175     }
176
177     toggleDiscardDraftVisibility(show) {
178         this.discardDraftWrap.classList.toggle('hidden', !show);
179     }
180
181 }
182
183 export default PageEditor;