diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 78ebe16..7855d0d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1778,6 +1778,11 @@ onBeforeUnmount(() => { " class="phone-resolution-wrapper phone-resolution-wrapper--primary" > +
{ it('uses layout zoom so the fixed-resolution phone stays sharply rasterized', () => { expect(source).toContain('phone-resolution-canvas--primary') + expect(source).toMatch( + /phone-resolution-wrapper--primary[\s\S]*?id="phone-home-drag-portal"[\s\S]*?phone-resolution-canvas--primary/, + ) + expect(source.match(/id="phone-home-drag-portal"/g)).toHaveLength(1) expect(source).toContain("'--phone-rendered-height'") expect(source).toContain("'--phone-rendered-width'") expect(mainCss).toMatch( diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 1eb36f7..b1b735a 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -295,6 +295,18 @@ button { height: var(--phone-rendered-height, 844px); } +.phone-home-drag-portal { + position: absolute; + z-index: 70; + inset: 0; + overflow: hidden; + pointer-events: none; +} + +.phone-home-drag-portal * { + pointer-events: none !important; +} + .phone-resolution-canvas { position: absolute; top: 0; @@ -2622,8 +2634,30 @@ button { .springboard--widget-dragging .springboard-widget-page-scroll { overflow: visible; } -.springboard--home-dragging .springboard-page--apps { - overflow: visible; +.home-drag-layer { + position: absolute; + overflow: hidden; + pointer-events: none; +} +.home-drag-layer * { + pointer-events: none !important; +} +.home-drag-position { + position: absolute; + top: 0; + left: 0; + will-change: transform; +} +.home-drag-ghost { + position: absolute; + top: 0; + left: 0; + margin: 0; + opacity: 1; + transition: none !important; + animation: none !important; + transform-origin: 0 0; + will-change: transform; } .app-grid { display: grid; @@ -2658,6 +2692,9 @@ button { transition: transform var(--springboard-page-duration) var(--springboard-page-easing); } +.app-icon-item--drag-source { + opacity: 0; +} .app-icon-remove { position: absolute; z-index: 4; diff --git a/frontend/src/components/AppIcon.vue b/frontend/src/components/AppIcon.vue index 230fb3a..d3b447c 100644 --- a/frontend/src/components/AppIcon.vue +++ b/frontend/src/components/AppIcon.vue @@ -30,11 +30,13 @@ const props = withDefaults( app: PhoneAppDefinition compact?: boolean editMode?: boolean + externalDragVisual?: boolean showLabel?: boolean }>(), { compact: false, editMode: false, + externalDragVisual: false, showLabel: true, }, ) @@ -63,14 +65,14 @@ let dragStartPage = 0 let dragPageWidth = 0 let dragPageMetrics: SpringboardDragMetrics | null = null const dragStyle = computed(() => - isDragging.value + isDragging.value && !props.externalDragVisual ? { transform: `translate3d(${springboardPageDragCompensation(dragStartPage, phone.currentPage, dragPageWidth)}px, 0, 0)`, } : undefined, ) const dragPointerStyle = computed(() => - isDragging.value + isDragging.value && !props.externalDragVisual ? { transform: `translate3d(${dragOffset.value.x}px, ${dragOffset.value.y}px, 0)`, } @@ -300,6 +302,7 @@ onBeforeUnmount(() => { class="app-icon-item" :class="{ 'app-icon-item--compact': compact, + 'app-icon-item--drag-source': isDragging && externalDragVisual, 'app-icon-item--dragging': isDragging, 'app-icon-item--editing': editMode, }" diff --git a/frontend/src/components/HomeFolderIcon.vue b/frontend/src/components/HomeFolderIcon.vue index bb25b93..552262c 100644 --- a/frontend/src/components/HomeFolderIcon.vue +++ b/frontend/src/components/HomeFolderIcon.vue @@ -22,11 +22,13 @@ const props = withDefaults( apps: PhoneAppDefinition[] defaultName: string editMode?: boolean + externalDragVisual?: boolean folder: HomeFolder showLabel?: boolean }>(), { editMode: false, + externalDragVisual: false, showLabel: true, }, ) @@ -55,14 +57,14 @@ let stopPointerSession: (() => void) | null = null const folderName = computed(() => props.folder.name || props.defaultName) const dragStyle = computed(() => - isDragging.value + isDragging.value && !props.externalDragVisual ? { transform: `translate3d(${springboardPageDragCompensation(dragStartPage, phone.currentPage, dragPageWidth)}px, 0, 0)`, } : undefined, ) const dragPointerStyle = computed(() => - isDragging.value + isDragging.value && !props.externalDragVisual ? { transform: `translate3d(${dragOffset.value.x}px, ${dragOffset.value.y}px, 0)`, } @@ -232,6 +234,7 @@ onBeforeUnmount(() => {
{ - expect(viewSource).not.toContain('source.cloneNode(true)') - expect(viewSource).not.toContain('') - expect(viewSource).not.toContain(':external-drag-visual') + it('renders the home drag visual through the unzoomed phone portal', () => { + expect(viewSource).toContain('source.cloneNode(true)') expect(viewSource).toContain( - "'springboard--home-dragging': draggingHomeApp !== null", + '', ) - expect(viewSource).toContain('draggedElement?.getBoundingClientRect()') - expect(appIconSource).not.toContain('externalDragVisual') - expect(folderIconSource).not.toContain('externalDragVisual') - expect(mainCss).not.toContain('.home-drag-layer') - expect(mainCss).not.toContain('.home-drag-ghost') - expect(mainCss).not.toContain('.app-icon-item--drag-source') + expect(viewSource).toContain( + '', + ) + expect(viewSource).toContain('readPhoneViewportGeometry') + expect(viewSource).toMatch(/const sourceBounds = \w+\.rect\(source\)/) + expect(viewSource).toMatch(/const clipBounds = \w+\.rect\(\w+\)/) + expect(viewSource).not.toContain('homeDragLocalPoint') + expect(viewSource).not.toContain('springboardViewportToLocal') + expect(viewSource).toContain("position.className = 'home-drag-position'") + expect(viewSource).toMatch( + /homeDragGrip = \{\s*x: event\.clientX - sourceBounds\.left,\s*y: event\.clientY - sourceBounds\.top,/, + ) + expect(viewSource).toMatch( + /ghost\.style\.transform = `scale\(\$\{\w+\.scaleX\}, \$\{\w+\.scaleY\}\)`/, + ) + expect( + viewSource.match(/:external-drag-visual="homeDragVisualActive"/g), + ).toHaveLength(4) + expect(viewSource).toContain('updateHomeDragGhost(event)') + expect(viewSource).toContain('const dropGhost = homeDragGhost') + expect(viewSource).toContain('const dropOrigin = homeDragPreviewBounds') + expect(viewSource).not.toContain('dropGhost?.getBoundingClientRect()') + expect(viewSource).toContain('clearHomeDragGhost(dropGhost)') + expect(viewSource).not.toContain('draggedElement?.getBoundingClientRect()') + expect(viewSource).not.toContain('springboard--home-dragging') + expect(appIconSource).toContain('externalDragVisual?: boolean') + expect(folderIconSource).toContain('externalDragVisual?: boolean') + expect(appIconSource).toContain('app-icon-item--drag-source') + expect(folderIconSource).toContain('app-icon-item--drag-source') expect(mainCss).toMatch( - /\.springboard--home-dragging \.springboard-page--apps\s*\{[^}]*overflow:\s*visible;/s, + /\.phone-home-drag-portal\s*\{[^}]*pointer-events:\s*none;/s, ) + expect(mainCss).toContain('.home-drag-position') + expect(mainCss).toContain('.home-drag-ghost') + expect(mainCss).toContain('.app-icon-item--drag-source') expect(mainCss).toMatch(/\.springboard\s*\{[\s\S]*?overflow:\s*hidden;/) expect(mainCss).toMatch( /\.springboard-page--apps\s*\{[^}]*overflow:\s*hidden;/s, ) }) + it('cleans up the external drag visual on every terminal path', () => { + expect( + viewSource.match( + /@dragstart="startHomeDrag\('(grid|dock)', [^,]+, \$event\)"/g, + ), + ).toHaveLength(4) + expect(viewSource.match(/@dragcancel="stopHomeDrag"/g)).toHaveLength(4) + expect(viewSource).toContain( + 'if (expectedGhost && homeDragGhost !== expectedGhost) return', + ) + expect(viewSource).toMatch( + /if \(!dragged\) \{\s*clearHomeDragGhost\(\)\s*return/, + ) + expect(viewSource).toMatch( + /animateHomeItemDrop\(\s*draggedItem,\s*dropArea,\s*dropIndex,\s*dropOrigin,?\s*\)\.finally\(/, + ) + expect(viewSource).toMatch( + /function stopHomeDrag[\s\S]*?clearHomeDragGhost\(\)/, + ) + expect(viewSource).toMatch( + /if \(folderId\) \{[\s\S]*?clearHomeDragGhost\(\)/, + ) + expect(viewSource).toMatch( + /onBeforeUnmount\(\(\) => \{[\s\S]*?clearHomeDragGhost\(\)/, + ) + }) + it('targets the active visual page and lets grid or dock drags turn pages', () => { expect(viewSource).toContain(':data-home-page="page.page"') expect(viewSource).toContain(':data-home-target-offset="cell.targetOffset"') expect(viewSource).toContain('nearestGridDropTarget') expect(viewSource).toContain('moveHomeAppToGridPage') expect(viewSource).not.toContain("draggingHomeApp.value?.area === 'grid'") - expect(viewSource).toContain('event.clientX < pageBounds.left') - expect(viewSource).toContain('event.clientY > pageBounds.bottom') + expect(viewSource).toContain('event.clientX < springboardBounds.left') + expect(viewSource).toContain('event.clientY > springboardBounds.bottom') + expect(viewSource).toContain('function homeDragViewportRect(') + expect(viewSource).toContain('if (geometry) return geometry.rect(element)') + expect(viewSource).toContain('homeDragViewportRect(pageElement, geometry)') + expect(viewSource).toContain('homeDragViewportRect(springboard, geometry)') + expect(viewSource).toContain('homeDragViewportRect(slot, geometry)') + expect(viewSource).toContain('homeDragViewportRect(dock, geometry)') + expect(viewSource).toMatch( + /homeDragViewportRect\(\s*appElement,\s*readPhoneViewportGeometry\(appElement\),?\s*\)/, + ) + expect(viewSource).toContain( + 'springboardBounds.left + (bounds.left - pageBounds.left)', + ) expect(viewSource).toContain('nearestSpringboardRectIndex') expect(viewSource).toContain("queueEdgePageTurn(lastHomePointer, 'app')") expect(viewSource).toContain("edgePageLocked = dragType === 'widget'") diff --git a/frontend/src/views/SpringboardView.vue b/frontend/src/views/SpringboardView.vue index 683ac1a..9bb3598 100644 --- a/frontend/src/views/SpringboardView.vue +++ b/frontend/src/views/SpringboardView.vue @@ -41,6 +41,11 @@ import { type HomeItem, } from '@/utils/homeLayout' import type { ReorderDirection } from '@/utils/keyboard' +import { + readPhoneViewportGeometry, + type PhoneViewportGeometry, + type PhoneViewportRect, +} from '@/utils/phoneViewportGeometry' import { layoutSpringboardHomePages } from '@/utils/springboardLayout' import { maximumRenderedWidgetPage, @@ -106,6 +111,8 @@ const draggingHomeApp = ref<{ area: HomeArea index: number } | null>(null) +const homeDragLayer = ref(null) +const homeDragVisualActive = ref(false) const temporaryHomePage = ref(null) const openedFolderId = ref(null) const folderDraggingOutside = ref(false) @@ -125,6 +132,11 @@ let edgePageDirection = 0 let edgePageLocked = false let folderHoverTimer: number | undefined let lastHomePointer: { clientX: number; clientY: number } | null = null +let homeDragGhost: HTMLElement | null = null +let homeDragGrip: { x: number; y: number } | null = null +let homeDragGeometry: PhoneViewportGeometry | null = null +let homeDragClipBounds: PhoneViewportRect | null = null +let homeDragPreviewBounds: PhoneViewportRect | null = null const installedApps = computed(() => PHONE_APPS.filter((app) => appStore.isInstalled(app.id)), @@ -453,7 +465,7 @@ function startWidgetDrag(id: string, event: PointerEvent): void { ).find((candidate) => candidate.dataset.widgetId === id) if (!widget) return pageTransitioning.value = false - const bounds = widget.getBoundingClientRect() + const bounds = homeDragViewportRect(widget) draggingWidgetId.value = id widgetDragGrip.value = { x: event.clientX - bounds.left, @@ -474,7 +486,7 @@ function updateWidgetDragPreview(event: { ) const pageElement = grid?.closest('.springboard-page') if (!grid || !pageElement) return - const renderedGridBounds = grid.getBoundingClientRect() + const renderedGridBounds = homeDragViewportRect(grid) const gridStyle = getComputedStyle(grid) const scaleX = grid.offsetWidth > 0 ? renderedGridBounds.width / grid.offsetWidth : 1 @@ -529,7 +541,7 @@ function clearTemporaryHomePage(keepCurrentPage = false): void { function resolveHomeEdgeTurn(event: { clientX: number }) { const springboard = document.querySelector('.springboard') if (!springboard) return null - const bounds = springboard.getBoundingClientRect() + const bounds = homeDragViewportRect(springboard) const renderedLastPage = appPages.value.length return resolveSpringboardHomeEdgeTurn( event.clientX, @@ -548,7 +560,7 @@ function queueEdgePageTurn( ): void { const springboard = document.querySelector('.springboard') if (!springboard) return - const bounds = springboard.getBoundingClientRect() + const bounds = homeDragViewportRect(springboard) const turn = dragType === 'app' ? resolveHomeEdgeTurn(event) @@ -733,10 +745,166 @@ async function saveWidgetConfig( if (configured) changePage(configured.page) } -function startHomeDrag(area: HomeArea, index: number): void { +function homeDragViewportRect( + element: Element, + geometry = homeDragGeometry ?? readPhoneViewportGeometry(element), +): PhoneViewportRect { + if (geometry) return geometry.rect(element) + const bounds = element.getBoundingClientRect() + return { + bottom: bounds.bottom, + height: bounds.height, + left: bounds.left, + right: bounds.right, + top: bounds.top, + width: bounds.width, + } +} + +function viewportRectAt( + left: number, + top: number, + width: number, + height: number, +): PhoneViewportRect { + return { + bottom: top + height, + height, + left, + right: left + width, + top, + width, + } +} + +function clearHomeDragGhost(expectedGhost?: HTMLElement | null): void { + if (expectedGhost && homeDragGhost !== expectedGhost) return + homeDragGhost?.remove() + homeDragGhost = null + homeDragGrip = null + homeDragGeometry = null + homeDragClipBounds = null + homeDragPreviewBounds = null + homeDragVisualActive.value = false + const layer = homeDragLayer.value + layer?.style.removeProperty('left') + layer?.style.removeProperty('top') + layer?.style.removeProperty('width') + layer?.style.removeProperty('height') +} + +function updateHomeDragGhost(event: { + clientX: number + clientY: number +}): void { + const ghost = homeDragGhost + const grip = homeDragGrip + const clipBounds = homeDragClipBounds + const previewBounds = homeDragPreviewBounds + if (!ghost || !grip || !clipBounds || !previewBounds) return + + const left = event.clientX - grip.x + const top = event.clientY - grip.y + ghost.style.transform = `translate3d(${left - clipBounds.left}px, ${top - clipBounds.top}px, 0)` + homeDragPreviewBounds = viewportRectAt( + left, + top, + previewBounds.width, + previewBounds.height, + ) +} + +function createHomeDragGhost(event: PointerEvent): void { + clearHomeDragGhost() + const eventTarget = + event.currentTarget instanceof Element + ? event.currentTarget + : event.target instanceof Element + ? event.target + : null + const source = eventTarget?.closest('.app-icon-item') + const layer = homeDragLayer.value + const springboard = source?.closest('.springboard') + const portal = layer?.closest('.phone-home-drag-portal') + const geometry = readPhoneViewportGeometry(source ?? null) + if (!source || !layer || !springboard || !portal || !geometry) return + + const portalBounds = portal.getBoundingClientRect() + const clipBounds = geometry.rect(springboard) + const sourceBounds = geometry.rect(source) + if ( + clipBounds.width <= 0 || + clipBounds.height <= 0 || + sourceBounds.width <= 0 || + sourceBounds.height <= 0 + ) { + return + } + + layer.style.left = `${clipBounds.left - portalBounds.left}px` + layer.style.top = `${clipBounds.top - portalBounds.top}px` + layer.style.width = `${clipBounds.width}px` + layer.style.height = `${clipBounds.height}px` + + const ghost = source.cloneNode(true) as HTMLElement + ghost.classList.remove( + 'app-icon-item--dragging', + 'app-icon-item--editing', + 'app-icon-item--drag-source', + 'app-icon-item--drop-settling', + 'home-folder-item--dragging', + 'home-item--folder-hover', + ) + ghost.classList.add('home-drag-ghost') + ghost.removeAttribute('style') + ghost + .querySelector('.app-icon-button') + ?.style.removeProperty('transform') + ghost.setAttribute('aria-hidden', 'true') + ghost.querySelector('.app-icon-remove')?.remove() + for (const element of [ghost, ...Array.from(ghost.querySelectorAll('*'))]) { + element.removeAttribute('id') + for (const attribute of element.getAttributeNames()) { + if (attribute.startsWith('data-home-')) element.removeAttribute(attribute) + } + } + for (const control of ghost.querySelectorAll( + 'a, button, input, select, textarea, [tabindex]', + )) { + control.tabIndex = -1 + } + + const position = document.createElement('div') + position.className = 'home-drag-position' + position.style.width = `${sourceBounds.width}px` + position.style.height = `${sourceBounds.height}px` + ghost.style.width = `${source.offsetWidth}px` + ghost.style.height = `${source.offsetHeight}px` + ghost.style.transform = `scale(${geometry.scaleX}, ${geometry.scaleY})` + position.appendChild(ghost) + + homeDragGhost = position + homeDragGrip = { + x: event.clientX - sourceBounds.left, + y: event.clientY - sourceBounds.top, + } + homeDragGeometry = geometry + homeDragClipBounds = clipBounds + homeDragPreviewBounds = sourceBounds + layer.appendChild(position) + homeDragVisualActive.value = true + updateHomeDragGhost(event) +} + +function startHomeDrag( + area: HomeArea, + index: number, + event: PointerEvent, +): void { clearTemporaryHomePage() lastHomePointer = null draggingHomeApp.value = { area, index } + createHomeDragGhost(event) } function clearFolderHover(): void { @@ -754,10 +922,40 @@ function queueFolderHover(event: PointerEvent): void { return } - const targetElement = document - .elementsFromPoint(event.clientX, event.clientY) - .map((element) => element.closest('[data-home-index]')) - .find((element) => element && !element.closest('.app-icon-item--dragging')) + const targetElement = Array.from( + document.querySelectorAll('[data-home-area][data-home-index]'), + ) + .filter((element) => { + const area = element.dataset.homeArea as HomeArea | undefined + const index = Number(element.dataset.homeIndex) + if ( + (area !== 'grid' && area !== 'dock') || + !Number.isInteger(index) || + (area === dragged.area && index === dragged.index) + ) { + return false + } + if ( + area === 'grid' && + Number( + element.closest('[data-home-page]')?.dataset.homePage, + ) !== phone.currentPage + ) { + return false + } + const bounds = homeDragViewportRect(element) + return ( + event.clientX >= bounds.left && + event.clientX <= bounds.right && + event.clientY >= bounds.top && + event.clientY <= bounds.bottom + ) + }) + .sort((left, right) => { + const leftIsDock = left.dataset.homeArea === 'dock' + const rightIsDock = right.dataset.homeArea === 'dock' + return Number(rightIsDock) - Number(leftIsDock) + })[0] const area = targetElement?.dataset.homeArea as HomeArea | undefined const targetIndex = Number(targetElement?.dataset.homeIndex) if ( @@ -822,6 +1020,7 @@ function queueFolderHover(event: PointerEvent): void { if (folderId) { draggingHomeApp.value = null lastHomePointer = null + clearHomeDragGhost() clearTemporaryHomePage() openedFolderId.value = folderId } @@ -832,6 +1031,7 @@ function queueFolderHover(event: PointerEvent): void { function moveHomeDrag(event: PointerEvent): void { if (!draggingHomeApp.value) return lastHomePointer = { clientX: event.clientX, clientY: event.clientY } + updateHomeDragGhost(event) if (resolveHomeEdgeTurn(event)) { clearFolderHover() queueEdgePageTurn(event, 'app') @@ -845,7 +1045,7 @@ async function animateHomeItemDrop( item: HomeItem, area: HomeArea, index: number, - from: DOMRect, + from: PhoneViewportRect, ): Promise { await nextTick() if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return @@ -861,7 +1061,10 @@ async function animateHomeItemDrop( ) if (!appElement) return - const target = appElement.getBoundingClientRect() + const target = homeDragViewportRect( + appElement, + readPhoneViewportGeometry(appElement), + ) const { x: offsetX, y: offsetY } = springboardViewportDeltaToLocal( from.left - target.left, from.top - target.top, @@ -924,13 +1127,16 @@ function nearestGridDropTarget(event: { `.springboard-page--apps[data-home-page="${page}"] .app-grid`, ) const pageElement = grid?.closest('.springboard-page') - if (!grid || !pageElement) return null - const pageBounds = pageElement.getBoundingClientRect() + const springboard = grid?.closest('.springboard') + if (!grid || !pageElement || !springboard) return null + const geometry = homeDragGeometry ?? readPhoneViewportGeometry(springboard) + const pageBounds = homeDragViewportRect(pageElement, geometry) + const springboardBounds = homeDragViewportRect(springboard, geometry) if ( - event.clientX < pageBounds.left || - event.clientX > pageBounds.right || - event.clientY < pageBounds.top || - event.clientY > pageBounds.bottom + event.clientX < springboardBounds.left || + event.clientX > springboardBounds.right || + event.clientY < springboardBounds.top || + event.clientY > springboardBounds.bottom ) { return null } @@ -940,7 +1146,15 @@ function nearestGridDropTarget(event: { const closestIndex = nearestSpringboardRectIndex( event.clientX, event.clientY, - slots.map((slot) => slot.getBoundingClientRect()), + slots.map((slot) => { + const bounds = homeDragViewportRect(slot, geometry) + return { + height: bounds.height, + left: springboardBounds.left + (bounds.left - pageBounds.left), + top: springboardBounds.top + (bounds.top - pageBounds.top), + width: bounds.width, + } + }), ) const closest = closestIndex === null ? null : slots[closestIndex] const targetOffset = Number(closest?.dataset.homeTargetOffset) @@ -953,7 +1167,8 @@ function nearestDockDropTarget(event: { }): HTMLElement | null { const dock = document.querySelector('.app-dock') if (!dock) return null - const dockBounds = dock.getBoundingClientRect() + const geometry = homeDragGeometry ?? readPhoneViewportGeometry(dock) + const dockBounds = homeDragViewportRect(dock, geometry) if ( event.clientX < dockBounds.left || event.clientX > dockBounds.right || @@ -970,7 +1185,7 @@ function nearestDockDropTarget(event: { const closestIndex = nearestSpringboardRectIndex( event.clientX, event.clientY, - slots.map((slot) => slot.getBoundingClientRect()), + slots.map((slot) => homeDragViewportRect(slot, geometry)), ) return closestIndex === null ? null : slots[closestIndex] } @@ -979,12 +1194,14 @@ function finishHomeDrag(event: PointerEvent): void { clearEdgePageTurn() clearFolderHover() const dragged = draggingHomeApp.value - if (!dragged) return + if (!dragged) { + clearHomeDragGhost() + return + } + updateHomeDragGhost(event) const draggedItem = appStore.homeLayout[dragged.area][dragged.index] - const draggedElement = document.querySelector( - `[data-home-area="${dragged.area}"][data-home-index="${dragged.index}"]`, - ) - const dropOrigin = draggedElement?.getBoundingClientRect() + const dropGhost = homeDragGhost + const dropOrigin = homeDragPreviewBounds ? { ...homeDragPreviewBounds } : null const dockTarget = nearestDockDropTarget(event) const gridTarget = dockTarget ? null : nearestGridDropTarget(event) let dropArea = dragged.area @@ -1022,7 +1239,14 @@ function finishHomeDrag(event: PointerEvent): void { lastHomePointer = null clearTemporaryHomePage(moved && gridTarget !== null) if (moved && draggedItem && dropOrigin) { - void animateHomeItemDrop(draggedItem, dropArea, dropIndex, dropOrigin) + void animateHomeItemDrop( + draggedItem, + dropArea, + dropIndex, + dropOrigin, + ).finally(() => clearHomeDragGhost(dropGhost)) + } else { + clearHomeDragGhost() } } @@ -1031,6 +1255,7 @@ function stopHomeDrag(): void { clearFolderHover() draggingHomeApp.value = null lastHomePointer = null + clearHomeDragGhost() clearTemporaryHomePage() } @@ -1043,7 +1268,9 @@ function reorderHomeApp( const appElement = document.querySelector( `[data-home-area="${area}"][data-home-index="${sourceIndex}"]`, ) - const moveOrigin = appElement?.getBoundingClientRect() + const moveOrigin = appElement + ? homeDragViewportRect(appElement, readPhoneViewportGeometry(appElement)) + : null const targetIndex = homeKeyboardTarget( appStore.homeLayout, area, @@ -1131,7 +1358,10 @@ function extractOpenedFolderApp( `.home-folder-panel [data-folder-app-index="${sourceIndex}"]`, ) ?.closest('.app-icon-item') - const sourceBounds = sourceElement?.getBoundingClientRect() + const geometry = readPhoneViewportGeometry(sourceElement ?? null) + const sourceBounds = sourceElement + ? homeDragViewportRect(sourceElement, geometry) + : null const candidates = Array.from( document.querySelectorAll('[data-home-index][data-home-area]'), ).filter((element) => { @@ -1145,8 +1375,8 @@ function extractOpenedFolderApp( }) const target = candidates.reduce((closest, candidate) => { if (!closest) return candidate - const bounds = candidate.getBoundingClientRect() - const closestBounds = closest.getBoundingClientRect() + const bounds = homeDragViewportRect(candidate, geometry) + const closestBounds = homeDragViewportRect(closest, geometry) const distance = Math.hypot( event.clientX - (bounds.left + bounds.width / 2), event.clientY - (bounds.top + bounds.height / 2), @@ -1212,6 +1442,7 @@ onBeforeUnmount(() => { pendingWidgetPointer = null lastWidgetPointer = null lastHomePointer = null + clearHomeDragGhost() temporaryHomePage.value = null draggingHomeApp.value = null draggingWidgetId.value = null @@ -1227,7 +1458,6 @@ onBeforeUnmount(() => { 'springboard--dragging': dragging, 'springboard--editing': editMode, 'springboard--folder-open': folderOverlayVisible, - 'springboard--home-dragging': draggingHomeApp !== null, 'springboard--widget-dragging': draggingWidgetId !== null, }, ]" @@ -1303,6 +1533,7 @@ onBeforeUnmount(() => { :data-home-index="cell.sourceIndex" :data-home-target-offset="cell.targetOffset" :edit-mode="editMode" + :external-drag-visual="homeDragVisualActive" :class="{ 'home-item--folder-hover': folderHoverTarget === `grid:${cell.sourceIndex}`, @@ -1310,7 +1541,7 @@ onBeforeUnmount(() => { @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" @dragmove="moveHomeDrag" - @dragstart="startHomeDrag('grid', cell.sourceIndex)" + @dragstart="startHomeDrag('grid', cell.sourceIndex, $event)" @edit="enterEditMode" @remove="removeHomeApp(cell.app.id)" @reorder="reorderHomeApp('grid', cell.sourceIndex, $event)" @@ -1325,6 +1556,7 @@ onBeforeUnmount(() => { :data-home-index="cell.sourceIndex" :data-home-target-offset="cell.targetOffset" :edit-mode="editMode" + :external-drag-visual="homeDragVisualActive" :folder="cell.folder" :class="{ 'home-item--folder-hover': @@ -1333,7 +1565,7 @@ onBeforeUnmount(() => { @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" @dragmove="moveHomeDrag" - @dragstart="startHomeDrag('grid', cell.sourceIndex)" + @dragstart="startHomeDrag('grid', cell.sourceIndex, $event)" @edit="enterEditMode" @open="openFolder(cell.folder.id)" @reorder="reorderHomeApp('grid', cell.sourceIndex, $event)" @@ -1484,6 +1716,10 @@ onBeforeUnmount(() => {
+ + + + { :data-home-item-key="`app-${slot.app.id}`" :data-home-index="slot.index" :edit-mode="editMode" + :external-drag-visual="homeDragVisualActive" :show-label="false" :class="{ 'home-item--folder-hover': @@ -1538,7 +1775,7 @@ onBeforeUnmount(() => { @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" @dragmove="moveHomeDrag" - @dragstart="startHomeDrag('dock', slot.index)" + @dragstart="startHomeDrag('dock', slot.index, $event)" @edit="enterEditMode" @remove="removeHomeApp(slot.app.id)" @reorder="reorderHomeApp('dock', slot.index, $event)" @@ -1552,6 +1789,7 @@ onBeforeUnmount(() => { :data-home-item-key="slot.folder.id" :data-home-index="slot.index" :edit-mode="editMode" + :external-drag-visual="homeDragVisualActive" :folder="slot.folder" :show-label="false" :class="{ @@ -1561,7 +1799,7 @@ onBeforeUnmount(() => { @dragcancel="stopHomeDrag" @dragend="finishHomeDrag" @dragmove="moveHomeDrag" - @dragstart="startHomeDrag('dock', slot.index)" + @dragstart="startHomeDrag('dock', slot.index, $event)" @edit="enterEditMode" @open="openFolder(slot.folder.id)" @reorder="reorderHomeApp('dock', slot.index, $event)"