Merge branch 'beta'

# Conflicts:
#	package-lock.json
This commit is contained in:
zadam 2023-09-14 00:19:19 +02:00
commit 035113db4d
21 changed files with 244 additions and 248 deletions

View File

@ -75,7 +75,6 @@ module.exports = {
glob: true,
log: true,
EditorWatchdog: true,
// \src\share\canvas_share.js
React: true,
appState: true,
ExcalidrawLib: true,

View File

@ -68,7 +68,7 @@
"jimp": "0.22.10",
"joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "22.1.0",
"marked": "8.0.1",
"marked": "9.0.0",
"mime-types": "2.1.35",
"multer": "1.4.5-lts.1",
"node-abi": "3.47.0",
@ -91,13 +91,13 @@
"tmp": "0.2.1",
"turndown": "7.1.2",
"unescape": "1.0.1",
"ws": "8.14.0",
"ws": "8.14.1",
"xml2js": "0.6.2",
"yauzl": "2.10.0"
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "25.8.0",
"electron": "25.8.1",
"electron-builder": "24.6.4",
"electron-packager": "17.1.2",
"electron-rebuild": "3.2.9",

View File

@ -229,7 +229,9 @@ class BNote extends AbstractBeccaEntity {
return this._getContent();
}
/** @returns {*} */
/**
* @returns {*}
* @throws Error in case of invalid JSON */
getJsonContent() {
const content = this.getContent();
@ -240,6 +242,16 @@ class BNote extends AbstractBeccaEntity {
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 {object} [opts]
@ -1143,7 +1155,7 @@ class BNote extends AbstractBeccaEntity {
}
/** @returns {BAttachment[]} */
getAttachmentByRole(role) {
getAttachmentsByRole(role) {
return sql.getRows(`
SELECT attachments.*
FROM attachments
@ -1154,6 +1166,18 @@ class BNote extends AbstractBeccaEntity {
.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)
*

View File

@ -15,4 +15,25 @@ export default class FBlob {
/** @type {string} */
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;
}
}
}

View File

@ -255,6 +255,12 @@ class FNote {
return this.attachments;
}
/** @returns {Promise<FAttachment[]>} */
async getAttachmentsByRole(role) {
return (await this.getAttachments())
.filter(attachment => attachment.role === role);
}
/** @returns {Promise<FAttachment>} */
async getAttachmentById(attachmentId) {
const attachments = await this.getAttachments();

View File

@ -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: '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: '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: "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.`);
}
else if (command === 'copyNotePathToClipboard') {
navigator.clipboard.writeText('#' + notePath);
}
else {
this.treeWidget.triggerCommand(command, {
node: this.node,

View File

@ -33,7 +33,7 @@ async function getRenderedContent(entity, options = {}) {
else if (type === 'code') {
await renderCode(entity, $renderedContent);
}
else if (type === 'image') {
else if (type === 'image' || type === 'canvas') {
renderImage(entity, $renderedContent, options);
}
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
@ -49,9 +49,6 @@ async function getRenderedContent(entity, options = {}) {
$renderedContent.append($content);
}
else if (type === 'canvas') {
await renderCanvas(entity, $renderedContent);
}
else if (!options.tooltip && type === 'protectedSession') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
@ -125,7 +122,7 @@ function renderImage(entity, $renderedContent, options = {}) {
let url;
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) {
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 {FNote} note

View File

@ -194,6 +194,10 @@ function goToLink(evt) {
const $link = $(evt.target).closest("a,.block-link");
const hrefLink = $link.attr('href') || $link.attr('data-href');
return goToLinkExt(evt, hrefLink, $link);
}
function goToLinkExt(evt, hrefLink, $link) {
if (hrefLink?.startsWith("data:")) {
return true;
}
@ -201,7 +205,7 @@ function goToLink(evt) {
evt.preventDefault();
evt.stopPropagation();
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const {notePath, viewScope} = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1;
@ -213,25 +217,23 @@ function goToLink(evt) {
if (notePath) {
if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
}
else if (isLeftClick) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
} else if (isLeftClick) {
const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
const noteContext = ntxId
? appContext.tabManager.getNoteContextById(ntxId)
: appContext.tabManager.getActiveContext();
noteContext.setNote(notePath, { viewScope }).then(() => {
noteContext.setNote(notePath, {viewScope}).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
});
}
}
else if (hrefLink) {
const withinEditLink = $link.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = $link.closest("[contenteditable]").length === 0;
} else if (hrefLink) {
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab
|| (withinEditLink && (leftClick || middleClick))
@ -239,8 +241,7 @@ function goToLink(evt) {
) {
if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) {
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');
electron.shell.openPath(hrefLink);
@ -364,6 +365,7 @@ export default {
getNotePathFromUrl,
createLink,
goToLink,
goToLinkExt,
loadReferenceLinkTitle,
getReferenceLinkTitle,
getReferenceLinkTitleSync,

View File

@ -98,7 +98,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
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));

View File

@ -86,6 +86,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
protectedSessionHolder.touchProtectedSessionIfNecessary(note);
await server.put(`notes/${noteId}/data`, data, this.componentId);
this.getTypeWidget().dataSaved?.();
});
appContext.addBeforeUnloadListener(this);
@ -167,7 +169,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
let type = note.type;
const viewScope = this.noteContext.viewScope;
if (type === 'text' && viewScope.viewMode === 'source') {
if (viewScope.viewMode === 'source') {
type = 'readOnlyCode';
} else if (viewScope.viewMode === 'attachments') {
type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList';

View File

@ -14,7 +14,6 @@ import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
import linkService from "../services/link.js";
import syncService from "../services/sync.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import dialogService from "../services/dialog.js";
@ -586,6 +585,17 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
});
},
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.isSelected() ? "Apply bulk actions on selected notes" : "");
}
@ -799,7 +809,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
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) {

View File

@ -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>');
utils.initHelpButtons($helpButton);
const noteLink = await linkService.createLink(this.noteId); // do separately to avoid race condition between empty() and .append()
this.$linksWrapper.empty().append(
$('<div>').append(
"Owning note: ",
await linkService.createLink(this.noteId),
noteLink,
),
$('<div>').append(
$('<button class="btn btn-sm">')

View File

@ -1,6 +1,7 @@
import libraryLoader from "../../services/library_loader.js";
import TypeWidget from "./type_widget.js";
import utils from '../../services/utils.js';
import linkService from '../../services/link.js';
import debounce from "../../services/debounce.js";
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
* 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.
* - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
* desktop instance mitigates that issue.
@ -92,7 +93,6 @@ const TPL = `
* - Support image-notes as reference in excalidraw
* - Support canvas note as reference (svg) in other canvas notes.
* - 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 {
constructor() {
@ -121,6 +121,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.isNewSceneVersion = this.isNewSceneVersion.bind(this);
this.libraryChanged = false;
}
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');
const documentStyle = window.getComputedStyle(document.documentElement);
this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim();
@ -174,7 +176,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const blob = await note.getBlob();
// 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...");
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
* newly instantiated?
*/
if (this.excalidrawRef.current && blob.content?.trim() === "") {
if (!blob.content?.trim()) {
const sceneData = {
elements: [],
appState: {
@ -196,16 +198,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.excalidrawRef.current.updateScene(sceneData);
}
else if (this.excalidrawRef.current && blob.content) {
else if (blob.content) {
// load saved content into excalidraw canvas
let content;
try {
content = JSON.parse(blob.content || "");
content = blob.getJsonContent();
} catch(err) {
console.error("Error parsing content. Probably note.type changed",
"Starting with empty canvas"
, note, blob, err);
console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err);
content = {
elements: [],
@ -251,6 +251,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
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
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
this.currentSceneVersion = this.getSceneVersion();
@ -294,15 +307,39 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const content = {
type: "excalidraw",
version: 2,
_meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.",
elements, // excalidraw
appState, // excalidraw
files: activeFiles, // excalidraw
svg: svgString, // not needed for excalidraw, used for note_short, content, and image api
elements,
appState,
files: activeFiles
};
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 {
content: JSON.stringify(content)
content: JSON.stringify(content),
attachments: attachments
};
}
@ -314,6 +351,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.spacedUpdate.scheduleUpdate();
}
dataSaved() {
this.libraryChanged = false;
}
onChangeHandler() {
// 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.
@ -331,8 +372,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
if (shouldSave) {
this.updateSceneVersion();
this.saveData();
} else {
// do nothing
}
}
@ -374,21 +413,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}, [excalidrawWrapperRef]);
const onLinkOpen = React.useCallback((element, event) => {
const 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);
let link = element.link;
if (isInternalLink && !isNewTab && !isNewWindow) {
// signal that we're handling the redirect ourselves
event.preventDefault();
// do a custom redirect, such as passing to react-router
// ...
} else {
// open in the same tab
if (link.startsWith("root/")) {
link = "#" + link;
}
const { nativeEvent } = event.detail;
event.preventDefault();
return linkService.goToLinkExt(nativeEvent, link, null);
}, []);
return React.createElement(
@ -409,6 +444,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
onPaste: (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),
viewModeEnabled: false,
zenModeEnabled: false,
@ -416,7 +456,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
isCollaborating: false,
detectScroll: false,
handleKeyboardGlobally: false,
autoFocus: true,
autoFocus: false,
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:
* https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
*
@ -434,8 +474,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const sceneVersion = this.getSceneVersion();
return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update
|| this.currentSceneVersion !== sceneVersion // ensure scene changed
;
|| this.currentSceneVersion !== sceneVersion; // ensure scene changed
}
getSceneVersion() {

View File

@ -21,19 +21,24 @@ function returnImage(req, res) {
* to avoid bitrot and enable usage as referenced image the svg is included.
*/
if (image.type === 'canvas') {
const content = image.getContent();
try {
const data = JSON.parse(content);
let svgString = '<svg/>'
const attachment = image.getAttachmentByTitle('canvas-export.svg');
const svg = data.svg || '<svg />'
res.set('Content-Type', "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
} catch(err) {
res.setHeader("Content-Type", "text/plain")
.status(500)
.send("there was an error parsing excalidraw to svg");
if (attachment) {
svgString = attachment.getContent();
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
if (contentSvg) {
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 {
res.set('Content-Type', image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
@ -50,7 +55,9 @@ function returnAttachedImage(req, res) {
}
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);

View File

@ -45,10 +45,10 @@ function createNote(req) {
}
function updateNoteData(req) {
const {content} = req.body;
const {content, attachments} = req.body;
const {noteId} = req.params;
return noteService.updateNoteData(noteId, content);
return noteService.updateNoteData(noteId, content, attachments);
}
function deleteNote(req) {

View File

@ -733,7 +733,7 @@ function saveRevisionIfNeeded(note) {
}
}
function updateNoteData(noteId, content) {
function updateNoteData(noteId, content, attachments = []) {
const note = becca.getNote(noteId);
if (!note.isContentAvailable()) {
@ -745,6 +745,23 @@ function updateNoteData(noteId, content) {
const { forceFrontendReload, content: newContent } = saveLinks(note, content);
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});
}
}
}
}
/**

View File

@ -289,6 +289,16 @@ function normalize(str) {
return removeDiacritic(str).toLowerCase();
}
function toMap(list, key) {
const map = {};
for (const el of list) {
map[el[key]] = el;
}
return map;
}
module.exports = {
randomSecureToken,
randomString,
@ -320,4 +330,5 @@ module.exports = {
removeDiacritic,
normalize,
hashedBlobId,
toMap,
};

View File

@ -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"));

View File

@ -25,14 +25,12 @@ function getContent(note) {
renderCode(result);
} else if (note.type === 'mermaid') {
renderMermaid(result);
} else if (note.type === 'image') {
} else if (note.type === 'image' || note.type === 'canvas') {
renderImage(result, note);
} else if (note.type === 'file') {
renderFile(note, result);
} else if (note.type === 'book') {
result.isEmpty = true;
} else if (note.type === 'canvas') {
renderCanvas(result, note);
} else {
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 = {
getContent
};

View File

@ -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) => {
if (req.path.substr(-1) !== '/') {
res.redirect('../share/');
@ -219,19 +217,24 @@ function register(router) {
* special "image" type. the canvas is actually type application/json
* to avoid bitrot and enable usage as referenced image the svg is included.
*/
const content = image.getContent();
try {
const data = JSON.parse(content);
let svgString = '<svg/>'
const attachment = image.getAttachmentByTitle('canvas-export.svg');
const svg = data.svg || '<svg />';
addNoIndexHeader(image, res);
res.set('Content-Type', "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
} catch (err) {
res.status(500)
.json({ message: "There was an error parsing excalidraw to svg." });
if (attachment) {
svgString = attachment.getContent();
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
if (contentSvg) {
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 {
// normal image
res.set('Content-Type', image.mime);

View File

@ -470,6 +470,11 @@ class SNote extends AbstractShacaEntity {
return this.attachments;
}
/** @returns {SAttachment} */
getAttachmentByTitle(title) {
return this.attachments.find(attachment => attachment.title === title);
}
/** @returns {string} */
get shareId() {
if (this.hasOwnedLabel('shareRoot')) {