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;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</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">
|
<link rel="stylesheet" crossorigin href="/assets/main-BW6W9uAx.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,9 @@
|
||||||
let inboxDropValid = false
|
let inboxDropValid = false
|
||||||
let captureDropActive = false
|
let captureDropActive = false
|
||||||
let captureDropLabel = ''
|
let captureDropLabel = ''
|
||||||
|
let captureDragDepth = 0
|
||||||
|
let lastCaptureDragOverAt = 0
|
||||||
|
let captureDragResetTimer = null
|
||||||
|
|
||||||
let showConfirm = false
|
let showConfirm = false
|
||||||
let confirmTitle = ''
|
let confirmTitle = ''
|
||||||
|
|
@ -384,9 +387,16 @@
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeydown)
|
window.addEventListener('keydown', handleKeydown)
|
||||||
window.addEventListener('paste', handleGlobalPaste)
|
window.addEventListener('paste', handleGlobalPaste)
|
||||||
|
window.addEventListener('dragenter', handleGlobalDragEnter)
|
||||||
window.addEventListener('dragover', handleGlobalDragOver)
|
window.addEventListener('dragover', handleGlobalDragOver)
|
||||||
window.addEventListener('dragleave', handleGlobalDragLeave)
|
window.addEventListener('dragleave', handleGlobalDragLeave)
|
||||||
|
window.addEventListener('dragend', resetCaptureDragState)
|
||||||
|
window.addEventListener('dragcancel', resetCaptureDragState)
|
||||||
window.addEventListener('drop', handleGlobalDrop)
|
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('auxclick', handleMouseNav)
|
||||||
window.addEventListener('mouseup', handleMouseNav)
|
window.addEventListener('mouseup', handleMouseNav)
|
||||||
|
|
||||||
|
|
@ -399,11 +409,19 @@
|
||||||
if (unlistenDrop) unlistenDrop()
|
if (unlistenDrop) unlistenDrop()
|
||||||
window.removeEventListener('keydown', handleKeydown)
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
window.removeEventListener('paste', handleGlobalPaste)
|
window.removeEventListener('paste', handleGlobalPaste)
|
||||||
|
window.removeEventListener('dragenter', handleGlobalDragEnter)
|
||||||
window.removeEventListener('dragover', handleGlobalDragOver)
|
window.removeEventListener('dragover', handleGlobalDragOver)
|
||||||
window.removeEventListener('dragleave', handleGlobalDragLeave)
|
window.removeEventListener('dragleave', handleGlobalDragLeave)
|
||||||
|
window.removeEventListener('dragend', resetCaptureDragState)
|
||||||
|
window.removeEventListener('dragcancel', resetCaptureDragState)
|
||||||
window.removeEventListener('drop', handleGlobalDrop)
|
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('auxclick', handleMouseNav)
|
||||||
window.removeEventListener('mouseup', handleMouseNav)
|
window.removeEventListener('mouseup', handleMouseNav)
|
||||||
|
clearCaptureDragTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== System view / Node selection =====
|
// ===== System view / Node selection =====
|
||||||
|
|
@ -774,6 +792,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e) {
|
function handleKeydown(e) {
|
||||||
|
if (e.key === 'Escape' && captureDropActive) {
|
||||||
|
e.preventDefault()
|
||||||
|
resetCaptureDragState()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isEditableTarget(e.target)) return
|
if (isEditableTarget(e.target)) return
|
||||||
|
|
||||||
if (e.key === 'Backspace' || (e.altKey && e.key === 'ArrowLeft')) {
|
if (e.key === 'Backspace' || (e.altKey && e.key === 'ArrowLeft')) {
|
||||||
|
|
@ -1734,8 +1757,12 @@
|
||||||
|
|
||||||
// ===== Drag-and-drop =====
|
// ===== Drag-and-drop =====
|
||||||
async function onFilesDropped(paths) {
|
async function onFilesDropped(paths) {
|
||||||
if (!paths || paths.length === 0) return
|
try {
|
||||||
await captureDroppedPaths(paths, 'drop')
|
if (!paths || paths.length === 0) return
|
||||||
|
await captureDroppedPaths(paths, 'drop')
|
||||||
|
} finally {
|
||||||
|
resetCaptureDragState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Helpers =====
|
// ===== Helpers =====
|
||||||
|
|
@ -2005,33 +2032,84 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hasExternalCaptureData(dataTransfer) {
|
function hasExternalCaptureData(dataTransfer) {
|
||||||
|
if (dragIds.length > 0) return false
|
||||||
const types = Array.from(dataTransfer?.types || [])
|
const types = Array.from(dataTransfer?.types || [])
|
||||||
return types.includes('Files') ||
|
return types.includes('Files') ||
|
||||||
types.includes('text/uri-list') ||
|
types.includes('text/uri-list') ||
|
||||||
types.includes('text/x-moz-url') ||
|
types.includes('text/x-moz-url') ||
|
||||||
(types.includes('text/plain') && !types.includes('application/x-verstak-node'))
|
(types.includes('text/plain') && !types.includes('application/x-verstak-node'))
|
||||||
}
|
}
|
||||||
function handleGlobalDragOver(e) {
|
|
||||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
function clearCaptureDragTimer() {
|
||||||
e.preventDefault()
|
if (captureDragResetTimer) {
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
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()
|
captureDropLabel = captureContextLabel()
|
||||||
captureDropActive = true
|
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) {
|
function handleGlobalDragLeave(e) {
|
||||||
|
if (captureDragDepth > 0) captureDragDepth -= 1
|
||||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
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) {
|
async function handleGlobalDrop(e) {
|
||||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
if (!hasExternalCaptureData(e.dataTransfer)) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
captureDropActive = false
|
|
||||||
try {
|
try {
|
||||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = String(err)
|
error = String(err)
|
||||||
|
} finally {
|
||||||
|
resetCaptureDragState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleInboxDragOver(e) {
|
function handleInboxDragOver(e) {
|
||||||
|
|
@ -2045,9 +2123,14 @@
|
||||||
async function handleInboxDrop(e) {
|
async function handleInboxDrop(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
inboxDropValid = false
|
try {
|
||||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err)
|
||||||
|
} finally {
|
||||||
|
resetCaptureDragState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function openAssignInbox(item) {
|
function openAssignInbox(item) {
|
||||||
assignInboxItem = item
|
assignInboxItem = item
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,16 @@ async function runReadyScenario(cdp, url) {
|
||||||
await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
|
await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
|
||||||
await clickText(cdp, '.overlay .btn', 'Удалить')
|
await clickText(cdp, '.overlay .btn', 'Удалить')
|
||||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox')
|
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 screenshot(cdp, 'inbox.png')
|
||||||
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
||||||
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
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 clickText(cdp, '.nav-item', 'Корзина')
|
||||||
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible')
|
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 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-child.txt', 'trash: opening deleted folder shows children immediately')
|
||||||
await assertText(cdp, 'Корзина / Trash Smoke Folder', 'trash: current folder breadcrumb visible')
|
await assertText(cdp, 'Корзина / Trash Smoke Folder', 'trash: current folder breadcrumb visible')
|
||||||
await dispatchBodyKeydown(cdp, 'Backspace')
|
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 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 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 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', 'Журнал')
|
await clickText(cdp, '.nav-item', 'Журнал')
|
||||||
|
|
@ -318,6 +328,7 @@ async function runReadyScenario(cdp, url) {
|
||||||
|
|
||||||
await clickText(cdp, '.nav-item', 'Журнал')
|
await clickText(cdp, '.nav-item', 'Журнал')
|
||||||
await waitForSelector(cdp, '.journal-screen')
|
await waitForSelector(cdp, '.journal-screen')
|
||||||
|
await clickText(cdp, '.journal-tab', 'Журнал работы')
|
||||||
await screenshot(cdp, 'journal.png')
|
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: filter section visible')
|
||||||
await assertEval(cdp, `document.body.innerText.toLowerCase().includes('экспорт отчёта')`, 'journal: export 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)
|
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) {
|
async function clickFolderOpenButton(cdp, name) {
|
||||||
const ok = await evalValue(cdp, `
|
const ok = await evalValue(cdp, `
|
||||||
(() => {
|
(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue