mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-15 14:00:35 +00:00
Merge branch 'feat/joint-editor' of gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine into feat/joint-editor
This commit is contained in:
commit
b55f6aa927
@ -218,18 +218,27 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
// 事件路由
|
// 事件路由
|
||||||
doc.addEventListener('mousedown', (downEvent: MouseEvent) => {
|
doc.addEventListener('mousedown', (downEvent: MouseEvent) => {
|
||||||
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
|
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
|
||||||
if (!nodeInst?.node) {
|
const node = nodeInst?.node || this.document.rootNode;
|
||||||
selection.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
|
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
|
||||||
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
|
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
|
||||||
|
const checkSelect = (e: MouseEvent) => {
|
||||||
|
doc.removeEventListener('mouseup', checkSelect, true);
|
||||||
|
if (!isShaken(downEvent, e)) {
|
||||||
|
const id = node.id;
|
||||||
|
designer.activeTracker.track(node);
|
||||||
|
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
||||||
|
selection.remove(id);
|
||||||
|
} else {
|
||||||
|
selection.select(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLeftButton) {
|
if (isLeftButton && !isRootNode(node)) {
|
||||||
const node: Node = nodeInst.node;
|
|
||||||
let nodes: Node[] = [node];
|
let nodes: Node[] = [node];
|
||||||
let ignoreUpSelected = false;
|
let ignoreUpSelected = false;
|
||||||
|
// 排除根节点拖拽
|
||||||
|
selection.remove(this.document.rootNode.id);
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
// multi select mode, directily add
|
// multi select mode, directily add
|
||||||
if (!selection.has(node.id)) {
|
if (!selection.has(node.id)) {
|
||||||
@ -257,20 +266,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSelect = (e: MouseEvent) => {
|
|
||||||
doc.removeEventListener('mouseup', checkSelect, true);
|
|
||||||
if (!isShaken(downEvent, e)) {
|
|
||||||
// const node = hasConditionFlow(target) ? target.conditionFlow : target;
|
|
||||||
const node = nodeInst.node!;
|
|
||||||
const id = node.id;
|
|
||||||
designer.activeTracker.track(node);
|
|
||||||
if (isMulti && selection.has(id)) {
|
|
||||||
selection.remove(id);
|
|
||||||
} else {
|
|
||||||
selection.select(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
doc.addEventListener('mouseup', checkSelect, true);
|
doc.addEventListener('mouseup', checkSelect, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -154,23 +154,56 @@ export class ComponentType {
|
|||||||
setter: 'StringSetter',
|
setter: 'StringSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'data',
|
||||||
title: '名称',
|
title: '数据',
|
||||||
setter: {
|
setter: {
|
||||||
componentName: 'ArraySetter',
|
componentName: 'ArraySetter',
|
||||||
props: {
|
props: {
|
||||||
itemConfig: {
|
itemConfig: {
|
||||||
setter: 'StringSetter',
|
setter: {
|
||||||
defaultValue: '',
|
componentName: 'ObjectSetter',
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
title: '名称',
|
||||||
|
setter: 'StringSetter',
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'records',
|
||||||
|
title: '记录集',
|
||||||
|
setter: {
|
||||||
|
componentName: 'ArraySetter',
|
||||||
|
props: {
|
||||||
|
itemConfig: {
|
||||||
|
setter: {
|
||||||
|
componentName: 'ArraySetter',
|
||||||
|
props: {
|
||||||
|
itemConfig: {
|
||||||
|
setter: 'StringSetter',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
important: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extraConfig: {},
|
||||||
|
},
|
||||||
|
// mode: 'popup'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'size',
|
|
||||||
title: '大小',
|
|
||||||
setter: 'StringSetter',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'age',
|
name: 'age',
|
||||||
title: '年龄',
|
title: '年龄',
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default class Prop implements IPropParent {
|
|||||||
}
|
}
|
||||||
return this.items!.map(prop => {
|
return this.items!.map(prop => {
|
||||||
const v = prop.export(serialize);
|
const v = prop.export(serialize);
|
||||||
return v === UNSET ? null : v
|
return v === UNSET ? null : v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ export default class Prop implements IPropParent {
|
|||||||
return JSON.stringify(this.value);
|
return JSON.stringify(this.value);
|
||||||
}
|
}
|
||||||
set code(val) {
|
set code(val) {
|
||||||
|
// todo
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed getAsString(): string {
|
@computed getAsString(): string {
|
||||||
@ -205,6 +205,7 @@ export default class Prop implements IPropParent {
|
|||||||
if (this.type === 'list') {
|
if (this.type === 'list') {
|
||||||
return this.size === other.size ? 1 : 2;
|
return this.size === other.size ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'literal' | 'map' | 'expression' | 'slot'
|
// 'literal' | 'map' | 'expression' | 'slot'
|
||||||
return this.code === other.code ? 0 : 2;
|
return this.code === other.code ? 0 : 2;
|
||||||
}
|
}
|
||||||
@ -548,7 +549,7 @@ export function isProp(obj: any): obj is Prop {
|
|||||||
return obj && obj.isProp;
|
return obj && obj.isProp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidArrayIndex(key: any, limit: number = -1): key is number {
|
function isValidArrayIndex(key: any, limit = -1): key is number {
|
||||||
const n = parseFloat(String(key));
|
const n = parseFloat(String(key));
|
||||||
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
|
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { obx, computed } from '@recore/obx';
|
import { obx, computed } from '@recore/obx';
|
||||||
import { INodeSelector, IViewport } from '../simulator';
|
import { INodeSelector, IViewport } from '../simulator';
|
||||||
import { uniqueId } from '../../../../utils/unique-id';
|
import { uniqueId } from '../../../../utils/unique-id';
|
||||||
|
import { isRootNode } from '../document/node/root-node';
|
||||||
|
|
||||||
export default class OffsetObserver {
|
export default class OffsetObserver {
|
||||||
readonly id = uniqueId('oobx');
|
readonly id = uniqueId('oobx');
|
||||||
@ -17,25 +18,25 @@ export default class OffsetObserver {
|
|||||||
@obx hasOffset = false;
|
@obx hasOffset = false;
|
||||||
@computed get offsetLeft() {
|
@computed get offsetLeft() {
|
||||||
if (!this.viewport.scrolling || this.lastOffsetLeft == null) {
|
if (!this.viewport.scrolling || this.lastOffsetLeft == null) {
|
||||||
this.lastOffsetLeft = (this.left + this.viewport.scrollX) * this.scale;
|
this.lastOffsetLeft = this.isRoot ? this.viewport.scrollX : (this.left + this.viewport.scrollX) * this.scale;
|
||||||
}
|
}
|
||||||
return this.lastOffsetLeft;
|
return this.lastOffsetLeft;
|
||||||
}
|
}
|
||||||
@computed get offsetTop() {
|
@computed get offsetTop() {
|
||||||
if (!this.viewport.scrolling || this.lastOffsetTop == null) {
|
if (!this.viewport.scrolling || this.lastOffsetTop == null) {
|
||||||
this.lastOffsetTop = (this.top + this.viewport.scrollY) * this.scale;
|
this.lastOffsetTop = this.isRoot ? this.viewport.scrollY : (this.top + this.viewport.scrollY) * this.scale;
|
||||||
}
|
}
|
||||||
return this.lastOffsetTop;
|
return this.lastOffsetTop;
|
||||||
}
|
}
|
||||||
@computed get offsetHeight() {
|
@computed get offsetHeight() {
|
||||||
if (!this.viewport.scrolling || this.lastOffsetHeight == null) {
|
if (!this.viewport.scrolling || this.lastOffsetHeight == null) {
|
||||||
this.lastOffsetHeight = this.height * this.scale;
|
this.lastOffsetHeight = this.isRoot ? this.viewport.height : this.height * this.scale;
|
||||||
}
|
}
|
||||||
return this.lastOffsetHeight;
|
return this.lastOffsetHeight;
|
||||||
}
|
}
|
||||||
@computed get offsetWidth() {
|
@computed get offsetWidth() {
|
||||||
if (!this.viewport.scrolling || this.lastOffsetWidth == null) {
|
if (!this.viewport.scrolling || this.lastOffsetWidth == null) {
|
||||||
this.lastOffsetWidth = this.width * this.scale;
|
this.lastOffsetWidth = this.isRoot ? this.viewport.width : this.width * this.scale;
|
||||||
}
|
}
|
||||||
return this.lastOffsetWidth;
|
return this.lastOffsetWidth;
|
||||||
}
|
}
|
||||||
@ -46,12 +47,18 @@ export default class OffsetObserver {
|
|||||||
|
|
||||||
private pid: number | undefined;
|
private pid: number | undefined;
|
||||||
private viewport: IViewport;
|
private viewport: IViewport;
|
||||||
|
private isRoot: boolean;
|
||||||
|
|
||||||
constructor(readonly nodeInstance: INodeSelector) {
|
constructor(readonly nodeInstance: INodeSelector) {
|
||||||
const { node, instance } = nodeInstance;
|
const { node, instance } = nodeInstance;
|
||||||
const doc = node.document;
|
const doc = node.document;
|
||||||
const host = doc.simulator!;
|
const host = doc.simulator!;
|
||||||
|
this.isRoot = isRootNode(node);
|
||||||
this.viewport = host.viewport;
|
this.viewport = host.viewport;
|
||||||
|
if (this.isRoot) {
|
||||||
|
this.hasOffset = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,15 @@
|
|||||||
属性面板
|
属性面板
|
||||||
|
|
||||||
|
其中 field 可独立出去一个包
|
||||||
|
|
||||||
|
提供:
|
||||||
|
1. 快捷设置面板服务
|
||||||
|
2. 对应节点的设置面板服务
|
||||||
|
3. 右侧设置面板
|
||||||
|
4. 提供 setters 服务,setters 注册、获取机制
|
||||||
|
|
||||||
|
依赖:
|
||||||
|
|
||||||
|
tip 处理
|
||||||
|
field
|
||||||
|
popup
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Component } from 'react';
|
import { Component, Fragment } from 'react';
|
||||||
import { Icon, Button, Message } from '@alifd/next';
|
import { Icon, Button, Message } from '@alifd/next';
|
||||||
import Sortable from './sortable';
|
import Sortable from './sortable';
|
||||||
import { SettingField, SetterType } from '../../main';
|
import { SettingField, SetterType, FieldConfig } from '../../main';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
import { createSettingFieldView } from '../../settings-pane';
|
import { createSettingFieldView } from '../../settings-pane';
|
||||||
|
import { PopupContext, PopupPipe } from '../../popup';
|
||||||
|
import Title from '../../title';
|
||||||
|
|
||||||
interface ArraySetterState {
|
interface ArraySetterState {
|
||||||
items: SettingField[];
|
items: SettingField[];
|
||||||
@ -45,7 +47,7 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
|||||||
const item = field.createField({
|
const item = field.createField({
|
||||||
...props.itemConfig,
|
...props.itemConfig,
|
||||||
name: i,
|
name: i,
|
||||||
forceInline: 1,
|
forceInline: 2,
|
||||||
});
|
});
|
||||||
items[i] = item;
|
items[i] = item;
|
||||||
itemsMap.set(item.id, item);
|
itemsMap.set(item.id, item);
|
||||||
@ -140,37 +142,39 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
|||||||
this.scrollToLast = false;
|
this.scrollToLast = false;
|
||||||
const lastIndex = items.length - 1;
|
const lastIndex = items.length - 1;
|
||||||
|
|
||||||
|
const content =
|
||||||
|
items.length > 0 ? (
|
||||||
|
<div className="lc-setter-list-scroll-body">
|
||||||
|
<Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}>
|
||||||
|
{items.map((field, index) => (
|
||||||
|
<ArrayItem
|
||||||
|
key={field.id}
|
||||||
|
scrollIntoView={scrollToLast && index === lastIndex}
|
||||||
|
field={field}
|
||||||
|
onRemove={this.onRemove.bind(this, field)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Sortable>
|
||||||
|
</div>
|
||||||
|
) : this.props.multiValue ? (
|
||||||
|
<Message type="warning">当前选择了多个节点,且值不一致,修改会覆盖所有值</Message>
|
||||||
|
) : (
|
||||||
|
<Message type="notice">当前项目为空</Message>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lc-setter-list lc-block-setter">
|
<div className="lc-setter-list lc-block-setter">
|
||||||
<div className="lc-block-setter-actions">
|
{/*<div className="lc-block-setter-actions">
|
||||||
<Button size="medium" onClick={this.onAdd.bind(this)}>
|
<Button size="medium" onClick={this.onAdd.bind(this)}>
|
||||||
<Icon type="add" />
|
<Icon type="add" />
|
||||||
<span>添加</span>
|
<span>添加</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>*/}
|
||||||
{this.props.multiValue && <Message type="warning">当前选择多个节点,且值不一致</Message>}
|
{content}
|
||||||
{items.length > 0 ? (
|
<Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}>
|
||||||
<div className="lc-setter-list-scroll-body">
|
<Icon type="add" />
|
||||||
<Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}>
|
<span>添加一项</span>
|
||||||
{items.map((field, index) => (
|
</Button>
|
||||||
<ArrayItem
|
|
||||||
key={field.id}
|
|
||||||
scrollIntoView={scrollToLast && index === lastIndex}
|
|
||||||
field={field}
|
|
||||||
onRemove={this.onRemove.bind(this, field)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Sortable>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="lc-setter-list-empty">
|
|
||||||
列表为空
|
|
||||||
<Button size="small" onClick={this.onAdd.bind(this)}>
|
|
||||||
<Icon type="add" />
|
|
||||||
<span>添加</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -208,7 +212,11 @@ class ArrayItem extends Component<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TableSetter extends ListSetter {}
|
class TableSetter extends ListSetter {
|
||||||
|
// todo:
|
||||||
|
// forceInline = 1
|
||||||
|
// has more actions
|
||||||
|
}
|
||||||
|
|
||||||
export default class ArraySetter extends Component<{
|
export default class ArraySetter extends Component<{
|
||||||
value: any[];
|
value: any[];
|
||||||
@ -218,17 +226,52 @@ export default class ArraySetter extends Component<{
|
|||||||
defaultValue?: any | ((field: SettingField) => any);
|
defaultValue?: any | ((field: SettingField) => any);
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
};
|
||||||
mode?: 'popup' | 'list' | 'table';
|
mode?: 'popup' | 'list';
|
||||||
forceInline?: boolean;
|
forceInline?: boolean;
|
||||||
multiValue?: boolean;
|
multiValue?: boolean;
|
||||||
}> {
|
}> {
|
||||||
|
static contextType = PopupContext;
|
||||||
|
private pipe: any;
|
||||||
render() {
|
render() {
|
||||||
const { mode, forceInline, ...props } = this.props;
|
const { mode, forceInline, ...props } = this.props;
|
||||||
|
const { field, itemConfig } = props;
|
||||||
if (mode === 'popup' || forceInline) {
|
if (mode === 'popup' || forceInline) {
|
||||||
// todo popup
|
const title = (
|
||||||
return <Button>编辑数组</Button>;
|
<Fragment>
|
||||||
} else if (mode === 'table') {
|
编辑:
|
||||||
return <TableSetter {...props} />;
|
<Title title={field.title} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
if (!this.pipe) {
|
||||||
|
let width = 360;
|
||||||
|
const setter: any = itemConfig?.setter;
|
||||||
|
if (setter?.componentName === 'ObjectSetter') {
|
||||||
|
const items: FieldConfig[] = setter.props?.config?.items;
|
||||||
|
if (items && Array.isArray(items)) {
|
||||||
|
const length = items.filter(item => item.required || item.important).length;
|
||||||
|
if (length === 3) {
|
||||||
|
width = 480;
|
||||||
|
} else if (length > 3) {
|
||||||
|
width = 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pipe = (this.context as PopupPipe).create({ width });
|
||||||
|
}
|
||||||
|
this.pipe.send(
|
||||||
|
<TableSetter key={field.id} {...props} />,
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={e => {
|
||||||
|
this.pipe.show((e as any).target);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="edit" />
|
||||||
|
{forceInline ? title : '编辑数组'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <ListSetter {...props} />;
|
return <ListSetter {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,14 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1 !important;
|
line-height: 1 !important;
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.lc-setter-list-empty {
|
|
||||||
text-align: center;
|
.lc-setter-list-add {
|
||||||
padding: 10px;
|
display: block;
|
||||||
.next-btn {
|
width: 100%;
|
||||||
margin-left: 5px;
|
margin-top: 8px;;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lc-setter-list-scroll-body {
|
.lc-setter-list-scroll-body {
|
||||||
@ -28,14 +29,16 @@
|
|||||||
border: 1px solid rgba(31,56,88,.2);
|
border: 1px solid rgba(31,56,88,.2);
|
||||||
background-color: var(--color-block-background-light);
|
background-color: var(--color-block-background-light);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-bottom: 8px;
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.lc-listitem {
|
.lc-listitem {
|
||||||
position: relative;
|
position: relative;
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: 32px;
|
height: 34px;
|
||||||
|
|
||||||
.lc-listitem-actions {
|
.lc-listitem-actions {
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
@ -54,9 +57,20 @@
|
|||||||
.lc-listitem-body {
|
.lc-listitem-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
.lc-field {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.next-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.lc-listitem-handler {
|
.lc-listitem-handler {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|||||||
@ -0,0 +1,167 @@
|
|||||||
|
import { Component, Fragment } from 'react';
|
||||||
|
import { Icon, Button } from '@alifd/next';
|
||||||
|
import { FieldConfig, SettingField, SetterType } from '../../main';
|
||||||
|
import { createSettingFieldView } from '../../settings-pane';
|
||||||
|
import { PopupContext, PopupPipe } from '../../popup';
|
||||||
|
import Title from '../../title';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
export default class ObjectSetter extends Component<{
|
||||||
|
field: SettingField;
|
||||||
|
descriptor?: string | ((rowField: SettingField) => string);
|
||||||
|
config: ObjectSetterConfig;
|
||||||
|
mode?: 'popup' | 'form';
|
||||||
|
// 1: in tablerow 2: in listrow 3: in column-cell
|
||||||
|
forceInline?: number;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
const { mode, forceInline = 0, ...props } = this.props;
|
||||||
|
if (forceInline || mode === 'popup') {
|
||||||
|
if (forceInline > 2 || mode === 'popup') {
|
||||||
|
// popup
|
||||||
|
return <RowSetter {...props} primaryButton={forceInline ? false : true} />;
|
||||||
|
} else {
|
||||||
|
return <RowSetter columns={forceInline > 1 ? 2 : 4} {...props} />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// form
|
||||||
|
return <FormSetter />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ObjectSetterConfig {
|
||||||
|
items?: FieldConfig[];
|
||||||
|
extraConfig?: {
|
||||||
|
setter?: SetterType;
|
||||||
|
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowSetterProps {
|
||||||
|
field: SettingField;
|
||||||
|
descriptor?: string | ((rowField: SettingField) => string);
|
||||||
|
config: ObjectSetterConfig;
|
||||||
|
columns?: number;
|
||||||
|
primaryButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RowSetter extends Component<RowSetterProps> {
|
||||||
|
static contextType = PopupContext;
|
||||||
|
|
||||||
|
state: any = {
|
||||||
|
descriptor: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
private items?: SettingField[];
|
||||||
|
constructor(props: RowSetterProps) {
|
||||||
|
super(props);
|
||||||
|
const { config, descriptor, field, columns } = props;
|
||||||
|
const items: SettingField[] = [];
|
||||||
|
if (columns && config.items) {
|
||||||
|
const l = Math.min(config.items.length, columns);
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
const conf = config.items[i];
|
||||||
|
if (conf.required || conf.important) {
|
||||||
|
const item = field.createField({
|
||||||
|
...conf,
|
||||||
|
// in column-cell
|
||||||
|
forceInline: 3,
|
||||||
|
});
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor) {
|
||||||
|
if (typeof descriptor === 'function') {
|
||||||
|
let firstRun: boolean = true;
|
||||||
|
field.onEffect(() => {
|
||||||
|
const state = {
|
||||||
|
descriptor: descriptor(field),
|
||||||
|
};
|
||||||
|
if (firstRun) {
|
||||||
|
firstRun = false;
|
||||||
|
this.state = state;
|
||||||
|
} else {
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.state = {
|
||||||
|
descriptor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// todo: onEffect change field.name
|
||||||
|
this.state = {
|
||||||
|
descriptor: field.title || `项目 ${field.name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(_: any, nextState: any) {
|
||||||
|
if (this.state.decriptor !== nextState.decriptor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private pipe: any;
|
||||||
|
render() {
|
||||||
|
const items = this.items;
|
||||||
|
const { field, primaryButton } = this.props;
|
||||||
|
|
||||||
|
if (!this.pipe) {
|
||||||
|
this.pipe = (this.context as PopupPipe).create({ width: 320 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<Fragment>
|
||||||
|
编辑:
|
||||||
|
<Title title={this.state.descriptor} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pipe.send(<FormSetter key={field.id} />, title);
|
||||||
|
|
||||||
|
if (items) {
|
||||||
|
return (
|
||||||
|
<div className="lc-setter-object-row">
|
||||||
|
<div
|
||||||
|
className="lc-setter-object-row-edit"
|
||||||
|
onClick={e => {
|
||||||
|
this.pipe.show((e as any).target);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size="small" type="edit" />
|
||||||
|
</div>
|
||||||
|
<div className="lc-setter-object-row-body">{items.map(item => createSettingFieldView(item, field))}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={primaryButton === false ? 'normal' : 'primary'}
|
||||||
|
onClick={e => {
|
||||||
|
this.pipe.show((e as any).target);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="edit" />
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// form-field setter
|
||||||
|
class FormSetter extends Component<{}> {
|
||||||
|
render() {
|
||||||
|
return 'yes';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { Component } from "react";
|
|
||||||
import { FieldConfig, SettingField } from '../../main';
|
|
||||||
|
|
||||||
class ObjectSetter extends Component<{
|
|
||||||
mode?: 'popup' | 'row' | 'form';
|
|
||||||
forceInline?: number;
|
|
||||||
}> {
|
|
||||||
render() {
|
|
||||||
const { mode, forceInline = 0 } = this.props;
|
|
||||||
if (forceInline || (mode === 'popup' || mode === 'row')) {
|
|
||||||
if (forceInline < 2 || mode === 'row') {
|
|
||||||
// row
|
|
||||||
} else {
|
|
||||||
// popup
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// form
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ObjectSetterConfig {
|
|
||||||
items?: FieldConfig[];
|
|
||||||
extraConfig?: {
|
|
||||||
setter?: SetterType;
|
|
||||||
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// for table|list row
|
|
||||||
class RowSetter extends Component<{
|
|
||||||
decriptor?: string | ((rowField: SettingField) => string);
|
|
||||||
config: ObjectSetterConfig;
|
|
||||||
columnsLimit?: number;
|
|
||||||
}> {
|
|
||||||
render() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// form-field setter
|
|
||||||
class FormSetter extends Component<{}> {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
.lc-setter-object-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
.lc-setter-object-row-edit {
|
||||||
|
width: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lc-setter-object-row-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
.lc-field {
|
||||||
|
padding: 0 !important;
|
||||||
|
.lc-field-body {
|
||||||
|
padding: 0 !important; margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
margin-left: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@
|
|||||||
}
|
}
|
||||||
> .lc-field-body {
|
> .lc-field-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import Title from './title';
|
|||||||
import SettingsPane, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane';
|
import SettingsPane, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane';
|
||||||
import Node from '../../designer/src/designer/document/node/node';
|
import Node from '../../designer/src/designer/document/node/node';
|
||||||
import ArraySetter from './builtin-setters/array-setter';
|
import ArraySetter from './builtin-setters/array-setter';
|
||||||
|
import ObjectSetter from './builtin-setters/object-setter';
|
||||||
|
|
||||||
export default class SettingsMainView extends Component {
|
export default class SettingsMainView extends Component {
|
||||||
private main: SettingsMain;
|
private main: SettingsMain;
|
||||||
@ -124,5 +125,6 @@ function selectNode(node: Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerSetter('ArraySetter', ArraySetter);
|
registerSetter('ArraySetter', ArraySetter);
|
||||||
|
registerSetter('ObjectSetter', ObjectSetter);
|
||||||
|
|
||||||
export { registerSetter, createSetterContent, getSetter, createSettingFieldView };
|
export { registerSetter, createSetterContent, getSetter, createSettingFieldView };
|
||||||
|
|||||||
142
packages/plugin-settings/src/popup/index.tsx
Normal file
142
packages/plugin-settings/src/popup/index.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { createContext, ReactNode, Component, PureComponent } from 'react';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { Balloon } from '@alifd/next';
|
||||||
|
import { uniqueId } from '../../../utils/unique-id';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
export const PopupContext = createContext<PopupPipe>({} as any);
|
||||||
|
|
||||||
|
export class PopupPipe {
|
||||||
|
private emitter = new EventEmitter();
|
||||||
|
private currentId?: string;
|
||||||
|
|
||||||
|
create(props?: object): { send: (content: ReactNode, title: ReactNode) => void; show: (target: Element) => void } {
|
||||||
|
let sendContent: ReactNode = null;
|
||||||
|
let sendTitle: ReactNode = null;
|
||||||
|
const id = uniqueId('popup');
|
||||||
|
return {
|
||||||
|
send: (content: ReactNode, title: ReactNode) => {
|
||||||
|
sendContent = content;
|
||||||
|
sendTitle = title;
|
||||||
|
if (this.currentId === id) {
|
||||||
|
this.popup({
|
||||||
|
...props,
|
||||||
|
content,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
show: (target: Element) => {
|
||||||
|
this.currentId = id;
|
||||||
|
this.popup({
|
||||||
|
...props,
|
||||||
|
content: sendContent,
|
||||||
|
title: sendTitle,
|
||||||
|
}, target);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private popup(props: object, target?: Element) {
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
this.emitter.emit('popupchange', props, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPopupChange(fn: (props: object, target?: Element) => void): () => void {
|
||||||
|
this.emitter.on('popupchange', fn);
|
||||||
|
return () => {
|
||||||
|
this.emitter.removeListener('popupchange', fn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
purge() {
|
||||||
|
this.emitter.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PopupService extends Component<{ safeId?: string }> {
|
||||||
|
private popupPipe = new PopupPipe();
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.popupPipe.purge();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PopupContext.Provider value={this.popupPipe}>
|
||||||
|
{this.props.children}
|
||||||
|
<PopupContent safeId={this.props.safeId} />
|
||||||
|
</PopupContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PopupContent extends PureComponent<{ safeId?: string }> {
|
||||||
|
static contextType = PopupContext;
|
||||||
|
state: any = {
|
||||||
|
visible: false,
|
||||||
|
pos: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
private dispose = (this.context as PopupPipe).onPopupChange((props, target) => {
|
||||||
|
const state: any = {
|
||||||
|
...props,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
if (target) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
state.pos = {
|
||||||
|
top: rect.top,
|
||||||
|
height: rect.height,
|
||||||
|
};
|
||||||
|
// todo: compute the align method
|
||||||
|
}
|
||||||
|
this.setState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { content, visible, width, title, pos } = this.state;
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let avoidLaterHidden = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
avoidLaterHidden = false;
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
const id = uniqueId('ball');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Balloon
|
||||||
|
className="lc-ballon"
|
||||||
|
align="l"
|
||||||
|
id={this.props.safeId}
|
||||||
|
safeId={this.props.safeId}
|
||||||
|
safeNode={id}
|
||||||
|
visible={visible}
|
||||||
|
style={{ width }}
|
||||||
|
onVisibleChange={visible => {
|
||||||
|
if (avoidLaterHidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!visible) {
|
||||||
|
this.setState({ visible: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
trigger={<div className="lc-popup-placeholder" style={pos} />}
|
||||||
|
triggerType="click"
|
||||||
|
animation={false}
|
||||||
|
// needAdjust
|
||||||
|
shouldUpdatePosition
|
||||||
|
>
|
||||||
|
<div className="lc-ballon-title">{title}</div>
|
||||||
|
<div className="lc-ballon-content"><PopupService safeId={id}>{content}</PopupService></div>
|
||||||
|
</Balloon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/plugin-settings/src/popup/style.less
Normal file
22
packages/plugin-settings/src/popup/style.less
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.lc-popup-placeholder {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lc-ballon {
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 640px;
|
||||||
|
width: 640px;
|
||||||
|
.lc-ballon-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.lc-ballon-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
// width: 300px;
|
||||||
|
}
|
||||||
|
.next-balloon-close {
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ import {
|
|||||||
} from './main';
|
} from './main';
|
||||||
import { Field, FieldGroup } from './field';
|
import { Field, FieldGroup } from './field';
|
||||||
import { TitleContent } from './title';
|
import { TitleContent } from './title';
|
||||||
|
import { Balloon } from '@alifd/next';
|
||||||
|
import PopupService from './popup';
|
||||||
|
|
||||||
export type RegisteredSetter = {
|
export type RegisteredSetter = {
|
||||||
component: CustomView;
|
component: CustomView;
|
||||||
@ -229,10 +231,6 @@ export function createSettingFieldView(item: SettingField | CustomView, field: S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showPopup() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SettingsPane extends Component<{ target: SettingTarget }> {
|
export default class SettingsPane extends Component<{ target: SettingTarget }> {
|
||||||
state: { items: Array<SettingField | CustomView> } = {
|
state: { items: Array<SettingField | CustomView> } = {
|
||||||
items: [],
|
items: [],
|
||||||
@ -272,7 +270,12 @@ export default class SettingsPane extends Component<{ target: SettingTarget }> {
|
|||||||
const { target } = this.props;
|
const { target } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="lc-settings-pane">
|
<div className="lc-settings-pane">
|
||||||
{items.map((item, index) => createSettingFieldView(item, target, index))}
|
{/* todo: add head for single use */}
|
||||||
|
<PopupService>
|
||||||
|
<div className="lc-settings-content">
|
||||||
|
{items.map((item, index) => createSettingFieldView(item, target, index))}
|
||||||
|
</div>
|
||||||
|
</PopupService>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user