mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	converted note revisions to new pattern
This commit is contained in:
		
							parent
							
								
									e140daa952
								
							
						
					
					
						commit
						eb8e5eafb6
					
				@ -1,232 +0,0 @@
 | 
				
			|||||||
import utils from '../services/utils.js';
 | 
					 | 
				
			||||||
import server from '../services/server.js';
 | 
					 | 
				
			||||||
import toastService from "../services/toast.js";
 | 
					 | 
				
			||||||
import appContext from "../services/app_context.js";
 | 
					 | 
				
			||||||
import libraryLoader from "../services/library_loader.js";
 | 
					 | 
				
			||||||
import openService from "../services/open.js";
 | 
					 | 
				
			||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $dialog = $("#note-revisions-dialog");
 | 
					 | 
				
			||||||
const $list = $("#note-revision-list");
 | 
					 | 
				
			||||||
const $listDropdown = $("#note-revision-list-dropdown");
 | 
					 | 
				
			||||||
const $content = $("#note-revision-content");
 | 
					 | 
				
			||||||
const $title = $("#note-revision-title");
 | 
					 | 
				
			||||||
const $titleButtons = $("#note-revision-title-buttons");
 | 
					 | 
				
			||||||
const $eraseAllRevisionsButton = $("#note-revisions-erase-all-revisions-button");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$listDropdown.dropdown();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$listDropdown.parent().on('hide.bs.dropdown', e => {
 | 
					 | 
				
			||||||
    // prevent closing dropdown by clicking outside
 | 
					 | 
				
			||||||
    if (e.clickEvent) {
 | 
					 | 
				
			||||||
        e.preventDefault();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let revisionItems = [];
 | 
					 | 
				
			||||||
let note;
 | 
					 | 
				
			||||||
let noteRevisionId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function showCurrentNoteRevisions() {
 | 
					 | 
				
			||||||
    await showNoteRevisionsDialog(appContext.tabManager.getActiveContextNoteId());
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function showNoteRevisionsDialog(noteId, noteRevisionId) {
 | 
					 | 
				
			||||||
    utils.openDialog($dialog);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await loadNoteRevisions(noteId, noteRevisionId);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function loadNoteRevisions(noteId, noteRevId) {
 | 
					 | 
				
			||||||
    $list.empty();
 | 
					 | 
				
			||||||
    $content.empty();
 | 
					 | 
				
			||||||
    $titleButtons.empty();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    note = appContext.tabManager.getActiveContextNote();
 | 
					 | 
				
			||||||
    revisionItems = await server.get(`notes/${noteId}/revisions`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const item of revisionItems) {
 | 
					 | 
				
			||||||
        $list.append(
 | 
					 | 
				
			||||||
            $('<a class="dropdown-item" tabindex="0">')
 | 
					 | 
				
			||||||
                .text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
 | 
					 | 
				
			||||||
                .attr('data-note-revision-id', item.noteRevisionId)
 | 
					 | 
				
			||||||
                .attr('title', 'This revision was last edited on ' + item.dateLastEdited)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $listDropdown.dropdown('show');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    noteRevisionId = noteRevId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (revisionItems.length > 0) {
 | 
					 | 
				
			||||||
        if (!noteRevisionId) {
 | 
					 | 
				
			||||||
            noteRevisionId = revisionItems[0].noteRevisionId;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        $title.text("No revisions for this note yet...");
 | 
					 | 
				
			||||||
        noteRevisionId = null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $eraseAllRevisionsButton.toggle(revisionItems.length > 0);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$dialog.on('shown.bs.modal', () => {
 | 
					 | 
				
			||||||
    $list.find(`[data-note-revision-id="${noteRevisionId}"]`)
 | 
					 | 
				
			||||||
        .trigger('focus');
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function setContentPane() {
 | 
					 | 
				
			||||||
    const noteRevisionId = $list.find(".active").attr('data-note-revision-id');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const revisionItem = revisionItems.find(r => r.noteRevisionId === noteRevisionId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $titleButtons.empty();
 | 
					 | 
				
			||||||
    $content.empty();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $title.html(revisionItem.title);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $restoreRevisionButton.on('click', async () => {
 | 
					 | 
				
			||||||
        const confirmDialog = await import('../dialogs/confirm.js');
 | 
					 | 
				
			||||||
        const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (await confirmDialog.confirm(text)) {
 | 
					 | 
				
			||||||
            await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $dialog.modal('hide');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            toastService.showMessage('Note revision has been restored.');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $eraseRevisionButton.on('click', async () => {
 | 
					 | 
				
			||||||
        const confirmDialog = await import('../dialogs/confirm.js');
 | 
					 | 
				
			||||||
        const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (await confirmDialog.confirm(text)) {
 | 
					 | 
				
			||||||
            await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            loadNoteRevisions(revisionItem.noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            toastService.showMessage('Note revision has been deleted.');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
					 | 
				
			||||||
        $titleButtons
 | 
					 | 
				
			||||||
            .append($restoreRevisionButton)
 | 
					 | 
				
			||||||
            .append('   ');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $titleButtons
 | 
					 | 
				
			||||||
        .append($eraseRevisionButton)
 | 
					 | 
				
			||||||
        .append('   ');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $downloadButton.on('click', () => openService.downloadNoteRevision(revisionItem.noteId, revisionItem.noteRevisionId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
					 | 
				
			||||||
        $titleButtons.append($downloadButton);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (revisionItem.type === 'text') {
 | 
					 | 
				
			||||||
        $content.html(fullNoteRevision.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($content.find('span.math-tex').length > 0) {
 | 
					 | 
				
			||||||
            await libraryLoader.requireLibrary(libraryLoader.KATEX);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            renderMathInElement($content[0], {trust: true});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (revisionItem.type === 'code') {
 | 
					 | 
				
			||||||
        $content.html($("<pre>").text(fullNoteRevision.content));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (revisionItem.type === 'image') {
 | 
					 | 
				
			||||||
        $content.html($("<img>")
 | 
					 | 
				
			||||||
            // reason why we put this inline as base64 is that we do not want to let user to copy this
 | 
					 | 
				
			||||||
            // as a URL to be used in a note. Instead if they copy and paste it into a note, it will be a uploaded as a new note
 | 
					 | 
				
			||||||
            .attr("src", `data:${note.mime};base64,` + fullNoteRevision.content)
 | 
					 | 
				
			||||||
            .css("max-width", "100%")
 | 
					 | 
				
			||||||
            .css("max-height", "100%"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (revisionItem.type === 'file') {
 | 
					 | 
				
			||||||
        const $table = $("<table cellpadding='10'>")
 | 
					 | 
				
			||||||
            .append($("<tr>").append(
 | 
					 | 
				
			||||||
                $("<th>").text("MIME: "),
 | 
					 | 
				
			||||||
                $("<td>").text(revisionItem.mime)
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
            .append($("<tr>").append(
 | 
					 | 
				
			||||||
                $("<th>").text("File size:"),
 | 
					 | 
				
			||||||
                $("<td>").text(revisionItem.contentLength + " bytes")
 | 
					 | 
				
			||||||
            ));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (fullNoteRevision.content) {
 | 
					 | 
				
			||||||
            $table.append($("<tr>").append(
 | 
					 | 
				
			||||||
                $('<td colspan="2">').append(
 | 
					 | 
				
			||||||
                    $('<div style="font-weight: bold;">').text("Preview:"),
 | 
					 | 
				
			||||||
                    $('<pre class="file-preview-content"></pre>')
 | 
					 | 
				
			||||||
                        .text(fullNoteRevision.content)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            ));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $content.html($table);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (revisionItem.type === 'canvas') {
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
 | 
					 | 
				
			||||||
         *        REMOVE external dependency!!!! This is defined in the svg in defs.style
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        const content = fullNoteRevision.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            const data = JSON.parse(content)
 | 
					 | 
				
			||||||
            const svg = data.svg || "no svg present."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            /**
 | 
					 | 
				
			||||||
             * maxWidth: 100% use full width of container but do not enlarge!
 | 
					 | 
				
			||||||
             * height:auto to ensure that height scales with width
 | 
					 | 
				
			||||||
             */
 | 
					 | 
				
			||||||
            const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
 | 
					 | 
				
			||||||
            $content.html($('<div>').append($svgHtml));
 | 
					 | 
				
			||||||
        } catch(err) {
 | 
					 | 
				
			||||||
            console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err);
 | 
					 | 
				
			||||||
            $content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        $content.text("Preview isn't available for this note type.");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$eraseAllRevisionsButton.on('click', async () => {
 | 
					 | 
				
			||||||
    const confirmDialog = await import('../dialogs/confirm.js');
 | 
					 | 
				
			||||||
    const text = 'Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (await confirmDialog.confirm(text)) {
 | 
					 | 
				
			||||||
        await server.remove(`notes/${note.noteId}/revisions`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $dialog.modal('hide');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        toastService.showMessage('Note revisions has been deleted.');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$list.on('click', '.dropdown-item', e => {
 | 
					 | 
				
			||||||
   e.preventDefault();
 | 
					 | 
				
			||||||
   return false;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$list.on('focus', '.dropdown-item', e => {
 | 
					 | 
				
			||||||
    $list.find('.dropdown-item').each((i, el) => {
 | 
					 | 
				
			||||||
        $(el).toggleClass('active', el === e.target);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setContentPane();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -69,6 +69,7 @@ import ImportDialog from "../widgets/dialogs/import.js";
 | 
				
			|||||||
import ExportDialog from "../widgets/dialogs/export.js";
 | 
					import ExportDialog from "../widgets/dialogs/export.js";
 | 
				
			||||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
 | 
					import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
 | 
				
			||||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
 | 
					import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
 | 
				
			||||||
 | 
					import NoteRevisionsDialog from "../widgets/dialogs/note_revisions.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class DesktopLayout {
 | 
					export default class DesktopLayout {
 | 
				
			||||||
    constructor(customWidgets) {
 | 
					    constructor(customWidgets) {
 | 
				
			||||||
@ -212,6 +213,7 @@ export default class DesktopLayout {
 | 
				
			|||||||
            .child(new ImportDialog())
 | 
					            .child(new ImportDialog())
 | 
				
			||||||
            .child(new ExportDialog())
 | 
					            .child(new ExportDialog())
 | 
				
			||||||
            .child(new MarkdownImportDialog())
 | 
					            .child(new MarkdownImportDialog())
 | 
				
			||||||
            .child(new ProtectedSessionPasswordDialog());
 | 
					            .child(new ProtectedSessionPasswordDialog())
 | 
				
			||||||
 | 
					            .child(new NoteRevisionsDialog());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,6 @@ import server from "./server.js";
 | 
				
			|||||||
import appContext from "./app_context.js";
 | 
					import appContext from "./app_context.js";
 | 
				
			||||||
import Component from "../widgets/component.js";
 | 
					import Component from "../widgets/component.js";
 | 
				
			||||||
import toastService from "./toast.js";
 | 
					import toastService from "./toast.js";
 | 
				
			||||||
import noteCreateService from "./note_create.js";
 | 
					 | 
				
			||||||
import ws from "./ws.js";
 | 
					import ws from "./ws.js";
 | 
				
			||||||
import bundleService from "./bundle.js";
 | 
					import bundleService from "./bundle.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,18 +18,6 @@ export default class Entrypoints extends Component {
 | 
				
			|||||||
            jQuery.hotkeys.options.filterContentEditable = false;
 | 
					            jQuery.hotkeys.options.filterContentEditable = false;
 | 
				
			||||||
            jQuery.hotkeys.options.filterTextInputs = false;
 | 
					            jQuery.hotkeys.options.filterTextInputs = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        $(document).on('click', "a[data-action='note-revision']", async event => {
 | 
					 | 
				
			||||||
            const linkEl = $(event.target);
 | 
					 | 
				
			||||||
            const noteId = linkEl.attr('data-note-path');
 | 
					 | 
				
			||||||
            const noteRevisionId = linkEl.attr('data-note-revision-id');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const attributesDialog = await import("../dialogs/note_revisions.js");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            attributesDialog.showNoteRevisionsDialog(noteId, noteRevisionId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    openDevToolsCommand() {
 | 
					    openDevToolsCommand() {
 | 
				
			||||||
 | 
				
			|||||||
@ -22,8 +22,7 @@ async function mouseEnterHandler() {
 | 
				
			|||||||
    const $link = $(this);
 | 
					    const $link = $(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ($link.hasClass("no-tooltip-preview")
 | 
					    if ($link.hasClass("no-tooltip-preview")
 | 
				
			||||||
        || $link.hasClass("disabled")
 | 
					        || $link.hasClass("disabled")) {
 | 
				
			||||||
        || $link.attr("data-action") === 'note-revision') {
 | 
					 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,14 +8,6 @@ import options from "./options.js";
 | 
				
			|||||||
import froca from "./froca.js";
 | 
					import froca from "./froca.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class RootCommandExecutor extends Component {
 | 
					export default class RootCommandExecutor extends Component {
 | 
				
			||||||
    showNoteRevisionsCommand() {
 | 
					 | 
				
			||||||
        import("../dialogs/note_revisions.js").then(d => d.showCurrentNoteRevisions());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pasteMarkdownIntoTextCommand() {
 | 
					 | 
				
			||||||
        import("../widgets/dialogs/markdown_import.js").then(d => d.importMarkdownInline());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    editReadOnlyNoteCommand() {
 | 
					    editReadOnlyNoteCommand() {
 | 
				
			||||||
        const noteContext = appContext.tabManager.getActiveContext();
 | 
					        const noteContext = appContext.tabManager.getActiveContext();
 | 
				
			||||||
        noteContext.readOnlyTemporarilyDisabled = true;
 | 
					        noteContext.readOnlyTemporarilyDisabled = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -64,6 +64,10 @@ export default class MarkdownImportDialog extends BasicWidget {
 | 
				
			|||||||
        toastService.showMessage("Markdown content has been imported into the document.");
 | 
					        toastService.showMessage("Markdown content has been imported into the document.");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async pasteMarkdownIntoTextEvent() {
 | 
				
			||||||
 | 
					        await this.importMarkdownInlineEvent(); // BC with keyboard shortcuts command
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async importMarkdownInlineEvent() {
 | 
					    async importMarkdownInlineEvent() {
 | 
				
			||||||
        if (appContext.tabManager.getActiveContextNoteType() !== 'text') {
 | 
					        if (appContext.tabManager.getActiveContextNoteType() !== 'text') {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										299
									
								
								src/public/app/widgets/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								src/public/app/widgets/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,299 @@
 | 
				
			|||||||
 | 
					import utils from '../../services/utils.js';
 | 
				
			||||||
 | 
					import server from '../../services/server.js';
 | 
				
			||||||
 | 
					import toastService from "../../services/toast.js";
 | 
				
			||||||
 | 
					import appContext from "../../services/app_context.js";
 | 
				
			||||||
 | 
					import libraryLoader from "../../services/library_loader.js";
 | 
				
			||||||
 | 
					import openService from "../../services/open.js";
 | 
				
			||||||
 | 
					import protectedSessionHolder from "../../services/protected_session_holder.js";
 | 
				
			||||||
 | 
					import BasicWidget from "../basic_widget.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TPL = `
 | 
				
			||||||
 | 
					<div class="note-revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog">
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        .note-revision-content-wrapper {
 | 
				
			||||||
 | 
					            flex-grow: 1;
 | 
				
			||||||
 | 
					            margin-left: 20px;
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
 | 
					            flex-direction: column;
 | 
				
			||||||
 | 
					            min-width: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .note-revision-content {
 | 
				
			||||||
 | 
					            overflow: auto;
 | 
				
			||||||
 | 
					            word-break: break-word;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .note-revision-content img {
 | 
				
			||||||
 | 
					            max-width: 100%;
 | 
				
			||||||
 | 
					            object-fit: contain;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .note-revision-content pre {
 | 
				
			||||||
 | 
					            max-width: 100%;
 | 
				
			||||||
 | 
					            word-break: break-all;
 | 
				
			||||||
 | 
					            white-space: pre-wrap;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="modal-dialog modal-xl" role="document">
 | 
				
			||||||
 | 
					        <div class="modal-content">
 | 
				
			||||||
 | 
					            <div class="modal-header">
 | 
				
			||||||
 | 
					                <h5 class="modal-title mr-auto">Note revisions</h5>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <button class="note-revisions-erase-all-revisions-button btn btn-xs"
 | 
				
			||||||
 | 
					                        title="Delete all revisions of this note"
 | 
				
			||||||
 | 
					                        style="padding: 0 10px 0 10px;" type="button">Delete all revisions</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <button class="help-button" type="button" data-help-page="Note-revisions" title="Help on Note revisions">?</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
 | 
				
			||||||
 | 
					                    <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="modal-body" style="display: flex; height: 80vh;">
 | 
				
			||||||
 | 
					                <div class="dropdown">
 | 
				
			||||||
 | 
					                    <button class="note-revision-list-dropdown" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <div class="note-revision-list dropdown-menu" style="position: static; height: 100%; overflow: auto;"></div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="note-revision-content-wrapper">
 | 
				
			||||||
 | 
					                    <div style="flex-grow: 0; display: flex; justify-content: space-between;">
 | 
				
			||||||
 | 
					                        <h3 class="note-revision-title" style="margin: 3px; flex-grow: 100;"></h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <div class="note-revision-title-buttons"></div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <div class="note-revision-content"></div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class NoteRevisionsDialog extends BasicWidget {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.revisionItems = [];
 | 
				
			||||||
 | 
					        this.note = null;
 | 
				
			||||||
 | 
					        this.noteRevisionId = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    doRender() {
 | 
				
			||||||
 | 
					        this.$widget = $(TPL);
 | 
				
			||||||
 | 
					        this.$list = this.$widget.find(".note-revision-list");
 | 
				
			||||||
 | 
					        this.$listDropdown = this.$widget.find(".note-revision-list-dropdown");
 | 
				
			||||||
 | 
					        this.$content = this.$widget.find(".note-revision-content");
 | 
				
			||||||
 | 
					        this.$title = this.$widget.find(".note-revision-title");
 | 
				
			||||||
 | 
					        this.$titleButtons = this.$widget.find(".note-revision-title-buttons");
 | 
				
			||||||
 | 
					        this.$eraseAllRevisionsButton = this.$widget.find(".note-revisions-erase-all-revisions-button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$listDropdown.dropdown();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$listDropdown.parent().on('hide.bs.dropdown', e => {
 | 
				
			||||||
 | 
					            // prevent closing dropdown by clicking outside
 | 
				
			||||||
 | 
					            if (e.clickEvent) {
 | 
				
			||||||
 | 
					                e.preventDefault();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$widget.on('shown.bs.modal', () => {
 | 
				
			||||||
 | 
					            this.$list.find(`[data-note-revision-id="${this.noteRevisionId}"]`)
 | 
				
			||||||
 | 
					                .trigger('focus');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$eraseAllRevisionsButton.on('click', async () => {
 | 
				
			||||||
 | 
					            const confirmDialog = await import('../../dialogs/confirm.js');
 | 
				
			||||||
 | 
					            const text = 'Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (await confirmDialog.confirm(text)) {
 | 
				
			||||||
 | 
					                await server.remove(`notes/${this.note.noteId}/revisions`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.$widget.modal('hide');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                toastService.showMessage('Note revisions has been deleted.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$list.on('click', '.dropdown-item', e => {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$list.on('focus', '.dropdown-item', e => {
 | 
				
			||||||
 | 
					            this.$list.find('.dropdown-item').each((i, el) => {
 | 
				
			||||||
 | 
					                $(el).toggleClass('active', el === e.target);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.setContentPane();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async showNoteRevisionsEvent({noteId = appContext.tabManager.getActiveContextNoteId()}) {
 | 
				
			||||||
 | 
					        utils.openDialog(this.$widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.loadNoteRevisions(noteId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async loadNoteRevisions(noteId) {
 | 
				
			||||||
 | 
					        this.$list.empty();
 | 
				
			||||||
 | 
					        this.$content.empty();
 | 
				
			||||||
 | 
					        this.$titleButtons.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.note = appContext.tabManager.getActiveContextNote();
 | 
				
			||||||
 | 
					        this.revisionItems = await server.get(`notes/${noteId}/revisions`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const item of this.revisionItems) {
 | 
				
			||||||
 | 
					            this.$list.append(
 | 
				
			||||||
 | 
					                $('<a class="dropdown-item" tabindex="0">')
 | 
				
			||||||
 | 
					                    .text(item.dateLastEdited.substr(0, 16) + ` (${item.contentLength} bytes)`)
 | 
				
			||||||
 | 
					                    .attr('data-note-revision-id', item.noteRevisionId)
 | 
				
			||||||
 | 
					                    .attr('title', 'This revision was last edited on ' + item.dateLastEdited)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$listDropdown.dropdown('show');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.revisionItems.length > 0) {
 | 
				
			||||||
 | 
					            if (!this.noteRevisionId) {
 | 
				
			||||||
 | 
					                this.noteRevisionId = this.revisionItems[0].noteRevisionId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.$title.text("No revisions for this note yet...");
 | 
				
			||||||
 | 
					            this.noteRevisionId = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$eraseAllRevisionsButton.toggle(this.revisionItems.length > 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async setContentPane() {
 | 
				
			||||||
 | 
					        const noteRevisionId = this.$list.find(".active").attr('data-note-revision-id');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const revisionItem = this.revisionItems.find(r => r.noteRevisionId === noteRevisionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$titleButtons.empty();
 | 
				
			||||||
 | 
					        this.$content.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$title.html(revisionItem.title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const $restoreRevisionButton = $('<button class="btn btn-sm" type="button">Restore this revision</button>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $restoreRevisionButton.on('click', async () => {
 | 
				
			||||||
 | 
					            const confirmDialog = await import('../../dialogs/confirm.js');
 | 
				
			||||||
 | 
					            const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (await confirmDialog.confirm(text)) {
 | 
				
			||||||
 | 
					                await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.$widget.modal('hide');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                toastService.showMessage('Note revision has been restored.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const $eraseRevisionButton = $('<button class="btn btn-sm" type="button">Delete this revision</button>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $eraseRevisionButton.on('click', async () => {
 | 
				
			||||||
 | 
					            const confirmDialog = await import('../../dialogs/confirm.js');
 | 
				
			||||||
 | 
					            const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (await confirmDialog.confirm(text)) {
 | 
				
			||||||
 | 
					                await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.loadNoteRevisions(revisionItem.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                toastService.showMessage('Note revision has been deleted.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
				
			||||||
 | 
					            this.$titleButtons
 | 
				
			||||||
 | 
					                .append($restoreRevisionButton)
 | 
				
			||||||
 | 
					                .append('   ');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$titleButtons
 | 
				
			||||||
 | 
					            .append($eraseRevisionButton)
 | 
				
			||||||
 | 
					            .append('   ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $downloadButton.on('click', () => openService.downloadNoteRevision(revisionItem.noteId, revisionItem.noteRevisionId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
				
			||||||
 | 
					            this.$titleButtons.append($downloadButton);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (revisionItem.type === 'text') {
 | 
				
			||||||
 | 
					            this.$content.html(fullNoteRevision.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.$content.find('span.math-tex').length > 0) {
 | 
				
			||||||
 | 
					                await libraryLoader.requireLibrary(libraryLoader.KATEX);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                renderMathInElement($content[0], {trust: true});
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (revisionItem.type === 'code') {
 | 
				
			||||||
 | 
					            this.$content.html($("<pre>").text(fullNoteRevision.content));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (revisionItem.type === 'image') {
 | 
				
			||||||
 | 
					            this.$content.html($("<img>")
 | 
				
			||||||
 | 
					                // reason why we put this inline as base64 is that we do not want to let user to copy this
 | 
				
			||||||
 | 
					                // as a URL to be used in a note. Instead if they copy and paste it into a note, it will be a uploaded as a new note
 | 
				
			||||||
 | 
					                .attr("src", `data:${note.mime};base64,` + fullNoteRevision.content)
 | 
				
			||||||
 | 
					                .css("max-width", "100%")
 | 
				
			||||||
 | 
					                .css("max-height", "100%"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (revisionItem.type === 'file') {
 | 
				
			||||||
 | 
					            const $table = $("<table cellpadding='10'>")
 | 
				
			||||||
 | 
					                .append($("<tr>").append(
 | 
				
			||||||
 | 
					                    $("<th>").text("MIME: "),
 | 
				
			||||||
 | 
					                    $("<td>").text(revisionItem.mime)
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					                .append($("<tr>").append(
 | 
				
			||||||
 | 
					                    $("<th>").text("File size:"),
 | 
				
			||||||
 | 
					                    $("<td>").text(revisionItem.contentLength + " bytes")
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (fullNoteRevision.content) {
 | 
				
			||||||
 | 
					                $table.append($("<tr>").append(
 | 
				
			||||||
 | 
					                    $('<td colspan="2">').append(
 | 
				
			||||||
 | 
					                        $('<div style="font-weight: bold;">').text("Preview:"),
 | 
				
			||||||
 | 
					                        $('<pre class="file-preview-content"></pre>')
 | 
				
			||||||
 | 
					                            .text(fullNoteRevision.content)
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.$content.html($table);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (revisionItem.type === 'canvas') {
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					             * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
 | 
				
			||||||
 | 
					             *        REMOVE external dependency!!!! This is defined in the svg in defs.style
 | 
				
			||||||
 | 
					             */
 | 
				
			||||||
 | 
					            const content = fullNoteRevision.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                const data = JSON.parse(content)
 | 
				
			||||||
 | 
					                const svg = data.svg || "no svg present."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                /**
 | 
				
			||||||
 | 
					                 * maxWidth: 100% use full width of container but do not enlarge!
 | 
				
			||||||
 | 
					                 * height:auto to ensure that height scales with width
 | 
				
			||||||
 | 
					                 */
 | 
				
			||||||
 | 
					                const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
 | 
				
			||||||
 | 
					                this.$content.html($('<div>').append($svgHtml));
 | 
				
			||||||
 | 
					            } catch(err) {
 | 
				
			||||||
 | 
					                console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err);
 | 
				
			||||||
 | 
					                this.$content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            this.$content.text("Preview isn't available for this note type.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -17,7 +17,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
 | 
					<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%- include('dialogs/note_revisions.ejs') %>
 | 
					 | 
				
			||||||
<%- include('dialogs/options.ejs') %>
 | 
					<%- include('dialogs/options.ejs') %>
 | 
				
			||||||
<%- include('dialogs/info.ejs') %>
 | 
					<%- include('dialogs/info.ejs') %>
 | 
				
			||||||
<%- include('dialogs/prompt.ejs') %>
 | 
					<%- include('dialogs/prompt.ejs') %>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,63 +0,0 @@
 | 
				
			|||||||
<div id="note-revisions-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog">
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        #note-revision-content-wrapper {
 | 
					 | 
				
			||||||
            flex-grow: 1;
 | 
					 | 
				
			||||||
            margin-left: 20px;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            flex-direction: column;
 | 
					 | 
				
			||||||
            min-width: 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #note-revision-content {
 | 
					 | 
				
			||||||
            overflow: auto;
 | 
					 | 
				
			||||||
            word-break: break-word;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #note-revision-content img {
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            object-fit: contain;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #note-revision-content pre {
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            word-break: break-all;
 | 
					 | 
				
			||||||
            white-space: pre-wrap;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="modal-dialog modal-xl" role="document">
 | 
					 | 
				
			||||||
        <div class="modal-content">
 | 
					 | 
				
			||||||
            <div class="modal-header">
 | 
					 | 
				
			||||||
                <h5 class="modal-title mr-auto">Note revisions</h5>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <button class="btn btn-xs"
 | 
					 | 
				
			||||||
                        id="note-revisions-erase-all-revisions-button"
 | 
					 | 
				
			||||||
                        title="Delete all revisions of this note"
 | 
					 | 
				
			||||||
                        style="padding: 0 10px 0 10px;" type="button">Delete all revisions</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <button class="help-button" type="button" data-help-page="Note-revisions" title="Help on Note revisions">?</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
 | 
					 | 
				
			||||||
                    <span aria-hidden="true">×</span>
 | 
					 | 
				
			||||||
                </button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="modal-body" style="display: flex; height: 80vh;">
 | 
					 | 
				
			||||||
                <div class="dropdown">
 | 
					 | 
				
			||||||
                    <button id="note-revision-list-dropdown" type="button" style="display: none;" data-toggle="dropdown">Dropdown trigger</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <div id="note-revision-list" style="position: static; height: 100%; overflow: auto;" class="dropdown-menu"></div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <div id="note-revision-content-wrapper">
 | 
					 | 
				
			||||||
                    <div style="flex-grow: 0; display: flex; justify-content: space-between;">
 | 
					 | 
				
			||||||
                        <h3 id="note-revision-title" style="margin: 3px; flex-grow: 100;"></h3>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <div id="note-revision-title-buttons"></div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <div id="note-revision-content"></div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user