mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
18 Commits
2.14.0
...
inside-arr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e60df0b47 | ||
|
|
e574d84b84 | ||
|
|
8f3ace89ea | ||
|
|
e4e4a07aa8 | ||
|
|
7a90bf753f | ||
|
|
6221d985ac | ||
|
|
41065ce9f9 | ||
|
|
351977f0d3 | ||
|
|
01b0b64a08 | ||
|
|
938e93cc43 | ||
|
|
cebcd2b43c | ||
|
|
80b6e30040 | ||
|
|
6e92a6a399 | ||
|
|
992ae8b238 | ||
|
|
5cd07e7766 | ||
|
|
07817a4d2d | ||
|
|
a22bb4c58c | ||
|
|
43e00a51be |
660
ea-scripts/Printable Layout Wizard.md
Normal file
660
ea-scripts/Printable Layout Wizard.md
Normal 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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```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();
|
||||
1
ea-scripts/Printable Layout Wizard.svg
Normal file
1
ea-scripts/Printable Layout Wizard.svg
Normal 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 |
@@ -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
|
||||
|
||||

|
||||
`;
|
||||
|
||||
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
@@ -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
|
||||
|
||||
BIN
images/scripts-layout-wizard-01.png
Normal file
BIN
images/scripts-layout-wizard-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
images/scripts-layout-wizard-02.png
Normal file
BIN
images/scripts-layout-wizard-02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.14.0",
|
||||
"version": "2.14.1-beta-2",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.18.0-25",
|
||||
"@zsviczian/excalidraw": "0.18.0-27",
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
|
||||
@@ -49,7 +49,7 @@ const jsxRuntimeShim = `
|
||||
|
||||
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
|
||||
|
||||
const LANGUAGES = ['ru', 'zh-cn', 'zh-tw']; //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(";")) {
|
||||
|
||||
@@ -86,7 +86,6 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
export let {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
@@ -120,7 +119,6 @@ export function updateExcalidrawLib() {
|
||||
const requiredFunctions = [
|
||||
'sceneCoordsToViewportCoords',
|
||||
'viewportCoordsToSceneCoords',
|
||||
'determineFocusDistance',
|
||||
'intersectElementWithLine',
|
||||
'getCommonBoundingBox',
|
||||
'measureText',
|
||||
@@ -138,7 +136,6 @@ export function updateExcalidrawLib() {
|
||||
({
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
@@ -262,6 +259,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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
149
src/lang/langDiffWithEn.js
Normal file
149
src/lang/langDiffWithEn.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -419,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
@@ -195,12 +195,15 @@ export default {
|
||||
//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",
|
||||
@@ -212,6 +215,8 @@ export default {
|
||||
LINKS_VTW: "参加研讨会",
|
||||
LINKS_BOOK_ARIA: "阅读我的视觉思维著作《Sketch Your Mind》",
|
||||
LINKS_BOOK: "阅读书籍",
|
||||
LINKS_WIKI : "插件 Wiki" ,
|
||||
LINKS_WIKI_ARIA : "探索 Excalidraw 插件 Wiki" ,
|
||||
|
||||
RELEASE_NOTES_NAME: "显示更新说明",
|
||||
RELEASE_NOTES_DESC:
|
||||
@@ -943,7 +948,7 @@ FILENAME_HEAD: "文件名",
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "缩放至标题",
|
||||
PIN_VIEW: "锁定视图",
|
||||
DO_NOT_PIN_VIEW: "不锁定视图",
|
||||
DO_NOT_PIN_VIEW: "取消锁定视图",
|
||||
NARROW_TO_BLOCK: "缩放至块",
|
||||
SHOW_ENTIRE_FILE: "显示全部",
|
||||
SELECT_SECTION: "从文档选择章节",
|
||||
|
||||
@@ -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>" +
|
||||
@@ -922,8 +947,12 @@ FILENAME_HEAD: "檔名",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "縮放至標題",
|
||||
PIN_VIEW: "釘選視圖",
|
||||
DO_NOT_PIN_VIEW: "取消釘選視圖",
|
||||
NARROW_TO_BLOCK: "縮放至塊",
|
||||
SHOW_ENTIRE_FILE: "顯示全部",
|
||||
SELECT_SECTION: "從文件選擇區塊",
|
||||
SELECT_VIEW: "從基礎選擇視圖",
|
||||
ZOOM_TO_FIT: "縮放至合適大小",
|
||||
RELOAD: "過載連結",
|
||||
OPEN_IN_BROWSER: "在瀏覽器中開啟",
|
||||
|
||||
@@ -17,6 +17,17 @@ 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":`
|
||||
## New
|
||||
- New frontmatter option \`excalidraw-embed-md\`. When set to \`true\`, embedding this Excalidraw file into another markdown file will display its markdown content (the "back of the note") 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).
|
||||
|
||||
## Fixed
|
||||
- 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.
|
||||
`,
|
||||
"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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
COLOR_NAMES,
|
||||
fileid,
|
||||
GITHUB_RELEASES,
|
||||
determineFocusDistance,
|
||||
getCommonBoundingBox,
|
||||
getLineHeight,
|
||||
getMaximumGroups,
|
||||
@@ -1745,26 +1744,12 @@ export class ExcalidrawAutomate {
|
||||
lastCommittedPoint: null,
|
||||
startBinding: {
|
||||
elementId: formatting?.startObjectId,
|
||||
focus: formatting?.startObjectId
|
||||
? determineFocusDistance(
|
||||
this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
endPoint,
|
||||
startPoint,
|
||||
)
|
||||
: 0.1,
|
||||
focus: 0.1,
|
||||
gap: GAP,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: formatting?.endObjectId,
|
||||
focus: formatting?.endObjectId
|
||||
? determineFocusDistance(
|
||||
this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
|
||||
elementsMap,
|
||||
startPoint,
|
||||
endPoint,
|
||||
)
|
||||
: 0.1,
|
||||
focus: 0.1,
|
||||
gap: GAP,
|
||||
},
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ContentSearcher {
|
||||
* Creates search UI elements styled like Obsidian's native search
|
||||
*/
|
||||
private createSearchElements(): void {
|
||||
this.searchBarWrapper = createDiv("document-search-container");
|
||||
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..."});
|
||||
|
||||
7
src/types/excalidrawLib.d.ts
vendored
7
src/types/excalidrawLib.d.ts
vendored
@@ -106,13 +106,6 @@ declare namespace ExcalidrawLib {
|
||||
},
|
||||
): { x: number; y: number };
|
||||
|
||||
function determineFocusDistance(
|
||||
element: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
a: GlobalPoint,
|
||||
b: GlobalPoint,
|
||||
): number;
|
||||
|
||||
function intersectElementWithLine(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: GlobalPoint,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ declare module "obsidian" {
|
||||
): WorkspaceLeaf;
|
||||
}
|
||||
interface Vault {
|
||||
getConfig(option: "attachmentFolderPath"): string;
|
||||
getAvailablePathForAttachments(filename: string, extension: string, file: TFile | null): Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
20
styles.css
20
styles.css
@@ -765,7 +765,7 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.document-search-container {
|
||||
.excalidraw-search.document-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background-secondary);
|
||||
@@ -780,12 +780,12 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.document-search {
|
||||
.excalidraw-search .document-search {
|
||||
align-items: center;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-input-container.document-search-input {
|
||||
.excalidraw-search .search-input-container.document-search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
@@ -795,13 +795,13 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-input-container .clickable-icon {
|
||||
.excalidraw-search .search-input-container .clickable-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.search-input-container input[type="text"] {
|
||||
.excalidraw-search .search-input-container input[type="text"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -812,7 +812,7 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.document-search-count {
|
||||
.excalidraw-search .document-search-count {
|
||||
margin-left: 0.5em;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.95em;
|
||||
@@ -821,13 +821,13 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.document-search-buttons {
|
||||
.excalidraw-search .document-search-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.document-search-button {
|
||||
.excalidraw-search .document-search-button {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -845,12 +845,12 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.document-search-button:hover, .document-search-button:focus {
|
||||
.excalidraw-search .document-search-button:hover, .excalidraw-search .document-search-button:focus {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.document-search-button svg {
|
||||
.excalidraw-search .document-search-button svg {
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
stroke: currentColor;
|
||||
|
||||
Reference in New Issue
Block a user