mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00de9d639b | |||
| 5a66c78428 | |||
| d1be193125 | |||
| f4c8d21a33 | |||
| d588f749d2 | |||
| c1f909427b | |||
| 2b38c03840 | |||
| 8cca77dcab | |||
| 5b341cb5fb | |||
| 526299e41f | |||
| ae82bce4da | |||
| 499ca87759 | |||
| aa4fbe1f6c | |||
| 05b72f9f07 | |||
| 53ffa50b15 |
@@ -69,10 +69,15 @@ const result = polyboolAction({
|
||||
const polygonHierachy = subordinateInnerPolygons(result.regions);
|
||||
drawPolygonHierachy(polygonHierachy);
|
||||
ea.deleteViewElements(elements);
|
||||
setPolygonTrue();
|
||||
ea.addElementsToView(false,false,true);
|
||||
return;
|
||||
|
||||
|
||||
function setPolygonTrue() {
|
||||
ea.getElements().filter(el=>el.type==="line").forEach(el => {
|
||||
el.polygon = true;
|
||||
});
|
||||
}
|
||||
|
||||
function traceElement(element) {
|
||||
const diamondPath = (diamond) => [
|
||||
|
||||
@@ -7,21 +7,27 @@ Scribble Helper can improve handwriting and add links. It lets you create and ed
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.25")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.11.0")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Constants and initialization
|
||||
// ------------------------------
|
||||
const helpLINK = "https://youtu.be/BvYkOaly-QM";
|
||||
const DBLCLICKTIMEOUT = 300;
|
||||
const maxWidth = 600;
|
||||
const padding = 6;
|
||||
const api = ea.getExcalidrawAPI();
|
||||
const win = ea.targetView.ownerWindow;
|
||||
|
||||
// Initialize global variables
|
||||
if(!win.ExcalidrawScribbleHelper) win.ExcalidrawScribbleHelper = {};
|
||||
if(typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = false;
|
||||
}
|
||||
|
||||
let windowOpen = false; //to prevent the modal window to open again while writing with scribble
|
||||
let prevZoomValue = api.getAppState().zoom.value; //used to avoid trigger on pinch zoom
|
||||
|
||||
@@ -49,8 +55,10 @@ if(typeof win.ExcalidrawScribbleHelper.action === "undefined") {
|
||||
}
|
||||
|
||||
//---------------------------------------
|
||||
// Color Palette for stroke color setting
|
||||
// Helper Functions
|
||||
//---------------------------------------
|
||||
|
||||
// Color Palette for stroke color setting
|
||||
// https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.8
|
||||
const defaultStrokeColors = [
|
||||
"#000000", "#343a40", "#495057", "#c92a2a", "#a61e4d",
|
||||
@@ -58,7 +66,7 @@ const defaultStrokeColors = [
|
||||
"#087f5b", "#2b8a3e", "#5c940d", "#e67700", "#d9480f"
|
||||
];
|
||||
|
||||
const loadColorPalette = () => {
|
||||
function loadColorPalette() {
|
||||
const st = api.getAppState();
|
||||
const strokeColors = new Set();
|
||||
let strokeColorPalette = st.colorPalette?.elementStroke ?? defaultStrokeColors;
|
||||
@@ -79,18 +87,8 @@ const loadColorPalette = () => {
|
||||
return strokeColors;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Define variables to cache element location on first click
|
||||
//----------------------------------------------------------
|
||||
// if a single element is selected when the action is started, update that existing text
|
||||
let containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
|
||||
//-------------------------------------------
|
||||
// Functions to add and remove event listners
|
||||
//-------------------------------------------
|
||||
const addEventHandler = (handler) => {
|
||||
// Event handler management
|
||||
function addEventHandler(handler) {
|
||||
if(win.ExcalidrawScribbleHelper.eventHandler) {
|
||||
win.removeEventListner("pointerdown", handler);
|
||||
}
|
||||
@@ -99,40 +97,44 @@ const addEventHandler = (handler) => {
|
||||
win.ExcalidrawScribbleHelper.window = win;
|
||||
}
|
||||
|
||||
const removeEventHandler = (handler) => {
|
||||
function removeEventHandler(handler) {
|
||||
win.removeEventListener("pointerdown",handler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
}
|
||||
|
||||
//Stop the script if scribble helper is clicked and no eligable element is selected
|
||||
let silent = false;
|
||||
if (win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
removeEventHandler(win.ExcalidrawScribbleHelper.eventHandler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
|
||||
new Notice ("Scribble Helper was stopped",1000);
|
||||
return;
|
||||
// Edit existing text element function
|
||||
async function editExistingTextElement(elements) {
|
||||
windowOpen = true;
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt(
|
||||
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true,true
|
||||
);
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
el.strokeColor = ea.style.strokeColor;
|
||||
el.originalText = text;
|
||||
el.text = text;
|
||||
el.rawText = text;
|
||||
if(el.autoResize) {
|
||||
ea.refreshTextElementSize(el.id);
|
||||
}
|
||||
await ea.addElementsToView(false,false);
|
||||
if(el.containerId) {
|
||||
const containers = ea.getViewElements().filter(e=>e.id === el.containerId);
|
||||
api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
}
|
||||
silent = true;
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Custom dialog controls
|
||||
// ----------------------
|
||||
if (typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = undefined;
|
||||
}
|
||||
if (typeof win.ExcalidrawScribbleHelper.penDetected === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penDetected = false;
|
||||
}
|
||||
let timer = Date.now();
|
||||
let eventHandler = () => {};
|
||||
|
||||
const customControls = (container) => {
|
||||
// Custom dialog UI components
|
||||
function customControls (container) {
|
||||
const helpDIV = container.createDiv();
|
||||
helpDIV.innerHTML = `<a href="${helpLINK}" target="_blank">Click here for help</a>`;
|
||||
helpDIV.style.paddingBottom = "0.25em";
|
||||
const viewBackground = api.getAppState().viewBackgroundColor;
|
||||
const el1 = new ea.obsidian.Setting(container)
|
||||
.setName(`Text color`)
|
||||
@@ -152,9 +154,10 @@ const customControls = (container) => {
|
||||
el1.nameEl.style.color = ea.style.strokeColor;
|
||||
el1.nameEl.style.background = viewBackground;
|
||||
el1.nameEl.style.fontWeight = "bold";
|
||||
|
||||
el1.settingEl.style.padding = "0.25em 0";
|
||||
|
||||
const el2 = new ea.obsidian.Setting(container)
|
||||
.setName(`Trigger editor by pen double tap only`)
|
||||
.setDesc(`Trigger editor by pen double tap only`)
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(win.ExcalidrawScribbleHelper.penOnly)
|
||||
.onChange(value => {
|
||||
@@ -162,13 +165,23 @@ const customControls = (container) => {
|
||||
})
|
||||
)
|
||||
el2.settingEl.style.border = "none";
|
||||
el2.settingEl.style.padding = "0.25em 0";
|
||||
el2.settingEl.style.display = win.ExcalidrawScribbleHelper.penDetected ? "" : "none";
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Cache element location on first click
|
||||
//----------------------------------------------------------
|
||||
// if a single element is selected when the action is started, update that existing text
|
||||
let containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
|
||||
// -------------------------------
|
||||
// Click / dbl click event handler
|
||||
// Main Click / dbl click event handler
|
||||
// -------------------------------
|
||||
eventHandler = async (evt) => {
|
||||
let timer = Date.now();
|
||||
async function eventHandler(evt) {
|
||||
if(windowOpen) return;
|
||||
if(ea.targetView !== app.workspace.activeLeaf.view) removeEventHandler(eventHandler);
|
||||
if(evt && evt.target && !evt.target.hasClass("excalidraw__canvas")) return;
|
||||
@@ -252,7 +265,7 @@ eventHandler = async (evt) => {
|
||||
},
|
||||
{
|
||||
caption: "☱",
|
||||
tooltip: "Add as Wrapped Text (rectangle with transparent border and background)",
|
||||
tooltip: "Add as Wrapped Text",
|
||||
action: () => {
|
||||
win.ExcalidrawScribbleHelper.action="Wrap";
|
||||
if(settings["Default action"].value!=="Wrap") {
|
||||
@@ -266,6 +279,7 @@ eventHandler = async (evt) => {
|
||||
if(win.ExcalidrawScribbleHelper.action !== "Text") actionButtons.push(actionButtons.shift());
|
||||
if(win.ExcalidrawScribbleHelper.action === "Wrap") actionButtons.push(actionButtons.shift());
|
||||
|
||||
// Apply styles from current app state
|
||||
ea.style.strokeColor = st.currentItemStrokeColor ?? ea.style.strokeColor;
|
||||
ea.style.roughness = st.currentItemRoughness ?? ea.style.roughness;
|
||||
ea.setStrokeSharpness(st.currentItemRoundness === "round" ? 0 : st.currentItemRoundness)
|
||||
@@ -281,9 +295,18 @@ eventHandler = async (evt) => {
|
||||
ea.style.verticalAlign = "middle";
|
||||
|
||||
windowOpen = true;
|
||||
const text = await utils.inputPrompt (
|
||||
"Edit text", "", "", containerID?undefined:actionButtons, 5, true, customControls, true
|
||||
);
|
||||
|
||||
const text = await utils.inputPrompt ({
|
||||
header: "Edit text",
|
||||
placeholder: "",
|
||||
value: "",
|
||||
buttons: containerID?undefined:actionButtons,
|
||||
lines: 5,
|
||||
displayEditorButtons: true,
|
||||
customComponents: customControls,
|
||||
blockPointerInputOutsideModal: true,
|
||||
controlsOnTop: true
|
||||
});
|
||||
windowOpen = false;
|
||||
|
||||
if(!text || text.trim() === "") return;
|
||||
@@ -297,8 +320,11 @@ eventHandler = async (evt) => {
|
||||
const textEl = ea.getElement(textId);
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Wrap")) {
|
||||
ea.style.backgroundColor = "transparent";
|
||||
ea.style.strokeColor = "transparent";
|
||||
textEl.autoResize = false;
|
||||
textEl.width = Math.min(textEl.width, maxWidth);
|
||||
ea.addElementsToView(false, false, true);
|
||||
addEventHandler(eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Sticky")) {
|
||||
@@ -342,36 +368,22 @@ eventHandler = async (evt) => {
|
||||
ea.selectElementsInView(containers);
|
||||
};
|
||||
|
||||
// ---------------------
|
||||
// Edit Existing Element
|
||||
// ---------------------
|
||||
const editExistingTextElement = async (elements) => {
|
||||
windowOpen = true;
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt(
|
||||
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true
|
||||
);
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
el.strokeColor = ea.style.strokeColor;
|
||||
el.originalText = text;
|
||||
el.text = text;
|
||||
el.rawText = text;
|
||||
ea.refreshTextElementSize(el.id);
|
||||
await ea.addElementsToView(false,false);
|
||||
if(el.containerId) {
|
||||
const containers = ea.getViewElements().filter(e=>e.id === el.containerId);
|
||||
api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
//---------------------
|
||||
// Script entry point
|
||||
//---------------------
|
||||
//Stop the script if scribble helper is clicked and no eligable element is selected
|
||||
let silent = false;
|
||||
if (win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
removeEventHandler(win.ExcalidrawScribbleHelper.eventHandler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
|
||||
new Notice ("Scribble Helper was stopped",1000);
|
||||
return;
|
||||
}
|
||||
silent = true;
|
||||
}
|
||||
|
||||
//--------------
|
||||
// Start actions
|
||||
//--------------
|
||||
if(!win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
if(!silent) new Notice(
|
||||
"To create a new text element,\ndouble-tap the screen.\n\n" +
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.
|
||||
There is also the option to close the object along the cut, which will close the cut in the shape of the line.
|
||||

|
||||

|
||||

|
||||

|
||||
Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ angles.forEach((angle, key) => {
|
||||
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
if (closeObject && cuttingLine) line.polygon = true;
|
||||
line.frameId = ellipse.frameId;
|
||||
line.groupIds = ellipse.groupIds;
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.11.0-beta-1",
|
||||
"version": "2.12.0-beta-1",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.10.1",
|
||||
"version": "2.11.1",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
Generated
+4
-49
@@ -11,12 +11,11 @@
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
"@zsviczian/excalidraw": "0.18.0-9",
|
||||
"@zsviczian/excalidraw": "0.18.0-14",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"es6-promise-pool": "2.5.0",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mathjax-full": "^3.2.2",
|
||||
@@ -3199,9 +3198,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zsviczian/excalidraw": {
|
||||
"version": "0.18.0-9",
|
||||
"resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.18.0-9.tgz",
|
||||
"integrity": "sha512-Eao+ETW1TF5O7Lskh0rJfZRQcPoHgXbnKPVooZzhmWPp3K3N2opbGgJrgt6QsdmCB49LUo2QTllXDNpSJdB7iA==",
|
||||
"version": "0.18.0-14",
|
||||
"resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.18.0-14.tgz",
|
||||
"integrity": "sha512-LXmlImnsYxXzObnO0YxPNl4+TeLyM5BhIwUUGxdL/GY+Ru4tTzbwGf7l5GaVsWejrCYvVWM6j/wDwJuL6V1s9Q==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
@@ -3476,14 +3475,6 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -3811,14 +3802,6 @@
|
||||
"postcss": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
@@ -5343,18 +5326,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -8400,14 +8371,6 @@
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -8736,14 +8699,6 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.18.0-12",
|
||||
"@zsviczian/excalidraw": "0.18.0-15",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
|
||||
@@ -1078,6 +1078,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.eventManager.onActiveLeafChangeHandler(leaf);
|
||||
}
|
||||
|
||||
public setDebounceActiveLeafChangeHandler() {
|
||||
this.eventManager.setDebounceActiveLeafChangeHandler();
|
||||
}
|
||||
|
||||
public registerHotkeyOverrides() {
|
||||
//this is repeated here because the same function is called when settings is closed after hotkeys have changed
|
||||
if (this.popScope) {
|
||||
|
||||
@@ -1038,8 +1038,13 @@ export class CommandManager {
|
||||
x:Math.max(0,centerX - width/2 + view.ownerWindow.screenX),
|
||||
y:Math.max(0,centerY - height/2 + view.ownerWindow.screenY),
|
||||
}
|
||||
|
||||
const focusOnFileTab = this.settings.focusOnFileTab;
|
||||
//override focusOnFileTab for popout windows
|
||||
if(DEVICE.isDesktop) {
|
||||
this.settings.focusOnFileTab = false;
|
||||
}
|
||||
this.plugin.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true, undefined, false, {x,y,width,height});
|
||||
this.settings.focusOnFileTab = focusOnFileTab;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export class EventManager {
|
||||
private removeEventLisnters:(()=>void)[] = []; //only used if I register an event directly, not via Obsidian's registerEvent
|
||||
private previouslyActiveLeaf: WorkspaceLeaf;
|
||||
private splitViewLeafSwitchTimestamp: number = 0;
|
||||
private debunceActiveLeafChangeHandlerTimer: number|null = null;
|
||||
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
@@ -103,6 +104,15 @@ export class EventManager {
|
||||
this.plugin.registerEvent(this.plugin.app.workspace.on("editor-menu", this.onEditorMenuHandler.bind(this)));
|
||||
}
|
||||
|
||||
public setDebounceActiveLeafChangeHandler() {
|
||||
if(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
window.clearTimeout(this.debunceActiveLeafChangeHandlerTimer);
|
||||
}
|
||||
this.debunceActiveLeafChangeHandlerTimer = window.setTimeout(() => {
|
||||
this.debunceActiveLeafChangeHandlerTimer = null;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private onLayoutChangeHandler() {
|
||||
getExcalidrawViews(this.app).forEach(excalidrawView=>excalidrawView.refresh());
|
||||
}
|
||||
@@ -164,6 +174,10 @@ export class EventManager {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onActiveLeafChangeHandler,`onActiveLeafChangeEventHandler`, leaf);
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
||||
|
||||
if(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
//In Obsidian 1.8.x the active excalidraw leaf is obscured by an empty leaf without a parent
|
||||
//This hack resolves it
|
||||
if(this.app.workspace.activeLeaf === leaf && isUnwantedLeaf(leaf)) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
|
||||
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
|
||||
TOGGLE_SPLASHSCREEN: "Show splash screen in new drawings",
|
||||
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
|
||||
FLIP_IMAGE: "Open the back-of-the-note for the selected image in a popout window",
|
||||
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
|
||||
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
|
||||
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",
|
||||
@@ -463,7 +463,7 @@ FILENAME_HEAD: "Filename",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "Focus on Existing Tab",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "When opening a link, Excalidraw will focus on the existing tab if the file is already open. " +
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open.",
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open except for the 'Open the back-of-the-note of the selected excalidraw image' command palette action.",
|
||||
SECOND_ORDER_LINKS_NAME: "Show second-order links",
|
||||
SECOND_ORDER_LINKS_DESC: "Show links when clicking on a link in Excalidraw. Second-order link are backlinks pointing to the link being clicked. " +
|
||||
"When using image icons to connect similar notes, second order links allow you to get to related notes in one click instead of two. " +
|
||||
@@ -961,6 +961,7 @@ FILENAME_HEAD: "Filename",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
|
||||
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
|
||||
PROMPT_BUTTON_UPPERCASE: "Uppercase",
|
||||
PROMPT_BUTTON_SPECIAL_CHARS: "Special Characters",
|
||||
PROMPT_SELECT_TEMPLATE: "Select a template",
|
||||
|
||||
//ModifierKeySettings
|
||||
@@ -1101,6 +1102,7 @@ FILENAME_HEAD: "Filename",
|
||||
EXPORTDIALOG_PDF_PROGRESS_ERROR: "Error exporting PDF, check developer console for details",
|
||||
|
||||
// Screenshot tab
|
||||
EXPORTDIALOG_NOT_AVAILALBE: "Sorry, this feature is only available when the drawing is open in the main Obsidian workspace.",
|
||||
EXPORTDIALOG_TAB_SCREENSHOT: "Screenshot",
|
||||
EXPORTDIALOG_SCREENSHOT_DESC: "Screenshots will include embeddables such as markdown pages, YouTube, websites, etc. They are only available on desktop, cannot be automatically exported, and only support PNG format.",
|
||||
SCREENSHOT_DESKTOP_ONLY: "Screenshot feature is only available on desktop",
|
||||
|
||||
@@ -961,6 +961,7 @@ FILENAME_HEAD: "文件名",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "插入空格",
|
||||
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
|
||||
PROMPT_BUTTON_UPPERCASE: "大写",
|
||||
PROMPT_BUTTON_SPECIAL_CHARS : "特殊字符" ,
|
||||
PROMPT_SELECT_TEMPLATE: "选择一个模板",
|
||||
|
||||
//ModifierKeySettings
|
||||
@@ -1100,6 +1101,16 @@ EXPORTDIALOG_PDF_PROGRESS_NOTICE: "正在导出 PDF。如果图像较大,可
|
||||
EXPORTDIALOG_PDF_PROGRESS_DONE: "导出完成" ,
|
||||
EXPORTDIALOG_PDF_PROGRESS_ERROR: "导出 PDF 时出错,请检查开发者控制台以获取详细信息" ,
|
||||
|
||||
// Screenshot tab
|
||||
EXPORTDIALOG_NOT_AVAILALBE : "抱歉,此功能仅在绘图在主 Obsidian 工作区打开时可用。",
|
||||
EXPORTDIALOG_TAB_SCREENSHOT : "截图" ,
|
||||
EXPORTDIALOG_SCREENSHOT_DESC : "截图将包括可嵌入的内容,例如 markdown 页面、YouTube、网站等。它们仅在桌面端可用,无法自动导出,并且仅支持 PNG 格式。" ,
|
||||
SCREENSHOT_DESKTOP_ONLY : "截图功能仅在桌面端可用" ,
|
||||
SCREENSHOT_FILE_SUCCESS : "截图已保存到仓库" ,
|
||||
SCREENSHOT_CLIPBOARD_SUCCESS : "截图已复制到剪贴板" ,
|
||||
SCREENSHOT_CLIPBOARD_ERROR : "无法复制截图到剪贴板:" ,
|
||||
SCREENSHOT_ERROR : "截图出错 - 请查看控制台日志" ,
|
||||
|
||||
// exportUtils.ts
|
||||
PDF_EXPORT_DESKTOP_ONLY: "PDF 导出功能仅限桌面端使用" ,
|
||||
};
|
||||
@@ -88,9 +88,13 @@ export class ExportDialog extends Modal {
|
||||
this.selectedOnlySetting = null;
|
||||
this.containerEl.remove();
|
||||
}
|
||||
|
||||
get isSelectedOnly(): boolean {
|
||||
return this.hasSelectedElements && this.exportSelectedOnly;
|
||||
}
|
||||
|
||||
updateBoundingBox() {
|
||||
if(this.hasSelectedElements && this.exportSelectedOnly) {
|
||||
if(this.isSelectedOnly) {
|
||||
this.boundingBox = this.ea.getBoundingBox(this.view.getViewSelectedElements());
|
||||
} else {
|
||||
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
|
||||
@@ -195,8 +199,10 @@ export class ExportDialog extends Modal {
|
||||
this.createPDFButton();
|
||||
break;
|
||||
case "screenshot":
|
||||
this.createImageSettings(true);
|
||||
this.createImageButtons(true);
|
||||
if(this.view.isInMainObsidianWorkspace) {
|
||||
this.createImageSettings(true);
|
||||
this.createImageButtons(true);
|
||||
}
|
||||
break;
|
||||
case "image":
|
||||
default:
|
||||
@@ -221,7 +227,11 @@ export class ExportDialog extends Modal {
|
||||
break;
|
||||
case "screenshot":
|
||||
this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_TAB_SCREENSHOT")});
|
||||
this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_SCREENSHOT_DESC")})
|
||||
if(this.view.isInMainObsidianWorkspace) {
|
||||
this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_SCREENSHOT_DESC")})
|
||||
} else {
|
||||
this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_NOT_AVAILALBE")})
|
||||
}
|
||||
break;
|
||||
case "image":
|
||||
default:
|
||||
@@ -362,12 +372,12 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPNG.onclick = () => {
|
||||
if(isScreenshot) {
|
||||
//allow dialot to close before taking screenshot
|
||||
//allow dialog to close before taking screenshot
|
||||
setTimeout(async () => {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -375,7 +385,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.exportPNG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportPNG(this.embedScene, this.isSelectedOnly);
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -392,7 +402,7 @@ export class ExportDialog extends Modal {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -400,7 +410,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
|
||||
this.view.savePNG(this.view.getScene(this.isSelectedOnly));
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -411,12 +421,12 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPNGClipboard.onclick = async () => {
|
||||
if(isScreenshot) {
|
||||
//allow dialot to close before taking screenshot
|
||||
//allow dialog to close before taking screenshot
|
||||
setTimeout(async () => {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -424,7 +434,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.isSelectedOnly);
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -446,7 +456,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVG.onclick = () => {
|
||||
this.view.exportSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportSVG(this.embedScene, this.isSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
@@ -456,7 +466,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVGVault.onclick = () => {
|
||||
this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
|
||||
this.view.saveSVG(this.view.getScene(this.isSelectedOnly));
|
||||
this.close();
|
||||
};
|
||||
|
||||
@@ -465,7 +475,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVGClipboard.onclick = async () => {
|
||||
const svg = await this.view.getSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
const svg = await this.view.getSVG(this.embedScene, this.isSelectedOnly);
|
||||
exportSVGToClipboard(svg);
|
||||
this.close();
|
||||
};
|
||||
@@ -498,7 +508,7 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPDFExport.onclick = () => {
|
||||
this.view.exportPDF(
|
||||
this.hasSelectedElements && this.exportSelectedOnly,
|
||||
this.isSelectedOnly,
|
||||
this.pageSize,
|
||||
this.pageOrientation
|
||||
);
|
||||
|
||||
@@ -16,13 +16,36 @@ export const RELEASE_NOTES: { [k: string]: string } = {
|
||||
I build this plugin in my free time, as a labor of love. Curious about the philosophy behind it? Check out [📕 Sketch Your Mind](https://sketch-your-mind.com). If you find it valuable, say THANK YOU or…
|
||||
|
||||
<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.12.0": `
|
||||
## Fixed
|
||||
- Dynamic styling was not working when there were frames in the scene.
|
||||
- Minor fix for the screenshot feature. This fix also solves the long-standing issue of the window control buttons (close, minimize, maximize) being visible in full-screen mode.
|
||||
- When ALT/OPT + dragging an Embeddable object, the object dragging was not working properly at times, resulting in an empty embeddable object (instead of dragging the element).
|
||||
|
||||
## New
|
||||
- Line polygons. Draw a closed line shape, and it will snap to a polygon. [#9477](https://github.com/excalidraw/excalidraw/pull/9477)
|
||||
- I updated the Split Ellipse and Boolean Operations Scripts to support this new feature.
|
||||
- If you enter line editor mode by CTRL/CMD + clicking on the line, the lock-point will be marked for easier editing. Look for the polygon action on the elements panel to break the polygon if required.
|
||||
- Override "Focus on Existing Tab" setting for the "Open the back-of-the-note for the selected image in a popout window" action. "Open the back-of-the-note" will open a new popout every time.
|
||||
`,
|
||||
"2.11.1": `
|
||||
## Fixed:
|
||||
- The new "Screenshot" option in the Export Image dialog was not working properly. [#2339](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2339)
|
||||
|
||||
## New from Excalidraw.com
|
||||
- Quarter snap points for diamonds [#9387](https://github.com/excalidraw/excalidraw/pull/9387)
|
||||
- Precise highlights for bindings [#9472](https://github.com/excalidraw/excalidraw/pull/9472)
|
||||
|
||||
`,
|
||||
"2.11.0": `
|
||||
## New
|
||||
- New "Screenshot" option in the Export Image dialog. This allows you to take a screenshot of the current view, including embedded web pages, youtube videos, and markdown documents. Screenshot is only possible in PNG.
|
||||
- Expose parameter in plugin settings to disable AI functionality [#2325](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2325)
|
||||
- Enable double-click text editing option in Excalidraw appearance and behavior (based on request on Discord)
|
||||
- Enable (disable) double-click text editing option in Excalidraw appearance and behavior (based on request on Discord)
|
||||
- Added two new PDF export sizes: "Match image", "HD Screen".
|
||||
- Switch between basic shapes. Quickly change the shape of the selected element by pressing TAB [#9270](https://github.com/excalidraw/excalidraw/pull/9270)
|
||||
- Updated the Scribble Helper Script. Now controls are at the top so your palm does accidently trigger them. I added a new button to insert special characters. Scribble helper now makes use of the new text element wrapping in Excalidraw.
|
||||
|
||||
## Fixed in the plugin
|
||||
- Scaling multiple embeddables at once did not work. [#2276](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2276)
|
||||
|
||||
@@ -22,8 +22,7 @@ import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/consta
|
||||
import { REGEX_LINK, REGEX_TAGS } from "../ExcalidrawData";
|
||||
import { ScriptEngine } from "../Scripts";
|
||||
import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/excalidrawViewUtils";
|
||||
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
import { ButtonDefinition } from "src/types/promptTypes";
|
||||
|
||||
export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
@@ -98,6 +97,7 @@ export class GenericInputPrompt extends Modal {
|
||||
private selectionUpdateTimer: number = 0;
|
||||
private customComponents: (container: HTMLElement) => void;
|
||||
private blockPointerInputOutsideModal: boolean = false;
|
||||
private controlsOnTop: boolean = false;
|
||||
|
||||
public static Prompt(
|
||||
view: ExcalidrawView,
|
||||
@@ -111,6 +111,7 @@ export class GenericInputPrompt extends Modal {
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
controlsOnTop?: boolean,
|
||||
): Promise<string> {
|
||||
const newPromptModal = new GenericInputPrompt(
|
||||
view,
|
||||
@@ -124,6 +125,7 @@ export class GenericInputPrompt extends Modal {
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
controlsOnTop,
|
||||
);
|
||||
return newPromptModal.waitForClose;
|
||||
}
|
||||
@@ -140,6 +142,7 @@ export class GenericInputPrompt extends Modal {
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
controlsOnTop?: boolean,
|
||||
) {
|
||||
super(app);
|
||||
this.view = view;
|
||||
@@ -151,6 +154,7 @@ export class GenericInputPrompt extends Modal {
|
||||
this.displayEditorButtons = this.lines > 1 ? (displayEditorButtons ?? false) : false;
|
||||
this.customComponents = customComponents;
|
||||
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
|
||||
this.controlsOnTop = controlsOnTop ?? false;
|
||||
|
||||
this.waitForClose = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
@@ -173,13 +177,29 @@ export class GenericInputPrompt extends Modal {
|
||||
this.titleEl.textContent = this.header;
|
||||
|
||||
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input
|
||||
);
|
||||
this.customComponents?.(mainContentContainer);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
|
||||
// Conditionally order elements based on controlsOnTop flag
|
||||
if (this.controlsOnTop) {
|
||||
// Create button bar first
|
||||
this.customComponents?.(mainContentContainer);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
|
||||
// Then add input field and custom components
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input
|
||||
);
|
||||
} else {
|
||||
// Original order: input field, custom components, then buttons
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input
|
||||
);
|
||||
this.customComponents?.(mainContentContainer);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
protected createInputField(
|
||||
@@ -240,12 +260,8 @@ export class GenericInputPrompt extends Modal {
|
||||
|
||||
private createButtonBar(mainContentContainer: HTMLDivElement) {
|
||||
const buttonBarContainer: HTMLDivElement = mainContentContainer.createDiv();
|
||||
buttonBarContainer.style.display = "flex";
|
||||
buttonBarContainer.style.justifyContent = "space-between";
|
||||
buttonBarContainer.style.marginTop = "1rem";
|
||||
|
||||
buttonBarContainer.addClass(`excalidraw-prompt-buttonbar-${this.controlsOnTop ? "top" : "bottom"}`);
|
||||
const editorButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
|
||||
|
||||
const actionButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
|
||||
|
||||
if (this.buttons && this.buttons.length > 0) {
|
||||
@@ -279,6 +295,7 @@ export class GenericInputPrompt extends Modal {
|
||||
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
|
||||
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback.bind(this), "Delete");
|
||||
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
|
||||
this.createButton(editorButtonContainer, "§", this.specialCharsBtnClickCallback.bind(this), t("PROMPT_BUTTON_SPECIAL_CHARS"));
|
||||
if(this.view) {
|
||||
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback.bind(this), t("PROMPT_BUTTON_INSERT_LINK"));
|
||||
}
|
||||
@@ -384,6 +401,74 @@ export class GenericInputPrompt extends Modal {
|
||||
);
|
||||
}
|
||||
|
||||
private specialCharsBtnClickCallback = (evt: MouseEvent) => {
|
||||
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer);
|
||||
|
||||
// Remove any existing popup
|
||||
const existingPopup = document.querySelector('.excalidraw-special-chars-popup');
|
||||
if (existingPopup) {
|
||||
existingPopup.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create popup element
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'excalidraw-special-chars-popup';
|
||||
popup.style.position = 'absolute';
|
||||
popup.style.zIndex = '1000';
|
||||
popup.style.background = 'var(--background-primary)';
|
||||
popup.style.border = '1px solid var(--background-modifier-border)';
|
||||
popup.style.borderRadius = '4px';
|
||||
popup.style.padding = '4px';
|
||||
popup.style.boxShadow = '0 2px 8px var(--background-modifier-box-shadow)';
|
||||
popup.style.display = 'flex';
|
||||
popup.style.flexWrap = 'wrap';
|
||||
popup.style.maxWidth = '200px';
|
||||
|
||||
// Position near the button
|
||||
const rect = (evt.target as HTMLElement).getBoundingClientRect();
|
||||
popup.style.top = `${rect.bottom + 5}px`;
|
||||
popup.style.left = `${rect.left}px`;
|
||||
|
||||
// Special characters to include
|
||||
const specialChars = [',', '.', ':', ';', '!', '?', '"', '{', '}', '[', ']', '(', ')'];
|
||||
|
||||
// Add character buttons
|
||||
specialChars.forEach(char => {
|
||||
const charButton = document.createElement('button');
|
||||
charButton.textContent = char;
|
||||
charButton.style.margin = '2px';
|
||||
charButton.style.width = '28px';
|
||||
charButton.style.height = '28px';
|
||||
charButton.style.cursor = 'pointer';
|
||||
charButton.style.background = 'var(--interactive-normal)';
|
||||
charButton.style.border = 'none';
|
||||
charButton.style.borderRadius = '4px';
|
||||
|
||||
charButton.addEventListener('click', () => {
|
||||
this.insertStringBtnClickCallback(char);
|
||||
popup.remove();
|
||||
});
|
||||
|
||||
popup.appendChild(charButton);
|
||||
});
|
||||
|
||||
// Add click outside listener to close popup
|
||||
const closePopupListener = (e: MouseEvent) => {
|
||||
if (!popup.contains(e.target as Node) &&
|
||||
(evt.target as HTMLElement) !== e.target) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closePopupListener);
|
||||
}
|
||||
};
|
||||
|
||||
// Add to document and set up listeners
|
||||
document.body.appendChild(popup);
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closePopupListener);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
this.inputComponent.inputEl.focus();
|
||||
|
||||
@@ -947,15 +947,16 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "inputPrompt",
|
||||
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean);",
|
||||
code: "inputPrompt: (opts: {header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean, controlsOnTop?: boolean});",
|
||||
desc:
|
||||
"Opens a prompt that asks for an input.\nReturns a string with the input.\nYou need to await the result of inputPrompt.\n" +
|
||||
"Editor buttons are text editing buttons like delete, enter, allcaps - these are only displayed if lines is greater than 1 \n" +
|
||||
"Custom components are components that you can add to the prompt. These will be displayed between the text input area and the buttons.\n" +
|
||||
"blockPointerInputOutsideModal will block pointer input outside the modal. This is useful if you want to prevent the user accidently closing the modal or interacting with the excalidraw canvas while the prompt is open.\n" +
|
||||
"controlsOnTop when set to true will move all the buttons to the top of the modal, leaving the text area at the bottom. This feature was developed for Scribble Helper script to avoid your palm pressing buttons while scribbling.\n"+
|
||||
"buttons.action(input: string) => string\nThe button action function will receive the actual input string. If action returns null, input will be unchanged. If action returns a string, input will receive that value when the promise is resolved. " +
|
||||
"example:\n<code>let fileType = '';\nconst filename = await utils.inputPrompt (\n 'Filename',\n '',\n '',\n, [\n {\n caption: 'Markdown',\n action: ()=>{fileType='md';return;}\n },\n {\n caption: 'Excalidraw',\n action: ()=>{fileType='ex';return;}\n }\n ]\n);</code>",
|
||||
after: "",
|
||||
after: `({header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean, controlsOnTop?: boolean})`,
|
||||
},
|
||||
{
|
||||
field: "suggester",
|
||||
|
||||
+23
-5
@@ -8,13 +8,14 @@ import {
|
||||
import { PLUGIN_ID } from "../constants/constants";
|
||||
import ExcalidrawView from "../view/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt";
|
||||
import { GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt";
|
||||
import { getIMGFilename } from "../utils/fileUtils";
|
||||
import { splitFolderAndFilename } from "../utils/fileUtils";
|
||||
import { getEA } from "src/core";
|
||||
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import { WeakArray } from "./WeakArray";
|
||||
import { getExcalidrawViews } from "../utils/obsidianUtils";
|
||||
import { ButtonDefinition, InputPromptOptions } from "src/types/promptTypes";
|
||||
|
||||
export type ScriptIconMap = {
|
||||
[key: string]: { name: string; group: string; svgString: string };
|
||||
@@ -271,7 +272,7 @@ export class ScriptEngine {
|
||||
//try {
|
||||
result = await new AsyncFunction("ea", "utils", script)(ea, {
|
||||
inputPrompt: (
|
||||
header: string,
|
||||
header: string | InputPromptOptions,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: ButtonDefinition[],
|
||||
@@ -279,8 +280,21 @@ export class ScriptEngine {
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
) =>
|
||||
ScriptEngine.inputPrompt(
|
||||
controlsOnTop?: boolean,
|
||||
) => {
|
||||
if (typeof header === "object") {
|
||||
const options = header as InputPromptOptions;
|
||||
header = options.header;
|
||||
placeholder = options.placeholder;
|
||||
value = options.value;
|
||||
buttons = options.buttons;
|
||||
lines = options.lines;
|
||||
displayEditorButtons = options.displayEditorButtons;
|
||||
customComponents = options.customComponents;
|
||||
blockPointerInputOutsideModal = options.blockPointerInputOutsideModal;
|
||||
controlsOnTop = options.controlsOnTop;
|
||||
}
|
||||
return ScriptEngine.inputPrompt(
|
||||
view,
|
||||
this.plugin,
|
||||
this.app,
|
||||
@@ -292,7 +306,9 @@ export class ScriptEngine {
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
),
|
||||
controlsOnTop,
|
||||
);
|
||||
},
|
||||
suggester: (
|
||||
displayItems: string[],
|
||||
items: any[],
|
||||
@@ -336,6 +352,7 @@ export class ScriptEngine {
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
controlsOnTop?: boolean,
|
||||
) {
|
||||
try {
|
||||
return await GenericInputPrompt.Prompt(
|
||||
@@ -350,6 +367,7 @@ export class ScriptEngine {
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
controlsOnTop
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
|
||||
export interface InputPromptOptions {
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: ButtonDefinition[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
controlsOnTop?: boolean,
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { DynamicStyle } from "src/types/types";
|
||||
import { cloneElement } from "./excalidrawAutomateUtils";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/element/src/types";
|
||||
import { addAppendUpdateCustomData } from "./utils";
|
||||
import { mutateElement } from "src/constants/constants";
|
||||
import { CaptureUpdateAction } from "src/constants/constants";
|
||||
|
||||
export const setDynamicStyle = (
|
||||
@@ -176,7 +175,7 @@ export const setDynamicStyle = (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
mutateElement(e,{customData: f.customData});
|
||||
(view.excalidrawAPI as ExcalidrawImperativeAPI).mutateElement(e,{customData: f.customData});
|
||||
});
|
||||
|
||||
view.updateScene({
|
||||
|
||||
+68
-43
@@ -1,5 +1,4 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { request } from "http";
|
||||
import { Notice } from "obsidian";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { getEA } from "src/core";
|
||||
@@ -20,12 +19,18 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
return null;
|
||||
}
|
||||
|
||||
(view.excalidrawAPI as ExcalidrawImperativeAPI).setForceRenderAllEmbeddables(true);
|
||||
|
||||
const wasFullscreen = view.isFullscreen();
|
||||
if (!wasFullscreen) {
|
||||
view.gotoFullscreen();
|
||||
}
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
api.setForceRenderAllEmbeddables(true);
|
||||
options.selectedOnly = options.selectedOnly && (view.getViewSelectedElements().length > 0);
|
||||
const remote = window.require("electron").remote;
|
||||
const scene = view.getScene();
|
||||
const viewSelectedElements = view.getViewSelectedElements();
|
||||
const selectedIDs = new Set(options.selectedOnly ? viewSelectedElements.map(el => el.id) : []);
|
||||
const elementsToInclude = options.selectedOnly
|
||||
? view.getViewSelectedElements()
|
||||
: view.getViewElements();
|
||||
const includedElementIDs = new Set(elementsToInclude.map(el => el.id));
|
||||
const savedOpacity: { id: string; opacity: number }[] = [];
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
|
||||
@@ -39,7 +44,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
if (options.selectedOnly) {
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id)));
|
||||
ea.copyViewElementsToEAforEditing(view.getViewElements().filter(el=>!includedElementIDs.has(el.id)));
|
||||
ea.getElements().forEach(el => {
|
||||
savedOpacity.push({
|
||||
id: el.id,
|
||||
@@ -52,7 +57,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
}
|
||||
}
|
||||
|
||||
let boundingBox = ea.getBoundingBox(options.selectedOnly ? viewSelectedElements : scene.elements);
|
||||
let boundingBox = ea.getBoundingBox(elementsToInclude);
|
||||
boundingBox = {
|
||||
topX: Math.ceil(boundingBox.topX),
|
||||
topY: Math.ceil(boundingBox.topY),
|
||||
@@ -61,8 +66,8 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
}
|
||||
|
||||
const margin = options.margin;
|
||||
const availableWidth = Math.floor(view.excalidrawAPI.getAppState().width);
|
||||
const availableHeight = Math.floor(view.excalidrawAPI.getAppState().height);
|
||||
const availableWidth = Math.floor(api.getAppState().width);
|
||||
const availableHeight = Math.floor(api.getAppState().height);
|
||||
|
||||
// Apply zoom to the total dimensions
|
||||
const totalWidth = Math.ceil(boundingBox.width * options.zoom + margin * 2);
|
||||
@@ -89,7 +94,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
viewModeEnabled,
|
||||
linkOpacity,
|
||||
theme,
|
||||
} = view.excalidrawAPI.getAppState();
|
||||
} = api.getAppState();
|
||||
return {
|
||||
scrollX,
|
||||
scrollY,
|
||||
@@ -123,10 +128,11 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
|
||||
// Hide UI elements (must be after changing to view mode)
|
||||
const container = view.excalidrawWrapperRef.current;
|
||||
const layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
const layerUIWrapperOriginalDisplay = layerUIWrapper.style.display;
|
||||
layerUIWrapper.style.display = "none";
|
||||
|
||||
let layerUIWrapperOriginalDisplay = "block";
|
||||
let appBottonBarOriginalDisplay = "block";
|
||||
let layerUIWrapper: HTMLElement | null = null;
|
||||
let appBottomBar: HTMLElement | null = null;
|
||||
|
||||
const originalStyle = {
|
||||
width: container.style.width,
|
||||
height: container.style.height,
|
||||
@@ -153,10 +159,12 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(50); // wait for frame to render
|
||||
|
||||
await sleep(200); // wait for frame to render
|
||||
|
||||
// Prepare to collect tile images as data URLs
|
||||
const rect = container.getBoundingClientRect();
|
||||
const { left,top } = container.getBoundingClientRect();
|
||||
//const { offsetLeft, offsetTop } = api.getAppState();
|
||||
|
||||
const tiles = [];
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
@@ -176,16 +184,30 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
height: tileHeight
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await sleep(250);
|
||||
|
||||
//set tileWidth/tileHeight will reset the button bar
|
||||
layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
appBottomBar = container.querySelector(".App-bottom-bar");
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapperOriginalDisplay = layerUIWrapper.style.display;
|
||||
layerUIWrapper.style.display = "none";
|
||||
}
|
||||
if (appBottomBar) {
|
||||
appBottonBarOriginalDisplay = appBottomBar.style.display;
|
||||
appBottomBar.style.display = "none";
|
||||
}
|
||||
|
||||
await sleep(50);
|
||||
|
||||
// Calculate the exact width/height for this tile
|
||||
const captureWidth = col === cols - 1 ? adjustedTotalWidth - tileWidth * (cols - 1) : tileWidth;
|
||||
const captureHeight = row === rows - 1 ? adjustedTotalHeight - tileHeight * (rows - 1) : tileHeight;
|
||||
|
||||
const image = await remote.getCurrentWebContents().capturePage({
|
||||
x: rect.left * devicePixelRatio,
|
||||
y: rect.top * devicePixelRatio,
|
||||
x: left, //offsetLeft,
|
||||
y: top, //offsetTop,
|
||||
width: captureWidth * devicePixelRatio,
|
||||
height: captureHeight * devicePixelRatio,
|
||||
});
|
||||
@@ -243,31 +265,34 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
new Notice(t("SCREENSHOT_ERROR"));
|
||||
return null;
|
||||
} finally {
|
||||
// Restore opacity for selected elements if necessary
|
||||
if (options.selectedOnly && savedOpacity.length > 0) {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(view.getViewElements().filter(el => !includedElementIDs.has(el.id)));
|
||||
savedOpacity.forEach(x => {
|
||||
ea.getElement(x.id).opacity = x.opacity;
|
||||
});
|
||||
await ea.addElementsToView(false, false, false, false);
|
||||
}
|
||||
|
||||
// Restore browser zoom to its original value
|
||||
webContents.setZoomFactor(originalZoomFactor);
|
||||
|
||||
// Restore UI elements
|
||||
try {
|
||||
const container = view.excalidrawWrapperRef.current;
|
||||
const layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapper.style.display = layerUIWrapperOriginalDisplay;
|
||||
}
|
||||
|
||||
// Restore original state
|
||||
restoreState(savedState);
|
||||
|
||||
// Restore opacity for selected elements if necessary
|
||||
if (options.selectedOnly && savedOpacity.length > 0) {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id)));
|
||||
savedOpacity.forEach(x => {
|
||||
ea.getElement(x.id).opacity = x.opacity;
|
||||
});
|
||||
await ea.addElementsToView(false, false, false, false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in cleanup:", e);
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapper.style.display = layerUIWrapperOriginalDisplay;
|
||||
}
|
||||
|
||||
if (appBottomBar) {
|
||||
appBottomBar.style.display = appBottonBarOriginalDisplay;
|
||||
}
|
||||
|
||||
// Restore original state
|
||||
restoreState(savedState);
|
||||
|
||||
if(!wasFullscreen) {
|
||||
view.exitFullscreen();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ export async function checkExcalidrawVersion() {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
errorlog({ where: "Utils/checkExcalidrawVersion", error: e });
|
||||
console.log({ where: "Utils/checkExcalidrawVersion", error: e });
|
||||
}
|
||||
versionUpdateCheckTimer = window.setTimeout(() => {
|
||||
versionUpdateChecked = false;
|
||||
|
||||
@@ -390,6 +390,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
return this.ownerDocument.defaultView;
|
||||
}
|
||||
|
||||
get isInMainObsidianWorkspace(): boolean {
|
||||
return document === this.ownerDocument;
|
||||
}
|
||||
|
||||
setHookServer(ea?:ExcalidrawAutomate) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setHookServer, "ExcalidrawView.setHookServer", ea);
|
||||
if(ea) {
|
||||
@@ -1154,6 +1158,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
doc.body.querySelectorAll(`div.workspace-ribbon`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.mobile-navbar`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.status-bar`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.titlebar`).forEach(node=>node.addClass(HIDE));
|
||||
}
|
||||
|
||||
hide(this.contentEl);
|
||||
|
||||
@@ -185,6 +185,7 @@ function RenderObsidianView(
|
||||
rootSplit.containerEl.style.width = '100%';
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
view.plugin.setDebounceActiveLeafChangeHandler();
|
||||
leafRef.current = {
|
||||
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null,
|
||||
|
||||
+38
-1
@@ -734,4 +734,41 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
|
||||
.excalidraw .context-menu {
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw-prompt-buttonbar-top,
|
||||
.excalidraw-prompt-buttonbar-bottom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start; /* keep both rows top‐aligned */
|
||||
row-gap: 0.5em; /* vertical space when wrapped */
|
||||
}
|
||||
|
||||
/* top bar specifics */
|
||||
.excalidraw-prompt-buttonbar-top {
|
||||
padding: 0.5em 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* bottom bar specifics */
|
||||
.excalidraw-prompt-buttonbar-bottom {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* make each child a flex row */
|
||||
.excalidraw-prompt-buttonbar-top > div,
|
||||
.excalidraw-prompt-buttonbar-bottom > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* push the first group to the left */
|
||||
.excalidraw-prompt-buttonbar-top > div:first-child,
|
||||
.excalidraw-prompt-buttonbar-bottom > div:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* push the second group to the right */
|
||||
.excalidraw-prompt-buttonbar-top > div:last-child,
|
||||
.excalidraw-prompt-buttonbar-bottom > div:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user