mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 04:19:05 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 |
@@ -56,7 +56,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.5',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-core": "^1.3.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -1425,3 +1425,22 @@ export function traverseFile(ast: t.File) {
|
||||
imports,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* app.js 入口文件解析
|
||||
* @param ast
|
||||
*/
|
||||
export function traverseEntryFile(ast: t.File) {
|
||||
let appConfig: any;
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const { node } = path;
|
||||
const calleeName = keyNode2value(node.callee) as string;
|
||||
if (calleeName === 'runApp') {
|
||||
appConfig = node2value(node.arguments[0]);
|
||||
path.stop();
|
||||
}
|
||||
},
|
||||
});
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,16 @@ export class Designer {
|
||||
*/
|
||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右键菜单
|
||||
*/
|
||||
_showContextMenu = false;
|
||||
|
||||
/**
|
||||
* 右键菜单在 iframe 上的位置
|
||||
*/
|
||||
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右侧面板
|
||||
*/
|
||||
@@ -136,6 +146,14 @@ export class Designer {
|
||||
return this._addComponentPopoverPosition;
|
||||
}
|
||||
|
||||
get showContextMenu() {
|
||||
return this._showContextMenu;
|
||||
}
|
||||
|
||||
get contextMenuPosition() {
|
||||
return this._contextMenuPosition;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
return this._menuData ?? ([] as MenuDataType);
|
||||
}
|
||||
@@ -178,6 +196,8 @@ export class Designer {
|
||||
_showRightPanel: observable,
|
||||
_showAddComponentPopover: observable,
|
||||
_addComponentPopoverPosition: observable,
|
||||
_showContextMenu: observable,
|
||||
_contextMenuPosition: observable,
|
||||
_menuData: observable,
|
||||
_isPreview: observable,
|
||||
simulator: computed,
|
||||
@@ -189,6 +209,8 @@ export class Designer {
|
||||
showSmartWizard: computed,
|
||||
showAddComponentPopover: computed,
|
||||
addComponentPopoverPosition: computed,
|
||||
showContextMenu: computed,
|
||||
contextMenuPosition: computed,
|
||||
menuData: computed,
|
||||
setSimulator: action,
|
||||
setViewport: action,
|
||||
@@ -199,6 +221,7 @@ export class Designer {
|
||||
toggleSmartWizard: action,
|
||||
toggleIsPreview: action,
|
||||
toggleAddComponentPopover: action,
|
||||
toggleContextMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,4 +287,15 @@ export class Designer {
|
||||
this.workspace.selectSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleContextMenu(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.contextMenuPosition,
|
||||
) {
|
||||
this._showContextMenu = value;
|
||||
this._contextMenuPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const config = traverseEntryFile(this.ast);
|
||||
this.routerType = config?.router?.type;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
@@ -152,6 +153,10 @@ export interface IWorkspace {
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -97,6 +98,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 应用入口模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -329,6 +335,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.AppEntryModule;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
@@ -338,6 +347,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.AppEntryModule:
|
||||
module = new AppEntryModule(this, props);
|
||||
this.appEntryModule = module;
|
||||
break;
|
||||
case FileType.StoreEntryModule:
|
||||
module = new TangoStoreEntryModule(this, props);
|
||||
this.storeEntryModule = module;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.2...@music163/tango-designer@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optimize variable panel scroll ui & onCancel bug ([#166](https://github.com/netease/tango/issues/166)) ([a0ed57f](https://github.com/netease/tango/commit/a0ed57ff4f332bd88749b8b1925bdb29319e4d13))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.1...@music163/tango-designer@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.1",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-sandbox": "^1.0.4",
|
||||
"@music163/tango-setting-form": "^1.2.2",
|
||||
"@music163/tango-ui": "^1.2.1",
|
||||
"@music163/tango-context": "^1.1.2",
|
||||
"@music163/tango-core": "^1.3.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-sandbox": "^1.0.5",
|
||||
"@music163/tango-setting-form": "^1.2.5",
|
||||
"@music163/tango-ui": "^1.3.0",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { getWidget } from '../widgets';
|
||||
import { Menu, MenuProps } from 'antd';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { ISelectedItemData, isString } from '@music163/tango-helpers';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
const contextMenuStyle = css`
|
||||
.ant-dropdown-menu {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-title-content {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-submenu-popup {
|
||||
padding: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParentNodesMenuItem = ({
|
||||
record,
|
||||
onClick,
|
||||
key,
|
||||
}: {
|
||||
record: ISelectedItemData;
|
||||
onClick: () => any;
|
||||
key?: string;
|
||||
}) => {
|
||||
const workspace = useWorkspace();
|
||||
const componentPrototype = workspace.componentPrototypes.get(record.name);
|
||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||
|
||||
const iconRender = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={icon} />
|
||||
) : (
|
||||
<img className="material-icon" src={icon} alt={componentPrototype.name} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
|
||||
{record.name}
|
||||
{!!record.codeId && ` (${record.codeId})`}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const ParentNodesMenu = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const parents = selectSource.first?.parents;
|
||||
|
||||
if (!parents?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key="parentNodes" title="选取父节点">
|
||||
{parents.map((item, index) => (
|
||||
<ParentNodesMenuItem
|
||||
key={item.id}
|
||||
record={item}
|
||||
onClick={() => {
|
||||
selectSource.select({
|
||||
...item,
|
||||
parents: parents.slice(index + 1),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
});
|
||||
|
||||
export interface ContextMenuProps extends MenuProps {
|
||||
/**
|
||||
* 动作列表,默认列出全部
|
||||
*/
|
||||
actions?: Array<string | React.ReactElement>;
|
||||
/**
|
||||
* 是否显示父节点选项
|
||||
*/
|
||||
showParents?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const ContextMenu = observer(
|
||||
({
|
||||
showParents,
|
||||
actions: actionsProp,
|
||||
className,
|
||||
style,
|
||||
menuStyle,
|
||||
...rest
|
||||
}: ContextMenuProps) => {
|
||||
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
|
||||
const menus = actions.map((item) => {
|
||||
if (isString(item)) {
|
||||
const widget = getWidget(['contextMenu', item].join('.'));
|
||||
return widget ? React.createElement(widget) : null;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (showParents) {
|
||||
menus.unshift(<ParentNodesMenu />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
|
||||
<Menu activeKey={null} {...rest} style={menuStyle}>
|
||||
{menus}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './context-menu';
|
||||
export * from './drag-box';
|
||||
export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
|
||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
<Box className="VariableList" width="40%" overflow="auto">
|
||||
{renderHeaderExtra?.(props, state)}
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
@@ -203,7 +203,11 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
zIndex={99999}
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onCancel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
@@ -333,7 +337,14 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
|
||||
<ValueDefine
|
||||
data={activeNode}
|
||||
onSave={(variableKey, code) => {
|
||||
onUpdateStoreVariable(variableKey, code);
|
||||
// 更新当前节点 code
|
||||
setActiveNode((pre) => ({ ...pre, raw: code }));
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
|
||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
// 重置到原始值
|
||||
setValue(data.raw);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CopyNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CopyOutlined />}
|
||||
hotkey="Command+C"
|
||||
onClick={() => {
|
||||
workspace.copySelectedNode();
|
||||
}}
|
||||
>
|
||||
复制节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
export const DeleteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<DeleteOutlined />}
|
||||
hotkey="Backspace"
|
||||
onClick={() => {
|
||||
workspace.removeSelectedNode();
|
||||
}}
|
||||
>
|
||||
删除节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './paste-node';
|
||||
export * from './view-source';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { SnippetsOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PasteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<SnippetsOutlined />}
|
||||
hotkey="Command+V"
|
||||
onClick={() => {
|
||||
workspace.pasteSelectedNode();
|
||||
}}
|
||||
>
|
||||
粘贴节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useDesigner, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CodeOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ViewSourceContextAction = observer(() => {
|
||||
const designer = useDesigner();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => {
|
||||
designer.setActiveView('code');
|
||||
}}
|
||||
>
|
||||
查看源码
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
|
||||
* 沙箱查询实例
|
||||
*/
|
||||
sandboxQuery: DndQuery;
|
||||
/**
|
||||
* 预览沙箱查询实例
|
||||
*/
|
||||
previewSandboxQuery: DndQuery;
|
||||
/**
|
||||
* 远程服务
|
||||
*/
|
||||
remoteServices?: Record<string, S>;
|
||||
remoteServices: Record<string, S>;
|
||||
}
|
||||
|
||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
export { DesignerProvider };
|
||||
|
||||
export const useSandboxQuery = () => {
|
||||
return useDesigner()?.sandboxQuery;
|
||||
return useDesigner().sandboxQuery;
|
||||
};
|
||||
|
||||
export const usePreviewSandboxQuery = () => {
|
||||
return useDesigner().previewSandboxQuery;
|
||||
};
|
||||
|
||||
export const useRemoteServices = () => {
|
||||
return useDesigner()?.remoteServices;
|
||||
return useDesigner().remoteServices;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import defaultTheme from './themes/default';
|
||||
import { DndQuery } from './dnd';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers';
|
||||
|
||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
const builtinSandboxQuery = new DndQuery({
|
||||
context: `#${DESIGN_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
const builtinPreviewSandboxQuery = new DndQuery({
|
||||
context: `#${PREVIEW_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
export interface DesignerProps extends Partial<IDesignerContext>, ITangoEngineContext {
|
||||
/**
|
||||
* 主题包
|
||||
*/
|
||||
@@ -20,13 +30,23 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
* @returns
|
||||
*/
|
||||
export function Designer(props: DesignerProps) {
|
||||
const { engine, config, theme: themeProp, sandboxQuery, remoteServices = {}, children } = props;
|
||||
const {
|
||||
engine,
|
||||
config,
|
||||
theme: themeProp,
|
||||
sandboxQuery = builtinSandboxQuery,
|
||||
previewSandboxQuery = builtinPreviewSandboxQuery,
|
||||
remoteServices = {},
|
||||
children,
|
||||
} = props;
|
||||
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||
return (
|
||||
<SystemProvider theme={theme} prefix="--tango">
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<TangoEngineProvider value={{ engine, config }}>
|
||||
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
|
||||
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
|
||||
{children}
|
||||
</DesignerProvider>
|
||||
</TangoEngineProvider>
|
||||
</ConfigProvider>
|
||||
</SystemProvider>
|
||||
|
||||
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
|
||||
interface DndQueryOptions {
|
||||
/**
|
||||
* DOM 查询上下文选择器
|
||||
* TODO: 是不是可以合并成一个 API
|
||||
*/
|
||||
context?: string;
|
||||
/**
|
||||
@@ -63,6 +62,9 @@ export class DndQuery {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的 window 对象
|
||||
*/
|
||||
get window() {
|
||||
if (this.context && 'defaultView' in this.context) {
|
||||
return (this.context as unknown as Document).defaultView;
|
||||
@@ -71,6 +73,9 @@ export class DndQuery {
|
||||
return window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的全局滚动偏移
|
||||
*/
|
||||
get scrollTop() {
|
||||
if (this.context && 'documentElement' in this.context) {
|
||||
return (this.context as unknown as Document).documentElement.scrollTop;
|
||||
|
||||
@@ -77,6 +77,9 @@ export function useDnd({
|
||||
if (designer.isPreview) {
|
||||
return;
|
||||
}
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
selectSource.setStart({
|
||||
@@ -128,6 +131,9 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
@@ -388,6 +394,56 @@ export function useDnd({
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
let target;
|
||||
if (workspace.selectSource.isSelected) {
|
||||
for (const item of workspace.selectSource.selected) {
|
||||
if (
|
||||
// 如果选中的节点是页面根节点(无 parents),则忽略
|
||||
item.parents?.length &&
|
||||
item.bounding &&
|
||||
clientX >= item.bounding.left &&
|
||||
clientX <= item.bounding.left + item.bounding.width &&
|
||||
clientY >= item.bounding.top &&
|
||||
clientY <= item.bounding.top + item.bounding.height
|
||||
) {
|
||||
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 否则,根据右键的元素选中最接近的组件
|
||||
if (!target) {
|
||||
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
|
||||
}
|
||||
if (target && target.id) {
|
||||
if (!target.parents?.length) {
|
||||
// 页面根节点不展示右键菜单操作
|
||||
return;
|
||||
}
|
||||
// 右键时高亮选中当前元素
|
||||
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
|
||||
event.preventDefault();
|
||||
selectSource.select(target);
|
||||
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
|
||||
requestAnimationFrame(() => {
|
||||
designer.toggleContextMenu(true, {
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTango = (e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
|
||||
@@ -437,6 +493,7 @@ export function useDnd({
|
||||
onDragEnd,
|
||||
onScroll,
|
||||
onKeyDown,
|
||||
onContextMenu,
|
||||
onTango,
|
||||
...selectHandler,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,20 @@ import {
|
||||
parseDndId,
|
||||
} from '@music163/tango-helpers';
|
||||
|
||||
// -----------
|
||||
// CONSTANTS
|
||||
// -----------
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const DESIGN_SANDBOX_ID = 'sandbox-container';
|
||||
|
||||
export const PREVIEW_SANDBOX_ID = 'preview-sandbox-container';
|
||||
|
||||
// -----------
|
||||
// DOM Helpers
|
||||
// -----------
|
||||
|
||||
export function buildQueryBySlotId(id: string) {
|
||||
return `[${SLOT.dnd}="${id}"]`;
|
||||
}
|
||||
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
|
||||
return $(selector).get(0);
|
||||
}
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
||||
|
||||
export function setElementStyle(selector: Selector, style: any) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
|
||||
import { DndQuery, useDnd } from '../dnd';
|
||||
import { Navigator } from './navigator';
|
||||
import { SelectionToolsProps } from '../simulator/selection';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||
|
||||
interface ISandboxEventHandlerConfig {
|
||||
sandboxQuery?: DndQuery;
|
||||
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
|
||||
|
||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
|
||||
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||
return routerType === 'hash' ? 'hash' : undefined;
|
||||
}
|
||||
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
@@ -62,6 +67,7 @@ function useSandbox({
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
|
||||
const isPreview = isPreviewProp ?? designer.isPreview;
|
||||
|
||||
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
||||
@@ -103,23 +109,26 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
files,
|
||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
}
|
||||
: {
|
||||
files,
|
||||
eventHandlers: dndHandlers as any,
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
};
|
||||
|
||||
return sandboxProps;
|
||||
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} iframeId="preview-sandbox-container" {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={PREVIEW_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './more-actions';
|
||||
export * from './select-parent-node';
|
||||
export * from './view-source';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { SelectAction } from '@music163/tango-ui';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Dropdown } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export const MoreActionsAction = observer(() => {
|
||||
return (
|
||||
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
|
||||
<SelectAction tooltip="更多">
|
||||
<MoreOutlined />
|
||||
</SelectAction>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
import { ContextMenu } from '../../components';
|
||||
|
||||
export interface ComponentsTreeProps {
|
||||
/**
|
||||
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
const handleSelect = (keys: React.Key[]) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
|
||||
return (
|
||||
<Box css={outlineStyle}>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={(keys) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
<Dropdown
|
||||
placement="bottomLeft"
|
||||
trigger={['contextMenu']}
|
||||
open={contextMenuOpen}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
setContextMenuOpen(val);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
}}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
|
||||
>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={handleSelect}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
onRightClick={({ event, node }) => {
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
// 顶层的 Page 组件不展示右键菜单
|
||||
if (node.component === 'Page') {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// 由于部分操作是基于 selectSource 实现的,因此右键时默认选中当前项
|
||||
handleSelect([node.key]);
|
||||
setContextMenuOpen(true);
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
|
||||
新建模型
|
||||
</Button>
|
||||
}
|
||||
bodyProps={{ p: 'm' }}
|
||||
bodyProps={{ p: 'm', height: '100%' }}
|
||||
>
|
||||
{isAdd ? (
|
||||
<AddStoreForm
|
||||
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
|
||||
/>
|
||||
) : (
|
||||
<VariableTree
|
||||
height="100%"
|
||||
defaultValueDetailMode="define"
|
||||
dataSource={storeVariables}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
|
||||
@@ -25,7 +25,13 @@ export interface SelectionToolsProps {
|
||||
|
||||
export const SelectionTools = observer(
|
||||
({
|
||||
actions: actionsProp = ['selectParentNode', 'viewSource', 'copyNode', 'deleteNode'],
|
||||
actions: actionsProp = [
|
||||
'selectParentNode',
|
||||
'viewSource',
|
||||
'copyNode',
|
||||
'deleteNode',
|
||||
'moreActions',
|
||||
],
|
||||
}: SelectionToolsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { Ghost } from './ghost';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { SelectionTools, SelectionToolsProps } from './selection';
|
||||
import { InsertionPrompt } from './insertion';
|
||||
import { DraggingMask } from './mask';
|
||||
import { Dropdown, DropdownProps } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
}
|
||||
|
||||
const ViewportContextMenu = observer((props: DropdownProps) => {
|
||||
const designer = useDesigner();
|
||||
|
||||
// FIXME: 考虑优化定位实现?
|
||||
// 由于 iframe 内的 contextmenu 事件无法被外层感知到,所以无法自动展示与定位
|
||||
// 而手动指定 align 会干扰 contextmenu 溢出时的位置处理,会定位到完全相反的位置
|
||||
// 因此,给 dropdown 单独提供一个定位容器,将其定位至 iframe 内触发点击的位置
|
||||
// 并在每次 contextmenu 隐藏时销毁 dropdown,让 dropdown 能重新获取新的位置
|
||||
return (
|
||||
designer.showContextMenu && (
|
||||
<Dropdown
|
||||
autoAdjustOverflow
|
||||
trigger={['contextMenu']}
|
||||
placement="bottomLeft"
|
||||
open
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
designer.toggleContextMenu(val);
|
||||
}
|
||||
}}
|
||||
overlay={<ContextMenu onClick={() => designer.toggleContextMenu(false)} showParents />}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
width={0}
|
||||
height={0}
|
||||
position="absolute"
|
||||
style={{
|
||||
left: designer.contextMenuPosition.clientX,
|
||||
top: designer.contextMenuPosition.clientY,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||
return (
|
||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||
<ViewportContextMenu />
|
||||
<ViewportBody>{children}</ViewportBody>
|
||||
<div className="AuxTools">
|
||||
<SelectionTools actions={selectionTools} />
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
|
||||
designer.setActiveView('design');
|
||||
}
|
||||
}}
|
||||
tooltip="预览"
|
||||
tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
|
||||
>
|
||||
<EyeOutlined />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
<Group display="flex" alignItems="center" gap="m">
|
||||
{centerTools}
|
||||
</Group>
|
||||
<Group display="flex" alignItems="center" gap="m">
|
||||
<Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
|
||||
{rightTools}
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@@ -18,14 +18,21 @@ import {
|
||||
DeleteNodeAction,
|
||||
ViewSourceAction,
|
||||
SelectParentNodeAction,
|
||||
MoreActionsAction,
|
||||
} from './selection-menu';
|
||||
import {
|
||||
CopyNodeContextAction,
|
||||
DeleteNodeContextAction,
|
||||
PasteNodeContextAction,
|
||||
ViewSourceContextAction,
|
||||
} from './context-menu';
|
||||
|
||||
const widgets = {};
|
||||
|
||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu)\.[a-zA-z]+$/.test(key)) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) {
|
||||
throw new Error(
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, or selection-menu`,
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||
);
|
||||
}
|
||||
setValue(widgets, key, component);
|
||||
@@ -55,3 +62,9 @@ registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
||||
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
||||
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
||||
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
||||
registerWidget('selectionMenu.moreActions', MoreActionsAction);
|
||||
|
||||
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
|
||||
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
|
||||
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
|
||||
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.1...@music163/tango-helpers@1.2.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.0...@music163/tango-helpers@1.1.1) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -68,3 +68,10 @@ export function isServiceVariablePath(key: string) {
|
||||
export function isInTangoDesignMode() {
|
||||
return !!(window as any).__TANGO_DESIGNER__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 macOS 或 iOS like 设备
|
||||
*/
|
||||
export function isApplePlatform() {
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.4...@music163/tango-sandbox@1.0.5) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use addEventListener to bind iframe event handlers ([#169](https://github.com/netease/tango/issues/169)) ([c1306e2](https://github.com/netease/tango/commit/c1306e24e0c747d0f4bd845dc1714488ddd8cdac))
|
||||
|
||||
## [1.0.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.3...@music163/tango-sandbox@1.0.4) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-core": "^1.3.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,7 +3,13 @@ import { TangoEventName } from '@music163/tango-helpers';
|
||||
import Loading from './loading';
|
||||
import Manager from './manager';
|
||||
import { createMissingPackageJSON, changeRoute } from './helper';
|
||||
import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from '../types';
|
||||
import {
|
||||
IFiles,
|
||||
CodeSandboxState,
|
||||
CodeSandboxProps,
|
||||
UnsubscribeFunction,
|
||||
EventHandlers,
|
||||
} from '../types';
|
||||
|
||||
// CodeSandbox 使用示例
|
||||
// <CodeSandbox files={files} entry={entry} template={template} bundlerURL={`${window.location.protocol}//codesandbox.fn.netease.com/`} />
|
||||
@@ -32,6 +38,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
iframe: HTMLIFrameElement;
|
||||
|
||||
private eventHandlers: EventHandlers;
|
||||
|
||||
constructor(props: CodeSandboxProps) {
|
||||
super(props);
|
||||
|
||||
@@ -75,11 +83,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
this.manager.cleanup();
|
||||
|
||||
// clear tango event
|
||||
if (this.iframe && this.props?.eventHandlers?.onTango) {
|
||||
this.iframe.contentDocument?.removeEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
this.props.eventHandlers.onTango,
|
||||
);
|
||||
if (this.iframe && this.eventHandlers) {
|
||||
this.unbindEvents(this.eventHandlers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +121,29 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
externalResources: this.props.externalResources,
|
||||
});
|
||||
|
||||
normalizeEventType = (eventType: string) =>
|
||||
eventType === 'onTango'
|
||||
? TangoEventName.DesignerAction
|
||||
: eventType.replace(/^on/, '').toLowerCase();
|
||||
|
||||
bindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.addEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
unbindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.removeEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Manager 实例化,即开始构建页面
|
||||
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
|
||||
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
|
||||
@@ -155,17 +183,11 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
// 注册监听函数
|
||||
const { eventHandlers } = this.props;
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
if (el.contentDocument) {
|
||||
el.contentDocument[eventType.toLowerCase()] = eventHandlers[eventType];
|
||||
}
|
||||
if (eventType === 'onTango') {
|
||||
el.contentDocument?.addEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
}
|
||||
});
|
||||
if (eventHandlers) {
|
||||
this.bindEvents(eventHandlers);
|
||||
// 保存一份当前的 eventHandlers,避免 props 变更导致无法取消现在已注册的事件绑定
|
||||
this.eventHandlers = eventHandlers;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.4...@music163/tango-setting-form@1.2.5) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.3...@music163/tango-setting-form@1.2.4) (2024-05-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.2...@music163/tango-setting-form@1.2.3) (2024-05-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.1...@music163/tango-setting-form@1.2.2) (2024-05-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-ui": "^1.2.1",
|
||||
"@music163/tango-core": "^1.3.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-ui": "^1.3.0",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.3...@music163/tango-ui@1.3.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.2...@music163/tango-ui@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- popover re-click position bug & drag panel drag bug ([#165](https://github.com/netease/tango/issues/165)) ([f83c6bc](https://github.com/netease/tango/commit/f83c6bc0f69820582720512eefedc9ddf5db2975))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.1...@music163/tango-ui@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.0...@music163/tango-ui@1.2.1) (2024-05-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lint": "^6.7.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"antd": "^4.24.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -14,6 +14,7 @@ const actionStyle = css`
|
||||
border-radius: var(--tango-radii-s);
|
||||
color: var(--tango-colors-text2);
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-fill2);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Menu, Space } from 'antd';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { isApplePlatform } from '@music163/tango-helpers';
|
||||
|
||||
const contextActionStyle = css`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.ContextActionContent {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ContextActionExtra {
|
||||
flex: none;
|
||||
color: var(--tango-colors-gray-40);
|
||||
}
|
||||
`;
|
||||
|
||||
interface ContextActionProps {
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
hotkey?: string;
|
||||
extra?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export function ContextAction({
|
||||
icon,
|
||||
children,
|
||||
hotkey,
|
||||
extra,
|
||||
disabled,
|
||||
onClick,
|
||||
className,
|
||||
key,
|
||||
}: ContextActionProps) {
|
||||
const normalizedHotKey = useMemo(() => {
|
||||
if (!hotkey) {
|
||||
return null;
|
||||
}
|
||||
const keyMap = isApplePlatform()
|
||||
? {
|
||||
command: '⌘',
|
||||
meta: '⌘',
|
||||
ctrl: '^',
|
||||
control: '^',
|
||||
alt: '⌥',
|
||||
option: '⌥',
|
||||
shift: '⇧',
|
||||
'+': '',
|
||||
}
|
||||
: {
|
||||
command: 'Ctrl',
|
||||
meta: 'Win',
|
||||
ctrl: 'Ctrl',
|
||||
control: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
option: 'Alt',
|
||||
shift: 'Shift',
|
||||
};
|
||||
const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\\+/, '\\+'), 'ig');
|
||||
return hotkey.replace(regexp, (match) => keyMap[match.toLowerCase()]);
|
||||
}, [hotkey]);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key} onClick={onClick} icon={icon} className={className} disabled={disabled}>
|
||||
<Box className="ContextAction" css={contextActionStyle}>
|
||||
<Box className="ContextActionContent">{children}</Box>
|
||||
<Box className="ContextActionExtra">
|
||||
<Space>
|
||||
{normalizedHotKey}
|
||||
{extra}
|
||||
</Space>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,24 @@ import Draggable from 'react-draggable';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { noop } from '@music163/tango-helpers';
|
||||
|
||||
// Dragging over an iframe stops dragging when moving the mouse too fast #613
|
||||
// https://github.com/react-grid-layout/react-draggable/issues/613
|
||||
const injectStyleToBody = () => {
|
||||
const id = 'react-draggable-transparent-selection';
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
const style = document.createElement('style');
|
||||
style.id = id;
|
||||
style.innerHTML = `
|
||||
/* Prevent iframes from stealing drag events */
|
||||
.react-draggable-transparent-selection iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
const CloseIcon = styled(CloseOutlined)`
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
@@ -17,7 +35,7 @@ const CloseIcon = styled(CloseOutlined)`
|
||||
}
|
||||
`;
|
||||
|
||||
interface DragPanelProps extends Omit<PopoverProps, 'overlay'> {
|
||||
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
|
||||
// 标题
|
||||
title?: React.ReactNode | string;
|
||||
// 内容
|
||||
@@ -59,7 +77,12 @@ export function DragPanel({
|
||||
onOpenChange(innerOpen);
|
||||
}}
|
||||
overlay={
|
||||
<Draggable handle=".selection-drag-bar">
|
||||
<Draggable
|
||||
handle=".selection-drag-bar"
|
||||
onStart={() => {
|
||||
injectStyleToBody();
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
bg="#FFF"
|
||||
borderRadius="m"
|
||||
|
||||
@@ -24,3 +24,4 @@ export * from './copy-clipboard';
|
||||
export * from './tag-select';
|
||||
export * from './popover';
|
||||
export * from './drag-panel';
|
||||
export * from './context-action';
|
||||
|
||||
@@ -97,6 +97,11 @@ export const Popover: React.FC<PopoverProps> = ({
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (visible) {
|
||||
setVisible(false);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
setLeft(x);
|
||||
@@ -104,7 +109,7 @@ export const Popover: React.FC<PopoverProps> = ({
|
||||
setVisible(true);
|
||||
onOpenChange(true);
|
||||
},
|
||||
[onOpenChange],
|
||||
[visible, onOpenChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -91,7 +91,7 @@ const buttonStyle = css`
|
||||
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
|
||||
color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
|
||||
border: 0;
|
||||
border-radius: var(--tango-radii-m);
|
||||
border-radius: var(--tango-radii-s);
|
||||
|
||||
&:hover {
|
||||
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);
|
||||
|
||||
Reference in New Issue
Block a user