Compare commits

...

30 Commits

Author SHA1 Message Date
zsviczian
7cac94bf2f 2.5.3-beta-1 2024-10-19 20:43:48 +02:00
zsviczian
43e98db174 remove font assets, replace with zip 2024-10-19 16:21:20 +02:00
zsviczian
253575bf23 font assets 2024-10-19 16:11:24 +02:00
zsviczian
7a08ced65a Merge pull request #2066 from heinrich26/master
Make the Tab Icons color change as well, if a Tab is dirty (unsaved)
2024-10-19 06:40:18 +02:00
Hendrik Horstmann
5a64e1c75e Merge branch 'zsviczian:master' into master 2024-10-16 19:28:22 +02:00
zsviczian
fc0ac92dd3 2.5.2 2024-10-13 20:59:43 +02:00
zsviczian
4e2d7eb637 Update README.md 2024-09-28 22:32:18 +02:00
zsviczian
f8f280c7d5 2.5.1 2024-09-28 14:10:25 +02:00
zsviczian
00b87f99c0 Merge pull request #2038 from mxsdlr/master
Update color palette details in README
2024-09-25 17:14:56 +02:00
mxsdlr
0b5c74dde8 Add Decompress JSON hint to README.md
- Add hint to "Decompress Excalidraw JSON in Markdown View" setting when editing JSON content
2024-09-24 14:33:10 +02:00
mxsdlr
906b3bdf92 Update color palette details in README
- Change `customColorPalette` to `colorPalette`
- Add section about `topPicks`
2024-09-24 11:43:29 +02:00
zsviczian
0c28e82212 2.5.1-beta-2 2024-09-22 16:22:17 +02:00
zsviczian
beb4301f14 Merge pull request #2033 from dmscode/master
Update zh-cn.ts to 268680f
2024-09-22 15:21:11 +02:00
dmscode
e96fe9c491 Update zh-cn.ts to 268680f 2024-09-22 07:18:10 +08:00
zsviczian
268680f494 2.5.1-beta-1 2024-09-19 20:22:04 +02:00
zsviczian
a1512fce26 Merge pull request #2024 from dmscode/master
Update zh-cn.ts to 74c0af2
2024-09-19 20:08:58 +02:00
zsviczian
c2e79f3439 Update bug_report.yml 2024-09-14 11:25:36 +02:00
zsviczian
01780a2bf8 Update bug_report.yml 2024-09-14 11:24:44 +02:00
zsviczian
10e54eb03e Update bug_report.yml 2024-09-14 11:24:01 +02:00
zsviczian
2760a9966b Update bug_report.yml 2024-09-14 11:23:40 +02:00
dmscode
a297dbbe52 Update zh-cn.ts to 74c0af2
And fixed some link path
2024-09-14 08:09:10 +08:00
zsviczian
74c0af2032 2.5.0 2024-09-13 18:45:13 +02:00
zsviczian
813c85accd 2.5.0-rc-1 2024-09-13 08:18:19 +02:00
zsviczian
c97d08c997 Merge pull request #2017 from zsviczian/fix-getExcalidrawViews
fixed getExcalidrawView
2024-09-12 12:29:49 +02:00
zsviczian
097d1bcd1b fixed getExcalidrawView 2024-09-12 10:27:04 +00:00
zsviczian
7c91186ed5 Update ExcalidrawViewUtils.ts 2024-09-12 11:41:47 +02:00
zsviczian
904bc7c994 Update ExcalidrawView.ts 2024-09-12 11:39:25 +02:00
zsviczian
9fd4ae2615 Update manifest-beta.json 2024-09-12 11:16:51 +02:00
zsviczian
18fbb0934e Merge pull request #2016 from zsviczian/style-tweaks
Obsidian 1.7.2: rework getExcalidrawViews from leaves, fix tools pane…
2024-09-12 11:10:11 +02:00
Hendrik Horstmann
a5771625df Make the Tab Icons color change as well, if a Tab is dirty (unsaved) 2024-05-22 16:17:30 +02:00
30 changed files with 617 additions and 166 deletions

View File

@@ -1,5 +1,5 @@
name: Bug report
description: When something is clearly broken. Everything else is a feature request.
description: If something is clearly broken, its a bug. Everything else is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
title: "BUG: "
body:
- type: markdown

View File

@@ -2,7 +2,7 @@
[简体中文](./docs/zh-cn/README.md)
👉👉👉 Check out and contribute to the new [Obsidian-Excalidraw Community Wiki](https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/WIKI/Welcome+to+the+WIKI)
👉👉👉 Check out and contribute to the new [Obsidian-Excalidraw Community Wiki](https://excalidraw-obsidian.online/WIKI/Welcome+to+the+WIKI)
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
@@ -100,15 +100,17 @@ Plugin settings are grouped into the following sections:
#### Templates
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate. With versions 1.6.13 or higher make sure to enable "Decompress Excalidraw JSON in Markdown View" in the settings before editing the JSON in the template. This can be disabled after the canges are performed.
- Via the template, you can customize the color palette used by Excalidraw.
- Switch to Markdown view.
- Scroll down to the bottom of the file and find `"AppState": {`.
- Find `"customColorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma-separated list of valid HTML colors (e.g. `#FF0000` for red).
in the array for each of the variables.
- Find `"colorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma-separated list of valid HTML colors (e.g. `#FF0000` for red) in the array for each of the variables.
- To change the previewed colors, a `"topPicks": {` may be specified containing the same three keys:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Note that the corresponding arrays must contain 5 elements.
- See my videos above for further help.
#### Export
@@ -227,6 +229,7 @@ For more details, see this [video](https://youtu.be/yZQoJg2RCKI)
- `excalidraw-export-dark`: true == Dark mode / false == light mode.
- `excalidraw-export-padding`: Specify the export padding for the image.
- `excalidraw-export-pngscale`: This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.
- Since 1.6.13, enable "Decompress Excalidraw JSON in Markdown View" in the settings if you want to change any JSON content.
### Embed complete markdown files into your drawings

BIN
assets/excalidraw-fonts.zip Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
> 此说明当前更新至 `5569cff`。
[English](./AutomateHowTo.md)
[English](../../AutomateHowTo.md)
Excalidraw 自动化允许您使用 [Templater](https://github.com/SilentVoid13/Templater) 插件创建 Excalidraw 绘图。

View File

@@ -2,7 +2,7 @@
> 此说明当前更新至 `5569cff`。
[English](./README.md)
[English](../../README.md)
👉👉👉 快来查看并为新的 [Obsidian-Excalidraw 社区维基](https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/WIKI/Welcome+to+the+WIKI)贡献你的力量吧

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-52",
"@zsviczian/excalidraw": "0.17.6-2",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -655,7 +655,7 @@ export class EmbeddedFilesLoader {
let equation;
const equations = excalidrawData.getEquationEntries();
while (!this.terminate && !(equation = equations.next()).done) {
if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue;
if(fileIDWhiteList && !fileIDWhiteList.has(equation.value[0])) continue;
if (!excalidrawData.getEquation(equation.value[0]).isLoaded) {
const latex = equation.value[1].latex;
const data = await tex2dataURL(latex);

View File

@@ -93,6 +93,7 @@ import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialog
import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
import { log } from "./utils/DebugHelper";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
extendPlugins([
HarmonyPlugin,
@@ -281,8 +282,17 @@ export class ExcalidrawAutomate {
return LZString.compressToBase64(str);
}
public decompressFromBase64(str:string): string {
return LZString.decompressFromBase64(str);
public decompressFromBase64(data:string): string {
if (!data) throw new Error("No input string provided for decompression.");
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\\n' && char !== '\\r') {
cleanedData += char;
}
}
return LZString.decompressFromBase64(cleanedData);
}
/**
@@ -1393,8 +1403,8 @@ export class ExcalidrawAutomate {
): string {
const box = getLineBox(points);
id = id ?? nanoid();
const startPoint = points[0];
const endPoint = points[points.length - 1];
const startPoint = points[0] as GlobalPoint;
const endPoint = points[points.length - 1] as GlobalPoint;
this.elementsDict[id] = {
points: normalizeLinePoints(points),
lastCommittedPoint: null,
@@ -1684,8 +1694,8 @@ export class ExcalidrawAutomate {
if (!connectionA) {
const intersect = intersectElementWithLine(
elA,
[bCenterX, bCenterY],
[aCenterX, aCenterY],
[bCenterX, bCenterY] as GlobalPoint,
[aCenterX, aCenterY] as GlobalPoint,
GAP,
);
if (intersect.length === 0) {
@@ -1698,8 +1708,8 @@ export class ExcalidrawAutomate {
if (!connectionB) {
const intersect = intersectElementWithLine(
elB,
[aCenterX, aCenterY],
[bCenterX, bCenterY],
[aCenterX, aCenterY] as GlobalPoint,
[bCenterX, bCenterY] as GlobalPoint,
GAP,
);
if (intersect.length === 0) {
@@ -2412,7 +2422,12 @@ export class ExcalidrawAutomate {
b: readonly [number, number],
gap?: number,
): Point[] {
return intersectElementWithLine(element, a, b, gap);
return intersectElementWithLine(
element,
a as GlobalPoint,
b as GlobalPoint,
gap
);
};
/**
@@ -2869,11 +2884,11 @@ function getFontFamily(id: number):string {
}
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
/*await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
};
};*/
}
export function _measureText(
@@ -3425,7 +3440,7 @@ export const getFrameElementsMatchingQuery = (
el.type === "frame" &&
query.some((q) => {
if (exactMatch) {
const text = el.name.toLowerCase().split("\n")[0].trim();
const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
const m = text.match(/^#*(# .*)/);
if (!m || m.length !== 2) {
return false;
@@ -3506,4 +3521,10 @@ export const cloneElement = (el: ExcalidrawElement):any => {
export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
}
}
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.length
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
: null;
};

View File

@@ -17,6 +17,7 @@ import {
FRONTMATTER_KEYS,
refreshTextDimensions,
getContainerElement,
loadSceneFonts,
} from "./constants/constants";
import ExcalidrawPlugin from "./main";
import { TextMode } from "./ExcalidrawView";
@@ -51,6 +52,9 @@ import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./
import { DEBUGGING, debug } from "./utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
import { getNewUniqueFilepath } from "./utils/FileUtils";
import { t } from "./lang/helpers";
import { displayFontMessage } from "./utils/ExcalidrawViewUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -744,6 +748,15 @@ export class ExcalidrawData {
this.deletedElements = this.scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted);
this.scene.elements = this.scene.elements.filter((el:ExcalidrawElement)=>!el.isDeleted);
const timer = window.setTimeout(()=>{
const notice = new Notice(t("FONT_LOAD_SLOW"),15000);
notice.noticeEl.oncontextmenu = () => {
displayFontMessage(this.app);
}
},2000);
await loadSceneFonts(this.scene.elements);
clearTimeout(timer);
if (!this.scene.files) {
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
@@ -1506,31 +1519,40 @@ export class ExcalidrawData {
return result;
}
public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId) {
public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId, name?:string) {
const scene = this.scene as SceneDataWithFiles;
let fname = `Pasted Image ${window
.moment()
.format("YYYYMMDDHHmmss_SSS")}`;
switch (mimeType) {
case "image/png":
fname += ".png";
break;
case "image/jpeg":
fname += ".jpg";
break;
case "image/svg+xml":
fname += ".svg";
break;
case "image/gif":
fname += ".gif";
break;
default:
fname += ".png";
let fname = name;
if(!fname) {
fname = `Pasted Image ${window
.moment()
.format("YYYYMMDDHHmmss_SSS")}`;
switch (mimeType) {
case "image/png":
fname += ".png";
break;
case "image/jpeg":
fname += ".jpg";
break;
case "image/svg+xml":
fname += ".svg";
break;
case "image/gif":
fname += ".gif";
break;
default:
fname += ".png";
}
}
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
const filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
/*
const filepath = (
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname)
).filepath;
).filepath;*/
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
if(!arrayBuffer) return null;
@@ -1657,7 +1679,9 @@ export class ExcalidrawData {
await this.saveDataURLtoVault(
scene.files[key].dataURL,
scene.files[key].mimeType,
key as FileId
key as FileId,
//@ts-ignore
scene.files[key].name,
);
}
}

View File

@@ -3,8 +3,9 @@ import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/t
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
type EmbeddedLink =
| ({
@@ -75,16 +76,16 @@ declare namespace ExcalidrawLib {
function determineFocusDistance(
element: ExcalidrawBindableElement,
a: Point,
b: Point,
a: GlobalPoint,
b: GlobalPoint,
): number;
function intersectElementWithLine(
element: ExcalidrawBindableElement,
a: Point,
b: Point,
a: GlobalPoint,
b: GlobalPoint,
gap?: number,
): Point[];
): GlobalPoint[];
function getCommonBoundingBox(
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
@@ -186,5 +187,6 @@ declare namespace ExcalidrawLib {
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
function loadSceneFonts(elements: NonDeletedExcalidrawElement[]): Promise<void>;
}

View File

@@ -9,6 +9,8 @@ import {
MarkdownView,
request,
requireApiVersion,
HoverParent,
HoverPopover,
} from "obsidian";
//import * as React from "react";
//import * as ReactDOM from "react-dom";
@@ -63,7 +65,8 @@ import {
cloneElement,
getFrameElementsMatchingQuery,
getElementsWithLinkMatchingQuery,
getImagesMatchingQuery
getImagesMatchingQuery,
getBoundTextElementId
} from "./ExcalidrawAutomate";
import { t } from "./lang/helpers";
import {
@@ -251,7 +254,8 @@ type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
let windowMigratedDisableZoomOnce = false;
export default class ExcalidrawView extends TextFileView {
export default class ExcalidrawView extends TextFileView implements HoverParent{
public hoverPopover: HoverPopover;
private freedrawLastActiveTimestamp: number = 0;
public exportDialog: ExportDialog;
public excalidrawData: ExcalidrawData;
@@ -277,6 +281,7 @@ export default class ExcalidrawView extends TextFileView {
private embeddableLeafRefs = new Map<ExcalidrawElement["id"], any>();
public semaphores: {
warnAboutLinearElementLinkClick: boolean;
//flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing
embeddableIsEditingSelf: boolean;
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
@@ -313,6 +318,7 @@ export default class ExcalidrawView extends TextFileView {
hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times
wheelTimeout:number; //used to avoid hover preview while zooming
} | null = {
warnAboutLinearElementLinkClick: true,
embeddableIsEditingSelf: false,
popoutUnload: false,
viewunload: false,
@@ -520,8 +526,8 @@ export default class ExcalidrawView extends TextFileView {
if (!svg) {
return;
}
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svg);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgString = svg.outerHTML;
if (file && file instanceof TFile) {
await this.app.vault.modify(file, svgString);
} else {
@@ -1141,61 +1147,111 @@ export default class ExcalidrawView extends TextFileView {
private getLinkTextForElement(
selectedText:SelectedElementWithLink,
selectedElementWithLink?:SelectedElementWithLink
selectedElementWithLink?:SelectedElementWithLink,
allowLinearElementClick: boolean = false,
): {
linkText: string,
selectedElement: ExcalidrawElement,
isLinearElement: boolean,
} {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getLinkTextForElement, "ExcalidrawView.getLinkTextForElement", selectedText, selectedElementWithLink);
if (selectedText?.id || selectedElementWithLink?.id) {
const selectedTextElement: ExcalidrawTextElement = selectedText.id
let selectedTextElement: ExcalidrawTextElement = selectedText.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id)
: null;
const selectedElement = selectedElementWithLink.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedElementWithLink.id)
let selectedElement = selectedElementWithLink.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>
el.id === selectedElementWithLink.id)
: null;
//if the user clicked on the label of an arrow then the label will be captured in selectedElement, because
//Excalidraw returns the container as the selected element. But in this case we want this to be treated as the
//text element, as the assumption is, if the user wants to invoke the linear element editor for an arrow that has
//a label with a link, then he/she should rather CTRL+click on the arrow line, not the label. CTRL+Click on
//the label is an indication of wanting to navigate.
if (!Boolean(selectedTextElement) && selectedElement?.type === "text") {
const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements()));
if(container?.type === "arrow") {
const x = getTextElementAtPointer(this.currentPosition,this);
if(x?.id === selectedElement.id) {
selectedTextElement = selectedElement;
selectedElement = null;
}
}
}
//CTRL click on a linear element with a link will navigate instead of line editor
if(!allowLinearElementClick && ["arrow", "line"].includes(selectedElement?.type)) {
return {linkText: selectedElement.link, selectedElement: selectedElement, isLinearElement: true};
}
if (!selectedTextElement && selectedElement?.type === "text") {
if(!allowLinearElementClick) {
//CTRL click on a linear element with a link will navigate instead of line editor
const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements()));
if(container?.type !== "arrow") {
selectedTextElement = selectedElement as ExcalidrawTextElement;
selectedElement = null;
} else {
const x = this.processLinkText(selectedElement.rawText, selectedElement as ExcalidrawTextElement, container, false);
return {linkText: x.linkText, selectedElement: container, isLinearElement: true};
}
} else {
selectedTextElement = selectedElement as ExcalidrawTextElement;
selectedElement = null;
}
}
let linkText =
selectedElementWithLink?.text ??
(this.textMode === TextMode.parsed
? this.excalidrawData.getRawText(selectedText.id)
: selectedText.text);
if(linkText.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
return {...this.processLinkText(linkText, selectedTextElement, selectedElement), isLinearElement: false};
}
return {linkText: null, selectedElement: null, isLinearElement: false};
}
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
}
processLinkText(linkText: string, selectedTextElement: ExcalidrawTextElement, selectedElement: ExcalidrawElement, shouldOpenLink: boolean = true) {
if(!linkText) {
return {linkText: null, selectedElement: null};
}
const partsArray = REGEX_LINK.getResList(linkText);
if (!linkText || partsArray.length === 0) {
//the container link takes precedence over the text link
if(selectedTextElement?.containerId) {
const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
if(linkText?.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
const maybeObsidianLink = parseObsidianLink(linkText, this.app);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
}
}
}
if(!linkText || partsArray.length === 0) {
linkText = selectedTextElement?.link;
}
}
if(linkText.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
return {linkText: null, selectedElement: null};
const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
}
const partsArray = REGEX_LINK.getResList(linkText);
if (!linkText || partsArray.length === 0) {
//the container link takes precedence over the text link
if(selectedTextElement?.containerId) {
const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
if(linkText?.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink);
if(typeof maybeObsidianLink === "string") {
linkText = maybeObsidianLink;
}
}
}
if(!linkText || partsArray.length === 0) {
linkText = selectedTextElement?.link;
}
}
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
async linkClick(
@@ -1203,7 +1259,8 @@ export default class ExcalidrawView extends TextFileView {
selectedText: SelectedElementWithLink,
selectedImage: SelectedImage,
selectedElementWithLink: SelectedElementWithLink,
keys?: ModifierKeys
keys?: ModifierKeys,
allowLinearElementClick: boolean = false,
) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.linkClick, "ExcalidrawView.linkClick", ev, selectedText, selectedImage, selectedElementWithLink, keys);
if(!selectedText) selectedText = {id:null, text: null};
@@ -1216,10 +1273,17 @@ export default class ExcalidrawView extends TextFileView {
let file = null;
let subpath: string = null;
let {linkText, selectedElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink);
let {linkText, selectedElement, isLinearElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink, allowLinearElementClick);
//if (selectedText?.id || selectedElementWithLink?.id) {
if (selectedElement) {
if (!allowLinearElementClick && linkText && isLinearElement) {
if(this.semaphores.warnAboutLinearElementLinkClick) {
new Notice(t("LINEAR_ELEMENT_LINK_CLICK_ERROR"), 20000);
this.semaphores.warnAboutLinearElementLinkClick = false;
}
return;
}
if (!linkText) {
return;
}
@@ -1298,6 +1362,9 @@ export default class ExcalidrawView extends TextFileView {
}
if (!linkText) {
if(allowLinearElementClick) {
return;
}
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000);
return;
}
@@ -1348,7 +1415,7 @@ export default class ExcalidrawView extends TextFileView {
//if link will open in the same pane I want to save the drawing before opening the link
await this.forceSaveIfRequired();
const {leaf, promise} = openLeaf({
const { promise } = openLeaf({
plugin: this.plugin,
fnGetLeaf: () => getLeaf(this.plugin,this.leaf,keys),
file,
@@ -1364,7 +1431,7 @@ export default class ExcalidrawView extends TextFileView {
}
}
async handleLinkClick(ev: MouseEvent | ModifierKeys) {
async handleLinkClick(ev: MouseEvent | ModifierKeys, allowLinearElementClick: boolean = false) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkClick, "ExcalidrawView.handleLinkClick", ev);
this.removeLinkTooltip();
@@ -1382,6 +1449,7 @@ export default class ExcalidrawView extends TextFileView {
selectedImage,
selectedElementWithLink,
ev instanceof MouseEvent ? null : ev,
allowLinearElementClick,
);
}
@@ -1545,7 +1613,9 @@ export default class ExcalidrawView extends TextFileView {
if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) {
return;
}
this.forceSave(true);
if((this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().activeTool.type !== "image") {
this.forceSave(true);
}
};
this.registerDomEvent(this.ownerWindow, "keydown", onKeyDown, false);
@@ -1802,6 +1872,7 @@ export default class ExcalidrawView extends TextFileView {
new Notice("Unknown error, save is taking too long");
return;
}
await this.forceSaveIfRequired();
}
private async forceSaveIfRequired():Promise<boolean> {
@@ -1810,9 +1881,9 @@ export default class ExcalidrawView extends TextFileView {
let dirty = false;
//if saving was already in progress
//the function awaits the save to finish.
while (this.semaphores.saving && watchdog++ < 10) {
while (this.semaphores.saving && watchdog++ < 200) {
dirty = true;
await sleep(20);
await sleep(40);
}
if(this.excalidrawAPI) {
this.checkSceneVersion(this.excalidrawAPI.getSceneElements());
@@ -2148,6 +2219,7 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear");
this.semaphores.warnAboutLinearElementLinkClick = true;
this.viewSaveData = "";
this.canvasNodeFactory.purgeNodes();
this.embeddableRefs.clear();
@@ -2337,8 +2409,8 @@ export default class ExcalidrawView extends TextFileView {
if (this.plugin.settings.gridSettings.DYNAMIC_COLOR) {
// Dynamic color: concatenate opacity to the HEX string
Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).alphaTo(opacity).stringRGB({ alpha: true });
Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).alphaTo(opacity).stringRGB({ alpha: true });
Regular = (isDark ? cm.lighterBy(10) : cm.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true });
Bold = (isDark ? cm.lighterBy(5) : cm.darkerBy(5)).alphaTo(opacity).stringRGB({ alpha: true });
} else {
// Custom color handling
const customCM = this.plugin.ea.getCM(this.plugin.settings.gridSettings.COLOR);
@@ -2348,7 +2420,7 @@ export default class ExcalidrawView extends TextFileView {
Regular = customCM.alphaTo(opacity).stringRGB({ alpha: true });
// Bold is 7 shades lighter or darker based on the custom color's darkness
Bold = (customIsDark ? customCM.lighterBy(7) : customCM.darkerBy(7)).alphaTo(opacity).stringRGB({ alpha: true });
Bold = (customIsDark ? customCM.lighterBy(10) : customCM.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true });
}
return { Bold, Regular };
@@ -2673,6 +2745,8 @@ export default class ExcalidrawView extends TextFileView {
}
if(!DEVICE.isMobile) {
if(requireApiVersion("0.16.0")) {
//@ts-ignore
this.leaf.tabHeaderInnerIconEl.style.color="var(--color-accent)"
//@ts-ignore
this.leaf.tabHeaderInnerTitleEl.style.color="var(--color-accent)"
}
@@ -2701,6 +2775,8 @@ export default class ExcalidrawView extends TextFileView {
this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty");
if(!DEVICE.isMobile) {
if(requireApiVersion("0.16.0")) {
//@ts-ignore
this.leaf.tabHeaderInnerIconEl.style.color=""
//@ts-ignore
this.leaf.tabHeaderInnerTitleEl.style.color=""
}
@@ -3134,6 +3210,16 @@ export default class ExcalidrawView extends TextFileView {
};
}
const textId = getBoundTextElementId(selectedElement[0]);
if (textId) {
const textElement = api
.getSceneElements()
.filter((el: any) => el.id === textId && el.link);
if (textElement.length > 0) {
return { id: textElement[0].id, text: textElement[0].text };
}
}
if (selectedElement[0].groupIds.length === 0) {
return { id: null, text: null };
} //is the selected element part of a group?
@@ -3362,6 +3448,13 @@ export default class ExcalidrawView extends TextFileView {
toDelete.forEach((k) => delete files[k]);
}
const activeTool = {...st.activeTool};
if(!["freedraw","hand"].includes(activeTool.type)) {
activeTool.type = "selection";
}
activeTool.customType = null;
activeTool.lastActiveTool = null;
return {
type: "excalidraw",
version: 2,
@@ -3396,7 +3489,7 @@ export default class ExcalidrawView extends TextFileView {
currentStrokeOptions: st.currentStrokeOptions,
frameRendering: st.frameRendering,
objectsSnapModeEnabled: st.objectsSnapModeEnabled,
activeTool: st.activeTool,
activeTool,
},
prevTextMode: this.prevTextMode,
files,
@@ -3424,7 +3517,15 @@ export default class ExcalidrawView extends TextFileView {
}
private clearHoverPreview() {
if (this.hoverPreviewTarget) {
if (this.hoverPopover) {
this.hoverPreviewTarget = null;
//@ts-ignore
if(this.hoverPopover.embed?.editor) {
return;
}
//@ts-ignore
this.hoverPopover?.hide();
} else if (this.hoverPreviewTarget) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearHoverPreview, "ExcalidrawView.clearHoverPreview", this);
const event = new MouseEvent("click", {
view: this.ownerWindow,
@@ -3610,7 +3711,7 @@ export default class ExcalidrawView extends TextFileView {
this.app.workspace.trigger("hover-link", {
event: this.lastMouseEvent,
source: VIEW_TYPE_EXCALIDRAW,
hoverParent: this.hoverPreviewTarget,
hoverParent: this,
targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!!
linktext: this.plugin.hover.linkText,
sourcePath: this.plugin.hover.sourcePath,
@@ -3639,6 +3740,7 @@ export default class ExcalidrawView extends TextFileView {
private excalidrawDIVonKeyDown(event: KeyboardEvent) {
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.excalidrawDIVonKeyDown, "ExcalidrawView.excalidrawDIVonKeyDown", event);
if (this.semaphores?.viewunload) return;
if (event.target === this.excalidrawWrapperRef.current) {
return;
} //event should originate from the canvas
@@ -3657,6 +3759,9 @@ export default class ExcalidrawView extends TextFileView {
if (!this.plugin.settings.allowCtrlClick && !isWinMETAorMacCTRL(e)) {
return;
}
if (Boolean((this.excalidrawAPI as ExcalidrawImperativeAPI)?.getAppState().contextMenu)) {
return;
}
//added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9.
//Timeout is required for Excalidraw to first complete the selection action before execution
//of the link click continues
@@ -4729,6 +4834,7 @@ export default class ExcalidrawView extends TextFileView {
null,
{id: element.id, text: link},
event,
true,
);
return;
}
@@ -4938,7 +5044,7 @@ export default class ExcalidrawView extends TextFileView {
t("OPEN_LINK_CLICK"),
() => {
const event = emulateKeysForLinkClick("new-tab");
this.handleLinkClick(event);
this.handleLinkClick(event, true);
},
onClose
),
@@ -5072,7 +5178,8 @@ export default class ExcalidrawView extends TextFileView {
React,
t("COPY_DRAWING_LINK"),
() => {
navigator.clipboard.writeText(`![[${this.file.path}]]`);
const path = this.file.path.match(/(.*)(\.md)$/)?.[1];
navigator.clipboard.writeText(`![[${path ?? this.file.path}]]`);
},
onClose
),

View File

@@ -29,6 +29,7 @@ import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
import { isHTMLElement } from "./utils/typechecks";
interface imgElementAttributes {
file?: TFile;
@@ -374,7 +375,9 @@ const getIMG = async (
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);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
//const svgString = new XMLSerializer().serializeToString(svg);
const svgString = svg.outerHTML;
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
img.setAttribute("src", blobUrl);
@@ -403,12 +406,13 @@ const createImgElement = async (
let timer:number;
const clickEvent = (ev:PointerEvent) => {
if(!(ev.target instanceof Element)) {
if (!isHTMLElement(ev.target)) {
return;
}
const containerElement = ev.target.hasClass("excalidraw-embedded-img")
const targetElement = ev.target as HTMLElement;
const containerElement = targetElement.hasClass("excalidraw-embedded-img")
? ev.target
: getParentOfClass(ev.target, "excalidraw-embedded-img");
: getParentOfClass(targetElement, "excalidraw-embedded-img");
if (!containerElement) {
return;
}
@@ -587,6 +591,102 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
return await createImageDiv(attr);
}
function getDimensionsFromAliasString(data: string) {
const dimensionRegex = /^(?<width>\d+%|\d+)(x(?<height>\d+%|\d+))?$/;
const heightOnlyRegex = /^x(?<height>\d+%|\d+)$/;
const match = data.match(dimensionRegex) || data.match(heightOnlyRegex);
if (match) {
const { width, height } = match.groups;
// Ensure width and height do not start with '0'
if ((width && width.startsWith('0') && width !== '0') ||
(height && height.startsWith('0') && height !== '0')) {
return null;
}
return {
width: width || undefined,
height: height || undefined,
};
}
// If the input starts with a 0 or is a decimal, return null
if (/^0\d|^\d+\.\d+/.test(data)) {
return null;
}
return null;
}
type AliasParts = { alias?: string, width?: string, height?: string, style?: string };
function parseAlias(input: string):AliasParts {
const result:AliasParts = {};
const parts = input.split('|').map(part => part.trim());
switch (parts.length) {
case 1:
const singleMatch = getDimensionsFromAliasString(parts[0]);
if (singleMatch) {
return singleMatch; // Return dimensions if valid
}
result.style = parts[0]; // Otherwise, return as style
break;
case 2:
const firstDim = getDimensionsFromAliasString(parts[0]);
const secondDim = getDimensionsFromAliasString(parts[1]);
if (secondDim) {
result.alias = parts[0];
result.width = secondDim.width;
result.height = secondDim.height;
} else if (firstDim) {
result.width = firstDim.width;
result.height = firstDim.height;
result.style = parts[1]; // Second part is style
} else {
result.alias = parts[0];
result.style = parts[1]; // Assuming second part is style
}
break;
case 3:
const middleMatch = getDimensionsFromAliasString(parts[1]);
if (middleMatch) {
result.alias = parts[0];
result.width = middleMatch.width;
result.height = middleMatch.height;
result.style = parts[2];
} else {
result.alias = parts[0];
result.style = parts[2]; // Last part is style
}
break;
default:
const secondValue = getDimensionsFromAliasString(parts[1]);
if (secondValue) {
result.alias = parts[0];
result.width = secondValue.width;
result.height = secondValue.height;
result.style = parts[parts.length - 1]; // Last part is style
} else {
result.alias = parts[0];
result.style = parts[parts.length - 1]; // Last part is style
}
break;
}
// Clean up the result to remove undefined properties
Object.keys(result).forEach((key: keyof AliasParts) => {
if (result[key] === undefined) {
delete result[key];
}
});
return result;
}
const processAltText = (
fname: string,
alt:string,
@@ -594,19 +694,11 @@ const processAltText = (
) => {
(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*%?))?\|?(.*)/);
attr.fwidth = parts[2] ?? attr.fwidth;
attr.fheight = parts[3] ?? attr.fheight;
if (parts[4] && !parts[4].startsWith(fname)) {
attr.style = [`excalidraw-svg${`-${parts[4]}`}`];
}
if (
(!parts[4] || parts[4]==="") &&
(!parts[2] || parts[2]==="") &&
parts[0] && parts[0] !== ""
) {
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
const aliasParts = parseAlias(alt);
attr.fwidth = aliasParts.width ?? attr.fwidth;
attr.fheight = aliasParts.height ?? attr.fheight;
if (aliasParts.style && !aliasParts.style.startsWith(fname)) {
attr.style = [`excalidraw-svg${`-${aliasParts.style}`}`];
}
}
}
@@ -795,7 +887,9 @@ export const markdownPostProcessor = async (
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
const isFrontmatter = el.hasClass("mod-frontmatter") || el.firstElementChild?.hasClass("frontmatter");
const isFrontmatter = el.hasClass("mod-frontmatter") ||
el.firstElementChild?.hasClass("frontmatter") ||
el.firstElementChild?.hasClass("block-language-yaml");
if(isPrinting && isFrontmatter) {
return;
}

View File

@@ -103,6 +103,7 @@ export const {
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
loadSceneFonts,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";

View File

@@ -17,6 +17,57 @@ 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.5.2": `
## Fixed
- Text became disconnected from sticky notes (rectangle/ellipse/diamond + text) if the sticky note contained a link (e.g., URL or wiki link), and in some cases, triggered a save error warning. [#2054](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2054)
- Long-clicking to open an Excalidraw drawing from a markdown note did not work when the note was in an Obsidian pop-out window.
- Active tool was deactivated after autosave, requiring the user to reselect the tool.
## Minor changes to default settings
- I adjusted some of the default settings. This change only affects new installs of Excalidraw; existing installs and settings remain unchanged:
- **Reuse Adjacent Pane** is now the default for opening new drawings. Excalidraw will try to open the drawing in the most recently used adjacent pane, if available.
- **Focus on Existing Tab** is the default for reopening an already open drawing. Excalidraw will switch to the existing tab where the drawing is open, instead of creating a new one.
- **Autosave Interval** is now set to a default value of 1 minute on Desktop and 30 seconds on mobile platforms.
`,
"2.5.1": `
## New
- Excalidraw will now save images using the filename from the file system when adding an image via the image tool (in the top toolbar).
- Increased the maximum image size from a width/height of 1440 to 2880 when adding an image via the image tool in the top toolbar.
- Flip arrowheads: If you have an arrow bound to elements, select only the arrow (not the bound elements) and press SHIFT+H or SHIFT+V to swap the arrowheads. [#8525](https://github.com/excalidraw/excalidraw/pull/8525)
## Fixed
- Zoom
- "Zoom to Fit" did not work correctly when multiple Obsidian tabs were open, and Excalidraw was in a lower tab. Additionally, there was an offset when the left side panel was open, especially if the panel was relatively large compared to the canvas area. [#2039](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2039)
- SHIFT+1 and SHIFT+2 will now honor the max zoom setting in Plugin Settings.
- Adding images using the image tool in the toolbar was unreliable. Sometimes it worked, sometimes it didn't, depending on whether the drawing had unsaved changes. Autosave was causing the issue with the image tool. [#1992](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1992)
- Frame related issues
- Fixed an issue where links to the back of the note were broken if an unnamed frame was present in the scene. [#2027](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2027)
- Frame transclusion was not working when there was a LaTeX equation anywhere in the scene. [#2028](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2028)
- Frame settings and rounded image corners were not honored when exporting (and auto-exporting) SVGs. [#2026](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026)
- Resolved issues with the width, height, and style parsing of Excalidraw drawings embedded in Markdown notes. ${String.fromCharCode(96)}![[my file|10 - my alias]]${String.fromCharCode(96)} was incorrectly parsed as a width of 10 and a style of "- my alias."
- Links
- When navigating element links, selecting a #tag from the link-list did not open the Obsidian tag in the search.
- False-positive tag results in second-order links list.
- Arrow label links did not work as expected. Since CTRL/CMD+Click is used in Excalidraw to start the line editor, the solution is not straightforward from a UX perspective. [#2023](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2023)
- You can open arrow links by ctrl+clicking on the label itself. If the arrow or line element contains the link, ctrl+click on the link indicator in the top right.
- You can also right-click the linear element and select "Open Link" from the context menu.
- Various elbow-arrow fixes and QoL improvements from excalidraw.com [#8324](https://github.com/excalidraw/excalidraw/pull/8324), [#8448](https://github.com/excalidraw/excalidraw/pull/8448), [#8440](https://github.com/excalidraw/excalidraw/pull/8440)
`,
"2.5.0": `
The new [Community Wiki](https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/WIKI/Welcome+to+the+WIKI) is waiting for your contribution!
## Fixed
- Regression from 2.4.3: Text flickers when editing text in a container [#2015](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2015).
- Significantly improved the performance of group and frame [image fragments](https://youtu.be/sjZfdqpxqsg) when the source drawing includes many images, but the fragment does not.
- Minor styling tweaks. Note that with Obsidian 1.7.1, the font size and zoom settings in Obsidian > Appearance will affect the size of buttons and menu items in Excalidraw as well.
## New
- New Canvas Search from Excalidraw.com (CTRL/CMD+F). The "old" search is still available in the Obsidian Command Palette _"Search for text in drawing"_. The old search will also search in image-file names and frame titles, but the result set is not as sophisticated as the one built by Excalidraw.com. If you want to use the old search, you can set up a hotkey in Obsidian settings, e.g., CTRL+ALT/CMD+OPT+F. [#8438](https://github.com/excalidraw/excalidraw/pull/8438)
- Grid Color settings under **Excalidraw Appearance and Behavior**. Note that the grid color and opacity also affect the color and transparency of the binding box when using the arrow tool. [#2007](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2007)
- Refactoring the code to be compatible with the upcoming Obsidian 1.7.2.
- ${String.fromCharCode(96)}ExcalidrawAutomate.decompressFromBase64()${String.fromCharCode(96)} will now remove line breaks from the input string so you can directly supply the compressed JSON string for decompression by script.
`,
"2.4.3": `
Check out the [Excalidraw Plugin's Community WIKI](https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/WIKI/Welcome+to+the+WIKI) and help with your content contribution.

View File

@@ -720,7 +720,7 @@ export async function linkPrompt (
message: string = "Select link to open",
):Promise<[file:TFile, linkText:string, subpath: string]> {
const linksArray = REGEX_LINK.getResList(linkText);
const tagsArray = REGEX_TAGS.getResList(linkText);
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g,"$1 "));
let subpath: string = null;
let file: TFile = null;
let parts = linksArray[0] ?? tagsArray[0];

View File

@@ -128,7 +128,10 @@ export default {
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
LINK_BUTTON_CLICK_NO_TEXT:
"Select an ImageElement, or select a TextElement that contains an internal or external link.\n",
"Select an element that contains an internal or external link.\n",
LINEAR_ELEMENT_LINK_CLICK_ERROR:
"Arrow- and Line-Element links cannot be navigated by " + labelCTRL() + " + CLICKing on the element because that also activates the line editor.\n" +
"Use the right-click context menu to open the link, or click the link indicator in the top right corner of the element.\n",
FILENAME_INVALID_CHARS:
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
FORCE_SAVE:
@@ -184,7 +187,7 @@ export default {
BASIC_HEAD: "Basic",
BASIC_DESC: `In the "Basic" settings, you can configure options such as displaying release notes after updates, receiving plugin update notifications, setting the default location for new drawings, specifying the Excalidraw folder for embedding drawings into active documents, defining an Excalidraw template file, and designating an Excalidraw Automate script folder for managing automation scripts.`,
FOLDER_NAME: "Excalidraw folder",
FOLDER_NAME: "Excalidraw folder (CAsE sEnsITive!)",
FOLDER_DESC:
"Default location for new drawings. If empty, drawings will be created in the Vault root.",
CROP_PREFIX_NAME: "Crop file prefix",
@@ -198,10 +201,10 @@ export default {
ANNOTATE_PRESERVE_SIZE_NAME: "Preserve image size when annotating",
ANNOTATE_PRESERVE_SIZE_DESC:
"When annotating an image in markdown the replacment image link will include the width of the original image.",
CROP_FOLDER_NAME: "Crop file folder",
CROP_FOLDER_NAME: "Crop file folder (CaSE senSItive!)",
CROP_FOLDER_DESC:
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
ANNOTATE_FOLDER_NAME: "Image annotation file folder",
ANNOTATE_FOLDER_NAME: "Image annotation file folder (CaSe SeNSitIVe!)",
ANNOTATE_FOLDER_DESC:
"Default location for new drawings created when annotating an image. If empty, drawings will be created following the Vault attachments settings.",
FOLDER_EMBED_NAME:
@@ -210,7 +213,7 @@ export default {
"Define which folder to place the newly inserted drawing into " +
"when using the command palette action: 'Create a new drawing and embed into active document'.<br>" +
"<b><u>Toggle ON:</u></b> Use Excalidraw folder<br><b><u>Toggle OFF:</u></b> Use the attachments folder defined in Obsidian settings.",
TEMPLATE_NAME: "Excalidraw template file or folder",
TEMPLATE_NAME: "Excalidraw template file or folder (caSe SenSiTive!)",
TEMPLATE_DESC:
"Full filepath or folderpath to the Excalidraw template.<br>" +
"<b>Template File:</b>E.g.: If your template is in the default Excalidraw folder and its name is " +
@@ -226,6 +229,15 @@ export default {
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
"hotkeys to your favorite scripts just like to any other Obsidian command. " +
"The folder may not be the root folder of your Vault. ",
ASSETS_FOLDER_NAME: "Local Font Assets Folder (cAsE sENsiTIvE!)",
ASSETS_FOLDER_DESC: `Since version 2.5.3, following the implementation of CJK font support, Excalidraw downloads fonts from the internet.
If you prefer to keep Excalidraw fully local, allowing it to work without internet access, or if your internet connection is slow
and you want to improve performance, you can download the necessary
<a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip" target="_blank">font assets from GitHub</a>.
After downloading, unzip the contents into a folder within your Vault.<br>
You can specify the location of that folder here. For example, you may choose to place it under <code>Excalidraw/FontAssets</code>.<br><br>
<strong>Important:</strong> Do not set this to the Vault root! Ensure that no other files are placed in this folder.<br><br>
<strong>Note:</strong> If you're using Obsidian Sync and want to synchronize these font files across your devices, ensure that Obsidian Sync is set to synchronize "All other file types".`,
AI_HEAD: "AI Settings - Experimental",
AI_DESC: `In the "AI" settings, you can configure options for using OpenAI's GPT API. ` +
`While the OpenAI API is in beta, its use is strictly limited — as such we require you use your own API key. ` +
@@ -402,7 +414,7 @@ FILENAME_HEAD: "Filename",
GRID_COLOR_NAME: "Grid color",
GRID_OPACITY_NAME: "Grid opacity",
GRID_OPACITY_DESC: "Grid opacity will also control the opacity of the binding box when binding an arrow to an element.<br>" +
"Set the opacity of the grid. 0 is transparent, 1 is opaque.",
"Set the opacity of the grid. 0 is transparent, 100 is opaque.",
LASER_HEAD: "Laser pointer",
LASER_COLOR: "Laser pointer color",
LASER_DECAY_TIME_NAME: "Laser pointer decay time",
@@ -813,6 +825,36 @@ FILENAME_HEAD: "Filename",
//ExcalidrawData.ts
LOAD_FROM_BACKUP: "Excalidraw file was corrupted. Loading from backup file.",
FONT_LOAD_SLOW: "Loading Fonts...\n\n This is taking longer than expected. If this delay occurs regulary then you may download the fonts locally to your Vault. \n\n" +
"(click=dismiss, right-click=Info)",
FONT_INFO_TITLE: "Starting v2.5.3 fonts load from the Internet",
FONT_INFO_DETAILED: `
<p>
To improve Obsidian's startup time and manage the large <strong>CJK font family</strong>,
I've moved the fonts out of the plugin's <code>main.js</code>. Starting with version 2.5.3,
fonts will be loaded from the internet. This typically shouldn't cause issues as Obsidian caches
these files after first use.
</p>
<p>
If you prefer to keep Obsidian 100% local or experience performance issues, you can download the font assets.
</p>
<h3>Instructions:</h3>
<ol>
<li>Download the fonts from <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip">GitHub</a>.</li>
<li>Unzip and copy files into a Vault folder (default: <code>Excalidraw/FontAssets</code>; folder names are cAse-senSITive).</li>
<li><mark>DO NOT</mark> set this folder to the Vault root or mix with other local fonts.</li>
</ol>
<h3>For Obsidian Sync Users:</h3>
<p>
Ensure Obsidian Sync is set to synchronize "All other file types" or download and unzip the file on all devices.
</p>
<h3>Note:</h3>
<p>
If you find this process cumbersome, please submit a feature request to Obsidian.md for supporting assets in the plugin folder.
Currently, only a single <code>main.js</code> is supported, which leads to large files and slow startup times for complex plugins like Excalidraw.
I apologize for the inconvenience.
</p>
`,
//ObsidianMenu.tsx
GOTO_FULLSCREEN: "Goto fullscreen mode",

View File

@@ -128,7 +128,10 @@ export default {
OPEN_LINK: "打开所选元素里的链接 \n按住 Shift 在新面板打开)",
EXPORT_EXCALIDRAW: "导出为 .excalidraw 文件(旧版绘图文件格式)",
LINK_BUTTON_CLICK_NO_TEXT:
"请选择一个含有链接的图形或文本元素。",
"请选择一个包含内部或外部链接的元素。\n",
LINEAR_ELEMENT_LINK_CLICK_ERROR:
"箭头和线元素的链接无法通过 " + labelCTRL() + " + 点击元素来导航,因为这也会激活线编辑器。\n" +
"请使用右键上下文菜单打开链接,或点击元素右上角的链接指示器。\n",
FILENAME_INVALID_CHARS:
'文件名不能含有以下符号: * " \\ < > : | ? #',
FORCE_SAVE:
@@ -395,7 +398,15 @@ FILENAME_HEAD: "文件名",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.550%)且不能超过 101000%)。",
LASER_HEAD: "激光笔工具More Tools > Laser pointer",
GRID_HEAD: "网格",
GRID_DYNAMIC_COLOR_NAME: "动态网格颜色",
GRID_DYNAMIC_COLOR_DESC:
"<b><u>开启:</u></b>更改网格颜色以匹配画布颜色<br><b><u>关闭:</u></b>将以下颜色用作网格颜色",
GRID_COLOR_NAME: "网格颜色",
GRID_OPACITY_NAME: "网格透明度",
GRID_OPACITY_DESC: "网格透明度还将控制将箭头绑定到元素时绑定框的透明度。<br>"+
"设置网格的不透明度。 0 表示完全透明100 表示完全不透明。",
LASER_HEAD: "激光笔工具(更多工具 > 激光笔)",
LASER_COLOR: "激光笔颜色",
LASER_DECAY_TIME_NAME: "激光笔消失时间",
LASER_DECAY_TIME_DESC: "单位是毫秒,默认是 1000即 1 秒)。",

View File

@@ -313,6 +313,15 @@ export default class ExcalidrawPlugin extends Plugin {
};
}*/
public async loadFontFromFile(fontName: string): Promise<ArrayBuffer> {
const assetsFoler = "Fonts/";
const file = this.app.vault.getAbstractFileByPath(assetsFoler + fontName);
if(!file || !(file instanceof TFile)) {
return;
}
return await this.app.vault.readBinary(file);
}
async onload() {
initCompressionWorker();
this.loadTimestamp = Date.now();

View File

@@ -262,7 +262,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
shiftKey: e.shiftKey,
altKey: e.altKey,
});
this.view.handleLinkClick(event);
this.view.handleLinkClick(event, true);
}
actionOpenLinkProperties() {

View File

@@ -49,6 +49,7 @@ export interface ExcalidrawSettings {
embedUseExcalidrawFolder: boolean;
templateFilePath: string;
scriptFolderPath: string;
fontAssetsPath: string;
compress: boolean;
decompressForMDView: boolean;
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
@@ -220,13 +221,14 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
embedUseExcalidrawFolder: false,
templateFilePath: "Excalidraw/Template.excalidraw",
scriptFolderPath: "Excalidraw/Scripts",
fontAssetsPath: "Excalidraw/FontAssets",
compress: true,
decompressForMDView: false,
onceOffCompressFlagReset: false,
onceOffGPTVersionReset: false,
autosave: true,
autosaveIntervalDesktop: 30000,
autosaveIntervalMobile: 20000,
autosaveIntervalDesktop: 60000,
autosaveIntervalMobile: 30000,
drawingFilenamePrefix: "Drawing ",
drawingEmbedPrefixWithFilename: true,
drawingFilnameEmbedPostfix: " ",
@@ -268,9 +270,9 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
done: "🗹",
hoverPreviewWithoutCTRL: false,
linkOpacity: 1,
openInAdjacentPane: false,
openInAdjacentPane: true,
showSecondOrderLinks: true,
focusOnFileTab: false,
focusOnFileTab: true,
openInMainWorkspace: true,
showLinkBrackets: true,
allowCtrlClick: true,
@@ -720,6 +722,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("ASSETS_FOLDER_NAME"))
.setDesc(fragWithHTML(t("ASSETS_FOLDER_DESC")))
.addText((text) =>
text
.setPlaceholder("e.g.: Excalidraw/FontAssets")
.setValue(this.plugin.settings.fontAssetsPath)
.onChange(async (value) => {
this.plugin.settings.fontAssetsPath = value;
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Saving
@@ -2484,7 +2498,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
d.addOption("Virgil", "Virgil");
this.app.vault
.getFiles()
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension) && !f.path.startsWith(this.plugin.settings.fontAssetsPath))
.forEach((f: TFile) => {
d.addOption(f.path, f.name);
});

View File

@@ -139,7 +139,9 @@ export class CropImage {
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"];
const svg = await this.buildSVG();
return new Promise((resolve, reject) => {
const svgData = new XMLSerializer().serializeToString(svg);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgData = svg.outerHTML;
//const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

View File

@@ -83,8 +83,9 @@ export const setDynamicStyle = (
[`--color-on-primary-container`]: str(!isDark?accent().darkerBy(15):accent().lighterBy(15)),
[`--color-surface-primary-container`]: str(isDark?accent().darkerBy(step):accent().lighterBy(step)),
[`--bold-color`]: str(!isDark?accent().darkerBy(15):accent().lighterBy(15)),
//[`--color-primary-darker`]: str(accent().darkerBy(step)),
//[`--color-primary-darkest`]: str(accent().darkerBy(step)),
[`--color-primary-darker`]: str(accent().darkerBy(step)),
[`--color-primary-darkest`]: str(accent().darkerBy(2*step)),
['--button-bg-color']: str(gray1()),
[`--button-gray-1`]: str(gray1()),
[`--button-gray-2`]: str(gray2()),
[`--input-border-color`]: str(gray1()),
@@ -97,12 +98,11 @@ export const setDynamicStyle = (
[`--overlay-bg-color`]: gray2().alphaTo(0.6).stringHEX(),
[`--popup-bg-color`]: str(gray1()),
[`--color-on-surface`]: str(text),
[`--default-border-color`]: str(text),
[`--default-border-color`]: str(gray1()),
//[`--color-gray-100`]: str(text),
[`--color-gray-40`]: str(text), //frame
[`--color-gray-50`]: str(text), //frame
[`--color-surface-highlight`]: str(gray1()),
//[`--color-gray-30`]: str(gray1),
[`--color-gray-20`]: str(gray1()),
[`--sidebar-border-color`]: str(gray1()),
[`--color-primary-light`]: str(accent().lighterBy(step)),
[`--button-hover-bg`]: str(gray1()),
@@ -118,9 +118,11 @@ export const setDynamicStyle = (
[`color`]: str(text),
['--excalidraw-caret-color']: str(isLightTheme ? text : cmBG()),
[`--select-highlight-color`]: str(gray1()),
[`--color-gray-80`]: str(isDark?text.darkerBy(40):text.lighterBy(40)), //frame
[`--color-gray-90`]: str(isDark?text.darkerBy(5):text.lighterBy(5)), //search background
[`--default-bg-color`]: str(text), //search background,
[`--color-gray-80`]: str(isDark?text.darkerBy(10):text.lighterBy(10)), //frame
[`--color-gray-70`]: str(isDark?text.darkerBy(10):text.lighterBy(10)), //frame
[`--default-bg-color`]: str(isDark?text.darkerBy(20):text.lighterBy(20)), //search background,
[`--color-gray-50`]: str(text), //frame
};
const styleString = Object.keys(styleObject)

View File

@@ -23,6 +23,9 @@ export function updateElementIdsInScene(
boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
if(boundEl.type === "text") {
boundEl.containerId = newID;
}
if(boundEl.type === "arrow") {
const arrow = boundEl as Mutable<ExcalidrawArrowElement>;
if(arrow.startBinding?.elementId === elementToChange.id) {

View File

@@ -1,6 +1,6 @@
import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
import { App, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { App, Modal, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData";
import ExcalidrawView from "src/ExcalidrawView";
@@ -81,20 +81,18 @@ export function openTagSearch(link: string, app: App, view?: ExcalidrawView) {
return;
}
const search = app.workspace.getLeavesOfType("search");
if (search.length === 0) {
return;
const query = `tag:${tags[0].value[1]}`;
const searchPlugin = app.internalPlugins.getPluginById("global-search");
if (searchPlugin) {
const searchInstance = searchPlugin.instance;
if (searchInstance) {
searchInstance.openGlobalSearch(query);
}
}
//@ts-ignore
search[0].view.setQuery(`tag:${tags[0].value[1]}`);
app.workspace.revealLeaf(search[0]);
if (view && view.isFullscreen()) {
view.exitFullscreen();
}
return;
}
function getLinkFromMarkdownLink(link: string): string {
@@ -123,14 +121,21 @@ export function openExternalLink (link:string, app: App, element?: ExcalidrawEle
* @param link
* @param app
* @param returnWikiLink
* @param openLink: if set to false, the link will not be opened just true will be returned for an obsidian link.
* @returns
* false if the link is not an obsidian link,
* true if the link is an obsidian link and it was opened (i.e. it is a link to another Vault or not a file link e.g. plugin link), or
* the link to the file path. By default as a wiki link, or as a file path if returnWikiLink is false.
*/
export function parseObsidianLink(link: string, app: App, returnWikiLink: boolean = true): boolean | string {
export function parseObsidianLink(
link: string,
app: App,
returnWikiLink: boolean = true,
openLink: boolean = true,
): boolean | string {
if(!link) return false;
link = getLinkFromMarkdownLink(link);
if (!link.startsWith("obsidian://")) {
if (!link?.startsWith("obsidian://")) {
return false;
}
const url = new URL(link);
@@ -153,7 +158,9 @@ export function parseObsidianLink(link: string, app: App, returnWikiLink: boolea
}
}
window.open(link, "_blank");
if(openLink) {
window.open(link, "_blank");
}
return true;
}
@@ -394,4 +401,20 @@ export function isTextImageTransclusion (
}
}
return false;
}
export function displayFontMessage(app: App) {
const modal = new Modal(app);
modal.onOpen = () => {
const contentEl = modal.contentEl;
contentEl.createEl("h2", { text: t("FONT_INFO_TITLE") });
const releaseNotesHTML = t("FONT_INFO_DETAILED");
const div = contentEl.createDiv({ cls: "release-notes" });
div.innerHTML = releaseNotesHTML;
}
modal.open();
}

View File

@@ -2,6 +2,7 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import ExcalidrawView, { TextMode } from "src/ExcalidrawView";
import { rotatedDimensions } from "./Utils";
import { getBoundTextElementId } from "src/ExcalidrawAutomate";
export const getElementsAtPointer = (
pointer: any,
@@ -93,13 +94,24 @@ const api = view.excalidrawAPI;
if (!api) {
return;
}
const elements = (
let elements = (
getElementsAtPointer(
pointer,
api.getSceneElements(),
) as ExcalidrawImageElement[]
) as ExcalidrawElement[]
).filter((el) => el.link);
//as a fallback let's check if any of the elements at pointer are containers with a text element that has a link.
if (elements.length === 0) {
const textElIDs = (
getElementsAtPointer(
pointer,
api.getSceneElements(),
) as ExcalidrawImageElement[]
).map((el) => getBoundTextElementId(el));
elements = view.getViewElements().filter((el) => el.type==="text" && el.link && textElIDs.includes(el.id));
}
if (elements.length === 0) {
return { id: null, text: null };
}

View File

@@ -26,7 +26,7 @@ export const getParentOfClass = (element: Element, cssClass: string):HTMLElement
};
export function getExcalidrawViews(app: App): ExcalidrawView[] {
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).filter(l=>l instanceof ExcalidrawView);
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).filter(l=>l.view instanceof ExcalidrawView);
return leaves.map(l=>l.view as ExcalidrawView);
}

18
src/utils/typechecks.ts Normal file
View File

@@ -0,0 +1,18 @@
/**
* Checks if a given target is an HTMLElement.
*
* This function is necessary because `instanceof HTMLElement` can fail
* in environments with multiple execution contexts (e.g., popout windows),
* where `HTMLElement` comes from different global objects.
* Instead, we use feature detection by checking for key properties
* common to all HTML elements (nodeType and tagName).
*
* @param target - The target to check.
* @returns True if the target is an HTMLElement, false otherwise.
*/
export function isHTMLElement (target: any): target is HTMLElement {
return target &&
typeof target === 'object' &&
target.nodeType === 1 && // nodeType 1 means it's an element
typeof target.tagName === 'string'; // tagName exists on HTML elements
}

View File

@@ -638,4 +638,16 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.ExcTextField__input input::placeholder {
color: var(--select-highlight-color);
}
}
.excalidraw textarea::placeholder {
color: var(--color-gray-50);
}
.excalidraw textarea.ttd-dialog-input {
caret-color: var(--excalidraw-caret-color);
}
.excalidraw .ToolIcon_type_button {
color: var(--text-primary-color);
}