fix: replace popup Monaco editor with fullscreen overlay
Monaco can't mount in a popup window — it references document.activeElement from the main window context, causing cross-window DOM errors. Replaced with a fullscreen overlay teleported to <body>: - Same dark theme toolbar with save/close/dirty indicator - Ctrl+S to save, Esc to close - Status bar shows language and keyboard shortcuts - File tree stays visible underneath (overlay dismisses to it) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e1be07f34c
commit
37781a4791
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||||
import { useSftp } from '~/composables/useSftp'
|
import { useSftp } from '~/composables/useSftp'
|
||||||
import { useSessionStore } from '~/stores/session.store'
|
import { useSessionStore } from '~/stores/session.store'
|
||||||
|
|
||||||
@ -38,10 +38,16 @@ const fileInput = ref<HTMLInputElement | null>(null)
|
|||||||
// Drag-and-drop state
|
// Drag-and-drop state
|
||||||
const dragOver = ref(false)
|
const dragOver = ref(false)
|
||||||
|
|
||||||
// Popup editor tracking
|
// Fullscreen editor overlay state
|
||||||
let editorPopup: Window | null = null
|
const editorOverlay = ref(false)
|
||||||
let editorMonaco: any = null
|
const editorFilePath = ref('')
|
||||||
|
const editorContainer = ref<HTMLElement | null>(null)
|
||||||
let editorInstance: any = null
|
let editorInstance: any = null
|
||||||
|
let editorMonaco: any = null
|
||||||
|
let editorOriginalContent = ''
|
||||||
|
const editorDirty = ref(false)
|
||||||
|
const editorSavedMsg = ref(false)
|
||||||
|
const editorLang = ref('plaintext')
|
||||||
|
|
||||||
function triggerUpload() {
|
function triggerUpload() {
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
@ -97,7 +103,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (editorPopup && !editorPopup.closed) editorPopup.close()
|
editorInstance?.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
function navigateTo(path: string) {
|
function navigateTo(path: string) {
|
||||||
@ -114,100 +120,46 @@ function handleOpenFile(path: string) {
|
|||||||
readFile(path)
|
readFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for file content and open popup editor
|
const langMap: Record<string, string> = {
|
||||||
watch(fileContent, async (fc) => {
|
|
||||||
if (!fc) return
|
|
||||||
|
|
||||||
const filePath = fc.path
|
|
||||||
const content = fc.content
|
|
||||||
// Clear fileContent so the file tree stays visible
|
|
||||||
fileContent.value = null
|
|
||||||
|
|
||||||
await openEditorPopup(filePath, content)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function openEditorPopup(filePath: string, content: string) {
|
|
||||||
// Close existing popup if any
|
|
||||||
if (editorPopup && !editorPopup.closed) {
|
|
||||||
editorPopup.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = filePath.split('/').pop() || 'file'
|
|
||||||
const ext = fileName.split('.').pop() || ''
|
|
||||||
const langMap: Record<string, string> = {
|
|
||||||
ts: 'typescript', js: 'javascript', json: 'json', py: 'python',
|
ts: 'typescript', js: 'javascript', json: 'json', py: 'python',
|
||||||
sh: 'shell', bash: 'shell', yml: 'yaml', yaml: 'yaml',
|
sh: 'shell', bash: 'shell', yml: 'yaml', yaml: 'yaml',
|
||||||
md: 'markdown', html: 'html', css: 'css', xml: 'xml',
|
md: 'markdown', html: 'html', css: 'css', xml: 'xml',
|
||||||
go: 'go', rs: 'rust', rb: 'ruby', php: 'php',
|
go: 'go', rs: 'rust', rb: 'ruby', php: 'php',
|
||||||
conf: 'ini', cfg: 'ini', ini: 'ini', toml: 'ini',
|
conf: 'ini', cfg: 'ini', ini: 'ini', toml: 'ini',
|
||||||
sql: 'sql', dockerfile: 'dockerfile',
|
sql: 'sql', dockerfile: 'dockerfile',
|
||||||
}
|
}
|
||||||
const lang = langMap[ext] || 'plaintext'
|
|
||||||
|
|
||||||
const w = 900, h = 700
|
// Watch for file content and open fullscreen editor
|
||||||
const left = window.screenX + (window.innerWidth - w) / 2
|
watch(fileContent, async (fc) => {
|
||||||
const top = window.screenY + (window.innerHeight - h) / 2
|
if (!fc) return
|
||||||
|
|
||||||
editorPopup = window.open('', '_blank',
|
editorFilePath.value = fc.path
|
||||||
`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,status=no,scrollbars=no`)
|
editorOriginalContent = fc.content
|
||||||
|
editorDirty.value = false
|
||||||
|
editorSavedMsg.value = false
|
||||||
|
|
||||||
if (!editorPopup) {
|
const ext = fc.path.split('.').pop() || ''
|
||||||
alert('Popup blocked — please allow popups for this site.')
|
editorLang.value = langMap[ext] || 'plaintext'
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editorPopup.document.write(`<!DOCTYPE html>
|
// Clear fileContent so the file tree stays visible underneath
|
||||||
<html>
|
fileContent.value = null
|
||||||
<head>
|
|
||||||
<title>${fileName} — Wraith Editor</title>
|
// Show overlay and mount Monaco
|
||||||
<style>
|
editorOverlay.value = true
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
await nextTick()
|
||||||
body { background: #0a0a0f; overflow: hidden; display: flex; flex-direction: column; height: 100vh; font-family: -apple-system, sans-serif; }
|
|
||||||
#toolbar { height: 36px; background: #1a1a2e; border-bottom: 1px solid #2a2a3e; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; flex-shrink: 0; }
|
if (!editorContainer.value) return
|
||||||
#toolbar .file-path { color: #6b7280; font-size: 12px; font-family: 'JetBrains Mono', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%; }
|
|
||||||
#toolbar .actions { display: flex; gap: 8px; align-items: center; }
|
|
||||||
#toolbar .dirty { color: #f59e0b; font-size: 12px; display: none; }
|
|
||||||
#toolbar button { padding: 4px 12px; border-radius: 4px; border: none; font-size: 12px; cursor: pointer; }
|
|
||||||
.save-btn { background: #e94560; color: white; }
|
|
||||||
.save-btn:hover { background: #c23152; }
|
|
||||||
.save-btn:disabled { opacity: 0.4; cursor: default; }
|
|
||||||
.close-btn { background: #374151; color: #d1d5db; }
|
|
||||||
.close-btn:hover { background: #4b5563; }
|
|
||||||
#editor { flex: 1; }
|
|
||||||
#status { height: 24px; background: #1a1a2e; border-top: 1px solid #2a2a3e; display: flex; align-items: center; padding: 0 12px; flex-shrink: 0; }
|
|
||||||
#status span { font-size: 11px; color: #6b7280; }
|
|
||||||
#status .saved-msg { color: #34d399; display: none; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="toolbar">
|
|
||||||
<span class="file-path" title="${filePath}">${filePath}</span>
|
|
||||||
<div class="actions">
|
|
||||||
<span id="dirty-indicator" class="dirty">unsaved</span>
|
|
||||||
<button id="save-btn" class="save-btn" disabled>Save</button>
|
|
||||||
<button id="close-btn" class="close-btn">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="editor"></div>
|
|
||||||
<div id="status">
|
|
||||||
<span id="lang-label">${lang}</span>
|
|
||||||
<span id="saved-msg" class="saved-msg" style="margin-left: 12px;">Saved!</span>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
editorPopup.document.close()
|
|
||||||
|
|
||||||
// Load Monaco into the popup
|
|
||||||
if (!editorMonaco) {
|
if (!editorMonaco) {
|
||||||
editorMonaco = await import('monaco-editor')
|
editorMonaco = await import('monaco-editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = editorPopup.document.getElementById('editor')
|
// Dispose previous instance if any
|
||||||
if (!container) return
|
editorInstance?.dispose()
|
||||||
|
|
||||||
editorInstance = editorMonaco.editor.create(container, {
|
editorInstance = editorMonaco.editor.create(editorContainer.value, {
|
||||||
value: content,
|
value: editorOriginalContent,
|
||||||
language: lang,
|
language: editorLang.value,
|
||||||
theme: 'vs-dark',
|
theme: 'vs-dark',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
||||||
@ -216,54 +168,42 @@ async function openEditorPopup(filePath: string, content: string) {
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
let originalContent = content
|
|
||||||
|
|
||||||
editorInstance.onDidChangeModelContent(() => {
|
editorInstance.onDidChangeModelContent(() => {
|
||||||
const dirty = editorInstance.getValue() !== originalContent
|
editorDirty.value = editorInstance.getValue() !== editorOriginalContent
|
||||||
const dirtyEl = editorPopup?.document.getElementById('dirty-indicator')
|
|
||||||
const saveBtn = editorPopup?.document.getElementById('save-btn') as HTMLButtonElement | null
|
|
||||||
if (dirtyEl) dirtyEl.style.display = dirty ? '' : 'none'
|
|
||||||
if (saveBtn) saveBtn.disabled = !dirty
|
|
||||||
if (editorPopup && !editorPopup.closed) {
|
|
||||||
editorPopup.document.title = `${fileName}${dirty ? ' *' : ''} — Wraith Editor`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const doSave = () => {
|
// Ctrl+S to save
|
||||||
const val = editorInstance.getValue()
|
editorInstance.addCommand(
|
||||||
writeFile(filePath, val)
|
editorMonaco.KeyMod.CtrlCmd | editorMonaco.KeyCode.KeyS,
|
||||||
originalContent = val
|
() => editorSave()
|
||||||
// Update UI
|
)
|
||||||
const dirtyEl = editorPopup?.document.getElementById('dirty-indicator')
|
|
||||||
const saveBtn = editorPopup?.document.getElementById('save-btn') as HTMLButtonElement | null
|
|
||||||
const savedMsg = editorPopup?.document.getElementById('saved-msg')
|
|
||||||
if (dirtyEl) dirtyEl.style.display = 'none'
|
|
||||||
if (saveBtn) saveBtn.disabled = true
|
|
||||||
if (savedMsg) {
|
|
||||||
savedMsg.style.display = ''
|
|
||||||
setTimeout(() => { if (savedMsg) savedMsg.style.display = 'none' }, 2000)
|
|
||||||
}
|
|
||||||
if (editorPopup && !editorPopup.closed) {
|
|
||||||
editorPopup.document.title = `${fileName} — Wraith Editor`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+S in popup
|
// Esc to close
|
||||||
editorInstance.addCommand(editorMonaco.KeyMod.CtrlCmd | editorMonaco.KeyCode.KeyS, doSave)
|
editorInstance.addCommand(
|
||||||
|
editorMonaco.KeyCode.Escape,
|
||||||
// Button handlers
|
() => editorClose()
|
||||||
editorPopup.document.getElementById('save-btn')?.addEventListener('click', doSave)
|
)
|
||||||
editorPopup.document.getElementById('close-btn')?.addEventListener('click', () => {
|
|
||||||
const dirty = editorInstance.getValue() !== originalContent
|
|
||||||
if (dirty && !editorPopup!.confirm('You have unsaved changes. Close anyway?')) return
|
|
||||||
editorPopup!.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
editorInstance.focus()
|
editorInstance.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
function editorSave() {
|
||||||
|
if (!editorInstance) return
|
||||||
|
const val = editorInstance.getValue()
|
||||||
|
writeFile(editorFilePath.value, val)
|
||||||
|
editorOriginalContent = val
|
||||||
|
editorDirty.value = false
|
||||||
|
editorSavedMsg.value = true
|
||||||
|
setTimeout(() => { editorSavedMsg.value = false }, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave(path: string, content: string) {
|
function editorClose() {
|
||||||
writeFile(path, content)
|
if (editorDirty.value) {
|
||||||
|
if (!confirm('You have unsaved changes. Close anyway?')) return
|
||||||
|
}
|
||||||
|
editorOverlay.value = false
|
||||||
|
editorInstance?.dispose()
|
||||||
|
editorInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(path: string) {
|
function handleDelete(path: string) {
|
||||||
@ -424,5 +364,41 @@ const breadcrumbs = computed(() => {
|
|||||||
class="w-1 bg-transparent hover:bg-wraith-500 cursor-col-resize transition-colors shrink-0"
|
class="w-1 bg-transparent hover:bg-wraith-500 cursor-col-resize transition-colors shrink-0"
|
||||||
@mousedown="startResize"
|
@mousedown="startResize"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Fullscreen Monaco editor overlay -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="editorOverlay"
|
||||||
|
class="fixed inset-0 z-[99999] flex flex-col bg-[#0a0a0f]"
|
||||||
|
>
|
||||||
|
<!-- Editor toolbar -->
|
||||||
|
<div class="h-9 bg-[#1a1a2e] border-b border-[#2a2a3e] flex items-center justify-between px-3 shrink-0">
|
||||||
|
<span class="text-xs text-gray-500 font-mono truncate max-w-[60%]" :title="editorFilePath">{{ editorFilePath }}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="editorDirty" class="text-xs text-amber-400">unsaved</span>
|
||||||
|
<span v-if="editorSavedMsg" class="text-xs text-green-400">Saved!</span>
|
||||||
|
<button
|
||||||
|
@click="editorSave"
|
||||||
|
:disabled="!editorDirty"
|
||||||
|
class="px-3 py-1 rounded text-xs"
|
||||||
|
:class="editorDirty ? 'bg-[#e94560] hover:bg-[#c23152] text-white cursor-pointer' : 'bg-[#e94560]/40 text-white/40 cursor-default'"
|
||||||
|
>Save</button>
|
||||||
|
<button
|
||||||
|
@click="editorClose"
|
||||||
|
class="px-3 py-1 rounded text-xs bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||||
|
>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monaco mount point -->
|
||||||
|
<div ref="editorContainer" class="flex-1" />
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<div class="h-6 bg-[#1a1a2e] border-t border-[#2a2a3e] flex items-center px-3 shrink-0">
|
||||||
|
<span class="text-[11px] text-gray-500">{{ editorLang }}</span>
|
||||||
|
<span class="text-[11px] text-gray-600 ml-3">Ctrl+S save | Esc close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user