mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-15 15:51:07 +08:00
feat(editor): 扩展 customContentMenu 支持 getTarget 获取右键目标
为数据源与代码块面板的右键菜单传入 getTarget 回调,便于业务在自定义菜单 handler 中读取当前节点 id 与原始数据。
This commit is contained in:
parent
734e6a8ee6
commit
bbe73aae64
@ -1365,13 +1365,34 @@ const guidesOptions = {
|
|||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
|
|
||||||
用于自定义组件树与画布的右键菜单
|
用于自定义组件树(图层)、画布、数据源、代码块的右键菜单
|
||||||
|
|
||||||
该函数会在显示右键菜单前被调用,接收默认菜单项作为参数,返回最终显示的菜单项
|
该函数会在显示右键菜单前被调用,接收默认菜单项与菜单类型作为参数,返回最终显示的菜单项
|
||||||
|
|
||||||
|
第二个参数 `type` 用于区分菜单来源:`'layer'`(图层)、`'viewer'`(画布)、`'data-source'`(数据源)、`'code-block'`(代码块)
|
||||||
|
|
||||||
|
第三个参数 `getTarget` 仅在**数据源**与**代码块**面板下传入,用于读取当前右键目标(节点 `id` 与原始数据);图层 / 画布不会传入该参数
|
||||||
|
|
||||||
- **默认值:** `(menus) => menus`
|
- **默认值:** `(menus) => menus`
|
||||||
|
|
||||||
- **类型:** `(menus: (MenuButton | MenuComponent)[], data: { node?: MNode; page?: MPage; parent?: MContainer; stage?: StageCore }) => (MenuButton | MenuComponent)[]`
|
- **类型:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ContentMenuTarget {
|
||||||
|
/** 目标 ID */
|
||||||
|
id: string;
|
||||||
|
/** 原始节点数据(树节点等) */
|
||||||
|
data?: TreeNodeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block';
|
||||||
|
|
||||||
|
type CustomContentMenuFunction = (
|
||||||
|
menus: (MenuButton | MenuComponent)[],
|
||||||
|
type: ContentMenuType,
|
||||||
|
getTarget?: () => ContentMenuTarget | null,
|
||||||
|
) => (MenuButton | MenuComponent)[];
|
||||||
|
```
|
||||||
|
|
||||||
- **示例:**
|
- **示例:**
|
||||||
|
|
||||||
@ -1381,20 +1402,24 @@ const guidesOptions = {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const customContentMenu = (menus, { node }) => {
|
const customContentMenu = (menus, type, getTarget) => {
|
||||||
// 为特定类型的组件添加自定义菜单
|
// 为数据源右键菜单追加自定义菜单
|
||||||
if (node?.type === 'container') {
|
if (type === 'data-source') {
|
||||||
menus.push({
|
menus.push({
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '清空容器',
|
text: '查看详情',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
// 清空容器的逻辑
|
// getTarget 仅在数据源 / 代码块面板下传入
|
||||||
|
const target = getTarget?.();
|
||||||
|
if (target) {
|
||||||
|
console.log(target.id, target.data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可以过滤掉某些菜单项
|
// 可以过滤掉某些菜单项
|
||||||
return menus.filter(menu => menu.text !== '删除');
|
return menus.filter((menu) => menu.text !== '删除');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|||||||
@ -121,8 +121,11 @@ const {
|
|||||||
nodeContentMenuHandler,
|
nodeContentMenuHandler,
|
||||||
menuData: contentMenuData,
|
menuData: contentMenuData,
|
||||||
contentMenuHideHandler,
|
contentMenuHideHandler,
|
||||||
|
getTarget,
|
||||||
} = useContentMenu((id: string) => {
|
} = useContentMenu((id: string) => {
|
||||||
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
||||||
});
|
});
|
||||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||||
|
props.customContentMenu(contentMenuData, 'code-block', getTarget),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
|
|||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
||||||
|
|
||||||
export const useContentMenu = (deleteCode: (id: string) => void) => {
|
export const useContentMenu = (deleteCode: (id: string) => void) => {
|
||||||
const eventBus = inject<EventBus>('eventBus');
|
const eventBus = inject<EventBus>('eventBus');
|
||||||
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
||||||
|
|
||||||
let selectId = '';
|
let selectId = '';
|
||||||
|
let selectData: TreeNodeData | null = null;
|
||||||
|
|
||||||
|
const getTarget = (): ContentMenuTarget | null => {
|
||||||
|
if (!selectId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { id: selectId, data: selectData ?? undefined };
|
||||||
|
};
|
||||||
|
|
||||||
const menuData: (MenuButton | MenuComponent)[] = [
|
const menuData: (MenuButton | MenuComponent)[] = [
|
||||||
{
|
{
|
||||||
@ -65,19 +73,23 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
|
|||||||
menuRef.value?.show(event);
|
menuRef.value?.show(event);
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
selectId = `${data.id}`;
|
selectId = `${data.id}`;
|
||||||
|
selectData = data;
|
||||||
} else {
|
} else {
|
||||||
selectId = '';
|
selectId = '';
|
||||||
|
selectData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentMenuHideHandler = () => {
|
const contentMenuHideHandler = () => {
|
||||||
selectId = '';
|
selectId = '';
|
||||||
|
selectData = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
menuData,
|
menuData,
|
||||||
nodeContentMenuHandler,
|
nodeContentMenuHandler,
|
||||||
contentMenuHideHandler,
|
contentMenuHideHandler,
|
||||||
|
getTarget,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -171,8 +171,8 @@ eventBus?.on('remove-data-source', (id: string) => {
|
|||||||
removeHandler(id);
|
removeHandler(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler } = useContentMenu();
|
const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler, getTarget } = useContentMenu();
|
||||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||||
props.customContentMenu(contentMenuData, 'data-source'),
|
props.customContentMenu(contentMenuData, 'data-source', getTarget),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
|
|||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
||||||
|
|
||||||
export const useContentMenu = () => {
|
export const useContentMenu = () => {
|
||||||
const eventBus = inject<EventBus>('eventBus');
|
const eventBus = inject<EventBus>('eventBus');
|
||||||
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
||||||
|
|
||||||
let selectId = '';
|
let selectId = '';
|
||||||
|
let selectData: TreeNodeData | null = null;
|
||||||
|
|
||||||
|
const getTarget = (): ContentMenuTarget | null => {
|
||||||
|
if (!selectId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { id: selectId, data: selectData ?? undefined };
|
||||||
|
};
|
||||||
|
|
||||||
const menuData: (MenuButton | MenuComponent)[] = [
|
const menuData: (MenuButton | MenuComponent)[] = [
|
||||||
{
|
{
|
||||||
@ -63,19 +71,23 @@ export const useContentMenu = () => {
|
|||||||
menuRef.value?.show(event);
|
menuRef.value?.show(event);
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
selectId = `${data.id}`;
|
selectId = `${data.id}`;
|
||||||
|
selectData = data;
|
||||||
} else {
|
} else {
|
||||||
selectId = '';
|
selectId = '';
|
||||||
|
selectData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentMenuHideHandler = () => {
|
const contentMenuHideHandler = () => {
|
||||||
selectId = '';
|
selectId = '';
|
||||||
|
selectData = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
menuData,
|
menuData,
|
||||||
nodeContentMenuHandler,
|
nodeContentMenuHandler,
|
||||||
contentMenuHideHandler,
|
contentMenuHideHandler,
|
||||||
|
getTarget,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1207,9 +1207,24 @@ export interface PageBarSortOptions extends PartSortableOptions {
|
|||||||
}
|
}
|
||||||
// #endregion PageBarSortOptions
|
// #endregion PageBarSortOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键菜单当前目标(侧栏树节点等)。
|
||||||
|
* 数据源 / 代码块面板通过 `customContentMenu` 的 `getTarget` 暴露,业务在 handler 内自行读取。
|
||||||
|
*/
|
||||||
|
export interface ContentMenuTarget {
|
||||||
|
/** 目标 ID */
|
||||||
|
id: string;
|
||||||
|
/** 原始节点数据(树节点等) */
|
||||||
|
data?: TreeNodeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block';
|
||||||
|
|
||||||
export type CustomContentMenuFunction = (
|
export type CustomContentMenuFunction = (
|
||||||
menus: (MenuButton | MenuComponent)[],
|
menus: (MenuButton | MenuComponent)[],
|
||||||
type: 'layer' | 'data-source' | 'viewer' | 'code-block',
|
type: ContentMenuType,
|
||||||
|
/** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */
|
||||||
|
getTarget?: () => ContentMenuTarget | null,
|
||||||
) => (MenuButton | MenuComponent)[];
|
) => (MenuButton | MenuComponent)[];
|
||||||
|
|
||||||
export interface EditorEvents {
|
export interface EditorEvents {
|
||||||
|
|||||||
@ -48,6 +48,7 @@ vi.mock('@editor/layouts/sidebar/code-block/useContentMenu', () => ({
|
|||||||
return menuDataState.items;
|
return menuDataState.items;
|
||||||
},
|
},
|
||||||
contentMenuHideHandler,
|
contentMenuHideHandler,
|
||||||
|
getTarget: () => null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -133,4 +133,16 @@ describe('code-block useContentMenu', () => {
|
|||||||
(result.menuData[2] as any).handler({});
|
(result.menuData[2] as any).handler({});
|
||||||
expect(deleteCode).not.toHaveBeenCalled();
|
expect(deleteCode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getTarget 返回当前右键目标', () => {
|
||||||
|
const deleteCode = vi.fn();
|
||||||
|
const { result } = mountHook(deleteCode);
|
||||||
|
expect(result.getTarget()).toBeNull();
|
||||||
|
|
||||||
|
result.nodeContentMenuHandler({ preventDefault: vi.fn() } as any, { type: 'code', id: 'c1', name: 'code1' } as any);
|
||||||
|
expect(result.getTarget()).toEqual({ id: 'c1', data: { type: 'code', id: 'c1', name: 'code1' } });
|
||||||
|
|
||||||
|
result.contentMenuHideHandler();
|
||||||
|
expect(result.getTarget()).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -38,12 +38,14 @@ vi.mock('@editor/hooks/use-data-source-edit', () => ({
|
|||||||
|
|
||||||
const nodeContentMenuHandler = vi.fn();
|
const nodeContentMenuHandler = vi.fn();
|
||||||
const contentMenuHideHandler = vi.fn();
|
const contentMenuHideHandler = vi.fn();
|
||||||
const menuData = ref<any[]>([{ type: 'button', text: 'Edit' }]);
|
const menuData = [{ type: 'button', text: 'Edit' }];
|
||||||
|
const getTarget = vi.fn(() => null);
|
||||||
vi.mock('@editor/layouts/sidebar/data-source/useContentMenu', () => ({
|
vi.mock('@editor/layouts/sidebar/data-source/useContentMenu', () => ({
|
||||||
useContentMenu: () => ({
|
useContentMenu: () => ({
|
||||||
nodeContentMenuHandler,
|
nodeContentMenuHandler,
|
||||||
menuData,
|
menuData,
|
||||||
contentMenuHideHandler,
|
contentMenuHideHandler,
|
||||||
|
getTarget,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -126,4 +126,15 @@ describe('data-source useContentMenu', () => {
|
|||||||
(result.menuData[2] as any).handler({});
|
(result.menuData[2] as any).handler({});
|
||||||
expect(eventBus.emit).not.toHaveBeenCalled();
|
expect(eventBus.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getTarget 返回当前右键目标', () => {
|
||||||
|
const { result } = mountHook();
|
||||||
|
expect(result.getTarget()).toBeNull();
|
||||||
|
|
||||||
|
result.nodeContentMenuHandler({ preventDefault: vi.fn() } as any, { type: 'ds', id: 'd1', name: 'ds1' } as any);
|
||||||
|
expect(result.getTarget()).toEqual({ id: 'd1', data: { type: 'ds', id: 'd1', name: 'ds1' } });
|
||||||
|
|
||||||
|
result.contentMenuHideHandler();
|
||||||
|
expect(result.getTarget()).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user