diff --git a/packages/demo/src/app.less b/packages/demo/src/app.less index 92a92ed97..83864c08f 100644 --- a/packages/demo/src/app.less +++ b/packages/demo/src/app.less @@ -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); } diff --git a/packages/designer/src/designer/component-type.ts b/packages/designer/src/designer/component-type.ts index 71519f2ce..3b05345b8 100644 --- a/packages/designer/src/designer/component-type.ts +++ b/packages/designer/src/designer/component-type.ts @@ -153,7 +153,15 @@ export class ComponentType { }, { name: 'name', title: '名称', - setter: 'StringSetter' + setter: { + componentName: 'ArraySetter', + props: { + itemConfig: { + setter: 'StringSetter', + defaultValue: '' + } + } + } }, { name: 'size', title: '大小', diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index a18e555c8..43e0e0c1e 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -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); } /** diff --git a/packages/designer/src/designer/document/node/props/prop-stash.ts b/packages/designer/src/designer/document/node/props/prop-stash.ts index 4c233717f..2618cd804 100644 --- a/packages/designer/src/designer/document/node/props/prop-stash.ts +++ b/packages/designer/src/designer/document/node/props/prop-stash.ts @@ -5,7 +5,7 @@ import Props from './props'; export type PendingItem = Prop[]; export default class PropStash implements IPropParent { @obx.val private space: Set = new Set(); - @computed private get maps(): Map { + @computed private get maps(): Map { 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); diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 7b8599b31..6ed5da245 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -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 | null = null; + @obx.val private _maps: Map | 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 | null { - if (!this.items || this.items.length < 1) { + @computed private get maps(): Map | 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); +} diff --git a/packages/designer/src/designer/document/node/props/props.ts b/packages/designer/src/designer/document/node/props/props.ts index 0a0196f56..8581ac515 100644 --- a/packages/designer/src/designer/document/node/props/props.ts +++ b/packages/designer/src/designer/document/node/props/props.ts @@ -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; } diff --git a/packages/plugin-settings-pane/src/builtin-setters/array-setter/index.tsx b/packages/plugin-settings-pane/src/builtin-setters/array-setter/index.tsx new file mode 100644 index 000000000..c62a22326 --- /dev/null +++ b/packages/plugin-settings-pane/src/builtin-setters/array-setter/index.tsx @@ -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; + 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(); + 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) { + 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 ( +
+
+ +
+ {this.props.multiValue && 当前选择多个节点,且值不一致} + {items.length > 0 ? ( +
+ + {items.map((field, index) => ( + + ))} + +
+ ) : ( +
+ 列表为空 + +
+ )} +
+ ); + } +} + +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 ( +
(this.shell = ref)}> +
+ +
+
{createSettingFieldView(field, field.parent)}
+
+
+ +
+
+
+ ); + } +} + +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 ; + } else if (mode === 'table') { + return ; + } else { + return ; + } + } +} diff --git a/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.less b/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.less new file mode 100644 index 000000000..12e9512cd --- /dev/null +++ b/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.less @@ -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; + } +} diff --git a/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.tsx b/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.tsx new file mode 100644 index 000000000..3329470f1 --- /dev/null +++ b/packages/plugin-settings-pane/src/builtin-setters/array-setter/sortable.tsx @@ -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) => void; + dragImageSourceHandler?: (elem: Element) => Element; + children: ReactElement[]; +}> { + private shell?: HTMLDivElement | null; + private items?: Array; + 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 = []; + const cards = Children.map(children, child => { + const id = child.key!; + items.push(id); + return ( +
+ {child} +
+ ); + }); + this.items = items; + + return ( +
{ + this.shell = ref; + }} + > + {cards} +
+ ); + } +} + +export default Sortable; diff --git a/packages/plugin-settings-pane/src/builtin-setters/array-setter/style.less b/packages/plugin-settings-pane/src/builtin-setters/array-setter/style.less new file mode 100644 index 000000000..5b6973874 --- /dev/null +++ b/packages/plugin-settings-pane/src/builtin-setters/array-setter/style.less @@ -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; + } + } + } +} diff --git a/packages/plugin-settings-pane/src/builtin-setters/events-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/events-setter.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugin-settings-pane/src/builtin-setters/list-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/list-setter.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugin-settings-pane/src/builtin-setters/object-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/object-setter.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx new file mode 100644 index 000000000..d1fbb7d8a --- /dev/null +++ b/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx @@ -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<{}> { + +} diff --git a/packages/plugin-settings-pane/src/builtin-setters/styles-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/styles-setter.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugin-settings-pane/src/field/index.less b/packages/plugin-settings-pane/src/field/index.less index 376bff387..4a662705d 100644 --- a/packages/plugin-settings-pane/src/field/index.less +++ b/packages/plugin-settings-pane/src/field/index.less @@ -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 { diff --git a/packages/plugin-settings-pane/src/field/index.tsx b/packages/plugin-settings-pane/src/field/index.tsx index fe54df2ae..a1e6e4bad 100644 --- a/packages/plugin-settings-pane/src/field/index.tsx +++ b/packages/plugin-settings-pane/src/field/index.tsx @@ -7,7 +7,7 @@ import './index.less'; export interface FieldProps { className?: string; // span - title?: TitleContent; + title?: TitleContent | null; } export class Field extends Component { diff --git a/packages/plugin-settings-pane/src/index.tsx b/packages/plugin-settings-pane/src/index.tsx index 06f8d5e86..1d63c2e37 100644 --- a/packages/plugin-settings-pane/src/index.tsx +++ b/packages/plugin-settings-pane/src/index.tsx @@ -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 ( -
+

请在左侧画布选中节点

@@ -76,7 +77,7 @@ export default class SettingsPane extends Component { if (!this.main.isSame) { // todo: future support 获取设置项交集编辑 return ( -
+

请选中同一类型节点编辑

@@ -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 ( -
+
{this.renderBreadcrumb()}
@@ -97,7 +98,7 @@ export default class SettingsPane extends Component { } return ( -
+
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; } /** diff --git a/packages/plugin-settings-pane/src/settings-tab.tsx b/packages/plugin-settings-pane/src/settings-pane.tsx similarity index 93% rename from packages/plugin-settings-pane/src/settings-tab.tsx rename to packages/plugin-settings-pane/src/settings-pane.tsx index 0bad74192..75555c5a0 100644 --- a/packages/plugin-settings-pane/src/settings-tab.tsx +++ b/packages/plugin-settings-pane/src/settings-pane.tsx @@ -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 ( - + {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 ; @@ -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 } = { items: [], }; @@ -254,7 +262,7 @@ export default class SettingsTab extends Component<{ target: SettingTarget }> { const { items } = this.state; const { target } = this.props; return ( -
+
{items.map((item, index) => createSettingFieldView(item, target, index))}
); diff --git a/packages/plugin-settings-pane/src/style.less b/packages/plugin-settings-pane/src/style.less index d5bc918cf..69f479316 100644 --- a/packages/plugin-settings-pane/src/style.less +++ b/packages/plugin-settings-pane/src/style.less @@ -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; }