diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index 59a7ef04f..e8c87c19b 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -218,18 +218,27 @@ export class SimulatorHost implements ISimulator { // 事件路由 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 { } } - 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); }); diff --git a/packages/designer/src/designer/component-type.ts b/packages/designer/src/designer/component-type.ts index d45ccb482..c3a8bf77d 100644 --- a/packages/designer/src/designer/component-type.ts +++ b/packages/designer/src/designer/component-type.ts @@ -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: '年龄', diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 3cd55d304..9b91be6fa 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -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); } diff --git a/packages/designer/src/designer/helper/offset-observer.ts b/packages/designer/src/designer/helper/offset-observer.ts index 3863b20e3..22b47b24a 100644 --- a/packages/designer/src/designer/helper/offset-observer.ts +++ b/packages/designer/src/designer/helper/offset-observer.ts @@ -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; } diff --git a/packages/plugin-settings/README.md b/packages/plugin-settings/README.md index 287c7fa70..9b40bf02b 100644 --- a/packages/plugin-settings/README.md +++ b/packages/plugin-settings/README.md @@ -1 +1,15 @@ 属性面板 + +其中 field 可独立出去一个包 + +提供: + 1. 快捷设置面板服务 + 2. 对应节点的设置面板服务 + 3. 右侧设置面板 + 4. 提供 setters 服务,setters 注册、获取机制 + +依赖: + +tip 处理 +field +popup diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx b/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx index 7ca7cfb1a..f68e3dd58 100644 --- a/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx +++ b/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx @@ -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 { 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 { this.scrollToLast = false; const lastIndex = items.length - 1; + const content = + items.length > 0 ? ( +
+ + {items.map((field, index) => ( + + ))} + +
+ ) : this.props.multiValue ? ( + 当前选择了多个节点,且值不一致,修改会覆盖所有值 + ) : ( + 当前项目为空 + ); + return (
-
+ {/*
-
- {this.props.multiValue && 当前选择多个节点,且值不一致} - {items.length > 0 ? ( -
- - {items.map((field, index) => ( - - ))} - -
- ) : ( -
- 列表为空 - -
- )} +
*/} + {content} +
); } @@ -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 ; - } else if (mode === 'table') { - return ; + const 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} />; } diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/style.less b/packages/plugin-settings/src/builtin-setters/array-setter/style.less index 5b6973874..86cf52f0e 100644 --- a/packages/plugin-settings/src/builtin-setters/array-setter/style.less +++ b/packages/plugin-settings/src/builtin-setters/array-setter/style.less @@ -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; diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx b/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx new file mode 100644 index 000000000..7e7c05e61 --- /dev/null +++ b/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx @@ -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'; + } +} diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/object-setter.tsx b/packages/plugin-settings/src/builtin-setters/object-setter/object-setter.tsx deleted file mode 100644 index 39ca6b11e..000000000 --- a/packages/plugin-settings/src/builtin-setters/object-setter/object-setter.tsx +++ /dev/null @@ -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<{}> { - -} diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/style.less b/packages/plugin-settings/src/builtin-setters/object-setter/style.less new file mode 100644 index 000000000..6d6064df1 --- /dev/null +++ b/packages/plugin-settings/src/builtin-setters/object-setter/style.less @@ -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; + } + } +} diff --git a/packages/plugin-settings/src/field/index.less b/packages/plugin-settings/src/field/index.less index 4a662705d..b6235aa0e 100644 --- a/packages/plugin-settings/src/field/index.less +++ b/packages/plugin-settings/src/field/index.less @@ -37,6 +37,7 @@ } > .lc-field-body { flex: 1; + min-width: 0; display: flex; align-items: center; } diff --git a/packages/plugin-settings/src/index.tsx b/packages/plugin-settings/src/index.tsx index 2c5b5a7b7..1508f6cb6 100644 --- a/packages/plugin-settings/src/index.tsx +++ b/packages/plugin-settings/src/index.tsx @@ -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 }; diff --git a/packages/plugin-settings/src/popup/index.tsx b/packages/plugin-settings/src/popup/index.tsx new file mode 100644 index 000000000..198f135bf --- /dev/null +++ b/packages/plugin-settings/src/popup/index.tsx @@ -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> + ); + } +} diff --git a/packages/plugin-settings/src/popup/style.less b/packages/plugin-settings/src/popup/style.less new file mode 100644 index 000000000..b115fd371 --- /dev/null +++ b/packages/plugin-settings/src/popup/style.less @@ -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; + } +} diff --git a/packages/plugin-settings/src/settings-pane.tsx b/packages/plugin-settings/src/settings-pane.tsx index 095593ccf..8d01b8c14 100644 --- a/packages/plugin-settings/src/settings-pane.tsx +++ b/packages/plugin-settings/src/settings-pane.tsx @@ -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> ); }