1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-04-27 15:57:04 +03:00
bookstack/resources/js/components/page-editor.js
Dan Brown 5c6671b3bf
Lexical: Fixed issues with content not saving
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.
2025-03-27 14:13:18 +00:00

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');
}
}