1 import {onSelect} from '../services/dom.ts';
2 import {debounce} from '../services/util.ts';
3 import {Component} from './component';
4 import {utcTimeStampToLocalTime} from '../services/dates.ts';
6 export class PageEditor extends Component {
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;
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.deleteDraftButton = this.$refs.deleteDraft;
23 this.deleteDraftWrap = this.$refs.deleteDraftWrap;
24 this.draftDisplay = this.$refs.draftDisplay;
25 this.draftDisplayIcon = this.$refs.draftDisplayIcon;
26 this.changelogInput = this.$refs.changelogInput;
27 this.changelogDisplay = this.$refs.changelogDisplay;
28 this.changeEditorButtons = this.$manyRefs.changeEditor || [];
29 this.switchDialogContainer = this.$refs.switchDialog;
30 this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
33 this.draftText = this.$opts.draftText;
34 this.autosaveFailText = this.$opts.autosaveFailText;
35 this.editingPageText = this.$opts.editingPageText;
36 this.draftDiscardedText = this.$opts.draftDiscardedText;
37 this.draftDeleteText = this.$opts.draftDeleteText;
38 this.draftDeleteFailText = this.$opts.draftDeleteFailText;
39 this.setChangelogText = this.$opts.setChangelogText;
48 this.shownWarningsCache = new Set();
50 if (this.pageId !== 0 && this.draftsEnabled) {
51 window.setTimeout(() => {
55 this.draftDisplay.innerHTML = this.draftText;
57 this.setupListeners();
58 this.setInitialFocus();
62 // Listen to save events from editor
63 window.$events.listen('editor-save-draft', this.saveDraft.bind(this));
64 window.$events.listen('editor-save-page', this.savePage.bind(this));
66 // Listen to content changes from the editor
67 const onContentChange = () => {
68 this.autoSave.pendingChange = true;
70 window.$events.listen('editor-html-change', onContentChange);
71 window.$events.listen('editor-markdown-change', onContentChange);
73 // Listen to changes on the title input
74 this.titleElem.addEventListener('input', onContentChange);
77 const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
78 this.changelogInput.addEventListener('input', () => {
79 const count = this.changelogInput.value.length;
80 const counterEl = document.getElementById('changelog-count');
81 if (counterEl) counterEl.innerText = `${count} / 250`;
82 updateChangelogDebounced();
86 onSelect(this.saveDraftButton, this.saveDraft.bind(this));
87 onSelect(this.discardDraftButton, this.discardDraft.bind(this));
88 onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
90 // Change editor controls
91 onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
95 if (this.hasDefaultTitle) {
96 this.titleElem.select();
100 window.setTimeout(() => {
101 window.$events.emit('editor::focus', '');
106 this.autoSave.interval = window.setInterval(this.runAutoSave.bind(this), this.autoSave.frequency);
110 // Stop if manually saved recently to prevent bombarding the server
111 const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency) / 2);
112 if (savedRecently || !this.autoSave.pendingChange) {
120 this.container.closest('form').requestSubmit();
124 const data = {name: this.titleElem.value.trim()};
126 const editorContent = await this.getEditorComponent().getContent();
127 Object.assign(data, editorContent);
131 const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
132 if (!this.isNewDraft) {
133 this.discardDraftWrap.toggleAttribute('hidden', false);
134 this.deleteDraftWrap.toggleAttribute('hidden', false);
137 this.draftNotifyChange(`${resp.data.message} ${utcTimeStampToLocalTime(resp.data.timestamp)}`);
138 this.autoSave.last = Date.now();
139 if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
140 window.$events.emit('warning', resp.data.warning);
141 this.shownWarningsCache.add(resp.data.warning);
145 this.autoSave.pendingChange = false;
147 // Save the editor content in LocalStorage as a last resort, just in case.
149 const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
150 window.localStorage.setItem(saveKey, JSON.stringify(data));
152 console.error(lsErr);
155 window.$events.emit('error', this.autosaveFailText);
161 draftNotifyChange(text) {
162 this.draftDisplay.innerText = text;
163 this.draftDisplayIcon.classList.add('visible');
164 window.setTimeout(() => {
165 this.draftDisplayIcon.classList.remove('visible');
169 async discardDraft(notify = true) {
172 response = await window.$http.get(`/ajax/page/${this.pageId}`);
178 if (this.autoSave.interval) {
179 window.clearInterval(this.autoSave.interval);
182 this.draftDisplay.innerText = this.editingPageText;
183 this.discardDraftWrap.toggleAttribute('hidden', true);
184 window.$events.emit('editor::replace', {
185 html: response.data.html,
186 markdown: response.data.markdown,
189 this.titleElem.value = response.data.name;
190 window.setTimeout(() => {
191 this.startAutoSave();
195 window.$events.success(this.draftDiscardedText);
199 async deleteDraft() {
200 /** @var {ConfirmDialog} * */
201 const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
202 const confirmed = await dialog.show();
208 const discard = this.discardDraft(false);
209 const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
210 await Promise.all([discard, draftDelete]);
211 window.$events.success(this.draftDeleteText);
212 this.deleteDraftWrap.toggleAttribute('hidden', true);
215 window.$events.error(this.draftDeleteFailText);
219 updateChangelogDisplay() {
220 let summary = this.changelogInput.value.trim();
221 if (summary.length === 0) {
222 summary = this.setChangelogText;
223 } else if (summary.length > 16) {
224 summary = `${summary.slice(0, 16)}...`;
226 this.changelogDisplay.innerText = summary;
229 async changeEditor(event) {
230 event.preventDefault();
232 const link = event.target.closest('a').href;
233 /** @var {ConfirmDialog} * */
234 const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
235 const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
237 if (saved && confirmed) {
238 window.location = link;
243 * @return {MarkdownEditor|WysiwygEditor|WysiwygEditorTinymce}
245 getEditorComponent() {
246 return window.$components.first('markdown-editor')
247 || window.$components.first('wysiwyg-editor')
248 || window.$components.first('wysiwyg-editor-tinymce');