vault backup: 2026-09-15 18:32:01
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<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="svg-icon lucide-map-plus"><path d="m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V12"/><path d="M15 5.764V12"/><path d="M18 15v6"/><path d="M21 18h-6"/><path d="M9 3.236v15"/></svg>
|
||||
|
After Width: | Height: | Size: 505 B |
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||

|
||||
This script creates mindmap like lines(only right and down side are available). The line will starts according to the creation time of the elements. So you may need to create the header element first.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
const elements = ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
groups = ea.getMaximumGroups(elements);
|
||||
|
||||
els=[];
|
||||
elsx=[];
|
||||
elsy=[];
|
||||
for (i = 0, len =groups.length; i < len; i++) {
|
||||
els.push(ea.getLargestElement(groups[i]));
|
||||
elsx.push(ea.getLargestElement(groups[i]).x);
|
||||
elsy.push(ea.getLargestElement(groups[i]).y);
|
||||
}
|
||||
//line style setting
|
||||
ea.style.strokeColor = els[0].strokeColor;
|
||||
ea.style.strokeWidth = els[0].strokeWidth;
|
||||
ea.style.strokeStyle = els[0].strokeStyle;
|
||||
ea.style.strokeSharpness = els[0].strokeSharpness;
|
||||
//all min max x y
|
||||
let maxy = Math.max.apply(null, elsy);
|
||||
let indexmaxy=elsy.indexOf(maxy);
|
||||
let miny = Math.min.apply(null, elsy);
|
||||
let indexminy = elsy.indexOf(miny);
|
||||
let maxx = Math.max.apply(null, elsx);
|
||||
let indexmaxx = elsx.indexOf(maxx);
|
||||
let minx = Math.min.apply(null, elsx);
|
||||
let indexminx = elsx.indexOf(minx);
|
||||
//child max min x y
|
||||
let gmaxy = Math.max.apply(null, elsy.slice(1));
|
||||
let gindexmaxy=elsy.indexOf(gmaxy);
|
||||
let gminy = Math.min.apply(null, elsy.slice(1));
|
||||
let gindexminy = elsy.indexOf(gminy);
|
||||
let gmaxx = Math.max.apply(null, elsx.slice(1));
|
||||
let gindexmaxx = elsx.indexOf(gmaxx);
|
||||
let gminx = Math.min.apply(null, elsx.slice(1));
|
||||
let gindexminx = elsx.indexOf(gminx);
|
||||
let s=0;//Set line direction down as default
|
||||
if (indexminx==0 && els[0].x + els[0].width<=gminx) {
|
||||
s=1;
|
||||
}
|
||||
else if (indexminy == 0) {
|
||||
s=0;
|
||||
}
|
||||
var length_left;
|
||||
if(els[0].x + els[0].width * 2<=gminx){length_left=els[0].x + els[0].width * 1.5;}
|
||||
else {length_left=(els[0].x + els[0].width+gminx)/2;}
|
||||
|
||||
var length_down;
|
||||
if(els[0].y + els[0].height* 2.5<=gminy){length_down=els[0].y + els[0].height * 2;}
|
||||
else {length_down=(els[0].y + els[0].height+gminy)/2;}
|
||||
if(s) {
|
||||
ea.addLine(
|
||||
[[length_left,
|
||||
maxy + els[indexmaxy].height / 2],
|
||||
[length_left,
|
||||
miny + els[indexminy].height / 2]]
|
||||
);
|
||||
for (i = 1, len = groups.length; i < len; i++) {
|
||||
ea.addLine(
|
||||
[[els[i].x,
|
||||
els[i].y + els[i].height/2],
|
||||
[length_left,
|
||||
els[i].y + els[i].height/2]]
|
||||
);
|
||||
}
|
||||
ea.addArrow(
|
||||
[[els[0].x+els[0].width,
|
||||
els[0].y + els[0].height / 2],
|
||||
[length_left,
|
||||
els[0].y + els[0].height / 2]],
|
||||
{
|
||||
startArrowHead: "none",
|
||||
endArrowHead: "dot"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
else {
|
||||
ea.addLine(
|
||||
[[maxx + els[indexmaxx].width / 2,
|
||||
length_down],
|
||||
[minx + els[indexminx].width / 2,
|
||||
length_down]]
|
||||
);
|
||||
for (i = 1, len = groups.length; i < len; i++) {
|
||||
ea.addLine(
|
||||
[[els[i].x + els[i].width / 2,
|
||||
els[i].y],
|
||||
[els[i].x + els[i].width / 2,
|
||||
length_down]]
|
||||
);
|
||||
}
|
||||
ea.addArrow(
|
||||
[[els[0].x + els[0].width / 2,
|
||||
els[0].y + els[0].height],
|
||||
[els[0].x + els[0].width / 2,
|
||||
length_down]],
|
||||
{
|
||||
startArrowHead: "none",
|
||||
endArrowHead: "dot"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false,false,true);
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg width="607" height="541" viewBox="0 0 607 541" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M280 43.355V253.355H140V206.687L0 206.691V346.691H140V300.023H280V510.023C280 522.925 290.453 533.355 303.332 533.355H490.002C502.904 533.355 513.334 522.925 513.334 510.023C513.334 497.144 502.904 486.691 490.002 486.691H326.672V300.021H490.002C502.904 300.021 513.334 289.568 513.334 276.689C513.334 263.81 502.904 253.357 490.002 253.357H326.672V66.6869H490.002C502.904 66.6869 513.334 56.2569 513.334 43.3549C513.334 30.4529 502.904 20.0229 490.002 20.0229H303.332C290.453 20.019 280 30.4489 280 43.3509V43.355ZM46.67 300.025V253.357H93.338V300.025H46.67Z" fill="black"/>
|
||||
<rect x="540" y="23" width="39" height="39" fill="#D9D9D9"/>
|
||||
<rect x="503" width="104" height="95" fill="#D9D9D9"/>
|
||||
<rect x="503.5" y="0.5" width="103" height="94" fill="black" stroke="black"/>
|
||||
<rect x="503" y="223" width="104" height="95" fill="black"/>
|
||||
<rect x="503.5" y="446.5" width="103" height="94" fill="black" stroke="black"/>
|
||||
<path d="M532 243H580V291H532V243Z" fill="white"/>
|
||||
<path d="M532 475H580V523H532V475Z" fill="white"/>
|
||||
<path d="M532 243H580V291H532V243Z" fill="white"/>
|
||||
<path d="M532 23H580V71H532V23Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
|
||||
format **the left to right** mind map
|
||||
|
||||

|
||||
|
||||
# tree
|
||||
|
||||
Mind map is actually a tree, so you must have a **root node**. The script will determine **the leftmost element** of the selected element as the root element (node is excalidraw element, e.g. rectangle, diamond, ellipse, text, image, but it can't be arrow, line, freedraw, **group**)
|
||||
|
||||
The element connecting node and node must be an **arrow** and have the correct direction, e.g. **parent node -> children node**
|
||||
|
||||
# sort
|
||||
|
||||
The order of nodes in the Y axis or vertical direction is determined by **the creation time** of the arrow connecting it
|
||||
|
||||

|
||||
|
||||
So if you want to readjust the order, you can **delete arrows and reconnect them**
|
||||
|
||||
# setting
|
||||
|
||||
Script provides options to adjust the style of mind map, The option is at the bottom of the option of the exalidraw plugin(e.g. Settings -> Community plugins -> Excalidraw -> drag to bottom)
|
||||
|
||||
# problem
|
||||
|
||||
1. since the start bingding and end bingding of the arrow are easily disconnected from the node, so if there are unformatted parts, please **check the connection** and use the script to **reformat**
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
let settings = ea.getScriptSettings();
|
||||
//set default values on first run
|
||||
if (!settings["MindMap Format"]) {
|
||||
settings = {
|
||||
"MindMap Format": {
|
||||
value: "Excalidraw/MindMap Format",
|
||||
description:
|
||||
"This is prepared for the namespace of MindMap Format and does not need to be modified",
|
||||
},
|
||||
"default gap": {
|
||||
value: 10,
|
||||
description: "Interval size of element",
|
||||
},
|
||||
"curve length": {
|
||||
value: 40,
|
||||
description: "The length of the curve part in the mind map line",
|
||||
},
|
||||
"length between element and line": {
|
||||
value: 50,
|
||||
description:
|
||||
"The distance between the tail of the connection and the connecting elements of the mind map",
|
||||
},
|
||||
};
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const sceneElements = ea.getExcalidrawAPI().getSceneElements();
|
||||
|
||||
// default X coordinate of the middle point of the arc
|
||||
const defaultDotX = Number(settings["curve length"].value);
|
||||
// The default length from the middle point of the arc on the X axis
|
||||
const defaultLengthWithCenterDot = Number(
|
||||
settings["length between element and line"].value
|
||||
);
|
||||
// Initial trimming distance of the end point on the Y axis
|
||||
const initAdjLength = 4;
|
||||
// default gap
|
||||
const defaultGap = Number(settings["default gap"].value);
|
||||
|
||||
const setCenter = (parent, line) => {
|
||||
// Focus and gap need the api calculation of excalidraw
|
||||
// e.g. determineFocusDistance, but they are not available now
|
||||
// so they are uniformly set to 0/1
|
||||
line.startBinding.focus = 0;
|
||||
line.startBinding.gap = 1;
|
||||
line.endBinding.focus = 0;
|
||||
line.endBinding.gap = 1;
|
||||
line.x = parent.x + parent.width;
|
||||
line.y = parent.y + parent.height / 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* set the middle point of curve
|
||||
* @param {any} lineEl the line element of excalidraw
|
||||
* @param {number} height height of dot on Y axis
|
||||
* @param {number} [ratio=1] ,coefficient of the initial trimming distance of the end point on the Y axis, default is 1
|
||||
*/
|
||||
const setTopCurveDotOnLine = (lineEl, height, ratio = 1) => {
|
||||
if (lineEl.points.length < 3) {
|
||||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1] - height]);
|
||||
} else if (lineEl.points.length === 3) {
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] - height];
|
||||
} else {
|
||||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] - height];
|
||||
}
|
||||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||||
// adjust the curvature of the second line segment
|
||||
lineEl.points[2][1] = lineEl.points[1][1] - initAdjLength * ratio * 0.8;
|
||||
};
|
||||
|
||||
const setMidCurveDotOnLine = (lineEl) => {
|
||||
if (lineEl.points.length < 3) {
|
||||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1]]);
|
||||
} else if (lineEl.points.length === 3) {
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1]];
|
||||
} else {
|
||||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1]];
|
||||
}
|
||||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||||
lineEl.points[2][1] = lineEl.points[1][1];
|
||||
};
|
||||
|
||||
/**
|
||||
* set the middle point of curve
|
||||
* @param {any} lineEl the line element of excalidraw
|
||||
* @param {number} height height of dot on Y axis
|
||||
* @param {number} [ratio=1] ,coefficient of the initial trimming distance of the end point on the Y axis, default is 1
|
||||
*/
|
||||
const setBottomCurveDotOnLine = (lineEl, height, ratio = 1) => {
|
||||
if (lineEl.points.length < 3) {
|
||||
lineEl.points.splice(1, 0, [defaultDotX, lineEl.points[0][1] + height]);
|
||||
} else if (lineEl.points.length === 3) {
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] + height];
|
||||
} else {
|
||||
lineEl.points.splice(2, lineEl.points.length - 3);
|
||||
lineEl.points[1] = [defaultDotX, lineEl.points[0][1] + height];
|
||||
}
|
||||
lineEl.points[2][0] = lineEl.points[1][0] + defaultLengthWithCenterDot;
|
||||
// adjust the curvature of the second line segment
|
||||
lineEl.points[2][1] = lineEl.points[1][1] + initAdjLength * ratio * 0.8;
|
||||
};
|
||||
|
||||
const setTextXY = (rect, text) => {
|
||||
text.x = rect.x + (rect.width - text.width) / 2;
|
||||
text.y = rect.y + (rect.height - text.height) / 2;
|
||||
};
|
||||
|
||||
const setChildrenXY = (parent, children, line, elementsMap) => {
|
||||
x = parent.x + parent.width + line.points[2][0];
|
||||
y = parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
|
||||
distX = children.x - x;
|
||||
distY = children.y - y;
|
||||
|
||||
ea.getElementsInTheSameGroupWithElement(children, sceneElements).forEach((el) => {
|
||||
el.x = el.x - distX;
|
||||
el.y = el.y - distY;
|
||||
});
|
||||
|
||||
if (
|
||||
["rectangle", "diamond", "ellipse"].includes(children.type) &&
|
||||
![null, undefined].includes(children.boundElements)
|
||||
) {
|
||||
const textDesc = children.boundElements.filter(
|
||||
(el) => el.type === "text"
|
||||
)[0];
|
||||
if (textDesc !== undefined) {
|
||||
const textEl = elementsMap.get(textDesc.id);
|
||||
setTextXY(children, textEl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the height of the upper part of all child nodes
|
||||
* and the height of the lower part of all child nodes
|
||||
* @param {Number[]} childrenTotalHeightArr
|
||||
* @returns {Number[]} [topHeight, bottomHeight]
|
||||
*/
|
||||
const getNodeCurrentHeight = (childrenTotalHeightArr) => {
|
||||
if (childrenTotalHeightArr.length <= 0) return [0, 0];
|
||||
else if (childrenTotalHeightArr.length === 1)
|
||||
return [childrenTotalHeightArr[0] / 2, childrenTotalHeightArr[0] / 2];
|
||||
const heightArr = childrenTotalHeightArr;
|
||||
let topHeight = 0,
|
||||
bottomHeight = 0;
|
||||
const isEven = heightArr.length % 2 === 0;
|
||||
const mid = Math.floor(heightArr.length / 2);
|
||||
const topI = mid - 1;
|
||||
const bottomI = isEven ? mid : mid + 1;
|
||||
topHeight = isEven ? 0 : heightArr[mid] / 2;
|
||||
for (let i = topI; i >= 0; i--) {
|
||||
topHeight += heightArr[i];
|
||||
}
|
||||
bottomHeight = isEven ? 0 : heightArr[mid] / 2;
|
||||
for (let i = bottomI; i < heightArr.length; i++) {
|
||||
bottomHeight += heightArr[i];
|
||||
}
|
||||
return [topHeight, bottomHeight];
|
||||
};
|
||||
|
||||
/**
|
||||
* handle the height of each point in the single-level tree
|
||||
* @param {Array} lines
|
||||
* @param {Map} elementsMap
|
||||
* @param {Boolean} isEven
|
||||
* @param {Number} mid 'lines' array midpoint index
|
||||
* @returns {Array} height array corresponding to 'lines'
|
||||
*/
|
||||
const handleDotYValue = (lines, elementsMap, isEven, mid) => {
|
||||
const getTotalHeight = (line, elementsMap) => {
|
||||
return elementsMap.get(line.endBinding.elementId).totalHeight;
|
||||
};
|
||||
const getTopHeight = (line, elementsMap) => {
|
||||
return elementsMap.get(line.endBinding.elementId).topHeight;
|
||||
};
|
||||
const getBottomHeight = (line, elementsMap) => {
|
||||
return elementsMap.get(line.endBinding.elementId).bottomHeight;
|
||||
};
|
||||
const heightArr = new Array(lines.length).fill(0);
|
||||
const upI = mid === 0 ? 0 : mid - 1;
|
||||
const bottomI = isEven ? mid : mid + 1;
|
||||
let initHeight = isEven ? 0 : getTopHeight(lines[mid], elementsMap);
|
||||
for (let i = upI; i >= 0; i--) {
|
||||
heightArr[i] = initHeight + getBottomHeight(lines[i], elementsMap);
|
||||
initHeight += getTotalHeight(lines[i], elementsMap);
|
||||
}
|
||||
initHeight = isEven ? 0 : getBottomHeight(lines[mid], elementsMap);
|
||||
for (let i = bottomI; i < lines.length; i++) {
|
||||
heightArr[i] = initHeight + getTopHeight(lines[i], elementsMap);
|
||||
initHeight += getTotalHeight(lines[i], elementsMap);
|
||||
}
|
||||
return heightArr;
|
||||
};
|
||||
|
||||
/**
|
||||
* format single-level tree
|
||||
* @param {any} parent
|
||||
* @param {Array} lines
|
||||
* @param {Map} childrenDescMap
|
||||
* @param {Map} elementsMap
|
||||
*/
|
||||
const formatTree = (parent, lines, childrenDescMap, elementsMap) => {
|
||||
lines.forEach((item) => setCenter(parent, item));
|
||||
|
||||
const isEven = lines.length % 2 === 0;
|
||||
const mid = Math.floor(lines.length / 2);
|
||||
const heightArr = handleDotYValue(lines, childrenDescMap, isEven, mid);
|
||||
lines.forEach((item, index) => {
|
||||
if (isEven) {
|
||||
if (index < mid) setTopCurveDotOnLine(item, heightArr[index], index + 1);
|
||||
else setBottomCurveDotOnLine(item, heightArr[index], index - mid + 1);
|
||||
} else {
|
||||
if (index < mid) setTopCurveDotOnLine(item, heightArr[index], index + 1);
|
||||
else if (index === mid) setMidCurveDotOnLine(item);
|
||||
else setBottomCurveDotOnLine(item, heightArr[index], index - mid);
|
||||
}
|
||||
});
|
||||
lines.forEach((item) => {
|
||||
if (item.endBinding !== null) {
|
||||
setChildrenXY(
|
||||
parent,
|
||||
elementsMap.get(item.endBinding.elementId),
|
||||
item,
|
||||
elementsMap
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const generateTree = (elements) => {
|
||||
const elIdMap = new Map([[elements[0].id, elements[0]]]);
|
||||
let minXEl = elements[0];
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
elIdMap.set(elements[i].id, elements[i]);
|
||||
if (
|
||||
!(elements[i].type === "arrow" || elements[i].type === "line") &&
|
||||
elements[i].x < minXEl.x
|
||||
) {
|
||||
minXEl = elements[i];
|
||||
}
|
||||
}
|
||||
const root = {
|
||||
el: minXEl,
|
||||
totalHeight: minXEl.height,
|
||||
topHeight: 0,
|
||||
bottomHeight: 0,
|
||||
linkChildrensLines: [],
|
||||
isLeafNode: false,
|
||||
children: [],
|
||||
};
|
||||
const preIdSet = new Set(); // The id_set of Elements that is already in the tree, avoid a dead cycle
|
||||
const dfsForTreeData = (root) => {
|
||||
if (preIdSet.has(root.el.id)) {
|
||||
return 0;
|
||||
}
|
||||
preIdSet.add(root.el.id);
|
||||
let lines = root.el.boundElements.filter(
|
||||
(el) =>
|
||||
el.type === "arrow" &&
|
||||
!preIdSet.has(el.id) &&
|
||||
elIdMap.get(el.id)?.startBinding?.elementId === root.el.id
|
||||
);
|
||||
if (lines.length === 0) {
|
||||
root.isLeafNode = true;
|
||||
root.totalHeight = root.el.height + 2 * defaultGap;
|
||||
[root.topHeight, root.bottomHeight] = [
|
||||
root.totalHeight / 2,
|
||||
root.totalHeight / 2,
|
||||
];
|
||||
return root.totalHeight;
|
||||
} else {
|
||||
lines = lines.map((elementDesc) => {
|
||||
preIdSet.add(elementDesc.id);
|
||||
return elIdMap.get(elementDesc.id);
|
||||
});
|
||||
}
|
||||
|
||||
const linkChildrensLines = [];
|
||||
lines.forEach((el) => {
|
||||
const line = el;
|
||||
if (
|
||||
line &&
|
||||
line.endBinding !== null &&
|
||||
line.endBinding !== undefined &&
|
||||
!preIdSet.has(elIdMap.get(line.endBinding.elementId).id)
|
||||
) {
|
||||
const children = elIdMap.get(line.endBinding.elementId);
|
||||
linkChildrensLines.push(line);
|
||||
root.children.push({
|
||||
el: children,
|
||||
totalHeight: 0,
|
||||
topHeight: 0,
|
||||
bottomHeight: 0,
|
||||
linkChildrensLines: [],
|
||||
isLeafNode: false,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let totalHeight = 0;
|
||||
root.children.forEach((el) => (totalHeight += dfsForTreeData(el)));
|
||||
|
||||
root.linkChildrensLines = linkChildrensLines;
|
||||
if (root.children.length === 0) {
|
||||
root.isLeafNode = true;
|
||||
root.totalHeight = root.el.height + 2 * defaultGap;
|
||||
[root.topHeight, root.bottomHeight] = [
|
||||
root.totalHeight / 2,
|
||||
root.totalHeight / 2,
|
||||
];
|
||||
} else if (root.children.length > 0) {
|
||||
root.totalHeight = Math.max(root.el.height + 2 * defaultGap, totalHeight);
|
||||
[root.topHeight, root.bottomHeight] = getNodeCurrentHeight(
|
||||
root.children.map((item) => item.totalHeight)
|
||||
);
|
||||
}
|
||||
|
||||
return totalHeight;
|
||||
};
|
||||
dfsForTreeData(root);
|
||||
const dfsForFormat = (root) => {
|
||||
if (root.isLeafNode) return;
|
||||
const childrenDescMap = new Map(
|
||||
root.children.map((item) => [item.el.id, item])
|
||||
);
|
||||
formatTree(root.el, root.linkChildrensLines, childrenDescMap, elIdMap);
|
||||
root.children.forEach((el) => dfsForFormat(el));
|
||||
};
|
||||
dfsForFormat(root);
|
||||
};
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
generateTree(elements);
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
await ea.addElementsToView(false, false);
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1673428425027" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1642" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24"><path d="M388.7 542.88c-16.57 0-30-13.43-30-30s13.43-30 30-30c52.3 0 94.85-42.55 94.85-94.85v-67.81c0-40.96 15.84-79.58 44.6-108.74 28.76-29.16 67.16-45.53 108.12-46.1l3.43-0.05c16.57-0.22 30.18 13.02 30.41 29.58 0.23 16.57-13.02 30.18-29.58 30.41l-3.43 0.05c-51.58 0.71-93.55 43.25-93.55 94.84v67.81c0 85.4-69.47 154.86-154.85 154.86z" fill="#000000" p-id="1643"></path><path d="M640.12 860.42h-0.42l-3.43-0.05c-40.96-0.56-79.36-16.93-108.12-46.09s-44.6-67.78-44.6-108.74v-67.8c0-52.3-42.55-94.85-94.85-94.85-16.57 0-30-13.43-30-30s13.43-30 30-30c85.38 0 154.85 69.47 154.85 154.85v67.8c0 51.59 41.96 94.13 93.55 94.84l3.43 0.05c16.57 0.23 29.81 13.84 29.59 30.41-0.24 16.42-13.62 29.58-30 29.58z" fill="#000000" p-id="1644"></path><path d="M640.11 542.88H388.7c-16.57 0-30-13.43-30-30s13.43-30 30-30h251.42c16.57 0 30 13.43 30 30-0.01 16.57-13.44 30-30.01 30z" fill="#000000" p-id="1645"></path><path d="M343.89 638.95H137.78c-38.6 0-70-31.4-70-70V456.81c0-38.6 31.4-70 70-70h206.11c38.6 0 70 31.4 70 70v112.13c0 38.6-31.4 70.01-70 70.01zM137.78 446.81c-5.51 0-10 4.49-10 10v112.13c0 5.51 4.49 10 10 10h206.11c5.51 0 10-4.49 10-10V456.81c0-5.51-4.49-10-10-10H137.78zM830.16 316.96h-93.98c-69.51 0-126.07-56.55-126.07-126.07S666.66 64.83 736.18 64.83h93.98c69.51 0 126.07 56.55 126.07 126.07-0.01 69.5-56.56 126.06-126.07 126.06z m-93.98-192.13c-36.43 0-66.07 29.64-66.07 66.07s29.64 66.07 66.07 66.07h93.98c36.43 0 66.07-29.64 66.07-66.07s-29.64-66.07-66.07-66.07h-93.98zM830.16 638.95h-93.98c-69.51 0-126.07-56.55-126.07-126.07 0-69.51 56.55-126.07 126.07-126.07h93.98c69.51 0 126.07 56.55 126.07 126.07-0.01 69.51-56.56 126.07-126.07 126.07z m-93.98-192.14c-36.43 0-66.07 29.64-66.07 66.07 0 36.43 29.64 66.07 66.07 66.07h93.98c36.43 0 66.07-29.64 66.07-66.07 0-36.43-29.64-66.07-66.07-66.07h-93.98z" fill="#000000" p-id="1646"></path><path d="M830.16 959.17h-93.98c-69.51 0-126.07-56.55-126.07-126.07s56.55-126.07 126.07-126.07h93.98c69.51 0 126.07 56.55 126.07 126.07s-56.56 126.07-126.07 126.07z m-93.98-192.13c-36.43 0-66.07 29.64-66.07 66.07s29.64 66.07 66.07 66.07h93.98c36.43 0 66.07-29.64 66.07-66.07s-29.64-66.07-66.07-66.07h-93.98z" fill="#000000" p-id="1647"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=Kwt_8WdOUT4" target="_blank"><img src ="https://i.ytimg.com/vi/Kwt_8WdOUT4/maxresdefault.jpg" style="width:560px;"></a>
|
||||
|
||||
|
||||
```js*/
|
||||
const el = ea.getViewSelectedElements().filter(el=>el.type==="image")[0];
|
||||
if(!el) {
|
||||
new Notice("Select a PDF page");
|
||||
return;
|
||||
}
|
||||
const f = ea.getViewFileForImageElement(el);
|
||||
if(f.extension.toLowerCase() !== "pdf") {
|
||||
new Notice("Select a PDF page");
|
||||
return;
|
||||
}
|
||||
|
||||
const pageNum = parseInt(ea.targetView.excalidrawData.getFile(el.fileId).linkParts.ref.replace(/\D/g, ""));
|
||||
if(isNaN(pageNum)) {
|
||||
new Notice("Can't find page number");
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfDoc = await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const text = await page.getTextContent();
|
||||
if(!text) {
|
||||
new Notice("Could not get text");
|
||||
return;
|
||||
}
|
||||
pdfDoc.destroy();
|
||||
window.navigator.clipboard.writeText(
|
||||
text.items.reduce((acc, cur) => acc + cur.str.replace(/\x00/ug, '') + (cur.hasEOL ? "\n" : ""),"")
|
||||
);
|
||||
new Notice("Page text is available on the clipboard");
|
||||
@@ -0,0 +1 @@
|
||||
<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="svg-icon lucide-file-text"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
Palm Guard: A mobile-friendly drawing mode for Excalidraw that prevents accidental palm touches by hiding UI controls and entering fullscreen mode. Perfect for drawing with a stylus on tablets.
|
||||
|
||||
Features:
|
||||
- Enters fullscreen to maximize drawing space (configurable in plugin script settings)
|
||||
- Hides all UI controls to prevent accidental taps
|
||||
- Provides a minimal floating toolbar with toggle visibility button
|
||||
- Enables a completely distraction-free canvas even on desktop devices by hiding the main toolbar and all chrome while keeping a tiny movable toggle control (addresses immersive canvas / beyond Zen Mode request)
|
||||
- Draggable toolbar can be positioned anywhere on screen
|
||||
- Exit Palm Guard mode with a single tap
|
||||
- Press the hotkey you configured for this script in Obsidian's Hotkey settings (e.g., ALT+X) to toggle UI visibility; if no hotkey is set, use the on-screen toggle button.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.14.2")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
function requestFullscreen() {
|
||||
const el = ea.targetView.ownerDocument.body;
|
||||
if (el.requestFullscreen) {
|
||||
el.requestFullscreen();
|
||||
} else if (el.webkitRequestFullscreen) {
|
||||
el.webkitRequestFullscreen();
|
||||
}
|
||||
ea.targetView.gotoFullscreen();
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
const doc = ea.targetView.ownerDocument;
|
||||
if (doc.exitFullscreen) {
|
||||
doc.exitFullscreen();
|
||||
} else if (doc.webkitExitFullscreen) {
|
||||
doc.webkitExitFullscreen();
|
||||
}
|
||||
ea.targetView.exitFullscreen();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if(window.excalidrawPalmGuard) {
|
||||
window.excalidrawPalmGuard()
|
||||
return;
|
||||
}
|
||||
const modal = new ea.FloatingModal(ea.plugin.app);
|
||||
if (modal.bgEl) {
|
||||
modal.containerEl.removeChild(modal.bgEl);
|
||||
}
|
||||
modal.modalEl.style.borderRadius = "6px";
|
||||
modal.contentEl.style.padding = "4px";
|
||||
const FULLSCREEN = "Goto fullscreen?";
|
||||
let settings = ea.getScriptSettings() || {};
|
||||
if(!settings[FULLSCREEN]) {
|
||||
settings[FULLSCREEN] = { value: true };
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
//added only to clean up settings if someone installed the initial version of the script
|
||||
const HOTKEY_MODIFIERS = "PalmGuard Toggle UI Hotkey Modifiers";
|
||||
const HOTKEY_KEY = "PalmGuard Toggle UI Hotkey Key";
|
||||
if(settings[HOTKEY_MODIFIERS] || settings[HOTKEY_KEY]) {
|
||||
delete settings[HOTKEY_MODIFIERS];
|
||||
delete settings[HOTKEY_KEY];
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const enableFullscreen = settings[FULLSCREEN].value;
|
||||
|
||||
// Initialize state
|
||||
let uiHidden = true;
|
||||
let currentIcon = "eye";
|
||||
let layerUIWrapper = ea.targetView.contentEl.querySelector(".excalidraw.excalidraw-container > .layer-ui__wrapper");
|
||||
const toolbar = ea.targetView.contentEl.querySelector(".excalidraw > .Island");
|
||||
let toolbarActive = toolbar?.style.display === "block";
|
||||
let prevHiddenState = false;
|
||||
|
||||
// Function to toggle UI visibility
|
||||
const toggleUIVisibility = (hidden) => {
|
||||
if(hidden === prevHiddenState) return hidden;
|
||||
prevHiddenState = hidden;
|
||||
if (!!layerUIWrapper) {
|
||||
try {
|
||||
if(hidden) {
|
||||
layerUIWrapper.style.display = "none";
|
||||
} else {
|
||||
layerUIWrapper.style.display = "block";
|
||||
}
|
||||
} catch {};
|
||||
} else {
|
||||
try{
|
||||
const topBar = ea.targetView.containerEl.querySelector(".App-top-bar");
|
||||
const bottomBar = ea.targetView.containerEl.querySelector(".App-bottom-bar");
|
||||
const sidebarToggle = ea.targetView.containerEl.querySelector(".sidebar-toggle");
|
||||
const plugins = ea.targetView.containerEl.querySelector(".plugins-container");
|
||||
|
||||
if(hidden) {
|
||||
if (toolbarActive && (toolbar?.style.display === "none")) {
|
||||
toolbarActive = false;
|
||||
}
|
||||
if (toolbarActive = toolbar?.style.display === "block") {
|
||||
toolbarActive = true;
|
||||
};
|
||||
}
|
||||
|
||||
const display = hidden ? "none" : "";
|
||||
|
||||
if (topBar) topBar.style.display = display;
|
||||
if (bottomBar) bottomBar.style.display = display;
|
||||
if (sidebarToggle) sidebarToggle.style.display = display;
|
||||
if (plugins) plugins.style.display = display;
|
||||
if (toolbarActive) toolbar.style.display = hidden ? "none" : "block";
|
||||
modal.modalEl.style.opacity = hidden ? "0.4" : "0.8";
|
||||
} catch {};
|
||||
};
|
||||
return hidden;
|
||||
};
|
||||
|
||||
// Enter fullscreen view mode
|
||||
if(enableFullscreen) {
|
||||
requestFullscreen ();
|
||||
}
|
||||
setTimeout(()=>toggleUIVisibility(true),100);
|
||||
|
||||
// Create floating toolbar modal
|
||||
Object.assign(modal.modalEl.style, {
|
||||
width: "fit-content",
|
||||
minWidth: "fit-content",
|
||||
height: "fit-content",
|
||||
minHeight: "fit-content",
|
||||
paddingBottom: "4px",
|
||||
paddingTop: "16px",
|
||||
paddingRight: "4px",
|
||||
paddingLeft: "4px"
|
||||
});
|
||||
|
||||
modal.headerEl.style.display = "none";
|
||||
// Configure modal
|
||||
modal.titleEl.setText(""); // No title for minimal UI
|
||||
|
||||
// Create modal content
|
||||
modal.contentEl.createDiv({ cls: "palm-guard-toolbar" }, div => {
|
||||
const container = div.createDiv({
|
||||
attr: {
|
||||
style: "display: flex; flex-direction: column; background-color: var(--background-secondary); border-radius: 4px;"
|
||||
}
|
||||
});
|
||||
|
||||
// Button container
|
||||
const buttonContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;"
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle UI visibility button
|
||||
const toggleButton = buttonContainer.createEl("button", {
|
||||
cls: "palm-guard-btn clickable-icon",
|
||||
attr: {
|
||||
style: "background-color: var(--interactive-accent); color: var(--text-on-accent);"
|
||||
}
|
||||
});
|
||||
toggleButton.innerHTML = ea.obsidian.getIcon("eye").outerHTML;
|
||||
// Keyboard hotkey listener (only acts if hotkey configured)
|
||||
window.excalidrawPalmGuard = () => toggleButton.click();
|
||||
toggleButton.addEventListener("click", () => {
|
||||
uiHidden = !uiHidden;
|
||||
toggleUIVisibility(uiHidden);
|
||||
|
||||
// Toggle icon
|
||||
currentIcon = uiHidden ? "eye" : "eye-off";
|
||||
toggleButton.innerHTML = ea.obsidian.getIcon(currentIcon).outerHTML;
|
||||
});
|
||||
|
||||
// Exit button
|
||||
const exitButton = buttonContainer.createEl("button", {
|
||||
cls: "palm-guard-btn clickable-icon",
|
||||
attr: {
|
||||
style: "background-color: var(--background-secondary-alt); color: var(--text-normal);"
|
||||
}
|
||||
});
|
||||
exitButton.innerHTML = ea.obsidian.getIcon("cross").outerHTML;
|
||||
|
||||
exitButton.addEventListener("click", () => {
|
||||
modal.close();
|
||||
});
|
||||
|
||||
// Add CSS
|
||||
div.createEl("style", {
|
||||
text: `
|
||||
.palm-guard-btn:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.modal-close-button {
|
||||
display: none;
|
||||
}
|
||||
.palm-guard-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border-radius: 10%;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
});
|
||||
});
|
||||
|
||||
const autocloseTimer = setInterval(()=>{
|
||||
if(!ea.targetView) modal.close();
|
||||
},1000);
|
||||
|
||||
// Handle modal close (exit Palm Guard mode)
|
||||
modal.onClose = () => {
|
||||
// Show all UI elements
|
||||
toggleUIVisibility(false);
|
||||
|
||||
// Exit fullscreen
|
||||
if(ea.targetView && enableFullscreen) {
|
||||
exitFullscreen();
|
||||
}
|
||||
clearInterval(autocloseTimer);
|
||||
delete window.excalidrawPalmGuard;
|
||||
};
|
||||
|
||||
// Open the modal
|
||||
modal.open();
|
||||
|
||||
// Position the modal in the top left initially
|
||||
setTimeout(() => {
|
||||
const modalEl = modal.modalEl;
|
||||
const rect = ea.targetView.contentEl.getBoundingClientRect();
|
||||
if (modalEl) {
|
||||
modalEl.style.left = `${rect.left+10}px`;
|
||||
modalEl.style.top = `${rect.top+10}px`;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1 @@
|
||||
<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="svg-icon lucide-hand"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg>
|
||||
|
After Width: | Height: | Size: 475 B |
@@ -0,0 +1,864 @@
|
||||
/*
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.15.0")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(window.excalidrawPrintableLayoutWizardModal) {
|
||||
window.excalidrawPrintableLayoutWizardModal.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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).
|
||||
|
||||

|
||||
|
||||

|
||||
`;
|
||||
|
||||
async function run() {
|
||||
modal = new ea.FloatingModal(ea.plugin.app);
|
||||
window.excalidrawPrintableLayoutWizardModal = modal;
|
||||
modal.contentEl.empty();
|
||||
let shouldRestart = false;
|
||||
// Enable frame rendering
|
||||
const st = ea.getExcalidrawAPI().getAppState();
|
||||
let {enabled, clip, name, outline, markerName, markerEnabled} = st.frameRendering;
|
||||
if(!enabled || !name || !outline || !markerEnabled || !markerName) {
|
||||
ea.viewUpdateScene({
|
||||
appState: {
|
||||
frameRendering: {
|
||||
enabled: true,
|
||||
clip: clip,
|
||||
name: true,
|
||||
outline: true,
|
||||
markerName: true,
|
||||
markerEnabled: 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";
|
||||
const PRINT_EMPTY = "Print empty pages";
|
||||
const PRINT_MARKERS_ONLY = "Print only marker frames";
|
||||
|
||||
// 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 };
|
||||
settings[PRINT_EMPTY] = { value: false };
|
||||
settings[PRINT_MARKERS_ONLY] = { value: true };
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
//once off correction. In the first version I incorrectly used valueSet with wrong casing.
|
||||
if(settings[PAGE_SIZE].valueSet) {
|
||||
settings[PAGE_SIZE].valueset = settings[PAGE_SIZE].valueSet;
|
||||
delete settings[PAGE_SIZE].valueSet;
|
||||
settings[ORIENTATION].valueset = settings[ORIENTATION].valueSet;
|
||||
delete settings[ORIENTATION].valueSet;
|
||||
settings[MARGIN].valueset = settings[MARGIN].valueSet;
|
||||
delete settings[MARGIN].valueSet;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if(!settings[LOCK_FRAME]) {
|
||||
settings[LOCK_FRAME] = { value: true };
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if(!settings[PRINT_EMPTY]) {
|
||||
settings[PRINT_EMPTY] = { value: false };
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
|
||||
if(!settings[PRINT_MARKERS_ONLY]) {
|
||||
settings[PRINT_MARKERS_ONLY] = { value: true };
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
let lockFrame = settings[LOCK_FRAME].value;
|
||||
let shouldClose = settings[SHOULD_CLOSE].value;
|
||||
let shouldZoom = settings[SHOULD_ZOOM].value;
|
||||
let printEmptyPages = settings[PRINT_EMPTY].value;
|
||||
let printMarkersOnly = settings[PRINT_MARKERS_ONLY].value;
|
||||
|
||||
const getSortedFrames = () => {
|
||||
return ea.getViewElements()
|
||||
.filter(el => isEligibleFrame(el))
|
||||
.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
|
||||
};
|
||||
};
|
||||
|
||||
const isEligibleFrame = (el) => el.type === "frame" && (printMarkersOnly ? el.frameRole === "marker" : true);
|
||||
|
||||
// Check if there are frames in the scene and if a frame is selected
|
||||
let existingFrames = ea.getViewElements().filter(el => isEligibleFrame(el));
|
||||
let selectedFrame = ea.getViewSelectedElements().find(el => isEligibleFrame(el));
|
||||
|
||||
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 => isEligibleFrame(el));
|
||||
}
|
||||
}
|
||||
|
||||
const hasSelectedFrame = !!selectedFrame;
|
||||
|
||||
// rotation is now a temporary UI state controlled by the center button
|
||||
let rotateOnAdd = false;
|
||||
let centerRotateBtn = null;
|
||||
const setRotateBtnActive = (active) => {
|
||||
if (!centerRotateBtn) return;
|
||||
centerRotateBtn.classList.toggle("is-accent", active);
|
||||
centerRotateBtn.setAttribute("aria-pressed", active ? "true" : "false");
|
||||
};
|
||||
|
||||
// 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 = "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 => isEligibleFrame(el));
|
||||
if (!selectedFrame) {
|
||||
const { activeLockedId } = ea.getExcalidrawAPI().getAppState();
|
||||
if(activeLockedId) {
|
||||
selectedFrame = ea.getViewElements().find(el=>el.id === activeLockedId && isEligibleFrame(el));
|
||||
}
|
||||
if (!selectedFrame) return;
|
||||
}
|
||||
ea.viewUpdateScene({appState: {activeLockedId: null}});
|
||||
|
||||
const { frames, nextPageNumber } = findExistingPages();
|
||||
|
||||
// Get dimensions from selected frame, support optional rotation
|
||||
const dimensions = {
|
||||
width: rotateOnAdd ? selectedFrame.height : selectedFrame.width,
|
||||
height: rotateOnAdd ? selectedFrame.width : selectedFrame.height
|
||||
};
|
||||
|
||||
// Format page number with leading zero
|
||||
const pageName = `${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;
|
||||
}
|
||||
|
||||
const added = await addFrameElement(x, y, dimensions.width, dimensions.height, pageName);
|
||||
// reset the rotate toggle after adding the frame
|
||||
rotateOnAdd = false;
|
||||
setRotateBtnActive(false);
|
||||
return added;
|
||||
};
|
||||
|
||||
addFrameElement = async (x, y, width, height, pageName, repositionToCursor = false) => {
|
||||
const frameId = ea.addFrame(x, y, width, height, pageName);
|
||||
ea.getElement(frameId).frameRole = "marker";
|
||||
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 = ({ x, y, width, height }, padding=0) => {
|
||||
const top = y, left = x, right = x + width, bottom = y + height;
|
||||
const {topX, topY, width:w, height:h} = 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,
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: detect if any non-frame element overlaps the given area
|
||||
const hasElementsInArea = (area) => ea.getElementsInArea(ea.getViewElements(), area).length>0;
|
||||
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Create a notice during processing
|
||||
const notice = new Notice("Preparing PDF, please wait...", 0);
|
||||
|
||||
// Create SVGs for each frame
|
||||
const svgPages = [];
|
||||
|
||||
let placeholderRects = [];
|
||||
ea.clear();
|
||||
for (const frame of frames) {
|
||||
ea.style.opacity = 0;
|
||||
ea.style.roughness = 0;
|
||||
ea.style.fillStyle = "solid";
|
||||
ea.style.backgroundColor = "black"
|
||||
ea.style.strokeWidth = 0.01;
|
||||
ea.addRect(frame.x, frame.y, frame.width, frame.height);
|
||||
}
|
||||
|
||||
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,
|
||||
elementsOverride: ea.getViewElements().concat(ea.getElements()),
|
||||
});
|
||||
ea.clear();
|
||||
for (const frame of frames) {
|
||||
// NEW: skip empty frames unless user opted to print them
|
||||
if(!printEmptyPages && !hasElementsInArea(frame)) continue;
|
||||
|
||||
const { top, left, bottom, right } = translateToZero(frame);
|
||||
|
||||
//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);
|
||||
}
|
||||
|
||||
// NEW: abort if nothing to print
|
||||
if(svgPages.length === 0) {
|
||||
notice.hide();
|
||||
new Notice("No pages to print (all selected frames are empty)");
|
||||
notice.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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: {},
|
||||
//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";
|
||||
|
||||
modal.onClose = async () => {
|
||||
delete window.excalidrawPrintableLayoutWizardModal;
|
||||
if (dirty) {
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
ea.viewUpdateScene({
|
||||
appState: {
|
||||
frameRendering: {enabled, clip, name, outline, markerName, markerEnabled}
|
||||
}
|
||||
});
|
||||
if(shouldRestart) setTimeout(()=>run());
|
||||
};
|
||||
|
||||
// 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;"
|
||||
}
|
||||
});
|
||||
|
||||
// Help section
|
||||
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)
|
||||
});
|
||||
});
|
||||
|
||||
// Tabs (show only when frames exist)
|
||||
let framesTabEl, printingTabEl, tabsHeaderEl, marginDropdown;
|
||||
if (hasFrames) {
|
||||
tabsHeaderEl = container.createDiv({
|
||||
attr: { style: "display:flex; gap:8px; border-bottom:1px solid var(--background-modifier-border); padding-bottom:0;" }
|
||||
});
|
||||
tabsHeaderEl.addClass("tabs-header"); // NEW
|
||||
|
||||
const framesTabBtn = tabsHeaderEl.createEl("button", {
|
||||
text: "Frames",
|
||||
attr: { style: "padding:8px 12px; cursor:pointer;" }
|
||||
});
|
||||
framesTabBtn.addClass("tab-btn"); // NEW
|
||||
|
||||
const printingTabBtn = tabsHeaderEl.createEl("button", {
|
||||
text: "Printing",
|
||||
attr: { style: "padding:8px 12px; cursor:pointer;" }
|
||||
});
|
||||
printingTabBtn.addClass("tab-btn"); // NEW
|
||||
|
||||
const tabsBody = container.createDiv();
|
||||
tabsBody.addClass("tab-panels"); // NEW
|
||||
|
||||
framesTabEl = tabsBody.createDiv({ attr: { style: "display:block;" } });
|
||||
framesTabEl.addClass("tab-panel"); // NEW
|
||||
|
||||
printingTabEl = tabsBody.createDiv({ attr: { style: "display:none;" } });
|
||||
printingTabEl.addClass("tab-panel"); // NEW
|
||||
|
||||
const activate = (tab) => {
|
||||
if (tab === "frames") {
|
||||
framesTabEl.style.display = "";
|
||||
printingTabEl.style.display = "none";
|
||||
framesTabBtn.classList.add("is-active");
|
||||
printingTabBtn.classList.remove("is-active");
|
||||
} else {
|
||||
framesTabEl.style.display = "none";
|
||||
printingTabEl.style.display = "";
|
||||
framesTabBtn.classList.remove("is-active");
|
||||
printingTabBtn.classList.add("is-active");
|
||||
}
|
||||
};
|
||||
framesTabBtn.addEventListener("click", () => {
|
||||
window.excalidrawPrintLayoutWizard = "frames";
|
||||
activate("frames")
|
||||
});
|
||||
printingTabBtn.addEventListener("click", () => {
|
||||
window.excalidrawPrintLayoutWizard = "printing";
|
||||
activate("printing")
|
||||
});
|
||||
activate(window.excalidrawPrintLayoutWizard ?? "frames");
|
||||
} else {
|
||||
// No frames yet, only frames tab content
|
||||
framesTabEl = container.createDiv();
|
||||
}
|
||||
|
||||
const createOptionsContainerCommonControls = (optionsContainer) => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Use only Marker Frames")
|
||||
.setDesc("When off, all frames will be printed (not just marker frames)")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(printMarkersOnly).onChange(value => {
|
||||
printMarkersOnly = value;
|
||||
if (settings[PRINT_MARKERS_ONLY].value !== value) {
|
||||
settings[PRINT_MARKERS_ONLY].value = value;
|
||||
dirty = true;
|
||||
shouldRestart = true;
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// FRAMES TAB CONTENT
|
||||
// When no frames yet: initial size/orientation inputs and Create First Frame button
|
||||
if (!hasFrames) {
|
||||
const settingsContainer = framesTabEl.createDiv({
|
||||
attr: {
|
||||
// four columns: label + input, label + input
|
||||
style: "display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 10px; align-items: center;"
|
||||
}
|
||||
});
|
||||
// Page Size
|
||||
settingsContainer.createEl("label", { text: "Page Size:" });
|
||||
const 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
|
||||
settingsContainer.createEl("label", { text: "Orientation:" });
|
||||
const 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;
|
||||
|
||||
const optionsContainer = framesTabEl.createDiv({ attr: { style: "margin-top: 10px;" } });
|
||||
createOptionsContainerCommonControls(optionsContainer);
|
||||
|
||||
// Create First Frame button
|
||||
const buttonContainer = framesTabEl.createDiv({
|
||||
attr: { style: "display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 10px;" }
|
||||
});
|
||||
const createFirstBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "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) {
|
||||
shouldRestart = true;
|
||||
modal.close()
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// hasFrames: frame-management options + arrow buttons
|
||||
const optionsContainer = framesTabEl.createDiv({ attr: { style: "margin-top: 10px;" } });
|
||||
createOptionsContainerCommonControls(optionsContainer);
|
||||
|
||||
// Arrow buttons with center rotate toggle
|
||||
const buttonContainer = framesTabEl.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
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"); });
|
||||
|
||||
buttonContainer.createDiv({ attr: { style: "grid-column: 3; grid-row: 1;" } });
|
||||
|
||||
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"); });
|
||||
|
||||
// Center toggle: Rotate next page
|
||||
centerRotateBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 2; grid-row: 2; height: 40px;" }
|
||||
});
|
||||
centerRotateBtn.textContent = "Rotate next page";
|
||||
centerRotateBtn.addEventListener("click", () => {
|
||||
rotateOnAdd = !rotateOnAdd;
|
||||
setRotateBtnActive(rotateOnAdd);
|
||||
});
|
||||
setRotateBtnActive(rotateOnAdd);
|
||||
|
||||
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"); });
|
||||
|
||||
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"); });
|
||||
|
||||
buttonContainer.createDiv({ attr: { style: "grid-column: 1; grid-row: 3;" } });
|
||||
}
|
||||
|
||||
// PRINTING TAB CONTENT (only when hasFrames)
|
||||
if (hasFrames && printingTabEl) {
|
||||
const marginContainer = printingTabEl.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; margin-top: 6px;"
|
||||
}
|
||||
});
|
||||
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;
|
||||
|
||||
const printingOptions = printingTabEl.createDiv({ attr: { style: "margin-top: 10px;" } });
|
||||
|
||||
new ea.obsidian.Setting(printingOptions)
|
||||
.setName(PRINT_EMPTY)
|
||||
.setDesc("Include frames with no content in the PDF")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(printEmptyPages).onChange(value => {
|
||||
printEmptyPages = value;
|
||||
if(settings[PRINT_EMPTY].value !== value) {
|
||||
settings[PRINT_EMPTY].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const printBtnRow = printingTabEl.createDiv({ attr: { style: "margin-top: 10px; display:flex; justify-content:flex-start;" } });
|
||||
const printBtn = printBtnRow.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "height: 40px; background-color: var(--interactive-accent);" }
|
||||
});
|
||||
printBtn.innerHTML = ea.obsidian.getIcon("printer").outerHTML;
|
||||
printBtn.addEventListener("click", async () => { await printToPDF(marginDropdown.value); });
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
.is-active {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-radius: 4px;
|
||||
}
|
||||
/* Tabs styling - NEW */
|
||||
.tabs-header {
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.tabs-header .tab-btn {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: -1px; /* sit on top of the panel border */
|
||||
}
|
||||
.tabs-header .tab-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
.tabs-header .tab-btn.is-active {
|
||||
background: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.tab-panels {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 0 6px 6px 6px; /* merge with active tab */
|
||||
padding: 12px;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
/* accent styling for center rotate toggle when active */
|
||||
.page-btn.is-accent {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
.page-btn.is-accent:hover {
|
||||
background-color: var(--interactive-accent-hover, var(--interactive-accent));
|
||||
}
|
||||
`
|
||||
});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1 @@
|
||||
<svg class="svg-icon lucide-printer" 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: 405 B |
Reference in New Issue
Block a user