diff --git a/frontend/src/lib/assets/app.css b/frontend/src/lib/assets/app.css index 99bc17ae8e..6a386c559d 100644 --- a/frontend/src/lib/assets/app.css +++ b/frontend/src/lib/assets/app.css @@ -145,3 +145,23 @@ svelte-virtual-list-contents > * + * { rgba(255, 69, 58, 0.2) 20px ); } + +.bg-draggedover { + background-image: repeating-linear-gradient( + -45deg, + rgba(0, 0, 128, 0.2), + rgba(0, 0, 192, 0.2) 10px, + rgba(0, 0, 128, 0.2) 10px, + rgba(0, 0, 192, 0.2) 20px + ); +} + +.bg-draggedover-dark { + background-image: repeating-linear-gradient( + -45deg, + rgba(0, 0, 128, 0.6), + rgba(0, 0, 192, 0.6) 10px, + rgba(0, 0, 128, 0.6) 10px, + rgba(0, 0, 192, 0.6) 20px + ); +} diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index fe3faab844..9eb1afc069 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -19,11 +19,11 @@ import AppEditorHeader from './AppEditorHeader.svelte' import GridEditor from './GridEditor.svelte' - import { Button, Tab } from '$lib/components/common' + import { Alert, Button, Tab } from '$lib/components/common' import TabContent from '$lib/components/common/tabs/TabContent.svelte' import Tabs from '$lib/components/common/tabs/Tabs.svelte' import { userStore, workspaceStore } from '$lib/stores' - import { classNames, encodeState, sendUserToast } from '$lib/utils' + import { classNames, encodeState, getModifierKey, sendUserToast } from '$lib/utils' import AppPreview from './AppPreview.svelte' import ComponentList from './componentsPanel/ComponentList.svelte' import ContextPanel from './contextPanel/ContextPanel.svelte' @@ -693,7 +693,15 @@ {#if $componentActive}
+ > +
+ +
+
{/if} diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index 926c5fe26f..c7e93d55dc 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -1,13 +1,21 @@
@@ -134,10 +190,37 @@ push(history, $app) $app.grid = e.detail }} + root let:dataItem let:hidden let:overlapped + let:moveMode + let:componentDraggedId cols={columnConfiguration} + on:dropped={(e) => { + const { id, overlapped, x, y } = e.detail + + const overlappedComponent = findGridItem($app, overlapped) + + if (overlappedComponent && !isContainer(overlappedComponent.data.type)) { + return + } + + if (!overlapped) { + return + } + + if (id === overlapped) { + return + } + + moveComponentBetweenSubgrids( + id, + overlapped, + subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore), + { x, y } + ) + }} > { handleFillHeight(dataItem.id) }} - overlapped={overlapped !== undefined} + {overlapped} + {moveMode} + {componentDraggedId} /> diff --git a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte index 30b5d76b86..5e16b51a3f 100644 --- a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte @@ -3,10 +3,19 @@ import { classNames } from '$lib/utils' import { createEventDispatcher, getContext, onDestroy } from 'svelte' import { twMerge } from 'tailwind-merge' - import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils' + import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils' import Grid from '../svelte-grid/Grid.svelte' import type { AppEditorContext, AppViewerContext, GridItem } from '../types' - import { expandGriditem, findGridItem, maxHeight, selectId } from './appUtils' + import { + expandGriditem, + findGridItem, + findGridItemParentGrid, + insertNewGridItem, + isContainer, + maxHeight, + selectId, + subGridIndexKey + } from './appUtils' import Component from './component/Component.svelte' import ComponentWrapper from './component/ComponentWrapper.svelte' import GridViewer from './GridViewer.svelte' @@ -15,6 +24,7 @@ export let containerHeight: number | undefined = undefined export let containerWidth: number | undefined = undefined let classes = '' + export { classes as class } export let style = '' export let noPadding = false @@ -34,7 +44,8 @@ mode, parentWidth, breakpoint, - allIdsInPath + allIdsInPath, + worldStore } = getContext('AppViewerContext') const editorContext = getContext('AppEditorContext') @@ -68,6 +79,98 @@ let container: HTMLElement | undefined = undefined $: maxRow = maxHeight($app.subgrids?.[subGridId] ?? [], containerHeight ?? 0, $breakpoint) + + export function moveComponentBetweenSubgrids( + componentId: string, + parentComponentId: string, + subGridIndex: number, + position?: { x: number; y: number } + ) { + // Find the component in the source subgrid + const component = findGridItem($app, componentId) + + if (!component) { + return + } + + let parentGrid = findGridItemParentGrid($app, component.id) + + if (parentGrid) { + $app.subgrids && + ($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter( + (item) => item.id !== component?.id + )) + } else { + $app.grid = $app.grid.filter((item) => item.id !== component?.id) + } + + const gridItem = component + + insertNewGridItem( + $app, + (id) => ({ ...gridItem.data, id }), + { parentComponentId: parentComponentId, subGridIndex: subGridIndex }, + Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])), + component.id, + position, + undefined, + undefined, + undefined, + true + ) + + // Update the app state + $app = { ...$app } + + if (parentGrid) { + $focusedGrid = { + parentComponentId, + subGridIndex + } + + $selectedComponent = [parentComponentId] + } else { + $focusedGrid = undefined + } + } + + export function moveToRoot(componentId: string, position?: { x: number; y: number }) { + // Find the component in the source subgrid + const component = findGridItem($app, componentId) + + if (!component) { + return + } + + let parentGrid = findGridItemParentGrid($app, component.id) + + if (parentGrid) { + $app.subgrids && + ($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter( + (item) => item.id !== component?.id + )) + } else { + $app.grid = $app.grid.filter((item) => item.id !== component?.id) + } + + const gridItem = component + + insertNewGridItem( + $app, + (id) => ({ ...gridItem.data, id }), + undefined, + Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])), + component.id, + position, + undefined, + undefined, + undefined, + true + ) + + // Update the app state + $app = { ...$app } + }
{ + const { id, overlapped, x, y } = e.detail + + if (!overlapped) { + moveToRoot(id, { x, y }) + } else { + const overlappedComponent = findGridItem($app, overlapped) + + if (overlappedComponent && !isContainer(overlappedComponent.data.type)) { + return + } + if (!overlapped) { + return + } + + if (id === overlapped) { + return + } + + moveComponentBetweenSubgrids( + id, + overlapped, + subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore), + { x, y } + ) + } + }} > diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index 602a5bad0a..998d3d8a1c 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -20,7 +20,6 @@ import { import { gridColumns } from '../gridUtils' import { allItems } from '../utils' import type { Output, World } from '../rx' -import gridHelp from '../svelte-grid/utils/helper' import type { FilledItem, Size } from '../svelte-grid/types' import type { StaticAppInput, @@ -35,6 +34,7 @@ import { deepMergeWithPriority } from '$lib/utils' import { sendUserToast } from '$lib/toast' import { getNextId } from '$lib/components/flows/idUtils' import { enterpriseLicense } from '$lib/stores' +import gridHelp from '../svelte-grid/utils/helper' export function findComponentSettings(app: App, id: string | undefined) { if (!id) return undefined @@ -245,7 +245,8 @@ export function createNewGridItem( columns?: Record, initialPosition: { x: number; y: number } = { x: 0, y: 0 }, recOverride?: Record, - fixed?: boolean + fixed?: boolean, + shouldNotFindSpace?: boolean ): GridItem { const newComponent = { fixed: fixed ?? false, @@ -272,11 +273,28 @@ export function createNewGridItem( h: rec.h } } else { - newItem[column] = columns[column] + newItem[column] = { + ...columns[column], + x: initialPosition.x, + y: initialPosition.y + } } - const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number } - newItem[column] = { ...newItem[column], ...position } + let shouldComputePosition: boolean = false + + // Fallback to avoid component disapearing + if (initialPosition.x === undefined || initialPosition.y === undefined) { + newItem[column].x = 0 + newItem[column].y = 0 + shouldComputePosition = true + } + + // Either the final position is controlled using initialPosition or the position is computed because the component positions are wrong + if (!shouldNotFindSpace || shouldComputePosition) { + const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number } + + newItem[column] = { ...newItem[column], ...position } + } }) return newItem @@ -392,7 +410,8 @@ export function insertNewGridItem( initialPosition: { x: number; y: number } = { x: 0, y: 0 }, recOverride?: Record, keepSubgrids?: boolean, - fixed?: boolean + fixed?: boolean, + shouldNotFindSpace?: boolean ): string { const id = keepId ?? getNextGridItemId(app) @@ -439,7 +458,16 @@ export function insertNewGridItem( let grid = focusedGrid ? app.subgrids[key!] : app.grid - const newItem = createNewGridItem(grid, id, data, columns, initialPosition, recOverride, fixed) + const newItem = createNewGridItem( + grid, + id, + data, + columns, + initialPosition, + recOverride, + fixed, + shouldNotFindSpace + ) grid.push(newItem) return id } @@ -1135,3 +1163,120 @@ export function setUpTopBarComponentContent(id: string, app: App) { } ) } + +export function isContainer(type: string): boolean { + return ( + type === 'containercomponent' || + type === 'tabscomponent' || + type === 'verticalsplitpanescomponent' || + type === 'horizontalsplitpanescomponent' || + type === 'steppercomponent' || + type === 'listcomponent' || + type === 'decisiontreecomponent' + ) +} + +export function subGridIndexKey(type: string | undefined, id: string, world: World): number { + switch (type) { + case 'containercomponent': + case 'verticalsplitpanescomponent': + case 'horizontalsplitpanescomponent': + case 'listcomponent': + return 0 + case 'tabscomponent': { + return (world?.outputsById?.[id]?.selectedTabIndex?.peak() as number) ?? 0 + } + case 'steppercomponent': { + return (world?.outputsById?.[id]?.currentStepIndex?.peak() as number) ?? 0 + } + case 'decisiontreecomponent': { + return (world?.outputsById?.[id]?.currentNodeIndex?.peak() as number) ?? 0 + } + } + + return 0 +} + +export function computePosition( + clientX: number, + clientY: number, + xPerPx: number, + yPerPx: number, + overlapped?: string, + element?: HTMLElement +) { + const overlappedElement = overlapped + ? document.getElementById(`component-${overlapped}`) + : document.getElementById('root-grid') + + const xRelativeToElement = element ? clientX - element.getBoundingClientRect().left : 0 + const yRelativeToElement = element ? clientY - element.getBoundingClientRect().top : 0 + + const xRelativeToOverlappedElement = overlappedElement + ? clientX - overlappedElement.getBoundingClientRect().left - xRelativeToElement + : 0 + const yRelativeToOverlappedElement = overlappedElement + ? clientY - overlappedElement.getBoundingClientRect().top - yRelativeToElement + : 0 + + const gridX = Math.max(Math.round(xRelativeToOverlappedElement / xPerPx) ?? 0, 0) + const gridY = Math.max(Math.round(yRelativeToOverlappedElement / yPerPx) ?? 0, 0) + + return { + x: gridX, + y: gridY + } +} + +export function getDeltaYByComponent(type: string) { + switch (type) { + case 'steppercomponent': { + return '36px + 0.5rem' + } + case 'tabscomponent': { + return '32px' + } + default: + return '0px' + } +} + +export function getDeltaXByComponent(type: string) { + switch (type) { + case 'steppercomponent': { + return '0.5rem' + } + case 'tabscomponent': { + return '0px' + } + default: + return '0px' + } +} + +export type GridShadow = { + x: number + y: number + xPerPx: number + yPerPx: number + w: number + h: number +} + +export function areShadowsTheSame( + shadow1: GridShadow | undefined, + shadow2: GridShadow | undefined +) { + if (!shadow1 || !shadow2) { + return false + } + + return ( + shadow1.x === shadow2.x && + shadow1.y === shadow2.y && + shadow1.xPerPx === shadow2.xPerPx && + shadow1.yPerPx === shadow2.yPerPx && + shadow1.w === shadow2.w && + shadow1.h === shadow2.h + ) +} diff --git a/frontend/src/lib/components/apps/editor/component/Component.svelte b/frontend/src/lib/components/apps/editor/component/Component.svelte index 6561c1b1a5..56c39ed072 100644 --- a/frontend/src/lib/components/apps/editor/component/Component.svelte +++ b/frontend/src/lib/components/apps/editor/component/Component.svelte @@ -81,6 +81,7 @@ import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte' import AppUserResource from '../../components/inputs/AppUserResource.svelte' import { Anchor } from 'lucide-svelte' + import { findGridItemParentGrid, isContainer } from '../appUtils' export let component: AppComponent export let selected: boolean @@ -88,9 +89,10 @@ export let render: boolean export let hidden: boolean export let fullHeight: boolean - export let overlapped: boolean = false - - const { mode, app, hoverStore, connectingInput, selectedComponent } = + export let overlapped: string | undefined = undefined + export let moveMode: string | undefined = undefined + export let componentDraggedId: string | undefined = undefined + const { mode, app, hoverStore, connectingInput } = getContext('AppViewerContext') const editorContext = getContext('AppEditorContext') @@ -121,6 +123,36 @@ } }, 50) } + + function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) { + let parentGrid = findGridItemParentGrid($app, componentDraggedId) + + return !parentGrid?.startsWith(`${componentId}-`) + } + + function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) { + return ( + findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId) + ) + } + + let cachedComponentDraggedIsNotChild: boolean | undefined + let cachedAreOnTheSameSubgrid: boolean | undefined + + function updateCache(componentDraggedId: string | undefined) { + if (componentDraggedId) { + cachedComponentDraggedIsNotChild = componentDraggedIsNotChild( + componentDraggedId, + component.id + ) + cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id) + } else { + cachedComponentDraggedIsNotChild = undefined + cachedAreOnTheSameSubgrid = undefined + } + } + + $: updateCache(componentDraggedId) @@ -139,18 +171,23 @@ hidden && $mode === 'preview' ? 'hidden' : '' )} > - {#if locked && componentActive && $componentActive && $selectedComponent?.[0] !== component.id && overlapped} + {#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
-
Anchored: The component cannot be moved
+
Anchored: The component cannot be moved.
+ {:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild} +
{/if} {#if $mode !== 'preview'} + import { writable } from 'svelte/store' + + const componentDraggedIdStore = writable(undefined) + const componentDraggedParentIdStore = writable(undefined) + const overlappedStore = writable(undefined) + const fakeShadowStore = writable(undefined) + const isCtrlOrMetaPressedStore = writable(false) + + -
+ + +
+ + {#if $isCtrlOrMetaPressedStore && root && $overlappedStore !== $componentDraggedParentIdStore} +
+ {#if $overlappedStore === undefined && $componentDraggedIdStore && $fakeShadowStore} + {@const columnGap = gapX} + + {@const containerBorder = 0.5 * 16} + + {@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0} + + {@const availableWidth = gridTotalWidth - 11 * columnGap} + + {@const columnWidthPx = availableWidth / 12} + + {@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w} + +
+
+
+
+
+ {/if} + {/if} + {#each sortedItems as item (item.id)} {#if item[getComputedCols] != undefined} + {#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore} + {@const columnGap = gapX} + + {@const containerBorder = 0.5 * 16} + + {@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0} + + {@const availableWidth = gridTotalWidth - 11 * columnGap} + + {@const columnWidthPx = availableWidth / 12} + + {@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w} + +
+
+
+
+
+ {/if} + handleInitMove(item.id)} on:move={handleMove} bind:shadow={shadows[item.id]} bind:this={moveResizes[item.id]} on:repaint={handleRepaint} + on:resizeStart={() => (resizing = true)} + on:resizeEnd={() => (resizing = false)} onTop={Boolean(allIdsInPath?.includes(item.id))} id={item.id} {xPerPx} {yPerPx} + fakeShadow={$fakeShadowStore} + on:dropped={(e) => { + $componentDraggedIdStore = undefined + $componentDraggedParentIdStore = undefined + $overlappedStore = undefined + $fakeShadowStore = undefined + lastDetail = undefined + + if (!$isCtrlOrMetaPressedStore) { + return + } + + dispatch('dropped', e.detail) + }} width={xPerPx == 0 ? 0 : Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx - @@ -205,10 +464,18 @@ {sensor} container={scroller} nativeContainer={container} - {overlapped} + overlapped={$overlappedStore} + moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'} + type={item.data['type']} > {#if item[getComputedCols]} - {/if} diff --git a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte index 7c8654e60c..c7fcc94d04 100644 --- a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte @@ -3,6 +3,13 @@ import type { AppEditorContext, AppViewerContext } from '../types' import { writable } from 'svelte/store' import { twMerge } from 'tailwind-merge' + import { + computePosition, + findGridItemParentGrid, + isContainer, + type GridShadow + } from '../editor/appUtils' + import { throttle } from './utils/other' const dispatch = createEventDispatcher() @@ -28,9 +35,12 @@ export let onTop export let shadow: { x: number; y: number; w: number; h: number } | undefined = undefined export let overlapped: string | undefined = undefined + export let moveMode: 'move' | 'insert' = 'move' + export let type: string | undefined = undefined + export let fakeShadow: GridShadow | undefined = undefined const ctx = getContext('AppEditorContext') - const { mode } = getContext('AppViewerContext') + const { mode, app } = getContext('AppViewerContext') const scale = ctx ? ctx.scale : writable(100) @@ -75,6 +85,7 @@ x: (moveX / $scale) * 100 - initX, y: (moveY / $scale) * 100 - initY } + dispatch('move', { cordDiff, clientY: clientY }) } return x @@ -168,7 +179,7 @@ } let dragClosure: (() => void) | undefined = undefined - const pointerdown = ({ clientX, clientY, pageX, pageY }) => { + const pointerdown = ({ clientX, clientY }) => { dragClosure = () => { dragClosure = undefined ctx.componentActive.set(true) @@ -223,16 +234,45 @@ } } + // Shared state for the shadow position + let currentShadowPosition: + | { x: number; y: number; xPerPx: number; yPerPx: number; h: number; w: number } + | undefined = undefined + let currentIntersectingElementId: string | undefined = undefined + + let moving: boolean = false + const pointermove = (event) => { dragClosure && dragClosure() event.preventDefault() event.stopPropagation() event.stopImmediatePropagation() + moving = true + const { clientX, clientY } = event const cordDiff = { x: (clientX / $scale) * 100 - initX, y: (clientY / $scale) * 100 - initY } - dispatch('move', { cordDiff, clientY }) + if (moveMode === 'move') { + dispatch('move', { + cordDiff, + clientY, + intersectingElement: undefined, + shadow: undefined + }) + + return + } + + throttledComputeShadow(clientX, clientY) + + dispatch('move', { + cordDiff, + clientY, + intersectingElement: currentIntersectingElementId, + shadow: currentShadowPosition, + overlapped + }) } export function updateMove(newCoordDiff, clientY) { @@ -274,6 +314,56 @@ update() } } + + let element: HTMLElement | undefined = undefined + + function computeShadow(clientX: number, clientY: number) { + const elementsAtPoint = document.elementsFromPoint(clientX, clientY) + const intersectingElement = elementsAtPoint.find( + (el) => + el.id !== divId && + el.classList.contains('svlt-grid-item') && + el.getAttribute('data-iscontainer') === 'true' + ) + + const newOverlapped = intersectingElement ? intersectingElement?.id.split('-')[1] : undefined + + const container = newOverlapped + ? intersectingElement?.querySelector('.svlt-grid-container') + : document.getElementById('root-grid') + + const xPerPxComputed = Number(container?.getAttribute('data-xperpx')) ?? xPerPx + + const position = computePosition( + clientX, + clientY, + xPerPxComputed, + yPerPx, + newOverlapped, + element + ) + + if (overlapped !== newOverlapped) { + currentShadowPosition = undefined + } else { + // Update shared state + currentShadowPosition = { + x: position.x, + y: position.y, + xPerPx: xPerPxComputed, + yPerPx, + h: item.h, + w: item.w + } + } + + currentIntersectingElementId = newOverlapped + } + + const throttledComputeShadow = throttle((clientX, clientY) => { + computeShadow(clientX, clientY) + }, 50) + const pointerup = (e) => { ctx.componentActive.set(false) stopAutoscroll() @@ -281,11 +371,29 @@ window.removeEventListener('pointerdown', pointerdown) window.removeEventListener('pointermove', pointermove) window.removeEventListener('pointerup', pointerup) + if (!dragClosure) { repaint(true, true) } else { dragClosure = undefined } + + if (!moving) { + return + } + + const parent = findGridItemParentGrid($app, id) + + if (overlapped && (overlapped === parent || parent?.startsWith(overlapped))) { + return + } + + dispatch('dropped', { + id, + overlapped, + x: fakeShadow?.x, + y: fakeShadow?.y + }) } let resizeInitPos = { x: 0, y: 0 } @@ -315,6 +423,7 @@ window.addEventListener('pointermove', resizePointerMove) window.addEventListener('pointerup', resizePointerUp) + dispatch('resizeStart') } const resizePointerMove = ({ pageX, pageY }) => { @@ -350,15 +459,36 @@ window.removeEventListener('pointermove', resizePointerMove) window.removeEventListener('pointerup', resizePointerUp) + dispatch('resizeEnd') + } + + function shouldDisplayShadow(moveMode: 'insert' | 'move', overlapped: string | undefined) { + if (moveMode === 'move') { + return true + } + + const parent = findGridItemParentGrid($app, id) + + if (parent === undefined) { + return overlapped === undefined + } else if (overlapped) { + return parent.startsWith(overlapped) + } + + return false } +
-
resizePointerDown(e, 'vertical')} /> -
resizePointerDown(e, 'horizontal')} /> -
resizePointerDown(e, 'both')} /> + {#if moveMode === 'move'} +
resizePointerDown(e, 'vertical')} + /> +
resizePointerDown(e, 'horizontal')} + /> +
resizePointerDown(e, 'both')} /> + {/if}
{#if xPerPx > 0 && (active || trans) && shadow}