Compare commits

...

5 Commits

Author SHA1 Message Date
zsviczian
6edd8b9a4e Implement draggable inputPrompt 2025-05-26 15:51:34 +00:00
zsviczian
778346b0dd Merge pull request #2358 from dmscode/zh-language-update/2025-05-26
Update zh-cn.ts to ff404e4
2025-05-26 11:36:06 +02:00
dmscode
85ac633263 Update zh-cn.ts to ff404e4 2025-05-26 05:56:02 +08:00
zsviczian
ff404e4dd6 text to path minor changes
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-25 23:01:00 +02:00
zsviczian
d0845a7d68 text to path updated 2025-05-25 22:56:20 +02:00
8 changed files with 2125 additions and 1423 deletions

View File

@@ -1,5 +1,5 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-path.jpg)
This script allows you to fit a text element along a selected path: line, arrow, freedraw, ellipse, rectangle, or diamond. You can select either a path or a text element, or both:
@@ -961,6 +961,9 @@ function diamondToLine(diamond, pointDensity = 16) {
}
async function addToView() {
ea.getElements()
.filter(el=>el.type==="text" && el.text === " " && !el.isDeleted)
.forEach(el=>tempElementIDs.push(el.id));
tempElementIDs.forEach(elID=>{
delete ea.elementsDict[elID];
});
@@ -1063,12 +1066,7 @@ async function fitTextToShape() {
// Place text along the path with natural spacing
const offsetValue = (parseInt(win.TextArchOffset ?? initialOffset) || 0);
// The `spacing` parameter (ea.measureText("i").width*0.3) is not used by distributeTextAlongPath
// as character advances are now calculated from substring widths.
// Pass 0 for spacing, or remove it if it's truly unused.
// For now, let's assume it might be intended for *extra* spacing beyond natural kerning.
// However, the current distributeTextAlongPath doesn't use the `spacing` parameter.
// Let's remove charWidths, charHeights, and spacing from the call as they are not used.
distributeTextAlongPath(text, pathPoints, pathID, objectIDs, offsetValue, isLeftToRight);
// Add all text characters to a group

File diff suppressed because one or more lines are too long

3392
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -991,6 +991,7 @@ FILENAME_HEAD: "文件名",
//Utils.ts
UPDATE_AVAILABLE: `Excalidraw 的新版本已在社区插件中可用。\n\n您正在使用 ${PLUGIN_VERSION}\n最新版本是`,
SCRIPT_UPDATES_AVAILABLE : `脚本更新可用 - 请检查脚本存储。\n\n ${ DEVICE . isDesktop ? `此消息可在控制台日志中查看 ( ${ DEVICE . isMacOS ? "CMD+OPT+i" : "CTRL+SHIFT+i" } )\n\n` : "" } 如果您已将脚本组织到脚本存储文件夹下的子文件夹中,并且存在同一脚本的多个副本,可能需要清理未使用的版本以消除此警报。对于不需要更新的私人脚本副本,请将它们存储在脚本存储文件夹之外。` ,
ERROR_PNG_TOO_LARGE: "导出 PNG 时出错 - PNG 文件过大,请尝试较小的分辨率",
// ModifierkeyHelper.ts

View File

@@ -98,6 +98,8 @@ export class GenericInputPrompt extends Modal {
private customComponents: (container: HTMLElement) => void;
private blockPointerInputOutsideModal: boolean = false;
private controlsOnTop: boolean = false;
private draggable: boolean = false;
private cleanupDragListeners: (() => void) | null = null;
public static Prompt(
view: ExcalidrawView,
@@ -112,6 +114,7 @@ export class GenericInputPrompt extends Modal {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
): Promise<string> {
const newPromptModal = new GenericInputPrompt(
view,
@@ -126,6 +129,7 @@ export class GenericInputPrompt extends Modal {
customComponents,
blockPointerInputOutsideModal,
controlsOnTop,
draggable,
);
return newPromptModal.waitForClose;
}
@@ -143,6 +147,7 @@ export class GenericInputPrompt extends Modal {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
) {
super(app);
this.view = view;
@@ -155,6 +160,7 @@ export class GenericInputPrompt extends Modal {
this.customComponents = customComponents;
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
this.controlsOnTop = controlsOnTop ?? false;
this.draggable = draggable ?? false;
this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
@@ -473,12 +479,137 @@ export class GenericInputPrompt extends Modal {
super.onOpen();
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.select();
if (this.draggable) {
this.makeModalDraggable();
}
}
private makeModalDraggable() {
let isDragging = false;
let startX: number, startY: number, initialX: number, initialY: number;
let activeElement: HTMLElement | null = null;
let cursorPosition: { start: number; end: number } | null = null;
const modalEl = this.modalEl;
const header = modalEl.querySelector('.modal-titlebar') || modalEl.querySelector('.modal-title') || modalEl;
(header as HTMLElement).style.cursor = 'move';
// Track focus changes to store the last focused interactive element
const onFocusIn = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (target && (target.tagName === 'SELECT' || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'BUTTON')) {
activeElement = target;
// Store cursor position for input/textarea elements (but not for number inputs)
if (target.tagName === 'TEXTAREA' ||
(target.tagName === 'INPUT' && (target as HTMLInputElement).type !== 'number')) {
const inputEl = target as HTMLInputElement | HTMLTextAreaElement;
cursorPosition = {
start: inputEl.selectionStart || 0,
end: inputEl.selectionEnd || 0
};
} else {
cursorPosition = null;
}
}
};
const onPointerDown = (e: PointerEvent) => {
const target = e.target as HTMLElement;
// Don't allow dragging if clicking on interactive controls
if (target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'BUTTON' ||
target.tagName === 'SELECT' ||
target.closest('button') ||
target.closest('input') ||
target.closest('textarea') ||
target.closest('select')) {
return;
}
// Allow dragging from header or modal content areas
if (!header.contains(target) && !modalEl.querySelector('.modal-content')?.contains(target)) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = modalEl.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
modalEl.style.position = 'absolute';
modalEl.style.margin = '0';
modalEl.style.left = `${initialX}px`;
modalEl.style.top = `${initialY}px`;
};
const onPointerMove = (e: PointerEvent) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
modalEl.style.left = `${initialX + dx}px`;
modalEl.style.top = `${initialY + dy}px`;
};
const onPointerUp = () => {
if (!isDragging) return;
isDragging = false;
// Restore focus and cursor position
if (activeElement && activeElement.isConnected) {
// Use setTimeout to ensure the pointer event is fully processed
setTimeout(() => {
activeElement.focus();
// Restore cursor position for input/textarea elements (but not for number inputs)
if (cursorPosition &&
(activeElement.tagName === 'TEXTAREA' ||
(activeElement.tagName === 'INPUT' && (activeElement as HTMLInputElement).type !== 'number'))) {
const inputEl = activeElement as HTMLInputElement | HTMLTextAreaElement;
inputEl.setSelectionRange(cursorPosition.start, cursorPosition.end);
}
}, 0);
}
};
// Initialize activeElement with the main input field
activeElement = this.inputComponent.inputEl;
cursorPosition = {
start: this.inputComponent.inputEl.selectionStart || 0,
end: this.inputComponent.inputEl.selectionEnd || 0
};
// Set up event listeners
modalEl.addEventListener('focusin', onFocusIn);
modalEl.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
// Store cleanup function for use in onClose
this.cleanupDragListeners = () => {
modalEl.removeEventListener('focusin', onFocusIn);
modalEl.removeEventListener('pointerdown', onPointerDown);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
};
}
onClose() {
super.onClose();
this.resolveInput();
this.removeInputListener();
// Clean up drag listeners to prevent memory leaks
if (this.cleanupDragListeners) {
this.cleanupDragListeners();
this.cleanupDragListeners = null;
}
}
}

View File

@@ -956,7 +956,7 @@ export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
"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: `({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})`,
after: `({\n header: "",\n placeholder: undefined, //string\n value: undefined, //string\n buttons: [{ //optional, may leave undefined\n caption: "", //string\n tooltip: undefined, //string\n action: (input)=>{} //Function\n }],\n lines: undefined, //number\n displayEditorButtons: undefined, //boolean\n customComponents: undefined, //(container: HTMLElement) => void\n blockPointerInputOutsideModal: undefined, //boolean\n controlsOnTop: undefined, //boolean\n draggable: undefined, //boolean\n});`,
},
{
field: "suggester",

View File

@@ -281,6 +281,7 @@ export class ScriptEngine {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
) => {
if (typeof header === "object") {
const options = header as InputPromptOptions;
@@ -293,6 +294,7 @@ export class ScriptEngine {
customComponents = options.customComponents;
blockPointerInputOutsideModal = options.blockPointerInputOutsideModal;
controlsOnTop = options.controlsOnTop;
draggable = options.draggable;
}
return ScriptEngine.inputPrompt(
view,
@@ -307,6 +309,7 @@ export class ScriptEngine {
customComponents,
blockPointerInputOutsideModal,
controlsOnTop,
draggable
);
},
suggester: (
@@ -353,6 +356,7 @@ export class ScriptEngine {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable: boolean = false,
) {
try {
return await GenericInputPrompt.Prompt(
@@ -367,7 +371,8 @@ export class ScriptEngine {
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
controlsOnTop
controlsOnTop,
draggable
);
} catch {
return undefined;

View File

@@ -10,4 +10,5 @@ export interface InputPromptOptions {
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
controlsOnTop?: boolean,
draggable?: boolean,
}