complete array-setter

This commit is contained in:
kangwei 2020-03-10 00:39:00 +08:00
parent 72a7d83f02
commit b59934eb2f
21 changed files with 838 additions and 179 deletions

View File

@ -6,7 +6,7 @@
flex: 1;
min-width: 0;
}
.lc-settings-pane {
.lc-settings-main {
width: 300px;
border-left: 1px solid rgba(31,56,88,.1);
}

View File

@ -153,7 +153,15 @@ export class ComponentType {
}, {
name: 'name',
title: '名称',
setter: 'StringSetter'
setter: {
componentName: 'ArraySetter',
props: {
itemConfig: {
setter: 'StringSetter',
defaultValue: ''
}
}
}
}, {
name: 'size',
title: '大小',

View File

@ -272,7 +272,7 @@ export default class Node {
*
*/
setPropValue(path: string, value: any) {
this.getProp(path, true)!.value = value;
this.getProp(path, true)!.setValue(value);
}
/**

View File

@ -5,7 +5,7 @@ import Props from './props';
export type PendingItem = Prop[];
export default class PropStash implements IPropParent {
@obx.val private space: Set<Prop> = new Set();
@computed private get maps(): Map<string, Prop> {
@computed private get maps(): Map<string | number, Prop> {
const maps = new Map();
if (this.space.size > 0) {
this.space.forEach(prop => {
@ -38,7 +38,7 @@ export default class PropStash implements IPropParent {
});
}
get(key: string): Prop {
get(key: string | number): Prop {
let prop = this.maps.get(key);
if (!prop) {
prop = new Prop(this, UNSET, key);

View File

@ -34,15 +34,15 @@ export default class Prop implements IPropParent {
/**
*
*/
@computed get value(): CompositeValue {
@computed get value(): CompositeValue | UNSET {
return this.export(true);
}
export(serialize = false): CompositeValue {
export(serialize = false): CompositeValue | UNSET {
const type = this._type;
if (type === 'unset') {
return null;
return UNSET;
}
if (type === 'literal' || type === 'expression') {
@ -62,7 +62,10 @@ export default class Prop implements IPropParent {
}
const maps: any = {};
this.items!.forEach((prop, key) => {
maps[key] = prop.value;
const v = prop.export(serialize);
if (v !== UNSET) {
maps[key] = v;
}
});
return maps;
}
@ -71,7 +74,10 @@ export default class Prop implements IPropParent {
if (!this._items) {
return this._items;
}
return this.items!.map(prop => prop.value);
return this.items!.map(prop => {
const v = prop.export(serialize);
return v === UNSET ? null : v
});
}
return null;
@ -103,7 +109,7 @@ export default class Prop implements IPropParent {
/**
* set value, val should be JSON Object
*/
set value(val: CompositeValue) {
setValue(val: CompositeValue) {
this._value = val;
const t = typeof val;
if (val == null) {
@ -183,44 +189,24 @@ export default class Prop implements IPropParent {
return this._type === 'unset';
}
isEqual(otherProp: Prop | null): boolean {
if (!otherProp) {
return this.isUnset();
// TODO: improve this logic
compare(other: Prop | null): number {
if (!other || other.isUnset()) {
return this.isUnset() ? 0 : 2;
}
if (otherProp.type !== this.type) {
return false;
if (other.type !== this.type) {
return 2;
}
// 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot'
return this.code === otherProp.code;
}
/**
* JS
* JSExpresion | JSSlot
*/
@computed isTypedJS(): boolean {
const type = this._type;
if (type === 'expression' || type === 'slot') {
return true;
// list
if (this.type === 'list') {
return this.size === other.size ? 1 : 2;
}
if (type === 'literal' || type === 'unset') {
return false;
}
if ((type === 'list' || type === 'map') && this.items) {
return this.items.some(item => item.isTypedJS());
}
return false;
}
/**
* JSON
*/
@computed isJSON() {
return !this.isTypedJS();
// 'literal' | 'map' | 'expression' | 'slot'
return this.code === other.code ? 0 : 2;
}
@obx.val private _items: Prop[] | null = null;
@obx.val private _maps: Map<string, Prop> | null = null;
@obx.val private _maps: Map<string | number, Prop> | null = null;
@computed private get items(): Prop[] | null {
let _items: any;
untracked(() => {
@ -255,8 +241,8 @@ export default class Prop implements IPropParent {
}
return _items;
}
@computed private get maps(): Map<string, Prop> | null {
if (!this.items || this.items.length < 1) {
@computed private get maps(): Map<string | number, Prop> | null {
if (!this.items) {
return null;
}
return this._maps;
@ -283,7 +269,7 @@ export default class Prop implements IPropParent {
) {
this.props = parent.props;
if (value !== UNSET) {
this.value = value;
this.setValue(value);
}
this.key = key;
this.spread = spread;
@ -291,43 +277,50 @@ export default class Prop implements IPropParent {
/**
*
* @param stash
* @param stash
*/
get(path: string, stash: false): Prop | null;
/**
* ,
* @param stash
*/
get(path: string, stash: true): Prop;
/**
* ,
*/
get(path: string): Prop;
get(path: string, stash = true) {
get(path: string | number, stash = true): Prop | null {
const type = this._type;
// todo: support list get
if (type !== 'map' && type !== 'unset' && !stash) {
if (type !== 'map' && type !== 'list' && type !== 'unset' && !stash) {
return null;
}
const maps = type === 'map' ? this.maps : null;
let prop: any = maps ? maps.get(path) : null;
const items = type === 'list' ? this.items : null;
let prop: any;
if (type === 'list') {
if (isValidArrayIndex(path, this.size)) {
prop = items![path];
}
} else if (type === 'map') {
prop = maps?.get(path);
}
if (prop) {
return prop;
}
const i = path.indexOf('.');
let entry = path;
let nest = '';
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
prop = maps ? maps.get(entry) : null;
if (prop) {
return prop.get(nest, stash);
if (typeof path !== 'number') {
const i = path.indexOf('.');
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
if (type === 'list') {
if (isValidArrayIndex(entry, this.size)) {
prop = items![entry];
}
} else if (type === 'map') {
prop = maps?.get(entry);
}
if (prop) {
return prop.get(nest, stash);
}
}
}
}
@ -336,7 +329,9 @@ export default class Prop implements IPropParent {
if (!this.stash) {
this.stash = new PropStash(this.props, item => {
// item take effect
this.set(String(item.key), item);
if (item.key) {
this.set(item.key, item, true);
}
item.parent = this;
});
}
@ -389,7 +384,7 @@ export default class Prop implements IPropParent {
/**
*
*/
size(): number {
get size(): number {
return this.items?.length || 0;
}
@ -404,7 +399,7 @@ export default class Prop implements IPropParent {
return null;
}
if (type === 'unset' || (force && type !== 'list')) {
this.value = [];
this.setValue([]);
}
const prop = new Prop(this, value);
this.items!.push(prop);
@ -416,29 +411,44 @@ export default class Prop implements IPropParent {
*
* @param force
*/
set(key: string, value: CompositeValue | Prop, force = false) {
set(key: string | number, value: CompositeValue | Prop, force = false) {
const type = this._type;
if (type !== 'map' && type !== 'unset' && !force) {
if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'map')) {
this.value = {};
if (isValidArrayIndex(key)) {
if (type !== 'list') {
this.setValue([]);
}
} else {
this.setValue({});
}
}
const prop = isProp(value) ? value : new Prop(this, value, key);
const items = this.items!;
const maps = this.maps!;
const orig = maps.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
if (this.type === 'list') {
if (!isValidArrayIndex(key)) {
return null;
}
items[key] = prop;
} else if (this.maps) {
const maps = this.maps;
const orig = maps.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
}
maps.set(key, prop);
} else {
// push
items.push(prop);
maps.set(key, prop);
}
maps.set(key, prop);
} else {
// push
items.push(prop);
maps.set(key, prop);
return null;
}
return prop;
@ -533,3 +543,8 @@ export default class Prop implements IPropParent {
export function isProp(obj: any): obj is Prop {
return obj && obj.isProp;
}
function isValidArrayIndex(key: any, limit: number = -1): key is number {
const n = parseFloat(String(key));
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
}

View File

@ -2,12 +2,9 @@ import { computed, obx } from '@recore/obx';
import { uniqueId } from '../../../../../../utils/unique-id';
import { CompositeValue, PropsList, PropsMap } from '../../../schema';
import PropStash from './prop-stash';
import Prop, { IPropParent } from './prop';
import Prop, { IPropParent, UNSET } from './prop';
import { NodeParent } from '../node';
export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET;
export default class Props implements IPropParent {
readonly id = uniqueId('props');
@obx.val private items: Prop[] = [];
@ -56,6 +53,7 @@ export default class Props implements IPropParent {
import(value?: PropsMap | PropsList | null) {
this.stash.clear();
const originItems = this.items;
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
@ -66,12 +64,12 @@ export default class Props implements IPropParent {
this.type = 'map';
this.items = [];
}
this.items.forEach(item => item.purge());
originItems.forEach(item => item.purge());
}
merge(value: PropsMap) {
Object.keys(value).forEach(key => {
this.query(key).value = value[key];
this.query(key, true)!.setValue(value[key]);
});
}
@ -80,11 +78,14 @@ export default class Props implements IPropParent {
return null;
}
if (this.type === 'list') {
return this.items.map(item => ({
spread: item.spread,
name: item.key as string,
value: item.export(serialize),
}));
return this.items.map(item => {
const v = item.export(serialize);
return {
spread: item.spread,
name: item.key as string,
value: v === UNSET ? null : v,
};
});
}
const maps: any = {};
this.items.forEach(prop => {
@ -95,26 +96,12 @@ export default class Props implements IPropParent {
return maps;
}
/**
* path
*/
query(path: string): Prop;
/**
* path
*
* @useStash
*/
query(path: string, useStash: true): Prop;
/**
* path
*/
query(path: string, useStash: false): Prop | null;
/**
* path
*
* @useStash
*/
query(path: string, useStash: boolean = true) {
query(path: string, useStash: boolean = true): Prop | null {
let matchedLength = 0;
let firstMatched = null;
if (this.items) {
@ -156,17 +143,7 @@ export default class Props implements IPropParent {
* ,
* @param useStash
*/
get(path: string, useStash: true): Prop;
/**
*
* @param useStash
*/
get(path: string, useStash: false): Prop | null;
/**
*
*/
get(path: string): Prop | null;
get(name: string, useStash = false) {
get(name: string, useStash = false): Prop | null {
return this.maps.get(name) || (useStash && this.stash.get(name)) || null;
}

View File

@ -0,0 +1,233 @@
import { Component } from 'react';
import { Icon, Button, Message } from '@alifd/next';
import Sortable from './sortable';
import { SettingField, SetterType } from '../../main';
import './style.less';
import { createSettingFieldView } from '../../settings-pane';
interface ArraySetterState {
items: SettingField[];
itemsMap: Map<string | number, SettingField>;
prevLength: number;
}
export class ListSetter extends Component<
{
value: any[];
field: SettingField;
itemConfig?: {
setter?: SetterType;
defaultValue?: any | ((field: SettingField, editor: any) => any);
required?: boolean;
};
multiValue?: boolean;
},
ArraySetterState
> {
static getDerivedStateFromProps(props: any, state: ArraySetterState) {
const { value, field } = props;
const newLength = value && Array.isArray(value) ? value.length : 0;
if (state && state.prevLength === newLength) {
return null;
}
// props value length change will go here
const originLength = state ? state.items.length : 0;
if (state && originLength === newLength) {
return {
prevLength: newLength,
};
}
const itemsMap = state ? state.itemsMap : new Map<string | number, SettingField>();
let items = state ? state.items.slice() : [];
if (newLength > originLength) {
for (let i = originLength; i < newLength; i++) {
const item = field.createField({
...props.itemConfig,
name: i,
forceInline: 1,
});
items[i] = item;
itemsMap.set(item.id, item);
}
} else if (newLength < originLength) {
const deletes = items.splice(newLength);
deletes.forEach(item => {
itemsMap.delete(item.id);
});
}
return {
items,
itemsMap,
prevLength: newLength,
};
}
onSort(sortedIds: Array<string | number>) {
const { itemsMap } = this.state;
const items = sortedIds.map((id, index) => {
const item = itemsMap.get(id)!;
item.setKey(index);
return item;
});
this.setState({
items,
});
}
private scrollToLast: boolean = false;
onAdd() {
const { items, itemsMap } = this.state;
const { itemConfig } = this.props;
const defaultValue = itemConfig ? itemConfig.defaultValue : null;
const item = this.props.field.createField({
...itemConfig,
name: items.length,
forceInline: 1,
});
items.push(item);
itemsMap.set(item.id, item);
item.setValue(typeof defaultValue === 'function' ? defaultValue(item, item.editor) : defaultValue);
this.scrollToLast = true;
this.setState({
items: items.slice(),
});
}
onRemove(field: SettingField) {
const { items } = this.state;
let i = items.indexOf(field);
if (i < 0) {
return;
}
items.splice(i, 1);
const l = items.length;
while (i < l) {
items[i].setKey(i);
i++;
}
field.remove();
this.setState({ items: items.slice() });
}
componentWillUnmount() {
this.state.items.forEach(field => {
field.purge();
});
}
shouldComponentUpdate(_: any, nextState: ArraySetterState) {
if (nextState.items !== this.state.items) {
return true;
}
return false;
}
render() {
// mini Button: depends popup
if (this.props.itemConfig) {
// check is ObjectSetter then check if show columns
}
const { items } = this.state;
const scrollToLast = this.scrollToLast;
this.scrollToLast = false;
const lastIndex = items.length - 1;
return (
<div className="lc-setter-list lc-block-setter">
<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>
);
}
}
class ArrayItem extends Component<{
field: SettingField;
onRemove: () => void;
scrollIntoView: boolean;
}> {
shouldComponentUpdate() {
return false;
}
private shell?: HTMLDivElement | null;
componentDidMount() {
if (this.props.scrollIntoView && this.shell) {
this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
render() {
const { onRemove, field } = this.props;
return (
<div className="lc-listitem" ref={ref => (this.shell = ref)}>
<div draggable className="lc-listitem-handler">
<Icon type="ellipsis" size="small" />
</div>
<div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div>
<div className="lc-listitem-actions">
<div className="lc-listitem-action" onClick={onRemove}>
<Icon type="ashbin" size="small" />
</div>
</div>
</div>
);
}
}
class TableSetter extends ListSetter {
}
export default class ArraySetter extends Component<
{
value: any[];
field: SettingField;
itemConfig?: {
setter?: SetterType;
defaultValue?: any | ((field: SettingField, editor: any) => any);
required?: boolean;
};
mode?: 'popup' | 'list' | 'table';
forceInline?: boolean;
multiValue?: boolean;
}> {
render() {
const { mode, forceInline, ...props } = this.props;
if (mode === 'popup' || forceInline) {
// todo popup
return <Button></Button>;
} else if (mode === 'table') {
return <TableSetter {...props} />;
} else {
return <ListSetter {...props} />;
}
}
}

View File

@ -0,0 +1,29 @@
.lc-sortable {
position: relative;
.lc-sortable-card {
box-sizing: border-box;
&:after, &:before {
content: "";
display: table;
}
&:after {
clear: both;
}
&.lc-dragging {
outline: 2px dashed var(--color-brand);
outline-offset: -2px;
> * {
visibility: hidden;
}
border-color: transparent !important;
box-shadow: none !important;
background: transparent !important;
}
}
[draggable] {
cursor: ns-resize;
}
}

View File

@ -0,0 +1,220 @@
import { Component, Children, ReactElement } from 'react';
import classNames from 'classnames';
import './sortable.less';
class Sortable extends Component<{
className?: string;
itemClassName?: string;
onSort?: (sortedIds: Array<string | number>) => void;
dragImageSourceHandler?: (elem: Element) => Element;
children: ReactElement[];
}> {
private shell?: HTMLDivElement | null;
private items?: Array<string | number>;
private willDetach?: () => void;
componentDidMount() {
const box = this.shell!;
let isDragEnd: boolean = false;
/**
* target node to be dragged
*/
let source: Element | null;
/**
* node to be placed
*/
let ref: Element | null;
/**
* next sibling of the source node
*/
let origRef: Element | null;
/**
* accurately locate the node from event
*/
const locate = (e: DragEvent) => {
let y = e.clientY;
if (e.view !== window && e.view!.frameElement) {
y += e.view!.frameElement.getBoundingClientRect().top;
}
let node = box.firstElementChild as HTMLDivElement;
while (node) {
if (node !== source && node.dataset.id) {
const rect = node.getBoundingClientRect();
if (rect.height <= 0) continue;
if (y < rect.top + rect.height / 2) {
break;
}
}
node = node.nextElementSibling as HTMLDivElement;
}
return node;
};
/**
* find the source node
*/
const getSource = (e: DragEvent) => {
const target = e.target as Element;
if (!target || !box.contains(target) || target === box) {
return null;
}
let node = box.firstElementChild;
while (node) {
if (node.contains(target)) {
return node;
}
node = node.nextElementSibling;
}
return null;
};
const sort = (beforeId: string | number | null | undefined) => {
if (!source) return;
const sourceId = (source as HTMLDivElement).dataset.id;
const items = this.items!;
const origIndex = items.findIndex(id => id == sourceId);
let newIndex = beforeId ? items.findIndex(id => id == beforeId) : items.length;
if (origIndex < 0 || newIndex < 0) return;
if (this.props.onSort) {
if (newIndex > origIndex) {
newIndex -= 1;
}
if (origIndex === newIndex) return;
const item = items.splice(origIndex, 1);
items.splice(newIndex, 0, item[0]);
this.props.onSort(items);
}
};
const dragstart = (e: DragEvent) => {
isDragEnd = false;
source = getSource(e);
if (!source) {
return false;
}
origRef = source.nextElementSibling;
const rect = source.getBoundingClientRect();
let dragSource = source;
if (this.props.dragImageSourceHandler) {
dragSource = this.props.dragImageSourceHandler(source);
}
if (e.dataTransfer) {
e.dataTransfer.setDragImage(dragSource, e.clientX - rect.left, e.clientY - rect.top);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move';
try {
e.dataTransfer.setData('application/json', {} as any);
} catch (ex) {
// eslint-disable-line
}
}
setTimeout(() => {
source!.classList.add('lc-dragging');
}, 0);
return true;
};
const placeAt = (beforeRef: Element | null) => {
if (beforeRef) {
if (beforeRef !== source) {
box.insertBefore(source!, beforeRef);
}
} else {
box.appendChild(source!);
}
};
const adjust = (e: DragEvent) => {
if (isDragEnd) return;
ref = locate(e);
placeAt(ref);
};
let lastDragEvent: DragEvent | null;
const drag = (e: DragEvent) => {
if (!source) return;
e.preventDefault();
if (lastDragEvent) {
if (lastDragEvent.clientX === e.clientX && lastDragEvent.clientY === e.clientY) {
return;
}
}
lastDragEvent = e;
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
}
adjust(e);
};
const dragend = (e: DragEvent) => {
isDragEnd = true;
if (!source) return;
e.preventDefault();
source.classList.remove('lc-dragging');
placeAt(origRef);
sort(ref ? (ref as HTMLDivElement).dataset.id : null);
source = null;
ref = null;
origRef = null;
lastDragEvent = null;
};
box.addEventListener('dragstart', dragstart);
document.addEventListener('dragover', drag);
document.addEventListener('drag', drag);
document.addEventListener('dragend', dragend);
this.willDetach = () => {
box.removeEventListener('dragstart', dragstart);
document.removeEventListener('dragover', drag);
document.removeEventListener('drag', drag);
document.removeEventListener('dragend', dragend);
};
}
componentWillUnmount() {
if (this.willDetach) {
this.willDetach();
}
}
render() {
const { className, itemClassName, children } = this.props;
const items: Array<string | number> = [];
const cards = Children.map(children, child => {
const id = child.key!;
items.push(id);
return (
<div key={id} data-id={id} className={classNames('lc-sortable-card', itemClassName)}>
{child}
</div>
);
});
this.items = items;
return (
<div
className={classNames('lc-sortable', className)}
ref={ref => {
this.shell = ref;
}}
>
{cards}
</div>
);
}
}
export default Sortable;

View File

@ -0,0 +1,72 @@
.lc-setter-list {
[draggable] {
cursor: move;
}
color: var(--color-text);
.next-btn {
display: inline-flex;
align-items: center;
line-height: 1 !important;
}
.lc-setter-list-empty {
text-align: center;
padding: 10px;
.next-btn {
margin-left: 5px;
}
}
.lc-setter-list-scroll-body {
margin: -8px -5px;
padding: 8px 10px;
overflow-y: auto;
max-height: 300px;
}
.lc-setter-list-card {
border: 1px solid rgba(31,56,88,.2);
background-color: var(--color-block-background-light);
border-radius: 3px;
margin-bottom: 8px;
.lc-listitem {
position: relative;
outline: none;
display: flex;
align-items: stretch;
height: 32px;
.lc-listitem-actions {
margin: 0 3px;
display: inline-flex;
align-items: center;
justify-content: flex-end;
.lc-listitem-action {
text-align: center;
cursor: pointer;
opacity: 0.6;
&:hover {
opacity: 1;
}
}
}
.lc-listitem-body {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
}
.lc-listitem-handler {
margin-left: 2px;
display: inline-flex;
align-items: center;
.next-icon-ellipsis {
transform: rotate(90deg);
}
opacity: 0.6;
}
}
}
}

View File

@ -0,0 +1,43 @@
import { Component } from "react";
import { FieldConfig } 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<{
config: ObjectSetterConfig;
columnsLimit?: number;
}> {
render() {
}
}
// form-field setter
class FormSetter extends Component<{}> {
}

View File

@ -68,6 +68,18 @@
}
}
&.lc-block-field {
position: relative;
>.lc-field-body>.lc-block-setter>.lc-block-setter-actions {
position: absolute;
right: 10px;
top: 0;
height: 32px;
display: flex;
align-items: center;
}
}
&.lc-accordion-field {
// collapsed
&.lc-field-is-collapsed {

View File

@ -7,7 +7,7 @@ import './index.less';
export interface FieldProps {
className?: string;
// span
title?: TitleContent;
title?: TitleContent | null;
}
export class Field extends Component<FieldProps> {

View File

@ -3,10 +3,11 @@ import { Tab, Breadcrumb, Icon } from '@alifd/next';
import { SettingsMain, SettingField, isSettingField } from './main';
import './style.less';
import Title from './title';
import SettingsTab, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-tab';
import SettingsTab, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane';
import Node from '../../designer/src/designer/document/node/node';
import ArraySetter from './builtin-setters/array-setter';
export default class SettingsPane extends Component {
export default class SettingsMainView extends Component {
private main: SettingsMain;
constructor(props: any) {
@ -65,7 +66,7 @@ export default class SettingsPane extends Component {
if (this.main.isNone) {
// 未选中节点,提示选中 或者 显示根节点设置
return (
<div className="lc-settings-pane">
<div className="lc-settings-main">
<div className="lc-settings-notice">
<p></p>
</div>
@ -76,7 +77,7 @@ export default class SettingsPane extends Component {
if (!this.main.isSame) {
// todo: future support 获取设置项交集编辑
return (
<div className="lc-settings-pane">
<div className="lc-settings-main">
<div className="lc-settings-notice">
<p></p>
</div>
@ -87,7 +88,7 @@ export default class SettingsPane extends Component {
const { items } = this.main;
if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) {
return (
<div className="lc-settings-pane">
<div className="lc-settings-main">
{this.renderBreadcrumb()}
<div className="lc-settings-body">
<SettingsTab target={this.main} />
@ -97,7 +98,7 @@ export default class SettingsPane extends Component {
}
return (
<div className="lc-settings-pane">
<div className="lc-settings-main">
<Tab
navClassName="lc-settings-tabs"
animation={false}
@ -122,4 +123,6 @@ function selectNode(node: Node) {
node.select();
}
registerSetter('ArraySetter', ArraySetter);
export { registerSetter, createSetterContent, getSetter, createSettingFieldView };

View File

@ -51,10 +51,10 @@ export interface SettingTarget {
onEffect(action: () => void): () => void;
// 获取属性值
getPropValue(propName: string): any;
getPropValue(propName: string | number): any;
// 设置属性值
setPropValue(path: string, value: any): void;
setPropValue(propName: string | number, value: any): void;
/*
// 所有属性值数据
@ -94,6 +94,10 @@ export interface SetterConfig {
export type SetterType = SetterConfig | string | CustomView;
export interface FieldExtraProps {
/**
*
*/
required?: boolean;
/**
* default value of target prop for setter use
*/
@ -109,6 +113,14 @@ export interface FieldExtraProps {
* default collapsed when display accordion
*/
defaultCollapsed?: boolean;
/**
* important field
*/
important?: boolean;
/**
* internal use
*/
forceInline?: number;
}
export interface FieldConfig extends FieldExtraProps {
@ -116,7 +128,7 @@ export interface FieldConfig extends FieldExtraProps {
/**
* the name of this setting field, which used in quickEditor
*/
name: string;
name: string | number;
/**
* the field title
* @default sameas .name
@ -141,7 +153,10 @@ export class SettingField implements SettingTarget {
readonly id = uniqueId('field');
readonly type: 'field' | 'virtual-field' | 'group';
readonly isGroup: boolean;
readonly name: string;
private _name: string | number;
get name() {
return this._name;
}
readonly title: TitleContent;
readonly editor: any;
readonly extraProps: FieldExtraProps;
@ -153,13 +168,19 @@ export class SettingField implements SettingTarget {
readonly nodes: Node[];
readonly componentType: ComponentType | null;
readonly designer: Designer;
readonly path: string[];
get path() {
const path = this.parent.path.slice();
if (this.type === 'field') {
path.push(String(this.name));
}
return path;
}
constructor(readonly parent: SettingTarget, config: FieldConfig) {
const { type, title, name, items, setter, extraProps, ...rest } = config;
if (type == null) {
const c = name.substr(0, 1);
const c = typeof name === 'string' ? name.substr(0, 1) : '';
if (c === '#') {
this.type = 'group';
} else if (c === '!') {
@ -171,8 +192,8 @@ export class SettingField implements SettingTarget {
this.type = type;
}
// initial self properties
this.name = name;
this.title = title || name;
this._name = name;
this.title = title || String(name);
this.setter = setter;
this.extraProps = {
...rest,
@ -189,10 +210,6 @@ export class SettingField implements SettingTarget {
this.isOne = parent.isOne;
this.isNone = parent.isNone;
this.designer = parent.designer!;
this.path = parent.path.slice();
if (this.type === 'field') {
this.path.push(this.name);
}
// initial items
if (this.type === 'group' && items) {
@ -219,30 +236,41 @@ export class SettingField implements SettingTarget {
this._items = [];
}
createField(config: FieldConfig): SettingField {
return new SettingField(this, config);
}
get items() {
return this._items;
}
// ====== 当前属性读写 =====
// Todo cache!!
/**
*
* 0 /
* 1
* 2
*/
get isSameValue(): boolean {
get valueState(): number {
if (this.type !== 'field') {
return false;
return 0;
}
const propName = this.path.join('.');
const first = this.nodes[0].getProp(propName)!;
let l = this.nodes.length;
let state = 2;
while (l-- > 1) {
const next = this.nodes[l].getProp(propName, false);
if (!first.isEqual(next)) {
return false;
const s = first.compare(next);
if (s > 1) {
return 0;
}
if (s === 1) {
state = 1;
}
}
return true;
return state;
}
/**
@ -252,6 +280,11 @@ export class SettingField implements SettingTarget {
if (this.type !== 'field') {
return null;
}
// todo: use getValue
const { getValue } = this.extraProps;
if (getValue) {
return getValue(this, this.editor);
}
return this.parent.getPropValue(this.name);
}
@ -262,13 +295,37 @@ export class SettingField implements SettingTarget {
if (this.type !== 'field') {
return;
}
// todo: use onChange
this.parent.setPropValue(this.name, val);
}
setKey(key: string | number) {
if (this.type !== 'field') {
return;
}
const propName = this.path.join('.');
let l = this.nodes.length;
while (l-- > 1) {
this.nodes[l].getProp(propName, true)!.key = key;
}
this._name = key;
}
remove() {
if (this.type !== 'field') {
return;
}
const propName = this.path.join('.');
let l = this.nodes.length;
while (l-- > 1) {
this.nodes[l].getProp(propName)?.remove()
}
}
/**
*
*/
setPropValue(propName: string, value: any) {
setPropValue(propName: string | number, value: any) {
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
this.parent.setPropValue(path, value);
}
@ -276,19 +333,11 @@ export class SettingField implements SettingTarget {
/**
*
*/
getPropValue(propName: string): any {
getPropValue(propName: string | number): any {
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
return this.parent.getPropValue(path);
}
// 添加
// addItem(config: FieldConfig): SettingField {}
// 删除
// deleteItem() {}
// 移动
// insertItem(item: SettingField, index?: number) {}
// remove() {}
purge() {
this.disposeItems();
}
@ -298,6 +347,7 @@ export function isSettingField(obj: any): obj is SettingField {
return obj && obj.isSettingField;
}
export class SettingsMain implements SettingTarget {
private emitter = new EventEmitter();
@ -396,18 +446,7 @@ export class SettingsMain implements SettingTarget {
*
*/
getPropValue(propName: string): any {
if (!this.isSame) {
return null;
}
const first = this.nodes[0].getProp(propName)!;
let l = this.nodes.length;
while (l-- > 1) {
const next = this.nodes[l].getProp(propName, false);
if (!first.isEqual(next)) {
return null;
}
}
return first.value;
return this.nodes[0].getProp(propName, false)?.value;
}
/**

View File

@ -79,11 +79,13 @@ class SettingFieldView extends Component<{ field: SettingField }> {
...(typeof setterProps === 'function' ? setterProps(field, editor) : setterProps),
};
if (field.type === 'field') {
state.value = field.getValue();
if (defaultValue != null && !('defaultValue' in state.setterProps)) {
state.setterProps.defaultValue = defaultValue;
}
if (!field.isSameValue) {
if (field.valueState > 0) {
state.value = field.getValue();
} else {
state.value = null;
state.setterProps.multiValue = true;
if (!('placeholder' in props)) {
state.setterProps.placeholder = '多种值';
@ -118,18 +120,20 @@ class SettingFieldView extends Component<{ field: SettingField }> {
}
render() {
const { field } = this.props;
const { visible, value, setterProps } = this.state;
if (!visible) {
return null;
}
const { field } = this.props;
const { title, extraProps } = field;
// todo: error handling
return (
<Field title={field.title}>
<Field title={extraProps.forceInline ? null : title}>
{createSetterContent(this.setterType, {
...setterProps,
forceInline: extraProps.forceInline,
key: field.id,
// === injection
prop: field,
@ -204,7 +208,7 @@ class SettingGroupView extends Component<{ field: SettingField }> {
}
}
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index: number) {
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index?: number) {
if (isSettingField(item)) {
if (item.isGroup) {
return <SettingGroupView field={item} key={item.id} />;
@ -216,7 +220,11 @@ export function createSettingFieldView(item: SettingField | CustomView, field: S
}
}
export default class SettingsTab extends Component<{ target: SettingTarget }> {
export function showPopup() {
}
export default class SettingsPane extends Component<{ target: SettingTarget }> {
state: { items: Array<SettingField | CustomView> } = {
items: [],
};
@ -254,7 +262,7 @@ export default class SettingsTab extends Component<{ target: SettingTarget }> {
const { items } = this.state;
const { target } = this.props;
return (
<div className="lc-settings-singlepane">
<div className="lc-settings-pane">
{items.map((item, index) => createSettingFieldView(item, target, index))}
</div>
);

View File

@ -31,7 +31,7 @@
--color-block-background-deep-dark: #BAC3CC;
}
.lc-settings-pane {
.lc-settings-main {
position: relative;
.lc-settings-notice {
@ -77,7 +77,7 @@
overflow-y: auto;
}
.lc-settings-singlepane {
.lc-settings-pane {
padding-bottom: 50px;
}