mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Implement draggable inputPrompt
This commit is contained in:
3392
package-lock.json
generated
3392
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface InputPromptOptions {
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
controlsOnTop?: boolean,
|
||||
draggable?: boolean,
|
||||
}
|
||||
Reference in New Issue
Block a user