Compare commits

...

9 Commits

Author SHA1 Message Date
Wells 00bf52a411 chore(release): publish
- @music163/tango-context@1.1.2
 - @music163/tango-core@1.3.0
 - @music163/tango-designer@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
2024-06-03 09:51:00 +08:00
864907600cc c1306e24e0 fix: use addEventListener to bind iframe event handlers (#169)
* feat: use addEventListener to bind iframe event handlers

* fix: types
2024-05-31 17:44:55 +08:00
864907600cc 28040fc006 feat: add context menu (#161)
* feat: add more actions menu on SelectionTools

* feat: add view source context action

* feat: normalize hotkey by platform

* feat: add context menu to components tree

* feat: add context menu to viewport
2024-05-31 17:31:58 +08:00
Wells 3f3981bb9d fix: parse app entry file & pass correct routerType to sandbox (#168)
* fix: parse app entry file & pass correct routerType to sandbox

* fix: export entry module
2024-05-31 14:54:12 +08:00
Wells 253ceca969 chore(release): publish
- @music163/tango-designer@1.2.3
 - @music163/tango-setting-form@1.2.4
 - @music163/tango-ui@1.2.3
2024-05-30 17:05:52 +08:00
BoBoooooo a0ed57ff4f fix: optimize variable panel scroll ui & onCancel bug (#166) 2024-05-30 14:06:12 +08:00
BoBoooooo f83c6bc0f6 fix: popover re-click position bug & drag panel drag bug (#165)
* fix: popover reClick position bug

* fix: drag event lose when drag over iframe
2024-05-29 15:26:00 +08:00
Wells bb92210594 chore(release): publish
- @music163/tango-designer@1.2.2
 - @music163/tango-setting-form@1.2.3
 - @music163/tango-ui@1.2.2
2024-05-27 10:18:03 +08:00
Wells 163ecedff3 fix: export usePreviewSandboxQuery & add builtin sandboxQuery (#163)
* fix: export usePreviewSandboxQuery

* fix: update ui

* fix: update ui
2024-05-24 18:28:04 +08:00
55 changed files with 918 additions and 122 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ const tangoConfigJson = {
}, },
'@music163/antd': { '@music163/antd': {
description: '云音乐低代码中后台应用基础物料', description: '云音乐低代码中后台应用基础物料',
version: '0.2.5', version: '0.2.6',
library: 'TangoAntd', library: 'TangoAntd',
type: 'baseDependency', type: 'baseDependency',
resources: [ 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>
);
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-context", "name": "@music163/tango-context",
"version": "1.1.1", "version": "1.1.2",
"description": "react context for tango-apps", "description": "react context for tango-apps",
"keywords": [ "keywords": [
"react", "react",
@@ -31,8 +31,8 @@
"react": ">= 16.8" "react": ">= 16.8"
}, },
"dependencies": { "dependencies": {
"@music163/tango-core": "^1.2.0", "@music163/tango-core": "^1.3.0",
"@music163/tango-helpers": "^1.1.1", "@music163/tango-helpers": "^1.2.0",
"mobx-react-lite": "4.0.7" "mobx-react-lite": "4.0.7"
}, },
"publishConfig": { "publishConfig": {
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) # [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 ### Bug Fixes
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-core", "name": "@music163/tango-core",
"version": "1.2.0", "version": "1.3.0",
"description": "tango core", "description": "tango core",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -28,7 +28,7 @@
"@babel/parser": "^7.23.5", "@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5", "@babel/traverse": "^7.23.5",
"@babel/types": "^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__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4", "@types/babel__traverse": "^7.20.4",
"mobx": "6.12.3", "mobx": "6.12.3",
+19
View File
@@ -1425,3 +1425,22 @@ export function traverseFile(ast: t.File) {
imports, 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;
}
+34
View File
@@ -83,6 +83,16 @@ export class Designer {
*/ */
_addComponentPopoverPosition = { clientX: 0, clientY: 0 }; _addComponentPopoverPosition = { clientX: 0, clientY: 0 };
/**
* 是否显示右键菜单
*/
_showContextMenu = false;
/**
* 右键菜单在 iframe 上的位置
*/
_contextMenuPosition = { clientX: 0, clientY: 0 };
/** /**
* 是否显示右侧面板 * 是否显示右侧面板
*/ */
@@ -136,6 +146,14 @@ export class Designer {
return this._addComponentPopoverPosition; return this._addComponentPopoverPosition;
} }
get showContextMenu() {
return this._showContextMenu;
}
get contextMenuPosition() {
return this._contextMenuPosition;
}
get menuData() { get menuData() {
return this._menuData ?? ([] as MenuDataType); return this._menuData ?? ([] as MenuDataType);
} }
@@ -178,6 +196,8 @@ export class Designer {
_showRightPanel: observable, _showRightPanel: observable,
_showAddComponentPopover: observable, _showAddComponentPopover: observable,
_addComponentPopoverPosition: observable, _addComponentPopoverPosition: observable,
_showContextMenu: observable,
_contextMenuPosition: observable,
_menuData: observable, _menuData: observable,
_isPreview: observable, _isPreview: observable,
simulator: computed, simulator: computed,
@@ -189,6 +209,8 @@ export class Designer {
showSmartWizard: computed, showSmartWizard: computed,
showAddComponentPopover: computed, showAddComponentPopover: computed,
addComponentPopoverPosition: computed, addComponentPopoverPosition: computed,
showContextMenu: computed,
contextMenuPosition: computed,
menuData: computed, menuData: computed,
setSimulator: action, setSimulator: action,
setViewport: action, setViewport: action,
@@ -199,6 +221,7 @@ export class Designer {
toggleSmartWizard: action, toggleSmartWizard: action,
toggleIsPreview: action, toggleIsPreview: action,
toggleAddComponentPopover: action, toggleAddComponentPopover: action,
toggleContextMenu: action,
}); });
} }
@@ -264,4 +287,15 @@ export class Designer {
this.workspace.selectSource.clear(); this.workspace.selectSource.clear();
} }
} }
toggleContextMenu(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.contextMenuPosition,
) {
this._showContextMenu = value;
this._contextMenuPosition = position;
}
} }
+18
View File
@@ -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;
}
}
+1
View File
@@ -7,6 +7,7 @@ export * from './drag-source';
export * from './history'; export * from './history';
export * from './file'; export * from './file';
export * from './module'; export * from './module';
export * from './entry-module';
export * from './route-module'; export * from './route-module';
export * from './service-module'; export * from './service-module';
export * from './store-module'; export * from './store-module';
+5
View File
@@ -16,6 +16,7 @@ import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module'; import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module'; import { TangoServiceModule } from './service-module';
import { IdGenerator } from '../helpers'; import { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module';
export interface IViewFile { export interface IViewFile {
readonly workspace: IWorkspace; readonly workspace: IWorkspace;
@@ -152,6 +153,10 @@ export interface IWorkspace {
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取 * 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/ */
tangoConfigJson: TangoJsonFile; tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/** /**
* 解析后的路由模块 * 解析后的路由模块
*/ */
+13
View File
@@ -35,6 +35,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module'; import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module'; import { TangoViewModule } from './view-module';
import { TangoComponentsEntryModule } from './component-module'; import { TangoComponentsEntryModule } from './component-module';
import { AppEntryModule } from './entry-module';
export interface IWorkspaceOptions { export interface IWorkspaceOptions {
/** /**
@@ -97,6 +98,11 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
activeViewFile: string; activeViewFile: string;
/**
* 应用入口模块
*/
appEntryModule: AppEntryModule;
/** /**
* 路由配置模块 * 路由配置模块
*/ */
@@ -329,6 +335,9 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型 * @param fileType 模块类型
*/ */
addFile(filename: string, code: string, fileType?: FileType) { addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.AppEntryModule;
}
const moduleType = fileType || inferFileType(filename); const moduleType = fileType || inferFileType(filename);
const props = { const props = {
filename, filename,
@@ -338,6 +347,10 @@ export class Workspace extends EventTarget implements IWorkspace {
let module; let module;
switch (moduleType) { switch (moduleType) {
case FileType.AppEntryModule:
module = new AppEntryModule(this, props);
this.appEntryModule = module;
break;
case FileType.StoreEntryModule: case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props); module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module; this.storeEntryModule = module;
+1
View File
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
export enum FileType { export enum FileType {
// js 文件 // js 文件
Module = 'module', Module = 'module',
AppEntryModule = 'appEntryModule',
StoreEntryModule = 'storeEntryModule', StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule', RouteModule = 'routeModule',
ServiceModule = 'serviceModule', ServiceModule = 'serviceModule',
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 ### Bug Fixes
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-designer", "name": "@music163/tango-designer",
"version": "1.2.1", "version": "1.3.0",
"description": "lowcode designer", "description": "lowcode designer",
"keywords": [ "keywords": [
"react" "react"
@@ -33,12 +33,12 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/request": "^0.2.0", "@music163/request": "^0.2.0",
"@music163/tango-context": "^1.1.1", "@music163/tango-context": "^1.1.2",
"@music163/tango-core": "^1.2.0", "@music163/tango-core": "^1.3.0",
"@music163/tango-helpers": "^1.1.1", "@music163/tango-helpers": "^1.2.0",
"@music163/tango-sandbox": "^1.0.4", "@music163/tango-sandbox": "^1.0.5",
"@music163/tango-setting-form": "^1.2.2", "@music163/tango-setting-form": "^1.2.5",
"@music163/tango-ui": "^1.2.1", "@music163/tango-ui": "^1.3.0",
"antd": "^4.24.2", "antd": "^4.24.2",
"cash-dom": "^8.1.2", "cash-dom": "^8.1.2",
"classnames": "^2.5.1", "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 './drag-box';
export * from './input-kv'; export * from './input-kv';
export * from './variable-tree'; export * from './variable-tree';
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
return ( return (
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}> <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)} {renderHeaderExtra?.(props, state)}
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}> <Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} /> <Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
@@ -203,7 +203,11 @@ export function VariableTree(props: VariableTreeProps) {
<Box flex="0 0 72px" textAlign="right"> <Box flex="0 0 72px" textAlign="right">
{node.showRemoveButton && ( {node.showRemoveButton && (
<Popconfirm <Popconfirm
zIndex={99999}
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`} title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
onCancel={(e) => {
e.stopPropagation();
}}
onConfirm={(e) => { onConfirm={(e) => {
e.stopPropagation(); e.stopPropagation();
if (isStoreVariablePath(node.key)) { 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> </ValueDetail>
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
onClick={() => { onClick={() => {
off(); off();
setError(''); 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>
);
});
+11 -3
View File
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
* 沙箱查询实例 * 沙箱查询实例
*/ */
sandboxQuery: DndQuery; sandboxQuery: DndQuery;
/**
* 预览沙箱查询实例
*/
previewSandboxQuery: DndQuery;
/** /**
* 远程服务 * 远程服务
*/ */
remoteServices?: Record<string, S>; remoteServices: Record<string, S>;
} }
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
export { DesignerProvider }; export { DesignerProvider };
export const useSandboxQuery = () => { export const useSandboxQuery = () => {
return useDesigner()?.sandboxQuery; return useDesigner().sandboxQuery;
};
export const usePreviewSandboxQuery = () => {
return useDesigner().previewSandboxQuery;
}; };
export const useRemoteServices = () => { export const useRemoteServices = () => {
return useDesigner()?.remoteServices; return useDesigner().remoteServices;
}; };
+23 -3
View File
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
import zhCN from 'antd/lib/locale/zh_CN'; import zhCN from 'antd/lib/locale/zh_CN';
import { DesignerProvider, IDesignerContext } from './context'; import { DesignerProvider, IDesignerContext } from './context';
import defaultTheme from './themes/default'; 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 * @returns
*/ */
export function Designer(props: DesignerProps) { 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]); const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
return ( return (
<SystemProvider theme={theme} prefix="--tango"> <SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine, config }}> <TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider> <DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
{children}
</DesignerProvider>
</TangoEngineProvider> </TangoEngineProvider>
</ConfigProvider> </ConfigProvider>
</SystemProvider> </SystemProvider>
+6 -1
View File
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
interface DndQueryOptions { interface DndQueryOptions {
/** /**
* DOM 查询上下文选择器 * DOM 查询上下文选择器
* TODO: 是不是可以合并成一个 API
*/ */
context?: string; context?: string;
/** /**
@@ -63,6 +62,9 @@ export class DndQuery {
return false; return false;
} }
/**
* 沙箱内的 window 对象
*/
get window() { get window() {
if (this.context && 'defaultView' in this.context) { if (this.context && 'defaultView' in this.context) {
return (this.context as unknown as Document).defaultView; return (this.context as unknown as Document).defaultView;
@@ -71,6 +73,9 @@ export class DndQuery {
return window; return window;
} }
/**
* 沙箱内的全局滚动偏移
*/
get scrollTop() { get scrollTop() {
if (this.context && 'documentElement' in this.context) { if (this.context && 'documentElement' in this.context) {
return (this.context as unknown as Document).documentElement.scrollTop; return (this.context as unknown as Document).documentElement.scrollTop;
+57
View File
@@ -77,6 +77,9 @@ export function useDnd({
if (designer.isPreview) { if (designer.isPreview) {
return; return;
} }
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY }); const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
selectSource.setStart({ selectSource.setStart({
@@ -128,6 +131,9 @@ export function useDnd({
}; };
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true); const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
if (data && data.id) { if (data && data.id) {
selectSource.select(data); 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 onTango = (e: CustomEvent) => {
const detail = e.detail || {}; const detail = e.detail || {};
@@ -437,6 +493,7 @@ export function useDnd({
onDragEnd, onDragEnd,
onScroll, onScroll,
onKeyDown, onKeyDown,
onContextMenu,
onTango, onTango,
...selectHandler, ...selectHandler,
}; };
+14 -2
View File
@@ -7,6 +7,20 @@ import {
parseDndId, parseDndId,
} from '@music163/tango-helpers'; } 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) { export function buildQueryBySlotId(id: string) {
return `[${SLOT.dnd}="${id}"]`; return `[${SLOT.dnd}="${id}"]`;
} }
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
return $(selector).get(0); return $(selector).get(0);
} }
export const DRAG_GHOST_ID = 'dragGhost';
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`); export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
export function setElementStyle(selector: Selector, style: any) { export function setElementStyle(selector: Selector, style: any) {
+13 -4
View File
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
import { DndQuery, useDnd } from '../dnd'; import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator'; import { Navigator } from './navigator';
import { SelectionToolsProps } from '../simulator/selection'; import { SelectionToolsProps } from '../simulator/selection';
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
interface ISandboxEventHandlerConfig { interface ISandboxEventHandlerConfig {
sandboxQuery?: DndQuery; sandboxQuery?: DndQuery;
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
const LANDING_PAGE_PATH = '/__background_landing_page__'; const LANDING_PAGE_PATH = '/__background_landing_page__';
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
return routerType === 'hash' ? 'hash' : undefined;
}
function useSandbox({ function useSandbox({
isPreview: isPreviewProp, isPreview: isPreviewProp,
configFormatter, configFormatter,
@@ -62,6 +67,7 @@ function useSandbox({
const workspace = useWorkspace(); const workspace = useWorkspace();
const designer = useDesigner(); const designer = useDesigner();
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
const isPreview = isPreviewProp ?? designer.isPreview; const isPreview = isPreviewProp ?? designer.isPreview;
// 组件不一定会立即刷新,因此 isActive 需要实时获取 // 组件不一定会立即刷新,因此 isActive 需要实时获取
@@ -103,23 +109,26 @@ function useSandbox({
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由 // 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
const display = isActive ? 'block' : 'none'; const display = isActive ? 'block' : 'none';
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH; const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
const sandboxProps = isPreview const sandboxProps = isPreview
? { ? {
files, files,
eventHandlers: pick(dndHandlers, ['onTango']), eventHandlers: pick(dndHandlers, ['onTango']),
onMessage, onMessage,
onLoad,
display, display,
startRoute: routePath, startRoute: routePath,
onLoad, routerMode,
} }
: { : {
files, files,
eventHandlers: dndHandlers as any, eventHandlers: dndHandlers as any,
onMessage, onMessage,
onLoad,
display, display,
startRoute: routePath, startRoute: routePath,
onLoad, routerMode,
}; };
return sandboxProps; return sandboxProps;
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
return ( return (
<Box display={display} width="100%" height="100%"> <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> </Box>
); );
}, },
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
return ( return (
<Box display={display} width="100%" height="100%"> <Box display={display} width="100%" height="100%">
<CodeSandbox ref={ref} {...sandboxProps} {...rest} /> <CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
</Box> </Box>
); );
}, },
@@ -1,4 +1,5 @@
export * from './copy-node'; export * from './copy-node';
export * from './delete-node'; export * from './delete-node';
export * from './more-actions';
export * from './select-parent-node'; export * from './select-parent-node';
export * from './view-source'; 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 { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers'; import { buildQueryBySlotId } from '../../helpers';
import { ContextMenu } from '../../components';
export interface ComponentsTreeProps { export interface ComponentsTreeProps {
/** /**
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
const file = workspace.activeViewModule; const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[]; const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree)); 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(() => { useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id)); setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
return ( return (
<Box css={outlineStyle}> <Box css={outlineStyle}>
<Tree <Dropdown
selectedKeys={selectedKeys as string[]} placement="bottomLeft"
fieldNames={filedNames} trigger={['contextMenu']}
treeData={nodesTree as any[]} open={contextMenuOpen}
onSelect={(keys) => { onOpenChange={(val) => {
const slotKey = keys?.[0] as string; if (!val) {
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true); setContextMenuOpen(val);
if (data && data.id) {
workspace.selectSource.select(data);
} }
// export selected
onSelect(slotKey);
setSelectedKeys(keys);
}} }}
blockNode overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
expandedKeys={expandedKeys} >
onExpand={(keys) => setExpandedKeys(keys as string[])} <Tree
draggable selectedKeys={selectedKeys as string[]}
onDragStart={(data) => { fieldNames={filedNames}
const prototype = workspace.componentPrototypes.get(data.node.component); treeData={nodesTree as any[]}
if (!prototype) { onSelect={handleSelect}
return; blockNode
} expandedKeys={expandedKeys}
const { canDrag } = prototype.rules || {}; onExpand={(keys) => setExpandedKeys(keys as string[])}
if (canDrag && !canDrag()) { draggable
return; onDragStart={(data) => {
} const prototype = workspace.componentPrototypes.get(data.node.component);
workspace.dragSource.set({ if (!prototype) {
id: data.node.id, return;
name: data.node.component, }
}); const { canDrag } = prototype.rules || {};
}} if (canDrag && !canDrag()) {
onDrop={(data) => { return;
const dropKey = data.node.key as string; }
let method; workspace.dragSource.set({
if (data.dropToGap) { id: data.node.id,
// 插入节点的后面 name: data.node.component,
method = DropMethod.InsertAfter; });
} else { }}
// 作为第一个子节点 onDrop={(data) => {
method = DropMethod.InsertFirstChild; const dropKey = data.node.key as string;
} let method;
workspace.dragSource.dropTarget.set( if (data.dropToGap) {
{ // 插入节点的后面
id: dropKey, method = DropMethod.InsertAfter;
}, } else {
method, // 作为第一个子节点
); method = DropMethod.InsertFirstChild;
workspace.dropNode(); }
}} workspace.dragSource.dropTarget.set(
titleRender={(node) => ( {
<OutlineTreeNode id: dropKey,
actionItems={actionItems} },
showToggleVisibleIcon={showToggleVisibleIcon} method,
node={node} );
/> 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> </Box>
); );
}, },
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
</Button> </Button>
} }
bodyProps={{ p: 'm' }} bodyProps={{ p: 'm', height: '100%' }}
> >
{isAdd ? ( {isAdd ? (
<AddStoreForm <AddStoreForm
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
/> />
) : ( ) : (
<VariableTree <VariableTree
height="100%"
defaultValueDetailMode="define" defaultValueDetailMode="define"
dataSource={storeVariables} dataSource={storeVariables}
onAddStoreVariable={(storeName, data) => { onAddStoreVariable={(storeName, data) => {
@@ -25,7 +25,13 @@ export interface SelectionToolsProps {
export const SelectionTools = observer( export const SelectionTools = observer(
({ ({
actions: actionsProp = ['selectParentNode', 'viewSource', 'copyNode', 'deleteNode'], actions: actionsProp = [
'selectParentNode',
'viewSource',
'copyNode',
'deleteNode',
'moreActions',
],
}: SelectionToolsProps) => { }: SelectionToolsProps) => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const selectSource = workspace.selectSource; const selectSource = workspace.selectSource;
+42 -1
View File
@@ -1,20 +1,61 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import cx from 'classnames'; import cx from 'classnames';
import { Box, HTMLCoralProps } from 'coral-system'; 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 { Ghost } from './ghost';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
import { SelectionTools, SelectionToolsProps } from './selection'; import { SelectionTools, SelectionToolsProps } from './selection';
import { InsertionPrompt } from './insertion'; import { InsertionPrompt } from './insertion';
import { DraggingMask } from './mask'; import { DraggingMask } from './mask';
import { Dropdown, DropdownProps } from 'antd';
import { ContextMenu } from '../components';
export interface ViewportProps extends HTMLCoralProps<'div'> { export interface ViewportProps extends HTMLCoralProps<'div'> {
selectionTools?: SelectionToolsProps['actions']; 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) { export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
return ( return (
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}> <Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
<ViewportContextMenu />
<ViewportBody>{children}</ViewportBody> <ViewportBody>{children}</ViewportBody>
<div className="AuxTools"> <div className="AuxTools">
<SelectionTools actions={selectionTools} /> <SelectionTools actions={selectionTools} />
+1 -1
View File
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
designer.setActiveView('design'); designer.setActiveView('design');
} }
}} }}
tooltip="预览" tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
> >
<EyeOutlined /> <EyeOutlined />
</ToggleButton> </ToggleButton>
+1 -1
View File
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
<Group display="flex" alignItems="center" gap="m"> <Group display="flex" alignItems="center" gap="m">
{centerTools} {centerTools}
</Group> </Group>
<Group display="flex" alignItems="center" gap="m"> <Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
{rightTools} {rightTools}
</Group> </Group>
</Box> </Box>
+15 -2
View File
@@ -18,14 +18,21 @@ import {
DeleteNodeAction, DeleteNodeAction,
ViewSourceAction, ViewSourceAction,
SelectParentNodeAction, SelectParentNodeAction,
MoreActionsAction,
} from './selection-menu'; } from './selection-menu';
import {
CopyNodeContextAction,
DeleteNodeContextAction,
PasteNodeContextAction,
ViewSourceContextAction,
} from './context-menu';
const widgets = {}; const widgets = {};
export function registerWidget(key: string, component: React.ComponentType<any>) { 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( 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); setValue(widgets, key, component);
@@ -55,3 +62,9 @@ registerWidget('selectionMenu.copyNode', CopyNodeAction);
registerWidget('selectionMenu.deleteNode', DeleteNodeAction); registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction); registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
registerWidget('selectionMenu.viewSource', ViewSourceAction); registerWidget('selectionMenu.viewSource', ViewSourceAction);
registerWidget('selectionMenu.moreActions', MoreActionsAction);
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 ### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-helpers", "name": "@music163/tango-helpers",
"version": "1.1.1", "version": "1.2.0",
"description": "Shared types, helpers, and hooks of tango-apps", "description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [ "keywords": [
"shared", "shared",
+7
View File
@@ -68,3 +68,10 @@ export function isServiceVariablePath(key: string) {
export function isInTangoDesignMode() { export function isInTangoDesignMode() {
return !!(window as any).__TANGO_DESIGNER__; return !!(window as any).__TANGO_DESIGNER__;
} }
/**
* 是否是 macOS 或 iOS like 设备
*/
export function isApplePlatform() {
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @music163/tango-sandbox
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-sandbox", "name": "@music163/tango-sandbox",
"version": "1.0.4", "version": "1.0.5",
"description": "sandbox of tango apps", "description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -29,8 +29,8 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.2.0", "@music163/tango-core": "^1.3.0",
"@music163/tango-helpers": "^1.1.1", "@music163/tango-helpers": "^1.2.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4" "react-frame-component": "^5.2.4"
+39 -17
View File
@@ -3,7 +3,13 @@ import { TangoEventName } from '@music163/tango-helpers';
import Loading from './loading'; import Loading from './loading';
import Manager from './manager'; import Manager from './manager';
import { createMissingPackageJSON, changeRoute } from './helper'; import { createMissingPackageJSON, changeRoute } from './helper';
import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from '../types'; import {
IFiles,
CodeSandboxState,
CodeSandboxProps,
UnsubscribeFunction,
EventHandlers,
} from '../types';
// CodeSandbox 使用示例 // CodeSandbox 使用示例
// <CodeSandbox files={files} entry={entry} template={template} bundlerURL={`${window.location.protocol}//codesandbox.fn.netease.com/`} /> // <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; iframe: HTMLIFrameElement;
private eventHandlers: EventHandlers;
constructor(props: CodeSandboxProps) { constructor(props: CodeSandboxProps) {
super(props); super(props);
@@ -75,11 +83,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
this.manager.cleanup(); this.manager.cleanup();
// clear tango event // clear tango event
if (this.iframe && this.props?.eventHandlers?.onTango) { if (this.iframe && this.eventHandlers) {
this.iframe.contentDocument?.removeEventListener( this.unbindEvents(this.eventHandlers);
TangoEventName.DesignerAction,
this.props.eventHandlers.onTango,
);
} }
} }
@@ -116,6 +121,29 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
externalResources: this.props.externalResources, 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 实例化,即开始构建页面 // Manager 实例化,即开始构建页面
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el // 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程 // 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
@@ -155,17 +183,11 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
// 注册监听函数 // 注册监听函数
const { eventHandlers } = this.props; const { eventHandlers } = this.props;
Object.keys(eventHandlers).forEach((eventType) => { if (eventHandlers) {
if (el.contentDocument) { this.bindEvents(eventHandlers);
el.contentDocument[eventType.toLowerCase()] = eventHandlers[eventType]; // 保存一份当前的 eventHandlers,避免 props 变更导致无法取消现在已注册的事件绑定
} this.eventHandlers = eventHandlers;
if (eventType === 'onTango') { }
el.contentDocument?.addEventListener(
TangoEventName.DesignerAction,
eventHandlers[eventType],
);
}
});
}; };
} }
}; };
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @music163/tango-setting-form
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-setting-form", "name": "@music163/tango-setting-form",
"version": "1.2.2", "version": "1.2.5",
"description": "setting form of tango-apps", "description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -28,9 +28,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.2.0", "@music163/tango-core": "^1.3.0",
"@music163/tango-helpers": "^1.1.1", "@music163/tango-helpers": "^1.2.0",
"@music163/tango-ui": "^1.2.1", "@music163/tango-ui": "^1.3.0",
"antd": "^4.24.2", "antd": "^4.24.2",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
"mobx": "6.12.3", "mobx": "6.12.3",
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @music163/tango-ui
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-ui", "name": "@music163/tango-ui",
"version": "1.2.1", "version": "1.3.0",
"description": "ui widgets of tango", "description": "ui widgets of tango",
"keywords": [ "keywords": [
"react", "react",
@@ -38,7 +38,7 @@
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lint": "^6.7.1", "@codemirror/lint": "^6.7.1",
"@codemirror/search": "^6.5.6", "@codemirror/search": "^6.5.6",
"@music163/tango-helpers": "^1.1.1", "@music163/tango-helpers": "^1.2.0",
"@uiw/react-codemirror": "^4.22.0", "@uiw/react-codemirror": "^4.22.0",
"antd": "^4.24.2", "antd": "^4.24.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
+1
View File
@@ -14,6 +14,7 @@ const actionStyle = css`
border-radius: var(--tango-radii-s); border-radius: var(--tango-radii-s);
color: var(--tango-colors-text2); color: var(--tango-colors-text2);
background-color: transparent; background-color: transparent;
white-space: nowrap;
&:hover { &:hover {
background-color: var(--tango-colors-fill2); background-color: var(--tango-colors-fill2);
+86
View File
@@ -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>
);
}
+25 -2
View File
@@ -5,6 +5,24 @@ import Draggable from 'react-draggable';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { noop } from '@music163/tango-helpers'; 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)` const CloseIcon = styled(CloseOutlined)`
cursor: pointer; cursor: pointer;
margin-left: 10px; 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; title?: React.ReactNode | string;
// 内容 // 内容
@@ -59,7 +77,12 @@ export function DragPanel({
onOpenChange(innerOpen); onOpenChange(innerOpen);
}} }}
overlay={ overlay={
<Draggable handle=".selection-drag-bar"> <Draggable
handle=".selection-drag-bar"
onStart={() => {
injectStyleToBody();
}}
>
<Box <Box
bg="#FFF" bg="#FFF"
borderRadius="m" borderRadius="m"
+1
View File
@@ -24,3 +24,4 @@ export * from './copy-clipboard';
export * from './tag-select'; export * from './tag-select';
export * from './popover'; export * from './popover';
export * from './drag-panel'; export * from './drag-panel';
export * from './context-action';
+6 -1
View File
@@ -97,6 +97,11 @@ export const Popover: React.FC<PopoverProps> = ({
const handleClick = useCallback( const handleClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
if (visible) {
setVisible(false);
onOpenChange(false);
return;
}
const x = e.clientX; const x = e.clientX;
const y = e.clientY; const y = e.clientY;
setLeft(x); setLeft(x);
@@ -104,7 +109,7 @@ export const Popover: React.FC<PopoverProps> = ({
setVisible(true); setVisible(true);
onOpenChange(true); onOpenChange(true);
}, },
[onOpenChange], [visible, onOpenChange],
); );
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -91,7 +91,7 @@ const buttonStyle = css`
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08)); background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
color: var(--tango-colors-custom-toolbarButtonTextColor, #999); color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
border: 0; border: 0;
border-radius: var(--tango-radii-m); border-radius: var(--tango-radii-s);
&:hover { &:hover {
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff); color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);