mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-27 15:57:04 +03:00
Found that saving via Ctrl+Enter did not save as logic to load editor output into form was bypassed, which this fixes by ensuring submit events are raised during for this shortcut. Submit handling also gets a timeout added since, at least in FF, requestSubmit did not re-submit a form while in a submit event.
247 lines
8.5 KiB
JavaScript
247 lines
8.5 KiB
JavaScript
import {onSelect} from '../services/dom.ts';
|
|
import {debounce} from '../services/util.ts';
|
|
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').requestSubmit();
|
|
}
|
|
|
|
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 {
|
|
// 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');
|
|
}
|
|
|
|
}
|