mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 13:39:01 +01:00 
			
		
		
		
	Merge branch 'beta'
# Conflicts: # package-lock.json
This commit is contained in:
		
						commit
						035113db4d
					
				@ -75,7 +75,6 @@ module.exports = {
 | 
				
			|||||||
        glob: true,
 | 
					        glob: true,
 | 
				
			||||||
        log: true,
 | 
					        log: true,
 | 
				
			||||||
        EditorWatchdog: true,
 | 
					        EditorWatchdog: true,
 | 
				
			||||||
        // \src\share\canvas_share.js
 | 
					 | 
				
			||||||
        React: true,
 | 
					        React: true,
 | 
				
			||||||
        appState: true,
 | 
					        appState: true,
 | 
				
			||||||
        ExcalidrawLib: true,
 | 
					        ExcalidrawLib: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -68,7 +68,7 @@
 | 
				
			|||||||
    "jimp": "0.22.10",
 | 
					    "jimp": "0.22.10",
 | 
				
			||||||
    "joplin-turndown-plugin-gfm": "1.0.12",
 | 
					    "joplin-turndown-plugin-gfm": "1.0.12",
 | 
				
			||||||
    "jsdom": "22.1.0",
 | 
					    "jsdom": "22.1.0",
 | 
				
			||||||
    "marked": "8.0.1",
 | 
					    "marked": "9.0.0",
 | 
				
			||||||
    "mime-types": "2.1.35",
 | 
					    "mime-types": "2.1.35",
 | 
				
			||||||
    "multer": "1.4.5-lts.1",
 | 
					    "multer": "1.4.5-lts.1",
 | 
				
			||||||
    "node-abi": "3.47.0",
 | 
					    "node-abi": "3.47.0",
 | 
				
			||||||
@ -91,13 +91,13 @@
 | 
				
			|||||||
    "tmp": "0.2.1",
 | 
					    "tmp": "0.2.1",
 | 
				
			||||||
    "turndown": "7.1.2",
 | 
					    "turndown": "7.1.2",
 | 
				
			||||||
    "unescape": "1.0.1",
 | 
					    "unescape": "1.0.1",
 | 
				
			||||||
    "ws": "8.14.0",
 | 
					    "ws": "8.14.1",
 | 
				
			||||||
    "xml2js": "0.6.2",
 | 
					    "xml2js": "0.6.2",
 | 
				
			||||||
    "yauzl": "2.10.0"
 | 
					    "yauzl": "2.10.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "cross-env": "7.0.3",
 | 
					    "cross-env": "7.0.3",
 | 
				
			||||||
    "electron": "25.8.0",
 | 
					    "electron": "25.8.1",
 | 
				
			||||||
    "electron-builder": "24.6.4",
 | 
					    "electron-builder": "24.6.4",
 | 
				
			||||||
    "electron-packager": "17.1.2",
 | 
					    "electron-packager": "17.1.2",
 | 
				
			||||||
    "electron-rebuild": "3.2.9",
 | 
					    "electron-rebuild": "3.2.9",
 | 
				
			||||||
 | 
				
			|||||||
@ -229,7 +229,9 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
        return this._getContent();
 | 
					        return this._getContent();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {*} */
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {*}
 | 
				
			||||||
 | 
					     * @throws Error in case of invalid JSON */
 | 
				
			||||||
    getJsonContent() {
 | 
					    getJsonContent() {
 | 
				
			||||||
        const content = this.getContent();
 | 
					        const content = this.getContent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -240,6 +242,16 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
        return JSON.parse(content);
 | 
					        return JSON.parse(content);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
 | 
				
			||||||
 | 
					    getJsonContentSafely() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            return this.getJsonContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @param content
 | 
					     * @param content
 | 
				
			||||||
     * @param {object} [opts]
 | 
					     * @param {object} [opts]
 | 
				
			||||||
@ -1143,7 +1155,7 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {BAttachment[]} */
 | 
					    /** @returns {BAttachment[]} */
 | 
				
			||||||
    getAttachmentByRole(role) {
 | 
					    getAttachmentsByRole(role) {
 | 
				
			||||||
        return sql.getRows(`
 | 
					        return sql.getRows(`
 | 
				
			||||||
                SELECT attachments.*
 | 
					                SELECT attachments.*
 | 
				
			||||||
                FROM attachments 
 | 
					                FROM attachments 
 | 
				
			||||||
@ -1154,6 +1166,18 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
            .map(row => new BAttachment(row));
 | 
					            .map(row => new BAttachment(row));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {BAttachment} */
 | 
				
			||||||
 | 
					    getAttachmentByTitle(title) {
 | 
				
			||||||
 | 
					        return sql.getRows(`
 | 
				
			||||||
 | 
					                SELECT attachments.*
 | 
				
			||||||
 | 
					                FROM attachments 
 | 
				
			||||||
 | 
					                WHERE ownerId = ? 
 | 
				
			||||||
 | 
					                  AND title = ?
 | 
				
			||||||
 | 
					                  AND isDeleted = 0
 | 
				
			||||||
 | 
					                ORDER BY position`, [this.noteId, title])
 | 
				
			||||||
 | 
					            .map(row => new BAttachment(row))[0];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
 | 
					     * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
 | 
				
			|||||||
@ -15,4 +15,25 @@ export default class FBlob {
 | 
				
			|||||||
        /** @type {string} */
 | 
					        /** @type {string} */
 | 
				
			||||||
        this.utcDateModified = row.utcDateModified;
 | 
					        this.utcDateModified = row.utcDateModified;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {*}
 | 
				
			||||||
 | 
					     * @throws Error in case of invalid JSON */
 | 
				
			||||||
 | 
					    getJsonContent() {
 | 
				
			||||||
 | 
					        if (!this.content || !this.content.trim()) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return JSON.parse(this.content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
 | 
				
			||||||
 | 
					    getJsonContentSafely() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            return this.getJsonContent();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -255,6 +255,12 @@ class FNote {
 | 
				
			|||||||
        return this.attachments;
 | 
					        return this.attachments;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<FAttachment[]>} */
 | 
				
			||||||
 | 
					    async getAttachmentsByRole(role) {
 | 
				
			||||||
 | 
					        return (await this.getAttachments())
 | 
				
			||||||
 | 
					            .filter(attachment => attachment.role === role);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {Promise<FAttachment>} */
 | 
					    /** @returns {Promise<FAttachment>} */
 | 
				
			||||||
    async getAttachmentById(attachmentId) {
 | 
					    async getAttachmentById(attachmentId) {
 | 
				
			||||||
        const attachments = await this.getAttachments();
 | 
					        const attachments = await this.getAttachments();
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,8 @@ export default class TreeContextMenu {
 | 
				
			|||||||
                    { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
 | 
					                    { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
 | 
				
			||||||
                    { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
 | 
					                    { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
 | 
				
			||||||
                    { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
 | 
					                    { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
 | 
				
			||||||
                    { title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted }
 | 
					                    { title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted },
 | 
				
			||||||
 | 
					                    { title: 'Copy note path to clipboard', command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
 | 
				
			||||||
                ] },
 | 
					                ] },
 | 
				
			||||||
            { title: "----" },
 | 
					            { title: "----" },
 | 
				
			||||||
            { title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
 | 
					            { title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
 | 
				
			||||||
@ -153,6 +154,9 @@ export default class TreeContextMenu {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            toastService.showMessage(`${converted} notes have been converted to attachments.`);
 | 
					            toastService.showMessage(`${converted} notes have been converted to attachments.`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        else if (command === 'copyNotePathToClipboard') {
 | 
				
			||||||
 | 
					            navigator.clipboard.writeText('#' + notePath);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            this.treeWidget.triggerCommand(command, {
 | 
					            this.treeWidget.triggerCommand(command, {
 | 
				
			||||||
                node: this.node,
 | 
					                node: this.node,
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ async function getRenderedContent(entity, options = {}) {
 | 
				
			|||||||
    else if (type === 'code') {
 | 
					    else if (type === 'code') {
 | 
				
			||||||
        await renderCode(entity, $renderedContent);
 | 
					        await renderCode(entity, $renderedContent);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (type === 'image') {
 | 
					    else if (type === 'image' || type === 'canvas') {
 | 
				
			||||||
        renderImage(entity, $renderedContent, options);
 | 
					        renderImage(entity, $renderedContent, options);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
 | 
					    else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
 | 
				
			||||||
@ -49,9 +49,6 @@ async function getRenderedContent(entity, options = {}) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $renderedContent.append($content);
 | 
					        $renderedContent.append($content);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (type === 'canvas') {
 | 
					 | 
				
			||||||
        await renderCanvas(entity, $renderedContent);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (!options.tooltip && type === 'protectedSession') {
 | 
					    else if (!options.tooltip && type === 'protectedSession') {
 | 
				
			||||||
        const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
 | 
					        const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
 | 
				
			||||||
            .on('click', protectedSessionService.enterProtectedSession);
 | 
					            .on('click', protectedSessionService.enterProtectedSession);
 | 
				
			||||||
@ -125,7 +122,7 @@ function renderImage(entity, $renderedContent, options = {}) {
 | 
				
			|||||||
    let url;
 | 
					    let url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (entity instanceof FNote) {
 | 
					    if (entity instanceof FNote) {
 | 
				
			||||||
        url = `api/images/${entity.noteId}/${sanitizedTitle}?${entity.utcDateModified}`;
 | 
					        url = `api/images/${entity.noteId}/${sanitizedTitle}?${Math.random()}`;
 | 
				
			||||||
    } else if (entity instanceof FAttachment) {
 | 
					    } else if (entity instanceof FAttachment) {
 | 
				
			||||||
        url = `api/attachments/${entity.attachmentId}/image/${sanitizedTitle}?${entity.utcDateModified}">`;
 | 
					        url = `api/attachments/${entity.attachmentId}/image/${sanitizedTitle}?${entity.utcDateModified}">`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -236,28 +233,6 @@ async function renderMermaid(note, $renderedContent) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function renderCanvas(note, $renderedContent) {
 | 
					 | 
				
			||||||
    // make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries
 | 
					 | 
				
			||||||
    $renderedContent.css({height: "100%", width: "100%"});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const blob = await note.getBlob();
 | 
					 | 
				
			||||||
    const content = blob.content || "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        const placeHolderSVG = "<svg />";
 | 
					 | 
				
			||||||
        const data = JSON.parse(content)
 | 
					 | 
				
			||||||
        const svg = data.svg || placeHolderSVG;
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * maxWidth: size down to 100% (full) width of container but do not enlarge!
 | 
					 | 
				
			||||||
         * height:auto to ensure that height scales with width
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        $renderedContent.append($(svg).css({maxWidth: "100%", maxHeight: "100%", height: "auto", width: "auto"}));
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
        console.error("error parsing content as JSON", content, err);
 | 
					 | 
				
			||||||
        $renderedContent.append($("<div>").text("Error parsing content. Please check console.error() for more details."));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {jQuery} $renderedContent
 | 
					 * @param {jQuery} $renderedContent
 | 
				
			||||||
 * @param {FNote} note
 | 
					 * @param {FNote} note
 | 
				
			||||||
 | 
				
			|||||||
@ -194,6 +194,10 @@ function goToLink(evt) {
 | 
				
			|||||||
    const $link = $(evt.target).closest("a,.block-link");
 | 
					    const $link = $(evt.target).closest("a,.block-link");
 | 
				
			||||||
    const hrefLink = $link.attr('href') || $link.attr('data-href');
 | 
					    const hrefLink = $link.attr('href') || $link.attr('data-href');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return goToLinkExt(evt, hrefLink, $link);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function goToLinkExt(evt, hrefLink, $link) {
 | 
				
			||||||
    if (hrefLink?.startsWith("data:")) {
 | 
					    if (hrefLink?.startsWith("data:")) {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -201,7 +205,7 @@ function goToLink(evt) {
 | 
				
			|||||||
    evt.preventDefault();
 | 
					    evt.preventDefault();
 | 
				
			||||||
    evt.stopPropagation();
 | 
					    evt.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
 | 
					    const {notePath, viewScope} = parseNavigationStateFromUrl(hrefLink);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ctrlKey = utils.isCtrlKey(evt);
 | 
					    const ctrlKey = utils.isCtrlKey(evt);
 | 
				
			||||||
    const isLeftClick = evt.which === 1;
 | 
					    const isLeftClick = evt.which === 1;
 | 
				
			||||||
@ -213,25 +217,23 @@ function goToLink(evt) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (notePath) {
 | 
					    if (notePath) {
 | 
				
			||||||
        if (openInNewTab) {
 | 
					        if (openInNewTab) {
 | 
				
			||||||
            appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
 | 
					            appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
 | 
				
			||||||
        }
 | 
					        } else if (isLeftClick) {
 | 
				
			||||||
        else if (isLeftClick) {
 | 
					 | 
				
			||||||
            const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
 | 
					            const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const noteContext = ntxId
 | 
					            const noteContext = ntxId
 | 
				
			||||||
                ? appContext.tabManager.getNoteContextById(ntxId)
 | 
					                ? appContext.tabManager.getNoteContextById(ntxId)
 | 
				
			||||||
                : appContext.tabManager.getActiveContext();
 | 
					                : appContext.tabManager.getActiveContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            noteContext.setNote(notePath, { viewScope }).then(() => {
 | 
					            noteContext.setNote(notePath, {viewScope}).then(() => {
 | 
				
			||||||
                if (noteContext !== appContext.tabManager.getActiveContext()) {
 | 
					                if (noteContext !== appContext.tabManager.getActiveContext()) {
 | 
				
			||||||
                    appContext.tabManager.activateNoteContext(noteContext.ntxId);
 | 
					                    appContext.tabManager.activateNoteContext(noteContext.ntxId);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    } else if (hrefLink) {
 | 
				
			||||||
    else if (hrefLink) {
 | 
					        const withinEditLink = $link?.hasClass("ck-link-actions__preview");
 | 
				
			||||||
        const withinEditLink = $link.hasClass("ck-link-actions__preview");
 | 
					        const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
 | 
				
			||||||
        const outsideOfCKEditor = $link.closest("[contenteditable]").length === 0;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (openInNewTab
 | 
					        if (openInNewTab
 | 
				
			||||||
            || (withinEditLink && (leftClick || middleClick))
 | 
					            || (withinEditLink && (leftClick || middleClick))
 | 
				
			||||||
@ -239,8 +241,7 @@ function goToLink(evt) {
 | 
				
			|||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) {
 | 
					            if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) {
 | 
				
			||||||
                window.open(hrefLink, '_blank');
 | 
					                window.open(hrefLink, '_blank');
 | 
				
			||||||
            }
 | 
					            } else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
 | 
				
			||||||
            else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
 | 
					 | 
				
			||||||
                const electron = utils.dynamicRequire('electron');
 | 
					                const electron = utils.dynamicRequire('electron');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                electron.shell.openPath(hrefLink);
 | 
					                electron.shell.openPath(hrefLink);
 | 
				
			||||||
@ -364,6 +365,7 @@ export default {
 | 
				
			|||||||
    getNotePathFromUrl,
 | 
					    getNotePathFromUrl,
 | 
				
			||||||
    createLink,
 | 
					    createLink,
 | 
				
			||||||
    goToLink,
 | 
					    goToLink,
 | 
				
			||||||
 | 
					    goToLinkExt,
 | 
				
			||||||
    loadReferenceLinkTitle,
 | 
					    loadReferenceLinkTitle,
 | 
				
			||||||
    getReferenceLinkTitle,
 | 
					    getReferenceLinkTitle,
 | 
				
			||||||
    getReferenceLinkTitleSync,
 | 
					    getReferenceLinkTitleSync,
 | 
				
			||||||
 | 
				
			|||||||
@ -98,7 +98,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book'].includes(note.type));
 | 
					        this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book'].includes(note.type));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.toggleDisabled(this.$showSourceButton, ['text', 'relationMap', 'mermaid'].includes(note.type));
 | 
					        this.toggleDisabled(this.$showSourceButton, ['text', 'code', 'relationMap', 'mermaid', 'canvas'].includes(note.type));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.toggleDisabled(this.$printActiveNoteButton, ['text', 'code'].includes(note.type));
 | 
					        this.toggleDisabled(this.$printActiveNoteButton, ['text', 'code'].includes(note.type));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -86,6 +86,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            protectedSessionHolder.touchProtectedSessionIfNecessary(note);
 | 
					            protectedSessionHolder.touchProtectedSessionIfNecessary(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await server.put(`notes/${noteId}/data`, data, this.componentId);
 | 
					            await server.put(`notes/${noteId}/data`, data, this.componentId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.getTypeWidget().dataSaved?.();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        appContext.addBeforeUnloadListener(this);
 | 
					        appContext.addBeforeUnloadListener(this);
 | 
				
			||||||
@ -167,7 +169,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        let type = note.type;
 | 
					        let type = note.type;
 | 
				
			||||||
        const viewScope = this.noteContext.viewScope;
 | 
					        const viewScope = this.noteContext.viewScope;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (type === 'text' && viewScope.viewMode === 'source') {
 | 
					        if (viewScope.viewMode === 'source') {
 | 
				
			||||||
            type = 'readOnlyCode';
 | 
					            type = 'readOnlyCode';
 | 
				
			||||||
        } else if (viewScope.viewMode === 'attachments') {
 | 
					        } else if (viewScope.viewMode === 'attachments') {
 | 
				
			||||||
            type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList';
 | 
					            type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList';
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,6 @@ import keyboardActionsService from "../services/keyboard_actions.js";
 | 
				
			|||||||
import clipboard from "../services/clipboard.js";
 | 
					import clipboard from "../services/clipboard.js";
 | 
				
			||||||
import protectedSessionService from "../services/protected_session.js";
 | 
					import protectedSessionService from "../services/protected_session.js";
 | 
				
			||||||
import linkService from "../services/link.js";
 | 
					import linkService from "../services/link.js";
 | 
				
			||||||
import syncService from "../services/sync.js";
 | 
					 | 
				
			||||||
import options from "../services/options.js";
 | 
					import options from "../services/options.js";
 | 
				
			||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
					import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
				
			||||||
import dialogService from "../services/dialog.js";
 | 
					import dialogService from "../services/dialog.js";
 | 
				
			||||||
@ -586,6 +585,17 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            select: (event, {node}) => {
 | 
					            select: (event, {node}) => {
 | 
				
			||||||
 | 
					                if (hoistedNoteService.getHoistedNoteId() === 'root'
 | 
				
			||||||
 | 
					                    && node.data.noteId === '_hidden'
 | 
				
			||||||
 | 
					                    && node.isSelected()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // hidden is hackily hidden from the tree via CSS when root is hoisted
 | 
				
			||||||
 | 
					                    // make sure it's not selected by mistake, it could be e.g. deleted by mistake otherwise
 | 
				
			||||||
 | 
					                    node.setSelected(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                $(node.span).find(".fancytree-custom-icon").attr("title",
 | 
					                $(node.span).find(".fancytree-custom-icon").attr("title",
 | 
				
			||||||
                    node.isSelected() ? "Apply bulk actions on selected notes" : "");
 | 
					                    node.isSelected() ? "Apply bulk actions on selected notes" : "");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -799,7 +809,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            nodes.push(this.getActiveNode());
 | 
					            nodes.push(this.getActiveNode());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return nodes;
 | 
					        // hidden subtree is hackily hidden via CSS when hoisted to root
 | 
				
			||||||
 | 
					        // make sure it's never selected for e.g. deletion in such a case
 | 
				
			||||||
 | 
					        return nodes.filter(node => hoistedNoteService.getHoistedNoteId() !== 'root'
 | 
				
			||||||
 | 
					                                               || node.data.noteId !== '_hidden');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async setExpandedStatusForSubtree(node, isExpanded) {
 | 
					    async setExpandedStatusForSubtree(node, isExpanded) {
 | 
				
			||||||
 | 
				
			|||||||
@ -42,10 +42,12 @@ export default class AttachmentListTypeWidget extends TypeWidget {
 | 
				
			|||||||
        const $helpButton = $('<button class="attachment-help-button" type="button" data-help-page="attachments" title="Open help page on attachments"><span class="bx bx-help-circle"></span></button>');
 | 
					        const $helpButton = $('<button class="attachment-help-button" type="button" data-help-page="attachments" title="Open help page on attachments"><span class="bx bx-help-circle"></span></button>');
 | 
				
			||||||
        utils.initHelpButtons($helpButton);
 | 
					        utils.initHelpButtons($helpButton);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const noteLink = await linkService.createLink(this.noteId); // do separately to avoid race condition between empty() and .append()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$linksWrapper.empty().append(
 | 
					        this.$linksWrapper.empty().append(
 | 
				
			||||||
            $('<div>').append(
 | 
					            $('<div>').append(
 | 
				
			||||||
                "Owning note: ",
 | 
					                "Owning note: ",
 | 
				
			||||||
                await linkService.createLink(this.noteId),
 | 
					                noteLink,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            $('<div>').append(
 | 
					            $('<div>').append(
 | 
				
			||||||
                $('<button class="btn btn-sm">')
 | 
					                $('<button class="btn btn-sm">')
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import libraryLoader from "../../services/library_loader.js";
 | 
					import libraryLoader from "../../services/library_loader.js";
 | 
				
			||||||
import TypeWidget from "./type_widget.js";
 | 
					import TypeWidget from "./type_widget.js";
 | 
				
			||||||
import utils from '../../services/utils.js';
 | 
					import utils from '../../services/utils.js';
 | 
				
			||||||
 | 
					import linkService from '../../services/link.js';
 | 
				
			||||||
import debounce from "../../services/debounce.js";
 | 
					import debounce from "../../services/debounce.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {sleep} = utils;
 | 
					const {sleep} = utils;
 | 
				
			||||||
@ -83,7 +84,7 @@ const TPL = `
 | 
				
			|||||||
 *  - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown
 | 
					 *  - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown
 | 
				
			||||||
 *    when requiring svg.
 | 
					 *    when requiring svg.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Discussion of storing svg in the note:
 | 
					 * Discussion of storing svg in the note attachment:
 | 
				
			||||||
 *  - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there.
 | 
					 *  - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there.
 | 
				
			||||||
 *  - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
 | 
					 *  - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
 | 
				
			||||||
 *         desktop instance mitigates that issue.
 | 
					 *         desktop instance mitigates that issue.
 | 
				
			||||||
@ -92,7 +93,6 @@ const TPL = `
 | 
				
			|||||||
 *  - Support image-notes as reference in excalidraw
 | 
					 *  - Support image-notes as reference in excalidraw
 | 
				
			||||||
 *  - Support canvas note as reference (svg) in other canvas notes.
 | 
					 *  - Support canvas note as reference (svg) in other canvas notes.
 | 
				
			||||||
 *  - Make it easy to include a canvas note inside a text note
 | 
					 *  - Make it easy to include a canvas note inside a text note
 | 
				
			||||||
 *  - Support for excalidraw libraries. Maybe special code notes with a tag.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
					export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
@ -121,6 +121,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this);
 | 
					        this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this);
 | 
				
			||||||
        this.onChangeHandler = this.onChangeHandler.bind(this);
 | 
					        this.onChangeHandler = this.onChangeHandler.bind(this);
 | 
				
			||||||
        this.isNewSceneVersion = this.isNewSceneVersion.bind(this);
 | 
					        this.isNewSceneVersion = this.isNewSceneVersion.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.libraryChanged = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static getType() {
 | 
					    static getType() {
 | 
				
			||||||
@ -137,7 +139,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$widget.toggleClass("full-height", true); // only add
 | 
					        this.$widget.toggleClass("full-height", true);
 | 
				
			||||||
        this.$render = this.$widget.find('.canvas-render');
 | 
					        this.$render = this.$widget.find('.canvas-render');
 | 
				
			||||||
        const documentStyle = window.getComputedStyle(document.documentElement);
 | 
					        const documentStyle = window.getComputedStyle(document.documentElement);
 | 
				
			||||||
        this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim();
 | 
					        this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim();
 | 
				
			||||||
@ -174,7 +176,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        const blob = await note.getBlob();
 | 
					        const blob = await note.getBlob();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // before we load content into excalidraw, make sure excalidraw has loaded
 | 
					        // before we load content into excalidraw, make sure excalidraw has loaded
 | 
				
			||||||
        while (!this.excalidrawRef || !this.excalidrawRef.current) {
 | 
					        while (!this.excalidrawRef?.current) {
 | 
				
			||||||
            console.log("excalidrawRef not yet loaded, sleep 200ms...");
 | 
					            console.log("excalidrawRef not yet loaded, sleep 200ms...");
 | 
				
			||||||
            await sleep(200);
 | 
					            await sleep(200);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -185,7 +187,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
         * note into this fresh note. Probably due to that this note-instance does not get
 | 
					         * note into this fresh note. Probably due to that this note-instance does not get
 | 
				
			||||||
         * newly instantiated?
 | 
					         * newly instantiated?
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        if (this.excalidrawRef.current && blob.content?.trim() === "") {
 | 
					        if (!blob.content?.trim()) {
 | 
				
			||||||
            const sceneData = {
 | 
					            const sceneData = {
 | 
				
			||||||
                elements: [],
 | 
					                elements: [],
 | 
				
			||||||
                appState: {
 | 
					                appState: {
 | 
				
			||||||
@ -196,16 +198,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            this.excalidrawRef.current.updateScene(sceneData);
 | 
					            this.excalidrawRef.current.updateScene(sceneData);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (this.excalidrawRef.current && blob.content) {
 | 
					        else if (blob.content) {
 | 
				
			||||||
            // load saved content into excalidraw canvas
 | 
					            // load saved content into excalidraw canvas
 | 
				
			||||||
            let content;
 | 
					            let content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                content = JSON.parse(blob.content || "");
 | 
					                content = blob.getJsonContent();
 | 
				
			||||||
            } catch(err) {
 | 
					            } catch(err) {
 | 
				
			||||||
                console.error("Error parsing content. Probably note.type changed",
 | 
					                console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err);
 | 
				
			||||||
                              "Starting with empty canvas"
 | 
					 | 
				
			||||||
                              , note, blob, err);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                content = {
 | 
					                content = {
 | 
				
			||||||
                    elements: [],
 | 
					                    elements: [],
 | 
				
			||||||
@ -251,6 +251,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
            this.excalidrawRef.current.addFiles(fileArray);
 | 
					            this.excalidrawRef.current.addFiles(fileArray);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Promise.all(
 | 
				
			||||||
 | 
					            (await note.getAttachmentsByRole('canvasLibraryItem'))
 | 
				
			||||||
 | 
					                .map(attachment => attachment.getBlob())
 | 
				
			||||||
 | 
					        ).then(blobs => {
 | 
				
			||||||
 | 
					            if (note.noteId !== this.currentNoteId) {
 | 
				
			||||||
 | 
					                // current note changed in the course of the async operation
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
 | 
				
			||||||
 | 
					            this.excalidrawRef.current.updateLibrary({libraryItems, merge: false});
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // set initial scene version
 | 
					        // set initial scene version
 | 
				
			||||||
        if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
 | 
					        if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
 | 
				
			||||||
            this.currentSceneVersion = this.getSceneVersion();
 | 
					            this.currentSceneVersion = this.getSceneVersion();
 | 
				
			||||||
@ -294,15 +307,39 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        const content = {
 | 
					        const content = {
 | 
				
			||||||
            type: "excalidraw",
 | 
					            type: "excalidraw",
 | 
				
			||||||
            version: 2,
 | 
					            version: 2,
 | 
				
			||||||
            _meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.",
 | 
					            elements,
 | 
				
			||||||
            elements, // excalidraw
 | 
					            appState,
 | 
				
			||||||
            appState, // excalidraw
 | 
					            files: activeFiles
 | 
				
			||||||
            files: activeFiles, // excalidraw
 | 
					 | 
				
			||||||
            svg: svgString, // not needed for excalidraw, used for note_short, content, and image api
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const attachments = [
 | 
				
			||||||
 | 
					            { role: 'image', title: 'canvas-export.svg', mime: 'image/svg+xml', content: svgString, position: 0 }
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.libraryChanged) {
 | 
				
			||||||
 | 
					            // this.libraryChanged is unset in dataSaved()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // there's no separate method to get library items, so have to abuse this one
 | 
				
			||||||
 | 
					            const libraryItems = await this.excalidrawRef.current.updateLibrary({merge: true});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let position = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const libraryItem of libraryItems) {
 | 
				
			||||||
 | 
					                attachments.push({
 | 
				
			||||||
 | 
					                    role: 'canvasLibraryItem',
 | 
				
			||||||
 | 
					                    title: libraryItem.id,
 | 
				
			||||||
 | 
					                    mime: 'application/json',
 | 
				
			||||||
 | 
					                    content: JSON.stringify(libraryItem),
 | 
				
			||||||
 | 
					                    position: position
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                position += 10;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            content: JSON.stringify(content)
 | 
					            content: JSON.stringify(content),
 | 
				
			||||||
 | 
					            attachments: attachments
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -314,6 +351,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        this.spacedUpdate.scheduleUpdate();
 | 
					        this.spacedUpdate.scheduleUpdate();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dataSaved() {
 | 
				
			||||||
 | 
					        this.libraryChanged = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onChangeHandler() {
 | 
					    onChangeHandler() {
 | 
				
			||||||
        // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
 | 
					        // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
 | 
				
			||||||
        // make sure only when a new element is added, we actually save something.
 | 
					        // make sure only when a new element is added, we actually save something.
 | 
				
			||||||
@ -331,8 +372,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        if (shouldSave) {
 | 
					        if (shouldSave) {
 | 
				
			||||||
            this.updateSceneVersion();
 | 
					            this.updateSceneVersion();
 | 
				
			||||||
            this.saveData();
 | 
					            this.saveData();
 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            // do nothing
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -374,21 +413,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        }, [excalidrawWrapperRef]);
 | 
					        }, [excalidrawWrapperRef]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const onLinkOpen = React.useCallback((element, event) => {
 | 
					        const onLinkOpen = React.useCallback((element, event) => {
 | 
				
			||||||
            const link = element.link;
 | 
					            let link = element.link;
 | 
				
			||||||
            const { nativeEvent } = event.detail;
 | 
					 | 
				
			||||||
            const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
 | 
					 | 
				
			||||||
            const isNewWindow = nativeEvent.shiftKey;
 | 
					 | 
				
			||||||
            const isInternalLink = link.startsWith("/")
 | 
					 | 
				
			||||||
                || link.includes(window.location.origin);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (isInternalLink && !isNewTab && !isNewWindow) {
 | 
					            if (link.startsWith("root/")) {
 | 
				
			||||||
                // signal that we're handling the redirect ourselves
 | 
					                link = "#" + link;
 | 
				
			||||||
                event.preventDefault();
 | 
					 | 
				
			||||||
                // do a custom redirect, such as passing to react-router
 | 
					 | 
				
			||||||
                // ...
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // open in the same tab
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const { nativeEvent } = event.detail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return linkService.goToLinkExt(nativeEvent, link, null);
 | 
				
			||||||
          }, []);
 | 
					          }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return React.createElement(
 | 
					        return React.createElement(
 | 
				
			||||||
@ -409,6 +444,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
                    onPaste: (data, event) => {
 | 
					                    onPaste: (data, event) => {
 | 
				
			||||||
                        console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
 | 
					                        console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
 | 
					                    onLibraryChange: () => {
 | 
				
			||||||
 | 
					                        this.libraryChanged = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        this.saveData();
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                    onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER),
 | 
					                    onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER),
 | 
				
			||||||
                    viewModeEnabled: false,
 | 
					                    viewModeEnabled: false,
 | 
				
			||||||
                    zenModeEnabled: false,
 | 
					                    zenModeEnabled: false,
 | 
				
			||||||
@ -416,7 +456,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
                    isCollaborating: false,
 | 
					                    isCollaborating: false,
 | 
				
			||||||
                    detectScroll: false,
 | 
					                    detectScroll: false,
 | 
				
			||||||
                    handleKeyboardGlobally: false,
 | 
					                    handleKeyboardGlobally: false,
 | 
				
			||||||
                    autoFocus: true,
 | 
					                    autoFocus: false,
 | 
				
			||||||
                    onLinkOpen,
 | 
					                    onLinkOpen,
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -424,7 +464,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * needed to ensure, that multipleOnChangeHandler calls do not trigger a safe.
 | 
					     * needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
 | 
				
			||||||
     * we compare the scene version as suggested in:
 | 
					     * we compare the scene version as suggested in:
 | 
				
			||||||
     * https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
 | 
					     * https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -434,8 +474,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
				
			|||||||
        const sceneVersion = this.getSceneVersion();
 | 
					        const sceneVersion = this.getSceneVersion();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update
 | 
					        return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update
 | 
				
			||||||
            || this.currentSceneVersion !== sceneVersion // ensure scene changed
 | 
					            || this.currentSceneVersion !== sceneVersion; // ensure scene changed
 | 
				
			||||||
        ;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getSceneVersion() {
 | 
					    getSceneVersion() {
 | 
				
			||||||
 | 
				
			|||||||
@ -21,19 +21,24 @@ function returnImage(req, res) {
 | 
				
			|||||||
     * to avoid bitrot and enable usage as referenced image the svg is included.
 | 
					     * to avoid bitrot and enable usage as referenced image the svg is included.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    if (image.type === 'canvas') {
 | 
					    if (image.type === 'canvas') {
 | 
				
			||||||
        const content = image.getContent();
 | 
					        let svgString = '<svg/>'
 | 
				
			||||||
        try {
 | 
					        const attachment = image.getAttachmentByTitle('canvas-export.svg');
 | 
				
			||||||
            const data = JSON.parse(content);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const svg = data.svg || '<svg />'
 | 
					        if (attachment) {
 | 
				
			||||||
            res.set('Content-Type', "image/svg+xml");
 | 
					            svgString = attachment.getContent();
 | 
				
			||||||
            res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
					        } else {
 | 
				
			||||||
            res.send(svg);
 | 
					            // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
 | 
				
			||||||
        } catch(err) {
 | 
					            const contentSvg = image.getJsonContentSafely()?.svg;
 | 
				
			||||||
            res.setHeader("Content-Type", "text/plain")
 | 
					
 | 
				
			||||||
                .status(500)
 | 
					            if (contentSvg) {
 | 
				
			||||||
                .send("there was an error parsing excalidraw to svg");
 | 
					                svgString = contentSvg;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const svg = svgString
 | 
				
			||||||
 | 
					        res.set('Content-Type', "image/svg+xml");
 | 
				
			||||||
 | 
					        res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
				
			||||||
 | 
					        res.send(svg);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        res.set('Content-Type', image.mime);
 | 
					        res.set('Content-Type', image.mime);
 | 
				
			||||||
        res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
					        res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
				
			||||||
@ -50,7 +55,9 @@ function returnAttachedImage(req, res) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!["image"].includes(attachment.role)) {
 | 
					    if (!["image"].includes(attachment.role)) {
 | 
				
			||||||
        return res.sendStatus(400);
 | 
					        return res.setHeader("Content-Type", "text/plain")
 | 
				
			||||||
 | 
					            .status(400)
 | 
				
			||||||
 | 
					            .send(`Attachment '${attachment.attachmentId}' has role '${attachment.role}', but 'image' was expected.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.set('Content-Type', attachment.mime);
 | 
					    res.set('Content-Type', attachment.mime);
 | 
				
			||||||
 | 
				
			|||||||
@ -45,10 +45,10 @@ function createNote(req) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateNoteData(req) {
 | 
					function updateNoteData(req) {
 | 
				
			||||||
    const {content} = req.body;
 | 
					    const {content, attachments} = req.body;
 | 
				
			||||||
    const {noteId} = req.params;
 | 
					    const {noteId} = req.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return noteService.updateNoteData(noteId, content);
 | 
					    return noteService.updateNoteData(noteId, content, attachments);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function deleteNote(req) {
 | 
					function deleteNote(req) {
 | 
				
			||||||
 | 
				
			|||||||
@ -733,7 +733,7 @@ function saveRevisionIfNeeded(note) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateNoteData(noteId, content) {
 | 
					function updateNoteData(noteId, content, attachments = []) {
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!note.isContentAvailable()) {
 | 
					    if (!note.isContentAvailable()) {
 | 
				
			||||||
@ -745,6 +745,23 @@ function updateNoteData(noteId, content) {
 | 
				
			|||||||
    const { forceFrontendReload, content: newContent } = saveLinks(note, content);
 | 
					    const { forceFrontendReload, content: newContent } = saveLinks(note, content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.setContent(newContent, { forceFrontendReload });
 | 
					    note.setContent(newContent, { forceFrontendReload });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (attachments?.length > 0) {
 | 
				
			||||||
 | 
					        /** @var {Object<string, BAttachment>} */
 | 
				
			||||||
 | 
					        const existingAttachmentsByTitle = utils.toMap(note.getAttachments({includeContentLength: false}), 'title');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const {attachmentId, role, mime, title, content, position} of attachments) {
 | 
				
			||||||
 | 
					            if (attachmentId || !(title in existingAttachmentsByTitle)) {
 | 
				
			||||||
 | 
					                note.saveAttachment({attachmentId, role, mime, title, content, position});
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const existingAttachment = existingAttachmentsByTitle[title];
 | 
				
			||||||
 | 
					                existingAttachment.role = role;
 | 
				
			||||||
 | 
					                existingAttachment.mime = mime;
 | 
				
			||||||
 | 
					                existingAttachment.position = position;
 | 
				
			||||||
 | 
					                existingAttachment.setContent(content, {forceSave: true});
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 | 
				
			|||||||
@ -289,6 +289,16 @@ function normalize(str) {
 | 
				
			|||||||
    return removeDiacritic(str).toLowerCase();
 | 
					    return removeDiacritic(str).toLowerCase();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toMap(list, key) {
 | 
				
			||||||
 | 
					    const map = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const el of list) {
 | 
				
			||||||
 | 
					        map[el[key]] = el;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    randomSecureToken,
 | 
					    randomSecureToken,
 | 
				
			||||||
    randomString,
 | 
					    randomString,
 | 
				
			||||||
@ -320,4 +330,5 @@ module.exports = {
 | 
				
			|||||||
    removeDiacritic,
 | 
					    removeDiacritic,
 | 
				
			||||||
    normalize,
 | 
					    normalize,
 | 
				
			||||||
    hashedBlobId,
 | 
					    hashedBlobId,
 | 
				
			||||||
 | 
					    toMap,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,99 +0,0 @@
 | 
				
			|||||||
/**
 | 
					 | 
				
			||||||
 * this is used as a "standalone js" file and required by a shared note directly via script-tags
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * data input comes via window variable as follows
 | 
					 | 
				
			||||||
 * const {elements, appState, files} = window.triliumExcalidraw;
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
document.getElementById("excalidraw-app").style.height = `${appState.height}px`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const App = () => {
 | 
					 | 
				
			||||||
    const excalidrawRef = React.useRef(null);
 | 
					 | 
				
			||||||
    const excalidrawWrapperRef = React.useRef(null);
 | 
					 | 
				
			||||||
    const [dimensions, setDimensions] = React.useState({
 | 
					 | 
				
			||||||
        width: undefined,
 | 
					 | 
				
			||||||
        height: appState.height,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const [viewModeEnabled, setViewModeEnabled] = React.useState(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ensure that assets are loaded from trilium
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * resizing
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    React.useEffect(() => {
 | 
					 | 
				
			||||||
        const dimensions = {
 | 
					 | 
				
			||||||
            width: excalidrawWrapperRef.current.getBoundingClientRect().width,
 | 
					 | 
				
			||||||
            height: excalidrawWrapperRef.current.getBoundingClientRect().height
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        setDimensions(dimensions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const onResize = () => {
 | 
					 | 
				
			||||||
            const dimensions = {
 | 
					 | 
				
			||||||
                width: excalidrawWrapperRef.current.getBoundingClientRect().width,
 | 
					 | 
				
			||||||
                height: excalidrawWrapperRef.current.getBoundingClientRect().height
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            setDimensions(dimensions);
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        window.addEventListener("resize", onResize);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return () => window.removeEventListener("resize", onResize);
 | 
					 | 
				
			||||||
    }, [excalidrawWrapperRef]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return React.createElement(
 | 
					 | 
				
			||||||
        React.Fragment,
 | 
					 | 
				
			||||||
        null,
 | 
					 | 
				
			||||||
        React.createElement(
 | 
					 | 
				
			||||||
            "div",
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                className: "excalidraw-wrapper",
 | 
					 | 
				
			||||||
                ref: excalidrawWrapperRef
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            React.createElement(ExcalidrawLib.Excalidraw, {
 | 
					 | 
				
			||||||
                ref: excalidrawRef,
 | 
					 | 
				
			||||||
                width: dimensions.width,
 | 
					 | 
				
			||||||
                height: dimensions.height,
 | 
					 | 
				
			||||||
                initialData: {
 | 
					 | 
				
			||||||
                    elements, appState, files
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                viewModeEnabled: !viewModeEnabled,
 | 
					 | 
				
			||||||
                zenModeEnabled: false,
 | 
					 | 
				
			||||||
                gridModeEnabled: false,
 | 
					 | 
				
			||||||
                isCollaborating: false,
 | 
					 | 
				
			||||||
                detectScroll: false,
 | 
					 | 
				
			||||||
                handleKeyboardGlobally: false,
 | 
					 | 
				
			||||||
                autoFocus: true,
 | 
					 | 
				
			||||||
                renderFooter: () => {
 | 
					 | 
				
			||||||
                    return React.createElement(
 | 
					 | 
				
			||||||
                        React.Fragment,
 | 
					 | 
				
			||||||
                        null,
 | 
					 | 
				
			||||||
                        React.createElement(
 | 
					 | 
				
			||||||
                            "div",
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                className: "excalidraw-top-right-ui excalidraw Island",
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            React.createElement(
 | 
					 | 
				
			||||||
                                "label",
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    style: {
 | 
					 | 
				
			||||||
                                        padding: "5px",
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    className: "excalidraw Stack",
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                React.createElement(
 | 
					 | 
				
			||||||
                                    "button",
 | 
					 | 
				
			||||||
                                    {
 | 
					 | 
				
			||||||
                                        onClick: () => setViewModeEnabled(!viewModeEnabled)
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    viewModeEnabled ? " Enter simple view mode " : " Enter extended view mode "
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                                ""
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                        ));
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
ReactDOM.render(React.createElement(App), document.getElementById("excalidraw-app"));
 | 
					 | 
				
			||||||
@ -25,14 +25,12 @@ function getContent(note) {
 | 
				
			|||||||
        renderCode(result);
 | 
					        renderCode(result);
 | 
				
			||||||
    } else if (note.type === 'mermaid') {
 | 
					    } else if (note.type === 'mermaid') {
 | 
				
			||||||
        renderMermaid(result);
 | 
					        renderMermaid(result);
 | 
				
			||||||
    } else if (note.type === 'image') {
 | 
					    } else if (note.type === 'image' || note.type === 'canvas') {
 | 
				
			||||||
        renderImage(result, note);
 | 
					        renderImage(result, note);
 | 
				
			||||||
    } else if (note.type === 'file') {
 | 
					    } else if (note.type === 'file') {
 | 
				
			||||||
        renderFile(note, result);
 | 
					        renderFile(note, result);
 | 
				
			||||||
    } else if (note.type === 'book') {
 | 
					    } else if (note.type === 'book') {
 | 
				
			||||||
        result.isEmpty = true;
 | 
					        result.isEmpty = true;
 | 
				
			||||||
    } else if (note.type === 'canvas') {
 | 
					 | 
				
			||||||
        renderCanvas(result, note);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        result.content = '<p>This note type cannot be displayed.</p>';
 | 
					        result.content = '<p>This note type cannot be displayed.</p>';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -151,39 +149,6 @@ function renderFile(note, result) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function renderCanvas(result, note) {
 | 
					 | 
				
			||||||
    result.header += `<script>
 | 
					 | 
				
			||||||
                    window.EXCALIDRAW_ASSET_PATH = window.location.origin + "/node_modules/@excalidraw/excalidraw/dist/";
 | 
					 | 
				
			||||||
                   </script>`;
 | 
					 | 
				
			||||||
    result.header += `<script src="../../${assetPath}/node_modules/react/umd/react.production.min.js"></script>`;
 | 
					 | 
				
			||||||
    result.header += `<script src="../../${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js"></script>`;
 | 
					 | 
				
			||||||
    result.header += `<script src="../../${assetPath}/node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>`;
 | 
					 | 
				
			||||||
    result.header += `<style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .excalidraw-wrapper {
 | 
					 | 
				
			||||||
                height: 100%;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            :root[dir="ltr"]
 | 
					 | 
				
			||||||
            .excalidraw
 | 
					 | 
				
			||||||
            .layer-ui__wrapper
 | 
					 | 
				
			||||||
            .zen-mode-transition.App-menu_bottom--transition-left {
 | 
					 | 
				
			||||||
                transform: none;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        </style>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    result.content = `<div>
 | 
					 | 
				
			||||||
            <script>
 | 
					 | 
				
			||||||
                const {elements, appState, files} = JSON.parse(${JSON.stringify(result.content)});
 | 
					 | 
				
			||||||
                window.triliumExcalidraw = {elements, appState, files}
 | 
					 | 
				
			||||||
            </script>
 | 
					 | 
				
			||||||
            <div id="excalidraw-app"></div>
 | 
					 | 
				
			||||||
            <hr>
 | 
					 | 
				
			||||||
            <a href="api/images/${note.noteId}/${note.escapedTitle}?utc=${note.utcDateModified}">Get Image Link</a>
 | 
					 | 
				
			||||||
            <script src="./canvas_share.js"></script>
 | 
					 | 
				
			||||||
        </div>`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    getContent
 | 
					    getContent
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -142,8 +142,6 @@ function register(router) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    router.use('/share/canvas_share.js', express.static(path.join(__dirname, 'canvas_share.js')));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    router.get('/share/', (req, res, next) => {
 | 
					    router.get('/share/', (req, res, next) => {
 | 
				
			||||||
        if (req.path.substr(-1) !== '/') {
 | 
					        if (req.path.substr(-1) !== '/') {
 | 
				
			||||||
            res.redirect('../share/');
 | 
					            res.redirect('../share/');
 | 
				
			||||||
@ -219,19 +217,24 @@ function register(router) {
 | 
				
			|||||||
             * special "image" type. the canvas is actually type application/json
 | 
					             * special "image" type. the canvas is actually type application/json
 | 
				
			||||||
             * to avoid bitrot and enable usage as referenced image the svg is included.
 | 
					             * to avoid bitrot and enable usage as referenced image the svg is included.
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
            const content = image.getContent();
 | 
					            let svgString = '<svg/>'
 | 
				
			||||||
            try {
 | 
					            const attachment = image.getAttachmentByTitle('canvas-export.svg');
 | 
				
			||||||
                const data = JSON.parse(content);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const svg = data.svg || '<svg />';
 | 
					            if (attachment) {
 | 
				
			||||||
                addNoIndexHeader(image, res);
 | 
					                svgString = attachment.getContent();
 | 
				
			||||||
                res.set('Content-Type', "image/svg+xml");
 | 
					            } else {
 | 
				
			||||||
                res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
					                // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
 | 
				
			||||||
                res.send(svg);
 | 
					                const contentSvg = image.getJsonContentSafely()?.svg;
 | 
				
			||||||
            } catch (err) {
 | 
					
 | 
				
			||||||
                res.status(500)
 | 
					                if (contentSvg) {
 | 
				
			||||||
                    .json({ message: "There was an error parsing excalidraw to svg." });
 | 
					                    svgString = contentSvg;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const svg = svgString
 | 
				
			||||||
 | 
					            res.set('Content-Type', "image/svg+xml");
 | 
				
			||||||
 | 
					            res.set("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
				
			||||||
 | 
					            res.send(svg);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // normal image
 | 
					            // normal image
 | 
				
			||||||
            res.set('Content-Type', image.mime);
 | 
					            res.set('Content-Type', image.mime);
 | 
				
			||||||
 | 
				
			|||||||
@ -470,6 +470,11 @@ class SNote extends AbstractShacaEntity {
 | 
				
			|||||||
        return this.attachments;
 | 
					        return this.attachments;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {SAttachment} */
 | 
				
			||||||
 | 
					    getAttachmentByTitle(title) {
 | 
				
			||||||
 | 
					        return this.attachments.find(attachment => attachment.title === title);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {string} */
 | 
					    /** @returns {string} */
 | 
				
			||||||
    get shareId() {
 | 
					    get shareId() {
 | 
				
			||||||
        if (this.hasOwnedLabel('shareRoot')) {
 | 
					        if (this.hasOwnedLabel('shareRoot')) {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user