object setter 50%

This commit is contained in:
kangwei 2020-03-12 03:47:03 +08:00
parent 901e06ac01
commit 0042a2ebe2
15 changed files with 560 additions and 129 deletions

View File

@ -218,18 +218,27 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
// 事件路由
doc.addEventListener('mousedown', (downEvent: MouseEvent) => {
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
if (!nodeInst?.node) {
selection.clear();
return;
}
const node = nodeInst?.node || this.document.rootNode;
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
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) {
const node: Node = nodeInst.node;
if (isLeftButton && !isRootNode(node)) {
let nodes: Node[] = [node];
let ignoreUpSelected = false;
// 排除根节点拖拽
selection.remove(this.document.rootNode.id);
if (isMulti) {
// multi select mode, directily add
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);
});

View File

@ -154,23 +154,56 @@ export class ComponentType {
setter: 'StringSetter',
},
{
name: 'name',
title: '名称',
name: 'data',
title: '数据',
setter: {
componentName: 'ArraySetter',
props: {
itemConfig: {
setter: 'StringSetter',
defaultValue: '',
setter: {
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',
title: '年龄',

View File

@ -76,7 +76,7 @@ export default class Prop implements IPropParent {
}
return this.items!.map(prop => {
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);
}
set code(val) {
// todo
}
@computed getAsString(): string {
@ -205,6 +205,7 @@ export default class Prop implements IPropParent {
if (this.type === 'list') {
return this.size === other.size ? 1 : 2;
}
// 'literal' | 'map' | 'expression' | 'slot'
return this.code === other.code ? 0 : 2;
}
@ -548,7 +549,7 @@ export function isProp(obj: any): obj is Prop {
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));
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
}

View File

@ -1,6 +1,7 @@
import { obx, computed } from '@recore/obx';
import { INodeSelector, IViewport } from '../simulator';
import { uniqueId } from '../../../../utils/unique-id';
import { isRootNode } from '../document/node/root-node';
export default class OffsetObserver {
readonly id = uniqueId('oobx');
@ -17,25 +18,25 @@ export default class OffsetObserver {
@obx hasOffset = false;
@computed get offsetLeft() {
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;
}
@computed get offsetTop() {
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;
}
@computed get offsetHeight() {
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;
}
@computed get offsetWidth() {
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;
}
@ -46,12 +47,18 @@ export default class OffsetObserver {
private pid: number | undefined;
private viewport: IViewport;
private isRoot: boolean;
constructor(readonly nodeInstance: INodeSelector) {
const { node, instance } = nodeInstance;
const doc = node.document;
const host = doc.simulator!;
this.isRoot = isRootNode(node);
this.viewport = host.viewport;
if (this.isRoot) {
this.hasOffset = true;
return;
}
if (!instance) {
return;
}

View File

@ -1 +1,15 @@
属性面板
其中 field 可独立出去一个包
提供:
1. 快捷设置面板服务
2. 对应节点的设置面板服务
3. 右侧设置面板
4. 提供 setters 服务setters 注册、获取机制
依赖:
tip 处理
field
popup

View File

@ -1,9 +1,11 @@
import { Component } from 'react';
import { Component, Fragment } from 'react';
import { Icon, Button, Message } from '@alifd/next';
import Sortable from './sortable';
import { SettingField, SetterType } from '../../main';
import { SettingField, SetterType, FieldConfig } from '../../main';
import './style.less';
import { createSettingFieldView } from '../../settings-pane';
import { PopupContext, PopupPipe } from '../../popup';
import Title from '../../title';
interface ArraySetterState {
items: SettingField[];
@ -45,7 +47,7 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
const item = field.createField({
...props.itemConfig,
name: i,
forceInline: 1,
forceInline: 2,
});
items[i] = item;
itemsMap.set(item.id, item);
@ -140,37 +142,39 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
this.scrollToLast = false;
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 (
<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)}>
<Icon type="add" />
<span></span>
</Button>
</div>
{this.props.multiValue && <Message type="warning"></Message>}
{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>
) : (
<div className="lc-setter-list-empty">
<Button size="small" onClick={this.onAdd.bind(this)}>
<Icon type="add" />
<span></span>
</Button>
</div>
)}
</div>*/}
{content}
<Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}>
<Icon type="add" />
<span></span>
</Button>
</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<{
value: any[];
@ -218,17 +226,52 @@ export default class ArraySetter extends Component<{
defaultValue?: any | ((field: SettingField) => any);
required?: boolean;
};
mode?: 'popup' | 'list' | 'table';
mode?: 'popup' | 'list';
forceInline?: boolean;
multiValue?: boolean;
}> {
static contextType = PopupContext;
private pipe: any;
render() {
const { mode, forceInline, ...props } = this.props;
const { field, itemConfig } = props;
if (mode === 'popup' || forceInline) {
// todo popup
return <Button></Button>;
} else if (mode === 'table') {
return <TableSetter {...props} />;
const title = (
<Fragment>
<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 {
return <ListSetter {...props} />;
}

View File

@ -8,13 +8,14 @@
display: inline-flex;
align-items: center;
line-height: 1 !important;
max-width: 100%;
text-overflow: ellipsis;
}
.lc-setter-list-empty {
text-align: center;
padding: 10px;
.next-btn {
margin-left: 5px;
}
.lc-setter-list-add {
display: block;
width: 100%;
margin-top: 8px;;
}
.lc-setter-list-scroll-body {
@ -28,14 +29,16 @@
border: 1px solid rgba(31,56,88,.2);
background-color: var(--color-block-background-light);
border-radius: 3px;
margin-bottom: 8px;
&:not(:last-child) {
margin-bottom: 5px;
}
.lc-listitem {
position: relative;
outline: none;
display: flex;
align-items: stretch;
height: 32px;
height: 34px;
.lc-listitem-actions {
margin: 0 3px;
@ -54,9 +57,20 @@
.lc-listitem-body {
flex: 1;
display: flex;
align-items: center;
align-items: stretch;
overflow: hidden;
min-width: 0;
text-overflow: ellipsis;
.lc-field {
padding: 0 !important;
}
> * {
width: 100%;
}
.next-btn {
display: block;
width: 100%;
}
}
.lc-listitem-handler {
margin-left: 2px;

View File

@ -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';
}
}

View File

@ -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<{}> {
}

View File

@ -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;
}
}
}

View File

@ -37,6 +37,7 @@
}
> .lc-field-body {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}

View File

@ -6,6 +6,7 @@ import Title from './title';
import SettingsPane, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane';
import Node from '../../designer/src/designer/document/node/node';
import ArraySetter from './builtin-setters/array-setter';
import ObjectSetter from './builtin-setters/object-setter';
export default class SettingsMainView extends Component {
private main: SettingsMain;
@ -124,5 +125,6 @@ function selectNode(node: Node) {
}
registerSetter('ArraySetter', ArraySetter);
registerSetter('ObjectSetter', ObjectSetter);
export { registerSetter, createSetterContent, getSetter, createSettingFieldView };

View 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>
);
}
}

View 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;
}
}

View File

@ -12,6 +12,8 @@ import {
} from './main';
import { Field, FieldGroup } from './field';
import { TitleContent } from './title';
import { Balloon } from '@alifd/next';
import PopupService from './popup';
export type RegisteredSetter = {
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 }> {
state: { items: Array<SettingField | CustomView> } = {
items: [],
@ -272,7 +270,12 @@ export default class SettingsPane extends Component<{ target: SettingTarget }> {
const { target } = this.props;
return (
<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>
);
}