mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
add support for storing canvas libraries in note attachments plus storing exported SVG in attachment
This commit is contained in:
parent
0b84524807
commit
f37dc66074
@ -75,7 +75,6 @@ module.exports = {
|
||||
glob: true,
|
||||
log: true,
|
||||
EditorWatchdog: true,
|
||||
// \src\share\canvas_share.js
|
||||
React: true,
|
||||
appState: true,
|
||||
ExcalidrawLib: true,
|
||||
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -5,7 +5,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trilium",
|
||||
"version": "0.61.6-beta",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
|
@ -211,7 +211,9 @@ class BNote extends AbstractBeccaEntity {
|
||||
return this._getContent();
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
/**
|
||||
* @returns {*}
|
||||
* @throws Error in case of invalid JSON */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
|
||||
@ -222,6 +224,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]
|
||||
@ -1125,7 +1137,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
/** @returns {BAttachment[]} */
|
||||
getAttachmentByRole(role) {
|
||||
getAttachmentsByRole(role) {
|
||||
return sql.getRows(`
|
||||
SELECT attachments.*
|
||||
FROM attachments
|
||||
@ -1136,6 +1148,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)
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,6 +236,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();
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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">')
|
||||
|
@ -75,7 +75,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.
|
||||
@ -84,7 +84,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() {
|
||||
@ -113,6 +112,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() {
|
||||
@ -129,7 +130,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();
|
||||
@ -166,7 +167,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);
|
||||
}
|
||||
@ -177,7 +178,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: {
|
||||
@ -188,16 +189,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: [],
|
||||
@ -243,6 +242,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();
|
||||
@ -286,15 +298,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
|
||||
};
|
||||
}
|
||||
|
||||
@ -306,6 +342,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.
|
||||
@ -323,8 +363,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
if (shouldSave) {
|
||||
this.updateSceneVersion();
|
||||
this.saveData();
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,8 +408,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const { nativeEvent } = event.detail;
|
||||
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
|
||||
const isNewWindow = nativeEvent.shiftKey;
|
||||
const isInternalLink = link.startsWith("/")
|
||||
|| link.includes(window.location.origin);
|
||||
const isInternalLink = link.startsWith("/") || link.includes(window.location.origin);
|
||||
|
||||
if (isInternalLink && !isNewTab && !isNewWindow) {
|
||||
// signal that we're handling the redirect ourselves
|
||||
@ -401,6 +438,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 +458,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
|
||||
*
|
||||
@ -426,8 +468,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() {
|
||||
|
@ -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");
|
||||
|
@ -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) {
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
} 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
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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')) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user