resources/js/components/page-editor.js
import {onSelect} from '../services/dom';
import {debounce} from '../services/util';
import {Component} from './component';
import {utcTimeStampToLocalTime} from '../services/dates.ts';
export class PageEditor extends Component {
setup() {
// Options
this.draftsEnabled = this.$opts.draftsEnabled === 'true';
this.editorType = this.$opts.editorType;
this.pageId = Number(this.$opts.pageId);
this.isNewDraft = this.$opts.pageNewDraft === 'true';
this.hasDefaultTitle = this.$opts.hasDefaultTitle || false;
// Elements
this.container = this.$el;
this.titleElem = this.$refs.titleContainer.querySelector('input');
this.saveDraftButton = this.$refs.saveDraft;
this.discardDraftButton = this.$refs.discardDraft;
this.discardDraftWrap = this.$refs.discardDraftWrap;
this.deleteDraftButton = this.$refs.deleteDraft;
this.deleteDraftWrap = this.$refs.deleteDraftWrap;
this.draftDisplay = this.$refs.draftDisplay;
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
this.changelogInput = this.$refs.changelogInput;
this.changelogDisplay = this.$refs.changelogDisplay;
this.changeEditorButtons = this.$manyRefs.changeEditor || [];
this.switchDialogContainer = this.$refs.switchDialog;
this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
// Translations
this.draftText = this.$opts.draftText;
this.autosaveFailText = this.$opts.autosaveFailText;
this.editingPageText = this.$opts.editingPageText;
this.draftDiscardedText = this.$opts.draftDiscardedText;
this.draftDeleteText = this.$opts.draftDeleteText;
this.draftDeleteFailText = this.$opts.draftDeleteFailText;
this.setChangelogText = this.$opts.setChangelogText;
// State data
this.autoSave = {
interval: null,
frequency: 30000,
last: 0,
pendingChange: false,
};
this.shownWarningsCache = new Set();
if (this.pageId !== 0 && this.draftsEnabled) {
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
}
this.draftDisplay.innerHTML = this.draftText;
this.setupListeners();
this.setInitialFocus();
}
setupListeners() {
// Listen to save events from editor
window.$events.listen('editor-save-draft', this.saveDraft.bind(this));
window.$events.listen('editor-save-page', this.savePage.bind(this));
// Listen to content changes from the editor
const onContentChange = () => {
this.autoSave.pendingChange = true;
};
window.$events.listen('editor-html-change', onContentChange);
window.$events.listen('editor-markdown-change', onContentChange);
// Listen to changes on the title input
this.titleElem.addEventListener('input', onContentChange);
// Changelog controls
const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
this.changelogInput.addEventListener('input', updateChangelogDebounced);
// Draft Controls
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
// Change editor controls
onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
}
setInitialFocus() {
if (this.hasDefaultTitle) {
this.titleElem.select();
return;
}
window.setTimeout(() => {
window.$events.emit('editor::focus', '');
}, 500);
}
startAutoSave() {
this.autoSave.interval = window.setInterval(this.runAutoSave.bind(this), this.autoSave.frequency);
}
runAutoSave() {
// Stop if manually saved recently to prevent bombarding the server
const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency) / 2);
if (savedRecently || !this.autoSave.pendingChange) {
return;
}
this.saveDraft();
}
savePage() {
this.container.closest('form').submit();
}
async saveDraft() {
const data = {name: this.titleElem.value.trim()};
const editorContent = await this.getEditorComponent().getContent();
Object.assign(data, editorContent);
let didSave = false;
try {
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
if (!this.isNewDraft) {
this.discardDraftWrap.toggleAttribute('hidden', false);
this.deleteDraftWrap.toggleAttribute('hidden', false);
}
this.draftNotifyChange(`${resp.data.message} ${utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
this.shownWarningsCache.add(resp.data.warning);
}
didSave = true;
this.autoSave.pendingChange = false;
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
try {
const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
window.localStorage.setItem(saveKey, JSON.stringify(data));
} catch (lsErr) {
console.error(lsErr);
}
window.$events.emit('error', this.autosaveFailText);
}
return didSave;
}
draftNotifyChange(text) {
this.draftDisplay.innerText = text;
this.draftDisplayIcon.classList.add('visible');
window.setTimeout(() => {
this.draftDisplayIcon.classList.remove('visible');
}, 2000);
}
async discardDraft(notify = true) {
let response;
try {
response = await window.$http.get(`/ajax/page/${this.pageId}`);
} catch (e) {
console.error(e);
return;
}
if (this.autoSave.interval) {
window.clearInterval(this.autoSave.interval);
}
this.draftDisplay.innerText = this.editingPageText;
this.discardDraftWrap.toggleAttribute('hidden', true);
window.$events.emit('editor::replace', {
html: response.data.html,
markdown: response.data.markdown,
});
this.titleElem.value = response.data.name;
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
if (notify) {
window.$events.success(this.draftDiscardedText);
}
}
async deleteDraft() {
/** @var {ConfirmDialog} * */
const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
const confirmed = await dialog.show();
if (!confirmed) {
return;
}
try {
const discard = this.discardDraft(false);
const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
await Promise.all([discard, draftDelete]);
window.$events.success(this.draftDeleteText);
this.deleteDraftWrap.toggleAttribute('hidden', true);
} catch (err) {
console.error(err);
window.$events.error(this.draftDeleteFailText);
}
}
updateChangelogDisplay() {
let summary = this.changelogInput.value.trim();
if (summary.length === 0) {
summary = this.setChangelogText;
} else if (summary.length > 16) {
summary = `${summary.slice(0, 16)}...`;
}
this.changelogDisplay.innerText = summary;
}
async changeEditor(event) {
event.preventDefault();
const link = event.target.closest('a').href;
/** @var {ConfirmDialog} * */
const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
if (saved && confirmed) {
window.location = link;
}
}
/**
* @return {MarkdownEditor|WysiwygEditor|WysiwygEditorTinymce}
*/
getEditorComponent() {
return window.$components.first('markdown-editor')
|| window.$components.first('wysiwyg-editor')
|| window.$components.first('wysiwyg-editor-tinymce');
}
}