fix: reset capture drag state reliably
This commit is contained in:
parent
6033ccffa9
commit
cf770262e5
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-DLPp-1Ge.js"></script>
|
||||
<script type="module" crossorigin src="/assets/main-Co5H5J-5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BW6W9uAx.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -164,6 +164,9 @@
|
|||
let inboxDropValid = false
|
||||
let captureDropActive = false
|
||||
let captureDropLabel = ''
|
||||
let captureDragDepth = 0
|
||||
let lastCaptureDragOverAt = 0
|
||||
let captureDragResetTimer = null
|
||||
|
||||
let showConfirm = false
|
||||
let confirmTitle = ''
|
||||
|
|
@ -384,9 +387,16 @@
|
|||
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('paste', handleGlobalPaste)
|
||||
window.addEventListener('dragenter', handleGlobalDragEnter)
|
||||
window.addEventListener('dragover', handleGlobalDragOver)
|
||||
window.addEventListener('dragleave', handleGlobalDragLeave)
|
||||
window.addEventListener('dragend', resetCaptureDragState)
|
||||
window.addEventListener('dragcancel', resetCaptureDragState)
|
||||
window.addEventListener('drop', handleGlobalDrop)
|
||||
window.addEventListener('mouseup', resetCaptureDragState)
|
||||
window.addEventListener('mouseleave', resetCaptureDragState)
|
||||
window.addEventListener('blur', resetCaptureDragState)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.addEventListener('auxclick', handleMouseNav)
|
||||
window.addEventListener('mouseup', handleMouseNav)
|
||||
|
||||
|
|
@ -399,11 +409,19 @@
|
|||
if (unlistenDrop) unlistenDrop()
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('paste', handleGlobalPaste)
|
||||
window.removeEventListener('dragenter', handleGlobalDragEnter)
|
||||
window.removeEventListener('dragover', handleGlobalDragOver)
|
||||
window.removeEventListener('dragleave', handleGlobalDragLeave)
|
||||
window.removeEventListener('dragend', resetCaptureDragState)
|
||||
window.removeEventListener('dragcancel', resetCaptureDragState)
|
||||
window.removeEventListener('drop', handleGlobalDrop)
|
||||
window.removeEventListener('mouseup', resetCaptureDragState)
|
||||
window.removeEventListener('mouseleave', resetCaptureDragState)
|
||||
window.removeEventListener('blur', resetCaptureDragState)
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.removeEventListener('auxclick', handleMouseNav)
|
||||
window.removeEventListener('mouseup', handleMouseNav)
|
||||
clearCaptureDragTimer()
|
||||
})
|
||||
|
||||
// ===== System view / Node selection =====
|
||||
|
|
@ -774,6 +792,11 @@
|
|||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'Escape' && captureDropActive) {
|
||||
e.preventDefault()
|
||||
resetCaptureDragState()
|
||||
return
|
||||
}
|
||||
if (isEditableTarget(e.target)) return
|
||||
|
||||
if (e.key === 'Backspace' || (e.altKey && e.key === 'ArrowLeft')) {
|
||||
|
|
@ -1734,8 +1757,12 @@
|
|||
|
||||
// ===== Drag-and-drop =====
|
||||
async function onFilesDropped(paths) {
|
||||
try {
|
||||
if (!paths || paths.length === 0) return
|
||||
await captureDroppedPaths(paths, 'drop')
|
||||
} finally {
|
||||
resetCaptureDragState()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
|
@ -2005,33 +2032,84 @@
|
|||
}
|
||||
}
|
||||
function hasExternalCaptureData(dataTransfer) {
|
||||
if (dragIds.length > 0) return false
|
||||
const types = Array.from(dataTransfer?.types || [])
|
||||
return types.includes('Files') ||
|
||||
types.includes('text/uri-list') ||
|
||||
types.includes('text/x-moz-url') ||
|
||||
(types.includes('text/plain') && !types.includes('application/x-verstak-node'))
|
||||
}
|
||||
function handleGlobalDragOver(e) {
|
||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
|
||||
function clearCaptureDragTimer() {
|
||||
if (captureDragResetTimer) {
|
||||
clearTimeout(captureDragResetTimer)
|
||||
captureDragResetTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function resetCaptureDragState() {
|
||||
captureDragDepth = 0
|
||||
captureDropActive = false
|
||||
captureDropLabel = ''
|
||||
lastCaptureDragOverAt = 0
|
||||
inboxDropValid = false
|
||||
clearCaptureDragTimer()
|
||||
}
|
||||
|
||||
function scheduleCaptureDragReset() {
|
||||
if (captureDragResetTimer) return
|
||||
captureDragResetTimer = setTimeout(() => {
|
||||
captureDragResetTimer = null
|
||||
if (captureDropActive && Date.now() - lastCaptureDragOverAt > 2000) {
|
||||
resetCaptureDragState()
|
||||
} else if (captureDropActive) {
|
||||
scheduleCaptureDragReset()
|
||||
}
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
function activateCaptureDrop(dataTransfer) {
|
||||
if (!hasExternalCaptureData(dataTransfer)) return false
|
||||
captureDropLabel = captureContextLabel()
|
||||
captureDropActive = true
|
||||
lastCaptureDragOverAt = Date.now()
|
||||
scheduleCaptureDragReset()
|
||||
return true
|
||||
}
|
||||
|
||||
function handleGlobalDragEnter(e) {
|
||||
if (!activateCaptureDrop(e.dataTransfer)) return
|
||||
captureDragDepth += 1
|
||||
}
|
||||
|
||||
function handleGlobalDragOver(e) {
|
||||
if (!activateCaptureDrop(e.dataTransfer)) return
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
function handleGlobalDragLeave(e) {
|
||||
if (captureDragDepth > 0) captureDragDepth -= 1
|
||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
captureDropActive = false
|
||||
resetCaptureDragState()
|
||||
} else if (captureDragDepth <= 0) {
|
||||
resetCaptureDragState()
|
||||
}
|
||||
}
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
resetCaptureDragState()
|
||||
}
|
||||
}
|
||||
async function handleGlobalDrop(e) {
|
||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
||||
e.preventDefault()
|
||||
captureDropActive = false
|
||||
try {
|
||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (err) {
|
||||
error = String(err)
|
||||
} finally {
|
||||
resetCaptureDragState()
|
||||
}
|
||||
}
|
||||
function handleInboxDragOver(e) {
|
||||
|
|
@ -2045,9 +2123,14 @@
|
|||
async function handleInboxDrop(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
inboxDropValid = false
|
||||
try {
|
||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (err) {
|
||||
error = String(err)
|
||||
} finally {
|
||||
resetCaptureDragState()
|
||||
}
|
||||
}
|
||||
function openAssignInbox(item) {
|
||||
assignInboxItem = item
|
||||
|
|
|
|||
|
|
@ -203,6 +203,16 @@ async function runReadyScenario(cdp, url) {
|
|||
await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
|
||||
await clickText(cdp, '.overlay .btn', 'Удалить')
|
||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox')
|
||||
await dispatchExternalDrag(cdp)
|
||||
await waitForSelector(cdp, '.capture-drop-overlay')
|
||||
await dispatchBodyKeydown(cdp, 'Escape')
|
||||
await waitForGone(cdp, '.capture-drop-overlay')
|
||||
await assertEval(cdp, `document.querySelector('.nav-item')?.click && !document.querySelector('.capture-drop-overlay')`, 'drag: Escape resets capture overlay')
|
||||
await dispatchExternalDrag(cdp)
|
||||
await waitForSelector(cdp, '.capture-drop-overlay')
|
||||
await dispatchWindowBlur(cdp)
|
||||
await waitForGone(cdp, '.capture-drop-overlay')
|
||||
await assertEval(cdp, `!document.querySelector('.capture-drop-overlay')`, 'drag: blur resets capture overlay')
|
||||
await screenshot(cdp, 'inbox.png')
|
||||
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
||||
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
||||
|
|
@ -211,12 +221,12 @@ async function runReadyScenario(cdp, url) {
|
|||
await clickText(cdp, '.nav-item', 'Корзина')
|
||||
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible')
|
||||
await assertEval(cdp, `!document.body.innerText.includes('.verstak/trash') && !document.body.innerText.includes('node-trash_Trash-Smoke-Folder')`, 'trash: physical implementation entries are hidden')
|
||||
await click(cdp, '.trash-row.folder .trash-row-actions .inbox-icon-btn[title="Открыть"]')
|
||||
await click(cdp, '.trash-row.folder .trash-row-icon')
|
||||
await assertText(cdp, 'trash-child.txt', 'trash: opening deleted folder shows children immediately')
|
||||
await assertText(cdp, 'Корзина / Trash Smoke Folder', 'trash: current folder breadcrumb visible')
|
||||
await dispatchBodyKeydown(cdp, 'Backspace')
|
||||
await assertEval(cdp, `document.body.innerText.includes('Trash Smoke Folder') && !document.body.innerText.includes('trash-child.txt')`, 'trash: Backspace returns to trash root')
|
||||
await click(cdp, '.trash-row.folder .trash-row-actions .inbox-icon-btn[title="Открыть"]')
|
||||
await click(cdp, '.trash-row.folder .trash-row-icon')
|
||||
await dispatchMouseBack(cdp)
|
||||
await assertEval(cdp, `document.body.innerText.includes('Trash Smoke Folder') && !document.body.innerText.includes('trash-child.txt')`, 'trash: mouse Back returns to trash root')
|
||||
await clickText(cdp, '.nav-item', 'Журнал')
|
||||
|
|
@ -318,6 +328,7 @@ async function runReadyScenario(cdp, url) {
|
|||
|
||||
await clickText(cdp, '.nav-item', 'Журнал')
|
||||
await waitForSelector(cdp, '.journal-screen')
|
||||
await clickText(cdp, '.journal-tab', 'Журнал работы')
|
||||
await screenshot(cdp, 'journal.png')
|
||||
await assertEval(cdp, `document.body.innerText.toLowerCase().includes('фильтры')`, 'journal: filter section visible')
|
||||
await assertEval(cdp, `document.body.innerText.toLowerCase().includes('экспорт отчёта')`, 'journal: export section visible')
|
||||
|
|
@ -528,6 +539,32 @@ async function emitDroppedFiles(cdp, paths) {
|
|||
await sleep(300)
|
||||
}
|
||||
|
||||
async function dispatchExternalDrag(cdp) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `
|
||||
(() => {
|
||||
const data = new DataTransfer();
|
||||
data.items.add(new File(['smoke'], 'drag-smoke.txt', { type: 'text/plain' }));
|
||||
const enter = new DragEvent('dragenter', { bubbles: true, cancelable: true, dataTransfer: data });
|
||||
const over = new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: data });
|
||||
window.dispatchEvent(enter);
|
||||
window.dispatchEvent(over);
|
||||
})()
|
||||
`,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
})
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
async function dispatchWindowBlur(cdp) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `window.dispatchEvent(new Event('blur'))`,
|
||||
returnByValue: true,
|
||||
})
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
async function clickFolderOpenButton(cdp, name) {
|
||||
const ok = await evalValue(cdp, `
|
||||
(() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue