@elementor/editor-floating-panels 4.3.0-970
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +117 -0
- package/dist/index.d.mts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +1108 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1062 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
- package/src/api.ts +36 -0
- package/src/components/external/floating-panel-body.tsx +15 -0
- package/src/components/external/floating-panel-footer.tsx +22 -0
- package/src/components/external/floating-panel-header.tsx +100 -0
- package/src/components/external/floating-panel.tsx +10 -0
- package/src/components/external/index.ts +4 -0
- package/src/components/internal/corner-resize-handle.tsx +69 -0
- package/src/components/internal/drag-handle.tsx +29 -0
- package/src/components/internal/host.tsx +86 -0
- package/src/components/internal/panel-window.tsx +100 -0
- package/src/components/internal/resize-handle.tsx +60 -0
- package/src/constants.ts +1 -0
- package/src/hooks/use-floating-panel-actions.ts +26 -0
- package/src/hooks/use-floating-panel-drag.ts +85 -0
- package/src/hooks/use-floating-panel-resize.ts +166 -0
- package/src/hooks/use-floating-panel-status.ts +17 -0
- package/src/hooks/use-floating-panel-z-index.ts +7 -0
- package/src/index.ts +15 -0
- package/src/init.ts +14 -0
- package/src/location.ts +3 -0
- package/src/persistence.ts +76 -0
- package/src/store/index.ts +2 -0
- package/src/store/selectors.ts +61 -0
- package/src/store/slice.ts +113 -0
- package/src/sync.ts +104 -0
- package/src/types.ts +64 -0
- package/src/utils/clamp.ts +3 -0
- package/src/utils/corner-position.ts +149 -0
- package/src/utils/direction.ts +3 -0
- package/src/utils/drag-math.ts +49 -0
- package/src/utils/resize-math.ts +107 -0
- package/src/utils/viewport-bounds.ts +7 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { __createSlice, type PayloadAction } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { type FloatingPanelDefaults, type FloatingPanelState, type LogicalPosition, type LogicalSize } from '../types';
|
|
4
|
+
import { buildInitialPosition, type PanelCorner } from '../utils/corner-position';
|
|
5
|
+
|
|
6
|
+
type SliceState = {
|
|
7
|
+
byId: Record< string, FloatingPanelState >;
|
|
8
|
+
minSizeById: Record< string, LogicalSize >;
|
|
9
|
+
titlesById: Record< string, string >;
|
|
10
|
+
isDraggableById: Record< string, boolean >;
|
|
11
|
+
isResizableById: Record< string, boolean >;
|
|
12
|
+
topZIndex: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CORNER: PanelCorner = 'block-start-inline-start';
|
|
16
|
+
|
|
17
|
+
const initialState: SliceState = {
|
|
18
|
+
byId: {},
|
|
19
|
+
minSizeById: {},
|
|
20
|
+
titlesById: {},
|
|
21
|
+
isDraggableById: {},
|
|
22
|
+
isResizableById: {},
|
|
23
|
+
topZIndex: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const slice = __createSlice( {
|
|
27
|
+
name: 'floatingPanels',
|
|
28
|
+
initialState,
|
|
29
|
+
reducers: {
|
|
30
|
+
register(
|
|
31
|
+
state,
|
|
32
|
+
action: PayloadAction< {
|
|
33
|
+
id: string;
|
|
34
|
+
defaults: FloatingPanelDefaults;
|
|
35
|
+
title?: string;
|
|
36
|
+
isDraggable?: boolean;
|
|
37
|
+
isResizable?: boolean;
|
|
38
|
+
persisted?: FloatingPanelState;
|
|
39
|
+
} >
|
|
40
|
+
) {
|
|
41
|
+
const { id, defaults, title, isDraggable, isResizable, persisted } = action.payload;
|
|
42
|
+
|
|
43
|
+
state.minSizeById[ id ] = { inlineSize: defaults.minWidth, blockSize: defaults.minHeight };
|
|
44
|
+
state.isDraggableById[ id ] = isDraggable ?? false;
|
|
45
|
+
state.isResizableById[ id ] = isResizable ?? false;
|
|
46
|
+
|
|
47
|
+
if ( title !== undefined ) {
|
|
48
|
+
state.titlesById[ id ] = title;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ( state.byId[ id ] ) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const corner = defaults.corner ?? DEFAULT_CORNER;
|
|
56
|
+
const canReusePersisted = persisted && persisted.corner === corner;
|
|
57
|
+
|
|
58
|
+
state.byId[ id ] = canReusePersisted
|
|
59
|
+
? persisted
|
|
60
|
+
: {
|
|
61
|
+
isOpen: false,
|
|
62
|
+
corner,
|
|
63
|
+
position: buildInitialPosition( corner, defaults.initialPosition ),
|
|
64
|
+
size: { inlineSize: defaults.width, blockSize: defaults.height },
|
|
65
|
+
zIndex: 0,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if ( persisted && persisted.zIndex > state.topZIndex ) {
|
|
69
|
+
state.topZIndex = persisted.zIndex;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
open( state, action: PayloadAction< string > ) {
|
|
73
|
+
const panel = state.byId[ action.payload ];
|
|
74
|
+
|
|
75
|
+
if ( panel ) {
|
|
76
|
+
panel.isOpen = true;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
close( state, action: PayloadAction< string > ) {
|
|
80
|
+
const panel = state.byId[ action.payload ];
|
|
81
|
+
|
|
82
|
+
if ( panel ) {
|
|
83
|
+
panel.isOpen = false;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
setPosition( state, action: PayloadAction< { id: string; position: LogicalPosition } > ) {
|
|
87
|
+
const panel = state.byId[ action.payload.id ];
|
|
88
|
+
|
|
89
|
+
if ( panel ) {
|
|
90
|
+
panel.position = action.payload.position;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
setSize( state, action: PayloadAction< { id: string; size: LogicalSize } > ) {
|
|
94
|
+
const panel = state.byId[ action.payload.id ];
|
|
95
|
+
|
|
96
|
+
if ( panel ) {
|
|
97
|
+
panel.size = action.payload.size;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
bringToFront( state, action: PayloadAction< string > ) {
|
|
101
|
+
const panel = state.byId[ action.payload ];
|
|
102
|
+
|
|
103
|
+
if ( ! panel ) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
state.topZIndex += 1;
|
|
108
|
+
panel.zIndex = state.topZIndex;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
export type FloatingPanelsSliceState = SliceState;
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { __getStore, __subscribeWithSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { decodePersistedState, encodePersistedState, PERSISTENCE_STORAGE_KEY } from './persistence';
|
|
4
|
+
import { type GlobalState } from './store/selectors';
|
|
5
|
+
import { type FloatingPanelState } from './types';
|
|
6
|
+
|
|
7
|
+
const PERSIST_DEBOUNCE_MS = 250;
|
|
8
|
+
|
|
9
|
+
export interface PanelStateStorage {
|
|
10
|
+
read: () => string | null;
|
|
11
|
+
write: ( value: string ) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const localStorageAdapter: PanelStateStorage = {
|
|
15
|
+
read: () => {
|
|
16
|
+
try {
|
|
17
|
+
return globalThis.localStorage?.getItem( PERSISTENCE_STORAGE_KEY ) ?? null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
write: ( value ) => {
|
|
23
|
+
try {
|
|
24
|
+
globalThis.localStorage?.setItem( PERSISTENCE_STORAGE_KEY, value );
|
|
25
|
+
} catch {
|
|
26
|
+
// Best-effort: storage may be full, blocked, or unavailable.
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let cachedPersistedState: Record< string, FloatingPanelState > = {};
|
|
32
|
+
let isSyncInitialized = false;
|
|
33
|
+
let persistenceTimer: ReturnType< typeof setTimeout > | null = null;
|
|
34
|
+
let persistenceUnsubscribe: ( () => void ) | null = null;
|
|
35
|
+
let persistenceSession = 0;
|
|
36
|
+
|
|
37
|
+
export function isFloatingPanelsSyncInitialized(): boolean {
|
|
38
|
+
return isSyncInitialized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sync( storage: PanelStateStorage = localStorageAdapter ): void {
|
|
42
|
+
isSyncInitialized = true;
|
|
43
|
+
cachedPersistedState = decodePersistedState( storage.read() );
|
|
44
|
+
|
|
45
|
+
schedulePersistence( storage );
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getPersistedState( id: string ): FloatingPanelState | undefined {
|
|
49
|
+
return cachedPersistedState[ id ];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const STORE_READY_POLL_MS = 16;
|
|
53
|
+
|
|
54
|
+
function clearPersistenceTimer(): void {
|
|
55
|
+
if ( persistenceTimer ) {
|
|
56
|
+
clearTimeout( persistenceTimer );
|
|
57
|
+
persistenceTimer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function schedulePersistence( storage: PanelStateStorage ): void {
|
|
62
|
+
clearPersistenceTimer();
|
|
63
|
+
persistenceUnsubscribe?.();
|
|
64
|
+
persistenceUnsubscribe = null;
|
|
65
|
+
|
|
66
|
+
const session = ++persistenceSession;
|
|
67
|
+
|
|
68
|
+
const subscribe = () => {
|
|
69
|
+
if ( session !== persistenceSession ) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
persistenceUnsubscribe = __subscribeWithSelector(
|
|
74
|
+
( state: GlobalState ) => state.floatingPanels.byId,
|
|
75
|
+
( byId ) => {
|
|
76
|
+
clearPersistenceTimer();
|
|
77
|
+
|
|
78
|
+
persistenceTimer = setTimeout( () => {
|
|
79
|
+
if ( session !== persistenceSession ) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cachedPersistedState = { ...cachedPersistedState, ...byId };
|
|
84
|
+
storage.write( encodePersistedState( cachedPersistedState ) );
|
|
85
|
+
}, PERSIST_DEBOUNCE_MS );
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const waitForStore = () => {
|
|
91
|
+
if ( session !== persistenceSession ) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ( __getStore() ) {
|
|
96
|
+
subscribe();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setTimeout( waitForStore, STORE_READY_POLL_MS );
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
waitForStore();
|
|
104
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { PanelCorner } from './utils/corner-position';
|
|
4
|
+
|
|
5
|
+
export type { PanelCorner };
|
|
6
|
+
|
|
7
|
+
export type LogicalSize = {
|
|
8
|
+
inlineSize: number;
|
|
9
|
+
blockSize: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LogicalPosition = {
|
|
13
|
+
insetInlineStart: number;
|
|
14
|
+
insetInlineEnd: number;
|
|
15
|
+
insetBlockStart: number;
|
|
16
|
+
insetBlockEnd: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Public API for declaring a floating panel's initial dimensions.
|
|
21
|
+
*
|
|
22
|
+
* Sizes use physical names (width/height) intentionally — Elementor renders
|
|
23
|
+
* in horizontal writing mode only, so these are equivalent to logical
|
|
24
|
+
* inline-size/block-size. Positioning continues to use logical names
|
|
25
|
+
* because position is genuinely direction-sensitive.
|
|
26
|
+
*
|
|
27
|
+
* If multi-writing-mode support is ever needed, replace these fields with
|
|
28
|
+
* `size: LogicalSize` and `minSize: LogicalSize` and update the store's
|
|
29
|
+
* register reducer.
|
|
30
|
+
*/
|
|
31
|
+
export type FloatingPanelDefaults = {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
minWidth: number;
|
|
35
|
+
minHeight: number;
|
|
36
|
+
corner?: PanelCorner;
|
|
37
|
+
initialPosition?: Partial< LogicalPosition >;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type FloatingPanelHeaderAction = {
|
|
41
|
+
id: string;
|
|
42
|
+
icon: ComponentType;
|
|
43
|
+
label: string;
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type FloatingPanelDeclaration = {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
icon: ComponentType;
|
|
52
|
+
component: ComponentType;
|
|
53
|
+
isDraggable?: boolean;
|
|
54
|
+
isResizable?: boolean;
|
|
55
|
+
defaults: FloatingPanelDefaults;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type FloatingPanelState = {
|
|
59
|
+
isOpen: boolean;
|
|
60
|
+
zIndex: number;
|
|
61
|
+
size: LogicalSize;
|
|
62
|
+
corner: PanelCorner;
|
|
63
|
+
position: LogicalPosition;
|
|
64
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { LogicalPosition, LogicalSize } from '../types';
|
|
2
|
+
|
|
3
|
+
export type PanelCorner =
|
|
4
|
+
| 'block-start-inline-start'
|
|
5
|
+
| 'block-start-inline-end'
|
|
6
|
+
| 'block-end-inline-start'
|
|
7
|
+
| 'block-end-inline-end';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_INSET_BLOCK_PX = 80;
|
|
10
|
+
export const DEFAULT_INSET_INLINE_PX = 24;
|
|
11
|
+
|
|
12
|
+
type InsetKey = keyof LogicalPosition;
|
|
13
|
+
|
|
14
|
+
const ACTIVE_INSETS: Record< PanelCorner, [ InsetKey, InsetKey ] > = {
|
|
15
|
+
'block-start-inline-start': [ 'insetInlineStart', 'insetBlockStart' ],
|
|
16
|
+
'block-start-inline-end': [ 'insetInlineEnd', 'insetBlockStart' ],
|
|
17
|
+
'block-end-inline-start': [ 'insetInlineStart', 'insetBlockEnd' ],
|
|
18
|
+
'block-end-inline-end': [ 'insetInlineEnd', 'insetBlockEnd' ],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CORNER_DEFAULTS: Record< PanelCorner, Partial< LogicalPosition > > = {
|
|
22
|
+
'block-start-inline-start': { insetInlineStart: DEFAULT_INSET_INLINE_PX, insetBlockStart: DEFAULT_INSET_BLOCK_PX },
|
|
23
|
+
'block-start-inline-end': { insetInlineEnd: DEFAULT_INSET_INLINE_PX, insetBlockStart: DEFAULT_INSET_BLOCK_PX },
|
|
24
|
+
'block-end-inline-start': { insetInlineStart: DEFAULT_INSET_INLINE_PX, insetBlockEnd: DEFAULT_INSET_BLOCK_PX },
|
|
25
|
+
'block-end-inline-end': { insetInlineEnd: DEFAULT_INSET_INLINE_PX, insetBlockEnd: DEFAULT_INSET_BLOCK_PX },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const EMPTY_POSITION: LogicalPosition = {
|
|
29
|
+
insetBlockStart: 0,
|
|
30
|
+
insetBlockEnd: 0,
|
|
31
|
+
insetInlineStart: 0,
|
|
32
|
+
insetInlineEnd: 0,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function getActiveInsetKeys( corner: PanelCorner ): [ InsetKey, InsetKey ] {
|
|
36
|
+
return ACTIVE_INSETS[ corner ];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function usesInlineStart( corner: PanelCorner ): boolean {
|
|
40
|
+
return corner.endsWith( 'inline-start' );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function usesBlockStart( corner: PanelCorner ): boolean {
|
|
44
|
+
return corner.startsWith( 'block-start' );
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildInitialPosition( corner: PanelCorner, overrides?: Partial< LogicalPosition > ): LogicalPosition {
|
|
48
|
+
const [ inlineKey, blockKey ] = getActiveInsetKeys( corner );
|
|
49
|
+
const defaults = CORNER_DEFAULTS[ corner ];
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...EMPTY_POSITION,
|
|
53
|
+
[ inlineKey ]: overrides?.[ inlineKey ] ?? defaults[ inlineKey ] ?? 0,
|
|
54
|
+
[ blockKey ]: overrides?.[ blockKey ] ?? defaults[ blockKey ] ?? 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function positionToCssInsets(
|
|
59
|
+
corner: PanelCorner,
|
|
60
|
+
position: LogicalPosition
|
|
61
|
+
): Partial< Record< InsetKey, string > > {
|
|
62
|
+
const [ inlineKey, blockKey ] = getActiveInsetKeys( corner );
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
[ inlineKey ]: `${ position[ inlineKey ] }px`,
|
|
66
|
+
[ blockKey ]: `${ position[ blockKey ] }px`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function toStartAnchoredPosition(
|
|
71
|
+
corner: PanelCorner,
|
|
72
|
+
position: LogicalPosition,
|
|
73
|
+
size: LogicalSize,
|
|
74
|
+
viewport: { width: number; height: number }
|
|
75
|
+
): Pick< LogicalPosition, 'insetInlineStart' | 'insetBlockStart' > {
|
|
76
|
+
return {
|
|
77
|
+
insetInlineStart: usesInlineStart( corner )
|
|
78
|
+
? position.insetInlineStart
|
|
79
|
+
: viewport.width - position.insetInlineEnd - size.inlineSize,
|
|
80
|
+
insetBlockStart: usesBlockStart( corner )
|
|
81
|
+
? position.insetBlockStart
|
|
82
|
+
: viewport.height - position.insetBlockEnd - size.blockSize,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function fromStartAnchoredPosition(
|
|
87
|
+
corner: PanelCorner,
|
|
88
|
+
startAnchored: Pick< LogicalPosition, 'insetInlineStart' | 'insetBlockStart' >,
|
|
89
|
+
size: LogicalSize,
|
|
90
|
+
viewport: { width: number; height: number }
|
|
91
|
+
): LogicalPosition {
|
|
92
|
+
if ( usesInlineStart( corner ) && usesBlockStart( corner ) ) {
|
|
93
|
+
return {
|
|
94
|
+
...EMPTY_POSITION,
|
|
95
|
+
insetInlineStart: startAnchored.insetInlineStart,
|
|
96
|
+
insetBlockStart: startAnchored.insetBlockStart,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const position = { ...EMPTY_POSITION };
|
|
101
|
+
|
|
102
|
+
if ( usesInlineStart( corner ) ) {
|
|
103
|
+
position.insetInlineStart = startAnchored.insetInlineStart;
|
|
104
|
+
position.insetBlockEnd = viewport.height - startAnchored.insetBlockStart - size.blockSize;
|
|
105
|
+
} else if ( usesBlockStart( corner ) ) {
|
|
106
|
+
position.insetInlineEnd = viewport.width - startAnchored.insetInlineStart - size.inlineSize;
|
|
107
|
+
position.insetBlockStart = startAnchored.insetBlockStart;
|
|
108
|
+
} else {
|
|
109
|
+
position.insetInlineEnd = viewport.width - startAnchored.insetInlineStart - size.inlineSize;
|
|
110
|
+
position.insetBlockEnd = viewport.height - startAnchored.insetBlockStart - size.blockSize;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return position;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function activePositionChanged( corner: PanelCorner, before: LogicalPosition, after: LogicalPosition ): boolean {
|
|
117
|
+
const [ inlineKey, blockKey ] = getActiveInsetKeys( corner );
|
|
118
|
+
|
|
119
|
+
return before[ inlineKey ] !== after[ inlineKey ] || before[ blockKey ] !== after[ blockKey ];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type DragBounds = {
|
|
123
|
+
minInlineStart: number;
|
|
124
|
+
maxInlineStart: number;
|
|
125
|
+
minInlineEnd: number;
|
|
126
|
+
maxInlineEnd: number;
|
|
127
|
+
minBlockStart: number;
|
|
128
|
+
maxBlockStart: number;
|
|
129
|
+
minBlockEnd: number;
|
|
130
|
+
maxBlockEnd: number;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function getDragBounds(
|
|
134
|
+
size: LogicalSize,
|
|
135
|
+
viewport: { width: number; height: number },
|
|
136
|
+
sidePanelInlineSize: number,
|
|
137
|
+
appBarHeight: number
|
|
138
|
+
): DragBounds {
|
|
139
|
+
return {
|
|
140
|
+
minInlineStart: sidePanelInlineSize,
|
|
141
|
+
maxInlineStart: viewport.width - size.inlineSize,
|
|
142
|
+
minInlineEnd: 0,
|
|
143
|
+
maxInlineEnd: viewport.width - sidePanelInlineSize - size.inlineSize,
|
|
144
|
+
minBlockStart: appBarHeight,
|
|
145
|
+
maxBlockStart: viewport.height - size.blockSize,
|
|
146
|
+
minBlockEnd: 0,
|
|
147
|
+
maxBlockEnd: viewport.height - appBarHeight - size.blockSize,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type LogicalPosition } from '../types';
|
|
2
|
+
import { clamp } from './clamp';
|
|
3
|
+
import {
|
|
4
|
+
type DragBounds,
|
|
5
|
+
getActiveInsetKeys,
|
|
6
|
+
type PanelCorner,
|
|
7
|
+
usesBlockStart,
|
|
8
|
+
usesInlineStart,
|
|
9
|
+
} from './corner-position';
|
|
10
|
+
|
|
11
|
+
export type { DragBounds } from './corner-position';
|
|
12
|
+
|
|
13
|
+
export type PhysicalDelta = { dx: number; dy: number };
|
|
14
|
+
|
|
15
|
+
export type LogicalDelta = { inlineDelta: number; blockDelta: number };
|
|
16
|
+
|
|
17
|
+
export function physicalToLogicalDelta( delta: PhysicalDelta, isRtl: boolean ): LogicalDelta {
|
|
18
|
+
return {
|
|
19
|
+
inlineDelta: isRtl ? -delta.dx : delta.dx,
|
|
20
|
+
blockDelta: delta.dy,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyDragDelta(
|
|
25
|
+
corner: PanelCorner,
|
|
26
|
+
position: LogicalPosition,
|
|
27
|
+
delta: LogicalDelta,
|
|
28
|
+
bounds: DragBounds
|
|
29
|
+
): LogicalPosition {
|
|
30
|
+
const [ inlineKey, blockKey ] = getActiveInsetKeys( corner );
|
|
31
|
+
const inlineDelta = usesInlineStart( corner ) ? delta.inlineDelta : -delta.inlineDelta;
|
|
32
|
+
const blockDelta = usesBlockStart( corner ) ? delta.blockDelta : -delta.blockDelta;
|
|
33
|
+
|
|
34
|
+
const inlineBounds =
|
|
35
|
+
inlineKey === 'insetInlineStart'
|
|
36
|
+
? { min: bounds.minInlineStart, max: bounds.maxInlineStart }
|
|
37
|
+
: { min: bounds.minInlineEnd, max: bounds.maxInlineEnd };
|
|
38
|
+
|
|
39
|
+
const blockBounds =
|
|
40
|
+
blockKey === 'insetBlockStart'
|
|
41
|
+
? { min: bounds.minBlockStart, max: bounds.maxBlockStart }
|
|
42
|
+
: { min: bounds.minBlockEnd, max: bounds.maxBlockEnd };
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...position,
|
|
46
|
+
[ inlineKey ]: clamp( position[ inlineKey ] + inlineDelta, inlineBounds.min, inlineBounds.max ),
|
|
47
|
+
[ blockKey ]: clamp( position[ blockKey ] + blockDelta, blockBounds.min, blockBounds.max ),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type LogicalPosition, type LogicalSize } from '../types';
|
|
2
|
+
import { clamp } from './clamp';
|
|
3
|
+
import { type PanelCorner } from './corner-position';
|
|
4
|
+
|
|
5
|
+
export type ResizeEdge = 'inline-start' | 'inline-end' | 'block-start' | 'block-end';
|
|
6
|
+
|
|
7
|
+
export type ResizeCorner = PanelCorner;
|
|
8
|
+
|
|
9
|
+
export type ResizeDirection = ResizeEdge | PanelCorner;
|
|
10
|
+
|
|
11
|
+
export type ResizeBounds = {
|
|
12
|
+
minInlineSize: number;
|
|
13
|
+
maxInlineSize: number;
|
|
14
|
+
minBlockSize: number;
|
|
15
|
+
maxBlockSize: number;
|
|
16
|
+
minInlineStart: number;
|
|
17
|
+
minBlockStart: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function applyInlineEndResize( size: LogicalSize, inlineDelta: number, bounds: ResizeBounds ): LogicalSize {
|
|
21
|
+
return {
|
|
22
|
+
inlineSize: clamp( size.inlineSize + inlineDelta, bounds.minInlineSize, bounds.maxInlineSize ),
|
|
23
|
+
blockSize: size.blockSize,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function applyBlockEndResize( size: LogicalSize, blockDelta: number, bounds: ResizeBounds ): LogicalSize {
|
|
28
|
+
return {
|
|
29
|
+
inlineSize: size.inlineSize,
|
|
30
|
+
blockSize: clamp( size.blockSize + blockDelta, bounds.minBlockSize, bounds.maxBlockSize ),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyInlineStartResize(
|
|
35
|
+
position: LogicalPosition,
|
|
36
|
+
size: LogicalSize,
|
|
37
|
+
inlineDelta: number,
|
|
38
|
+
bounds: ResizeBounds
|
|
39
|
+
): { position: LogicalPosition; size: LogicalSize } {
|
|
40
|
+
const anchorInlineEnd = position.insetInlineStart + size.inlineSize;
|
|
41
|
+
const lowBound = Math.max( bounds.minInlineStart, anchorInlineEnd - bounds.maxInlineSize );
|
|
42
|
+
const highBound = anchorInlineEnd - bounds.minInlineSize;
|
|
43
|
+
const nextInlineStart = clamp( position.insetInlineStart + inlineDelta, lowBound, highBound );
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
position: { ...position, insetInlineStart: nextInlineStart },
|
|
47
|
+
size: { ...size, inlineSize: anchorInlineEnd - nextInlineStart },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applyBlockStartResize(
|
|
52
|
+
position: LogicalPosition,
|
|
53
|
+
size: LogicalSize,
|
|
54
|
+
blockDelta: number,
|
|
55
|
+
bounds: ResizeBounds
|
|
56
|
+
): { position: LogicalPosition; size: LogicalSize } {
|
|
57
|
+
const anchorBlockEnd = position.insetBlockStart + size.blockSize;
|
|
58
|
+
const lowBound = Math.max( bounds.minBlockStart, anchorBlockEnd - bounds.maxBlockSize );
|
|
59
|
+
const highBound = anchorBlockEnd - bounds.minBlockSize;
|
|
60
|
+
const nextBlockStart = clamp( position.insetBlockStart + blockDelta, lowBound, highBound );
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
position: { ...position, insetBlockStart: nextBlockStart },
|
|
64
|
+
size: { ...size, blockSize: anchorBlockEnd - nextBlockStart },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function applyResize(
|
|
69
|
+
direction: ResizeDirection,
|
|
70
|
+
position: LogicalPosition,
|
|
71
|
+
size: LogicalSize,
|
|
72
|
+
inlineDelta: number,
|
|
73
|
+
blockDelta: number,
|
|
74
|
+
bounds: ResizeBounds
|
|
75
|
+
): { position: LogicalPosition; size: LogicalSize } {
|
|
76
|
+
if ( direction === 'inline-end' ) {
|
|
77
|
+
return { position, size: applyInlineEndResize( size, inlineDelta, bounds ) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if ( direction === 'block-end' ) {
|
|
81
|
+
return { position, size: applyBlockEndResize( size, blockDelta, bounds ) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ( direction === 'inline-start' ) {
|
|
85
|
+
return applyInlineStartResize( position, size, inlineDelta, bounds );
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ( direction === 'block-start' ) {
|
|
89
|
+
return applyBlockStartResize( position, size, blockDelta, bounds );
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let next = { position, size };
|
|
93
|
+
|
|
94
|
+
if ( direction.includes( 'inline-start' ) ) {
|
|
95
|
+
next = applyInlineStartResize( next.position, next.size, inlineDelta, bounds );
|
|
96
|
+
} else {
|
|
97
|
+
next = { position: next.position, size: applyInlineEndResize( next.size, inlineDelta, bounds ) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ( direction.includes( 'block-start' ) ) {
|
|
101
|
+
next = applyBlockStartResize( next.position, next.size, blockDelta, bounds );
|
|
102
|
+
} else {
|
|
103
|
+
next = { position: next.position, size: applyBlockEndResize( next.size, blockDelta, bounds ) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return next;
|
|
107
|
+
}
|