1 import * as Dates from "../services/dates";
2 import {onSelect} from "../services/dom";
3 import {debounce} from "../services/util";
4 import {Component} from "./component";
6 export class PageEditor extends Component {
9 this.draftsEnabled = this.$opts.draftsEnabled === 'true';
10 this.editorType = this.$opts.editorType;
11 this.pageId = Number(this.$opts.pageId);
12 this.isNewDraft = this.$opts.pageNewDraft === 'true';
13 this.hasDefaultTitle = this.$opts.hasDefaultTitle || false;
16 this.container = this.$el;
17 this.titleElem = this.$refs.titleContainer.querySelector('input');
18 this.saveDraftButton = this.$refs.saveDraft;
19 this.discardDraftButton = this.$refs.discardDraft;
20 this.discardDraftWrap = this.$refs.discardDraftWrap;
21 this.draftDisplay = this.$refs.draftDisplay;
22 this.draftDisplayIcon = this.$refs.draftDisplayIcon;
23 this.changelogInput = this.$refs.changelogInput;
24 this.changelogDisplay = this.$refs.changelogDisplay;
25 this.changeEditorButtons = this.$manyRefs.changeEditor || [];
26 this.switchDialogContainer = this.$refs.switchDialog;
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;
42 this.shownWarningsCache = new Set();
44 if (this.pageId !== 0 && this.draftsEnabled) {
45 window.setTimeout(() => {
49 this.draftDisplay.innerHTML = this.draftText;
51 this.setupListeners();
52 this.setInitialFocus();
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));
60 // Listen to content changes from the editor
61 const onContentChange = () => this.autoSave.pendingChange = true;
62 window.$events.listen('editor-html-change', onContentChange);
63 window.$events.listen('editor-markdown-change', onContentChange);
65 // Listen to changes on the title input
66 this.titleElem.addEventListener('input', onContentChange);
69 const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
70 this.changelogInput.addEventListener('input', updateChangelogDebounced);
73 onSelect(this.saveDraftButton, this.saveDraft.bind(this));
74 onSelect(this.discardDraftButton, this.discardDraft.bind(this));
76 // Change editor controls
77 onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
81 if (this.hasDefaultTitle) {
82 return this.titleElem.select();
85 window.setTimeout(() => {
86 window.$events.emit('editor::focus', '');
91 this.autoSave.interval = window.setInterval(this.runAutoSave.bind(this), this.autoSave.frequency);
95 // Stop if manually saved recently to prevent bombarding the server
96 const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
97 if (savedRecently || !this.autoSave.pendingChange) {
105 this.container.closest('form').submit();
109 const data = {name: this.titleElem.value.trim()};
111 const editorContent = this.getEditorComponent().getContent();
112 Object.assign(data, editorContent);
116 const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
117 if (!this.isNewDraft) {
118 this.toggleDiscardDraftVisibility(true);
121 this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
122 this.autoSave.last = Date.now();
123 if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
124 window.$events.emit('warning', resp.data.warning);
125 this.shownWarningsCache.add(resp.data.warning);
129 this.autoSave.pendingChange = false;
131 // Save the editor content in LocalStorage as a last resort, just in case.
133 const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
134 window.localStorage.setItem(saveKey, JSON.stringify(data));
137 window.$events.emit('error', this.autosaveFailText);
143 draftNotifyChange(text) {
144 this.draftDisplay.innerText = text;
145 this.draftDisplayIcon.classList.add('visible');
146 window.setTimeout(() => {
147 this.draftDisplayIcon.classList.remove('visible');
151 async discardDraft() {
154 response = await window.$http.get(`/ajax/page/${this.pageId}`);
156 return console.error(e);
159 if (this.autoSave.interval) {
160 window.clearInterval(this.autoSave.interval);
163 this.draftDisplay.innerText = this.editingPageText;
164 this.toggleDiscardDraftVisibility(false);
165 window.$events.emit('editor::replace', {
166 html: response.data.html,
167 markdown: response.data.markdown,
170 this.titleElem.value = response.data.name;
171 window.setTimeout(() => {
172 this.startAutoSave();
174 window.$events.emit('success', this.draftDiscardedText);
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) + '...';
185 this.changelogDisplay.innerText = summary;
188 toggleDiscardDraftVisibility(show) {
189 this.discardDraftWrap.classList.toggle('hidden', !show);
192 async changeEditor(event) {
193 event.preventDefault();
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()]);
200 if (saved && confirmed) {
201 window.location = link;
206 * @return MarkdownEditor|WysiwygEditor
208 getEditorComponent() {
209 return window.$components.first('markdown-editor') || window.$components.first('wysiwyg-editor');