@flowgram-vue/panel-manager-plugin 0.2.0

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { inject, onBeforeUnmount, shallowRef, type ShallowRef } from 'vue';
7
+ import type { StoreApi } from 'zustand';
8
+ import { shallow } from 'zustand/shallow';
9
+
10
+ import { PanelEntity, PanelEntityState } from '../services/panel-factory';
11
+ import { PanelContextKey } from '../contexts';
12
+
13
+ export const usePanel = (): PanelEntity => {
14
+ const panel = inject(PanelContextKey);
15
+ if (!panel) {
16
+ throw new Error('usePanel must be used within a PanelContext provider');
17
+ }
18
+ return panel;
19
+ };
20
+
21
+ export const usePanelStore = <T>(selector: (s: PanelEntityState) => T): ShallowRef<T> => {
22
+ const panel = usePanel();
23
+ const selected = shallowRef(selector(panel.store.getState()));
24
+ const unsubscribe = (panel.store as StoreApi<PanelEntityState>).subscribe((state) => {
25
+ const next = selector(state);
26
+ if (!shallow(selected.value, next)) {
27
+ selected.value = next;
28
+ }
29
+ });
30
+ onBeforeUnmount(unsubscribe);
31
+ return selected;
32
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { InjectionKey } from 'vue';
7
+
8
+ import type { PanelEntity } from './services/panel-factory';
9
+
10
+ export const PanelContextKey: InjectionKey<PanelEntity> = Symbol('PanelContext');
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { definePluginCreator } from '@flowgram-vue/core';
7
+
8
+ import {
9
+ PanelEntityFactory,
10
+ PanelEntity,
11
+ PanelEntityFactoryConstant,
12
+ PanelEntityConfigConstant,
13
+ } from './services/panel-factory';
14
+ import { defineConfig } from './services/panel-config';
15
+ import {
16
+ PanelManager,
17
+ PanelManagerConfig,
18
+ PanelLayer,
19
+ PanelRestore,
20
+ PanelRestoreImpl,
21
+ } from './services';
22
+
23
+ export const createPanelManagerPlugin = definePluginCreator<Partial<PanelManagerConfig>>({
24
+ onBind: ({ bind }, opt) => {
25
+ bind(PanelManager).to(PanelManager).inSingletonScope();
26
+ bind(PanelRestore).to(PanelRestoreImpl).inSingletonScope();
27
+ bind(PanelManagerConfig).toConstantValue(defineConfig(opt));
28
+ bind(PanelEntityFactory).toFactory(
29
+ (context) =>
30
+ ({
31
+ factory,
32
+ config,
33
+ }: {
34
+ factory: PanelEntityFactoryConstant;
35
+ config: PanelEntityConfigConstant;
36
+ }) => {
37
+ const container = context.container.createChild();
38
+ container.bind(PanelEntityFactoryConstant).toConstantValue(factory);
39
+ container.bind(PanelEntityConfigConstant).toConstantValue(config);
40
+ const panel = container.resolve(PanelEntity);
41
+ panel.init();
42
+ return panel;
43
+ }
44
+ );
45
+ },
46
+ onInit(ctx) {
47
+ ctx.playground.registerLayer(PanelLayer);
48
+ const panelManager = ctx.container.get<PanelManager>(PanelManager);
49
+ panelManager.init();
50
+ },
51
+ });
package/src/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ declare module '*.vue' {
7
+ import type { DefineComponent } from 'vue';
8
+ const component: DefineComponent<object, object, unknown>;
9
+ export default component;
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /** create plugin function */
7
+ export { createPanelManagerPlugin } from './create-panel-manager-plugin';
8
+
9
+ /** services */
10
+ export { PanelManager, PanelRestore, type PanelManagerConfig } from './services';
11
+
12
+ /** vue composables */
13
+ export { usePanelManager } from './composables/use-panel-manager';
14
+ export { usePanel } from './composables/use-panel';
15
+
16
+ export { DockedPanelLayer, type DockedPanelLayerProps } from './components/panel-layer';
17
+ export { ResizeBar } from './components/resize-bar';
18
+
19
+ /** types */
20
+ export type { Area, PanelFactory } from './types';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export { PanelManager } from './panel-manager';
7
+ export { PanelManagerConfig } from './panel-config';
8
+ export { PanelLayer } from './panel-layer';
9
+ export { PanelRestore, PanelRestoreImpl } from './panel-restore';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { VNode } from 'vue';
7
+ import { PluginContext } from '@flowgram-vue/core';
8
+
9
+ import type { PanelFactory, PanelConfig } from '../types';
10
+ import { ResizeBar } from '../components/resize-bar';
11
+ import type { PanelLayerProps } from '../components/panel-layer';
12
+
13
+ export interface PanelManagerConfig {
14
+ factories: PanelFactory<any>[];
15
+ right: PanelConfig;
16
+ bottom: PanelConfig;
17
+ dockedRight: PanelConfig;
18
+ dockedBottom: PanelConfig;
19
+ /** Resizable, and multi-panel options mutually exclusive */
20
+ autoResize: boolean;
21
+ layerProps: PanelLayerProps;
22
+ resizeBarRender: ({
23
+ size,
24
+ }: {
25
+ size: number;
26
+ direction?: 'vertical' | 'horizontal';
27
+ onResize: (size: number) => void;
28
+ }) => VNode | null;
29
+ getPopupContainer: (ctx: PluginContext) => HTMLElement; // default playground.node.parentElement
30
+ }
31
+
32
+ export const PanelManagerConfig = Symbol('PanelManagerConfig');
33
+
34
+ export const defineConfig = (config: Partial<PanelManagerConfig>) => {
35
+ const defaultConfig: PanelManagerConfig = {
36
+ right: {
37
+ max: 1,
38
+ },
39
+ bottom: {
40
+ max: 1,
41
+ },
42
+ dockedRight: {
43
+ max: 1,
44
+ },
45
+ dockedBottom: {
46
+ max: 1,
47
+ },
48
+ factories: [],
49
+ autoResize: true,
50
+ layerProps: {},
51
+ resizeBarRender: ResizeBar,
52
+ getPopupContainer: (ctx: PluginContext) => ctx.playground.node.parentNode as HTMLElement,
53
+ };
54
+ return {
55
+ ...defaultConfig,
56
+ ...config,
57
+ };
58
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { createStore, StoreApi } from 'zustand/vanilla';
7
+ import { nanoid } from 'nanoid';
8
+ import { inject, injectable } from 'inversify';
9
+ import type { VNode } from 'vue';
10
+
11
+ import type { PanelFactory, PanelEntityConfig, Area } from '../types';
12
+ import { PanelRestore } from './panel-restore';
13
+ import { PanelManagerConfig } from './panel-config';
14
+ import { merge } from '../utils';
15
+
16
+ export const PanelEntityFactory = Symbol('PanelEntityFactory');
17
+ export type PanelEntityFactory = (options: {
18
+ factory: PanelEntityFactoryConstant;
19
+ config: PanelEntityConfigConstant;
20
+ }) => PanelEntity;
21
+
22
+ export const PanelEntityFactoryConstant = Symbol('PanelEntityFactoryConstant');
23
+ export type PanelEntityFactoryConstant = PanelFactory<any>;
24
+ export const PanelEntityConfigConstant = Symbol('PanelEntityConfigConstant');
25
+ export type PanelEntityConfigConstant = PanelEntityConfig<any> & {
26
+ area: Area;
27
+ };
28
+
29
+ const PANEL_SIZE_DEFAULT = 400;
30
+
31
+ export interface PanelEntityState {
32
+ size: number;
33
+ fullscreen: boolean;
34
+ visible: boolean;
35
+ }
36
+
37
+ @injectable()
38
+ export class PanelEntity {
39
+ @inject(PanelRestore) restore: PanelRestore;
40
+
41
+ /** 面板工厂 */
42
+ @inject(PanelEntityFactoryConstant) public factory: PanelEntityFactoryConstant;
43
+
44
+ @inject(PanelEntityConfigConstant) public config: PanelEntityConfigConstant;
45
+
46
+ @inject(PanelManagerConfig) readonly globalConfig: PanelManagerConfig;
47
+
48
+ private initialized = false;
49
+
50
+ /** 实例唯一标识 */
51
+ id: string = nanoid();
52
+
53
+ /** 渲染缓存 */
54
+ node: VNode | null = null;
55
+
56
+ store: StoreApi<PanelEntityState>;
57
+
58
+ get area() {
59
+ return this.config.area;
60
+ }
61
+
62
+ get mode() {
63
+ return this.config.area.startsWith('docked') ? 'docked' : 'floating';
64
+ }
65
+
66
+ get key() {
67
+ return this.factory.key;
68
+ }
69
+
70
+ get renderer() {
71
+ if (!this.node) {
72
+ this.node = this.factory.render(this.config.props);
73
+ }
74
+ return this.node;
75
+ }
76
+
77
+ get fullscreen() {
78
+ return this.store.getState().fullscreen;
79
+ }
80
+
81
+ set fullscreen(next: boolean) {
82
+ this.store.setState({ fullscreen: next });
83
+ }
84
+
85
+ get resizable() {
86
+ if (this.fullscreen) {
87
+ return false;
88
+ }
89
+ return this.factory.resize !== undefined ? this.factory.resize : this.globalConfig.autoResize;
90
+ }
91
+
92
+ get keepDOM() {
93
+ return this.factory.keepDOM;
94
+ }
95
+
96
+ get visible() {
97
+ return this.store.getState().visible;
98
+ }
99
+
100
+ set visible(next: boolean) {
101
+ this.store.setState({ visible: next });
102
+ }
103
+
104
+ get layer() {
105
+ return document.querySelector(
106
+ this.mode ? '.gedit-flow-panel-layer-wrap-docked' : '.gedit-flow-panel-layer-wrap-floating'
107
+ );
108
+ }
109
+
110
+ init() {
111
+ if (this.initialized) {
112
+ return;
113
+ }
114
+ this.initialized = true;
115
+ const cache = this.restore.restore<PanelEntityState>(this.key);
116
+
117
+ const initialState = merge<PanelEntityState>(
118
+ {
119
+ size: this.config.defaultSize,
120
+ fullscreen: this.config.fullscreen,
121
+ },
122
+ cache ? cache : {},
123
+ {
124
+ size: this.factory.defaultSize || PANEL_SIZE_DEFAULT,
125
+ fullscreen: this.factory.fullscreen || false,
126
+ ...(this.factory.keepDOM ? { visible: true } : {}),
127
+ }
128
+ );
129
+
130
+ this.store = createStore<PanelEntityState>(() => initialState);
131
+ }
132
+
133
+ mergeState() {}
134
+
135
+ dispose() {
136
+ this.restore.store(this.key, this.store.getState());
137
+ }
138
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { defineComponent, h, provide, Teleport, type VNode } from 'vue';
7
+ import { injectable, inject } from 'inversify';
8
+ import { domUtils, Disposable } from '@flowgram-vue/utils';
9
+ import {
10
+ Layer,
11
+ Playground,
12
+ PlaygroundContainerFactory,
13
+ PlaygroundVueContainerKey,
14
+ PlaygroundVueRefKey,
15
+ PluginContext,
16
+ } from '@flowgram-vue/core';
17
+
18
+ import { PanelLayer as PanelLayerComp } from '../components/panel-layer';
19
+ import { PanelManagerConfig } from './panel-config';
20
+
21
+ @injectable()
22
+ export class PanelLayer extends Layer {
23
+ @inject(PanelManagerConfig) private readonly panelConfig: PanelManagerConfig;
24
+
25
+ @inject(PluginContext) private readonly pluginContext: PluginContext;
26
+
27
+ @inject(PlaygroundContainerFactory)
28
+ private readonly playgroundContainer: PlaygroundContainerFactory;
29
+
30
+ readonly panelRoot = domUtils.createDivWithClass('gedit-flow-panel-layer');
31
+
32
+ layout: VNode | null = null;
33
+
34
+ onReady(): void {
35
+ this.panelConfig.getPopupContainer(this.pluginContext).appendChild(this.panelRoot);
36
+ this.toDispose.push(
37
+ Disposable.create(() => {
38
+ this.panelRoot.remove();
39
+ })
40
+ );
41
+ const commonStyle = {
42
+ pointerEvents: 'none',
43
+ width: '100%',
44
+ height: '100%',
45
+ position: 'absolute',
46
+ left: 0,
47
+ top: 0,
48
+ zIndex: 100,
49
+ };
50
+ domUtils.setStyle(this.panelRoot, commonStyle);
51
+ }
52
+
53
+ render(): VNode {
54
+ if (!this.layout) {
55
+ const { children, ...layoutProps } = this.panelConfig.layerProps;
56
+ const playgroundContainer = this.playgroundContainer;
57
+ const panelRoot = this.panelRoot;
58
+ const LayerProvide = defineComponent({
59
+ name: 'PanelLayerProvide',
60
+ setup() {
61
+ provide(PlaygroundVueContainerKey, playgroundContainer as any);
62
+ try {
63
+ provide(PlaygroundVueRefKey, playgroundContainer.get(Playground));
64
+ } catch {
65
+ // ignore
66
+ }
67
+ return () =>
68
+ h(Teleport, { to: panelRoot }, [
69
+ h(PanelLayerComp, layoutProps as any, () => children as any),
70
+ ]);
71
+ },
72
+ });
73
+ this.layout = h(LayerProvide);
74
+ }
75
+ return this.layout;
76
+ }
77
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable, inject } from 'inversify';
7
+ import { Emitter } from '@flowgram-vue/utils';
8
+
9
+ import { PanelManagerConfig } from './panel-config';
10
+ import type { Area, PanelEntityConfig, PanelFactory } from '../types';
11
+ import { PanelEntity, PanelEntityFactory } from './panel-factory';
12
+
13
+ @injectable()
14
+ export class PanelManager {
15
+ @inject(PanelManagerConfig) readonly config: PanelManagerConfig;
16
+
17
+ @inject(PanelEntityFactory) readonly createPanel: PanelEntityFactory;
18
+
19
+ readonly panelRegistry = new Map<string, PanelFactory<any>>();
20
+
21
+ private panels = new Map<string, PanelEntity>();
22
+
23
+ private onPanelsChangeEvent = new Emitter<void>();
24
+
25
+ public onPanelsChange = this.onPanelsChangeEvent.event;
26
+
27
+ init() {
28
+ this.config.factories.forEach((factory) => this.register(factory));
29
+ }
30
+
31
+ /** registry panel factory */
32
+ register<T extends any>(factory: PanelFactory<T>) {
33
+ this.panelRegistry.set(factory.key, factory);
34
+ }
35
+
36
+ /** open panel */
37
+ public open(key: string, area: Area = 'right', options?: PanelEntityConfig) {
38
+ const factory = this.panelRegistry.get(key);
39
+ if (!factory) {
40
+ return;
41
+ }
42
+
43
+ const sameKeyPanels = this.getPanels(area).filter((p) => p.key === key);
44
+
45
+ if (factory.keepDOM && sameKeyPanels.length) {
46
+ const [panel] = sameKeyPanels;
47
+ // move to last
48
+ this.panels.delete(panel.id);
49
+ this.panels.set(panel.id, panel);
50
+ panel.visible = true;
51
+ } else {
52
+ if (!factory.allowDuplicates && sameKeyPanels.length) {
53
+ sameKeyPanels.forEach((p) => this.remove(p.id));
54
+ }
55
+ const panel = this.createPanel({
56
+ factory,
57
+ config: {
58
+ area,
59
+ ...options,
60
+ },
61
+ });
62
+
63
+ this.panels.set(panel.id, panel);
64
+ }
65
+
66
+ this.trim(area);
67
+ this.onPanelsChangeEvent.fire();
68
+ }
69
+
70
+ /** close panel */
71
+ public close(key?: string) {
72
+ const panels = this.getPanels();
73
+ const closedPanels = key ? panels.filter((p) => p.key === key) : panels;
74
+ closedPanels.forEach((panel) => {
75
+ this.remove(panel.id);
76
+ });
77
+ this.onPanelsChangeEvent.fire();
78
+ }
79
+
80
+ private trim(area: Area) {
81
+ /** 1. general panel; 2. keepDOM visible panel */
82
+ const panels = this.getPanels(area).filter((p) => !p.keepDOM || p.visible);
83
+ const areaConfig = this.getAreaConfig(area);
84
+ while (panels.length > areaConfig.max) {
85
+ const removed = panels.shift();
86
+ if (removed) {
87
+ this.remove(removed.id);
88
+ }
89
+ }
90
+ }
91
+
92
+ private remove(id: string) {
93
+ const panel = this.panels.get(id);
94
+ if (!panel) {
95
+ return;
96
+ }
97
+ if (panel.keepDOM) {
98
+ panel.visible = false;
99
+ } else {
100
+ panel.dispose();
101
+ this.panels.delete(id);
102
+ }
103
+ }
104
+
105
+ getPanels(area?: Area) {
106
+ const panels: PanelEntity[] = [];
107
+ this.panels.forEach((panel) => {
108
+ if (!area || panel.area === area) {
109
+ panels.push(panel);
110
+ }
111
+ });
112
+ return panels;
113
+ }
114
+
115
+ getAreaConfig(area: Area) {
116
+ switch (area) {
117
+ case 'docked-bottom':
118
+ return this.config.dockedBottom;
119
+ case 'docked-right':
120
+ return this.config.dockedRight;
121
+ case 'bottom':
122
+ return this.config.bottom;
123
+ case 'right':
124
+ default:
125
+ return this.config.right;
126
+ }
127
+ }
128
+
129
+ dispose() {
130
+ this.onPanelsChangeEvent.dispose();
131
+ }
132
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+
8
+ export const PanelRestore = Symbol('PanelRestore');
9
+ export interface PanelRestore {
10
+ store: (k: string, v: any) => void;
11
+ restore: <T>(k: string) => T | undefined;
12
+ }
13
+
14
+ @injectable()
15
+ export class PanelRestoreImpl implements PanelRestore {
16
+ map = new Map<string, any>();
17
+
18
+ store(k: string, v: any) {
19
+ this.map.set(k, v);
20
+ }
21
+
22
+ restore<T>(k: string): T | undefined {
23
+ return this.map.get(k) as T;
24
+ }
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import type { CSSProperties, VNode } from 'vue';
7
+
8
+ export type Area = 'right' | 'bottom' | 'docked-right' | 'docked-bottom';
9
+
10
+ export interface PanelConfig {
11
+ /** max panel */
12
+ max: number;
13
+ }
14
+
15
+ export interface PanelFactory<T extends any> {
16
+ key: string;
17
+ defaultSize: number;
18
+ fullscreen?: boolean;
19
+ maxSize?: number;
20
+ minSize?: number;
21
+ style?: CSSProperties;
22
+ /** Allows multiple panels with the same key to be rendered simultaneously */
23
+ allowDuplicates?: boolean;
24
+ resize?: boolean;
25
+ keepDOM?: boolean;
26
+ render: (props: T) => VNode | null;
27
+ }
28
+
29
+ export interface PanelEntityConfig<T extends any = any> {
30
+ defaultSize?: number;
31
+ fullscreen?: boolean;
32
+ style?: CSSProperties;
33
+ props?: T;
34
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export const merge = <T>(...objs: Partial<T>[]) => {
7
+ const result: any = {};
8
+
9
+ for (const obj of objs) {
10
+ if (!obj || typeof obj !== 'object') continue;
11
+
12
+ for (const key of Object.keys(obj)) {
13
+ const value = (obj as any)[key];
14
+
15
+ if (result[key] === undefined) {
16
+ result[key] = value;
17
+ }
18
+ }
19
+ }
20
+
21
+ return result as T;
22
+ };