mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
43 Commits
ImageEleme
...
1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3cf60eab | ||
|
|
32fdbf9dc2 | ||
|
|
1aed684ebe | ||
|
|
1ad791d9bc | ||
|
|
bb9925024d | ||
|
|
e676255d69 | ||
|
|
691c60be24 | ||
|
|
f4a458061a | ||
|
|
c88c898f4a | ||
|
|
d2da408a59 | ||
|
|
3b9a6404c5 | ||
|
|
d9306922c3 | ||
|
|
578cc7a99c | ||
|
|
aa9f9ba91f | ||
|
|
b9251d4f1d | ||
|
|
13a980afed | ||
|
|
c911e0118f | ||
|
|
eca02a5941 | ||
|
|
9a57db43f2 | ||
|
|
f6b65ac3e9 | ||
|
|
929348b390 | ||
|
|
57f1b9f8da | ||
|
|
fed106c811 | ||
|
|
739e919a43 | ||
|
|
e85cf4e196 | ||
|
|
0c42353fce | ||
|
|
7ebdec7713 | ||
|
|
1917dad8cd | ||
|
|
3100e2d70f | ||
|
|
7712cd49b6 | ||
|
|
856573763e | ||
|
|
3bbff7f8d5 | ||
|
|
034927ada0 | ||
|
|
0cccdad13f | ||
|
|
fe7f3f58c5 | ||
|
|
48fd854944 | ||
|
|
8f9746393f | ||
|
|
23da271b73 | ||
|
|
627775c6c3 | ||
|
|
59db43c3f0 | ||
|
|
597ee4f70e | ||
|
|
8222d8c146 | ||
|
|
f785d756be |
47
README.md
47
README.md
@@ -2,33 +2,17 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
# Important notice to the 1.2.0 update
|
|
||||||
|
|
||||||
This version comes with tons of new features and possibilities.
|
|
||||||
|
|
||||||
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
|
|
||||||
|
|
||||||
## Conversion and compatibility
|
|
||||||
To convert files you have the following options:
|
|
||||||
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
|
|
||||||
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
|
|
||||||
- To convert files individually:
|
|
||||||
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
|
|
||||||
- `*.excalidraw => *.excalidraw.md`
|
|
||||||
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
|
|
||||||
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
|
|
||||||
|
|
||||||
# Video walkthrough
|
# Video walkthrough
|
||||||
| | | |
|
| | | |
|
||||||
|----|----|----|
|
|----|----|----|
|
||||||
|[](https://youtu.be/UxJLLYtgDKE)|[](https://youtu.be/sY4FoflGaiM)|[](https://youtu.be/Iy_oVTq12Gw)|
|
|[](https://youtu.be/UxJLLYtgDKE)|[](https://youtu.be/sY4FoflGaiM)|[](https://youtu.be/Iy_oVTq12Gw)|
|
||||||
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
||||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)||
|
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)|[](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|
||||||
|
|
||||||
# Key features
|
# Key features
|
||||||
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
||||||
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
|
- CTRL/CMD+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
|
||||||
- Settings will allow you to customzie Excalidraw to your needs:
|
- Settings will allow you to customzie Excalidraw to your needs:
|
||||||
- Default folder for new drawings and define custom filename pattern for new drawings.
|
- Default folder for new drawings and define custom filename pattern for new drawings.
|
||||||
- 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.
|
||||||
@@ -46,13 +30,23 @@ To convert files you have the following options:
|
|||||||
- `![[myfile#section]]` also works, this will transclude the section
|
- `![[myfile#section]]` also works, this will transclude the section
|
||||||
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
||||||
- For convenience you can also use the command palette to insert links into drawings
|
- For convenience you can also use the command palette to insert links into drawings
|
||||||
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
|
- CTRL/CMD + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
|
||||||
- CTRL/META + CLICK a text element to open it as a link.
|
- CTRL/CMD + CLICK a text element to open it as a link.
|
||||||
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
|
- CTRL/CMD + ALT + CLICK to create the file (if it does not yet exist) and open it
|
||||||
- CTRL/META + SHIFT + CLICK to open the file in a new pane
|
- CTRL/CMD + SHIFT + CLICK to open the file in a new pane
|
||||||
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
|
- CTRL/CMD + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
|
||||||
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
|
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
|
||||||
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
|
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
|
||||||
|
- Drag & Drop support
|
||||||
|
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw.
|
||||||
|
- Dragging image files (PNG, SVG, JPG, Excalidraw) from obsidian files explorer while pressing the CTRL/CMD button will embed the image into your drawing.
|
||||||
|
- You can drag and drop images from outside obsidian onto Excalidraw. These images will be embedded into your drawing and saved to Obsidian.
|
||||||
|
- You can drag and drop text from Markdown views onto Excalidraw.
|
||||||
|
- You can drag and drop web addresses from your browser and they will become links.
|
||||||
|
- Image support
|
||||||
|
- On iOS and Android you can add images from your camera by pressing the add image button in Excalidraw.
|
||||||
|
- You can copy/paste images into your drawing. Images will be saved in your vault.
|
||||||
|
- You can drag and drop images as explained above.
|
||||||
- Since 1.2.0 Drawing files are stored in Markdown files
|
- Since 1.2.0 Drawing files are stored in Markdown files
|
||||||
- You can add tags to drawings
|
- You can add tags to drawings
|
||||||
- You can add metadata to the YAML front matter of drawings
|
- You can add metadata to the YAML front matter of drawings
|
||||||
@@ -62,18 +56,17 @@ To convert files you have the following options:
|
|||||||
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
||||||
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
||||||
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
||||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
- Includes full [QuickAdd](https://github.com/chhoumann/quickadd), [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/). I also have a [YouTube ExcalidrawAutomate Playlist](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ) with lots of examples.
|
||||||
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
|
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
|
||||||
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
||||||
|
|
||||||
# Known issues
|
# Known issues
|
||||||
- Mobile support
|
- Mobile support
|
||||||
- Positioning of the pen gets misaligned after you open the command palette.
|
|
||||||
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
|
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
|
||||||
|
- Text elements "jumps off screen" when editing, if drawing is zoomed in and text element does not fit the visible screen area. I am working on a resolution.
|
||||||
|
|
||||||
# Tips and tricks
|
# Tips and tricks
|
||||||
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
|
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode.
|
||||||
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
|
|
||||||
|
|
||||||
# Feedback, questions, ideas, problems
|
# Feedback, questions, ideas, problems
|
||||||
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)
|
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)
|
||||||
|
|||||||
14
TODO.md
Normal file
14
TODO.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw
|
||||||
|
[x] add ```html <SVG>...</SVG> ``` codeblock to excalidraw markdown
|
||||||
|
[x] read pre-saved `<SVG>` when generating image preview
|
||||||
|
[x] update code to adopt change files moving from AppState to App
|
||||||
|
- Add "files" to legacy excalidraw export
|
||||||
|
|
||||||
|
[x] PNG preview
|
||||||
|
[x] markdown embed SVG 190
|
||||||
|
[x] markdown embed PNG
|
||||||
|
[x] embed Excalidraw into other Excalidraw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-excalidraw-plugin",
|
"id": "obsidian-excalidraw-plugin",
|
||||||
"name": "Excalidraw",
|
"name": "Excalidraw",
|
||||||
"version": "1.3.16",
|
"version": "1.4.0",
|
||||||
"minAppVersion": "0.12.0",
|
"minAppVersion": "0.12.0",
|
||||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||||
"author": "Zsolt Viczian",
|
"author": "Zsolt Viczian",
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-excalidraw-plugin",
|
"name": "obsidian-excalidraw-plugin",
|
||||||
"version": "1.1.10",
|
"version": "1.3.21",
|
||||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zsviczian/excalidraw": "0.9.0-obsidian-image-support-3",
|
"@zsviczian/excalidraw": "0.10.0-obsidian-3",
|
||||||
"monkey-around": "^2.2.0",
|
"monkey-around": "^2.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@@ -19,22 +19,24 @@
|
|||||||
"roughjs": "4.4.1"
|
"roughjs": "4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.6",
|
"@babel/core": "^7.15.5",
|
||||||
"@babel/preset-env": "^7.3.1",
|
"@babel/preset-env": "^7.15.6",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@rollup/plugin-commonjs": "^15.1.0",
|
"@rollup/plugin-commonjs": "^21.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.5",
|
||||||
|
"@types/js-beautify": "^1.13.3",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react-dom": "^17.0.9",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"js-beautify": "1.13.3",
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
"obsidian": "^0.12.16",
|
"obsidian": "^0.12.16",
|
||||||
"rollup": "^2.52.3",
|
"rollup": "^2.52.3",
|
||||||
"rollup-plugin-visualizer": "^5.5.0",
|
"rollup-plugin-visualizer": "^5.5.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
normalizePath,
|
normalizePath,
|
||||||
TFile
|
TFile
|
||||||
} from "obsidian"
|
} from "obsidian"
|
||||||
import ExcalidrawView from "./ExcalidrawView";
|
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
|
||||||
import { getJSON } from "./ExcalidrawData";
|
import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
nanoid,
|
nanoid,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
VIEW_TYPE_EXCALIDRAW,
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
MAX_IMAGE_SIZE
|
MAX_IMAGE_SIZE
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { getObsidianImage, wrapText } from "./Utils";
|
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
|
||||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||||
@@ -73,7 +73,7 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
):Promise<string>;
|
):Promise<string>;
|
||||||
createSVG (templatePath?:string):Promise<SVGSVGElement>;
|
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
|
||||||
createPNG (templatePath?:string):Promise<any>;
|
createPNG (templatePath?:string):Promise<any>;
|
||||||
wrapText (text:string, lineLen:number):string;
|
wrapText (text:string, lineLen:number):string;
|
||||||
addRect (topX:number, topY:number, width:number, height:number):string;
|
addRect (topX:number, topY:number, width:number, height:number):string;
|
||||||
@@ -283,7 +283,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
):Promise<string> {
|
):Promise<string> {
|
||||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
elements = elements.concat(this.getElements());
|
elements = elements.concat(this.getElements());
|
||||||
let frontmatter:string;
|
let frontmatter:string;
|
||||||
@@ -301,73 +301,83 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
} else {
|
} else {
|
||||||
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scene = {
|
||||||
|
type: "excalidraw",
|
||||||
|
version: 2,
|
||||||
|
source: "https://excalidraw.com",
|
||||||
|
elements: elements,
|
||||||
|
appState: {
|
||||||
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
|
currentItemStrokeColor: template?.appState?.currentItemStrokeColor ?? this.style.strokeColor,
|
||||||
|
currentItemBackgroundColor: template?.appState?.currentItemBackgroundColor ?? this.style.backgroundColor,
|
||||||
|
currentItemFillStyle: template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
|
||||||
|
currentItemStrokeWidth: template?.appState?.currentItemStrokeWidth ?? this.style.strokeWidth,
|
||||||
|
currentItemStrokeStyle: template?.appState?.currentItemStrokeStyle ?? this.style.strokeStyle,
|
||||||
|
currentItemRoughness: template?.appState?.currentItemRoughness ?? this.style.roughness,
|
||||||
|
currentItemOpacity: template?.appState?.currentItemOpacity ?? this.style.opacity,
|
||||||
|
currentItemFontFamily: template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
|
||||||
|
currentItemFontSize: template?.appState?.currentItemFontSize ?? this.style.fontSize,
|
||||||
|
currentItemTextAlign: template?.appState?.currentItemTextAlign ?? this.style.textAlign,
|
||||||
|
currentItemStrokeSharpness: template?.appState?.currentItemStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
currentItemStartArrowhead: template?.appState?.currentItemStartArrowhead ?? this.style.startArrowHead,
|
||||||
|
currentItemEndArrowhead: template?.appState?.currentItemEndArrowhead ?? this.style.endArrowHead,
|
||||||
|
currentItemLinearStrokeSharpness: template?.appState?.currentItemLinearStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||||
|
},
|
||||||
|
files: template?.files ?? {},
|
||||||
|
};
|
||||||
|
|
||||||
return plugin.createDrawing(
|
return plugin.createDrawing(
|
||||||
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
||||||
params?.onNewPane ? params.onNewPane : false,
|
params?.onNewPane ? params.onNewPane : false,
|
||||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||||
frontmatter + plugin.exportSceneToMD(
|
this.plugin.settings.compatibilityMode
|
||||||
JSON.stringify({
|
? JSON.stringify(scene,null,"\t")
|
||||||
|
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
|
||||||
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
|
let elements = template ? template.elements : [];
|
||||||
|
elements = elements.concat(automateElements);
|
||||||
|
const svg = await getSVG(
|
||||||
|
{//createDrawing
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
source: "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
elements: elements,
|
elements: elements,
|
||||||
appState: {
|
appState: {
|
||||||
theme: template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
|
},
|
||||||
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
|
files: template?.files ?? {}
|
||||||
currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle,
|
},
|
||||||
currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth,
|
|
||||||
currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle,
|
|
||||||
currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness,
|
|
||||||
currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity,
|
|
||||||
currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily,
|
|
||||||
currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize,
|
|
||||||
currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign,
|
|
||||||
currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness,
|
|
||||||
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
|
|
||||||
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
|
|
||||||
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
|
|
||||||
gridSize: template ? template.appState.gridSize : this.canvas.gridSize
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
|
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
|
||||||
let elements = template ? template.elements : [];
|
|
||||||
elements = elements.concat(this.getElements());
|
|
||||||
return await ExcalidrawView.getSVG(
|
|
||||||
{//createDrawing
|
|
||||||
"type": "excalidraw",
|
|
||||||
"version": 2,
|
|
||||||
"source": "https://excalidraw.com",
|
|
||||||
"elements": elements,
|
|
||||||
"appState": {
|
|
||||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
|
||||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
|
||||||
}
|
|
||||||
},//),
|
|
||||||
{
|
{
|
||||||
withBackground: plugin.settings.exportWithBackground,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
withTheme: plugin.settings.exportWithTheme
|
withTheme: plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return embedFont ? embedFontsInSVG(svg) : svg;
|
||||||
},
|
},
|
||||||
async createPNG(templatePath?:string, scale:number=1) {
|
async createPNG(templatePath?:string, scale:number=1) {
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
elements = elements.concat(this.getElements());
|
elements = elements.concat(automateElements);
|
||||||
return ExcalidrawView.getPNG(
|
return getPNG(
|
||||||
{
|
{
|
||||||
"type": "excalidraw",
|
type: "excalidraw",
|
||||||
"version": 2,
|
version: 2,
|
||||||
"source": "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
"elements": elements,
|
elements: elements,
|
||||||
"appState": {
|
appState: {
|
||||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
}
|
},
|
||||||
|
files: template?.files ?? {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
withBackground: plugin.settings.exportWithBackground,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
@@ -521,10 +531,12 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const image = await getObsidianImage(this.plugin.app,imageFile);
|
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||||
if(!image) return null;
|
if(!image) return null;
|
||||||
this.imagesDict[image.imageId] = {
|
this.imagesDict[image.fileId] = {
|
||||||
type:"image",
|
mimeType: image.mimeType,
|
||||||
id: image.imageId,
|
id: image.fileId,
|
||||||
dataURL: image.dataURL
|
dataURL: image.dataURL,
|
||||||
|
created: image.created,
|
||||||
|
file: imageFile.path
|
||||||
}
|
}
|
||||||
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||||
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||||
@@ -532,7 +544,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
image.size.height = scale*image.size.height;
|
image.size.height = scale*image.size.height;
|
||||||
}
|
}
|
||||||
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||||
this.elementsDict[id].imageId = image.imageId;
|
this.elementsDict[id].fileId = image.fileId;
|
||||||
this.elementsDict[id].scale = [1,1];
|
this.elementsDict[id].scale = [1,1];
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
@@ -621,7 +633,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
errorMessage("targetView not set", "getExcalidrawAPI()");
|
errorMessage("targetView not set", "getExcalidrawAPI()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (this.targetView as ExcalidrawView).excalidrawRef.current;
|
return (this.targetView as ExcalidrawView).excalidrawAPI;
|
||||||
},
|
},
|
||||||
getViewElements ():ExcalidrawElement[] {
|
getViewElements ():ExcalidrawElement[] {
|
||||||
if (!this.targetView || !this.targetView?._loaded) {
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
@@ -804,27 +816,61 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
|
|||||||
return {w: width, h: height, baseline: baseline };
|
return {w: width, h: height, baseline: baseline };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any, frontmatter: string}> {
|
async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{
|
||||||
|
elements: any,
|
||||||
|
appState: any,
|
||||||
|
frontmatter: string,
|
||||||
|
files: any,
|
||||||
|
svgSnapshot: string
|
||||||
|
}> {
|
||||||
const app = window.ExcalidrawAutomate.plugin.app;
|
const app = window.ExcalidrawAutomate.plugin.app;
|
||||||
const vault = app.vault;
|
const vault = app.vault;
|
||||||
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
||||||
if(file && file instanceof TFile) {
|
if(file && file instanceof TFile) {
|
||||||
const data = await vault.read(file);
|
const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||||
|
let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin);
|
||||||
|
|
||||||
|
if(file.extension === "excalidraw") {
|
||||||
|
await excalidrawData.loadLegacyData(data,file);
|
||||||
|
return {
|
||||||
|
elements: excalidrawData.scene.elements,
|
||||||
|
appState: excalidrawData.scene.appState,
|
||||||
|
frontmatter: "",
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||||
|
await excalidrawData.loadData(data,file,parsed ? TextMode.parsed : TextMode.raw)
|
||||||
|
|
||||||
let trimLocation = data.search("# Text Elements\n");
|
let trimLocation = data.search("# Text Elements\n");
|
||||||
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
||||||
|
|
||||||
const excalidrawData = JSON_parse(getJSON(data)[0]);
|
if(loadFiles) {
|
||||||
|
await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{
|
||||||
|
for(const f of fileArray) {
|
||||||
|
excalidrawData.scene.files[f.id] = f;
|
||||||
|
}
|
||||||
|
let foo;
|
||||||
|
[foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.scene.elements,
|
||||||
appState: excalidrawData.appState,
|
appState: excalidrawData.scene.appState,
|
||||||
frontmatter: data.substring(0,trimLocation)
|
frontmatter: data.substring(0,trimLocation),
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: excalidrawData.svgSnapshot
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
elements: [],
|
elements: [],
|
||||||
appState: {},
|
appState: {},
|
||||||
frontmatter: null
|
frontmatter: null,
|
||||||
|
files: [],
|
||||||
|
svgSnapshot: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import {
|
|||||||
JSON_parse
|
JSON_parse
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { TextMode } from "./ExcalidrawView";
|
import { TextMode } from "./ExcalidrawView";
|
||||||
import { wrapText } from "./Utils";
|
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, isObsidianThemeDark, wrapText } from "./Utils";
|
||||||
|
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
|
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
|
||||||
|
|
||||||
declare module "obsidian" {
|
declare module "obsidian" {
|
||||||
interface MetadataCache {
|
interface MetadataCache {
|
||||||
@@ -25,7 +28,10 @@ declare module "obsidian" {
|
|||||||
export const REGEX_LINK = {
|
export const REGEX_LINK = {
|
||||||
//![[link|alias]] [alias](link){num}
|
//![[link|alias]] [alias](link){num}
|
||||||
// 1 2 3 4 5 6 7 8 9
|
// 1 2 3 4 5 6 7 8 9
|
||||||
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
|
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||||
|
getRes: (text:string):IterableIterator<RegExpMatchArray> => {
|
||||||
|
return text.matchAll(REGEX_LINK.EXPR);
|
||||||
|
},
|
||||||
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||||
return parts.value[1] ? true:false;
|
return parts.value[1] ? true:false;
|
||||||
},
|
},
|
||||||
@@ -48,7 +54,7 @@ export const REGEX_LINK = {
|
|||||||
|
|
||||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||||
|
|
||||||
const DRAWING_REG = /\n%%\n# Drawing\n(```json\n)(.*)\n```%%/gm;
|
const DRAWING_REG = /\n%%\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||||
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||||
export function getJSON(data:string):[string,number] {
|
export function getJSON(data:string):[string,number] {
|
||||||
let res = data.matchAll(DRAWING_REG);
|
let res = data.matchAll(DRAWING_REG);
|
||||||
@@ -64,10 +70,24 @@ export function getJSON(data:string):[string,number] {
|
|||||||
const result = parts.value[2];
|
const result = parts.value[2];
|
||||||
return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||||
}
|
}
|
||||||
return [data,parts.value.index];
|
return [data,parts.value ? parts.value.index : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
//extracts SVG snapshot from Excalidraw Markdown string
|
||||||
|
const SVG_REG = /.*?```html\n([\s\S]*?)```/gm;
|
||||||
|
export function getSVGString(data:string):string {
|
||||||
|
let res = data.matchAll(SVG_REG);
|
||||||
|
|
||||||
|
let parts;
|
||||||
|
parts = res.next();
|
||||||
|
if(parts.value && parts.value.length>1) {
|
||||||
|
return parts.value[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExcalidrawData {
|
export class ExcalidrawData {
|
||||||
|
public svgSnapshot: string = null;
|
||||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||||
public scene:any = null;
|
public scene:any = null;
|
||||||
private file:TFile = null;
|
private file:TFile = null;
|
||||||
@@ -78,10 +98,13 @@ export class ExcalidrawData {
|
|||||||
private textMode: TextMode = TextMode.raw;
|
private textMode: TextMode = TextMode.raw;
|
||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
public loaded: boolean = false;
|
public loaded: boolean = false;
|
||||||
|
public files:Map<FileId,string> = null; //fileId, path
|
||||||
|
private compatibilityMode:boolean = false;
|
||||||
|
|
||||||
constructor(plugin: ExcalidrawPlugin) {
|
constructor(plugin: ExcalidrawPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.app = plugin.app;
|
this.app = plugin.app;
|
||||||
|
this.files = new Map<FileId,string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +116,8 @@ export class ExcalidrawData {
|
|||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
|
this.files.clear();
|
||||||
|
this.compatibilityMode = false;
|
||||||
|
|
||||||
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
||||||
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||||
@@ -123,6 +148,17 @@ export class ExcalidrawData {
|
|||||||
if (!this.scene) {
|
if (!this.scene) {
|
||||||
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.plugin.settings.matchThemeAlways) {
|
||||||
|
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
|
||||||
|
|
||||||
data = data.substring(0,pos);
|
data = data.substring(0,pos);
|
||||||
|
|
||||||
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
||||||
@@ -136,10 +172,13 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
||||||
|
|
||||||
|
data = data.substring(position);
|
||||||
|
position = 0;
|
||||||
|
|
||||||
//iterating through all the text elements in .md
|
//iterating through all the text elements in .md
|
||||||
//Text elements always contain the raw value
|
//Text elements always contain the raw value
|
||||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||||
const res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts = res.next()).done) {
|
while(!(parts = res.next()).done) {
|
||||||
const text = data.substring(position,parts.value.index);
|
const text = data.substring(position,parts.value.index);
|
||||||
@@ -152,6 +191,15 @@ export class ExcalidrawData {
|
|||||||
position = parts.value.index + BLOCKREF_LEN;
|
position = parts.value.index + BLOCKREF_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Load Embedded files
|
||||||
|
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||||
|
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
|
||||||
|
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||||
|
while(!(parts = res.next()).done) {
|
||||||
|
this.files.set(parts.value[1] as FileId,parts.value[2]);
|
||||||
|
}
|
||||||
|
|
||||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||||
//e.g. if the entire text elements section was deleted.
|
//e.g. if the entire text elements section was deleted.
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
@@ -161,12 +209,20 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
||||||
|
this.compatibilityMode = true;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
this.setShowLinkBrackets();
|
this.setShowLinkBrackets();
|
||||||
this.setLinkPrefix();
|
this.setLinkPrefix();
|
||||||
this.setUrlPrefix();
|
this.setUrlPrefix();
|
||||||
this.scene = JSON.parse(data);
|
this.scene = JSON.parse(data);
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes without the files element
|
||||||
|
}
|
||||||
|
if(this.plugin.settings.matchThemeAlways) {
|
||||||
|
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
|
||||||
|
}
|
||||||
|
this.files.clear();
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
||||||
return true;
|
return true;
|
||||||
@@ -347,7 +403,7 @@ export class ExcalidrawData {
|
|||||||
private async parse(text:string):Promise<string>{
|
private async parse(text:string):Promise<string>{
|
||||||
let outString = "";
|
let outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
@@ -387,7 +443,7 @@ export class ExcalidrawData {
|
|||||||
*/
|
*/
|
||||||
private quickParse(text:string):string {
|
private quickParse(text:string):string {
|
||||||
const hasTransclusion = (text:string):boolean => {
|
const hasTransclusion = (text:string):boolean => {
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts=res.next()).done) {
|
while(!(parts=res.next()).done) {
|
||||||
if (REGEX_LINK.isTransclusion(parts)) return true;
|
if (REGEX_LINK.isTransclusion(parts)) return true;
|
||||||
@@ -398,7 +454,7 @@ export class ExcalidrawData {
|
|||||||
|
|
||||||
let outString = "";
|
let outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
@@ -432,13 +488,58 @@ export class ExcalidrawData {
|
|||||||
for(const key of this.textElements.keys()){
|
for(const key of this.textElements.keys()){
|
||||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene));
|
if(this.files.size>0) {
|
||||||
|
outString += '\n# Embedded files\n';
|
||||||
|
for(const key of this.files.keys()) {
|
||||||
|
outString += key +': [['+this.files.get(key) + ']]\n';
|
||||||
|
}
|
||||||
|
outString += '\n';
|
||||||
|
}
|
||||||
|
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
//remove files that no longer have a corresponding image element
|
||||||
|
const fileIds = (scene.elements.filter((e)=>e.type==="image") as ExcalidrawImageElement[]).map((e)=>e.fileId);
|
||||||
|
this.files.forEach((value,key)=>{
|
||||||
|
if(!fileIds.contains(key)) {
|
||||||
|
this.files.delete(key);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//check if there are any images that need to be processed in the new scene
|
||||||
|
if(!scene.files || scene.files == {}) return false;
|
||||||
|
|
||||||
|
for(const key of Object.keys(scene.files)) {
|
||||||
|
if(!this.files.has(key as FileId)) {
|
||||||
|
dirty = true;
|
||||||
|
let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS");
|
||||||
|
switch(scene.files[key].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 [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname);
|
||||||
|
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL));
|
||||||
|
this.files.set(key as FileId,filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async syncElements(newScene:any):Promise<boolean> {
|
public async syncElements(newScene:any):Promise<boolean> {
|
||||||
//console.log("Excalidraw.Data.syncElements()");
|
this.scene = newScene;
|
||||||
this.scene = newScene;//JSON_parse(newScene);
|
let result = false;
|
||||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
if(!this.compatibilityMode) {
|
||||||
|
result = await this.syncFiles(newScene);
|
||||||
|
this.scene.files = {};
|
||||||
|
}
|
||||||
|
result = result || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||||
await this.updateTextElementsFromScene();
|
await this.updateTextElementsFromScene();
|
||||||
return result || this.findNewTextElementsInScene();
|
return result || this.findNewTextElementsInScene();
|
||||||
}
|
}
|
||||||
@@ -520,6 +621,4 @@ export class ExcalidrawData {
|
|||||||
return showLinkBrackets != this.showLinkBrackets;
|
return showLinkBrackets != this.showLinkBrackets;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -10,9 +10,10 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
|
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
|
||||||
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
import { ExcalidrawElement,ExcalidrawImageElement,ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
LibraryItems
|
LibraryItems
|
||||||
} from "@zsviczian/excalidraw/types/types";
|
} from "@zsviczian/excalidraw/types/types";
|
||||||
import {
|
import {
|
||||||
@@ -32,12 +33,13 @@ import {
|
|||||||
IMAGE_TYPES
|
IMAGE_TYPES
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import ExcalidrawPlugin from './main';
|
import ExcalidrawPlugin from './main';
|
||||||
import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
import {ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
||||||
import { t } from "./lang/helpers";
|
import { t } from "./lang/helpers";
|
||||||
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
||||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils";
|
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, loadSceneFiles, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, viewportCoordsToSceneCoords } from "./Utils";
|
||||||
import { Prompt } from "./Prompt";
|
import { Prompt } from "./Prompt";
|
||||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||||
|
import { ifStatement } from "@babel/types";
|
||||||
|
|
||||||
declare let window: ExcalidrawAutomate;
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
@@ -62,9 +64,11 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
private getScene: Function = null;
|
private getScene: Function = null;
|
||||||
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
||||||
private getSelectedTextElement: Function = null;
|
private getSelectedTextElement: Function = null;
|
||||||
|
private getSelectedImageElement: Function = null;
|
||||||
public addText:Function = null;
|
public addText:Function = null;
|
||||||
private refresh: Function = null;
|
private refresh: Function = null;
|
||||||
public excalidrawRef: React.MutableRefObject<any> = null;
|
public excalidrawRef: React.MutableRefObject<any> = null;
|
||||||
|
public excalidrawAPI: any = null;
|
||||||
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||||
private justLoaded: boolean = false;
|
private justLoaded: boolean = false;
|
||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
@@ -80,7 +84,6 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
private ctrlKeyDown = false;
|
private ctrlKeyDown = false;
|
||||||
private shiftKeyDown = false;
|
private shiftKeyDown = false;
|
||||||
private altKeyDown = false;
|
private altKeyDown = false;
|
||||||
private mouseEvent:any = null;
|
|
||||||
|
|
||||||
id: string = (this.leaf as any).id;
|
id: string = (this.leaf as any).id;
|
||||||
|
|
||||||
@@ -97,8 +100,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||||
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
|
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene,null,"\t"));
|
||||||
else this.app.vault.create(filepath,JSON.stringify(scene));
|
else this.app.vault.create(filepath,JSON.stringify(scene,null,"\t"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveSVG(scene?: any) {
|
public saveSVG(scene?: any) {
|
||||||
@@ -113,26 +116,15 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
|
const svg = await getSVG(scene,exportSettings);
|
||||||
if(!svg) return;
|
if(!svg) return;
|
||||||
let serializer =new XMLSerializer();
|
let serializer =new XMLSerializer();
|
||||||
const svgString = serializer.serializeToString(ExcalidrawView.embedFontsInSVG(svg));
|
const svgString = serializer.serializeToString(embedFontsInSVG(svg));
|
||||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||||
else await this.app.vault.create(filepath,svgString);
|
else await this.app.vault.create(filepath,svgString);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement {
|
|
||||||
//replace font references with base64 fonts
|
|
||||||
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
|
||||||
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
|
||||||
const defs = svg.querySelector("defs");
|
|
||||||
if (defs && (includesCascadia || includesVirgil)) {
|
|
||||||
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
|
||||||
}
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public savePNG(scene?: any) {
|
public savePNG(scene?: any) {
|
||||||
if(!scene) {
|
if(!scene) {
|
||||||
if (!this.getScene) return false;
|
if (!this.getScene) return false;
|
||||||
@@ -147,7 +139,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
const png = await getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
||||||
if(!png) return;
|
if(!png) return;
|
||||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||||
@@ -158,13 +150,16 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!this.getScene) return;
|
if(!this.getScene) return;
|
||||||
this.preventReload = preventReload;
|
this.preventReload = preventReload;
|
||||||
this.dirty = null;
|
this.dirty = null;
|
||||||
|
const scene = this.getScene();
|
||||||
|
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
await this.excalidrawData.syncElements(this.getScene());
|
await this.excalidrawData.syncElements(scene);
|
||||||
} else {
|
} else {
|
||||||
if(await this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
if(await this.excalidrawData.syncElements(scene) && !this.autosaving) {
|
||||||
await this.loadDrawing(false);
|
await this.loadDrawing(false);
|
||||||
}
|
}
|
||||||
|
//generate SVG preview snapshot
|
||||||
|
this.excalidrawData.svgSnapshot = await generateSVGString(this.getScene(),this.plugin.settings);
|
||||||
}
|
}
|
||||||
await super.save();
|
await super.save();
|
||||||
}
|
}
|
||||||
@@ -176,12 +171,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
//console.log("ExcalidrawView.getViewData()");
|
//console.log("ExcalidrawView.getViewData()");
|
||||||
if(!this.getScene) return this.data;
|
if(!this.getScene) return this.data;
|
||||||
if(!this.excalidrawData.loaded) return this.data;
|
if(!this.excalidrawData.loaded) return this.data;
|
||||||
|
const scene = this.getScene();
|
||||||
if(!this.compatibilityMode) {
|
if(!this.compatibilityMode) {
|
||||||
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
||||||
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
||||||
if(trimLocation == -1) return this.data;
|
if(trimLocation == -1) return this.data;
|
||||||
|
|
||||||
const scene = this.excalidrawData.scene;
|
|
||||||
if(!this.autosaving) {
|
if(!this.autosaving) {
|
||||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||||
@@ -193,84 +188,102 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return header + this.excalidrawData.generateMD();
|
return header + this.excalidrawData.generateMD();
|
||||||
}
|
}
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
const scene = this.excalidrawData.scene;
|
|
||||||
if(!this.autosaving) {
|
if(!this.autosaving) {
|
||||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||||
}
|
}
|
||||||
return JSON.stringify(scene);
|
return JSON.stringify(scene,null,"\t");
|
||||||
}
|
}
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||||
let text:string = (this.textMode == TextMode.parsed)
|
const selectedText = this.getSelectedTextElement();
|
||||||
? this.excalidrawData.getRawText(this.getSelectedTextElement().id)
|
let file = null;
|
||||||
: this.getSelectedTextElement().text;
|
let lineNum = 0;
|
||||||
if(!text) {
|
let linkText:string = null;
|
||||||
|
|
||||||
|
if(selectedText?.id) {
|
||||||
|
linkText = (this.textMode == TextMode.parsed)
|
||||||
|
? this.excalidrawData.getRawText(selectedText.id)
|
||||||
|
: selectedText.text;
|
||||||
|
|
||||||
|
linkText = linkText.replaceAll("\n",""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||||
|
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||||
|
window.open(linkText,"_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = REGEX_LINK.getRes(linkText).next();
|
||||||
|
if(!parts.value) {
|
||||||
|
const tags = linkText.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
|
||||||
|
if(!tags.value || tags.value.length<2) {
|
||||||
|
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const search=this.app.workspace.getLeavesOfType("search");
|
||||||
|
if(search.length==0) return;
|
||||||
|
//@ts-ignore
|
||||||
|
search[0].view.setQuery("tag:"+tags.value[1]);
|
||||||
|
this.app.workspace.revealLeaf(search[0]);
|
||||||
|
|
||||||
|
if(document.fullscreenElement === this.contentEl) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
this.zoomToFit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkText = REGEX_LINK.getLink(parts);
|
||||||
|
|
||||||
|
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||||
|
window.open(linkText,"_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(linkText.search("#")>-1) {
|
||||||
|
let t;
|
||||||
|
[t,lineNum] = await this.excalidrawData.getTransclusion(linkText);
|
||||||
|
linkText = linkText.substring(0,linkText.search("#"));
|
||||||
|
}
|
||||||
|
if(linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||||
|
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file = view.app.metadataCache.getFirstLinkpathDest(linkText,view.file.path);
|
||||||
|
if (!ev.altKey && !file) {
|
||||||
|
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const selectedImage = this.getSelectedImageElement();
|
||||||
|
if(selectedImage?.id) {
|
||||||
|
await this.save(true); //in case pasted images haven't been saved yet
|
||||||
|
if(this.excalidrawData.files.has(selectedImage.fileId)) {
|
||||||
|
linkText = this.excalidrawData.files.get(selectedImage.fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!linkText) {
|
||||||
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
||||||
window.open(text,"_blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = text.matchAll(REGEX_LINK.EXPR).next();
|
|
||||||
if(!parts.value) {
|
|
||||||
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
|
|
||||||
if(!tags.value || tags.value.length<2) {
|
|
||||||
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const search=this.app.workspace.getLeavesOfType("search");
|
|
||||||
if(search.length==0) return;
|
|
||||||
//@ts-ignore
|
|
||||||
search[0].view.setQuery("tag:"+tags.value[1]);
|
|
||||||
this.app.workspace.revealLeaf(search[0]);
|
|
||||||
//if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
|
||||||
if(document.fullscreenElement === this.contentEl) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
this.zoomToFit();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
|
||||||
|
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
||||||
window.open(text,"_blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lineNum = null;
|
|
||||||
if(text.search("#")>-1) {
|
|
||||||
let t;
|
|
||||||
[t,lineNum] = await this.excalidrawData.getTransclusion(text);
|
|
||||||
text = text.substring(0,text.search("#"));
|
|
||||||
}
|
|
||||||
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
|
|
||||||
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
|
|
||||||
if (!ev.altKey && !file) {
|
|
||||||
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const f = view.file;
|
const f = view.file;
|
||||||
if(ev.shiftKey && document.fullscreenElement === this.contentEl) {
|
if(ev.shiftKey && document.fullscreenElement === this.contentEl) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
}
|
}
|
||||||
if(lineNum) {
|
const leaf = ev.shiftKey ? getNewOrAdjacentLeaf(this.plugin,view.leaf) : view.leaf;
|
||||||
const leaf = ev.shiftKey ? view.app.workspace.createLeafBySplit(view.leaf) : view.leaf;
|
view.app.workspace.setActiveLeaf(leaf);
|
||||||
leaf.openFile(file,{eState: {line: lineNum-1}});
|
if(file) {
|
||||||
return;
|
leaf.openFile(file,{eState: {line: lineNum-1}}); //if file exists open file and jump to reference
|
||||||
|
} else {
|
||||||
|
leaf.view.app.workspace.openLinkText(linkText,view.file.path);
|
||||||
}
|
}
|
||||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(e,4000);
|
new Notice(e,4000);
|
||||||
}
|
}
|
||||||
@@ -326,7 +339,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
if(reload) {
|
if(reload) {
|
||||||
await this.save(false);
|
await this.save(false);
|
||||||
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
|
this.excalidrawAPI.history.clear(); //to avoid undo replacing links with parsed text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +369,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.preventReload = false;
|
this.preventReload = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.compatibilityMode) {
|
||||||
|
this.dirty = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
if(!this.file) return;
|
if(!this.file) return;
|
||||||
if(file) this.data = await this.app.vault.cachedRead(file);
|
if(file) this.data = await this.app.vault.cachedRead(file);
|
||||||
@@ -368,8 +385,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
// clear the view content
|
// clear the view content
|
||||||
clear() {
|
clear() {
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
this.excalidrawRef.current.resetScene();
|
this.excalidrawAPI.resetScene();
|
||||||
this.excalidrawRef.current.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewData (data: string, clear: boolean = false) {
|
async setViewData (data: string, clear: boolean = false) {
|
||||||
@@ -377,7 +394,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
|
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||||
this.app.workspace.onLayoutReady(async ()=>{
|
this.app.workspace.onLayoutReady(async ()=>{
|
||||||
this.dirty = null;
|
this.dirty = null;
|
||||||
this.compatibilityMode = this.file.extension == "excalidraw";
|
this.compatibilityMode = this.file.extension === "excalidraw";
|
||||||
await this.plugin.loadSettings();
|
await this.plugin.loadSettings();
|
||||||
this.plugin.opencount++;
|
this.plugin.opencount++;
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
@@ -415,27 +432,46 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const excalidrawData = this.excalidrawData.scene;
|
const excalidrawData = this.excalidrawData.scene;
|
||||||
this.justLoaded = justloaded;
|
this.justLoaded = justloaded;
|
||||||
if(this.excalidrawRef) {
|
if(this.excalidrawRef) {
|
||||||
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
|
const viewModeEnabled = this.excalidrawAPI.getAppState().viewModeEnabled;
|
||||||
const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled;
|
const zenModeEnabled = this.excalidrawAPI.getAppState().zenModeEnabled;
|
||||||
this.excalidrawRef.current.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.elements,
|
||||||
appState: {
|
appState: {
|
||||||
zenModeEnabled: zenModeEnabled,
|
zenModeEnabled: zenModeEnabled,
|
||||||
viewModeEnabled: viewModeEnabled,
|
viewModeEnabled: viewModeEnabled,
|
||||||
... excalidrawData.appState,
|
... excalidrawData.appState,
|
||||||
},
|
},
|
||||||
|
files: excalidrawData.files,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
||||||
this.excalidrawWrapperRef.current.focus();
|
this.excalidrawWrapperRef.current.focus();
|
||||||
}
|
}
|
||||||
|
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
|
||||||
} else {
|
} else {
|
||||||
this.instantiateExcalidraw({
|
this.instantiateExcalidraw({
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.elements,
|
||||||
appState: excalidrawData.appState,
|
appState: excalidrawData.appState,
|
||||||
|
files: excalidrawData.files,
|
||||||
libraryItems: await this.getLibrary(),
|
libraryItems: await this.getLibrary(),
|
||||||
});
|
});
|
||||||
|
//files are loaded on excalidrawRef readyPromise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addFiles(files:any) {
|
||||||
|
if(files.length === 0) return;
|
||||||
|
const [dirty, scene] = scaleLoadedImage(this.getScene(),files);
|
||||||
|
|
||||||
|
if(dirty) {
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: scene.appState,
|
||||||
|
commitToHistory: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.excalidrawAPI.addFiles(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Compatibility mode with .excalidraw files
|
//Compatibility mode with .excalidraw files
|
||||||
@@ -461,7 +497,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
|
|
||||||
setMarkdownView() {
|
setMarkdownView() {
|
||||||
if(this.excalidrawRef) {
|
if(this.excalidrawRef) {
|
||||||
const el = this.excalidrawRef.current.getSceneElements();
|
const el = this.excalidrawAPI.getSceneElements();
|
||||||
if(el.filter((e:any)=>e.type==="image").length>0) {
|
if(el.filter((e:any)=>e.type==="image").length>0) {
|
||||||
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
||||||
}
|
}
|
||||||
@@ -497,12 +533,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
||||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||||
this.app.vault.create(fname,JSON.stringify(this.getScene()));
|
this.app.vault.create(fname,JSON.stringify(this.getScene(),null,"\t"));
|
||||||
new Notice("Exported to " + fname,6000);
|
new Notice("Exported to " + fname,6000);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene(),null,"\t")), this.file.basename+'.excalidraw');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -523,12 +559,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
.setIcon(PNG_ICON_NAME)
|
.setIcon(PNG_ICON_NAME)
|
||||||
.onClick( async (ev)=> {
|
.onClick( async (ev)=> {
|
||||||
if(!this.getScene || !this.file) return;
|
if(!this.getScene || !this.file) return;
|
||||||
if(ev.ctrlKey || ev.metaKey) {
|
if(ev.ctrlKey||ev.metaKey) {
|
||||||
const exportSettings: ExportSettings = {
|
const exportSettings: ExportSettings = {
|
||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
const png = await getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
||||||
if(!png) return;
|
if(!png) return;
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.readAsDataURL(png);
|
reader.readAsDataURL(png);
|
||||||
@@ -548,15 +584,15 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
.setIcon(SVG_ICON_NAME)
|
.setIcon(SVG_ICON_NAME)
|
||||||
.onClick(async (ev)=> {
|
.onClick(async (ev)=> {
|
||||||
if(!this.getScene || !this.file) return;
|
if(!this.getScene || !this.file) return;
|
||||||
if(ev.ctrlKey || ev.metaKey) {
|
if(ev.ctrlKey||ev.metaKey) {
|
||||||
const exportSettings: ExportSettings = {
|
const exportSettings: ExportSettings = {
|
||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
let svg = await getSVG(this.getScene(),exportSettings);
|
||||||
if(!svg) return null;
|
if(!svg) return null;
|
||||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
svg = embedFontsInSVG(svg);
|
||||||
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
download(null,svgToBase64(svg.outerHTML),this.file.basename+'.svg');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveSVG()
|
this.saveSVG()
|
||||||
@@ -578,13 +614,45 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const reactElement = React.createElement(() => {
|
const reactElement = React.createElement(() => {
|
||||||
let previousSceneVersion = 0;
|
let previousSceneVersion = 0;
|
||||||
let currentPosition = {x:0, y:0};
|
let currentPosition = {x:0, y:0};
|
||||||
const excalidrawRef = React.useRef(null);
|
|
||||||
const excalidrawWrapperRef = React.useRef(null);
|
const excalidrawWrapperRef = React.useRef(null);
|
||||||
const [dimensions, setDimensions] = React.useState({
|
const [dimensions, setDimensions] = React.useState({
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined
|
height: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//excalidrawRef readypromise based on
|
||||||
|
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
||||||
|
const resolvablePromise = () => {
|
||||||
|
let resolve;
|
||||||
|
let reject;
|
||||||
|
const promise = new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve;
|
||||||
|
reject = _reject;
|
||||||
|
});
|
||||||
|
//@ts-ignore
|
||||||
|
promise.resolve = resolve;
|
||||||
|
//@ts-ignore
|
||||||
|
promise.reject = reject;
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// To memoize value between rerenders
|
||||||
|
const excalidrawRef = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
current: {
|
||||||
|
readyPromise: resolvablePromise()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
excalidrawRef.current.readyPromise.then((api) => {
|
||||||
|
this.excalidrawAPI = api;
|
||||||
|
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
|
||||||
|
});
|
||||||
|
}, [excalidrawRef]);
|
||||||
|
|
||||||
this.excalidrawRef = excalidrawRef;
|
this.excalidrawRef = excalidrawRef;
|
||||||
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
||||||
|
|
||||||
@@ -606,24 +674,23 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return () => window.removeEventListener("resize", onResize);
|
return () => window.removeEventListener("resize", onResize);
|
||||||
}, [excalidrawWrapperRef]);
|
}, [excalidrawWrapperRef]);
|
||||||
|
|
||||||
|
|
||||||
this.getSelectedTextElement = ():{id: string, text:string} => {
|
this.getSelectedTextElement = ():{id: string, text:string} => {
|
||||||
if(!excalidrawRef?.current) return {id:null,text:null};
|
if(!excalidrawRef?.current) return {id:null,text:null};
|
||||||
if(this.excalidrawRef.current.getAppState().viewModeEnabled) {
|
if(this.excalidrawAPI.getAppState().viewModeEnabled) {
|
||||||
if(selectedTextElement) {
|
if(selectedTextElement) {
|
||||||
const retval = selectedTextElement;
|
const retval = selectedTextElement;
|
||||||
selectedTextElement == null;
|
selectedTextElement = null;
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
return {id:null,text:null};
|
return {id:null,text:null};
|
||||||
}
|
}
|
||||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]);
|
||||||
if(selectedElement.length==0) return {id:null,text:null};
|
if(selectedElement.length==0) return {id:null,text:null};
|
||||||
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text
|
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text
|
||||||
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
|
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
|
||||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||||
const textElement = excalidrawRef
|
const textElement = this
|
||||||
.current
|
.excalidrawAPI
|
||||||
.getSceneElements()
|
.getSceneElements()
|
||||||
.filter((el:any)=>el.groupIds?.includes(group))
|
.filter((el:any)=>el.groupIds?.includes(group))
|
||||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||||
@@ -631,12 +698,36 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text
|
return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.getSelectedImageElement = ():{id: string, fileId:string} => {
|
||||||
|
if(!excalidrawRef?.current) return {id:null,fileId:null};
|
||||||
|
if(this.excalidrawAPI.getAppState().viewModeEnabled) {
|
||||||
|
if(selectedImageElement) {
|
||||||
|
const retval = selectedImageElement;
|
||||||
|
selectedImageElement = null;
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
return {id:null,fileId:null};
|
||||||
|
}
|
||||||
|
const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]);
|
||||||
|
if(selectedElement.length===0) return {id:null,fileId:null};
|
||||||
|
if(selectedElement[0].type == "image") return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //an image element was selected. Return fileId
|
||||||
|
if(selectedElement[0].groupIds.length === 0) return {id:null,fileId:null}; //is the selected element part of a group?
|
||||||
|
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||||
|
const imageElement = this
|
||||||
|
.excalidrawAPI
|
||||||
|
.getSceneElements()
|
||||||
|
.filter((el:any)=>el.groupIds?.includes(group))
|
||||||
|
.filter((el:any)=>el.type=="image"); //filter for Image elements of the group
|
||||||
|
if(imageElement.length===0) return {id:null,fileId:null}; //the group had no image element member
|
||||||
|
return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //return image element fileId
|
||||||
|
};
|
||||||
|
|
||||||
this.addText = (text:string, fontFamily?:1|2|3) => {
|
this.addText = (text:string, fontFamily?:1|2|3) => {
|
||||||
if(!excalidrawRef?.current) {
|
if(!excalidrawRef?.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
window.ExcalidrawAutomate.reset();
|
window.ExcalidrawAutomate.reset();
|
||||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||||
@@ -646,7 +737,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||||
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
|
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
|
||||||
if(!excalidrawRef?.current) return false;
|
if(!excalidrawRef?.current) return false;
|
||||||
|
|
||||||
@@ -659,27 +750,28 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
let st: AppState = excalidrawRef.current.getAppState();
|
let st: AppState = this.excalidrawAPI.getAppState();
|
||||||
if(!st.files) {
|
|
||||||
st.files = {};
|
|
||||||
}
|
|
||||||
if(images) {
|
|
||||||
Object.keys(images).forEach((k)=>{
|
|
||||||
st.files[k]={
|
|
||||||
type:images[k].type,
|
|
||||||
id: images[k].id,
|
|
||||||
dataURL: images[k].dataURL
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//merge appstate.files with files
|
|
||||||
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
|
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
|
||||||
this.excalidrawRef.current.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements: el.concat(newElements),
|
elements: el.concat(newElements),
|
||||||
appState: st,
|
appState: st,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
|
if(images) {
|
||||||
|
let files:BinaryFileData[] = [];
|
||||||
|
Object.keys(images).forEach((k)=>{
|
||||||
|
files.push({
|
||||||
|
mimeType :images[k].mimeType,
|
||||||
|
id: images[k].id,
|
||||||
|
dataURL: images[k].dataURL,
|
||||||
|
created: images[k].created
|
||||||
|
});
|
||||||
|
this.excalidrawData.files.set(images[k].id,images[k].file);
|
||||||
|
});
|
||||||
|
this.excalidrawAPI.addFiles(files);
|
||||||
|
}
|
||||||
if(save) this.save(); else this.dirty = this.file?.path;
|
if(save) this.save(); else this.dirty = this.file?.path;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -688,15 +780,16 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!excalidrawRef?.current) {
|
if(!excalidrawRef?.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
|
const files = this.excalidrawAPI.getFiles();
|
||||||
|
|
||||||
if(st.files) {
|
if(files) {
|
||||||
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.imageId);
|
const imgIds = el.filter((e)=>e.type==="image").map((e:any)=>e.fileId);
|
||||||
const toDelete = Object.keys(st.files).filter((k)=>!imgIds.contains(k));
|
const toDelete = Object.keys(files).filter((k)=>!imgIds.contains(k));
|
||||||
toDelete.forEach((k)=>delete st.files[k]);
|
toDelete.forEach((k)=>delete files[k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -720,31 +813,58 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||||
gridSize: st.gridSize,
|
gridSize: st.gridSize,
|
||||||
files: st.files??{},
|
},
|
||||||
}
|
files: files,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
this.refresh = () => {
|
this.refresh = () => {
|
||||||
if(!excalidrawRef?.current) return;
|
if(!excalidrawRef?.current) return;
|
||||||
excalidrawRef.current.refresh();
|
this.excalidrawAPI.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
//variables used to handle click events in view mode
|
//variables used to handle click events in view mode
|
||||||
let selectedTextElement:{id:string,text:string} = null;
|
let selectedTextElement:{id:string,text:string} = null;
|
||||||
|
let selectedImageElement:{id:string,fileId:string} = null;
|
||||||
let timestamp = 0;
|
let timestamp = 0;
|
||||||
let blockOnMouseButtonDown = false;
|
let blockOnMouseButtonDown = false;
|
||||||
|
|
||||||
const getTextElementAtPointer = (pointer:any) => {
|
const getElementsAtPointer = (pointer:any, elements:ExcalidrawElement[], type:string):ExcalidrawElement[] => {
|
||||||
const elements = this.excalidrawRef.current.getSceneElements()
|
return elements.filter((e:ExcalidrawElement)=>{
|
||||||
.filter((e:ExcalidrawElement)=>{
|
if (e.type !== type) return false;
|
||||||
return e.type == "text"
|
const [x,y,w,h] = rotatedDimensions(e);
|
||||||
&& e.x<=pointer.x && (e.x+e.width)>=pointer.x
|
return x<=pointer.x && x+w>=pointer.x
|
||||||
&& e.y<=pointer.y && (e.y+e.height)>=pointer.y;
|
&& y<=pointer.y && y+h>=pointer.y;
|
||||||
});
|
});
|
||||||
if(elements.length==0) return null;
|
|
||||||
return {id:elements[0].id,text:elements[0].text};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTextElementAtPointer = (pointer:any) => {
|
||||||
|
const elements = getElementsAtPointer(pointer,this.excalidrawAPI.getSceneElements(),'text') as ExcalidrawTextElement[];
|
||||||
|
if(elements.length==0) return {id:null, text:null};
|
||||||
|
if(elements.length===1) return {id:elements[0].id,text:elements[0].text};
|
||||||
|
//if more than 1 text elements are at the location, look for one that has a link
|
||||||
|
const elementsWithLinks = elements.filter((e:ExcalidrawTextElement)=> {
|
||||||
|
const text:string = (this.textMode == TextMode.parsed)
|
||||||
|
? this.excalidrawData.getRawText(e.id)
|
||||||
|
: e.text;
|
||||||
|
if(!text) return false;
|
||||||
|
if(text.match(REG_LINKINDEX_HYPERLINK)) return true;
|
||||||
|
const parts = REGEX_LINK.getRes(text).next();
|
||||||
|
if(!parts.value) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
//if there are no text elements with links, return the first element without a link
|
||||||
|
if(elementsWithLinks.length==0) return {id:elements[0].id,text:elements[0].text};
|
||||||
|
//if there are still multiple text elements with links on top of each other, return the first
|
||||||
|
return {id:elementsWithLinks[0].id,text:elementsWithLinks[0].text};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImageElementAtPointer = (pointer:any) => {
|
||||||
|
const elements = getElementsAtPointer(pointer,this.excalidrawAPI.getSceneElements(),'image') as ExcalidrawImageElement[];
|
||||||
|
if(elements.length===0) return {id:null, fileId:null};
|
||||||
|
if(elements.length>=1) return {id:elements[0].id,fileId:elements[0].fileId};
|
||||||
|
//if more than 1 image elements are at the location, return the first
|
||||||
|
}
|
||||||
|
|
||||||
let hoverPoint = {x:0,y:0};
|
let hoverPoint = {x:0,y:0};
|
||||||
let hoverPreviewTarget:EventTarget = null;
|
let hoverPreviewTarget:EventTarget = null;
|
||||||
@@ -762,8 +882,6 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
|
|
||||||
const dropAction = (transfer: DataTransfer) => {
|
const dropAction = (transfer: DataTransfer) => {
|
||||||
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
|
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
|
||||||
|
|
||||||
//if (transfer.types.includes('text/uri-list')) return 'link';
|
|
||||||
let files = (this.app as any).dragManager.draggable?.files;
|
let files = (this.app as any).dragManager.draggable?.files;
|
||||||
if(files) {
|
if(files) {
|
||||||
if(files[0] == this.file) {
|
if(files[0] == this.file) {
|
||||||
@@ -772,9 +890,30 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['file', 'files'].includes((this.app as any).dragManager.draggable?.type)) return 'link';
|
if (['file', 'files'].includes((this.app as any).dragManager.draggable?.type)) return 'link';
|
||||||
if (transfer.types?.includes('text/html') || transfer.types?.includes('text/plain')) return 'copy';
|
if ( transfer.types?.includes('text/html')
|
||||||
|
|| transfer.types?.includes('text/plain')
|
||||||
|
|| transfer.types?.includes('Files')
|
||||||
|
) return 'copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewModeEnabled = false;
|
||||||
|
const handleLinkClick = () => {
|
||||||
|
selectedTextElement = getTextElementAtPointer(currentPosition);
|
||||||
|
if(selectedTextElement && selectedTextElement.id) {
|
||||||
|
const event = new MouseEvent("click", {ctrlKey:true, metaKey:true, shiftKey:this.shiftKeyDown, altKey:this.altKeyDown});
|
||||||
|
this.handleLinkClick(this,event);
|
||||||
|
selectedTextElement = null;
|
||||||
|
}
|
||||||
|
selectedImageElement = getImageElementAtPointer(currentPosition);
|
||||||
|
if(selectedImageElement && selectedImageElement.id) {
|
||||||
|
const event = new MouseEvent("click", {ctrlKey:true, metaKey:true, shiftKey:this.shiftKeyDown, altKey:this.altKeyDown});
|
||||||
|
this.handleLinkClick(this,event);
|
||||||
|
selectedImageElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseEvent:any = null;
|
||||||
|
|
||||||
const excalidrawDiv = React.createElement(
|
const excalidrawDiv = React.createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@@ -783,15 +922,18 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
key: "abc",
|
key: "abc",
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
onKeyDown: (e:any) => {
|
onKeyDown: (e:any) => {
|
||||||
|
//@ts-ignore
|
||||||
|
if(e.target === excalidrawDiv.ref.current) return; //event should originate from the canvas
|
||||||
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
|
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
}
|
}
|
||||||
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
|
||||||
|
this.ctrlKeyDown = e.ctrlKey||e.metaKey;
|
||||||
this.shiftKeyDown = e.shiftKey;
|
this.shiftKeyDown = e.shiftKey;
|
||||||
this.altKeyDown = e.altKey;
|
this.altKeyDown = e.altKey;
|
||||||
|
|
||||||
if(e.ctrlKey && !e.shiftKey && !e.altKey) { // && !e.metaKey) {
|
if((e.ctrlKey||e.metaKey) && !e.shiftKey && !e.altKey) {
|
||||||
const selectedElement = getTextElementAtPointer(currentPosition);
|
const selectedElement = getTextElementAtPointer(currentPosition);
|
||||||
if(!selectedElement) return;
|
if(!selectedElement) return;
|
||||||
|
|
||||||
@@ -802,7 +944,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!text) return;
|
if(!text) return;
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
|
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||||
|
|
||||||
const parts = text.matchAll(REGEX_LINK.EXPR).next();
|
const parts = REGEX_LINK.getRes(text).next();
|
||||||
if(!parts.value) return;
|
if(!parts.value) return;
|
||||||
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
||||||
|
|
||||||
@@ -812,7 +954,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.plugin.hover.sourcePath = this.file.path;
|
this.plugin.hover.sourcePath = this.file.path;
|
||||||
hoverPreviewTarget = this.contentEl; //e.target;
|
hoverPreviewTarget = this.contentEl; //e.target;
|
||||||
this.app.workspace.trigger('hover-link', {
|
this.app.workspace.trigger('hover-link', {
|
||||||
event: this.mouseEvent,
|
event: mouseEvent,
|
||||||
source: VIEW_TYPE_EXCALIDRAW,
|
source: VIEW_TYPE_EXCALIDRAW,
|
||||||
hoverParent: hoverPreviewTarget,
|
hoverParent: hoverPreviewTarget,
|
||||||
targetEl: hoverPreviewTarget,
|
targetEl: hoverPreviewTarget,
|
||||||
@@ -830,20 +972,19 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyUp: (e:any) => {
|
onKeyUp: (e:any) => {
|
||||||
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
this.ctrlKeyDown = e.ctrlKey||e.metaKey;
|
||||||
this.shiftKeyDown = e.shiftKey;
|
this.shiftKeyDown = e.shiftKey;
|
||||||
this.altKeyDown = e.altKey;
|
this.altKeyDown = e.altKey;
|
||||||
},
|
},
|
||||||
onClick: (e:MouseEvent):any => {
|
onClick: (e:MouseEvent):any => {
|
||||||
//@ts-ignore
|
|
||||||
if(!(e.ctrlKey||e.metaKey)) return;
|
if(!(e.ctrlKey||e.metaKey)) return;
|
||||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||||
if(!this.getSelectedTextElement().id) return;
|
if(!(this.getSelectedTextElement().id || this.getSelectedImageElement().id)) return;
|
||||||
this.handleLinkClick(this,e);
|
this.handleLinkClick(this,e);
|
||||||
},
|
},
|
||||||
onMouseMove: (e:MouseEvent) => {
|
onMouseMove: (e:MouseEvent) => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.mouseEvent = e.nativeEvent;
|
mouseEvent = e.nativeEvent;
|
||||||
},
|
},
|
||||||
onMouseOver: (e:MouseEvent) => {
|
onMouseOver: (e:MouseEvent) => {
|
||||||
clearHoverPreview();
|
clearHoverPreview();
|
||||||
@@ -877,17 +1018,9 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
onPointerUpdate: (p:any) => {
|
onPointerUpdate: (p:any) => {
|
||||||
currentPosition = p.pointer;
|
currentPosition = p.pointer;
|
||||||
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
|
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
|
||||||
if(!this.excalidrawRef.current.getAppState().viewModeEnabled) return;
|
if(!viewModeEnabled) return;
|
||||||
const handleLinkClick = () => {
|
|
||||||
selectedTextElement = getTextElementAtPointer(p.pointer);
|
const buttonDown = !blockOnMouseButtonDown && p.button === "down";
|
||||||
if(selectedTextElement) {
|
|
||||||
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
|
|
||||||
this.handleLinkClick(this,event);
|
|
||||||
selectedTextElement = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonDown = !blockOnMouseButtonDown && p.button=="down";
|
|
||||||
if(buttonDown) {
|
if(buttonDown) {
|
||||||
blockOnMouseButtonDown = true;
|
blockOnMouseButtonDown = true;
|
||||||
|
|
||||||
@@ -905,11 +1038,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
timestamp = now;
|
timestamp = now;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (p.button=="up") {
|
if (p.button === "up") {
|
||||||
blockOnMouseButtonDown=false;
|
blockOnMouseButtonDown=false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||||
|
viewModeEnabled = st.viewModeEnabled;
|
||||||
if(this.justLoaded) {
|
if(this.justLoaded) {
|
||||||
this.justLoaded = false;
|
this.justLoaded = false;
|
||||||
this.zoomToFit(false);
|
this.zoomToFit(false);
|
||||||
@@ -940,7 +1074,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
||||||
|
|
||||||
const draggable = (this.app as any).dragManager.draggable;
|
const draggable = (this.app as any).dragManager.draggable;
|
||||||
@@ -975,7 +1109,9 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
switch(draggable?.type) {
|
switch(draggable?.type) {
|
||||||
case "file":
|
case "file":
|
||||||
if (!onDropHook("file",[draggable.file],null)) {
|
if (!onDropHook("file",[draggable.file],null)) {
|
||||||
if((event.ctrlKey || event.metaKey) && IMAGE_TYPES.contains(draggable.file.extension)) {
|
if((event.ctrlKey||event.metaKey)
|
||||||
|
&& (IMAGE_TYPES.contains(draggable.file.extension)
|
||||||
|
|| this.plugin.isExcalidrawFile(draggable.file))) {
|
||||||
const f = draggable.file;
|
const f = draggable.file;
|
||||||
const topX = currentPosition.x;
|
const topX = currentPosition.x;
|
||||||
const topY = currentPosition.y;
|
const topY = currentPosition.y;
|
||||||
@@ -1038,14 +1174,14 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
await this.save(false);
|
await this.save(false);
|
||||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
if(this.textMode == TextMode.parsed) this.excalidrawAPI.history.clear();
|
||||||
this.setupAutosaveTimer();
|
this.setupAutosaveTimer();
|
||||||
});
|
});
|
||||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||||
this.setupAutosaveTimer();
|
this.setupAutosaveTimer();
|
||||||
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||||
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||||
this.excalidrawRef.current.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
return parseResult;
|
return parseResult;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1063,51 +1199,22 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus());
|
|
||||||
|
ReactDOM.render(reactElement,this.contentEl,()=>{
|
||||||
|
this.excalidrawWrapperRef.current.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public zoomToFit(delay:boolean = true) {
|
public zoomToFit(delay:boolean = true) {
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
const current = this.excalidrawRef.current;
|
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
||||||
|
const current = this.excalidrawAPI;
|
||||||
const fullscreen = (document.fullscreenElement==this.contentEl);
|
const fullscreen = (document.fullscreenElement==this.contentEl);
|
||||||
const elements = current.getSceneElements();
|
const elements = current.getSceneElements();
|
||||||
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
|
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
|
||||||
setTimeout(() => current.zoomToFit(elements,2,fullscreen?0:0.05),100);
|
setTimeout(() => current.zoomToFit(elements,maxZoom,fullscreen?0:0.05),100);
|
||||||
} else {
|
} else {
|
||||||
current.zoomToFit(elements,2,fullscreen?0:0.05);
|
current.zoomToFit(elements,maxZoom,fullscreen?0:0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
|
|
||||||
try {
|
|
||||||
return exportToSvg({
|
|
||||||
elements: scene.elements,
|
|
||||||
appState: {
|
|
||||||
exportBackground: exportSettings.withBackground,
|
|
||||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
|
||||||
... scene.appState,},
|
|
||||||
exportPadding:10,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
|
|
||||||
try {
|
|
||||||
return await Excalidraw.exportToBlob({
|
|
||||||
elements: scene.elements,
|
|
||||||
appState: {
|
|
||||||
exportBackground: exportSettings.withBackground,
|
|
||||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
|
||||||
... scene.appState,},
|
|
||||||
mimeType: "image/png",
|
|
||||||
exportWithDarkMode: "true",
|
|
||||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
|
||||||
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { App, Modal } from "obsidian";
|
|
||||||
import { t } from "./lang/helpers";
|
|
||||||
import ExcalidrawPlugin from "./main";
|
|
||||||
|
|
||||||
export class MigrationPrompt extends Modal {
|
|
||||||
private plugin: ExcalidrawPlugin;
|
|
||||||
|
|
||||||
constructor(app: App, plugin:ExcalidrawPlugin) {
|
|
||||||
super(app);
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen(): void {
|
|
||||||
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
|
||||||
this.createForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose(): void {
|
|
||||||
this.contentEl.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
createForm(): void {
|
|
||||||
const div = this.contentEl.createDiv();
|
|
||||||
div.addClass("excalidarw-prompt-div");
|
|
||||||
div.style.maxWidth = "600px";
|
|
||||||
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
|
|
||||||
div.createEl('p',{text: ""} , (el) => {
|
|
||||||
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
|
|
||||||
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
|
||||||
});
|
|
||||||
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
|
|
||||||
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
|
|
||||||
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
|
||||||
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
|
||||||
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
|
|
||||||
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
|
|
||||||
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
|
||||||
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
|
||||||
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
|
||||||
});
|
|
||||||
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
|
|
||||||
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
|
|
||||||
bConvert.onclick = (ev)=>{
|
|
||||||
this.plugin.convertExcalidrawToMD();
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
const bCancel = div.createEl('button', {text: "CANCEL"});
|
|
||||||
bCancel.onclick = (ev)=>{
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
202
src/OneOffs.ts
Normal file
202
src/OneOffs.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { App, Modal, Notice, TFile } from "obsidian";
|
||||||
|
import { getJSON } from "./ExcalidrawData";
|
||||||
|
import ExcalidrawPlugin from "./main";
|
||||||
|
|
||||||
|
export class OneOffs {
|
||||||
|
private plugin:ExcalidrawPlugin
|
||||||
|
|
||||||
|
constructor(plugin: ExcalidrawPlugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public patchCommentBlock() {
|
||||||
|
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
|
||||||
|
if(!this.plugin.settings.patchCommentBlock) return;
|
||||||
|
const plugin = this.plugin;
|
||||||
|
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
|
||||||
|
setTimeout(async ()=>{
|
||||||
|
await plugin.loadSettings();
|
||||||
|
if (!plugin.settings.patchCommentBlock) {
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
|
||||||
|
let i = 0;
|
||||||
|
const excalidrawFiles = plugin.app.vault.getFiles();
|
||||||
|
for (const f of (excalidrawFiles || []).filter((f:TFile) => plugin.isExcalidrawFile(f))) {
|
||||||
|
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
|
||||||
|
&& (plugin.app.workspace.getActiveFile() !== f)) { //file is currently being edited
|
||||||
|
let drawing = await plugin.app.vault.read(f);
|
||||||
|
const orig_drawing = drawing;
|
||||||
|
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
|
||||||
|
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
|
||||||
|
if (drawing.search("\n%%\n# Drawing\n") === -1) {
|
||||||
|
const [json,pos] = getJSON(drawing);
|
||||||
|
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
|
||||||
|
};
|
||||||
|
if (drawing !== orig_drawing) {
|
||||||
|
i++;
|
||||||
|
console.log("Excalidraw patched: " + f.path);
|
||||||
|
await plugin.app.vault.modify(f,drawing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.settings.patchCommentBlock = false;
|
||||||
|
plugin.saveSettings();
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
|
||||||
|
},300000) //5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
public migrationNotice () {
|
||||||
|
if(this.plugin.settings.loadCount>0) return;
|
||||||
|
const plugin = this.plugin;
|
||||||
|
|
||||||
|
plugin.app.workspace.onLayoutReady(async () => {
|
||||||
|
plugin.settings.loadCount++;
|
||||||
|
plugin.saveSettings();
|
||||||
|
const files = plugin.app.vault.getFiles().filter((f)=>f.extension==="excalidraw");
|
||||||
|
if(files.length>0) {
|
||||||
|
const prompt = new MigrationPrompt(plugin.app, plugin);
|
||||||
|
prompt.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public imageElementLaunchNotice () {
|
||||||
|
if(!this.plugin.settings.imageElementNotice) return;
|
||||||
|
const plugin = this.plugin;
|
||||||
|
|
||||||
|
plugin.app.workspace.onLayoutReady(async () => {
|
||||||
|
const prompt = new ImageElementNotice(plugin.app, plugin);
|
||||||
|
prompt.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MigrationPrompt extends Modal {
|
||||||
|
private plugin: ExcalidrawPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin:ExcalidrawPlugin) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(): void {
|
||||||
|
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(): void {
|
||||||
|
this.contentEl.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
createForm(): void {
|
||||||
|
const div = this.contentEl.createDiv();
|
||||||
|
// div.addClass("excalidraw-prompt-div");
|
||||||
|
// div.style.maxWidth = "600px";
|
||||||
|
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
|
||||||
|
div.createEl('p',{text: ""} , (el) => {
|
||||||
|
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
|
||||||
|
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
||||||
|
});
|
||||||
|
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
|
||||||
|
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
|
||||||
|
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
||||||
|
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
||||||
|
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
|
||||||
|
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
|
||||||
|
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
||||||
|
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
||||||
|
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
||||||
|
});
|
||||||
|
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
|
||||||
|
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
|
||||||
|
bConvert.onclick = (ev)=>{
|
||||||
|
this.plugin.convertExcalidrawToMD();
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
const bCancel = div.createEl('button', {text: "CANCEL"});
|
||||||
|
bCancel.onclick = (ev)=>{
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageElementNotice extends Modal {
|
||||||
|
private plugin: ExcalidrawPlugin;
|
||||||
|
private saveChanges: boolean = false;
|
||||||
|
|
||||||
|
constructor(app: App, plugin:ExcalidrawPlugin) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(): void {
|
||||||
|
this.titleEl.setText("Image Elements have arrived!");
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(): void {
|
||||||
|
this.contentEl.empty();
|
||||||
|
if(!this.saveChanges) return;
|
||||||
|
this.plugin.settings.imageElementNotice = false;
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
createForm(): void {
|
||||||
|
const div = this.contentEl.createDiv();
|
||||||
|
//div.addClass("excalidraw-prompt-div");
|
||||||
|
//div.style.maxWidth = "600px";
|
||||||
|
|
||||||
|
div.createEl('p',{text: ""},(el) => {
|
||||||
|
el.innerHTML = "Welcome to Obsidian-Excalidraw 1.4! I've added Image Elements. "
|
||||||
|
+ "Please watch the video below to learn how to use this new feature.";
|
||||||
|
});
|
||||||
|
|
||||||
|
div.createEl('p',{text: ""}, (el) => {
|
||||||
|
el.innerHTML = "<u>⚠ WARNING:</u> Opening new drawings with an older version of the plugin will lead to loss of images. "
|
||||||
|
+ "Update the plugin on all your devices.";
|
||||||
|
});
|
||||||
|
|
||||||
|
div.createEl('p',{text: ""}, (el) => {
|
||||||
|
el.innerHTML = "Since March, I have spent most of my free time building this plugin. Close to 75 workdays worth of my time (assuming 8-hour days). "
|
||||||
|
+ "Some of you have already bought me a coffee. THANK YOU! Your support really means a lot to me! If you have not yet done so, please consider clicking the button below.";
|
||||||
|
});
|
||||||
|
|
||||||
|
const coffeeDiv = div.createDiv('coffee');
|
||||||
|
coffeeDiv.addClass('ex-coffee-div');
|
||||||
|
const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/zsolt' });
|
||||||
|
const coffeeImg = coffeeLink.createEl('img', {
|
||||||
|
attr: {
|
||||||
|
src: 'https://cdn.ko-fi.com/cdn/kofi3.png?v=3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
coffeeImg.height = 45;
|
||||||
|
|
||||||
|
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
|
||||||
|
el.style.textAlign = "center";
|
||||||
|
el.innerHTML = '<iframe width="560" height="315" src="https://www.youtube.com/embed/_c_0zpBJ4Xc?start=20" title="YouTube video player" '
|
||||||
|
+'frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" '
|
||||||
|
+ 'allowfullscreen></iframe>';
|
||||||
|
});
|
||||||
|
|
||||||
|
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
|
||||||
|
el.style.textAlign = "right";
|
||||||
|
|
||||||
|
const bOk = el.createEl('button', {text: "OK - Don't show this again"});
|
||||||
|
bOk.onclick = (ev)=>{
|
||||||
|
this.saveChanges = true;
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bCancel = el.createEl('button', {text: "CANCEL - Read next time"});
|
||||||
|
bCancel.onclick = (ev)=>{
|
||||||
|
this.saveChanges = false;
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/Utils.ts
266
src/Utils.ts
@@ -1,9 +1,28 @@
|
|||||||
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
|
||||||
|
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian";
|
||||||
import { Random } from "roughjs/bin/math";
|
import { Random } from "roughjs/bin/math";
|
||||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { IMAGE_TYPES } from "./constants";
|
import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
|
||||||
|
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||||
|
import ExcalidrawPlugin from "./main";
|
||||||
|
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { ExportSettings } from "./ExcalidrawView";
|
||||||
|
import { ExcalidrawSettings } from "./settings";
|
||||||
|
import { html_beautify } from "js-beautify"
|
||||||
|
|
||||||
|
declare module "obsidian" {
|
||||||
|
interface Workspace {
|
||||||
|
getAdjacentLeafInDirection(leaf: WorkspaceLeaf, direction: string): WorkspaceLeaf;
|
||||||
|
}
|
||||||
|
interface Vault {
|
||||||
|
getConfig(option:"attachmentFolderPath"): string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
|
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||||
@@ -105,6 +124,38 @@ export function wrapText(text:string, lineLen:number, forceWrap:boolean=false):s
|
|||||||
return outstring.replace(/\n$/, '');
|
return outstring.replace(/\n$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rotate = (
|
||||||
|
pointX: number,
|
||||||
|
pointY: number,
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
angle: number,
|
||||||
|
): [number, number] =>
|
||||||
|
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||||
|
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||||
|
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||||
|
[
|
||||||
|
(pointX - centerX) * Math.cos(angle) - (pointY - centerY) * Math.sin(angle) + centerX,
|
||||||
|
(pointX - centerX) * Math.sin(angle) + (pointY - centerY) * Math.cos(angle) + centerY,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const rotatedDimensions = (
|
||||||
|
element: ExcalidrawElement
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if(element.angle===0) [element.x,element.y,element.width,element.height];
|
||||||
|
const centerX = element.x+element.width/2;
|
||||||
|
const centerY = element.y+element.height/2;
|
||||||
|
const [left,top] = rotate(element.x,element.y,centerX,centerY,element.angle);
|
||||||
|
const [right,bottom] = rotate(element.x+element.width,element.y+element.height,centerX,centerY,element.angle);
|
||||||
|
return [
|
||||||
|
left<right ? left : right,
|
||||||
|
top<bottom ? top : bottom,
|
||||||
|
Math.abs(left-right),
|
||||||
|
Math.abs(top-bottom)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const viewportCoordsToSceneCoords = (
|
export const viewportCoordsToSceneCoords = (
|
||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
{
|
{
|
||||||
@@ -127,32 +178,69 @@ export const viewportCoordsToSceneCoords = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLeaf):WorkspaceLeaf => {
|
||||||
|
if(plugin.settings.openInAdjacentPane) {
|
||||||
|
let leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "right");
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "left");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "bottom");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "top");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.createLeafBySplit(leaf);}
|
||||||
|
return leafToUse;
|
||||||
|
}
|
||||||
|
return plugin.app.workspace.createLeafBySplit(leaf);
|
||||||
|
}
|
||||||
|
|
||||||
export const getObsidianImage = async (app: App, file: TFile)
|
export const getObsidianImage = async (app: App, file: TFile)
|
||||||
:Promise<{
|
:Promise<{
|
||||||
imageId: string,
|
mimeType: MimeType,
|
||||||
dataURL: string,
|
fileId: FileId,
|
||||||
|
dataURL: DataURL,
|
||||||
|
created: number,
|
||||||
size: {height: number, width: number},
|
size: {height: number, width: number},
|
||||||
}> => {
|
}> => {
|
||||||
if(!app || !file) return null;
|
if(!app || !file) return null;
|
||||||
if (!IMAGE_TYPES.contains(file.extension)) return null;
|
const isExcalidrawFile = window.ExcalidrawAutomate.isExcalidrawFile(file);
|
||||||
|
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const ab = await app.vault.readBinary(file);
|
const ab = await app.vault.readBinary(file);
|
||||||
|
const excalidrawSVG = isExcalidrawFile
|
||||||
|
? svgToBase64((await window.ExcalidrawAutomate.createSVG(file.path,true)).outerHTML) as DataURL
|
||||||
|
: null;
|
||||||
|
let mimeType:MimeType = "image/svg+xml";
|
||||||
|
if (!isExcalidrawFile) {
|
||||||
|
switch (file.extension) {
|
||||||
|
case "png": mimeType = "image/png";break;
|
||||||
|
case "jpeg":mimeType = "image/jpeg";break;
|
||||||
|
case "jpg": mimeType = "image/jpeg";break;
|
||||||
|
case "gif": mimeType = "image/gif";break;
|
||||||
|
case "svg": mimeType = "image/svg+xml";break;
|
||||||
|
default: mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
imageId: await generateIdFromFile(ab),
|
mimeType: mimeType,
|
||||||
dataURL: file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab),
|
fileId: await generateIdFromFile(ab),
|
||||||
size: await getImageSize(app,file)
|
dataURL: excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab)),
|
||||||
|
created: file.stat.mtime,
|
||||||
|
size: await getImageSize(app,excalidrawSVG??app.vault.getResourcePath(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSVGData = async (app: App, file: TFile): Promise<string> => {
|
|
||||||
|
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||||
const svg = await app.vault.read(file);
|
const svg = await app.vault.read(file);
|
||||||
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))))
|
return svgToBase64(svg) as DataURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
export const svgToBase64 = (svg:string):string => {
|
||||||
|
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))));
|
||||||
|
}
|
||||||
|
const getDataURL = async (file: ArrayBuffer): Promise<DataURL> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const dataURL = reader.result as string;
|
const dataURL = reader.result as DataURL;
|
||||||
resolve(dataURL);
|
resolve(dataURL);
|
||||||
};
|
};
|
||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
@@ -160,8 +248,8 @@ const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||||
let id: string;
|
let id: FileId;
|
||||||
try {
|
try {
|
||||||
const hashBuffer = await window.crypto.subtle.digest(
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
"SHA-1",
|
"SHA-1",
|
||||||
@@ -172,19 +260,159 @@ const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
|||||||
Array.from(new Uint8Array(hashBuffer))
|
Array.from(new Uint8Array(hashBuffer))
|
||||||
// convert to hex string
|
// convert to hex string
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("") as FileId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
id = nanoid(40);
|
id = nanoid(40) as FileId;
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageSize = async (app: App, file:TFile):Promise<{height:number, width:number}> => {
|
const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let img = new Image()
|
let img = new Image()
|
||||||
img.onload = () => resolve({height: img.height, width:img.width});
|
img.onload = () => resolve({height: img.height, width:img.width});
|
||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.src = app.vault.getResourcePath(file);
|
img.src = src;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getBinaryFileFromDataURL = (dataURL:string):ArrayBuffer => {
|
||||||
|
if(!dataURL) return null;
|
||||||
|
const parts = dataURL.matchAll(/base64,(.*)/g).next();
|
||||||
|
const binary_string = window.atob(parts.value[1]);
|
||||||
|
const len = binary_string.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePath:string, newFileName:string):Promise<[string,string]> => {
|
||||||
|
let folder = app.vault.getConfig("attachmentFolderPath");
|
||||||
|
// folder == null: save to vault root
|
||||||
|
// folder == "./" save to same folder as current file
|
||||||
|
// folder == "folder" save to specific folder in vault
|
||||||
|
// folder == "./folder" save to specific subfolder of current active folder
|
||||||
|
if(folder && folder.startsWith("./")) { // folder relative to current file
|
||||||
|
const activeFileFolder = splitFolderAndFilename(activeViewFilePath).folderpath + "/";
|
||||||
|
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||||
|
}
|
||||||
|
if(!folder) folder = "";
|
||||||
|
await checkAndCreateFolder(app.vault,folder);
|
||||||
|
return [folder,normalizePath(folder + "/" + newFileName)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> => {
|
||||||
|
try {
|
||||||
|
return exportToSvg({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
exportPadding:10,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateSVGString = async (scene:any, settings: ExcalidrawSettings):Promise<string> => {
|
||||||
|
const exportSettings: ExportSettings = {
|
||||||
|
withBackground: settings.exportWithBackground,
|
||||||
|
withTheme: settings.exportWithTheme
|
||||||
|
}
|
||||||
|
const svg = await getSVG(scene,exportSettings);
|
||||||
|
if(svg) {
|
||||||
|
|
||||||
|
return html_beautify(svg.outerHTML,{"indent_with_tabs": true});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:number = 1) => {
|
||||||
|
try {
|
||||||
|
return await Excalidraw.exportToBlob({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
mimeType: "image/png",
|
||||||
|
exportWithDarkMode: "true",
|
||||||
|
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||||
|
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
|
||||||
|
//replace font references with base64 fonts
|
||||||
|
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
||||||
|
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||||
|
const defs = svg.querySelector("defs");
|
||||||
|
if (defs && (includesCascadia || includesVirgil)) {
|
||||||
|
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||||
|
}
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const loadSceneFiles = async (app:App, filesMap: Map<FileId, string>,addFiles:Function) => {
|
||||||
|
const entries = filesMap.entries();
|
||||||
|
let entry;
|
||||||
|
let files:BinaryFileData[] = [];
|
||||||
|
while(!(entry = entries.next()).done) {
|
||||||
|
const file = app.vault.getAbstractFileByPath(entry.value[1]);
|
||||||
|
if(file && file instanceof TFile) {
|
||||||
|
const data = await getObsidianImage(app,file);
|
||||||
|
files.push({
|
||||||
|
mimeType : data.mimeType,
|
||||||
|
id: entry.value[0],
|
||||||
|
dataURL: data.dataURL,
|
||||||
|
created: data.created,
|
||||||
|
//@ts-ignore
|
||||||
|
size: data.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try { //in try block because by the time files are loaded the user may have closed the view
|
||||||
|
addFiles(files);
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
|
||||||
|
let dirty = false;
|
||||||
|
for(const f of files) {
|
||||||
|
const [w_image,h_image] = [f.size.width,f.size.height];
|
||||||
|
const imageAspectRatio = f.size.width/f.size.height;
|
||||||
|
scene
|
||||||
|
.elements
|
||||||
|
.filter((e:any)=>(e.type === "image" && e.fileId === f.id))
|
||||||
|
.forEach((el:any)=>{
|
||||||
|
const [w_old,h_old] = [el.width,el.height];
|
||||||
|
const elementAspectRatio = w_old/h_old;
|
||||||
|
if(imageAspectRatio != elementAspectRatio) {
|
||||||
|
dirty = true;
|
||||||
|
const h_new = Math.sqrt(w_old*h_old*h_image/w_image);
|
||||||
|
const w_new = Math.sqrt(w_old*h_old*w_image/h_image);
|
||||||
|
el.height = h_new;
|
||||||
|
el.width = w_new;
|
||||||
|
el.y += (h_old-h_new)/2;
|
||||||
|
el.x += (w_old-w_new)/2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [dirty,scene];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark");
|
||||||
@@ -15,6 +15,7 @@ export const MAX_COLORS = 5;
|
|||||||
export const COLOR_FREQ = 6;
|
export const COLOR_FREQ = 6;
|
||||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||||
|
export const DARK_BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||||
|
|||||||
@@ -1,162 +1,174 @@
|
|||||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
||||||
|
|
||||||
// English
|
// English
|
||||||
export default {
|
export default {
|
||||||
// main.ts
|
// main.ts
|
||||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||||
CREATE_NEW : "New Excalidraw drawing",
|
CREATE_NEW : "New Excalidraw drawing",
|
||||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||||
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
||||||
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||||
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
||||||
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
||||||
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
||||||
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
||||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
||||||
EXPORT_SVG: "Save as SVG next to the current file",
|
EXPORT_SVG: "Save as SVG next to the current file",
|
||||||
EXPORT_PNG: "Save as PNG next to the current file",
|
EXPORT_PNG: "Save as PNG next to the current file",
|
||||||
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||||
INSERT_LINK: "Insert link to file",
|
INSERT_LINK: "Insert link to file",
|
||||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||||
|
|
||||||
//ExcalidrawView.ts
|
//ExcalidrawView.ts
|
||||||
OPEN_AS_MD: "Open as Markdown",
|
OPEN_AS_MD: "Open as Markdown",
|
||||||
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
|
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/CMD+CLICK to export)",
|
||||||
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/CMD+CLICK to export)",
|
||||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+
|
||||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||||
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
|
'CTRL/CMD CLICK the Image or TextElement on the canvas has the same effect!',
|
||||||
TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||||
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||||
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||||
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
||||||
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
||||||
NOFILE: "Excalidraw (no file)",
|
NOFILE: "Excalidraw (no file)",
|
||||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||||
CONVERT_FILE: "Convert to new format",
|
CONVERT_FILE: "Convert to new format",
|
||||||
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||||
"loading Markdown View may take a while. Please be patient. ",
|
"loading Markdown View may take a while. Please be patient. ",
|
||||||
|
|
||||||
//settings.ts
|
//settings.ts
|
||||||
FOLDER_NAME: "Excalidraw folder",
|
FOLDER_NAME: "Excalidraw folder",
|
||||||
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||||
TEMPLATE_NAME: "Excalidraw template file",
|
TEMPLATE_NAME: "Excalidraw template file",
|
||||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
||||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
||||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
||||||
"such as Excalidraw/Template.excalidraw.",
|
"such as Excalidraw/Template.excalidraw.",
|
||||||
AUTOSAVE_NAME: "Autosave",
|
AUTOSAVE_NAME: "Autosave",
|
||||||
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
||||||
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
||||||
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
||||||
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
||||||
FILENAME_HEAD: "Filename",
|
FILENAME_HEAD: "Filename",
|
||||||
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
||||||
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
||||||
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||||
"date and time format reference</a>.</p>",
|
"date and time format reference</a>.</p>",
|
||||||
FILENAME_SAMPLE: "The current file format is: <b>",
|
FILENAME_SAMPLE: "The current file format is: <b>",
|
||||||
FILENAME_PREFIX_NAME: "Filename prefix",
|
FILENAME_PREFIX_NAME: "Filename prefix",
|
||||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||||
FILENAME_DATE_NAME: "Filename date",
|
FILENAME_DATE_NAME: "Filename date",
|
||||||
FILENAME_DATE_DESC: "The second part of the filename",
|
FILENAME_DATE_DESC: "The second part of the filename",
|
||||||
DISPLAY_HEAD: "Display",
|
DISPLAY_HEAD: "Display",
|
||||||
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||||
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
|
||||||
LINKS_HEAD: "Links and transclusion",
|
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
|
||||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
MATCH_THEME_ALWAYS_NAME: "Existing drawings to match Obsidian theme",
|
||||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
MATCH_THEME_ALWAYS_DESC: "If theme is dark, drawings will be opened in dark mode. If your theme is light, they will be opened in light mode. ",
|
||||||
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||||
"the plugin will open it in a browser. " +
|
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
|
||||||
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
ZOOM_TO_FIT_MAX_LEVEL_DESC: "Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
|
||||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
LINKS_HEAD: "Links and transclusion",
|
||||||
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
LINKS_DESC: "CTRL/CMD + CLICK on Text Elements to open them as links. " +
|
||||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||||
": true/false' to the file\'s frontmatter.",
|
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
||||||
LINK_PREFIX_NAME:"Link prefix",
|
"the plugin will open it in a browser. " +
|
||||||
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
||||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||||
': "📍 "\' to the file\'s frontmatter.',
|
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||||
URL_PREFIX_NAME:"URL prefix",
|
ADJACENT_PANE_DESC: "When CTRL/CMD+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
|
||||||
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
|
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
|
||||||
': "🌐 "\' to the file\'s frontmatter.',
|
"a new pane.",
|
||||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||||
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
||||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||||
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
": true/false' to the file\'s frontmatter.",
|
||||||
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
LINK_PREFIX_NAME:"Link prefix",
|
||||||
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||||
"text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).",
|
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
': "📍 "\' to the file\'s frontmatter.',
|
||||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
URL_PREFIX_NAME:"URL prefix",
|
||||||
"![[markdown page]] format.",
|
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
||||||
EMBED_HEAD: "Embed & Export",
|
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
|
||||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
': "🌐 "\' to the file\'s frontmatter.',
|
||||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
LINK_CTRL_CLICK_NAME: "CTRL/CMD + CLICK on text to open them as links",
|
||||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||||
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||||
"[[drawing.excalidraw|100x100]] format.",
|
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||||
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||||
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
"text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).",
|
||||||
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||||
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
"![[markdown page]] format.",
|
||||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
EMBED_HEAD: "Embed & Export",
|
||||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||||
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||||
EXPORT_THEME_NAME: "Export image with theme",
|
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
||||||
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||||
"drawings created in drak mode will appear as they would in light mode.",
|
"[[drawing.excalidraw|100x100]] format.",
|
||||||
EXPORT_HEAD: "Export Settings",
|
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
||||||
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
||||||
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||||
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||||
EXPORT_SVG_NAME: "Auto-export SVG",
|
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||||
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||||
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||||
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||||
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
||||||
EXPORT_PNG_NAME: "Auto-export PNG",
|
EXPORT_THEME_NAME: "Export image with theme",
|
||||||
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
||||||
COMPATIBILITY_HEAD: "Compatibility features",
|
"drawings created in drak mode will appear as they would in light mode.",
|
||||||
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
EXPORT_HEAD: "Export Settings",
|
||||||
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
||||||
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
||||||
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
||||||
"then update the drawing in the .md file based on the .excalidraw file",
|
EXPORT_SVG_NAME: "Auto-export SVG",
|
||||||
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
||||||
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
||||||
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
||||||
"when you open a legacy file for editing.",
|
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
||||||
EXPERIMENTAL_HEAD: "Experimental features",
|
EXPORT_PNG_NAME: "Auto-export PNG",
|
||||||
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
||||||
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
COMPATIBILITY_HEAD: "Compatibility features",
|
||||||
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
||||||
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
||||||
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
||||||
INSERT_EMOJI: "Insert an emoji",
|
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
||||||
|
"then update the drawing in the .md file based on the .excalidraw file",
|
||||||
|
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
||||||
//openDrawings.ts
|
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
||||||
SELECT_FILE: "Select a file then press enter.",
|
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
||||||
NO_MATCH: "No file matches your query.",
|
"when you open a legacy file for editing.",
|
||||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
EXPERIMENTAL_HEAD: "Experimental features",
|
||||||
TYPE_FILENAME: "Type name of drawing to select.",
|
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
||||||
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
||||||
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
||||||
};
|
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
||||||
|
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
||||||
|
INSERT_EMOJI: "Insert an emoji",
|
||||||
|
|
||||||
|
|
||||||
|
//openDrawings.ts
|
||||||
|
SELECT_FILE: "Select a file then press enter.",
|
||||||
|
NO_MATCH: "No file matches your query.",
|
||||||
|
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||||
|
TYPE_FILENAME: "Type name of drawing to select.",
|
||||||
|
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
||||||
|
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
||||||
|
};
|
||||||
@@ -29,13 +29,13 @@ export default {
|
|||||||
|
|
||||||
//ExcalidrawView.ts
|
//ExcalidrawView.ts
|
||||||
OPEN_AS_MD: "打开为 Markdown 文件",
|
OPEN_AS_MD: "打开为 Markdown 文件",
|
||||||
SAVE_AS_PNG: "保存成 PNG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
SAVE_AS_PNG: "保存成 PNG 文件到库里(CTRL/CMD 加左键点击来指定导出位置)",
|
||||||
SAVE_AS_SVG: "保存成 SVG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
SAVE_AS_SVG: "保存成 SVG 文件到库里(CTRL/CMD 加左键点击来指定导出位置)",
|
||||||
OPEN_LINK: "以链接的方式打开文本 \n(按住 SHIFT 来在新面板中打开)",
|
OPEN_LINK: "以链接的方式打开文本 \n(按住 SHIFT 来在新面板中打开)",
|
||||||
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
|
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
|
||||||
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
|
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
|
||||||
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
|
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
|
||||||
'CTRL/META 加左键在画布中点击文本元素也可以打开对应的链接。',
|
'CTRL/CMD 加左键在画布中点击文本元素也可以打开对应的链接。',
|
||||||
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
|
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
|
||||||
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
|
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
|
||||||
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT(或者 ALT + SHIFT)加左键点击来创建新文件。",
|
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT(或者 ALT + SHIFT)加左键点击来创建新文件。",
|
||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
FILENAME_DATE_NAME: "文件名日期",
|
FILENAME_DATE_NAME: "文件名日期",
|
||||||
FILENAME_DATE_DESC: "文件名的第二部分",
|
FILENAME_DATE_DESC: "文件名的第二部分",
|
||||||
LINKS_HEAD: "链接",
|
LINKS_HEAD: "链接",
|
||||||
LINKS_DESC: "CTRL/META 加左键点击文本元素来打开链接。" +
|
LINKS_DESC: "CTRL/CMD 加左键点击文本元素来打开链接。" +
|
||||||
"如果选中的文本指向多个双链,只会打开其中第一个。" +
|
"如果选中的文本指向多个双链,只会打开其中第一个。" +
|
||||||
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
|
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
|
||||||
"插件会在浏览器中打开超链接。" +
|
"插件会在浏览器中打开超链接。" +
|
||||||
@@ -85,7 +85,7 @@ export default {
|
|||||||
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
|
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
|
||||||
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||||
': "👉 "\' 单独更改',
|
': "👉 "\' 单独更改',
|
||||||
LINK_CTRL_CLICK_NAME: "CTRL 加左键点击文本来打开链接",
|
LINK_CTRL_CLICK_NAME: "CTRL/CMD 加左键点击文本来打开链接",
|
||||||
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的 Excalidraw 功能,您可以将其关闭。 如果" +
|
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的 Excalidraw 功能,您可以将其关闭。 如果" +
|
||||||
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
|
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
|
||||||
EMBED_HEAD: "嵌入 & 导出",
|
EMBED_HEAD: "嵌入 & 导出",
|
||||||
|
|||||||
145
src/main.ts
145
src/main.ts
@@ -31,10 +31,11 @@ import {
|
|||||||
FRONTMATTER_KEY,
|
FRONTMATTER_KEY,
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
JSON_parse,
|
JSON_parse,
|
||||||
nanoid
|
nanoid,
|
||||||
|
DARK_BLANK_DRAWING
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
||||||
import {getJSON} from "./ExcalidrawData";
|
import {getJSON, getSVGString} from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
ExcalidrawSettings,
|
ExcalidrawSettings,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
@@ -54,16 +55,13 @@ import {
|
|||||||
import { Prompt } from "./Prompt";
|
import { Prompt } from "./Prompt";
|
||||||
import { around } from "monkey-around";
|
import { around } from "monkey-around";
|
||||||
import { t } from "./lang/helpers";
|
import { t } from "./lang/helpers";
|
||||||
import { MigrationPrompt } from "./MigrationPrompt";
|
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, isObsidianThemeDark, splitFolderAndFilename, svgToBase64 } from "./Utils";
|
||||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
import { OneOffs } from "./OneOffs";
|
||||||
|
|
||||||
declare module "obsidian" {
|
declare module "obsidian" {
|
||||||
interface App {
|
interface App {
|
||||||
isMobile():boolean;
|
isMobile():boolean;
|
||||||
}
|
}
|
||||||
interface Vault {
|
|
||||||
getConfig(option:"attachmentFolderPath"): string;
|
|
||||||
}
|
|
||||||
interface Workspace {
|
interface Workspace {
|
||||||
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
||||||
}
|
}
|
||||||
@@ -108,53 +106,24 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
|
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
|
||||||
this.registerCommands();
|
this.registerCommands();
|
||||||
this.registerEventListeners();
|
this.registerEventListeners();
|
||||||
|
|
||||||
//inspiration taken from kanban:
|
//inspiration taken from kanban:
|
||||||
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
||||||
this.registerMonkeyPatches();
|
this.registerMonkeyPatches();
|
||||||
if(this.settings.loadCount<1) this.migrationNotice();
|
|
||||||
const electron:string = process.versions.electron;
|
|
||||||
if(electron.startsWith("8.")) {
|
|
||||||
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
|
|
||||||
}
|
|
||||||
this.switchToExcalidarwAfterLoad()
|
|
||||||
|
|
||||||
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
|
if(!this.app.isMobile) {
|
||||||
if(this.settings.patchCommentBlock) {
|
const electron:string = process?.versions?.electron;
|
||||||
const self = this;
|
if(electron && electron?.startsWith("8.")) {
|
||||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
|
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
|
||||||
setTimeout(async ()=>{
|
}
|
||||||
await self.loadSettings();
|
|
||||||
if (!self.settings.patchCommentBlock) {
|
|
||||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
|
|
||||||
let i = 0;
|
|
||||||
const excalidrawFiles = this.app.vault.getFiles();
|
|
||||||
for (const f of (excalidrawFiles || []).filter((f:TFile) => self.isExcalidrawFile(f))) {
|
|
||||||
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
|
|
||||||
&& (self.app.workspace.getActiveFile() !== f)) { //file is currently being edited
|
|
||||||
let drawing = await self.app.vault.read(f);
|
|
||||||
const orig_drawing = drawing;
|
|
||||||
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
|
|
||||||
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
|
|
||||||
if (drawing.search("\n%%\n# Drawing\n") === -1) {
|
|
||||||
const [json,pos] = getJSON(drawing);
|
|
||||||
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
|
|
||||||
};
|
|
||||||
if (drawing !== orig_drawing) {
|
|
||||||
i++;
|
|
||||||
console.log("Excalidraw patched: " + f.path);
|
|
||||||
await self.app.vault.modify(f,drawing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.settings.patchCommentBlock = false;
|
|
||||||
self.saveSettings();
|
|
||||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
|
|
||||||
},300000) //5 minutes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const patches = new OneOffs(this);
|
||||||
|
patches.migrationNotice();
|
||||||
|
patches.patchCommentBlock();
|
||||||
|
patches.imageElementLaunchNotice();
|
||||||
|
|
||||||
|
this.switchToExcalidarwAfterLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
private switchToExcalidarwAfterLoad() {
|
private switchToExcalidarwAfterLoad() {
|
||||||
@@ -171,19 +140,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private migrationNotice(){
|
|
||||||
const self = this;
|
|
||||||
this.app.workspace.onLayoutReady(async () => {
|
|
||||||
self.settings.loadCount++;
|
|
||||||
self.saveSettings();
|
|
||||||
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
|
|
||||||
if(files.length>0) {
|
|
||||||
const prompt = new MigrationPrompt(self.app, self);
|
|
||||||
prompt.open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a transcluded .excalidraw image in markdown preview mode
|
* Displays a transcluded .excalidraw image in markdown preview mode
|
||||||
*/
|
*/
|
||||||
@@ -222,24 +178,39 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
||||||
img.addClass(imgAttributes.style);
|
img.addClass(imgAttributes.style);
|
||||||
|
|
||||||
|
const [scene,pos] = getJSON(content);
|
||||||
|
const svgSnapshot = getSVGString(content.substr(pos+scene.length));
|
||||||
|
|
||||||
if(!this.settings.displaySVGInPreview) {
|
//Removed in 1.4.0 when implementing ImageElement. Key reason for removing this
|
||||||
|
//is to use SVG snapshot in file, to avoid resource intensive process to generating PNG
|
||||||
|
//due to the need to load excalidraw plus all linked images
|
||||||
|
/* if(!this.settings.displaySVGInPreview) {
|
||||||
const width = parseInt(imgAttributes.fwidth);
|
const width = parseInt(imgAttributes.fwidth);
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
if(width>=800) scale = 2;
|
if(width>=800) scale = 2;
|
||||||
if(width>=1600) scale = 3;
|
if(width>=1600) scale = 3;
|
||||||
if(width>=2400) scale = 4;
|
if(width>=2400) scale = 4;
|
||||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)[0]),exportSettings, scale);
|
const png = await getPNG(JSON_parse(scene),exportSettings, scale);
|
||||||
if(!png) return null;
|
if(!png) return null;
|
||||||
img.src = URL.createObjectURL(png);
|
img.src = URL.createObjectURL(png);
|
||||||
return img;
|
return img;
|
||||||
|
}*/
|
||||||
|
let svg:SVGSVGElement = null;
|
||||||
|
if(svgSnapshot) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = svgSnapshot;
|
||||||
|
const firstChild = el.firstChild;
|
||||||
|
if(firstChild instanceof SVGSVGElement) {
|
||||||
|
svg=firstChild;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
svg = await getSVG(JSON_parse(scene),exportSettings);
|
||||||
}
|
}
|
||||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)[0]),exportSettings);
|
|
||||||
if(!svg) return null;
|
if(!svg) return null;
|
||||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
svg = embedFontsInSVG(svg);
|
||||||
svg.removeAttribute('width');
|
svg.removeAttribute('width');
|
||||||
svg.removeAttribute('height');
|
svg.removeAttribute('height');
|
||||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
img.setAttribute("src",svgToBase64(svg.outerHTML));
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,21 +546,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
||||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
if(!activeView) return;
|
if(!activeView) return;
|
||||||
let folder = this.app.vault.getConfig("attachmentFolderPath");
|
|
||||||
// folder == null: save to vault root
|
|
||||||
// folder == "./" save to same folder as current file
|
|
||||||
// folder == "folder" save to specific folder in vault
|
|
||||||
// folder == "./folder" save to specific subfolder of current active folder
|
|
||||||
if(folder && folder.startsWith("./")) { // folder relative to current file
|
|
||||||
const activeFileFolder = splitFolderAndFilename(activeView.file.path).folderpath + "/";
|
|
||||||
folder = normalizePath(activeFileFolder + folder.substring(2));
|
|
||||||
}
|
|
||||||
if(!folder) folder = "";
|
|
||||||
await checkAndCreateFolder(this.app.vault,folder);
|
|
||||||
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
||||||
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
||||||
this.embedDrawing(normalizePath(folder + "/" + filename));
|
const [folder, filepath] = await getAttachmentsFolderAndFilePath(this.app,activeView.file.path,filename);
|
||||||
this.createDrawing(filename, inNewPane,folder==""?null:folder);
|
this.embedDrawing(filepath);
|
||||||
|
this.createDrawing(filename, inNewPane, folder===""?null:folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -785,7 +746,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
||||||
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
||||||
console.log(fname);
|
console.log(fname);
|
||||||
const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data));
|
const result = await this.app.vault.create(fname,FRONTMATTER + await this.exportSceneToMD(data));
|
||||||
if (this.settings.keepInSync) {
|
if (this.settings.keepInSync) {
|
||||||
['.svg','.png'].forEach( (ext:string)=>{
|
['.svg','.png'].forEach( (ext:string)=>{
|
||||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||||
@@ -1101,16 +1062,24 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.settings.compatibilityMode) {
|
if (this.settings.compatibilityMode) {
|
||||||
return BLANK_DRAWING;
|
return this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
}
|
}
|
||||||
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(BLANK_DRAWING);
|
const blank = this.settings.matchTheme && isObsidianThemeDark() ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
|
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,'<SVG></SVG>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMarkdownDrawingSection(jsonString: string) {
|
public getMarkdownDrawingSection(jsonString: string,svgString: string) {
|
||||||
return '%%\n# Drawing\n'
|
return '%%\n# Drawing\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||||
+ jsonString + '\n'
|
+ jsonString + '\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '%%';
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
+ (svgString ?
|
||||||
|
'\n\n# SVG snapshot\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'html\n'
|
||||||
|
+ svgString + '\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
: '')
|
||||||
|
+ '\n%%';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1118,9 +1087,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
* @param {string} data - Excalidraw scene JSON string
|
* @param {string} data - Excalidraw scene JSON string
|
||||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||||
*/
|
*/
|
||||||
public exportSceneToMD(data:string): string {
|
public async exportSceneToMD(data:string): Promise<string> {
|
||||||
if(!data) return "";
|
if(!data) return "";
|
||||||
const excalidrawData = JSON_parse(data);
|
const excalidrawData = JSON_parse(data);
|
||||||
|
const svgString = await generateSVGString(excalidrawData,this.settings);
|
||||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||||
let outString = '# Text Elements\n';
|
let outString = '# Text Elements\n';
|
||||||
let id:string;
|
let id:string;
|
||||||
@@ -1135,7 +1105,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
outString += te.text+' ^'+id+'\n\n';
|
outString += te.text+' ^'+id+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data)));
|
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"),svgString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
||||||
@@ -1179,3 +1149,4 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ export interface ExcalidrawSettings {
|
|||||||
templateFilePath: string,
|
templateFilePath: string,
|
||||||
drawingFilenamePrefix: string,
|
drawingFilenamePrefix: string,
|
||||||
drawingFilenameDateTime: string,
|
drawingFilenameDateTime: string,
|
||||||
displaySVGInPreview: boolean,
|
//displaySVGInPreview: boolean,
|
||||||
width: string,
|
width: string,
|
||||||
|
matchTheme: boolean,
|
||||||
|
matchThemeAlways: boolean,
|
||||||
zoomToFitOnResize: boolean,
|
zoomToFitOnResize: boolean,
|
||||||
|
zoomToFitMaxLevel: number,
|
||||||
|
openInAdjacentPane: boolean,
|
||||||
showLinkBrackets: boolean,
|
showLinkBrackets: boolean,
|
||||||
linkPrefix: string,
|
linkPrefix: string,
|
||||||
urlPrefix: string,
|
urlPrefix: string,
|
||||||
@@ -40,6 +44,7 @@ export interface ExcalidrawSettings {
|
|||||||
drawingOpenCount: number,
|
drawingOpenCount: number,
|
||||||
library: string,
|
library: string,
|
||||||
patchCommentBlock: boolean, //1.3.12
|
patchCommentBlock: boolean, //1.3.12
|
||||||
|
imageElementNotice: boolean, //1.4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||||
@@ -47,11 +52,15 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||||
drawingFilenamePrefix: 'Drawing ',
|
drawingFilenamePrefix: 'Drawing ',
|
||||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||||
displaySVGInPreview: true,
|
//displaySVGInPreview: true,
|
||||||
width: '400',
|
width: '400',
|
||||||
|
matchTheme: false,
|
||||||
|
matchThemeAlways: false,
|
||||||
zoomToFitOnResize: true,
|
zoomToFitOnResize: true,
|
||||||
|
zoomToFitMaxLevel: 2,
|
||||||
linkPrefix: "📍",
|
linkPrefix: "📍",
|
||||||
urlPrefix: "🌐",
|
urlPrefix: "🌐",
|
||||||
|
openInAdjacentPane: false,
|
||||||
showLinkBrackets: true,
|
showLinkBrackets: true,
|
||||||
allowCtrlClick: true,
|
allowCtrlClick: true,
|
||||||
forceWrap: false,
|
forceWrap: false,
|
||||||
@@ -72,6 +81,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
drawingOpenCount: 0,
|
drawingOpenCount: 0,
|
||||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||||
patchCommentBlock: true,
|
patchCommentBlock: true,
|
||||||
|
imageElementNotice: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||||
@@ -189,6 +199,27 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("MATCH_THEME_NAME"))
|
||||||
|
.setDesc(t("MATCH_THEME_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.matchTheme)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.matchTheme = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("MATCH_THEME_ALWAYS_NAME"))
|
||||||
|
.setDesc(t("MATCH_THEME_ALWAYS_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.matchThemeAlways)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.matchThemeAlways = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("ZOOM_TO_FIT_NAME"))
|
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||||
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||||
@@ -199,10 +230,41 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
this.applySettingsUpdate();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let zoomText:HTMLDivElement;
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ZOOM_TO_FIT_MAX_LEVEL_NAME"))
|
||||||
|
.setDesc(t("ZOOM_TO_FIT_MAX_LEVEL_DESC"))
|
||||||
|
.addSlider(slider => slider
|
||||||
|
.setLimits(0.5,10,0.5)
|
||||||
|
.setValue(this.plugin.settings.zoomToFitMaxLevel)
|
||||||
|
.onChange(async (value)=> {
|
||||||
|
zoomText.innerText = " " + value.toString();
|
||||||
|
this.plugin.settings.zoomToFitMaxLevel = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}))
|
||||||
|
.settingEl.createDiv('',(el)=>{
|
||||||
|
zoomText = el;
|
||||||
|
el.style.minWidth = "2.3em";
|
||||||
|
el.style.textAlign = "right";
|
||||||
|
el.innerText = " " + this.plugin.settings.zoomToFitMaxLevel.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||||
this.containerEl.createEl('p',{
|
this.containerEl.createEl('p',{
|
||||||
text: t("LINKS_DESC")});
|
text: t("LINKS_DESC")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ADJACENT_PANE_NAME"))
|
||||||
|
.setDesc(t("ADJACENT_PANE_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.openInAdjacentPane)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.openInAdjacentPane = value;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("LINK_BRACKETS_NAME"))
|
.setName(t("LINK_BRACKETS_NAME"))
|
||||||
.setDesc(t("LINK_BRACKETS_DESC"))
|
.setDesc(t("LINK_BRACKETS_DESC"))
|
||||||
@@ -282,8 +344,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||||
|
|
||||||
|
//Removed in 1.4.0 when implementing ImageElement.
|
||||||
new Setting(containerEl)
|
/* new Setting(containerEl)
|
||||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||||
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
@@ -291,8 +353,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.displaySVGInPreview = value;
|
this.plugin.settings.displaySVGInPreview = value;
|
||||||
this.applySettingsUpdate();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("EMBED_WIDTH_NAME"))
|
.setName(t("EMBED_WIDTH_NAME"))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"1.3.16": "0.11.13"
|
"1.4.0": "0.11.13"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user