mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
386 lines
14 KiB
Markdown
386 lines
14 KiB
Markdown
/*
|
|

|
|
|
|
Scribble Helper can improve handwriting and add links. It lets you create and edit text elements, including wrapped text and sticky notes, by double-tapping on the canvas. When you run the script, it creates an event handler that will activate the editor when you double-tap. If you select a text element on the canvas before running the script, it will open the editor for that element. If you use a pen, you can set it up to only activate Scribble Helper when you double-tap with the pen. The event handler is removed when you run the script a second time or switch to a different tab.
|
|
|
|
```javascript
|
|
*/
|
|
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.25")) {
|
|
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
|
return;
|
|
}
|
|
|
|
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;
|
|
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
|
|
|
|
// -------------
|
|
// Load settings
|
|
// -------------
|
|
let settings = ea.getScriptSettings();
|
|
//set default values on first-ever run of the script
|
|
if(!settings["Default action"]) {
|
|
settings = {
|
|
"Default action" : {
|
|
value: "Text",
|
|
valueset: ["Text","Sticky","Wrap"],
|
|
description: "What type of element should CTRL/CMD+ENTER create. TEXT: A regular text element. " +
|
|
"STICKY: A sticky note with border color and background color " +
|
|
"(using the current setting of the canvas). STICKY: A sticky note with transparent " +
|
|
"border and background color."
|
|
},
|
|
};
|
|
await ea.setScriptSettings(settings);
|
|
}
|
|
|
|
if(typeof win.ExcalidrawScribbleHelper.action === "undefined") {
|
|
win.ExcalidrawScribbleHelper.action = settings["Default action"].value;
|
|
}
|
|
|
|
//---------------------------------------
|
|
// 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",
|
|
"#862e9c", "#5f3dc4", "#364fc7", "#1864ab", "#0b7285",
|
|
"#087f5b", "#2b8a3e", "#5c940d", "#e67700", "#d9480f"
|
|
];
|
|
|
|
const loadColorPalette = () => {
|
|
const st = api.getAppState();
|
|
const strokeColors = new Set();
|
|
let strokeColorPalette = st.colorPalette?.elementStroke ?? defaultStrokeColors;
|
|
if(Object.entries(strokeColorPalette).length === 0) {
|
|
strokeColorPalette = defaultStrokeColors;
|
|
}
|
|
|
|
ea.getViewElements().forEach(el => {
|
|
if(el.strokeColor.toLowerCase()==="transparent") return;
|
|
strokeColors.add(el.strokeColor);
|
|
});
|
|
|
|
strokeColorPalette.forEach(color => {
|
|
strokeColors.add(color)
|
|
});
|
|
|
|
strokeColors.add(st.currentItemStrokeColor ?? ea.style.strokeColor);
|
|
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) => {
|
|
if(win.ExcalidrawScribbleHelper.eventHandler) {
|
|
win.removeEventListner("pointerdown", handler);
|
|
}
|
|
win.addEventListener("pointerdown",handler);
|
|
win.ExcalidrawScribbleHelper.eventHandler = handler;
|
|
win.ExcalidrawScribbleHelper.window = win;
|
|
}
|
|
|
|
const 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;
|
|
}
|
|
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) => {
|
|
const helpDIV = container.createDiv();
|
|
helpDIV.innerHTML = `<a href="${helpLINK}" target="_blank">Click here for help</a>`;
|
|
const viewBackground = api.getAppState().viewBackgroundColor;
|
|
const el1 = new ea.obsidian.Setting(container)
|
|
.setName(`Text color`)
|
|
.addDropdown(dropdown => {
|
|
Array.from(loadColorPalette()).forEach(color => {
|
|
const options = dropdown.addOption(color, color).selectEl.options;
|
|
options[options.length-1].setAttribute("style",`color: ${color
|
|
}; background: ${viewBackground};`);
|
|
});
|
|
dropdown
|
|
.setValue(ea.style.strokeColor)
|
|
.onChange(value => {
|
|
ea.style.strokeColor = value;
|
|
el1.nameEl.style.color = value;
|
|
})
|
|
})
|
|
el1.nameEl.style.color = ea.style.strokeColor;
|
|
el1.nameEl.style.background = viewBackground;
|
|
el1.nameEl.style.fontWeight = "bold";
|
|
|
|
const el2 = new ea.obsidian.Setting(container)
|
|
.setName(`Trigger editor by pen double tap only`)
|
|
.addToggle((toggle) => toggle
|
|
.setValue(win.ExcalidrawScribbleHelper.penOnly)
|
|
.onChange(value => {
|
|
win.ExcalidrawScribbleHelper.penOnly = value;
|
|
})
|
|
)
|
|
el2.settingEl.style.border = "none";
|
|
el2.settingEl.style.display = win.ExcalidrawScribbleHelper.penDetected ? "" : "none";
|
|
}
|
|
|
|
// -------------------------------
|
|
// Click / dbl click event handler
|
|
// -------------------------------
|
|
eventHandler = async (evt) => {
|
|
if(windowOpen) return;
|
|
if(ea.targetView !== app.workspace.activeLeaf.view) removeEventHandler(eventHandler);
|
|
if(evt && evt.target && !evt.target.hasClass("excalidraw__canvas")) return;
|
|
if(evt && (evt.ctrlKey || evt.altKey || evt.metaKey || evt.shiftKey)) return;
|
|
const st = api.getAppState();
|
|
win.ExcalidrawScribbleHelper.penDetected = st.penDetected;
|
|
|
|
//don't trigger text editor when editing a line or arrow
|
|
if(st.editingElement && ["arrow","line"].contains(st.editingElment.type)) return;
|
|
|
|
if(typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
|
win.ExcalidrawScribbleHelper.penOnly = false;
|
|
}
|
|
|
|
if (evt && win.ExcalidrawScribbleHelper.penOnly &&
|
|
win.ExcalidrawScribbleHelper.penDetected && evt.pointerType !== "pen") return;
|
|
const now = Date.now();
|
|
|
|
//the <50 condition is to avoid false double click when pinch zooming
|
|
if((now-timer > DBLCLICKTIMEOUT) || (now-timer < 50)) {
|
|
prevZoomValue = st.zoom.value;
|
|
timer = now;
|
|
containerElements = ea.getViewSelectedElements()
|
|
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
|
selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
|
return;
|
|
}
|
|
//further safeguard against triggering when pinch zooming
|
|
if(st.zoom.value !== prevZoomValue) return;
|
|
|
|
//sleeping to allow keyboard to pop up on mobile devices
|
|
await sleep(200);
|
|
ea.clear();
|
|
|
|
//if a single element with text is selected, edit the text
|
|
//(this can be an arrow, a sticky note, or just a text element)
|
|
if(selectedTextElements.length === 1) {
|
|
editExistingTextElement(selectedTextElements);
|
|
return;
|
|
}
|
|
|
|
let containerID;
|
|
let container;
|
|
//if no text elements are selected (i.e. not multiple text elements selected),
|
|
//check if there is a single eligeable container selected
|
|
if(selectedTextElements.length === 0) {
|
|
if(containerElements.length === 1) {
|
|
ea.copyViewElementsToEAforEditing(containerElements);
|
|
containerID = containerElements[0].id
|
|
container = ea.getElement(containerID);
|
|
}
|
|
}
|
|
|
|
const {x,y} = ea.targetView.currentPosition;
|
|
|
|
if(ea.targetView !== app.workspace.activeLeaf.view) return;
|
|
const actionButtons = [
|
|
{
|
|
caption: `A`,
|
|
tooltip: "Add as Text Element",
|
|
action: () => {
|
|
win.ExcalidrawScribbleHelper.action="Text";
|
|
if(settings["Default action"].value!=="Text") {
|
|
settings["Default action"].value = "Text";
|
|
ea.setScriptSettings(settings);
|
|
};
|
|
return;
|
|
}
|
|
},
|
|
{
|
|
caption: "📝",
|
|
tooltip: "Add as Sticky Note (rectangle with border color and background color)",
|
|
action: () => {
|
|
win.ExcalidrawScribbleHelper.action="Sticky";
|
|
if(settings["Default action"].value!=="Sticky") {
|
|
settings["Default action"].value = "Sticky";
|
|
ea.setScriptSettings(settings);
|
|
};
|
|
return;
|
|
}
|
|
},
|
|
{
|
|
caption: "☱",
|
|
tooltip: "Add as Wrapped Text (rectangle with transparent border and background)",
|
|
action: () => {
|
|
win.ExcalidrawScribbleHelper.action="Wrap";
|
|
if(settings["Default action"].value!=="Wrap") {
|
|
settings["Default action"].value = "Wrap";
|
|
ea.setScriptSettings(settings);
|
|
};
|
|
return;
|
|
}
|
|
}
|
|
];
|
|
if(win.ExcalidrawScribbleHelper.action !== "Text") actionButtons.push(actionButtons.shift());
|
|
if(win.ExcalidrawScribbleHelper.action === "Wrap") actionButtons.push(actionButtons.shift());
|
|
|
|
ea.style.strokeColor = st.currentItemStrokeColor ?? ea.style.strokeColor;
|
|
ea.style.roughness = st.currentItemRoughness ?? ea.style.roughness;
|
|
ea.setStrokeSharpness(st.currentItemRoundness === "round" ? 0 : st.currentItemRoundness)
|
|
ea.style.backgroundColor = st.currentItemBackgroundColor ?? ea.style.backgroundColor;
|
|
ea.style.fillStyle = st.currentItemFillStyle ?? ea.style.fillStyle;
|
|
ea.style.fontFamily = st.currentItemFontFamily ?? ea.style.fontFamily;
|
|
ea.style.fontSize = st.currentItemFontSize ?? ea.style.fontSize;
|
|
ea.style.textAlign = (container && ["arrow","line"].contains(container.type))
|
|
? "center"
|
|
: (container && ["rectangle","diamond","ellipse"].contains(container.type))
|
|
? "center"
|
|
: st.currentItemTextAlign ?? "center";
|
|
ea.style.verticalAlign = "middle";
|
|
|
|
windowOpen = true;
|
|
const text = await utils.inputPrompt (
|
|
"Edit text", "", "", containerID?undefined:actionButtons, 5, true, customControls, true
|
|
);
|
|
windowOpen = false;
|
|
|
|
if(!text || text.trim() === "") return;
|
|
|
|
const textId = ea.addText(x,y, text);
|
|
if (!container && (win.ExcalidrawScribbleHelper.action === "Text")) {
|
|
ea.addElementsToView(false, false, true);
|
|
addEventHandler(eventHandler);
|
|
return;
|
|
}
|
|
const textEl = ea.getElement(textId);
|
|
|
|
if(!container && (win.ExcalidrawScribbleHelper.action === "Wrap")) {
|
|
ea.style.backgroundColor = "transparent";
|
|
ea.style.strokeColor = "transparent";
|
|
}
|
|
|
|
if(!container && (win.ExcalidrawScribbleHelper.action === "Sticky")) {
|
|
textEl.textAlign = "center";
|
|
}
|
|
|
|
const boxes = [];
|
|
if(container) {
|
|
boxes.push(containerID);
|
|
const linearElement = ["arrow","line"].contains(container.type);
|
|
const l = linearElement ? container.points.length-1 : 0;
|
|
const dx = linearElement && (container.points[l][0] < 0) ? -1 : 1;
|
|
const dy = linearElement && (container.points[l][1] < 0) ? -1 : 1;
|
|
cx = container.x + dx*container.width/2;
|
|
cy = container.y + dy*container.height/2;
|
|
textEl.x = cx - textEl.width/2;
|
|
textEl.y = cy - textEl.height/2;
|
|
}
|
|
|
|
if(!container) {
|
|
const width = textEl.width+2*padding;
|
|
const widthOK = width<=maxWidth;
|
|
containerID = ea.addRect(
|
|
textEl.x-padding,
|
|
textEl.y-padding,
|
|
widthOK ? width : maxWidth,
|
|
textEl.height + 2 * padding
|
|
);
|
|
container = ea.getElement(containerID);
|
|
}
|
|
boxes.push(containerID);
|
|
container.boundElements=[{type:"text",id: textId}];
|
|
textEl.containerId = containerID;
|
|
//ensuring the correct order of elements, first container, then text
|
|
delete ea.elementsDict[textEl.id];
|
|
ea.elementsDict[textEl.id] = textEl;
|
|
|
|
await ea.addElementsToView(false,false,true);
|
|
const containers = ea.getViewElements().filter(el=>boxes.includes(el.id));
|
|
if(["rectangle","diamond","ellipse"].includes(container.type)) api.updateContainerSize(containers);
|
|
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);
|
|
}
|
|
}
|
|
|
|
//--------------
|
|
// Start actions
|
|
//--------------
|
|
if(!win.ExcalidrawScribbleHelper?.eventHandler) {
|
|
if(!silent) new Notice(
|
|
"To create a new text element,\ndouble-tap the screen.\n\n" +
|
|
"To edit text,\ndouble-tap an existing element.\n\n" +
|
|
"To stop the script,\ntap it again or switch to a different tab.",
|
|
5000
|
|
);
|
|
addEventHandler(eventHandler);
|
|
}
|
|
|
|
if(containerElements.length === 1 || selectedTextElements.length === 1) {
|
|
timer = timer - 100;
|
|
eventHandler();
|
|
} |