diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 1d77a3c..adc1e0e 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -316,6 +316,39 @@ import { t } from './utils/i18n.js'; }); } + // window.prompt() is a silent no-op inside the webview (wry does not + // implement the native JS dialogs), so text input goes through our own + // modal. Resolves with the entered string, or null on cancel. + let promptModal = $state<{ + show: boolean; + title: string; + message: string; + value: string; + resolve: ((v: string | null) => void) | null; + }>({ show: false, title: '', message: '', value: '', resolve: null }); + + function promptCustom(message: string, options: { title: string; initial?: string }): Promise { + return new Promise((resolve) => { + promptModal = { + show: true, + title: options.title, + message, + value: options.initial ?? '', + resolve, + }; + }); + } + + function handlePromptConfirm() { + if (promptModal.resolve) promptModal.resolve(promptModal.value); + promptModal.show = false; + } + + function handlePromptCancel() { + if (promptModal.resolve) promptModal.resolve(null); + promptModal.show = false; + } + function handleModalSave() { if (modalState.resolve) modalState.resolve('save'); modalState.show = false; @@ -2724,7 +2757,10 @@ import { t } from './utils/i18n.js'; const tab = tabManager.tabs.find((t) => t.id === tabId); if (!tab || !tab.path) return; - const newName = window.prompt(t('menu.renameFile', settings.language), tab.title); + const newName = await promptCustom(t('menu.renameFile', settings.language), { + title: t('menu.rename', settings.language), + initial: tab.title, + }); if (newName && newName !== tab.title) { const oldPath = tab.path; const newPath = oldPath.replace(/[/\\][^/\\]+$/, (m) => m.charAt(0) + newName); @@ -3387,6 +3423,16 @@ import { t } from './utils/i18n.js'; onsave={handleModalSave} oncancel={handleModalCancel} /> + +
diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index a4c45ce..27a821f 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -9,6 +9,8 @@ message, kind = 'info', showSave = false, + showInput = false, + inputValue = $bindable(''), onconfirm, onsave, oncancel, @@ -18,18 +20,26 @@ message: string; kind?: 'info' | 'warning' | 'error'; showSave?: boolean; + showInput?: boolean; + inputValue?: string; onconfirm: () => void; onsave?: () => void; oncancel: () => void; }>(); let modalContent = $state(); + let inputElement = $state(); let previousActiveElement: HTMLElement | null = null; $effect(() => { if (show) { previousActiveElement = document.activeElement as HTMLElement; setTimeout(() => { + if (showInput && inputElement) { + inputElement.focus(); + inputElement.select(); + return; + } const focusable = modalContent?.querySelector('button.primary') as HTMLElement; if (focusable) { focusable.focus(); @@ -43,6 +53,9 @@ }); function handleKeydown(e: KeyboardEvent) { + // While the user is typing in the input, plain letters are text — + // the y/n confirm/cancel shortcuts must not fire. + const typingInInput = showInput && e.target === inputElement; if (e.key === 'Escape') { e.preventDefault(); oncancel(); @@ -56,12 +69,12 @@ } } // Y for Yes/Confirm - if (e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (!typingInInput && e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); onconfirm(); } // N for No/Cancel - if (e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (!typingInInput && e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); oncancel(); } @@ -112,6 +125,9 @@