Compare commits

..

14 Commits

Author SHA1 Message Date
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
zsviczian
bf148adc68 2.4.0-beta-2 2024-08-09 23:36:20 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
zsviczian
9fc0452b70 2.4.0-beta-1 2024-08-08 23:27:57 +02:00
zsviczian
83eda9b3f5 Update README.md 2024-08-04 11:01:12 +02:00
zsviczian
9bfbf47963 2.3.0 2024-08-04 10:37:06 +02:00
zsviczian
252bf411b1 2.3.0-beta-1 2024-08-03 21:26:47 +02:00
zsviczian
5622c019dd rebuild view DEVICE.isDesktop 2024-08-02 17:55:23 +02:00
zsviczian
b32fab7865 2.2.13-1 2024-08-01 22:18:16 +02:00
zsviczian
cafdad1f7a onWindowMigrated 2024-07-30 18:54:54 +02:00
19 changed files with 501 additions and 179 deletions

View File

@@ -63,6 +63,13 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Eraser, left-handed mode, improved filename configuration</a><br>
</details>
### Beta testing
The plugin follows a monthly release schedule. If you want to receive more frequent updates with new features (e.g. shiny new stuff available on excalidraw.com, but not yet in Obsidian) and minor bug fixes, then join the beta community.
[![Thumbnail - 20240803 Excalidraw Release Approach (Custom)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw Plugin Release Strategy (Phone)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## Features

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.2.10-2",
"version": "2.4.0-beta-6",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,12 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.2.13",
"version": "2.3.0",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"authorUrl": "https://www.zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
}

View File

@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-34",
"@zsviczian/excalidraw": "0.17.1-obsidian-40",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",

View File

@@ -15,7 +15,7 @@ import cssnano from 'cssnano';
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);

View File

@@ -35,6 +35,7 @@ import {
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
cropCanvas,
} from "./utils/Utils";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
@@ -746,6 +747,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
// Render the page
const renderPage = async (num:number) => {
@@ -766,6 +769,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};

View File

@@ -13,6 +13,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
@@ -1026,6 +1027,22 @@ export class ExcalidrawAutomate {
return id;
};
/**
* Add elements to frame
* @param frameId
* @param elementIDs to add
* @returns void
*/
addElementsToFrame(frameId: string, elementIDs: string[]):void {
if(!this.getElement(frameId)) return;
elementIDs.forEach(elID => {
const el = this.getElement(elID);
if(el) {
el.frameId = frameId;
}
});
}
/**
*
* @param topX
@@ -1571,6 +1588,26 @@ export class ExcalidrawAutomate {
return id;
};
/**
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
* @param tex The LaTeX equation as string
* @param scale of the image, default value is 4
* @returns
*/
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
async tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
};
/**
*
* @param objectA
@@ -1896,15 +1933,16 @@ export class ExcalidrawAutomate {
/**
*
* @param includeFrameChildren
* @returns
*/
getViewSelectedElements(): any[] {
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewSelectedElements()");
return [];
}
return this.targetView.getViewSelectedElements();
return this.targetView.getViewSelectedElements(includeFrameChildren);
};
/**
@@ -2396,24 +2434,44 @@ export class ExcalidrawAutomate {
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInTheSameGroupWithElement(
element: ExcalidrawElement,
elements: ExcalidrawElement[],
includeFrameElements: boolean = false,
): ExcalidrawElement[] {
if(!element || !elements) return [];
const container = (element.type === "text" && element.containerId)
? elements.filter(el=>el.id === element.containerId)
: [];
if(element.groupIds.length === 0) {
if(includeFrameElements && element.type === "frame") {
return this.getElementsInFrame(element,elements,true);
}
if(container.length === 1) return [element,container[0]];
return [element];
}
if(container.length === 1) {
return elements.filter(el=>
el.groupIds.some(id=>element.groupIds.includes(id)) ||
el === container[0]
);
const conditionFN = container.length === 1
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
if(!includeFrameElements) {
return elements.filter(el=>conditionFN(el));
} else {
//I use the set and the filter at the end to preserve scene layer seqeuence
//adding frames could potentially mess up the sequence otherwise
const elementIDs = new Set<string>();
elements
.filter(el=>conditionFN(el))
.forEach(el=>{
if(el.type === "frame") {
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
} else {
elementIDs.add(el.id);
}
});
return elements.filter(el=>elementIDs.has(el.id));
}
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
}
/**
@@ -2738,7 +2796,7 @@ export async function initExcalidrawAutomate(
function normalizeLinePoints(
points: [x: number, y: number][],
//box: { x: number; y: number; w: number; h: number },
) {
): number[][] {
const p = [];
const [x, y] = points[0];
for (let i = 0; i < points.length; i++) {
@@ -2747,7 +2805,9 @@ function normalizeLinePoints(
return p;
}
function getLineBox(points: [x: number, y: number][]) {
function getLineBox(
points: [x: number, y: number][]
):{x:number, y:number, w: number, h:number} {
const [x1, y1, x2, y2] = estimateLineBound(points);
return {
x: x1,
@@ -2757,16 +2817,16 @@ function getLineBox(points: [x: number, y: number][]) {
};
}
function getFontFamily(id: number) {
getFontFamilyString({fontFamily:id})
function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
export async function initFonts() {
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
/*const fonts = excalidrawLib.getFontFamilies();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
await (document as any).fonts.load(`20px ${fonts[i]}`);
};*/
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
};
}
export function _measureText(
@@ -2774,7 +2834,7 @@ export function _measureText(
fontSize: number,
fontFamily: number,
lineHeight: number,
) {
): {w: number, h:number} {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
fontSize = 20;
@@ -2873,7 +2933,7 @@ async function getTemplate(
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length > 0) {
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
@@ -2932,7 +2992,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -3016,7 +3076,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
@@ -3165,7 +3225,7 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string) {
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({

View File

@@ -103,7 +103,7 @@ import {
_getContainerElement,
arrayToMap,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf } from "./utils/ObsidianUtils";
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "./utils/ObsidianUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "./dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -140,7 +140,6 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { SelectCard } from "./dialogs/SelectCard";
import { Packages } from "./types/types";
import React from "react";
import { StoreAction } from "@zsviczian/excalidraw";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -245,6 +244,8 @@ const warningUnknowSeriousError = () => {
type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
let windowMigratedDisableZoomOnce = false;
export default class ExcalidrawView extends TextFileView {
public exportDialog: ExportDialog;
public excalidrawData: ExcalidrawData;
@@ -1353,12 +1354,25 @@ export default class ExcalidrawView extends TextFileView {
const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined")
this.packages = this.plugin.getPackage(this.ownerWindow);
/*if(!DEVICE.isMobile && !apiMissing) {
if(DEVICE.isDesktop && !apiMissing) {
this.destroyers.push(
//@ts-ignore
this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
//this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
this.containerEl.onWindowMigrated(async() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onWindowMigrated");
const f = this.file;
const l = this.leaf;
await closeLeafView(l);
windowMigratedDisableZoomOnce = true;
l.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: {
file: f.path,
}
});
})
);
}*/
}
this.semaphores.scriptsReady = true;
@@ -1614,7 +1628,7 @@ export default class ExcalidrawView extends TextFileView {
}
const st = api.getAppState();
const isEditing = st.editingElement !== null;
const isDragging = st.draggingElement !== null;
const isDragging = st.newElement !== null;
//this will reset positioning of the cursor in case due to the popup keyboard,
//or the command palette, or some other unexpected reason the onResize would not fire...
this.refreshCanvasOffset();
@@ -2156,7 +2170,7 @@ export default class ExcalidrawView extends TextFileView {
await this.app.vault.modify(file, drawingBAK);
//@ts-ignore
plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
plugin.setExcalidrawView(leaf);
setExcalidrawView(leaf);
}
});
@@ -2203,8 +2217,8 @@ export default class ExcalidrawView extends TextFileView {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st);
const cm = this.plugin.ea.getCM(bgColor);
const isDark = cm.isDark();
const Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).stringHEX();
const Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).stringHEX();
const Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).stringHEX({alpha: false});
const Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).stringHEX({alpha: false});
return {Bold, Regular, MajorGridFrequency:st.gridColor.MajorGridFrequency};
}
@@ -3358,7 +3372,7 @@ export default class ExcalidrawView extends TextFileView {
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element);
if(!this.lastMouseEvent) return;
const st = this.excalidrawAPI?.getAppState();
if(st?.editingElement || st?.draggingElement) return; //should not activate hover preview when element is being edited or dragged
if(st?.editingElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
if(this.semaphores.wheelTimeout) return;
//if link text is not provided, try to get it from the element
if (!linktext) {
@@ -3683,7 +3697,7 @@ export default class ExcalidrawView extends TextFileView {
//Removed because of
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565
/*st.resizingElement === null &&
st.draggingElement === null &&
st.newElement === null &&
st.editingGroupId === null &&*/
st.editingLinearElement === null
) {
@@ -3752,8 +3766,14 @@ export default class ExcalidrawView extends TextFileView {
ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]);
ea.destroy();
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
const ea = getEA(this) as ExcalidrawAutomate;
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
ea.addElementsToView(false,false).then(()=>ea.destroy());
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
}
}
this.setDirty(9);
})) {
@@ -5510,6 +5530,10 @@ export default class ExcalidrawView extends TextFileView {
if (!api || this.semaphores.isEditingText || this.semaphores.preventAutozoom) {
return;
}
if (windowMigratedDisableZoomOnce) {
windowMigratedDisableZoomOnce = false;
return;
}
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000);
if((DEVICE.isMobile && elements.length>1000) || elements.length>2500) {
@@ -5663,9 +5687,14 @@ export default class ExcalidrawView extends TextFileView {
return api.getSceneElements();
}
public getViewSelectedElements(): ExcalidrawElement[] {
/**
*
* @param deepSelect: if set to true, child elements of the selected frame will also be selected
* @returns
*/
public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements");
const api = this.excalidrawAPI;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!api) {
return [];
}
@@ -5677,6 +5706,9 @@ export default class ExcalidrawView extends TextFileView {
if (!selectedElementsKeys) {
return [];
}
const elementIDs = new Set<string>();
const elements: ExcalidrawElement[] = api
.getSceneElements()
.filter((e: any) => selectedElementsKeys.includes(e.id));
@@ -5694,15 +5726,27 @@ export default class ExcalidrawView extends TextFileView {
.map((be) => be.id)[0],
);
const elementIDs = elements
.map((el) => el.id)
.concat(containerBoundTextElmenetsReferencedInElements);
if(includFrameChildren && elements.some(el=>el.type === "frame")) {
elements.filter(el=>el.type === "frame").forEach(frameEl => {
api.getSceneElements()
.filter(el=>el.frameId === frameEl.id)
.forEach(el=>elementIDs.add(el.id))
})
}
elements.forEach(el=>elementIDs.add(el.id));
containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id));
return api
.getSceneElements()
.filter((el: ExcalidrawElement) => elementIDs.contains(el.id));
.filter((el: ExcalidrawElement) => elementIDs.has(el.id));
}
/**
*
* @param prefix - defines the default button.
* @returns
*/
public async copyLinkToSelectedElementToClipboard(prefix:string) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix);
const elements = this.getViewSelectedElements();
@@ -5729,58 +5773,59 @@ export default class ExcalidrawView extends TextFileView {
: this.plugin.ea.getLargestElement(elements).id;
}
const isFrame = elements.some(el=>el.id === elementId && el.type==="frame");
const frames = elements.filter(el=>el.type==="frame");
const hasFrame = frames.length === 1;
const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0);
let button = {
area: {caption: "Area", action:()=>{prefix="area="; return;}},
link: {caption: "Link", action:()=>{prefix="";return}},
group: {caption: "Group", action:()=>{prefix="group="; return;}},
frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}},
clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}},
}
let buttons = [];
if(isFrame) {
switch(prefix) {
case "clippedframe=":
buttons = [
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
case "area=":
case "group=":
case "frame=":
buttons = [
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
]
}
} else {
switch(prefix) {
case "area=":
buttons = [
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Group", action:()=>{prefix="group="; return;}},
];
break;
case "group=":
buttons = [
{caption: "Group", action:()=>{prefix="group="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
]
}
switch(prefix) {
case "area=":
buttons = [
button.area,
button.link,
...hasGroup ? [button.group] : [],
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "group=":
buttons = [
...hasGroup ? [button.group] : [],
button.link,
button.area,
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "frame=":
buttons = [
...hasFrame ? [button.frame, button.clippedframe] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
case "clippedframe=":
buttons = [
...hasFrame ? [button.clippedframe, button.frame] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
...hasFrame ? [button.frame, button.clippedframe] : [],
]
}
const alias = await ScriptEngine.inputPrompt(

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -25,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -38,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -60,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -74,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -140,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
@@ -171,6 +178,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -238,6 +246,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -300,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -347,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -375,6 +386,7 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
@@ -502,6 +514,7 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
@@ -510,6 +523,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -541,6 +555,7 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
@@ -577,6 +592,7 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
@@ -596,6 +612,7 @@ const processAltText = (
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
@@ -606,7 +623,11 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -624,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
@@ -656,14 +677,14 @@ const tmpObsidianWYSIWYG = async (
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
@@ -675,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
@@ -762,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -771,12 +793,35 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
if(isPrinting && el.hasClass("mod-frontmatter")) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = isPrinting || isMarkdownReadingMode ||
(isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if (!isPreview && embeddedItems.length === 0) {
if(el.hasClass("mod-frontmatter")) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId)) {
if(!el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -785,8 +830,7 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
if (excalidrawFile && !isPrinting) {
if (!isPreview && excalidrawFile) {
el.style.display = "none";
return;
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent, TFile } from "obsidian";
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
@@ -7,9 +7,11 @@ import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { t } from "src/lang/helpers";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private frame: boolean = false;
private gapSize:number = 20;
private groupPages: boolean = false;
private direction: "down" | "right" = "right";
@@ -48,6 +50,7 @@ export class InsertPDFModal extends Modal {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfFrame = this.frame;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
@@ -120,6 +123,7 @@ export class InsertPDFModal extends Modal {
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.frame = this.plugin.settings.pdfFrame;
this.gapSize = this.plugin.settings.pdfGapSize;
this.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
@@ -138,13 +142,13 @@ export class InsertPDFModal extends Modal {
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importMessage.innerText = t("IPM_SELECT_PDF");
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
@@ -161,7 +165,7 @@ export class InsertPDFModal extends Modal {
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
numPagesMessage.innerText = t("IPM_SELECT_PDF");
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
@@ -211,7 +215,7 @@ export class InsertPDFModal extends Modal {
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
new Setting(ce)
.setName("Pages to import")
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
.setDesc("e.g.: 1,3-5,7,9-10")
.addText(text => {
pageRangesTextComponent = text;
@@ -222,18 +226,52 @@ export class InsertPDFModal extends Modal {
})
importPagesMessage = ce.createEl("p", {text: ""});
new Setting(ce)
.setName("Add border box")
.addToggle(toggle => toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
this.dirty = true;
}))
let bbToggle: ToggleComponent;
let fToggle: ToggleComponent;
let laiToggle: ToggleComponent;
this.frame = this.borderBox ? false : this.frame;
new Setting(ce)
.setName("Group pages")
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
.addToggle(toggle => {
bbToggle = toggle;
toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
if(value) {
this.frame = false;
fToggle.setValue(false);
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_ADD_FRAME_NAME"))
.setDesc(t("IPM_ADD_FRAME_DESC"))
.addToggle(toggle => {
fToggle = toggle;
toggle
.setValue(this.frame)
.onChange((value) => {
this.frame = value;
if(value) {
this.borderBox = false;
bbToggle.setValue(false);
if(!this.lockAfterImport) {
this.lockAfterImport = true;
laiToggle.setValue(true);
}
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_GROUP_PAGES_NAME"))
.setDesc(t("IPM_GROUP_PAGES_DESC"))
.addToggle(toggle => toggle
.setValue(this.groupPages)
.onChange((value) => {
@@ -244,12 +282,15 @@ export class InsertPDFModal extends Modal {
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
}))
.addToggle(toggle => {
laiToggle = toggle;
toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
})
})
let numColumnsSetting: Setting;
let numRowsSetting: Setting;
@@ -391,6 +432,12 @@ export class InsertPDFModal extends Modal {
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
if(this.frame) {
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
ea.addElementsToFrame(frameID, [boxID,imageID]);
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
}
switch(this.direction) {
case "right":
@@ -404,7 +451,9 @@ export class InsertPDFModal extends Modal {
}
}
if(this.groupPages) {
const ids = ea.getElements().map(el => el.id);
const ids = ea.getElements()
.filter(el=>!this.frame || (el.type === "frame"))
.map(el => el.id);
ea.addToGroup(ids);
}
await ea.addElementsToView(true,true,false);

View File

@@ -17,6 +17,21 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.3.0": `
I am moving to a new release approach aiming to publish one update per month to the Obsidian script store. If you want to continue to receive more frequent updates with new features and minor bug fixes, then join the beta testing team. [#1912](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1912)
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/2poSS-Z91lY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Elbow connectors: https://x.com/excalidraw/status/1819084086222393554
## Fixed
- Convert Markdown to Excalidraw did not work correctly when there was ${String.fromCharCode(96)}---${String.fromCharCode(96)} anywhere in the file, but no frontmatter (e.g. a table)
- Fixed Obsidian move tab to new window
- Fixed duplicating bound arrows without its bound elements throwing error [#8315](https://github.com/excalidraw/excalidraw/issues/8315)
`,
"2.2.13": `
## Fixed
- Could not undo element after pasting [#1906](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1906)

View File

@@ -233,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addElementsToFrame",
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
desc: null,
after: "",
},
{
field: "addFrame",
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
desc: null,
after: "",
},
{
field: "addRect",
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
@@ -311,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "tex2dataURL",
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
@@ -387,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getViewSelectedElements",
code: "getViewSelectedElements(): ExcalidrawElement[];",
desc: null,
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
after: "",
},
{
@@ -489,8 +507,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getElementsInTheSameGroupWithElement",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
after: ""
},
{

View File

@@ -322,24 +322,29 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
"The effect depends on the device. Crosshair is typically visible on drawing tablets, MS Surface, but not on iOS.",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render image in hover preview for MD files",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render Excalidraw file as an image in hover preview...",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"This setting effects files that have the <b>excalidraw-open-md: true</b> frontmatter key.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"Must close the active excalidraw/markdown file and reopen it for this change to take effect.<br>When you are in markdown reading mode (aka. reading the back side of the drawing), should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode, when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render image when EXPORT TO PDF in markdown mode",
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"Must close the active excalidraw/markdown file and reopen for this change to take effect.<br>When you are printing the markdown side of the note to PDF (aka. the back side of the drawing), should the Excalidraw drawing be rendered as an image?<br><ul>" +
"<li>See other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
"This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -835,4 +840,16 @@ FILENAME_HEAD: "Filename",
FRAME_SETTIGNS_NAME: "Display Frame Name",
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
IPM_ADD_BORDER_BOX_NAME: "Add border box",
IPM_ADD_FRAME_NAME: "Add page to frame",
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
IPM_GROUP_PAGES_NAME: "Group pages",
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
IPM_SELECT_PDF: "Please select a PDF file",
};

View File

@@ -101,7 +101,7 @@ import {
versionUpdateCheckTimer,
getFontMetrics,
} from "./utils/Utils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf, setExcalidrawView } from "./utils/ObsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ScriptEngine } from "./Scripts";
import {
@@ -187,7 +187,7 @@ export default class ExcalidrawPlugin extends Plugin {
public editorHandler: EditorHandler;
//if set, the next time this file is opened it will be opened as markdown
public forceToOpenInMarkdownFilepath: string = null;
private slob:string;
//private slob:string;
private ribbonIcon:HTMLElement;
public loadTimestamp:number;
@@ -201,9 +201,9 @@ export default class ExcalidrawPlugin extends Plugin {
this.equationsMaster = new Map<FileId, string>();
this.mermaidsMaster = new Map<FileId, string>();
setExcalidrawPlugin(this);
if((process.env.NODE_ENV === 'development')) {
/*if((process.env.NODE_ENV === 'development')) {
this.slob = new Array(200 * 1024 * 1024 + 1).join('A'); // Create a 200MB blob
}
}*/
}
get locale() {
@@ -488,7 +488,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, this.app)) {
this.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(leaf);
setExcalidrawView(leaf);
} else {
foldExcalidrawSection(leaf.view);
}
@@ -2377,7 +2377,7 @@ export default class ExcalidrawPlugin extends Plugin {
const activeLeaf = markdownView.leaf;
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(activeLeaf);
setExcalidrawView(activeLeaf);
})()
return;
}
@@ -2412,7 +2412,7 @@ export default class ExcalidrawPlugin extends Plugin {
activeFile,
mergedTarget,
);
this.setExcalidrawView(activeView.leaf);
setExcalidrawView(activeView.leaf);
})();
},
});
@@ -2538,7 +2538,7 @@ export default class ExcalidrawPlugin extends Plugin {
await view.save();
//@ts-ignore
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(leaf);
setExcalidrawView(leaf);
}));
},
),
@@ -2563,7 +2563,7 @@ export default class ExcalidrawPlugin extends Plugin {
await view.save();
//@ts-ignore
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
this.setExcalidrawView(leaf);
setExcalidrawView(leaf);
})});
//@ts-ignore
menu.items.unshift(menu.items.pop());
@@ -3598,14 +3598,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
public async setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawView,`ExcalidrawPlugin.setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
public isExcalidrawFile(f: TFile) {
if(!f) return false;

View File

@@ -78,6 +78,7 @@ export interface ExcalidrawSettings {
matchThemeTrigger: boolean;
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeDoubleTapEraser: boolean;
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
@@ -168,6 +169,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -243,6 +245,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeDoubleTapEraser: true,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
@@ -339,6 +342,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -1024,6 +1028,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))

View File

@@ -10,7 +10,8 @@ export let DEBUGGING = false;
export const log = console.log.bind(window.console);
export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
console.log(fnName,fn,...messages);
//console.log(fnName,fn,...messages);
console.log(fnName, ...messages);
};
export class CustomMutationObserver {

View File

@@ -253,16 +253,16 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const store = this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
return await new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
@@ -275,7 +275,7 @@ class ImageCache {
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const store = this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
@@ -308,7 +308,9 @@ class ImageCache {
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
new Promise<undefined>((_,reject) => setTimeout(() => {
reject(undefined);
}, 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;

View File

@@ -3,13 +3,14 @@ import {
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml, { Mark } from "js-yaml";
import { debug, DEBUGGING } from "./DebugHelper";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -300,7 +301,7 @@ export const openLeaf = ({
return {leaf, promise};
}
export const mergeMarkdownFiles = (template: string, target: string): string => {
export function mergeMarkdownFiles (template: string, target: string): string {
// Extract frontmatter from the template
const templateFrontmatterEnd = template.indexOf('---', 4); // Find end of frontmatter
const templateFrontmatter = template.substring(4, templateFrontmatterEnd).trim();
@@ -312,8 +313,8 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
// Extract frontmatter from the target if it exists
let targetFrontmatterObj: FrontMatterCache = {};
let targetContent = '';
if (target.includes('---')) {
const targetFrontmatterEnd = target.indexOf('---', 4); // Find end of frontmatter
if (target.startsWith('---\n') && target.indexOf('---\n', 4) > 0) {
const targetFrontmatterEnd = target.indexOf('---\n', 4); // Find end of frontmatter
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
@@ -392,3 +393,20 @@ export const foldExcalidrawSection = (view: MarkdownView) => {
view.currentMode.applyFoldInfo({ folds: foldPositions, lines: lineCount });
}
};
export async function setExcalidrawView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
export async function closeLeafView(leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setExcalidrawView,`setExcalidrawView`, leaf);
await leaf.setViewState({
type: "empty",
state: {},
});
}

View File

@@ -922,3 +922,20 @@ export async function getFontMetrics(fontUrl: string, name: string): Promise<Fon
return null;
}
}
// Thanks https://stackoverflow.com/a/54555834
export function cropCanvas(
srcCanvas: HTMLCanvasElement,
crop: { left: number, top: number, width: number, height: number },
output: { width: number, height: number } = { width: crop.width, height: crop.height })
{
const dstCanvas = createEl('canvas');
dstCanvas.width = output.width;
dstCanvas.height = output.height;
dstCanvas.getContext('2d')!.drawImage(
srcCanvas,
crop.left, crop.top, crop.width, crop.height,
0, 0, output.width, output.height
);
return dstCanvas;
}