mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-03 02:13:16 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			252 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			252 lines
		
	
	
		
			8.8 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.changelogCounter = this.$refs.changelogCounter;
 | 
						|
        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', () => {
 | 
						|
            const count = this.changelogInput.value.length;
 | 
						|
            this.changelogCounter.innerText = `${count} / 180`;
 | 
						|
            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');
 | 
						|
    }
 | 
						|
 | 
						|
}
 |