mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
feat(frontend): components can be moved inside containers by holding ctrl/cmd
* feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): move component inside containers * feat(frontend): done * feat(frontend): done * fix(frontend): wip * fix(frontend): wip * fix(frontend): done * fix(frontend): done * fix(frontend): fix anchored * fix(frontend): restore event * feat(frontend): support dropping on the top level + hide the shadow when leaving the grid * feat(frontend): drop in the correct subgrid when there are multiple * feat(frontend): wip * feat(frontend): fix hide shadow * feat(frontend): remove dead code * feat(frontend): wip * feat(frontend): fix overflow * feat(frontend): wip * feat(frontend): done * feat(frontend): fix z-index * feat(frontend): wip * feat(frontend): fix shadow * feat(frontend): fix resize + drop position * feat(frontend): fix dropped when no movement * feat(frontend): hide the fake shadow if the move mode is set * feat(frontend): add small timeout when we leave the insert mode * feat(frontend): render the shadow where the component will drop * feat(frontend): done * feat(frontend): done * feat(frontend): improve perf * feat(frontend): done * feat(frontend): remove console.log * feat(frontend): fix move toplevel * feat(frontend): Components positions are restored when the mode changes * feat(frontend): Components positions are restored when the mode changes * feat(frontend): Components positions are restored when the mode changes * feat(frontend): fix edges cases + improve perf * feat(frontend): fix drop position * feat(frontend): fix final drop position * feat(frontend): fix final drop position * feat(frontend): fix insert from component bar * feat(frontend): Fix disapearing components
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
<div
|
||||
class="absolute z-50 inset-0 h-full w-full bg-surface-secondary [background-size:16px_16px]"
|
||||
/>
|
||||
>
|
||||
<div class="w-min whitespace-nowrap mx-auto pt-0.5 z-50">
|
||||
<Alert
|
||||
title={`Press ${getModifierKey()} to drop component inside a container.`}
|
||||
size="xs"
|
||||
class="h-10 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SplitPanesWrapper>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { push } from '$lib/history'
|
||||
import { dfs, expandGriditem, findGridItem } from './appUtils'
|
||||
import {
|
||||
dfs,
|
||||
expandGriditem,
|
||||
findGridItem,
|
||||
findGridItemParentGrid,
|
||||
insertNewGridItem,
|
||||
isContainer,
|
||||
subGridIndexKey
|
||||
} from './appUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import ComponentWrapper from './component/ComponentWrapper.svelte'
|
||||
@@ -32,7 +40,8 @@
|
||||
parentWidth,
|
||||
breakpoint,
|
||||
allIdsInPath,
|
||||
bgRuns
|
||||
bgRuns,
|
||||
worldStore
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history, scale, componentActive } = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -61,6 +70,53 @@
|
||||
}
|
||||
$app = $app
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
$selectedComponent = [parentComponentId]
|
||||
$focusedGrid = {
|
||||
parentComponentId,
|
||||
subGridIndex
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full z-[1000] overflow-visible h-full">
|
||||
@@ -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 }
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
@@ -177,7 +260,9 @@
|
||||
on:fillHeight={() => {
|
||||
handleFillHeight(dataItem.id)
|
||||
}}
|
||||
overlapped={overlapped !== undefined}
|
||||
{overlapped}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
|
||||
@@ -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>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('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 }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -109,10 +212,39 @@
|
||||
let:dataItem
|
||||
let:hidden
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
cols={columnConfiguration}
|
||||
scroller={container}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
on:dropped={(e) => {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
@@ -160,6 +292,8 @@
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
|
||||
@@ -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<number, any>,
|
||||
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
||||
recOverride?: Record<number, Size>,
|
||||
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<number, Size>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('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)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
@@ -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}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 bg-locked center-center flex-col z-50',
|
||||
overlapped ? 'bg-locked-hover' : ''
|
||||
)}
|
||||
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
|
||||
>
|
||||
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
|
||||
<Anchor size={24} class="text-primary " />
|
||||
<div class="text-xs">Anchored: The component cannot be moved</div>
|
||||
<div class="text-xs"> Anchored: The component cannot be moved. </div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $mode !== 'preview'}
|
||||
<ComponentHeader
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const componentDraggedIdStore = writable<string | undefined>(undefined)
|
||||
const componentDraggedParentIdStore = writable<string | undefined>(undefined)
|
||||
const overlappedStore = writable<string | undefined>(undefined)
|
||||
const fakeShadowStore = writable<GridShadow | undefined>(undefined)
|
||||
const isCtrlOrMetaPressedStore = writable<boolean>(false)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import gridHelp from './utils/helper'
|
||||
import type { AppViewerContext, GridItem } from '../types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { getContainerHeight } from './utils/container'
|
||||
import { moveItem, getItemById, specifyUndefinedColumns } from './utils/item'
|
||||
import { onMount, createEventDispatcher } from 'svelte'
|
||||
import { onMount, createEventDispatcher, getContext } from 'svelte'
|
||||
import { getColumn, throttle } from './utils/other'
|
||||
import MoveResize from './MoveResize.svelte'
|
||||
import type { FilledItem } from './types'
|
||||
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT, sortGridItemsPosition } from '../editor/appUtils'
|
||||
import {
|
||||
areShadowsTheSame,
|
||||
findGridItemParentGrid,
|
||||
getDeltaXByComponent,
|
||||
getDeltaYByComponent,
|
||||
isContainer,
|
||||
ROW_GAP_X,
|
||||
ROW_GAP_Y,
|
||||
ROW_HEIGHT,
|
||||
sortGridItemsPosition,
|
||||
subGridIndexKey,
|
||||
type GridShadow
|
||||
} from '../editor/appUtils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type T = $$Generic
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let items: FilledItem<T>[]
|
||||
export let rowHeight: number = ROW_HEIGHT
|
||||
export let cols: [number, number][]
|
||||
@@ -22,11 +50,10 @@
|
||||
export let containerWidth: number | undefined = undefined
|
||||
export let scroller: HTMLElement | undefined = undefined
|
||||
export let sensor = 20
|
||||
|
||||
export let root: boolean = false
|
||||
export let parentWidth: number | undefined = undefined
|
||||
|
||||
let getComputedCols
|
||||
|
||||
let container
|
||||
|
||||
$: [gapX, gapY] = gap
|
||||
@@ -81,7 +108,21 @@
|
||||
let sortedItems: FilledItem<T>[] = []
|
||||
$: sortedItems = JSON.parse(JSON.stringify(items)).sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
let resizing: boolean = false
|
||||
|
||||
function handleKeyUp(event) {
|
||||
if ((event.key === 'Control' || event.key === 'Meta') && $isCtrlOrMetaPressedStore) {
|
||||
setTimeout(() => {
|
||||
$isCtrlOrMetaPressedStore = false
|
||||
|
||||
$fakeShadowStore = undefined
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
const initialFixedStates = new Map()
|
||||
|
||||
let initItems: FilledItem<T>[] | undefined = undefined
|
||||
|
||||
const updateMatrix = ({ detail }) => {
|
||||
let isPointerUp = detail.isPointerUp
|
||||
let citems: FilledItem<T>[]
|
||||
@@ -114,10 +155,44 @@
|
||||
...shadows[id]
|
||||
}
|
||||
}
|
||||
let { items, overlap } = moveItem(activeItem, sortedItems, getComputedCols)
|
||||
|
||||
sortedItems = items
|
||||
overlapped = overlap ? id : undefined
|
||||
if ($isCtrlOrMetaPressedStore) {
|
||||
if ($componentDraggedParentIdStore === $overlappedStore) {
|
||||
const fixedContainer = sortedItems.map((item) => {
|
||||
if (isContainer(item.data['type'])) {
|
||||
initialFixedStates.set(item.id, {
|
||||
item3Fixed: item[3].fixed,
|
||||
item12Fixed: item[12].fixed
|
||||
})
|
||||
|
||||
item[3].fixed = true
|
||||
item[12].fixed = true
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
let { items } = moveItem(activeItem, fixedContainer, getComputedCols)
|
||||
|
||||
items = items.map((item) => {
|
||||
if (initialFixedStates.has(item.id)) {
|
||||
const initialState = initialFixedStates.get(item.id)
|
||||
|
||||
if (initialState) {
|
||||
item[3].fixed = initialState.item3Fixed
|
||||
item[12].fixed = initialState.item12Fixed
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
sortedItems = items
|
||||
}
|
||||
} else {
|
||||
let { items } = moveItem(activeItem, sortedItems, getComputedCols)
|
||||
|
||||
sortedItems = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +214,8 @@
|
||||
|
||||
//let hiddenComponents = writable({})
|
||||
|
||||
let lastDetail: { isPointerUp: false; activate: false; id: string | undefined } | undefined =
|
||||
undefined
|
||||
const handleRepaint = ({ detail }) => {
|
||||
if (!detail.isPointerUp) {
|
||||
throttleMatrix({ detail })
|
||||
@@ -156,6 +233,21 @@
|
||||
*/
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if ((event.key === 'Control' || event.key === 'Meta') && !$isCtrlOrMetaPressedStore) {
|
||||
if (resizing) {
|
||||
return
|
||||
}
|
||||
|
||||
$isCtrlOrMetaPressedStore = true
|
||||
|
||||
if (lastDetail) {
|
||||
throttleMatrix({ detail: lastDetail })
|
||||
lastDetail = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let moveResizes: Record<string, MoveResize> = {}
|
||||
let shadows: Record<string, { x: number; y: number; w: number; h: number } | undefined> = {}
|
||||
|
||||
@@ -165,32 +257,199 @@
|
||||
moveResize?.updateMove(JSON.parse(JSON.stringify(detail.cordDiff)), detail.eventY)
|
||||
}
|
||||
})
|
||||
|
||||
lastDetail = detail
|
||||
throttleMatrix({ detail: { isPointerUp: false, activate: false } })
|
||||
|
||||
if (!$isCtrlOrMetaPressedStore) {
|
||||
$overlappedStore = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
// We don't display the fake shadow if the dragged component is a child of the overlapped component
|
||||
$componentDraggedParentIdStore !== $overlappedStore &&
|
||||
detail.shadow &&
|
||||
// only update the fake shadow if the are different
|
||||
!areShadowsTheSame($fakeShadowStore, detail.shadow)
|
||||
) {
|
||||
const draggedItem = sortedItems.find((item) => item.id === $componentDraggedIdStore)
|
||||
|
||||
if (draggedItem) {
|
||||
draggedItem[getComputedCols].x = detail.shadow.x
|
||||
draggedItem[getComputedCols].y = detail.shadow.y
|
||||
}
|
||||
|
||||
let items: GridItem[] = []
|
||||
|
||||
if ($overlappedStore) {
|
||||
const div = document.getElementById(`component-${$overlappedStore}`)
|
||||
const type = div?.getAttribute('data-componenttype')
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return
|
||||
}
|
||||
|
||||
const index = type ? subGridIndexKey(type, $overlappedStore, $worldStore) : 0
|
||||
|
||||
items = $app.subgrids[`${$overlappedStore}-${index}`] ?? []
|
||||
} else {
|
||||
items = $app.grid ?? []
|
||||
}
|
||||
|
||||
if (!draggedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const freeSpace = gridHelp.findSpace(draggedItem, items, getComputedCols)
|
||||
|
||||
$fakeShadowStore = {
|
||||
x: freeSpace.x,
|
||||
y: freeSpace.y,
|
||||
xPerPx: detail.shadow.xPerPx,
|
||||
yPerPx: detail.shadow.yPerPx,
|
||||
w: detail.shadow.w,
|
||||
h: detail.shadow.h
|
||||
}
|
||||
}
|
||||
|
||||
// When leaving the overlapped component, we clear the fake shadow
|
||||
// to avoid rendering it with the wrong position at the next intersection
|
||||
if (detail.intersectingElement !== $overlappedStore) {
|
||||
$fakeShadowStore = undefined
|
||||
}
|
||||
|
||||
// Update the overlapped component
|
||||
$overlappedStore = detail.intersectingElement
|
||||
}
|
||||
|
||||
export function handleInitMove({ detail }) {
|
||||
export function handleInitMove(id: string) {
|
||||
$componentDraggedIdStore = id
|
||||
$componentDraggedParentIdStore = findGridItemParentGrid($app, id)?.split('-')[0] ?? undefined
|
||||
|
||||
Object.entries(moveResizes).forEach(([id, moveResize]) => {
|
||||
if (selectedIds?.includes(id)) {
|
||||
moveResize?.initmove()
|
||||
}
|
||||
})
|
||||
}
|
||||
let overlapped: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
|
||||
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
|
||||
|
||||
<div
|
||||
class="svlt-grid-container"
|
||||
style="height: {containerHeight}px"
|
||||
bind:this={container}
|
||||
id={root ? 'root-grid' : undefined}
|
||||
data-xperpx={xPerPx}
|
||||
>
|
||||
<!-- ROOT SHADOW-->
|
||||
{#if $isCtrlOrMetaPressedStore && root && $overlappedStore !== $componentDraggedParentIdStore}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
$componentDraggedIdStore && $overlappedStore === undefined
|
||||
? 'bg-draggedover dark:bg-draggedover-dark'
|
||||
: ''
|
||||
)}
|
||||
/>
|
||||
{#if $overlappedStore === undefined && $componentDraggedIdStore && $fakeShadowStore}
|
||||
{@const columnGap = gapX}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
<div class="absolute inset-0">
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class="absolute bg-blue-300 transition-all"
|
||||
style={`
|
||||
left:${Math.min(maxX, $fakeShadowStore.x) * xPerPx + gapX}px ;
|
||||
top: ${$fakeShadowStore.y * yPerPx + gapY}px;
|
||||
width: ${$fakeShadowStore.w * xPerPx - gapX}px;
|
||||
height: ${$fakeShadowStore.h * yPerPx - gapY}px;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/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}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
<div
|
||||
class="absolute"
|
||||
style={`
|
||||
left: ${item[getComputedCols].x * xPerPx + gapX}px;
|
||||
top: ${item[getComputedCols].y * yPerPx + gapY}px;
|
||||
`}
|
||||
>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
|
||||
style={`
|
||||
left: calc(${
|
||||
Math.min($fakeShadowStore.x, maxX) * $fakeShadowStore.xPerPx + gapX
|
||||
}px + 0.5rem + ${getDeltaXByComponent(item.data['type'])});
|
||||
top: calc(${
|
||||
$fakeShadowStore.y * $fakeShadowStore.yPerPx + gapY
|
||||
}px + 0.5rem + ${getDeltaYByComponent(item.data['type'])});
|
||||
width: ${$fakeShadowStore.w * $fakeShadowStore.xPerPx - gapX * 2}px;
|
||||
height: ${$fakeShadowStore.h * $fakeShadowStore.yPerPx - gapY * 2}px;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<MoveResize
|
||||
on:initmove={handleInitMove}
|
||||
on:initmove={() => 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]}
|
||||
<slot dataItem={item} hidden={false} {overlapped} />
|
||||
<slot
|
||||
dataItem={item}
|
||||
hidden={false}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
componentDraggedId={$componentDraggedIdStore}
|
||||
/>
|
||||
{/if}
|
||||
</MoveResize>
|
||||
{/if}
|
||||
|
||||
@@ -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>('AppEditorContext')
|
||||
const { mode } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { mode, app } = getContext<AppViewerContext>('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
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={element}
|
||||
draggable="false"
|
||||
on:pointerdown|stopPropagation|preventDefault={pointerdown}
|
||||
id={divId}
|
||||
class="svlt-grid-item"
|
||||
data-iscontainer={type ? isContainer(type) : false}
|
||||
data-componenttype={type}
|
||||
class:svlt-grid-active={active || (trans && rect)}
|
||||
style="width: {xPerPx == 0 ? 0 : active ? newSize.width : width}px; height:{xPerPx == 0
|
||||
? 0
|
||||
@@ -377,16 +507,24 @@
|
||||
} transform: translate(${left}px, ${top}px); `} "
|
||||
>
|
||||
<slot />
|
||||
<div class="svlt-grid-resizer-bottom" on:pointerdown={(e) => resizePointerDown(e, 'vertical')} />
|
||||
<div class="svlt-grid-resizer-side" on:pointerdown={(e) => resizePointerDown(e, 'horizontal')} />
|
||||
<div class="svlt-grid-resizer" on:pointerdown={(e) => resizePointerDown(e, 'both')} />
|
||||
{#if moveMode === 'move'}
|
||||
<div
|
||||
class="svlt-grid-resizer-bottom"
|
||||
on:pointerdown={(e) => resizePointerDown(e, 'vertical')}
|
||||
/>
|
||||
<div
|
||||
class="svlt-grid-resizer-side"
|
||||
on:pointerdown={(e) => resizePointerDown(e, 'horizontal')}
|
||||
/>
|
||||
<div class="svlt-grid-resizer" on:pointerdown={(e) => resizePointerDown(e, 'both')} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if xPerPx > 0 && (active || trans) && shadow}
|
||||
<div
|
||||
class={twMerge(
|
||||
'svlt-grid-shadow shadow-active',
|
||||
overlapped ? 'svlte-grid-shadow-forbidden' : ''
|
||||
shouldDisplayShadow(moveMode, overlapped) ? '' : 'hidden'
|
||||
)}
|
||||
style="width: {shadow.w * xPerPx - gapX * 2}px; height: {shadow.h * yPerPx -
|
||||
gapY * 2}px; transform: translate({shadow.x * xPerPx + gapX}px, {shadow.y * yPerPx +
|
||||
@@ -470,8 +608,4 @@
|
||||
-webkit-backface-visibility: hidden;
|
||||
background: #93c4fdd0;
|
||||
}
|
||||
|
||||
.svlte-grid-shadow-forbidden {
|
||||
background: rgba(255, 99, 71, 0.2) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,7 @@ function distance(a, b): number {
|
||||
|
||||
export function findFreeSpaceForItem<T>(matrix: FilledItem<T>[][], item: ItemLayout) {
|
||||
const cols = matrix[0].length
|
||||
|
||||
const w = Math.min(cols, item.w)
|
||||
const h = item.h
|
||||
let xNtime = cols - w + 1
|
||||
@@ -135,8 +136,7 @@ export function moveItem(active, items, cols) {
|
||||
// If found fixed, reset the active to its original position
|
||||
if (fixed) {
|
||||
return {
|
||||
items: items,
|
||||
overlap: closeBlocks.length > 0
|
||||
items: items
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export function moveItem(active, items, cols) {
|
||||
// Return result
|
||||
return {
|
||||
items: tempItems,
|
||||
overlap: false
|
||||
overlap: undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user