This commit is contained in:
zsviczian
2023-04-22 21:24:33 +02:00
parent 7ab8f07d1f
commit 7e930c2339
13 changed files with 136 additions and 56 deletions

View File

@@ -1,16 +1,16 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg)
iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the script creates a text element at the pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then the script opens the input prompt where you can modify this text with scribble.
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.24")) {
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/MIZ5hv-pSSs";
const helpLINK = "https://youtu.be/BvYkOaly-QM";
const DBLCLICKTIMEOUT = 300;
const maxWidth = 600;
const padding = 6;
@@ -84,21 +84,6 @@ let containerElements = ea.getViewSelectedElements()
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
let textLocationCache = {};
//Address text jumping on mobile when keyboard is popping up
//This should really be solved in the core product, but that is not yet happening
const cacheTextElCoords = () => {
if(selectedTextElements.length === 1) {
textLocationCache.x = selectedTextElements[0].x;
textLocationCache.y = selectedTextElements[0].y;
return;
}
delete textLocationCache.x;
delete textLocationCache.y;
}
const isLocationCacheValid = () => !(typeof textLocationCache.x === "undefined");
cacheTextElCoords();
//-------------------------------------------
// Functions to add and remove event listners
//-------------------------------------------
@@ -124,7 +109,7 @@ if (win.ExcalidrawScribbleHelper?.eventHandler) {
delete win.ExcalidrawScribbleHelper.eventHandler;
delete win.ExcalidrawScribbleHelper.window;
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
new Notice ("Scribble Helper was stopped");
new Notice ("Scribble Helper was stopped",1000);
return;
}
silent = true;
@@ -143,8 +128,8 @@ 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 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`)
@@ -183,11 +168,13 @@ const customControls = (container) => {
eventHandler = async (evt) => {
if(windowOpen) return;
if(ea.targetView !== app.workspace.activeLeaf.view) removeEventHandler(eventHandler);
if(evt && (evt.ctrlKey || evt.altKey || evt.metaKey || evt.shiftKey)) return;
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;
if(st.editingElement) return; //don't trigger text editor when editing a line or arrow
//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;
@@ -277,20 +264,22 @@ eventHandler = async (evt) => {
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"
: st.currentItemTextAlign ?? ea.style.textAlign;
ea.style.verticalAlign = (container && ["arrow","line"].contains(container.type))
? "middle"
: ea.style.verticalAlign;
: (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
"Edit text", "", "", containerID?undefined:actionButtons, 5, true, customControls, true
);
windowOpen = false;
@@ -309,6 +298,10 @@ eventHandler = async (evt) => {
ea.style.strokeColor = "transparent";
}
if(!container && (win.ExcalidrawScribbleHelper.action === "Sticky")) {
textEl.textAlign = "center";
}
const boxes = [];
if(container) {
boxes.push(containerID);
@@ -336,6 +329,9 @@ eventHandler = async (evt) => {
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));
@@ -350,14 +346,9 @@ const editExistingTextElement = async (elements) => {
windowOpen = true;
ea.copyViewElementsToEAforEditing(elements);
const el = ea.getElements()[0];
//this is a hack to address jumping text
if(isLocationCacheValid()) {
el.x = textLocationCache.x;
el.y = textLocationCache.y;
}
ea.style.strokeColor = el.strokeColor;
const text = await utils.inputPrompt(
"Edit text","",elements[0].rawText,undefined,5,true,customControls
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true
);
windowOpen = false;
if(!text) return;
@@ -380,9 +371,10 @@ const editExistingTextElement = async (elements) => {
//--------------
if(!win.ExcalidrawScribbleHelper?.eventHandler) {
if(!silent) new Notice(
"Double click the screen to create a new text element, " +
"or double click an element to add or edit text\nClick the script again or move to " +
"another Obsidian-tab to stop the script"
"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);
}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -18,7 +18,7 @@
"license": "MIT",
"dependencies": {
"@types/lz-string": "^1.3.34",
"@zsviczian/excalidraw": "0.14.2-obsidian-5",
"@zsviczian/excalidraw": "0.15.2-obsidian-1",
"chroma-js": "^2.4.2",
"clsx": "^1.2.1",
"colormaster": "^1.2.1",

View File

@@ -37,6 +37,7 @@ import {
LinkParts,
svgToBase64,
} from "./utils/Utils";
import { ValueOf } from "./types";
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
@@ -49,15 +50,27 @@ const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
//and getObsidianImage is aborted if the file is already in the Watchdog stack
const markdownRendererRecursionWatcthdog = new Set<TFile>();
export declare type MimeType =
| "image/svg+xml"
export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
ico: "image/x-icon",
avif: "image/avif",
jfif: "image/jfif",
} as const;
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
/* | "image/svg+xml"
| "image/png"
| "image/jpeg"
| "image/gif"
| "image/webp"
| "image/bmp"
| "image/x-icon"
| "application/octet-stream";
| "application/octet-stream";*/
export type FileData = BinaryFileData & {
size: Size;
hasSVGwithBitmap: boolean;

View File

@@ -610,7 +610,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
opacity: this.style.opacity,
roundness: this.style.strokeSharpness
? (this.style.strokeSharpness === "round"
? {type: ROUNDNESS.LEGACY}
? {type: ROUNDNESS.ADAPTIVE_RADIUS}
: null)
: this.style.roundness,
seed: Math.floor(Math.random() * 100000),

View File

@@ -76,6 +76,15 @@ export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while(!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getRes: (text: string): IterableIterator<RegExpMatchArray> => {
return text.matchAll(REGEX_LINK.EXPR);
},

View File

@@ -778,11 +778,13 @@ export default class ExcalidrawView extends TextFileView {
}
removeLinkTooltip() {
//.classList.remove("excalidraw-tooltip--visible");document.querySelector(".excalidraw-tooltip",);
const tooltip = this.ownerDocument.body.querySelector(
"body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible",
);
if (tooltip) {
this.ownerDocument.body.removeChild(tooltip);
tooltip.classList.remove("excalidraw-tooltip--visible")
//this.ownerDocument.body.removeChild(tooltip);
}
}
@@ -886,7 +888,19 @@ export default class ExcalidrawView extends TextFileView {
if(this.handleLinkHookCall(el,linkText,ev)) return;
if(this.openExternalLink(linkText)) return;
const parts = REGEX_LINK.getRes(linkText).next();
const partsArray = REGEX_LINK.getResList(linkText);
let parts = partsArray[0];
if (partsArray.length > 1) {
parts = await ScriptEngine.suggester(
app,
partsArray.filter(p=>Boolean(p.value)).map(p => REGEX_LINK.getLink(p)),
partsArray.filter(p=>Boolean(p.value)),
"Select link to open"
);
if(!parts) return;
}
//parts = REGEX_LINK.getRes(linkText).next();
if (!parts.value) {
this.openTagSearch(linkText);
return;
@@ -3497,21 +3511,30 @@ export default class ExcalidrawView extends TextFileView {
if (!element) {
return;
}
const link = element.link;
let link = element.link;
if (!link || link === "") {
return;
}
this.removeLinkTooltip();
setTimeout(()=>this.removeLinkTooltip(),500);
const event = e?.detail?.nativeEvent;
if(this.handleLinkHookCall(element,element.link,event)) return;
if(this.openExternalLink(element.link, !isSHIFT(event) && !isCTRL(event) && !isMETA(event) && !isALT(event) ? element : undefined)) return;
//if element is type text and element has multiple links, then submit the element text to linkClick to trigger link suggester
if(element.type === "text") {
const linkText = element.rawText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
const partsArray = REGEX_LINK.getResList(linkText);
if(partsArray.filter(p=>Boolean(p.value)).length > 1) {
link = linkText;
}
}
this.linkClick(
event,
null,
null,
{id: element.id, text: element.link},
{id: element.id, text: link},
emulateCTRLClickForLinks(event)
);
return;

View File

@@ -228,6 +228,7 @@ export class ScriptEngine {
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) =>
ScriptEngine.inputPrompt(
view,
@@ -240,6 +241,7 @@ export class ScriptEngine {
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
),
suggester: (
displayItems: string[],
@@ -285,7 +287,8 @@ export class ScriptEngine {
buttons?: ButtonDefinition[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) {
try {
return await GenericInputPrompt.Prompt(
@@ -298,7 +301,8 @@ export class ScriptEngine {
buttons,
lines,
displayEditorButtons,
customComponents
customComponents,
blockPointerInputOutsideModal,
);
} catch {
return undefined;

View File

@@ -16,6 +16,23 @@ export const RELEASE_NOTES: { [k: string]: string } = {
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"1.8.25": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New & improved
- Multi-link support
- Updated [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script for better handwritten text support.
- Add links to text elements
- Creating wrapped text in transparent sticky notes
- Add text to arrows and lines
- Handwriting support on iOS via Scribble
## Fixed
- The long-standing issue of jumping text
`,
"1.8.24": `
## Updates from Excalidraw.com

View File

@@ -90,6 +90,7 @@ export class GenericInputPrompt extends Modal {
private selectionEnd: number = 0;
private selectionUpdateTimer: number = 0;
private customComponents: (container: HTMLElement) => void;
private blockPointerInputOutsideModal: boolean = false;
public static Prompt(
view: ExcalidrawView,
@@ -102,6 +103,7 @@ export class GenericInputPrompt extends Modal {
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
): Promise<string> {
const newPromptModal = new GenericInputPrompt(
view,
@@ -113,7 +115,8 @@ export class GenericInputPrompt extends Modal {
buttons,
lines,
displayEditorButtons,
customComponents
customComponents,
blockPointerInputOutsideModal,
);
return newPromptModal.waitForClose;
}
@@ -129,6 +132,7 @@ export class GenericInputPrompt extends Modal {
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) {
super(app);
this.view = view;
@@ -139,6 +143,7 @@ export class GenericInputPrompt extends Modal {
this.lines = lines ?? 1;
this.displayEditorButtons = this.lines > 1 ? (displayEditorButtons ?? false) : false;
this.customComponents = customComponents;
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
@@ -152,6 +157,12 @@ export class GenericInputPrompt extends Modal {
private display() {
this.contentEl.empty();
if(this.blockPointerInputOutsideModal) {
//@ts-ignore
const bgEl = this.bgEl;
bgEl.style.pointerEvents = this.blockPointerInputOutsideModal ? "none" : "auto";
}
this.titleEl.textContent = this.header;
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
@@ -271,6 +282,9 @@ export class GenericInputPrompt extends Modal {
private linkBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
const addText = (text: string) => {
const v = this.inputComponent.inputEl.value;
if(this.selectionStart>0 && v.slice(this.selectionStart-1, this.selectionStart) !== " ") text = " "+text;
if(this.selectionStart<v.length && v.slice(this.selectionStart, this.selectionStart+1) !== " ") text = text+" ";
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + text + this.inputComponent.inputEl.value.slice(this.selectionStart);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
@@ -360,7 +374,6 @@ export class GenericInputPrompt extends Modal {
onOpen() {
super.onOpen();
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.select();
}

View File

@@ -132,6 +132,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "setStrokeSharpness",
code: "setStrokeSharpness(sharpness: number): void;",
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
after: "",
},
{
field: "addToGroup",
code: "addToGroup(objectIds: []): string;",
@@ -521,11 +527,12 @@ 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);",
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);",
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 > 1 \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" +
"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: "",

2
src/types.d.ts vendored
View File

@@ -15,6 +15,8 @@ export type Packages = {
excalidrawLib: any,
}
export type ValueOf<T> = T[keyof T];
export type DynamicStyle = "none" | "gray" | "colorful";
export interface ExcalidrawAutomateInterface {