diff --git a/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx b/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx index d2931c661..a61d2fc74 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx +++ b/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx @@ -85,7 +85,7 @@ function processDetail({ target, detail, document }: Location): InsertionData { if (!instances) { return {}; } - const edge = sim.computeComponentInstanceRect(instances[0]); + const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rectSelector); return edge ? { edge, insertType: 'cover', coverRect: edge } : {}; } } diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx index 5a6b2860c..412490df4 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx +++ b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx @@ -88,7 +88,7 @@ export class OutlineHovering extends Component { scale={this.scale} scrollX={this.scrollX} scrollY={this.scrollY} - rect={host.computeComponentInstanceRect(instances[0])} + rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rectSelector)} /> ); } @@ -101,7 +101,7 @@ export class OutlineHovering extends Component { scale={this.scale} scrollX={this.scrollX} scrollY={this.scrollY} - rect={host.computeComponentInstanceRect(inst)} + rect={host.computeComponentInstanceRect(inst, current.componentMeta.rectSelector)} /> ))} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx b/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx index 8b148cbfa..4b7cf86fb 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx +++ b/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx @@ -1,4 +1,13 @@ -import { Component, Fragment } from 'react'; +import { + Component, + Fragment, + ReactNodeArray, + isValidElement, + cloneElement, + createElement, + ReactNode, + ComponentType, +} from 'react'; import classNames from 'classnames'; import { observer } from '@recore/obx-react'; import { SimulatorContext } from '../context'; @@ -6,6 +15,8 @@ import { SimulatorHost } from '../host'; import { computed } from '@recore/obx'; import OffsetObserver from '../../../../designer/helper/offset-observer'; import Node from '../../../../designer/document/node/node'; +import { isContentObject, ContentObject } from '../../../../designer/component-meta'; +import { createIcon, EmbedTip, isReactComponent } from '../../../../../../globals'; @observer export class OutlineSelectingInstance extends Component<{ @@ -38,12 +49,89 @@ export class OutlineSelectingInstance extends Component<{ return (
- {observed.nodeInstance.node.title} + {!dragging && }
); } } +@observer +class Toolbar extends Component<{ observed: OffsetObserver }> { + shouldComponentUpdate() { + return false; + } + render() { + const { observed } = this.props; + const { height, width } = observed.viewport; + const BAR_HEIGHT = 20; + const MARGIN = 1; + const BORDER = 2; + const SPACE_HEIGHT = BAR_HEIGHT + MARGIN + BORDER; + let style: any; + if (observed.top > SPACE_HEIGHT) { + style = { + right: Math.max(-BORDER, observed.right - width - BORDER), + top: -SPACE_HEIGHT, + height: BAR_HEIGHT, + }; + } else if (observed.bottom + SPACE_HEIGHT < height) { + style = { + bottom: -SPACE_HEIGHT, + height: BAR_HEIGHT, + right: Math.max(-BORDER, observed.right - width - BORDER), + }; + } else { + style = { + height: BAR_HEIGHT, + top: Math.max(MARGIN, MARGIN - observed.top), + right: Math.max(MARGIN, MARGIN + observed.right - width), + }; + } + const { node } = observed; + const actions: ReactNodeArray = []; + node.componentMeta.availableActions.forEach(action => { + const { important, condition, content, name } = action; + if (node.isSlotRoot && (name === 'copy' || name === 'remove')) { + // FIXME: need this? + return; + } + if (important && (typeof condition === 'function' ? condition(node) : condition !== false)) { + actions.push(createAction(content, name, node)); + } + }); + return ( +
+ {actions} +
+ ); + } +} + +function createAction(content: ReactNode | ComponentType | ContentObject, key: string, node: Node) { + if (isValidElement(content)) { + return cloneElement(content, { key, node }); + } + if (isReactComponent(content)) { + return createElement(content, { key, node }); + } + if (isContentObject(content)) { + const { action, description, icon } = content; + return ( +
{ + action && action(node); + }} + > + {icon && createIcon(icon)} + {description} +
+ ); + } + return null; +} + @observer export class OutlineSelectingForNode extends Component<{ node: Node }> { static contextType = SimulatorContext; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outlines.less b/packages/designer/src/builtins/simulator/host/auxilary/outlines.less index 6a1ba2111..19f4333ec 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outlines.less +++ b/packages/designer/src/builtins/simulator/host/auxilary/outlines.less @@ -18,6 +18,32 @@ transform: translateY(-100%); font-weight: lighter; } + & > &-actions { + position: absolute; + display: flex; + flex-direction: row-reverse; + align-items: stretch; + justify-content: flex-end; + pointer-events: all; + } + + &-action { + box-sizing: border-box; + cursor: pointer; + height: 20px; + width: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--color-brand, #006cff); + opacity: 1; + max-height: 100%; + overflow: hidden; + color: white; + &:hover { + background: var(--color-brand-light, #006cff); + } + } &&-hovering { z-index: 1; diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index 9fab87841..2b81ede54 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -33,6 +33,7 @@ import { ComponentMetadata } from '../../../designer/component-meta'; import { ReactInstance } from 'react'; import { isRootNode } from '../../../designer/document/node/root-node'; import { parseProps } from '../utils/parse-props'; +import { isElement } from '../../../utils/is-element'; export interface LibraryItem { package: string; @@ -450,13 +451,13 @@ export class SimulatorHost implements ISimulator { if (!instances) { return null; } - return this.computeComponentInstanceRect(instances[0]); + return this.computeComponentInstanceRect(instances[0], node.componentMeta.rectSelector); } /** * @see ISimulator */ - computeComponentInstanceRect(instance: ReactInstance): Rect | null { + computeComponentInstanceRect(instance: ReactInstance, selector?: string): Rect | null { const renderer = this.renderer!; const elements = renderer.findDOMNodes(instance); if (!elements) { @@ -466,20 +467,27 @@ export class SimulatorHost implements ISimulator { let rects: DOMRect[] | undefined; let last: { x: number; y: number; r: number; b: number } | undefined; let computed = false; - const elems = elements.slice(); - const commonParent: Element | null = null; + const elems = selector + ? elements + .map(elem => { + if (isElement(elem)) { + // TODO: if has selector use exact match + if (elem.matches(selector)) { + return elem; + } + + return elem.querySelector(selector); + } + return null; + }) + .filter(Boolean) + : elements.slice(); while (true) { if (!rects || rects.length < 1) { const elem = elems.pop(); if (!elem) { break; } - /* - if (!commonParent) { - commonParent = elem.parentElement; - } else if (elem.parentElement !== commonParent) { - continue; - }*/ rects = renderer.getClientRects(elem); } const rect = rects.pop(); @@ -716,7 +724,10 @@ export class SimulatorHost implements ISimulator { const targetInstance = e.targetInstance as ReactInstance; const parentInstance = this.getClosestNodeInstance(targetInstance, target.id); - const edge = this.computeComponentInstanceRect(parentInstance?.instance as any); + const edge = this.computeComponentInstanceRect( + parentInstance?.instance as any, + parentInstance?.node?.componentMeta.rectSelector, + ); if (!edge) { return null; @@ -755,7 +766,7 @@ export class SimulatorHost implements ISimulator { ? instances.find(inst => this.getClosestNodeInstance(inst, target.id)?.instance === targetInstance) : instances[0] : null; - const rect = inst ? this.computeComponentInstanceRect(inst) : null; + const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null; if (!rect) { continue; diff --git a/packages/designer/src/builtins/simulator/renderer/builtin-components.ts b/packages/designer/src/builtins/simulator/renderer/builtin-components.ts new file mode 100644 index 000000000..bf09bf75c --- /dev/null +++ b/packages/designer/src/builtins/simulator/renderer/builtin-components.ts @@ -0,0 +1,412 @@ +import { ReactElement, createElement, ReactType } from 'react'; +import classNames from 'classnames'; + +const supportedEvents = [ + // MouseEvents + { + name: 'onClick', + description: '点击时', + }, + { + name: 'onDoubleClick', + description: '双击时', + }, + { + name: 'onMouseDown', + description: '鼠标按下', + }, + { + name: 'onMouseEnter', + description: '鼠标进入', + }, + { + name: 'onMouseMove', + description: '鼠标移动', + }, + { + name: 'onMouseOut', + description: '鼠标移出', + }, + { + name: 'onMouseOver', + description: '鼠标悬停', + }, + { + name: 'onMouseUp', + description: '鼠标松开', + }, + // Focus Events + { + name: 'onFocus', + description: '获得焦点', + snippet: '', + }, + { + name: 'onBlur', + description: '失去焦点', + snippet: '', + }, + // Form Events + { + name: 'onChange', + description: '值改变时', + snippet: '', + }, + { + name: 'onSelect', + description: '选择', + }, + { + name: 'onInput', + description: '输入', + snippet: '', + }, + { + name: 'onReset', + description: '重置', + snippet: '', + }, + { + name: 'onSubmit', + description: '提交', + snippet: '', + }, + // Clipboard Events + { + name: 'onCopy', + description: '复制', + snippet: '', + }, + { + name: 'onCut', + description: '剪切', + snippet: '', + }, + { + name: 'onPaste', + description: '粘贴', + snippet: '', + }, + + // Keyboard Events + { + name: 'onKeyDown', + description: '键盘按下', + snippet: '', + }, + { + name: 'onKeyPress', + description: '键盘按下并释放', + snippet: '', + }, + { + name: 'onKeyUp', + description: '键盘松开', + snippet: '', + }, + // Touch Events + { + name: 'onTouchCancel', + description: '触摸退出', + snippet: '', + }, + { + name: 'onTouchEnd', + description: '触摸结束', + snippet: '', + }, + { + name: 'onTouchMove', + description: '触摸移动', + snippet: '', + }, + { + name: 'onTouchStart', + description: '触摸开始', + snippet: '', + }, + // UI Events + { + name: 'onScroll', + description: '滚动', + snippet: '', + }, + { + name: 'onLoad', + description: '加载完毕', + snippet: '', + }, + { + name: 'onWheel', + description: '滚轮事件', + snippet: '', + }, + // Animation Events + { + name: 'onAnimationStart', + description: '动画开始', + }, + { + name: 'onAnimationEnd', + description: '动画结束', + }, +]; + +const builtinComponents = new Map ReactElement>(); +function getBlockElement(tag: string): (props: any) => ReactElement { + if (builtinComponents.has(tag)) { + return builtinComponents.get(tag)!; + } + const mock = ({ className, children, ...rest }: any = {}) => { + const props = { + ...rest, + className: classNames('lc-block-container', className), + }; + return createElement(tag, props, children); + }; + + mock.metadata = { + componentName: tag, + // selfControlled: true, + configure: { + props: [], + events: { + supportedEvents, + }, + styles: { + supportClassName: true, + supportInlineStyle: true, + }, + component: { + ...metasMap[tag], + }, + }, + }; + + builtinComponents.set(tag, mock); + return mock; +} + +const HTMLBlock = [ + 'div', + 'p', + 'article', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'aside', + 'blockquote', + 'footer', + 'form', + 'header', + 'table', + 'tbody', + 'section', + 'ul', + 'li', +]; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const HTMLInlineBlock = ['a', 'b', 'span', 'em']; +export function getIntrinsicMock(tag: string): ReactType { + if (HTMLBlock.indexOf(tag) > -1) { + return getBlockElement(tag); + } + + return tag as any; +} + +const metasMap: any = { + div: { + isContainer: true, + nesting: { + ancestorBlacklist: 'p', + }, + }, + ul: { + isContainer: true, + nesting: { + childWhitelist: 'li', + }, + }, + p: { + isContainer: true, + nesting: { + ancestorBlacklist: 'button,p', + }, + }, + li: { + isContainer: true, + nesting: { + parentWhitelist: 'ui,ol', + }, + }, + span: { + isContainer: true, + selfControlled: true, + }, + a: { + isContainer: true, + nesting: { + ancestorBlacklist: 'a', + }, + }, + b: { + isContainer: true, + }, + strong: { + isContainer: true, + }, + em: { + isContainer: true, + }, + i: { + isContainer: true, + }, + form: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'form,button', + }, + }, + table: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + caption: { + isContainer: true, + selfControlled: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + select: { + isContainer: true, + selfControlled: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + button: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + input: { + isContainer: false, + nestingRule: { + ancestorBlacklist: 'button,h1,h2,h3,h4,h5,h6', + }, + }, + textarea: { + isContainer: false, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + image: { + isContainer: false, + }, + canvas: { + isContainer: false, + }, + br: { + isContainer: false, + }, + h1: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + h2: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + h3: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + h4: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + h5: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + h6: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + article: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + aside: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + footer: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + header: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + blockquote: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + address: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + section: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'p,h1,h2,h3,h4,h5,h6,button', + }, + }, + summary: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, + nav: { + isContainer: true, + nestingRule: { + ancestorBlacklist: 'button', + }, + }, +}; diff --git a/packages/designer/src/builtins/simulator/renderer/renderer.ts b/packages/designer/src/builtins/simulator/renderer/renderer.ts index 40cb6647d..26c246399 100644 --- a/packages/designer/src/builtins/simulator/renderer/renderer.ts +++ b/packages/designer/src/builtins/simulator/renderer/renderer.ts @@ -85,7 +85,7 @@ export class SimulatorRenderer { } @computed get designMode(): any { - return 'border'; + return 'preview'; } @obx.ref private _componentsMap = {}; @computed get componentsMap(): any { diff --git a/packages/designer/src/builtins/simulator/utils/intrinsic-mocks.ts b/packages/designer/src/builtins/simulator/utils/intrinsic-mocks.ts deleted file mode 100644 index a269fde2c..000000000 --- a/packages/designer/src/builtins/simulator/utils/intrinsic-mocks.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { ReactElement, createElement, ReactType } from 'react'; -import classNames from 'classnames'; - -const mocksCache = new Map ReactElement>(); -// endpoint element: input,select,video,audio,canvas,textarea -// -function getBlockElement(tag: string): (props: any) => ReactElement { - if (mocksCache.has(tag)) { - return mocksCache.get(tag)!; - } - const mock = ({ className, children, ...rest }: any = {}) => { - const props = { - ...rest, - className: classNames('my-intrinsic-container', className), - }; - return createElement(tag, props, children); - }; - - mock.prototypeConfig = { - uri: `@html:${tag}`, - selfControlled: true, - ...(prototypeMap as any)[tag], - }; - - mocksCache.set(tag, mock); - return mock; -} - -const HTMLBlock = [ - 'div', - 'p', - 'article', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'aside', - 'blockquote', - 'footer', - 'form', - 'header', - 'table', - 'tbody', - 'section', - 'ul', - 'li', - 'span', -]; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const HTMLInlineBlock = ['a', 'b', 'span', 'em']; -export function getIntrinsicMock(tag: string): ReactType { - if (HTMLBlock.indexOf(tag) > -1) { - return getBlockElement(tag); - } - - return tag as any; -} - -const prototypeMap = { - div: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: 'p', - }, - }, - ul: { - isContainer: true, - selfControlled: true, - nesting: { - childWhitelist: 'li', - }, - }, - p: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: 'button,p', - }, - }, - li: { - isContainer: true, - selfControlled: true, - nesting: { - parentWhitelist: 'ui,ol', - }, - }, - span: { - isContainer: true, - selfControlled: true, - }, - a: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!a', - }, - }, - b: { - isContainer: true, - selfControlled: true, - }, - strong: { - isContainer: true, - selfControlled: true, - }, - em: { - isContainer: true, - selfControlled: true, - }, - i: { - isContainer: true, - selfControlled: true, - }, - form: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!form,!button', - }, - }, - table: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - caption: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - select: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - button: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - input: { - isContainer: false, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button,!h1,!h2,!h3,!h4,!h5', - }, - }, - textarea: { - isContainer: false, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - image: { - isContainer: false, - selfControlled: true, - }, - canvas: { - isContainer: false, - selfControlled: true, - }, - br: { - isContainer: false, - selfControlled: true, - }, - h1: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - h2: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - h3: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - h4: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - h5: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - h6: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - article: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - aside: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - footer: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - header: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - blockquote: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - address: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - section: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button', - }, - }, - summary: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, - nav: { - isContainer: true, - selfControlled: true, - nesting: { - ancestorBlacklist: '!button', - }, - }, -}; diff --git a/packages/designer/src/designer/component-meta.ts b/packages/designer/src/designer/component-meta.ts index 71562673e..0ec9329f5 100644 --- a/packages/designer/src/designer/component-meta.ts +++ b/packages/designer/src/designer/component-meta.ts @@ -1,7 +1,11 @@ -import { ReactNode } from 'react'; +import { ReactNode, ReactElement, ComponentType, createElement } from 'react'; import Node, { NodeParent } from './document/node/node'; import { NodeData, NodeSchema } from './schema'; import { PropConfig } from './prop-config'; +import Designer from './designer'; +import { Remove, Clone } from '../../../globals'; +import { computed } from '@recore/obx'; +import { intl } from '../locale'; export interface NestingRule { childWhitelist?: string[]; @@ -17,9 +21,39 @@ export interface Configure { isModal?: boolean; descriptor?: string; nestingRule?: NestingRule; + rectSelector?: string; + // copy,move,delete + disableBehaviors?: string[]; + actions?: ComponentAction[]; }; } +export interface ContentObject { + // 图标 + icon?: string | ComponentType | ReactElement; + // 描述 + description?: string; + // 执行动作 + action?: (node: Node) => void; +} + +export interface ComponentAction { + // behaviorName + name: string; + // 菜单名称 + content: string | ReactNode | ContentObject; + // 子集 + items?: ComponentAction[]; + // 不显示 + condition?: boolean | ((node: Node) => boolean); + // 显示在工具条上 + important?: boolean; +} + +export function isContentObject(obj: any): obj is ContentObject { + return obj && typeof obj === 'object'; +} + export interface ComponentMetadata { componentName: string; /** @@ -52,7 +86,7 @@ export interface ComponentMetadata { } interface TransformedComponentMetadata extends ComponentMetadata { - configure?: Configure & { + configure: Configure & { combined?: any[]; }; } @@ -102,9 +136,12 @@ function npmToURI(npm: { return uri; } -export type MetadataTransducer = (prev: ComponentMetadata) => TransformedComponentMetadata; +export type MetadataTransducer = (prev: TransformedComponentMetadata) => TransformedComponentMetadata; const metadataTransducers: MetadataTransducer[] = []; +// propsParser +// + export function registerMetadataTransducer(transducer: MetadataTransducer) { metadataTransducers.push(transducer); } @@ -128,8 +165,12 @@ export class ComponentMeta { return this._isModal!; } private _descriptor?: string; - get descriptor(): string { - return this._descriptor!; + get descriptor(): string | undefined { + return this._descriptor; + } + private _rectSelector?: string; + get rectSelector(): string | undefined { + return this._rectSelector; } private _acceptable?: boolean; get acceptable(): boolean { @@ -152,7 +193,7 @@ export class ComponentMeta { return this._metadata.icon; } - constructor(private _metadata: ComponentMetadata) { + constructor(readonly designer: Designer, private _metadata: ComponentMetadata) { this.parseMetadata(_metadata); } @@ -173,6 +214,7 @@ export class ComponentMeta { this._isContainer = component.isContainer ? true : false; this._isModal = component.isModal ? true : false; this._descriptor = component.descriptor; + this._rectSelector = component.rectSelector; if (component.nestingRule) { const { parentWhitelist, childWhitelist } = component.nestingRule; this.parentWhitelist = ensureAList(parentWhitelist); @@ -187,7 +229,7 @@ export class ComponentMeta { private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata { const result = metadataTransducers.reduce((prevMetadata, current) => { return current(prevMetadata); - }, metadta); + }, preprocessMetadata(metadta)); if (!result.configure) { result.configure = {}; @@ -199,6 +241,18 @@ export class ComponentMeta { return this.componentName === 'Page' || this.componentName === 'Block' || this.componentName === 'Component'; } + @computed get availableActions() { + let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {}; + actions = builtinComponentActions.concat(this.designer.getGlobalComponentActions() || [], actions || []); + if (!disableBehaviors && this.isRootComponent()) { + disableBehaviors = ['copy', 'remove']; + } + if (disableBehaviors) { + return actions.filter(action => disableBehaviors!.indexOf(action.name) < 0); + } + return actions; + } + set metadata(metadata: ComponentMetadata) { this._metadata = metadata; this.parseMetadata(metadata); @@ -222,3 +276,87 @@ export class ComponentMeta { return true; } } + +function preprocessMetadata(metadata: ComponentMetadata): TransformedComponentMetadata { + if (metadata.configure) { + if (Array.isArray(metadata.configure)) { + return { + ...metadata, + configure: { + props: metadata.configure, + }, + }; + } + return metadata as any; + } + + return { + ...metadata, + configure: {}, + }; +} + +registerMetadataTransducer(metadata => { + const { configure, componentName } = metadata; + const { component = {} } = configure; + if (!component.nestingRule) { + let m; + // uri match xx.Group set subcontrolling: true, childWhiteList + if ((m = /^(.+)\.Group$/.exec(componentName))) { + // component.subControlling = true; + if (!component.nestingRule) { + component.nestingRule = { + childWhitelist: [`${m[1]}`], + }; + } + } + // uri match xx.Node set selfControlled: false, parentWhiteList + else if ((m = /^(.+)\.Node$/.exec(componentName))) { + // component.selfControlled = false; + component.nestingRule = { + parentWhitelist: [`${m[1]}`, componentName], + }; + } + // uri match .Item .Node .Option set parentWhiteList + else if ((m = /^(.+)\.(Item|Node|Option)$/.exec(componentName))) { + component.nestingRule = { + parentWhitelist: [`${m[1]}`], + }; + } + } + if (component.isModal == null && /Dialog/.test(componentName)) { + component.isModal = true; + } + return { + ...metadata, + configure: { + ...configure, + component, + }, + }; +}); + +const builtinComponentActions: ComponentAction[] = [ + { + name: 'remove', + content: { + icon: Remove, + description: intl('remove'), + action(node: Node) { + node.remove(); + }, + }, + important: true, + }, + { + name: 'copy', + content: { + icon: Clone, + description: intl('copy'), + action(node: Node) { + // node.remove(); + }, + }, + important: true, + }, +]; diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 73c479f26..732b48fe2 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -10,7 +10,7 @@ import Location, { LocationData, isLocationChildrenDetail } from './helper/locat import DocumentModel from './document/document-model'; import Node, { insertChildren } from './document/node/node'; import { isRootNode } from './document/node/root-node'; -import { ComponentMetadata, ComponentMeta } from './component-meta'; +import { ComponentMetadata, ComponentMeta, ComponentAction } from './component-meta'; import Scroller, { IScrollable } from './helper/scroller'; import { INodeSelector } from './simulator'; import OffsetObserver, { createOffsetObserver } from './helper/offset-observer'; @@ -25,8 +25,9 @@ export interface DesignerProps { simulatorComponent?: ReactComponentType; dragGhostComponent?: ReactComponentType; suspensed?: boolean; - componentsDescription?: ComponentMetadata[]; + componentMetadatas?: ComponentMetadata[]; eventPipe?: EventEmitter; + globalComponentActions?: ComponentAction[]; onMount?: (designer: Designer) => void; onDragstart?: (e: LocateEvent) => void; onDrag?: (e: LocateEvent) => void; @@ -229,8 +230,8 @@ export default class Designer { if (props.suspensed !== this.props.suspensed && props.suspensed != null) { this.suspensed = props.suspensed; } - if (props.componentsDescription !== this.props.componentsDescription && props.componentsDescription != null) { - this.buildComponentMetasMap(props.componentsDescription); + if (props.componentMetadatas !== this.props.componentMetadatas && props.componentMetadatas != null) { + this.buildComponentMetasMap(props.componentMetadatas); } } else { // init hotkeys @@ -246,8 +247,8 @@ export default class Designer { if (props.suspensed != null) { this.suspensed = props.suspensed; } - if (props.componentsDescription != null) { - this.buildComponentMetasMap(props.componentsDescription); + if (props.componentMetadatas != null) { + this.buildComponentMetasMap(props.componentMetadatas); } } this.props = props; @@ -307,7 +308,7 @@ export default class Designer { meta.metadata = data; this._lostComponentMetasMap.delete(key); } else { - meta = new ComponentMeta(data); + meta = new ComponentMeta(this, data); } this._componentMetasMap.set(key, meta); @@ -315,6 +316,10 @@ export default class Designer { }); } + getGlobalComponentActions(): ComponentAction[] | null { + return this.props?.globalComponentActions || null; + } + getComponentMeta(componentName: string, generateMetadata?: () => ComponentMetadata | null): ComponentMeta { if (this._componentMetasMap.has(componentName)) { return this._componentMetasMap.get(componentName)!; @@ -324,7 +329,7 @@ export default class Designer { return this._lostComponentMetasMap.get(componentName)!; } - const meta = new ComponentMeta({ + const meta = new ComponentMeta(this, { componentName, ...(generateMetadata ? generateMetadata() : null), }); diff --git a/packages/designer/src/designer/helper/offset-observer.ts b/packages/designer/src/designer/helper/offset-observer.ts index 21a8175b7..798ec43a8 100644 --- a/packages/designer/src/designer/helper/offset-observer.ts +++ b/packages/designer/src/designer/helper/offset-observer.ts @@ -2,6 +2,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'; +import Node from '../document/node/node'; export default class OffsetObserver { readonly id = uniqueId('oobx'); @@ -10,39 +11,65 @@ export default class OffsetObserver { private lastOffsetTop?: number; private lastOffsetHeight?: number; private lastOffsetWidth?: number; - @obx private height = 0; - @obx private width = 0; - @obx private left = 0; - @obx private top = 0; + @obx private _height = 0; + @obx private _width = 0; + @obx private _left = 0; + @obx private _top = 0; + @obx private _right = 0; + @obx private _bottom = 0; + + @computed get height() { + return this.isRoot ? this.viewport.height : this._height * this.scale; + } + + @computed get width() { + return this.isRoot ? this.viewport.width : this._width * this.scale; + } + + @computed get top() { + return this.isRoot ? 0 : this._top * this.scale; + } + + @computed get left() { + return this.isRoot ? 0 : this._left * this.scale; + } + + @computed get bottom() { + return this.isRoot ? this.viewport.height : this._bottom * this.scale; + } + + @computed get right() { + return this.isRoot ? this.viewport.width : this._right * this.scale; + } @obx hasOffset = false; @computed get offsetLeft() { if (this.isRoot) { - return this.viewport.scrollX; + return this.viewport.scrollX * this.scale; } if (!this.viewport.scrolling || this.lastOffsetLeft == null) { - this.lastOffsetLeft = (this.left + this.viewport.scrollX) * this.scale; + this.lastOffsetLeft = this.left + this.viewport.scrollX * this.scale; } return this.lastOffsetLeft; } @computed get offsetTop() { if (this.isRoot) { - return this.viewport.scrollY; + return this.viewport.scrollY * this.scale; } if (!this.viewport.scrolling || this.lastOffsetTop == null) { - this.lastOffsetTop = (this.top + this.viewport.scrollY) * this.scale; + this.lastOffsetTop = this.top + this.viewport.scrollY * this.scale; } return this.lastOffsetTop; } @computed get offsetHeight() { if (!this.viewport.scrolling || this.lastOffsetHeight == null) { - this.lastOffsetHeight = this.isRoot ? this.viewport.height : this.height * this.scale; + this.lastOffsetHeight = this.isRoot ? this.viewport.height : this.height; } return this.lastOffsetHeight; } @computed get offsetWidth() { if (!this.viewport.scrolling || this.lastOffsetWidth == null) { - this.lastOffsetWidth = this.isRoot ? this.viewport.width : this.width * this.scale; + this.lastOffsetWidth = this.isRoot ? this.viewport.width : this.width; } return this.lastOffsetWidth; } @@ -52,11 +79,13 @@ export default class OffsetObserver { } private pid: number | undefined; - private viewport: IViewport; + readonly viewport: IViewport; private isRoot: boolean; + readonly node: Node; constructor(readonly nodeInstance: INodeSelector) { const { node, instance } = nodeInstance; + this.node = node; const doc = node.document; const host = doc.simulator!; this.isRoot = isRootNode(node); @@ -75,16 +104,18 @@ export default class OffsetObserver { return; } - const rect = host.computeComponentInstanceRect(instance!); + const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rectSelector); if (!rect) { this.hasOffset = false; } else { if (!this.viewport.scrolling || !this.hasOffset) { - this.height = rect.height; - this.width = rect.width; - this.left = rect.left; - this.top = rect.top; + this._height = rect.height; + this._width = rect.width; + this._left = rect.left; + this._top = rect.top; + this._right = rect.right; + this._bottom = rect.bottom; this.hasOffset = true; } } diff --git a/packages/designer/src/designer/simulator.ts b/packages/designer/src/designer/simulator.ts index e21b5e8c9..99c73ad47 100644 --- a/packages/designer/src/designer/simulator.ts +++ b/packages/designer/src/designer/simulator.ts @@ -134,7 +134,7 @@ export interface ISimulator

extends ISensor { computeRect(node: Node): DOMRect | null; - computeComponentInstanceRect(instance: ComponentInstance): DOMRect | null; + computeComponentInstanceRect(instance: ComponentInstance, selector?: string): DOMRect | null; findDOMNodes(instance: ComponentInstance): Array | null; diff --git a/packages/designer/src/locale/en-US.json b/packages/designer/src/locale/en-US.json new file mode 100644 index 000000000..722b8a4c6 --- /dev/null +++ b/packages/designer/src/locale/en-US.json @@ -0,0 +1,4 @@ +{ + "copy": "Copy", + "remove": "Remove" +} diff --git a/packages/designer/src/locale/index.ts b/packages/designer/src/locale/index.ts new file mode 100644 index 000000000..32205fb5c --- /dev/null +++ b/packages/designer/src/locale/index.ts @@ -0,0 +1,10 @@ +import { createIntl } from '../../../globals'; +import en_US from './en-US.json'; +import zh_CN from './zh-CN.json'; + +const { intl, getLocale, setLocale } = createIntl({ + 'en-US': en_US, + 'zh-CN': zh_CN, +}); + +export { intl, getLocale, setLocale }; diff --git a/packages/designer/src/locale/zh-CN.json b/packages/designer/src/locale/zh-CN.json new file mode 100644 index 000000000..f90c1c8a8 --- /dev/null +++ b/packages/designer/src/locale/zh-CN.json @@ -0,0 +1,4 @@ +{ + "copy": "复制", + "remove": "删除" +} diff --git a/packages/editor/src/config/assets.js b/packages/editor/src/config/assets.js index ce83e3438..d8506368f 100644 --- a/packages/editor/src/config/assets.js +++ b/packages/editor/src/config/assets.js @@ -1196,7 +1196,8 @@ export default { isContainer: true, nestingRule: { childWhitelist: 'Select.Option' - } + }, + rectSelector: '.next-select', }, props: [ { @@ -1762,6 +1763,26 @@ export default { } } } + }, + Dialog: { + componentName: 'Dialog', + title: '弹窗', + devMode: 'proCode', + npm: { + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Dialog' + }, + props: [{ + name: 'title', + propType: 'string' + }], + configure: { + component: { + rectSelector: '.next-dialog' + } + } } }, componentList: [ @@ -1889,6 +1910,30 @@ export default { } } ] + }, + { + componentName: 'Dialog', + libraryId: 3, + title: '弹窗', + icon: '', + snippets: [ + { + title: '弹窗', + screenshot: '', + schema: { + componentName: 'Dialog', + props: { + title: '这是一个dialog', + visible: true + }, + children: [ + { + compoentName: 'Div' + } + ] + } + } + ] } ] } diff --git a/packages/editor/src/plugins/designer/index.tsx b/packages/editor/src/plugins/designer/index.tsx index 7c559c311..b953f4b6e 100644 --- a/packages/editor/src/plugins/designer/index.tsx +++ b/packages/editor/src/plugins/designer/index.tsx @@ -168,7 +168,7 @@ export default class DesignerPlugin extends PureComponent { className="lowcode-plugin-designer" defaultSchema={SCHEMA as any} eventPipe={editor as any} - componentsDescription={Object.values(assets.components) as any} + componentMetadatas={Object.values(assets.components) as any} simulatorProps={{ library: Object.values(assets.packages), }} diff --git a/packages/globals/.eslintignore b/packages/globals/.eslintignore new file mode 100644 index 000000000..1fb2edf7c --- /dev/null +++ b/packages/globals/.eslintignore @@ -0,0 +1,6 @@ +.idea/ +.vscode/ +build/ +.* +~* +node_modules diff --git a/packages/globals/.eslintrc b/packages/globals/.eslintrc new file mode 100644 index 000000000..db78d35d1 --- /dev/null +++ b/packages/globals/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/@recore/config/.eslintrc" +} diff --git a/packages/globals/.prettierrc b/packages/globals/.prettierrc new file mode 100644 index 000000000..8748c5ed3 --- /dev/null +++ b/packages/globals/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/packages/globals/README.md b/packages/globals/README.md new file mode 100644 index 000000000..070f5ca3b --- /dev/null +++ b/packages/globals/README.md @@ -0,0 +1 @@ +shared globals diff --git a/packages/globals/package.json b/packages/globals/package.json new file mode 100644 index 000000000..26761d9ba --- /dev/null +++ b/packages/globals/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ali/lowcode-globals", + "version": "0.0.0", + "description": "xxx for Ali lowCode engine", + "main": "src/index.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc", + "test": "ava", + "test:snapshot": "ava --update-snapshots" + }, + "dependencies": { + "@alifd/next": "^1.19.16", + "classnames": "^2.2.6", + "react": "^16", + "react-dom": "^16.7.0" + }, + "devDependencies": { + "@recore/config": "^2.0.0", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "@types/react-dom": "^16", + "eslint": "^6.5.1", + "prettier": "^1.18.2", + "tslib": "^1.9.3", + "typescript": "^3.1.3", + "ts-node": "^8.0.1" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, + "license": "MIT" +} diff --git a/packages/globals/src/components/index.ts b/packages/globals/src/components/index.ts new file mode 100644 index 000000000..f6eb9a1ff --- /dev/null +++ b/packages/globals/src/components/index.ts @@ -0,0 +1,2 @@ +export * from './tip'; +export * from './title'; diff --git a/packages/plugin-settings/src/tip/embed-tip.tsx b/packages/globals/src/components/tip/embed-tip.tsx similarity index 90% rename from packages/plugin-settings/src/tip/embed-tip.tsx rename to packages/globals/src/components/tip/embed-tip.tsx index fa68e8d08..fd8b3d4f8 100644 --- a/packages/plugin-settings/src/tip/embed-tip.tsx +++ b/packages/globals/src/components/tip/embed-tip.tsx @@ -1,4 +1,4 @@ -import { uniqueId } from '../../../utils/unique-id'; +import { uniqueId } from '../../../../utils/unique-id'; import { Component, ReactNode } from 'react'; import { saveTips } from './tip-handler'; diff --git a/packages/plugin-settings/src/tip/index.ts b/packages/globals/src/components/tip/index.ts similarity index 100% rename from packages/plugin-settings/src/tip/index.ts rename to packages/globals/src/components/tip/index.ts diff --git a/packages/plugin-settings/src/tip/style.less b/packages/globals/src/components/tip/style.less similarity index 100% rename from packages/plugin-settings/src/tip/style.less rename to packages/globals/src/components/tip/style.less diff --git a/packages/plugin-settings/src/tip/tip-container.tsx b/packages/globals/src/components/tip/tip-container.tsx similarity index 100% rename from packages/plugin-settings/src/tip/tip-container.tsx rename to packages/globals/src/components/tip/tip-container.tsx diff --git a/packages/plugin-settings/src/tip/tip-handler.ts b/packages/globals/src/components/tip/tip-handler.ts similarity index 100% rename from packages/plugin-settings/src/tip/tip-handler.ts rename to packages/globals/src/components/tip/tip-handler.ts diff --git a/packages/plugin-settings/src/tip/tip.tsx b/packages/globals/src/components/tip/tip.tsx similarity index 100% rename from packages/plugin-settings/src/tip/tip.tsx rename to packages/globals/src/components/tip/tip.tsx diff --git a/packages/plugin-settings/src/tip/utils.ts b/packages/globals/src/components/tip/utils.ts similarity index 100% rename from packages/plugin-settings/src/tip/utils.ts rename to packages/globals/src/components/tip/utils.ts diff --git a/packages/plugin-settings/src/title/index.tsx b/packages/globals/src/components/title/index.tsx similarity index 72% rename from packages/plugin-settings/src/title/index.tsx rename to packages/globals/src/components/title/index.tsx index 266a947de..226a5f15e 100644 --- a/packages/plugin-settings/src/title/index.tsx +++ b/packages/globals/src/components/title/index.tsx @@ -3,12 +3,7 @@ import { Icon } from '@alifd/next'; import classNames from 'classnames'; import EmbedTip, { TipConfig } from '../tip/embed-tip'; import './title.less'; - -export interface IconConfig { - type: string; - size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; - className?: string; -} +import { IconConfig, createIcon } from '../../utils'; export interface TitleConfig { label?: ReactNode; @@ -19,7 +14,7 @@ export interface TitleConfig { export type TitleContent = string | ReactElement | TitleConfig; -export default class Title extends Component<{ title: TitleContent; onClick?: () => void }> { +export class Title extends Component<{ title: TitleContent; onClick?: () => void }> { render() { let { title } = this.props; if (isValidElement(title)) { @@ -29,15 +24,7 @@ export default class Title extends Component<{ title: TitleContent; onClick?: () title = { label: title }; // tslint:disable-line } - let icon = null; - if (title.icon) { - if (isValidElement(title.icon)) { - icon = title.icon; - } else { - const iconProps = typeof title.icon === 'string' ? { type: title.icon } : title.icon; - icon = ; - } - } + const icon = title.icon ? createIcon(title.icon) : null; let tip: any = null; if (title.tip) { diff --git a/packages/plugin-settings/src/title/title.less b/packages/globals/src/components/title/title.less similarity index 100% rename from packages/plugin-settings/src/title/title.less rename to packages/globals/src/components/title/title.less diff --git a/packages/globals/src/icons/clone.tsx b/packages/globals/src/icons/clone.tsx new file mode 100644 index 000000000..57dd701db --- /dev/null +++ b/packages/globals/src/icons/clone.tsx @@ -0,0 +1,11 @@ +import { IconBase, IconBaseProps } from "./icon-base"; + +export function Clone(props: IconBaseProps) { + return ( + + + + ); +} + +Clone.displayName = 'Clone'; diff --git a/packages/globals/src/icons/hidden.tsx b/packages/globals/src/icons/hidden.tsx new file mode 100644 index 000000000..3abdc4448 --- /dev/null +++ b/packages/globals/src/icons/hidden.tsx @@ -0,0 +1,10 @@ +import { IconBase, IconBaseProps } from "./icon-base"; + +export function Hidden(props: IconBaseProps) { + return ( + + + + ); +} +Hidden.displayName = 'Hidden'; diff --git a/packages/globals/src/icons/icon-base.tsx b/packages/globals/src/icons/icon-base.tsx new file mode 100644 index 000000000..843c3d700 --- /dev/null +++ b/packages/globals/src/icons/icon-base.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +const SizePresets: any = { + xsmall: 8, + small: 12, + medium: 16, + large: 20, + xlarge: 30, +}; + +export interface IconBaseProps { + className?: string; + fill?: string; + size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | number; + viewBox: string; + children?: ReactNode; + style?: object; +}; + +export function IconBase({ fill, size = 'medium', viewBox, style, children, ...props }: IconBaseProps) { + if (SizePresets.hasOwnProperty(size)) { + size = SizePresets[size]; + } + + return ( + {children} + ); +} diff --git a/packages/globals/src/icons/index.ts b/packages/globals/src/icons/index.ts new file mode 100644 index 000000000..f843f3a52 --- /dev/null +++ b/packages/globals/src/icons/index.ts @@ -0,0 +1,5 @@ +export * from './clone'; +export * from './hidden'; +export * from './remove'; +export * from './settings'; +export * from './icon-base'; diff --git a/packages/globals/src/icons/remove.tsx b/packages/globals/src/icons/remove.tsx new file mode 100644 index 000000000..9f90088e7 --- /dev/null +++ b/packages/globals/src/icons/remove.tsx @@ -0,0 +1,10 @@ +import { IconBase, IconBaseProps } from './icon-base'; + +export function Remove(props: IconBaseProps) { + return ( + + + + ); +} +Remove.displayName = 'Remove'; diff --git a/packages/globals/src/icons/settings.tsx b/packages/globals/src/icons/settings.tsx new file mode 100644 index 000000000..05628a7c1 --- /dev/null +++ b/packages/globals/src/icons/settings.tsx @@ -0,0 +1,12 @@ +import { IconBase, IconBaseProps } from './icon-base'; + +export function Setting(props: IconBaseProps) { + return ( + + + + + ); +} + +Setting.displayName = 'Setting'; diff --git a/packages/globals/src/index.ts b/packages/globals/src/index.ts new file mode 100644 index 000000000..bbef70f75 --- /dev/null +++ b/packages/globals/src/index.ts @@ -0,0 +1,4 @@ +export * from './intl'; +export * from './components'; +export * from './utils'; +export * from './icons'; diff --git a/packages/globals/src/intl/ali-global-locale.ts b/packages/globals/src/intl/ali-global-locale.ts new file mode 100644 index 000000000..37d97a115 --- /dev/null +++ b/packages/globals/src/intl/ali-global-locale.ts @@ -0,0 +1,124 @@ +import { EventEmitter } from 'events'; +const languageMap: { [key: string]: string } = { + en: 'en-US', + zh: 'zh-CN', + zt: 'zh-TW', + es: 'es-ES', + pt: 'pt-PT', + fr: 'fr-FR', + de: 'de-DE', + it: 'it-IT', + ru: 'ru-RU', + ja: 'ja-JP', + ko: 'ko-KR', + ar: 'ar-SA', + tr: 'tr-TR', + th: 'th-TH', + vi: 'vi-VN', + nl: 'nl-NL', + he: 'iw-IL', + id: 'in-ID', + pl: 'pl-PL', + hi: 'hi-IN', + uk: 'uk-UA', + ms: 'ms-MY', + tl: 'tl-PH', +}; + +const LowcodeConfigKey = 'ali-lowcode-config'; + +class AliGlobalLocale { + private locale: string = ''; + private emitter = new EventEmitter(); + + constructor() { + this.emitter.setMaxListeners(0); + } + + setLocale(locale: string) { + this.locale = locale; + if (hasLocalStorage(window)) { + const store = window.localStorage; + let config: any; + try { + config = JSON.parse(store.getItem(LowcodeConfigKey) || ''); + } catch (e) { + // ignore; + } + + if (config && typeof config === 'object') { + config.locale = locale; + } else { + config = { locale }; + } + + store.setItem(LowcodeConfigKey, JSON.stringify(config)); + } + } + + getLocale() { + if (this.locale) { + return this.locale; + } + + const { g_config, navigator } = window as any; + if (hasLocalStorage(window)) { + const store = window.localStorage; + let config: any; + try { + config = JSON.parse(store.getItem(LowcodeConfigKey) || ''); + } catch (e) { + // ignore; + } + if (config?.locale) { + this.locale = (config.locale || '').replace('_', '-'); + return this.locale; + } + } else if (g_config) { + if (g_config.locale) { + this.locale = languageMap[g_config.locale] || (g_config.locale || '').replace('_', '-'); + return this.locale; + } + } + + if (navigator.language) { + this.locale = (navigator.language as string).replace('_', '-'); + } + + // IE10 及更低版本使用 browserLanguage + if (navigator.browserLanguage) { + const it = navigator.browserLanguage.split('-'); + this.locale = it[0]; + if (it[1]) { + this.locale += '-' + it[1].toUpperCase(); + } + } + + if (!this.locale) { + this.locale = 'zh-CN'; + } + + return this.locale; + } + + onLocaleChange(fn: (locale: string) => void): () => void { + this.emitter.on('localechange', fn); + return () => { + this.emitter.removeListener('localechange', fn); + }; + } +} + +function hasLocalStorage(obj: any): obj is WindowLocalStorage { + return obj.localStorage; +} + +let globalLocale: AliGlobalLocale; +if ((window as any).__aliGlobalLocale) { + globalLocale = (window as any).__aliGlobalLocale as any; +} else { + globalLocale = new AliGlobalLocale(); + (window as any).__aliGlobalLocale = globalLocale; +} + +export { globalLocale }; diff --git a/packages/globals/src/intl/index.tsx b/packages/globals/src/intl/index.tsx new file mode 100644 index 000000000..1f54012f7 --- /dev/null +++ b/packages/globals/src/intl/index.tsx @@ -0,0 +1,122 @@ +import { globalLocale } from './ali-global-locale'; +import { PureComponent, ReactNode } from 'react'; + +function injectVars(template: string, params: any): string { + if (!template || !params) { + return template; + } + return template.replace(/({\w+})/g, (_, $1) => { + const key = (/\d+/.exec($1) || [])[0] as any; + if (key && params[key] != null) { + return params[key]; + } + return $1; + }); +} + +export interface I18nData { + type: 'i18n'; + [key: string]: string; +} + +export function isI18nData(obj: any): obj is I18nData { + return obj && obj.type === 'i18n'; +} + +export function localeFormat(data: any, params?: object): string { + if (!isI18nData(data)) { + return data; + } + const locale = globalLocale.getLocale(); + const tpl = data[locale]; + if (tpl == null) { + return `##intl null@${locale}##`; + } + return injectVars(tpl, params); +} + +class Intl extends PureComponent<{ data: any; params?: object }> { + private dispose = globalLocale.onLocaleChange(() => this.forceUpdate()); + componentWillUnmount() { + this.dispose(); + } + render() { + const { data, params } = this.props; + return localeFormat(data, params); + } +} + +export function intl(data: any, params?: object): ReactNode { + if (isI18nData(data)) { + return ; + } + return data; +} + +export function createIntl( + instance: string | object, +): { + intl(id: string, params?: object): ReactNode; + getIntlString(id: string, params?: object): string; + getLocale(): string; + setLocale(locale: string): void; +} { + let lastLocale: string | undefined; + let data: any = {}; + function useLocale(locale: string) { + lastLocale = locale; + if (typeof instance === 'string') { + if ((window as any)[instance]) { + data = (window as any)[instance][locale] || {}; + } else { + const key = `${instance}_${locale.toLocaleLowerCase()}`; + data = (window as any)[key] || {}; + } + } else if (instance && typeof instance === 'object') { + data = (instance as any)[locale] || {}; + } + } + + useLocale(globalLocale.getLocale()); + + function getIntlString(key: string, params?: object): string { + const str = data[key]; + + if (str == null) { + return `##intl null@${key}##`; + } + + return injectVars(str, params); + } + + class Intl extends PureComponent<{ id: string; params?: object }> { + private dispose = globalLocale.onLocaleChange(locale => { + if (lastLocale !== locale) { + useLocale(locale); + this.forceUpdate(); + } + }); + componentWillUnmount() { + this.dispose(); + } + render() { + const { id, params } = this.props; + return getIntlString(id, params); + } + } + + return { + intl(id: string, params?: object) { + return ; + }, + getIntlString, + getLocale() { + return globalLocale.getLocale(); + }, + setLocale(locale: string) { + globalLocale.setLocale(locale); + }, + }; +} + +export { globalLocale }; diff --git a/packages/globals/src/utils/create-icon.tsx b/packages/globals/src/utils/create-icon.tsx new file mode 100644 index 000000000..728d2fcdd --- /dev/null +++ b/packages/globals/src/utils/create-icon.tsx @@ -0,0 +1,34 @@ +import { Icon } from '@alifd/next'; +import { isValidElement, ReactNode, ComponentType, createElement, cloneElement, ReactElement } from 'react'; +import { isReactComponent } from './is-react'; + +export interface IconConfig { + type: string; + size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; + className?: string; +} + +export type IconType = string | ReactElement | ComponentType | IconConfig; + +const URL_RE = /^(https?:)\/\//i; + +export function createIcon(icon: IconType, props?: object): ReactNode { + if (typeof icon === 'string') { + if (URL_RE.test(icon)) { + return ; + } + return ; + } + if (isValidElement(icon)) { + return cloneElement(icon, {...props}); + } + if (isReactComponent(icon)) { + return createElement(icon, {...props}); + } + + if (icon) { + return ; + } + + return null; +} diff --git a/packages/globals/src/utils/index.ts b/packages/globals/src/utils/index.ts new file mode 100644 index 000000000..365722a71 --- /dev/null +++ b/packages/globals/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './create-icon'; +export * from './is-react'; diff --git a/packages/globals/src/utils/is-react.ts b/packages/globals/src/utils/is-react.ts new file mode 100644 index 000000000..1f755138a --- /dev/null +++ b/packages/globals/src/utils/is-react.ts @@ -0,0 +1,9 @@ +import { ComponentClass, Component, ComponentType } from 'react'; + +export function isReactClass(obj: any): obj is ComponentClass { + return obj && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component); +} + +export function isReactComponent(obj: any): obj is ComponentType { + return obj && (isReactClass(obj) || typeof obj === 'function'); +} diff --git a/packages/globals/tsconfig.json b/packages/globals/tsconfig.json new file mode 100644 index 000000000..aad669598 --- /dev/null +++ b/packages/globals/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@recore/config/tsconfig", + "compilerOptions": { + "experimentalDecorators": true + }, + "include": [ + "./src/" + ] +} 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 96688d593..1e889ce30 100644 --- a/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx +++ b/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx @@ -5,7 +5,7 @@ import { SettingField, SetterType, FieldConfig, SetterConfig } from '../../main' import './style.less'; import { createSettingFieldView } from '../../settings-pane'; import { PopupContext, PopupPipe } from '../../popup'; -import Title from '../../title'; +import { Title } from '../../../../globals'; interface ArraySetterState { items: SettingField[]; diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx b/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx index 32dbf25e9..36da0db58 100644 --- a/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx +++ b/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx @@ -3,7 +3,7 @@ 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 { Title } from '../../../..//globals'; import './style.less'; export default class ObjectSetter extends Component<{ diff --git a/packages/plugin-settings/src/field/index.tsx b/packages/plugin-settings/src/field/index.tsx index a1e6e4bad..db80cf72c 100644 --- a/packages/plugin-settings/src/field/index.tsx +++ b/packages/plugin-settings/src/field/index.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; import classNames from 'classnames'; import { Icon } from '@alifd/next'; -import Title, { TitleContent } from '../title'; +import { Title, TitleContent } from '../../../globals'; import './index.less'; export interface FieldProps { diff --git a/packages/plugin-settings/src/index.tsx b/packages/plugin-settings/src/index.tsx index 36f6fc754..8ed11d15b 100644 --- a/packages/plugin-settings/src/index.tsx +++ b/packages/plugin-settings/src/index.tsx @@ -2,13 +2,12 @@ import React, { Component } from 'react'; import { Tab, Breadcrumb, Icon } from '@alifd/next'; import { SettingsMain, SettingField, isSettingField } from './main'; import './style.less'; -import Title from './title'; +import { Title, TipContainer } from '../../globals'; 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'; import './register-transducer'; -import { TipContainer } from './tip'; export default class SettingsMainView extends Component { private main: SettingsMain; diff --git a/packages/plugin-settings/src/main.ts b/packages/plugin-settings/src/main.ts index 7c07e91f2..2cd28bbfc 100644 --- a/packages/plugin-settings/src/main.ts +++ b/packages/plugin-settings/src/main.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import { uniqueId } from '../../utils/unique-id'; import { ComponentMeta } from '../../designer/src/designer/component-meta'; import Node from '../../designer/src/designer/document/node/node'; -import { TitleContent } from './title'; +import { TitleContent } from '../../globals'; import { ReactElement, ComponentType as ReactComponentType, isValidElement } from 'react'; import { isReactComponent } from '../../utils/is-react'; import Designer from '../../designer/src/designer/designer'; diff --git a/packages/plugin-settings/src/register-transducer.ts b/packages/plugin-settings/src/register-transducer.ts index 8229a62b3..5c1ba15d7 100644 --- a/packages/plugin-settings/src/register-transducer.ts +++ b/packages/plugin-settings/src/register-transducer.ts @@ -70,7 +70,7 @@ export function propTypeToSetter(propType: PropType): SetterType { }; case 'element': - case 'node': + case 'node': // TODO: use Mixin return { // slotSetter componentName: 'NodeSetter', @@ -156,22 +156,9 @@ export function propTypeToSetter(propType: PropType): SetterType { const EVENT_RE = /^on[A-Z][\w]*$/; +// parseProps registerMetadataTransducer(metadata => { - if (metadata.configure) { - if (Array.isArray(metadata.configure)) { - return { - ...metadata, - configure: { - props: metadata.configure, - }, - }; - } - if (metadata.configure.props) { - return metadata as any; - } - } - - const { configure = {} } = metadata; + const { configure } = metadata; if (!metadata.props) { return { @@ -237,46 +224,7 @@ registerMetadataTransducer(metadata => { }; }); -registerMetadataTransducer(metadata => { - const { configure = {}, componentName } = metadata; - const { component = {} } = configure as any; - if (!component.nestingRule) { - let m; - // uri match xx.Group set subcontrolling: true, childWhiteList - if ((m = /^(.+)\.Group$/.exec(componentName))) { - // component.subControlling = true; - if (!component.nestingRule) { - component.nestingRule = { - childWhitelist: [`${m[1]}`], - }; - } - } - // uri match xx.Node set selfControlled: false, parentWhiteList - else if ((m = /^(.+)\.Node$/.exec(componentName))) { - // component.selfControlled = false; - component.nestingRule = { - parentWhitelist: [`${m[1]}`, componentName], - }; - } - // uri match .Item .Node .Option set parentWhiteList - else if ((m = /^(.+)\.(Item|Node|Option)$/.exec(componentName))) { - component.nestingRule = { - parentWhitelist: [`${m[1]}`], - }; - } - } - if (component.isModal == null && /Dialog/.test(componentName)) { - component.isModal = true; - } - return { - ...metadata, - configure: { - ...configure, - component, - }, - }; -}); - +// addon/platform custom registerMetadataTransducer(metadata => { const { componentName, configure = {} } = metadata; if (componentName === 'Leaf') { diff --git a/packages/plugin-settings/src/settings-pane.tsx b/packages/plugin-settings/src/settings-pane.tsx index 8d01b8c14..710210597 100644 --- a/packages/plugin-settings/src/settings-pane.tsx +++ b/packages/plugin-settings/src/settings-pane.tsx @@ -11,8 +11,7 @@ import { DynamicProps, } from './main'; import { Field, FieldGroup } from './field'; -import { TitleContent } from './title'; -import { Balloon } from '@alifd/next'; +import { TitleContent } from '../../globals'; import PopupService from './popup'; export type RegisteredSetter = {