Compare commits

...

35 Commits

Author SHA1 Message Date
dependabot[bot]
5b203a6757 Bump @codemirror/view from 6.36.8 to 6.38.1
---
updated-dependencies:
- dependency-name: "@codemirror/view"
  dependency-version: 6.38.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-05 17:27:01 +00:00
zsviczian
6689d9497c 2.14.1
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-08-05 19:17:20 +02:00
zsviczian
3e86419421 Merge pull request #2431 from dmscode/zh-2025-08-02
Update zh-cn.ts to e574d84
2025-08-05 18:16:52 +02:00
dmscode
d2d4f16ebd Update zh-cn.ts to e574d84 2025-08-02 06:48:05 +08:00
zsviczian
e574d84b84 2.14.1-beta-2, 0.18.0-27 2025-08-01 20:49:47 +02:00
zsviczian
8f3ace89ea Merge pull request #2427 from dmscode/zh-2025-07-29
Update zh-cn.ts to 41065ce
2025-07-29 18:42:56 +02:00
zsviczian
e4e4a07aa8 Merge pull request #2426 from Joakim31/master
Update es.ts to 01b0b64a
2025-07-29 07:49:43 +02:00
dmscode
7a90bf753f Update zh-cn.ts to 41065ce 2025-07-29 13:30:36 +08:00
Joakim31
6221d985ac Update es.ts to 01b0b64a 2025-07-28 20:10:04 -05:00
zsviczian
41065ce9f9 2.14.1-beta-1, 0.18.0-26 2025-07-28 21:48:44 +02:00
zsviczian
351977f0d3 Merge pull request #2421 from mnaoumov/custom-attachment-location
Use internal `app.vault.getAvailablePathForAttachments()`
2025-07-28 19:54:08 +02:00
zsviczian
01b0b64a08 Merge pull request #2425 from Joakim31/master
Add Spanish translation
2025-07-28 19:37:38 +02:00
Joakim31
938e93cc43 Add Spanish translation 2025-07-27 22:56:57 -05:00
Michael Naumov
cebcd2b43c Use internal getAvailablePathForAttachments
Fixes https://github.com/RainCat1998/obsidian-custom-attachment-location/issues/179
2025-07-23 22:42:02 -06:00
zsviczian
80b6e30040 shade master help converted to Markdown
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-23 22:33:40 +02:00
zsviczian
6e92a6a399 fixed svg 2025-07-23 20:25:41 +02:00
zsviczian
992ae8b238 update index 2025-07-23 20:23:23 +02:00
zsviczian
5cd07e7766 update index 2025-07-23 20:22:29 +02:00
zsviczian
07817a4d2d publish script 2025-07-23 20:18:29 +02:00
zsviczian
a22bb4c58c images 2025-07-23 19:56:37 +02:00
zsviczian
43e00a51be images 2025-07-23 19:55:01 +02:00
zsviczian
049b5bfa85 2.14.0 2025-07-22 08:45:29 +02:00
zsviczian
743afa73b4 Merge pull request #2413 from rlan/master
Add Taiwan-idiomatic Traditional Chinese
2025-07-22 08:17:25 +02:00
zsviczian
6469eec051 added NotebookLM link 2025-07-22 06:25:12 +02:00
zsviczian
9f46821a41 Merge pull request #2412 from dmscode/zh-2025-07-20
Update zh-cn.ts to 207fea3
2025-07-21 10:07:01 +02:00
dmscode
1fb3a47bdc Update zh-cn.ts to 207fea3 2025-07-21 14:08:59 +08:00
稻米鼠
bcd0cdda65 Merge pull request #1 from PlayerMiller109/zh-2025-07-20
Supplement (2.13.0...2.13.1)
2025-07-21 13:56:57 +08:00
zsviczian
207fea3f57 ContentSearcher.ts, added to ScriptInstallPrompt.ts and Settings.ts 2025-07-20 21:07:03 +02:00
Rick Lan
e55ba3cc21 Add Taiwan-idiomatic Traditional Chinese 2025-07-20 20:54:04 +09:00
PlayerMiller109
71c87a7630 Supplement (2.13.0...2.13.1) 2025-07-20 10:31:38 +08:00
dmscode
e598a91f43 Update zh-cn.ts to 972fe1b 2025-07-20 06:09:52 +08:00
zsviczian
972fe1baea copy settings to the clipboard
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-19 18:43:01 +02:00
zsviczian
d7e1268afa updated info links in settings 2025-07-19 17:45:05 +02:00
zsviczian
7a3b937ea7 Merge pull request #2411 from dmscode/zh-2025-07-19
Fixed for issue 2175
2025-07-19 08:17:27 +02:00
dmscode
c850cd15ae Fixed for issue 2175 2025-07-19 10:02:10 +08:00
32 changed files with 13295 additions and 9808 deletions

View File

@@ -0,0 +1,660 @@
/*
Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-01.png)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-02.png)
```js
*/
async function run() {
// Help text for the script
const HELP_TEXT = `
**Easily split your Excalidraw drawing into printable pages!**
If you find this script helpful, consider [buying me a coffee](https://ko-fi.com/zsolt). Thank you.
---
### How it works
- **Define Pages:** Use frames to mark out each page area in your drawing. You can create the first frame with this script (choose a standard size or orientation), or draw your own frame for a custom page size.
- **Add More Pages:** Select a frame, then use the arrow buttons to add new frames next to it. All new frames will match the size of the selected one.
- **Rename Frames:** You can rename frames as you like. When exporting to PDF, pages will be ordered alphabetically by frame name.
---
### Important Notes
- **Same Size & Orientation:** All frames must have the same size and orientation (e.g., all A4 Portrait) to export to PDF. Excalidraw currently does not support PDFs with different-sized pages.
- **Custom Sizes:** If you draw your own frame, the PDF will use that exact size—great for custom page layouts!
- **Margins:** If you set a margin, the page size stays the same, but your content will shrink to fit inside the printable area.
- **No Frame Borders/Titles in Print:** Frame borders and frame titles will *not* appear in the PDF.
- **No Frame Clipping:** The script disables frame clipping for this drawing.
- **Templates:** You can save a template document with prearranged frames (even locked ones) for reuse.
- **Lock Frames:** Frames only define print areas—they don't "contain" elements. Locking frames is recommended to prevent accidental movement.
- **Outside Content:** Anything outside the frames will *not* appear in the PDF.
---
### Printing
- **Export to PDF:** Click the printer button to export each frame as a separate page in a PDF.
- **Order:** Pages are exported in alphabetical order of frame names.
---
### Settings
You can also access script settings at the bottom of Excalidraw Plugin settings. The script stores your preferences for:
- Locking new frames after creation
- Zooming to new frames
- Closing the dialog after adding a frame
- Default page size and orientation
- Print margin
---
**Tip:** For more on templates, see [Mastering Excalidraw Templates](https://youtu.be/jgUpYznHP9A). For referencing pages in markdown, see [Image Fragments](https://youtu.be/sjZfdqpxqsg) and [Image Block References](https://youtu.be/yZQoJg2RCKI).
`;
// Enable frame rendering
const st = ea.getExcalidrawAPI().getAppState();
const {enabled, clip, name, outline} = st.frameRendering;
if(!enabled || clip || !name || !outline) {
ea.viewUpdateScene({
appState: {
frameRendering: {
enabled: true,
clip: false,
name: true,
outline: true
}
}
});
}
// Page size options (using standard sizes from ExcalidrawAutomate)
const PAGE_SIZES = [
"A0", "A1", "A2", "A3", "A4", "A5", "A6",
"Letter", "Legal", "Tabloid", "Ledger"
];
const PAGE_ORIENTATIONS = ["portrait", "landscape"];
// Margin sizes in points
const MARGINS = {
"none": 0,
"tiny": 10,
"normal": 60,
};
// Initialize settings
let settings = ea.getScriptSettings();
let dirty = false;
// Define setting keys
const PAGE_SIZE = "Page size";
const ORIENTATION = "Page orientation";
const MARGIN = "Print-margin";
const LOCK_FRAME = "Lock frame after it is created";
const SHOULD_ZOOM = "Should zoom after adding page";
const SHOULD_CLOSE = "Should close after adding page";
// Set default values on first run
if (!settings[PAGE_SIZE]) {
settings = {};
settings[PAGE_SIZE] = { value: "A4", valueSet: PAGE_SIZES };
settings[ORIENTATION] = { value: "portrait", valueSet: PAGE_ORIENTATIONS };
settings[MARGIN] = { value: "none", valueSet: Object.keys(MARGINS)};
settings[SHOULD_ZOOM] = { value: false };
settings[SHOULD_CLOSE] = { value: false };
settings[LOCK_FRAME] = { value: true };
await ea.setScriptSettings(settings);
}
const getSortedFrames = () => ea.getViewElements()
.filter(el => el.type === "frame")
.sort((a, b) => {
const nameA = a.name || "";
const nameB = b.name || "";
return nameA.localeCompare(nameB);
});
// Find existing page frames and determine next page number
const findExistingPages = (selectLastFrame = false) => {
const frameElements = getSortedFrames();
// Extract page numbers from frame names
const pageNumbers = frameElements
.map(frame => {
const match = frame.name?.match(/Page\s+(\d+)/i);
return match ? parseInt(match[1]) : 0;
})
.filter(num => !isNaN(num));
// Find the highest page number
const nextPageNumber = pageNumbers.length > 0
? Math.max(...pageNumbers) + 1
: 1;
if(selectLastFrame && frameElements.length > 0) {
ea.selectElementsInView([frameElements[frameElements.length-1]]);
}
return {
frames: frameElements,
nextPageNumber: nextPageNumber
};
};
// Check if there are frames in the scene and if a frame is selected
let existingFrames = ea.getViewElements().filter(el => el.type === "frame");
let selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
const hasFrames = existingFrames.length > 0;
if(hasFrames && !selectedFrame) {
if(st.activeLockedId && existingFrames.find(f=>f.id === st.activeLockedId)) {
selectedFrame = existingFrames.find(f=>f.id === st.activeLockedId);
ea.viewUpdateScene({ appState: { activeLockedId: null }});
ea.selectElementsInView([selectedFrame]);
} else {
findExistingPages(true);
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
}
}
const hasSelectedFrame = !!selectedFrame;
const modal = new ea.FloatingModal(ea.plugin.app);
let lockFrame = !!settings[LOCK_FRAME]?.value;
let shouldClose = settings[SHOULD_CLOSE].value;
let shouldZoom = settings[SHOULD_ZOOM].value;
// Show notice if there are frames but none selected
if (hasFrames && !hasSelectedFrame) {
new Notice("Select a frame before running the script", 7000);
return;
}
// Create the first frame
const createFirstFrame = async (pageSize, orientation) => {
// Use ExcalidrawAutomate's built-in function to get page dimensions
const dimensions = ea.getPagePDFDimensions(pageSize, orientation);
if (!dimensions) {
new Notice("Invalid page size selected");
return;
}
// Save settings when creating first frame
if (settings[PAGE_SIZE].value !== pageSize) {
settings[PAGE_SIZE].value = pageSize;
dirty = true;
}
if (settings[ORIENTATION].value !== orientation) {
settings[ORIENTATION].value = orientation;
dirty = true;
}
// Format page number with leading zero
const pageName = "Page 01";
// Calculate position to center the frame
const appState = ea.getExcalidrawAPI().getAppState();
const x = (appState.width - dimensions.width) / 2;
const y = (appState.height - dimensions.height) / 2;
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName, true);
};
// Add new page frame
const addPage = async (direction, pageSize, orientation) => {
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
if (!selectedFrame) return;
const { frames, nextPageNumber } = findExistingPages();
// Get dimensions from selected frame
const dimensions = {
width: selectedFrame.width,
height: selectedFrame.height
};
// Format page number with leading zero
const pageName = `Page ${nextPageNumber.toString().padStart(2, '0')}`;
// Calculate position based on direction and selected frame
let x = 0;
let y = 0;
switch (direction) {
case "right":
x = selectedFrame.x + selectedFrame.width;
y = selectedFrame.y;
break;
case "left":
x = selectedFrame.x - dimensions.width;
y = selectedFrame.y;
break;
case "down":
x = selectedFrame.x;
y = selectedFrame.y + selectedFrame.height;
break;
case "up":
x = selectedFrame.x;
y = selectedFrame.y - dimensions.height;
break;
}
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName);
};
addFrameElement = async (x, y, width, height, pageName, repositionToCursor = false) => {
const frameId = ea.addFrame(x, y, width, height, pageName);
if(lockFrame) {
ea.getElement(frameId).locked = true;
}
await ea.addElementsToView(repositionToCursor);
const addedFrame = ea.getViewElements().find(el => el.id === frameId);
if(shouldZoom) {
ea.viewZoomToElements(true, [addedFrame]);
} else {
ea.selectElementsInView([addedFrame]);
}
//ready for the next frame
ea.clear();
selectedFrame = addedFrame;
if(shouldClose) {
modal.close();
}
return addedFrame;
}
const translateToZero = ({ top, left, bottom, right }, padding=0) => {
const {topX, topY, width, height} = ea.getBoundingBox(ea.getViewElements());
const newTop = top - (topY - padding);
const newLeft = left - (topX - padding);
const newBottom = bottom - (topY - padding);
const newRight = right - (topX - padding);
return {
top: newTop,
left: newLeft,
bottom: newBottom,
right: newRight,
};
}
// Check if all frames have the same size
const checkFrameSizes = (frames) => {
if (frames.length <= 1) return true;
const referenceWidth = frames[0].width;
const referenceHeight = frames[0].height;
return frames.every(frame =>
Math.abs(frame.width - referenceWidth) < 1 &&
Math.abs(frame.height - referenceHeight) < 1
);
};
// Print frames to PDF
const printToPDF = async (marginSize) => {
const margin = MARGINS[marginSize] || 0;
// Save margin setting
if (settings[MARGIN].value !== marginSize) {
settings[MARGIN].value = marginSize;
dirty = true;
}
// Get all frame elements and sort by name
const frames = getSortedFrames();
if (frames.length === 0) {
new Notice("No frames found to print");
return;
}
// Check if all frames have the same size
if (!checkFrameSizes(frames)) {
new Notice("Only same sized pages are supported currently", 7000);
return;
}
// Create a notice during processing
const notice = new Notice("Preparing PDF, please wait...", 0);
// Create SVGs for each frame
const svgPages = [];
const svgScene = await ea.createViewSVG({
withBackground: true,
theme: st.theme,
frameRendering: { enabled: false, name: false, outline: false, clip: false },
padding: 0,
selectedOnly: false,
skipInliningFonts: false,
embedScene: false,
});
for (const frame of frames) {
const { top, left, bottom, right } = translateToZero({
top: frame.y,
left: frame.x,
bottom: frame.y + frame.height,
right: frame.x + frame.width,
});
//always create the new SVG in the main Obsidian workspace (not the popout window, if present)
const host = window.createDiv();
host.innerHTML = svgScene.outerHTML;
const clonedSVG = host.firstElementChild;
const width = Math.abs(left-right);
const height = Math.abs(top-bottom);
clonedSVG.setAttribute("viewBox", `${left} ${top} ${width} ${height}`);
clonedSVG.setAttribute("width", `${width}`);
clonedSVG.setAttribute("height", `${height}`);
svgPages.push(clonedSVG);
}
// Use dimensions from the first frame
const width = frames[0].width;
const height = frames[0].height;
// Create PDF
await ea.createPDF({
SVG: svgPages,
scale: { fitToPage: true },
pageProps: {
dimensions: { width, height },
backgroundColor: "#ffffff",
margin: {
left: margin,
right: margin,
top: margin,
bottom: margin
},
alignment: "center"
},
filename: ea.targetView.file.basename + "-pages.pdf"
});
notice.hide();
};
// -----------------------
// Create a floating modal
// -----------------------
modal.titleEl.setText("Page Management");
modal.titleEl.style.textAlign = "center";
// Handle save settings on modal close
modal.onClose = () => {
if (dirty) {
ea.setScriptSettings(settings);
}
};
// Create modal content
modal.contentEl.createDiv({ cls: "excalidraw-page-manager" }, div => {
const container = div.createDiv({
attr: {
style: "display: flex; flex-direction: column; gap: 15px; padding: 10px;"
}
});
// Add help section at the top
const helpDiv = container.createDiv({
attr: {
style: "margin-bottom: 10px;"
}
});
helpDiv.createEl("details", {}, (details) => {
details.createEl("summary", {
text: "Help & Information",
attr: {
style: "cursor: pointer; font-weight: bold; margin-bottom: 10px;"
}
});
details.createEl("div", {
attr: {
style: "padding: 10px; border: 1px solid var(--background-modifier-border); border-radius: 4px; margin-top: 8px; font-size: 0.9em; max-height: 300px; overflow-y: auto;"
}
}, div => {
ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, div, "", ea.plugin)
});
});
let pageSizeDropdown, orientationDropdown, marginDropdown;
// Settings section - only show for first frame creation
if (!hasFrames) {
const settingsContainer = container.createDiv({
attr: {
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
}
});
// Page size dropdown
settingsContainer.createEl("label", { text: "Page Size:" });
pageSizeDropdown = settingsContainer.createEl("select", {
cls: "dropdown",
attr: { style: "width: 100%;" }
});
PAGE_SIZES.forEach(size => {
pageSizeDropdown.createEl("option", { text: size, value: size });
});
pageSizeDropdown.value = settings[PAGE_SIZE].value;
// Orientation dropdown
settingsContainer.createEl("label", { text: "Orientation:" });
orientationDropdown = settingsContainer.createEl("select", {
cls: "dropdown",
attr: { style: "width: 100%;" }
});
PAGE_ORIENTATIONS.forEach(orientation => {
orientationDropdown.createEl("option", { text: orientation, value: orientation });
});
orientationDropdown.value = settings[ORIENTATION].value;
}
// Show margin settings only if frames exist
if (hasFrames) {
const marginContainer = container.createDiv({
attr: {
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
}
});
// Margin dropdown (for printing)
marginContainer.createEl("label", { text: "Print Margin:" });
marginDropdown = marginContainer.createEl("select", {
cls: "dropdown",
attr: { style: "width: 100%;" }
});
Object.keys(MARGINS).forEach(margin => {
marginDropdown.createEl("option", { text: margin, value: margin });
});
marginDropdown.value = settings[MARGIN].value;
}
// Add checkboxes for zoom and modal behavior only when frames exist
const optionsContainer = container.createDiv({
attr: {
style: "margin-top: 10px;"
}
});
new ea.obsidian.Setting(optionsContainer)
.setName("Lock")
.setDesc("Lock the new frame element after it is created.")
.addToggle(toggle => {
toggle.setValue(lockFrame)
.onChange(value => {
lockFrame = value;
if (settings[LOCK_FRAME].value !== value) {
settings[LOCK_FRAME].value = value;
dirty = true;
}
});
});
// Zoom to added frame checkbox
new ea.obsidian.Setting(optionsContainer)
.setName("Zoom to new frame")
.setDesc("Automatically zoom to the newly created frame")
.addToggle(toggle => {
toggle.setValue(shouldZoom)
.onChange(value => {
shouldZoom = value;
if (settings[SHOULD_ZOOM].value !== value) {
settings[SHOULD_ZOOM].value = value;
dirty = true;
}
});
});
// Close after adding checkbox
new ea.obsidian.Setting(optionsContainer)
.setName("Close after adding")
.setDesc("Close this dialog after adding a new frame")
.addToggle(toggle => {
toggle.setValue(shouldClose)
.onChange(value => {
shouldClose = value;
if (settings[SHOULD_CLOSE].value !== value) {
settings[SHOULD_CLOSE].value = value;
dirty = true;
}
});
});
// Buttons section
const buttonContainer = container.createDiv({
attr: {
style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px;"
}
});
if (!hasFrames) {
// First frame creation button (centered)
const createFirstBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: {
style: "grid-column: 1 / span 3; height: 40px; background-color: var(--interactive-accent); color: var(--text-on-accent);"
}
});
createFirstBtn.textContent = "Create First Frame";
createFirstBtn.addEventListener("click", async () => {
const tmpShouldClose = shouldClose;
shouldClose = true;
await createFirstFrame(pageSizeDropdown.value, orientationDropdown.value);
shouldClose = tmpShouldClose;
if(!shouldClose) run();
});
} else if (hasSelectedFrame) {
// Only show navigation buttons and print when a frame is selected
// Up button (in middle of top row)
const upBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: {
style: "grid-column: 2; grid-row: 1; height: 40px;"
}
});
upBtn.innerHTML = ea.obsidian.getIcon("arrow-big-up").outerHTML;
upBtn.addEventListener("click", async () => {
await addPage("up");
});
// Add empty space
buttonContainer.createDiv({
attr: { style: "grid-column: 3; grid-row: 1;" }
});
// Left button
const leftBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: { style: "grid-column: 1; grid-row: 2; height: 40px;" }
});
leftBtn.innerHTML = ea.obsidian.getIcon("arrow-big-left").outerHTML;
leftBtn.addEventListener("click", async () => {
await addPage("left");
});
// Print button (center)
const printBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: {
style: "grid-column: 2; grid-row: 2; height: 40px; background-color: var(--interactive-accent);"
}
});
printBtn.innerHTML = ea.obsidian.getIcon("printer").outerHTML;
printBtn.addEventListener("click", async () => {
await printToPDF(marginDropdown.value);
});
// Right button
const rightBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: { style: "grid-column: 3; grid-row: 2; height: 40px;" }
});
rightBtn.innerHTML = ea.obsidian.getIcon("arrow-big-right").outerHTML;
rightBtn.addEventListener("click", async () => {
await addPage("right");
});
// Down button (in middle of bottom row)
const downBtn = buttonContainer.createEl("button", {
cls: "page-btn",
attr: { style: "grid-column: 2; grid-row: 3; height: 40px;" }
});
downBtn.innerHTML = ea.obsidian.getIcon("arrow-big-down").outerHTML;
downBtn.addEventListener("click", async () => {
await addPage("down");
});
// Add empty space
buttonContainer.createDiv({
attr: { style: "grid-column: 1; grid-row: 3;" }
});
}
// Add CSS
div.createEl("style", {
text: `
.page-btn {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
}
.page-btn:hover {
background-color: var(--interactive-hover);
}
.dropdown {
height: 30px;
background-color: var(--background-secondary);
color: var(--text-normal);
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
padding: 0 10px;
}
`
});
});
modal.open();
}
run();

View File

@@ -0,0 +1 @@
<svg class="skip" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -7,28 +7,23 @@ This script modifies the color lightness/hue/saturation/transparency of selected
- The color of SVG elements and nested Excalidraw drawings will only be mapped. When mapping colors, the original image remains unchanged, only a mapping table is created and the image is recolored during rendering of your Excalidraw screen. In case you want to make manual changes you can also edit the mapping in Markdown View Mode under `## Embedded Files`
If you select only a single SVG or nested Excalidraw element, then the script offers an additional feature. You can map colors one by one in the image.
```js*/
```js
*/
const HELP_TEXT = `
<ul>
<li dir="auto">Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements</li>
<li dir="auto">For a single selected image, you can map colors individually in the color mapping section</li>
<li dir="auto">For Excalidraw elements: stroke and background colors are modified permanently</li>
<li dir="auto">For SVG/nested drawings: original files stay unchanged, color mapping is stored under <code>## Embedded Files</code></li>
<li dir="auto">Using color maps helps maintain links between drawings while allowing different color themes</li>
<li dir="auto">Sliders work on relative scale - the amount of change is applied to current values</li>
<li dir="auto">Unlike Excalidraw's opacity setting which affects the whole element:
<ul>
<li dir="auto">Shade Master can set different opacity for stroke vs background</li>
<li dir="auto">Note: SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"</li>
<li dir="auto">Additionally if the same color is used as fill and stroke the color can only be mapped once</li>
</ul>
</li>
<li dir="auto">This is an experimental script - contributions welcome on GitHub via PRs</li>
</ul>
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
- Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements
- For a single selected image, you can map colors individually in the color mapping section
- For Excalidraw elements: stroke and background colors are modified permanently
- For SVG/nested drawings: original files stay unchanged, color mapping is stored under \`## Embedded Files\`
- Using color maps helps maintain links between drawings while allowing different color themes
- Sliders work on relative scale - the amount of change is applied to current values
- Unlike Excalidraw's opacity setting which affects the whole element:
- Shade Master can set different opacity for stroke vs background
- **Note:** SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"
- Additionally if the same color is used as fill and stroke the color can only be mapped once
- This is an experimental script - contributions welcome on GitHub via PRs
![](https://www.youtube.com/embed/ISuORbVKyhQ)
`;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.2")) {
@@ -343,7 +338,8 @@ function showModal() {
const helpDetailsDiv = helpDiv.createEl("div", {
attr: { style: "margin-top: 0em; " }
});
helpDetailsDiv.innerHTML = HELP_TEXT;
//helpDetailsDiv.innerHTML = HELP_TEXT;
await ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, helpDetailsDiv, "", ea.plugin);
const component = new ea.obsidian.Setting(contentEl)
.setName(FORMAT)

File diff suppressed because one or more lines are too long

View File

@@ -47,6 +47,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.svg"/></div>|[[#Printable Layout Wizard]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
## Connectors and Arrows
@@ -632,6 +633,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Uniform%20size.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-uniform-size.jpg"><br>The script will standardize the sizes of rectangles, diamonds and ellipses adjusting all the elements to match the largest width and height within the group.</td></tr></table>
# Printable Layout Wizard
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Printable%20Layout%20Wizard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.<br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-01.png" style="max-width: 400px;"><br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-02.png" style="max-width: 400px;"></td></tr></table>
## Zoom to Fit Selected Elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

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

View File

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

19102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.18.0-25",
"@zsviczian/excalidraw": "0.18.0-28",
"chroma-js": "^3.1.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -49,7 +49,7 @@ const jsxRuntimeShim = `
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
const LANGUAGES = ['ru', 'zh-cn']; //english is not compressed as it is always loaded by default
const LANGUAGES = ['ru', 'zh-cn', 'zh-tw', 'es']; //english is not compressed as it is always loaded by default
function trimLastSemicolon(input) {
if (input.endsWith(";")) {

View File

@@ -79,7 +79,7 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
'ur': 'ur-PK', // Assuming Pakistan for Urdu
'vi': 'vi-VN',
'zh': 'zh-CN',
'zh-TW': 'zh-TW',
'zh-tw': 'zh-TW',
};
@@ -262,6 +262,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
"embed-as-markdown": {name: "excalidraw-embed-md", type: "checkbox"},
};
export const CaptureUpdateAction = {

View File

@@ -710,7 +710,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
(fnameParts.hasBlockref || fnameParts.hasSectionref) && fnameParts.blockref !== "as-image"
}
const tmpObsidianWYSIWYG = async (
@@ -724,6 +724,9 @@ const tmpObsidianWYSIWYG = async (
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
if(ctx.frontmatter?.["excalidraw-embed-md"]) {
return;
}
//@ts-ignore
if (ctx.remainingNestLevel < 4) {

View File

@@ -3,8 +3,10 @@ import {
ButtonComponent,
DropdownComponent,
getIcon,
htmlToMarkdown,
Modifier,
normalizePath,
Notice,
PluginSettingTab,
Setting,
TextComponent,
@@ -44,6 +46,7 @@ import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
import { PDFExportSettingsComponent, PDFExportSettings } from "src/shared/Dialogs/PDFExportSettingsComponent";
import { ContentSearcher } from "src/shared/components/ContentSearcher";
export interface ExcalidrawSettings {
disableDoubleClickTextEditing: boolean;
@@ -604,6 +607,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
containerEl.addClass("excalidraw-settings");
this.containerEl.empty();
// ------------------------------------------------
// Search and Settings to Clipboard
// ------------------------------------------------
const notebookLMLinkContainer = createDiv("setting-item-description excalidraw-settings-links-container");
new ContentSearcher(containerEl, notebookLMLinkContainer);
notebookLMLinkContainer.createEl("a",{
href: "https://notebooklm.google.com/notebook/42d76a2f-c11d-4002-9286-1683c43d0ab0",
attr: {
"aria-label": t("NOTEBOOKLM_LINK_ARIA"),
"style": "margin: auto;"
}},
(a)=> {
//Lucide: message-circle-question-mark
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>${
t("NOTEBOOKLM_LINK_TEXT")
}`;
}
);
// ------------------------------------------------
// Promo links
// ------------------------------------------------
const coffeeDiv = containerEl.createDiv("coffee");
coffeeDiv.addClass("ex-coffee-div");
const coffeeLink = coffeeDiv.createEl("a", {
@@ -618,41 +646,53 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
const iconLinks = [
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
icon: getIcon("bug").outerHTML,
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
text: "Bugs and Feature Requests",
aria: t("LINKS_BUGS_ARIA"),
text: t("LINKS_BUGS"),
},
{
icon: getIcon("globe").outerHTML,
href: "https://excalidraw-obsidian.online/",
aria: t("LINKS_WIKI_ARIA"),
text: t("LINKS_WIKI"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
icon: getIcon("youtube").outerHTML,
href: "https://www.youtube.com/@VisualPKM",
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
text: "Visual PKM on YouTube",
aria: t("LINKS_YT_ARIA"),
text: t("LINKS_YT"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
href: "https://discord.gg/DyfAXFwUHc",
aria: "Join the Visual Thinking Workshop Discord Server",
text: "Community on Discord",
aria: t("LINKS_DISCORD_ARIA"),
text: t("LINKS_DISCORD"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
icon: getIcon("twitter").outerHTML,
href: "https://twitter.com/zsviczian",
aria: "Follow me on Twitter",
text: "Follow me on Twitter",
aria: t("LINKS_TWITTER"),
text: t("LINKS_TWITTER"),
},
{
icon: getIcon("graduation-cap").outerHTML,
href: "https://visual-thinking-workshop.com",
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
text: "Join the Visual Thinking Workshop",
}
aria: t("LINKS_VTW_ARIA"),
text: t("LINKS_VTW"),
},
{
icon: getIcon("book").outerHTML,
href: "https://sketch-your-mind.com",
aria: t("LINKS_BOOK_ARIA"),
text: t("LINKS_BOOK"),
},
];
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
iconLinks.forEach(({ icon, href, aria, text }) => {
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
a.innerHTML = icon + text;
a.innerHTML = icon + text;
});
});

149
src/lang/langDiffWithEn.js Normal file
View File

@@ -0,0 +1,149 @@
/**
* Language Pack Diff Checker Script
*
* Purpose:
* Compare src/lang/locale/en.ts and a target language file (e.g. zh-cn.ts), output missing and extra keys.
* Output is color-coded:
* - Green: Key exists in en.ts but missing in the target language file
* - Red: Key exists in the target language file but not in en.ts
* - Yellow: The same key exists in both files but the line numbers are different
*
* Dependencies:
* Node.js environment, no extra dependencies required.
*
* Usage:
* 1. Open a terminal in this file's directory (or project root).
* 2. Run:
* node src/lang/langDiffWithEn.js zh-cn
* # or node src/lang/langDiffWithEn.js ja
* # The argument is the language file name (without extension), default is zh-cn
*
* Output:
* + (green): Key exists in en.ts but missing in the target language file
* - (red): Key exists in the target language file but not in en.ts
* ! (yellow): The same key exists in both files but the line numbers are different
* If there is no output, the two files have identical keys.
*/
/**
* 语言包差异检测脚本
*
* 用途:
* 比较 src/lang/locale/en.ts 与指定语言文件(如 zh-cn.ts中的 key输出缺失和多余的 key。
* 输出内容会用不同颜色区分:
* - 绿色en.ts 有但目标语言缺失的 key
* - 红色:目标语言有但 en.ts 没有的 key
* - 黄色:相同的 key 所在的行号不相同
*
* 依赖:
* Node.js 环境,无需额外依赖。
*
* 用法:
* 1. 在命令行进入本文件所在目录(或项目根目录)。
* 2. 运行:
* node src/lang/langDiffWithEn.js zh-cn
* # 或 node src/lang/langDiffWithEn.js ja
* # 参数为语言文件名(不带扩展名),默认为 zh-cn
*
* 输出说明:
* + 开头绿色en.ts 有但目标语言缺失的 key
* - 开头(红色):目标语言有但 en.ts 没有的 key
* ! 开头(黄色):相同的 key 所在的行号不相同
* 若无输出则表示两个文件 key 完全一致。
*/
const fs = require("fs");
const path = require("path");
/**
* 读取 locale 文件夹下指定语言和 en.ts 文件,逐行解析键值和行号
* @param lang 语言文件名(不含扩展名),如 zh-cn
* @returns { en: Record<string, {value: string, line: number}>, lang: Record<string, {value: string, line: number}> }
*/
function readLocaleFiles(lang = "zh-cn") {
// 构造 locale 目录路径
const localeDir = path.join(__dirname, "./locale");
// 获取 en.ts 文件路径
const enFile = path.join(localeDir, "en.ts");
// 获取指定语言文件路径
const langFile = path.join(localeDir, `${lang}.ts`);
/**
* 解析指定的语言文件,提取每个 key 的值和所在行号
* @param file 文件路径
* @returns {Record<string, {value: string, line: number}>}
*/
const parse = (file) => {
// 读取文件内容并按行分割
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
const result = {};
let inExport = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检查是否进入 export default 块
if (!inExport && line.includes("export default")) {
inExport = true;
continue;
}
if (!inExport) continue;
// 匹配 key: "value", 格式的行
const m = line.match(/^\s*([A-Z0-9_]+)\s*:\s*.*?,?$/);
if (m) {
let key = m[1];
// 保存 key 及其所在行号
result[key] = i + 1;
}
}
return result;
};
// 返回 en 和指定语言的解析结果
return {
en: parse(enFile),
lang: parse(langFile),
};
}
/**
* 比较 en 和 lang 的 key/value输出差异
*/
function diffLang(lang = "zh-cn") {
const { en, lang: l } = readLocaleFiles(lang);
const allKeys = new Set([...Object.keys(en), ...Object.keys(l)]);
const diffs = [];
for (const key of allKeys) {
if (!(key in en)) {
diffs.push(`- ${key}: ${l[key]}`);
} else if (!(key in l)) {
diffs.push(`+ ${key}: ${en[key]}`);
} else {
if (en[key] !== l[key]) {
diffs.push(`! ${key}: en: ${en[key]} <--> lang: ${l[key]}`);
}
}
}
return diffs;
}
// CLI
if (require.main === module) {
const lang = process.argv[2] || "zh-cn";
const diffs = diffLang(lang);
if (diffs.length === 0) {
console.log(`语言文件 ${lang} 与 en.ts 无差异`);
} else {
diffs.forEach((line) => {
if (line.startsWith("+")) {
// 绿色
console.log("\x1b[32m%s\x1b[0m", line);
} else if (line.startsWith("-")) {
// 红色
console.log("\x1b[31m%s\x1b[0m", line);
} else if (line.startsWith("!")) {
// 黄色
console.log("\x1b[33m%s\x1b[0m", line);
} else {
// 默认
console.log(line);
}
});
}
}

View File

@@ -192,7 +192,32 @@ export default {
SAVE_IS_TAKING_LONG: "Saving your previous file is taking a long time. Please wait...",
SAVE_IS_TAKING_VERY_LONG: "For better performance, consider splitting large drawings into several smaller files.",
//ContentSearcher.ts
SEARCH_COPIED_TO_CLIPBOARD: "Markdown ready on clipboard",
SEARCH_COPY_TO_CLIPBOARD_ARIA: "Copy the entire dialog to the clipboard as Markdown. Ideal for use with tools like ChatGPT to search and understand.",
SEARCH_SHOWHIDE_ARIA: "Show/Hide search bar",
SEARCH_NEXT: "Next",
SEARCH_PREVIOUS: "Previous",
//settings.ts
NOTEBOOKLM_LINK_ARIA: "Ask NotebookLM for help about the plugin. This model is pre-loaded with all my video transcripts, release notes and other helpful content. Chat with NotebookLM to explore my 250+ videos and the Excalidraw documentation.",
NOTEBOOKLM_LINK_TEXT: "Learn the Plugin. Access the NotebookLM knowledgebase.",
LINKS_BUGS_ARIA: "Report bugs and raise feature requsts on the plugin's GitHub page",
LINKS_BUGS: "Report Bugs",
LINKS_YT_ARIA: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
LINKS_YT: "Learn on YouTube",
LINKS_DISCORD_ARIA: "Join the Visual Thinking Workshop Discord Server",
LINKS_DISCORD: "Join the Community",
LINKS_TWITTER: "Follow me",
LINKS_VTW_ARIA: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
LINKS_VTW: "Join a Workshop",
LINKS_BOOK_ARIA: "Read Sketch Your Mind, my book on Visual Thinking",
LINKS_BOOK: "Read the Book",
LINKS_WIKI: "Plugin Wiki",
LINKS_WIKI_ARIA: "Explore the Excalidraw Plugin Wiki",
RELEASE_NOTES_NAME: "Display Release Notes after update",
RELEASE_NOTES_DESC:
"<b><u>Toggle ON:</u></b> Display release notes each time you update Excalidraw to a newer version.<br>" +
@@ -394,7 +419,8 @@ FILENAME_HEAD: "Filename",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
"markdown side of the document.<br>" +
"Note: <b>excalidraw-open-md</b> is different from <b>excalidraw-embed-md</b>. If <b>excalidraw-embed-md</b> is set to true, the hover preview will always show the markdown side, regardless of this setting. To force image rendering when embedding, use <code>![[drawing#^as-image]]</code> in your markdown file.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +

File diff suppressed because it is too large Load Diff

View File

@@ -192,7 +192,32 @@ export default {
SAVE_IS_TAKING_LONG: "保存您之前的文件花费的时间较长,请稍候...",
SAVE_IS_TAKING_VERY_LONG: "为了更好的性能,请考虑将大型绘图拆分成几个较小的文件。",
//ContentSearcher.ts
SEARCH_COPIED_TO_CLIPBOARD: "Markdown 已复制到剪贴板",
SEARCH_COPY_TO_CLIPBOARD_ARIA: "将整个对话框复制为 Markdown 到剪贴板。非常适合搭配 ChatGPT 等工具进行搜索和理解。",
SEARCH_SHOWHIDE_ARIA : "显示/隐藏搜索栏" ,
SEARCH_NEXT: "下一个",
SEARCH_PREVIOUS: "上一个",
//settings.ts
NOTEBOOKLM_LINK_ARIA : "向 NotebookLM 咨询有关插件的帮助。此模型已预加载我的所有视频转录、发布说明以及其他有用内容。与 NotebookLM 聊天,探索我的 250+ 视频以及 Excalidraw 文档。" ,
NOTEBOOKLM_LINK_TEXT : "了解插件。访问 NotebookLM 知识库。" ,
LINKS_BUGS_ARIA: "在插件的 GitHub 页面报告错误和提交功能请求",
LINKS_BUGS: "报告错误",
LINKS_YT_ARIA: "访问我的 YouTube 频道学习视觉思维和 Excalidraw",
LINKS_YT: "在 YouTube 学习",
LINKS_DISCORD_ARIA: "加入视觉思维研讨会 Discord 服务器",
LINKS_DISCORD: "加入社区",
LINKS_TWITTER: "关注我",
LINKS_VTW_ARIA: "了解视觉知识管理、Excalidraw、Obsidian、ExcaliBrain 等内容",
LINKS_VTW: "参加研讨会",
LINKS_BOOK_ARIA: "阅读我的视觉思维著作《Sketch Your Mind》",
LINKS_BOOK: "阅读书籍",
LINKS_WIKI : "插件 Wiki" ,
LINKS_WIKI_ARIA : "探索 Excalidraw 插件 Wiki" ,
RELEASE_NOTES_NAME: "显示更新说明",
RELEASE_NOTES_DESC:
"<b>开启:</b>每次更新本插件后,显示最新发行版本的说明。<br>" +
@@ -390,11 +415,12 @@ FILENAME_HEAD: "文件名",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
"",
"<br>" +
"注意:<b>excalidraw-open-md</b> 与 <b>excalidraw-embed-md</b> 是不同的。如果 <b>excalidraw-embed-md</b> 设置为 true则悬停预览始终显示 markdown 部分,而不受此设置影响。要在嵌入时强制图像渲染,请在你的 markdown 文件中使用 <code>![[drawing#^as-image]]</code>。" ,
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Excalidraw 文件在 Markdown 阅读模式下渲染为图片",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"当您处于 Markdown 阅读模式即查看绘图的背景笔记Excalidraw 绘图是否应该渲染为图像?" +
@@ -922,8 +948,12 @@ FILENAME_HEAD: "文件名",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
PIN_VIEW: "锁定视图",
DO_NOT_PIN_VIEW: "取消锁定视图",
NARROW_TO_BLOCK: "缩放至块",
SHOW_ENTIRE_FILE: "显示全部",
SELECT_SECTION: "从文档选择章节",
SELECT_VIEW: "从 base 选择视图",
ZOOM_TO_FIT: "缩放至合适大小",
RELOAD: "重载链接",
OPEN_IN_BROWSER: "在浏览器中打开",

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,23 @@ I build this plugin in my free time, as a labor of love. Curious about the philo
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.14.1":`
## Fixed
- Excalidraw broke Obsidian's UI in Arabic, Persian, and Hebrew due to LTR, RTL conflict. [#2423](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2423)
- Styling issues impacting native Obsidian search/replace dialogs. [#2420](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2420)
- Now using native Obsidian attachment location function. 🙏 [mnaoumov](https://github.com/mnaoumov) [#2421](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2421), potentially fixes [#179](https://github.com/RainCat1998/obsidian-custom-attachment-location/issues/179) of the Obsidian Custom Attachment Location plugin issue.
## New
- New frontmatter option \`excalidraw-embed-md\`. When set to \`true\`, embedding the drawing into a markdown note will render the "back of the note" (its markdown content) instead of rendering it as an image.
- If you want to always display the drawing as an image, even when \`excalidraw-embed-md\` is enabled, use the special embed syntax: \`![[drawing#^as-image]]\`. Here, \`as-image\` is a phantom block reference that forces image rendering.
- Added Spanish translation by [@Joakim31](https://github.com/Joakim31) [#2425](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2425)
- Incremental minor updates from the main [Excalidraw project](https://github.com/excalidraw/excalidraw).
`,
"2.14.0":`
## A Big "Small" Update
- Added search to Excalidraw Settings, plus added a link to access the public NotebookLM workbook pre-loaded with everything about the plugin
- New Taiwan-idiomatic Traditional Chinese translation by [@rlan](https://github.com/rlan) [#2413](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2413)
`,
"2.13.2":`
## New
- Excalidraw now properly supports drag and drop of obsidian links from Bases.

View File

@@ -25,8 +25,10 @@ export class ReleaseNotes extends Modal {
async onClose() {
this.contentEl.empty();
await this.plugin.loadSettings();
this.plugin.settings.previousRelease = PLUGIN_VERSION
await this.plugin.saveSettings();
if(this.plugin.settings.previousRelease !== PLUGIN_VERSION) {
this.plugin.settings.previousRelease = PLUGIN_VERSION;
await this.plugin.saveSettings();
}
}
async createForm() {
@@ -39,7 +41,8 @@ export class ReleaseNotes extends Modal {
.slice(0, 10)
.join("\n\n---\n")
: FIRST_RUN;
await MarkdownRenderer.renderMarkdown(
await MarkdownRenderer.render(
this.app,
message,
this.contentEl,
"",

View File

@@ -1,80 +1,27 @@
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
import ExcalidrawPlugin from "../../core/main";
import { errorlog, escapeRegExp } from "../../utils/utils";
import { errorlog } from "../../utils/utils";
import { log } from "src/utils/debugHelper";
import { ContentSearcher } from "../components/ContentSearcher";
const URL =
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
export class ScriptInstallPrompt extends Modal {
private contentDiv: HTMLDivElement;
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
// this.titleEl.setText(t("INSTAL_MODAL_TITLE"));
}
async onOpen(): Promise<void> {
const searchBarWrapper = document.createElement("div");
searchBarWrapper.classList.add('search-bar-wrapper');
const searchBar = document.createElement("input");
searchBar.type = "text";
searchBar.id = "search-bar";
searchBar.placeholder = "Search...";
//searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count
const nextButton = document.createElement("button");
nextButton.textContent = "→";
nextButton.onclick = () => this.navigateSearchResults("next");
const prevButton = document.createElement("button");
prevButton.textContent = "←";
prevButton.onclick = () => this.navigateSearchResults("previous");
const hitCount = document.createElement("span");
hitCount.id = "hit-count";
hitCount.classList.add('hit-count');
searchBarWrapper.appendChild(prevButton);
searchBarWrapper.appendChild(nextButton);
searchBarWrapper.appendChild(searchBar);
searchBarWrapper.appendChild(hitCount);
this.contentEl.prepend(searchBarWrapper);
searchBar.addEventListener("input", (e) => {
this.clearHighlights();
const searchTerm = (e.target as HTMLInputElement).value;
if (searchTerm && searchTerm.length > 0) {
this.highlightSearchTerm(searchTerm);
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : "";
setTimeout(()=>this.navigateSearchResults("next"));
} else {
hitCount.textContent = "";
}
});
searchBar.addEventListener("keydown", (e) => {
// If Ctrl/Cmd + F is pressed, focus on search bar
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
searchBar.focus();
}
// If Enter is pressed, navigate to next result
else if (e.key === "Enter") {
e.preventDefault();
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
}
});
this.contentEl.classList.add("excalidraw-scriptengine-install");
this.contentDiv = document.createElement("div");
this.contentEl.appendChild(this.contentDiv);
new ContentSearcher(this.contentDiv);
this.containerEl.classList.add("excalidraw-scriptengine-install");
try {
const source = await request({ url: URL });
@@ -111,99 +58,6 @@ export class ScriptInstallPrompt extends Modal {
}
}
highlightSearchTerm(searchTerm: string): void {
// Create a walker to traverse text nodes
const walker = document.createTreeWalker(
this.contentDiv,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
}
}
);
const nodesToReplace: Text[] = [];
while (walker.nextNode()) {
nodesToReplace.push(walker.currentNode as Text);
}
nodesToReplace.forEach(node => {
const nodeContent = node.nodeValue!;
const newNode = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
// Iterate over all matches in the text node
while ((match = regex.exec(nodeContent)) !== null) {
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
const highlighted = document.createElement('mark');
highlighted.className = 'search-highlight';
highlighted.textContent = match[0];
highlighted.classList.add('search-result');
newNode.appendChild(before);
newNode.appendChild(highlighted);
lastIndex = regex.lastIndex;
}
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
node.replaceWith(newNode);
});
}
clearHighlights(): void {
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
el.outerHTML = el.innerHTML;
});
}
navigateSearchResults(direction: "next" | "previous"): void {
const highlights: HTMLElement[] = Array.from(
this.contentDiv.querySelectorAll("mark.search-highlight")
);
if (highlights.length === 0) return;
const currentActiveIndex = highlights.findIndex((highlight) =>
highlight.classList.contains("active-highlight")
);
if (currentActiveIndex !== -1) {
highlights[currentActiveIndex].classList.remove("active-highlight");
highlights[currentActiveIndex].style.border = "none";
}
let nextActiveIndex = 0;
if (direction === "next") {
nextActiveIndex =
currentActiveIndex === highlights.length - 1
? 0
: currentActiveIndex + 1;
} else if (direction === "previous") {
nextActiveIndex =
currentActiveIndex === 0
? highlights.length - 1
: currentActiveIndex - 1;
}
const nextActiveHighlight = highlights[nextActiveIndex];
nextActiveHighlight.classList.add("active-highlight");
nextActiveHighlight.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
// Update the hit count
const hitCount = document.getElementById("hit-count");
hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`;
}
onClose(): void {
this.contentEl.empty();
}

View File

@@ -1067,6 +1067,13 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
desc: "If this key is present the file will be opened as a markdown file in the editor",
after: ": true",
},
{
field: "embed-md",
code: null,
desc: "If this key is present, when embedding the ![[image]] into a markdown document, it will be embedded as markdown, not as an image.\n" +
"If however you embed ![[image#^as-image]], i.e. you reference the 'as-image' block, then the image will be embedded as an image.",
after: ": true",
},
{
field: "autoexport",
code: null,

View File

@@ -191,7 +191,7 @@ export class ScriptEngine {
...this.scriptIconMap,
};
const splitname = splitFolderAndFilename(name)
this.scriptIconMap[scriptPath] = { name:splitname.filename, group: splitname.folderpath === "/" ? "" : splitname.folderpath, svgString };
this.scriptIconMap[scriptPath] = { name:splitname.filename, group: splitname.folderpath, svgString };
this.updateToolPannels();
}

View File

@@ -0,0 +1,306 @@
import { t } from "src/lang/helpers";
import { escapeRegExp } from "../../utils/utils";
// @ts-ignore
import { getIcon, htmlToMarkdown, Notice } from "obsidian";
export class ContentSearcher {
private contentDiv: HTMLElement;
private searchBar: HTMLInputElement;
private prevButton: HTMLButtonElement;
private nextButton: HTMLButtonElement;
private exportMarkdown: HTMLButtonElement;
private showHideButton: HTMLButtonElement;
private customElemenentContainer: HTMLDivElement;
private inputContainer: HTMLDivElement;
private customElement: HTMLElement;
private hitCount: HTMLSpanElement;
private searchBarWrapper: HTMLDivElement;
constructor(contentDiv: HTMLElement, customElement?: HTMLElement) {
this.contentDiv = contentDiv;
this.customElement = customElement;
this.createSearchElements();
this.setupEventListeners();
contentDiv.prepend(this.getSearchBarWrapper());
}
/**
* Creates search UI elements styled like Obsidian's native search
*/
private createSearchElements(): void {
this.searchBarWrapper = createDiv("excalidraw-search document-search-container");
const documentSearch = createDiv("document-search");
this.inputContainer = createDiv("search-input-container document-search-input");
this.searchBar = createEl("input",{type: "text", placeholder: "Find..."});
this.hitCount = createDiv("document-search-count");
this.inputContainer.appendChild(this.searchBar);
this.inputContainer.appendChild(this.hitCount);
const buttonContainer = createDiv("document-search-buttons");
this.prevButton = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_PREVIOUS"),
"data-tooltip-position": "top",
},
type: "button",
});
this.prevButton.innerHTML = getIcon("arrow-up").outerHTML;
this.nextButton = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_NEXT"),
"data-tooltip-position": "top",
},
type: "button",
});
this.nextButton.innerHTML = getIcon("arrow-down").outerHTML;
this.exportMarkdown = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_COPY_TO_CLIPBOARD_ARIA"),
"data-tooltip-position": "top",
},
type: "button",
});
this.exportMarkdown.innerHTML = getIcon("clipboard-copy").outerHTML;
this.showHideButton = createEl("button", {
cls: ["clickable-icon", "document-search-button", "search-visible"],
attr: {
"aria-label": t("SEARCH_SHOWHIDE_ARIA"),
"data-tooltip-position": "top",
},
type: "button",
});
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
buttonContainer.appendChild(this.prevButton);
buttonContainer.appendChild(this.nextButton);
buttonContainer.appendChild(this.exportMarkdown);
buttonContainer.appendChild(this.showHideButton);
documentSearch.appendChild(this.inputContainer);
documentSearch.appendChild(buttonContainer);
this.searchBarWrapper.appendChild(documentSearch);
this.customElemenentContainer = createDiv();
if(this.customElement) {
this.customElemenentContainer.appendChild(this.customElement);
this.searchBarWrapper.appendChild(this.customElemenentContainer)
}
}
/**
* Attach event listeners to search elements
*/
private setupEventListeners(): void {
this.nextButton.onclick = () => this.navigateSearchResults("next");
this.prevButton.onclick = () => this.navigateSearchResults("previous");
this.exportMarkdown.onclick = () => {
// Get the full HTML content first
const fullHtml = this.contentDiv.outerHTML;
// Find the index of the first <hr> element
const startIndex = fullHtml.indexOf('<hr');
// Extract HTML from the first <hr> element onwards
const html = startIndex > -1 ? fullHtml.substring(startIndex) : fullHtml;
function replaceHeading(html:string,level:number):string {
const re = new RegExp(`<summary class="excalidraw-setting-h${level}">([^<]+)<\/summary>`,"g");
return html.replaceAll(re,`<summary class="excalidraw-setting-h${level}"><h${level}>$1</h${level}></summary>`);
}
let x = replaceHeading(html,1);
x = replaceHeading(x,2);
x = replaceHeading(x,3);
x = replaceHeading(x,4);
x = x.replaceAll(/<div class="setting-item-name">([^<]+)<\/div>/g,"<h5>$1</h5>");
const md = htmlToMarkdown(x);
window.navigator.clipboard.writeText(md);
new Notice(t("SEARCH_COPIED_TO_CLIPBOARD"));
};
this.showHideButton.onclick = () => {
const setOpacity = (value:string|null) => {
this.inputContainer.style.opacity = value;
this.prevButton.style.opacity = value;
this.nextButton.style.opacity = value;
this.exportMarkdown.style.opacity = value;
this.customElemenentContainer.style.opacity = value;
}
if(this.showHideButton.hasClass("search-visible")) {
this.showHideButton.removeClass("search-visible");
this.showHideButton.addClass("search-hidden");
this.searchBarWrapper.style.backgroundColor = "transparent";
setOpacity("0");
this.showHideButton.innerHTML = getIcon("maximize-2").outerHTML;
} else {
this.showHideButton.removeClass("search-hidden");
this.showHideButton.addClass("search-visible");
this.searchBarWrapper.style.backgroundColor = null;
setOpacity(null);
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
}
}
this.searchBar.addEventListener("input", (e) => {
this.clearHighlights();
const searchTerm = (e.target as HTMLInputElement).value;
if (searchTerm && searchTerm.length > 0) {
this.highlightSearchTerm(searchTerm);
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
this.hitCount.textContent = totalHits > 0 ? `1 / ${totalHits}` : "";
setTimeout(() => this.navigateSearchResults("next"));
} else {
this.hitCount.textContent = "";
}
});
this.searchBar.addEventListener("keydown", (e) => {
// If Ctrl/Cmd + F is pressed, focus on search bar
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
this.searchBar.focus();
}
// If Enter is pressed, navigate to next result
else if (e.key === "Enter") {
e.preventDefault();
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
}
});
}
/**
* Get the search bar wrapper element to add to the DOM
*/
public getSearchBarWrapper(): HTMLElement {
return this.searchBarWrapper;
}
/**
* Highlight all instances of the search term in the content
*/
public highlightSearchTerm(searchTerm: string): void {
// Create a walker to traverse text nodes
const walker = document.createTreeWalker(
this.contentDiv,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
}
}
);
const nodesToReplace: Text[] = [];
while (walker.nextNode()) {
nodesToReplace.push(walker.currentNode as Text);
}
nodesToReplace.forEach(node => {
const nodeContent = node.nodeValue!;
const newNode = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
// Iterate over all matches in the text node
while ((match = regex.exec(nodeContent)) !== null) {
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
const highlighted = document.createElement('mark');
highlighted.className = 'search-highlight';
highlighted.textContent = match[0];
highlighted.classList.add('search-result');
newNode.appendChild(before);
newNode.appendChild(highlighted);
lastIndex = regex.lastIndex;
}
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
node.replaceWith(newNode);
});
}
/**
* Remove all search highlights
*/
public clearHighlights(): void {
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
el.outerHTML = el.innerHTML;
});
}
/**
* Navigate to next or previous search result
*/
public navigateSearchResults(direction: "next" | "previous"): void {
const highlights: HTMLElement[] = Array.from(
this.contentDiv.querySelectorAll("mark.search-highlight")
);
if (highlights.length === 0) return;
const currentActiveIndex = highlights.findIndex((highlight) =>
highlight.classList.contains("active-highlight")
);
if (currentActiveIndex !== -1) {
highlights[currentActiveIndex].classList.remove("active-highlight");
highlights[currentActiveIndex].style.border = "none";
}
let nextActiveIndex = 0;
if (direction === "next") {
nextActiveIndex =
currentActiveIndex === highlights.length - 1
? 0
: currentActiveIndex + 1;
} else if (direction === "previous") {
nextActiveIndex =
currentActiveIndex === 0
? highlights.length - 1
: currentActiveIndex - 1;
}
const nextActiveHighlight = highlights[nextActiveIndex];
nextActiveHighlight.classList.add("active-highlight");
// Expand all parent details elements
this.expandParentDetails(nextActiveHighlight);
// Use setTimeout to ensure DOM has time to update after expanding details
setTimeout(() => {
nextActiveHighlight.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 100);
// Update the hit count
this.hitCount.textContent = `${nextActiveIndex + 1} / ${highlights.length}`;
}
/**
* Expand all parent <details> elements to make the element visible
*/
private expandParentDetails(element: HTMLElement): void {
let parent = element.parentElement;
while (parent) {
if (parent.tagName === "DETAILS") {
parent.setAttribute("open", "");
}
parent = parent.parentElement;
}
}
}

View File

@@ -2,7 +2,7 @@ import { ExcalidrawEmbeddableElement, ExcalidrawFrameElement, ExcalidrawImageEle
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import { getEA } from "src/core";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { getCropFileNameAndFolder, getListOfTemplateFiles, splitFolderAndFilename } from "./fileUtils";
import { getCropFileNameAndFolder, getListOfTemplateFiles } from "./fileUtils";
import { Notice, TFile } from "obsidian";
import { Radians } from "@zsviczian/excalidraw/types/math/src/types";

View File

@@ -11,7 +11,7 @@ import ExcalidrawView from "src/view/ExcalidrawView";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
* @param filepath
* @returns folderpath will be normalized. This means "/" for root folder and no trailing "/" for other folders
* @returns returns "" for root folder and normalized path for subfolders (no trailing "/", e.g. "folder/subfolder")
*/
type ImageExtension = keyof typeof IMAGE_MIME_TYPES;
@@ -23,11 +23,13 @@ export function splitFolderAndFilename(filepath: string): {
} {
const lastIndex = filepath.lastIndexOf("/");
const filename = lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1);
const lastDotIndex = filename.lastIndexOf(".");
const folderpath = filepath.substring(0, lastIndex);
return {
folderpath: normalizePath(filepath.substring(0, lastIndex)),
folderpath: folderpath ? normalizePath(folderpath) : "",
filename,
basename: filename.replace(/\.[^/.]+$/, ""),
extension: filename.substring(filename.lastIndexOf(".") + 1),
extension: lastDotIndex > 0 ? filename.substring(lastDotIndex + 1) : "",
};
}
@@ -540,7 +542,7 @@ export async function importFileToVault(app: App, fname: string, content: string
export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer | Blob): Promise<TFile> {
const {folderpath} = splitFolderAndFilename(path);
if(folderpath && folderpath !== "/") {
if(folderpath) {
await checkAndCreateFolder(folderpath);
}
const file = app.vault.getAbstractFileByPath(normalizePath(path));

View File

@@ -3,10 +3,10 @@ import {
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../core/main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./fileUtils";
import { splitFolderAndFilename } from "./fileUtils";
import { linkClickModifierType, ModifierKeys } from "./modifierkeyHelper";
import { DEVICE, EXCALIDRAW_PLUGIN, REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml from "js-yaml";
@@ -174,25 +174,13 @@ export const getAttachmentsFolderAndFilePath = async (
activeViewFilePath: string,
newFileName: string
): Promise<{ folder: string; filepath: 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 === "/") {
folder = "";
}
await checkAndCreateFolder(folder);
const { basename, extension } = splitFolderAndFilename(newFileName);
const activeViewFile = app.vault.getFileByPath(activeViewFilePath);
const attachmentFilePath = await app.vault.getAvailablePathForAttachments(basename, extension, activeViewFile);
const { folderpath } = splitFolderAndFilename(attachmentFilePath);
return {
folder,
filepath: normalizePath(
folder === "" ? newFileName : `${folder}/${newFileName}`
),
folder: folderpath,
filepath: attachmentFilePath
};
};

View File

@@ -47,7 +47,7 @@ declare module "obsidian" {
): WorkspaceLeaf;
}
interface Vault {
getConfig(option: "attachmentFolderPath"): string;
getAvailablePathForAttachments(filename: string, extension: string, file: TFile | null): Promise<string>
}
}

View File

@@ -4041,7 +4041,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(st.newElement?.type === "freedraw") {
this.freedrawLastActiveTimestamp = Date.now();
}
if (st.newElement || st.editingTextElement || st.editingLinearElement) {
if (
st.newElement ||
st.editingTextElement ||
(st.selectedLinearElement && st.selectedLinearElement.isEditing)
) {
this.plugin.wasPenModeActivePreviously = st.penMode;
}
this.viewModeEnabled = st.viewModeEnabled;
@@ -4083,7 +4087,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
/*st.resizingElement === null &&
st.newElement === null &&
st.editingGroupId === null &&*/
st.editingLinearElement === null
(st.selectedLinearElement === null || !st.selectedLinearElement.isEditing)
) {
this.checkSceneVersion(et);
}

View File

@@ -431,25 +431,6 @@ div.excalidraw-draginfo {
margin: auto;
}
.modal-content.excalidraw-scriptengine-install .search-bar-wrapper {
position: sticky;
top: 1rem;
margin-right: 1rem;
display: flex;
align-items: center;
gap: 5px;
flex-wrap: nowrap;
z-index: 10;
background: var(--background-secondary);
padding: 0.5rem;
border-bottom: 1px solid var(--background-modifier-border);
float: right;
max-width: 28rem;
}
div.search-bar-wrapper input {
margin-right: -0.5rem;
}
.modal-content.excalidraw-scriptengine-install .hit-count {
margin-left: 0.5em;
@@ -641,20 +622,27 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
}
.excalidraw-settings-links-container {
display: flex; /* Align SVG and text horizontally */
align-items: center; /* Center SVG and text vertically */
text-decoration: none; /* Remove underline from links */
color: inherit; /* Inherit text color */
text-align: center;
align-items: center;
color: inherit;
display: flex;
gap: .3em;
text-align: center;
text-decoration: none;
flex-wrap: wrap;
justify-content: center;
flex-direction: row;
}
.excalidraw-settings-links-container a {
display: flex; /* Align children horizontally */
align-items: center; /* Center items vertically */
text-align: left;
margin-right: 4px;
margin-left: 4px;
}
.excalidraw-settings-links-container svg {
.excalidraw-settings-links-container svg,
.ex-setting-actions-container svg {
margin-right: 8px;
height: 30px;
width: 30px;
@@ -776,3 +764,96 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw-prompt-buttonbar-bottom > div:last-child {
margin-left: auto;
}
.excalidraw-search.document-search-container {
display: flex;
flex-direction: column;
background: var(--background-secondary);
border-radius: 8px;
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.10);
padding: 0.5em 0.8em;
margin-bottom: 2em;
min-width: 18rem;
position: sticky;
top: 1rem;
margin-right: 1rem;
margin-left: 1rem;
}
.excalidraw-search .document-search {
align-items: center;
max-width: none;
}
.excalidraw-search .search-input-container.document-search-input {
display: flex;
align-items: center;
flex: 1 1 auto;
background: var(--background-primary);
border-radius: 6px;
border: 1px solid var(--background-modifier-border);
min-width: 0;
}
.excalidraw-search .search-input-container .clickable-icon {
display: flex;
align-items: center;
color: var(--text-faint);
}
.excalidraw-search .search-input-container input[type="text"] {
background: transparent;
border: none;
outline: none;
color: var(--text-normal);
font-size: 1em;
flex: 1 1 auto;
padding: 0.1em 2em;
margin: 0;
}
.excalidraw-search .document-search-count {
margin-left: 0.5em;
color: var(--text-faint);
font-size: 0.95em;
white-space: nowrap;
min-width: 3.5em;
text-align: right;
}
.excalidraw-search .document-search-buttons {
display: flex;
align-items: center;
gap: 2px;
}
.excalidraw-search .document-search-button {
background: none;
border: none;
outline: none;
box-shadow: none;
padding: 0.1em 0.2em;
margin: 0 1px;
border-radius: 4px;
cursor: pointer;
color: var(--text-faint);
transition: background 0.15s;
height: 2em;
width: 2em;
display: flex;
align-items: center;
justify-content: center;
}
.excalidraw-search .document-search-button:hover, .excalidraw-search .document-search-button:focus {
background: var(--background-modifier-hover);
color: var(--text-accent);
}
.excalidraw-search .document-search-button svg {
width: 1.3em;
height: 1.3em;
stroke: currentColor;
fill: none;
pointer-events: none;
}